[fonts] Ephemeral FontProvider

- Read code_points field from manifest if possible
- Use FontResolver if assets are not already present
- FontService is more async due to asset::Collection.get_asset()
becoming async
- Add a mock FontResolver service for testing

Bug: 8904
Change-Id: I391f99edabeb023d63ef22155083f5d503d139b4
diff --git a/src/fonts/BUILD.gn b/src/fonts/BUILD.gn
index 224e155..a351c65 100644
--- a/src/fonts/BUILD.gn
+++ b/src/fonts/BUILD.gn
@@ -23,6 +23,7 @@
 
   deps = [
     "//garnet/lib/rust/fidl_fuchsia_fonts_ext",
+    "//garnet/lib/rust/io_util",
     "//garnet/public/lib/fidl/rust/fidl",
     "//garnet/public/rust/fdio",
     "//garnet/public/rust/fuchsia-async",
@@ -32,6 +33,8 @@
     "//sdk/fidl/fuchsia.fonts:fuchsia.fonts-rustc",
     "//sdk/fidl/fuchsia.fonts.experimental:fuchsia.fonts.experimental-rustc",
     "//sdk/fidl/fuchsia.intl:fuchsia.intl-rustc",
+    "//sdk/fidl/fuchsia.pkg:fuchsia.pkg-rustc",
+    "//src/sys/lib/fuchsia_url",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
     "//third_party/rust_crates:getopts",
@@ -45,6 +48,7 @@
     "//third_party/rust_crates:serde_derive",
     "//third_party/rust_crates:serde_json",
     "//third_party/rust_crates:unicase",
+    "//zircon/public/fidl/fuchsia-io:fuchsia-io-rustc",
     "//zircon/public/fidl/fuchsia-mem:fuchsia-mem-rustc",
   ]
 
@@ -99,12 +103,16 @@
     "//garnet/public/rust/fdio",
     "//garnet/public/rust/fuchsia-async",
     "//garnet/public/rust/fuchsia-component",
+    "//garnet/public/rust/fuchsia-syslog",
     "//garnet/public/rust/fuchsia-zircon",
     "//sdk/fidl/fuchsia.fonts:fuchsia.fonts-rustc",
     "//sdk/fidl/fuchsia.fonts.experimental:fuchsia.fonts.experimental-rustc",
     "//sdk/fidl/fuchsia.intl:fuchsia.intl-rustc",
+    "//sdk/fidl/fuchsia.pkg:fuchsia.pkg-rustc",
+    "//src/sys/lib/fuchsia_url",
     "//third_party/rust_crates:failure",
     "//third_party/rust_crates:futures-preview",
+    "//zircon/public/fidl/fuchsia-io:fuchsia-io-rustc",
   ]
   source_root = "tests/font_provider_test.rs"
 }
@@ -113,6 +121,20 @@
   deps = [
     ":font_provider_test_test",
     ":font_server_test",
+    ":mock_font_resolver_bin",
+  ]
+
+  meta = [
+    {
+      path = rebase_path("meta/mock_font_resolver.cmx")
+      dest = "mock_font_resolver.cmx"
+    },
+  ]
+
+  binaries = [
+    {
+      name = "mock_font_resolver"
+    },
   ]
 
   tests = [
@@ -135,6 +157,10 @@
       path = rebase_path("tests/all_fonts_manifest.json")
       dest = "testdata/test_fonts/all_fonts_manifest.json"
     },
+    {
+      path = rebase_path("tests/ephemeral_manifest.json")
+      dest = "testdata/test_fonts/ephemeral_manifest.json"
+    },
   ]
 
   # TODO(sergeyu): Noto CJK fonts are not included in the default fonts package
@@ -188,3 +214,24 @@
     ]
   }
 }
+
+rustc_binary("mock_font_resolver_bin") {
+  name = "mock_font_resolver"
+  edition = "2018"
+  source_root = "tests/mock_font_resolver.rs"
+  deps = [
+    "//garnet/public/lib/fidl/rust/fidl",
+    "//garnet/public/rust/fdio",
+    "//garnet/public/rust/fuchsia-async",
+    "//garnet/public/rust/fuchsia-component",
+    "//garnet/public/rust/fuchsia-syslog",
+    "//garnet/public/rust/fuchsia-vfs/pseudo-fs",
+    "//garnet/public/rust/fuchsia-zircon",
+    "//sdk/fidl/fuchsia.pkg:fuchsia.pkg-rustc",
+    "//src/sys/lib/fuchsia_url:fuchsia_url",
+    "//third_party/rust_crates:failure",
+    "//third_party/rust_crates:futures-preview",
+    "//third_party/rust_crates:lazy_static",
+    "//zircon/public/fidl/fuchsia-io:fuchsia-io-rustc",
+  ]
+}
diff --git a/src/fonts/meta/font_provider_test.cmx b/src/fonts/meta/font_provider_test.cmx
index 9337147..75e225c 100644
--- a/src/fonts/meta/font_provider_test.cmx
+++ b/src/fonts/meta/font_provider_test.cmx
@@ -2,6 +2,7 @@
     "facets": {
         "fuchsia.test": {
             "injected-services": {
+                "fuchsia.pkg.FontResolver": "fuchsia-pkg://fuchsia.com/font_provider_tests#meta/mock_font_resolver.cmx",
                 "fuchsia.tracing.provider.Registry": "fuchsia-pkg://fuchsia.com/trace_manager#meta/trace_manager.cmx"
             }
         }
@@ -11,6 +12,7 @@
     },
     "sandbox": {
         "services": [
+            "fuchsia.pkg.FontResolver",
             "fuchsia.sys.Launcher"
         ]
     }
diff --git a/src/fonts/meta/fonts.cmx b/src/fonts/meta/fonts.cmx
index 90e76cb..fff4d9d3 100644
--- a/src/fonts/meta/fonts.cmx
+++ b/src/fonts/meta/fonts.cmx
@@ -7,7 +7,8 @@
             "config-data"
         ],
         "services": [
-            "fuchsia.logger.LogSink"
+            "fuchsia.logger.LogSink",
+            "fuchsia.pkg.FontResolver"
         ]
     }
 }
diff --git a/src/fonts/meta/mock_font_resolver.cmx b/src/fonts/meta/mock_font_resolver.cmx
new file mode 100644
index 0000000..712d9b6
--- /dev/null
+++ b/src/fonts/meta/mock_font_resolver.cmx
@@ -0,0 +1,10 @@
+{
+    "program": {
+        "binary": "bin/mock_font_resolver"
+    },
+    "sandbox": {
+        "services": [
+            "fuchsia.logger.LogSink"
+        ]
+    }
+}
diff --git a/src/fonts/src/font_service/asset/collection.rs b/src/fonts/src/font_service/asset/collection.rs
index ad36b20..20f1ece 100644
--- a/src/fonts/src/font_service/asset/collection.rs
+++ b/src/fonts/src/font_service/asset/collection.rs
@@ -5,8 +5,14 @@
 use {
     super::{asset::Asset, cache::Cache},
     failure::{format_err, Error, ResultExt},
-    fidl_fuchsia_mem as mem,
-    parking_lot::RwLock,
+    fidl::endpoints::create_proxy,
+    fidl_fuchsia_io as io, fidl_fuchsia_mem as mem,
+    fidl_fuchsia_pkg::FontResolverMarker,
+    fuchsia_component::client::connect_to_service,
+    fuchsia_url::pkg_url::PkgUrl,
+    fuchsia_zircon as zx,
+    futures::lock::Mutex,
+    io_util,
     std::{
         collections::BTreeMap,
         fs::File,
@@ -36,9 +42,13 @@
     path_to_id_map: BTreeMap<PathBuf, u32>,
     /// Inverse of `path_to_id_map`.
     id_to_path_map: BTreeMap<u32, PathBuf>,
+    /// Maps asset paths to package URLs.
+    path_to_url_map: BTreeMap<PathBuf, PkgUrl>,
+    /// Maps asset paths to previously-resolved directory handles.
+    path_to_dir_map: Mutex<BTreeMap<PathBuf, io::DirectoryProxy>>,
     /// Next ID to assign, autoincremented from 0.
     next_id: u32,
-    cache: RwLock<Cache>,
+    cache: Mutex<Cache>,
 }
 
 const CACHE_SIZE_BYTES: u64 = 4_000_000;
@@ -48,48 +58,94 @@
         Collection {
             path_to_id_map: BTreeMap::new(),
             id_to_path_map: BTreeMap::new(),
+            path_to_url_map: BTreeMap::new(),
+            path_to_dir_map: Mutex::new(BTreeMap::new()),
             next_id: 0,
-            cache: RwLock::new(Cache::new(CACHE_SIZE_BYTES)),
+            cache: Mutex::new(Cache::new(CACHE_SIZE_BYTES)),
         }
     }
 
-    /// Add the [`Asset`] found at `path` to the collection and return its ID.
+    /// Add the [`Asset`] found at `path` to the collection, store its package URL if provided,
+    /// and return the asset's ID.
     /// If `path` is already in the collection, return the existing ID.
-    ///
-    /// TODO(seancuff): Switch to updating ID of existing entries. This would allow assets to be
-    /// updated without restarting the service (e.g. installing a newer version of a file). Clients
-    /// would need to check the ID of their currently-held asset against the response.
-    pub fn add_or_get_asset_id(&mut self, path: &Path) -> u32 {
+    pub fn add_or_get_asset_id(&mut self, path: &Path, package_url: Option<&PkgUrl>) -> u32 {
         if let Some(id) = self.path_to_id_map.get(&path.to_path_buf()) {
             return *id;
         }
         let id = self.next_id;
         self.id_to_path_map.insert(id, path.to_path_buf());
         self.path_to_id_map.insert(path.to_path_buf(), id);
+        if let Some(url) = package_url {
+            self.path_to_url_map.insert(path.to_path_buf(), url.clone());
+        }
         self.next_id += 1;
         id
     }
 
     /// Get a `Buffer` holding the `Vmo` for the [`Asset`] corresponding to `id`, using the cache
     /// if possible.
-    pub fn get_asset(&self, id: u32) -> Result<mem::Buffer, Error> {
+    pub async fn get_asset(&self, id: u32) -> Result<mem::Buffer, Error> {
         if let Some(path) = self.id_to_path_map.get(&id) {
-            let mut cache_writer = self.cache.write();
-            let buf = match cache_writer.get(id) {
+            let mut cache_lock = self.cache.lock().await;
+            let buf = match cache_lock.get(id) {
                 Some(cached) => cached.buffer,
                 None => {
-                    cache_writer
-                        .push(Asset {
-                            id,
-                            buffer: load_asset_to_vmo(path).with_context(|_| {
-                                format!("Failed to load {}", path.to_string_lossy())
-                            })?,
-                        })
-                        .buffer
+                    let buffer = if path.exists() {
+                        load_asset_to_vmo(path).with_context(|_| {
+                            format!("Failed to load {}.", path.to_string_lossy())
+                        })?
+                    } else {
+                        self.get_ephemeral_asset(path).await?
+                    };
+
+                    cache_lock.push(Asset { id, buffer }).buffer
                 }
             };
             return Ok(buf);
         }
         Err(format_err!("No asset found with id {}", id))
     }
+
+    async fn get_ephemeral_asset(&self, path_buf: &PathBuf) -> Result<mem::Buffer, Error> {
+        let filename = path_buf.as_path().file_name().ok_or(format_err!(
+            "Path '{}' does not contain a valid filename.",
+            path_buf.to_string_lossy()
+        ))?;
+
+        // Get cached directory if it is cached
+        let mut cache_lock = self.path_to_dir_map.lock().await;
+
+        let directory_proxy = match cache_lock.get(path_buf) {
+            Some(dir_proxy) => dir_proxy,
+            None => {
+                let url = self.path_to_url_map.get(path_buf).ok_or(format_err!(
+                    "No asset found with path {}",
+                    path_buf.to_string_lossy()
+                ))?;
+
+                // Get directory handle from FontResolver
+                let font_resolver = connect_to_service::<FontResolverMarker>()?;
+                let (dir_proxy, dir_request) = create_proxy::<io::DirectoryMarker>()?;
+
+                let status = font_resolver.resolve(&url.to_string(), dir_request).await?;
+                zx::Status::ok(status)?;
+
+                // Cache directory handle
+                cache_lock.insert(path_buf.to_path_buf(), dir_proxy);
+                cache_lock.get(path_buf).unwrap() // Safe because just inserted
+            }
+        };
+
+        let file_proxy =
+            io_util::open_file(directory_proxy, Path::new(&filename), io::OPEN_RIGHT_READABLE)?;
+
+        drop(cache_lock);
+
+        let (status, buffer) = file_proxy.get_buffer(io::VMO_FLAG_READ).await?;
+        zx::Status::ok(status)?;
+
+        let buffer = *buffer
+            .ok_or(format_err!("Failed to get buffer for {}.", filename.to_string_lossy()))?;
+        Ok(buffer)
+    }
 }
diff --git a/src/fonts/src/font_service/font_info/char_set.rs b/src/fonts/src/font_service/font_info/char_set.rs
index 86626f0..7245439 100644
--- a/src/fonts/src/font_service/font_info/char_set.rs
+++ b/src/fonts/src/font_service/font_info/char_set.rs
@@ -2,7 +2,10 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-use std::cmp::Ordering;
+use {
+    failure::{format_err, Error},
+    std::cmp::Ordering,
+};
 
 type BitmapElement = u64;
 const BITMAP_ELEMENT_SIZE: usize = 64;
@@ -88,6 +91,37 @@
         CharSet { ranges }
     }
 
+    pub fn from_string(s: String) -> Result<CharSet, Error> {
+        let mut code_points: Vec<u32> = vec![];
+        let mut prev: u32 = 0;
+        for range in s.split(',').filter(|x| !x.is_empty()) {
+            let mut split = range.split('+');
+
+            let offset: u32 = match split.next() {
+                Some(off) => off
+                    .parse()
+                    .or_else(|_| Err(format_err!("Failed to parse {:?} as u32.", off)))?,
+                None => return Err(format_err!("Failed to parse {:?}: not a valid range.", range)),
+            };
+
+            let length: u32 = match split.next() {
+                Some(len) => len
+                    .parse()
+                    .or_else(|_| Err(format_err!("Failed to parse {:?} as u32.", len)))?,
+                None => 0, // We can treat "0,2,..." as "0+0,2+0,..."
+            };
+
+            if split.next().is_some() {
+                return Err(format_err!("Failed to parse {:?}: not a valid range.", range));
+            }
+
+            let begin = prev + offset;
+            prev = begin + length;
+            code_points.extend(begin..=prev);
+        }
+        Ok(CharSet::new(code_points))
+    }
+
     pub fn contains(&self, c: u32) -> bool {
         match self.ranges.binary_search_by(|r| {
             if r.end() < c {
@@ -102,6 +136,16 @@
             Err(_) => false,
         }
     }
+
+    pub fn is_empty(&self) -> bool {
+        self.ranges.is_empty()
+    }
+}
+
+impl Default for CharSet {
+    fn default() -> Self {
+        CharSet::new(vec![])
+    }
 }
 
 #[cfg(test)]
@@ -127,4 +171,20 @@
         assert!(charset.contains(10000));
         assert!(!charset.contains(10001));
     }
+
+    #[test]
+    fn test_charset_from_string() -> Result<(), Error> {
+        let s = "0,2,11,19+94".to_string();
+        let charset = CharSet::from_string(s)?;
+        assert!([0, 2, 13, 32, 54, 126].into_iter().all(|c| charset.contains(*c)));
+        assert!([1, 11, 19, 127, 10000].into_iter().all(|c| !charset.contains(*c)));
+        Ok(())
+    }
+
+    #[test]
+    fn test_charset_from_string_not_a_number() {
+        for s in &["0,p,11", "q+1", "3+r"] {
+            assert!(CharSet::from_string(s.to_string()).is_err())
+        }
+    }
 }
diff --git a/src/fonts/src/font_service/manifest.rs b/src/fonts/src/font_service/manifest.rs
index 54fa72a..c3b7881 100644
--- a/src/fonts/src/font_service/manifest.rs
+++ b/src/fonts/src/font_service/manifest.rs
@@ -3,8 +3,10 @@
 // found in the LICENSE file.
 
 use {
+    crate::font_service::font_info::CharSet,
     failure::{self, format_err, ResultExt},
     fidl_fuchsia_fonts::{GenericFontFamily, Slant, Width, WEIGHT_NORMAL},
+    fuchsia_url::pkg_url::PkgUrl,
     lazy_static::lazy_static,
     regex::Regex,
     serde::de::{self, Deserialize, Deserializer, Error},
@@ -68,6 +70,12 @@
         deserialize_with = "deserialize_languages"
     )]
     pub languages: LanguageSet,
+
+    #[serde(default = "default_package", deserialize_with = "deserialize_package")]
+    pub package: Option<PkgUrl>,
+
+    #[serde(default, deserialize_with = "deserialize_code_points")]
+    pub code_points: CharSet,
 }
 
 fn default_fallback() -> bool {
@@ -98,6 +106,10 @@
     LanguageSet::new()
 }
 
+fn default_package() -> Option<PkgUrl> {
+    None
+}
+
 lazy_static! {
     static ref SEPARATOR_REGEX: Regex = Regex::new(r"[_ ]").unwrap();
 }
@@ -204,6 +216,21 @@
     deserializer.deserialize_any(LanguageSetVisitor)
 }
 
+fn deserialize_package<'d, D>(deserializer: D) -> Result<Option<PkgUrl>, D::Error>
+where
+    D: Deserializer<'d>,
+{
+    Some(PkgUrl::deserialize(deserializer)).transpose()
+}
+
+fn deserialize_code_points<'d, D>(deserializer: D) -> Result<CharSet, D::Error>
+where
+    D: Deserializer<'d>,
+{
+    let s = String::deserialize(deserializer)?;
+    CharSet::from_string(s).map_err(|e| D::Error::custom(format!("{:?}", e)))
+}
+
 impl FontsManifest {
     pub fn load_from_file(path: &Path) -> Result<FontsManifest, failure::Error> {
         let path = fs::canonicalize(path)?;
diff --git a/src/fonts/src/font_service/mod.rs b/src/fonts/src/font_service/mod.rs
index 92671d2..ad8295b 100644
--- a/src/fonts/src/font_service/mod.rs
+++ b/src/fonts/src/font_service/mod.rs
@@ -29,7 +29,6 @@
     fuchsia_component::server::{ServiceFs, ServiceObj},
     futures::prelude::*,
     itertools::Itertools,
-    log,
     std::{collections::BTreeMap, iter, path::Path, sync::Arc},
     unicase::UniCase,
 };
@@ -72,17 +71,17 @@
         Ok(())
     }
 
-    pub fn load_manifest(&mut self, manifest_path: &Path) -> Result<(), Error> {
+    pub async fn load_manifest(&mut self, manifest_path: &Path) -> Result<(), Error> {
         fx_vlog!(1, "Loading manifest {:?}", manifest_path);
         let manifest = FontsManifest::load_from_file(&manifest_path)?;
-        self.add_fonts_from_manifest(manifest).with_context(|_| {
+        self.add_fonts_from_manifest(manifest).await.with_context(|_| {
             format!("Failed to load fonts from {}", manifest_path.to_string_lossy())
         })?;
 
         Ok(())
     }
 
-    fn add_fonts_from_manifest(&mut self, mut manifest: FontsManifest) -> Result<(), Error> {
+    async fn add_fonts_from_manifest(&mut self, mut manifest: FontsManifest) -> Result<(), Error> {
         let font_info_loader = FontInfoLoader::new()?;
 
         for mut family_manifest in manifest.families.drain(..) {
@@ -116,27 +115,42 @@
                 }
             };
 
-            for font_manifest in family_manifest.fonts.drain(..) {
-                let asset_id = self.assets.add_or_get_asset_id(font_manifest.asset.as_path());
+            for mut font_manifest in family_manifest.fonts.drain(..) {
+                let asset_path = font_manifest.asset.as_path();
+                let asset_id =
+                    self.assets.add_or_get_asset_id(asset_path, font_manifest.package.as_ref());
 
-                let buffer = self.assets.get_asset(asset_id).with_context(|_| {
-                    format!("Failed to load font from {}", font_manifest.asset.to_string_lossy())
-                })?;
+                // Read `code_points` from file if not provided by manifest.
+                if font_manifest.code_points.is_empty() {
+                    if !asset_path.exists() {
+                        return Err(format_err!(
+                            "Unable to load code point info for '{}'. Manifest entry has no \
+                             code_points field and the file does not exist.",
+                            asset_path.to_string_lossy(),
+                        ));
+                    }
 
-                let info = font_info_loader
-                    .load_font_info(buffer.vmo, buffer.size as usize, font_manifest.index)
-                    .with_context(|_| {
-                        format!(
-                            "Failed to load font info from {}",
-                            font_manifest.asset.to_string_lossy()
-                        )
+                    let buffer = self.assets.get_asset(asset_id).await.with_context(|_| {
+                        format!("Failed to load font from {}", asset_path.to_string_lossy())
                     })?;
+
+                    let info = font_info_loader
+                        .load_font_info(buffer.vmo, buffer.size as usize, font_manifest.index)
+                        .with_context(|_| {
+                            format!(
+                                "Failed to load font info from {}",
+                                asset_path.to_string_lossy()
+                            )
+                        })?;
+
+                    font_manifest.code_points = info.char_set;
+                }
+
                 let typeface = Arc::new(Typeface::new(
                     asset_id,
                     font_manifest,
-                    info.char_set,
                     family_manifest.generic_family,
-                ));
+                )?);
                 family.faces.add_typeface(typeface.clone());
                 if family_manifest.fallback {
                     self.fallback_collection.add_typeface(typeface);
@@ -207,7 +221,7 @@
             .unique_by(|family| &family.name)
     }
 
-    fn match_request(
+    async fn match_request(
         &self,
         mut request: fonts::TypefaceRequest,
     ) -> Result<fonts::TypefaceResponse, Error> {
@@ -238,20 +252,21 @@
             typeface = self.fallback_collection.match_request(&request)?;
         }
 
-        let typeface_response = typeface
-            .ok_or("Couldn't match a typeface")
-            .and_then(|font| match self.assets.get_asset(font.asset_id) {
-                Ok(buffer) => Result::Ok(fonts::TypefaceResponse {
-                    buffer: Some(buffer),
-                    buffer_id: Some(font.asset_id),
-                    font_index: Some(font.font_index),
-                }),
-                Err(err) => {
-                    log::error!("Failed to load font file: {}", err);
-                    Err("Failed to load font file")
-                }
-            })
-            .unwrap_or_else(|_| fonts::TypefaceResponse::new_empty());
+        let typeface_response = match typeface {
+            Some(font) => self
+                .assets
+                .get_asset(font.asset_id)
+                .await
+                .and_then(|buffer| {
+                    Ok(fonts::TypefaceResponse {
+                        buffer: Some(buffer),
+                        buffer_id: Some(font.asset_id),
+                        font_index: Some(font.font_index),
+                    })
+                })
+                .unwrap_or_else(|_| fonts::TypefaceResponse::new_empty()),
+            None => fonts::TypefaceResponse::new_empty(),
+        };
 
         // Note that not finding a typeface is not an error, as long as the query was legal.
         Ok(typeface_response)
@@ -269,8 +284,11 @@
         )
     }
 
-    fn get_typeface_by_id(&self, id: u32) -> Result<fonts::TypefaceResponse, fonts_exp::Error> {
-        match self.assets.get_asset(id) {
+    async fn get_typeface_by_id(
+        &self,
+        id: u32,
+    ) -> Result<fonts::TypefaceResponse, fonts_exp::Error> {
+        match self.assets.get_asset(id).await {
             Ok(buffer) => {
                 let response = fonts::TypefaceResponse {
                     buffer: Some(buffer),
@@ -451,7 +469,7 @@
             // TODO(I18N-12): Remove when all clients have migrated to GetTypeface
             GetFont { request, responder } => {
                 let request = request.into_typeface_request();
-                let mut response = self.match_request(request)?.into_font_response();
+                let mut response = self.match_request(request).await?.into_font_response();
                 Ok(responder.send(response.as_mut().map(OutOfLine))?)
             }
             // TODO(I18N-12): Remove when all clients have migrated to GetFontFamilyInfo
@@ -461,7 +479,7 @@
                 Ok(responder.send(font_info.as_mut().map(OutOfLine))?)
             }
             GetTypeface { request, responder } => {
-                let response = self.match_request(request)?;
+                let response = self.match_request(request).await?;
                 // TODO(kpozin): OutOfLine?
                 Ok(responder.send(response)?)
             }
@@ -481,7 +499,7 @@
 
         match request {
             GetTypefaceById { id, responder } => {
-                let mut response = self.get_typeface_by_id(id);
+                let mut response = self.get_typeface_by_id(id).await;
                 Ok(responder.send(&mut response)?)
             }
             GetTypefacesByFamily { family, responder } => {
diff --git a/src/fonts/src/font_service/typeface/collection.rs b/src/fonts/src/font_service/typeface/collection.rs
index 3e36988..9e32b04 100644
--- a/src/fonts/src/font_service/typeface/collection.rs
+++ b/src/fonts/src/font_service/typeface/collection.rs
@@ -153,6 +153,8 @@
         char_set: &[u32],
         generic_family: Option<GenericFontFamily>,
     ) -> Typeface {
+        // Prevent error if char_set is empty
+        let char_set = if char_set.is_empty() { &[0] } else { char_set };
         Typeface::new(
             0,
             manifest::Font {
@@ -162,10 +164,12 @@
                 weight,
                 width,
                 languages: languages.iter().map(|s| s.to_string()).collect(),
+                code_points: CharSet::new(char_set.to_vec()),
+                package: None,
             },
-            CharSet::new(char_set.to_vec()),
             generic_family,
         )
+        .unwrap() // Safe because char_set is not empty
     }
 
     fn request_typeface<'a, 'b>(
diff --git a/src/fonts/src/font_service/typeface/typeface.rs b/src/fonts/src/font_service/typeface/typeface.rs
index c4afa81..b5ce9ae 100644
--- a/src/fonts/src/font_service/typeface/typeface.rs
+++ b/src/fonts/src/font_service/typeface/typeface.rs
@@ -4,6 +4,7 @@
 
 use {
     crate::font_service::{font_info::CharSet, manifest::Font},
+    failure::{format_err, Error},
     fidl_fuchsia_fonts::{FamilyName, GenericFontFamily, Slant, Style2, TypefaceRequest, Width},
     fidl_fuchsia_fonts_experimental::TypefaceInfo,
     fidl_fuchsia_intl::LocaleId,
@@ -23,22 +24,26 @@
 }
 
 impl Typeface {
+    /// Create a new `Typeface`, copying all fields except `asset_id` and `generic_family` from
+    /// `manifest_font`.
     pub fn new(
         asset_id: u32,
         manifest_font: Font,
-        char_set: CharSet,
         generic_family: Option<GenericFontFamily>,
-    ) -> Typeface {
-        Typeface {
+    ) -> Result<Typeface, Error> {
+        if manifest_font.code_points.is_empty() {
+            return Err(format_err!("Can't create Typeface from Font with empty CharSet."));
+        }
+        Ok(Typeface {
             asset_id,
             font_index: manifest_font.index,
             weight: manifest_font.weight,
             width: manifest_font.width,
             slant: manifest_font.slant,
             languages: manifest_font.languages.iter().map(|x| x.to_string()).collect(),
-            char_set,
+            char_set: manifest_font.code_points,
             generic_family,
-        }
+        })
     }
 
     /// Returns value in the range `[0, 2 * request_languages.len()]`. The language code is used for
@@ -140,3 +145,28 @@
         }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use {
+        super::*,
+        fidl_fuchsia_fonts::{Slant, Width, WEIGHT_NORMAL},
+        std::path::PathBuf,
+    };
+
+    #[test]
+    fn test_typeface_new_empty_char_set_is_error() {
+        let font = Font {
+            asset: PathBuf::default(),
+            index: 0,
+            slant: Slant::Upright,
+            weight: WEIGHT_NORMAL,
+            width: Width::Normal,
+            languages: vec![],
+            package: None,
+            code_points: CharSet::new(vec![]),
+        };
+
+        assert!(Typeface::new(0, font, None).is_err())
+    }
+}
diff --git a/src/fonts/src/main.rs b/src/fonts/src/main.rs
index 239a37a..4d2b701 100644
--- a/src/fonts/src/main.rs
+++ b/src/fonts/src/main.rs
@@ -42,18 +42,20 @@
     let mut service = FontService::new();
 
     if !options.opt_present("n") {
-        service.load_manifest(&PathBuf::from(FONT_MANIFEST_PATH))?;
+        let font_manifest_path = PathBuf::from(FONT_MANIFEST_PATH);
+        service.load_manifest(&font_manifest_path).await?;
 
         let font_manifest_path = PathBuf::from(VENDOR_FONT_MANIFEST_PATH);
         if font_manifest_path.exists() {
-            service.load_manifest(&font_manifest_path)?;
+            service.load_manifest(&font_manifest_path).await?;
         }
     } else {
         fx_vlog!(1, "no-default-fonts set, not loading fonts from default location");
     }
 
     for m in options.opt_strs("m") {
-        service.load_manifest(&PathBuf::from(m.as_str()))?;
+        let path_buf = PathBuf::from(m.as_str());
+        service.load_manifest(&path_buf).await?;
     }
 
     service.check_can_start()?;
diff --git a/src/fonts/tests/ephemeral_manifest.json b/src/fonts/tests/ephemeral_manifest.json
new file mode 100644
index 0000000..115c5ca
--- /dev/null
+++ b/src/fonts/tests/ephemeral_manifest.json
@@ -0,0 +1,15 @@
+{
+  "families": [
+    {
+      "family": "Ephemeral",
+      "fallback": true,
+      "fonts": [
+        {
+          "asset": "Ephemeral.ttf",
+          "package": "fuchsia-pkg://fuchsia.com/font_package_ephemeral_ttf",
+          "code_points": "0,1,48+9,38,2+25"
+        }
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/fonts/tests/font_provider_test.rs b/src/fonts/tests/font_provider_test.rs
index 98e6022..af5619a 100644
--- a/src/fonts/tests/font_provider_test.rs
+++ b/src/fonts/tests/font_provider_test.rs
@@ -3,9 +3,6 @@
 // found in the LICENSE file.
 
 #![feature(async_await)]
-// This is only needed because GN's invocation of the Rust compiler doesn't recognize the test_
-// methods as entry points, so it complains about the helper methods being "dead code".
-#![cfg(test)]
 
 const FONTS_CMX: &str = "fuchsia-pkg://fuchsia.com/fonts#meta/fonts.cmx";
 
@@ -543,6 +540,71 @@
 
         Ok(())
     }
+
+    #[cfg(test)]
+    mod ephemeral {
+        use super::*;
+
+        fn start_provider_with_ephemeral_fonts() -> Result<(App, fonts::ProviderProxy), Error> {
+            let mut launch_options = LaunchOptions::new();
+            launch_options.add_dir_to_namespace(
+                "/test_fonts".to_string(),
+                std::fs::File::open("/pkg/data/testdata/test_fonts")?,
+            )?;
+
+            let launcher = launcher().context("Failed to open launcher service")?;
+            let app = launch_with_options(
+                &launcher,
+                FONTS_CMX.to_string(),
+                Some(vec![
+                    "--no-default-fonts".to_string(),
+                    "--font-manifest".to_string(),
+                    "/test_fonts/ephemeral_manifest.json".to_string(),
+                ]),
+                launch_options,
+            )
+            .context("Failed to launch fonts::Provider")?;
+            let font_provider = app
+                .connect_to_service::<fonts::ProviderMarker>()
+                .context("Failed to connect to fonts::Provider")?;
+
+            Ok((app, font_provider))
+        }
+
+        #[fasync::run_singlethreaded(test)]
+        async fn test_ephemeral_get_font_family_info() -> Result<(), Error> {
+            let (_app, font_provider) = start_provider_with_ephemeral_fonts()?;
+
+            let mut family = fonts::FamilyName { name: "Ephemeral".to_string() };
+
+            let response = font_provider.get_font_family_info(&mut family).await?;
+
+            assert_eq!(response.name, Some(family));
+            Ok(())
+        }
+
+        #[fasync::run_singlethreaded(test)]
+        async fn test_ephemeral_get_typeface() -> Result<(), Error> {
+            let (_app, font_provider) = start_provider_with_ephemeral_fonts()?;
+
+            let family = Some(fonts::FamilyName { name: "Ephemeral".to_string() });
+            let query = Some(fonts::TypefaceQuery {
+                family,
+                style: None,
+                code_points: None,
+                languages: None,
+                fallback_family: None,
+            });
+            let request = fonts::TypefaceRequest { query, flags: None };
+
+            let response = font_provider.get_typeface(request).await?;
+
+            assert!(response.buffer.is_some(), "{:?}", response);
+            assert_eq!(response.buffer_id.unwrap(), 0, "{:?}", response);
+            assert_eq!(response.font_index.unwrap(), 0, "{:?}", response);
+            Ok(())
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/src/fonts/tests/mock_font_resolver.rs b/src/fonts/tests/mock_font_resolver.rs
new file mode 100644
index 0000000..3c455fb
--- /dev/null
+++ b/src/fonts/tests/mock_font_resolver.rs
@@ -0,0 +1,73 @@
+// Copyright 2019 The Fuchsia Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#![feature(async_await)]
+
+use {
+    failure::Error,
+    fidl::endpoints::ServerEnd,
+    fidl_fuchsia_io::{
+        DirectoryMarker, MODE_TYPE_DIRECTORY, OPEN_FLAG_DIRECTORY, OPEN_RIGHT_READABLE,
+    },
+    fidl_fuchsia_pkg::{FontResolverRequest, FontResolverRequestStream},
+    fuchsia_async as fasync,
+    fuchsia_component::server::ServiceFs,
+    fuchsia_syslog::{self as syslog, fx_vlog, macros::*},
+    fuchsia_url::pkg_url::PkgUrl,
+    fuchsia_vfs_pseudo_fs::{
+        directory::entry::DirectoryEntry, file::simple::read_only, pseudo_directory,
+    },
+    fuchsia_zircon::Status,
+    futures::{StreamExt, TryStreamExt},
+};
+
+#[fasync::run_singlethreaded]
+async fn main() -> Result<(), Error> {
+    syslog::init_with_tags(&["mock_font_resolver"])?;
+    fx_log_info!("Starting mock FontResolver service.");
+
+    let mut fs = ServiceFs::new_local();
+    fs.dir("svc").add_fidl_service(move |stream| {
+        fasync::spawn_local(async move {
+            run_resolver_service(stream).await.expect("Failed to run mock FontResolver.")
+        });
+    });
+    fs.take_and_serve_directory_handle()?;
+    fs.collect::<()>().await;
+    Ok(())
+}
+
+async fn run_resolver_service(mut stream: FontResolverRequestStream) -> Result<(), Error> {
+    while let Some(request) = stream.try_next().await? {
+        fx_vlog!(1, "FontResolver got request {:?}", request);
+        let FontResolverRequest::Resolve { package_url, directory_request, responder } = request;
+        let status = resolve(package_url, directory_request).await;
+        responder.send(Status::from(status).into_raw())?;
+    }
+    Ok(())
+}
+
+async fn resolve(
+    package_url: String,
+    directory_request: ServerEnd<DirectoryMarker>,
+) -> Result<(), Status> {
+    PkgUrl::parse(&package_url).map_err(|_| Err(Status::INVALID_ARGS))?;
+
+    let mut root = pseudo_directory! {
+        "Ephemeral.ttf" => read_only(|| Ok(b"not actually a font".to_vec())),
+    };
+
+    let flags = OPEN_RIGHT_READABLE | OPEN_FLAG_DIRECTORY;
+    let mode = MODE_TYPE_DIRECTORY;
+    let mut path = std::iter::empty();
+    let node = ServerEnd::from(directory_request.into_channel());
+
+    root.open(flags, mode, &mut path, node);
+
+    fasync::spawn(async move {
+        root.await;
+    });
+
+    Ok(())
+}