package_directory/
lib.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![allow(clippy::let_unit_value)]
6
7use fidl::endpoints::ServerEnd;
8use fidl_fuchsia_io as fio;
9use std::collections::HashSet;
10use std::convert::TryInto as _;
11use std::future::Future;
12use vfs::common::send_on_open_with_error;
13use vfs::directory::entry::EntryInfo;
14use vfs::directory::entry_container::Directory;
15use vfs::{ObjectRequest, ObjectRequestRef};
16
17mod meta_as_dir;
18mod meta_subdir;
19mod non_meta_subdir;
20mod root_dir;
21mod root_dir_cache;
22
23pub use root_dir::{PathError, ReadFileError, RootDir, SubpackagesError};
24pub use root_dir_cache::RootDirCache;
25pub use vfs::execution_scope::ExecutionScope;
26
27pub(crate) const DIRECTORY_ABILITIES: fio::Abilities =
28    fio::Abilities::GET_ATTRIBUTES.union(fio::Abilities::ENUMERATE).union(fio::Abilities::TRAVERSE);
29
30pub(crate) const ALLOWED_FLAGS: fio::Flags = fio::Flags::empty()
31    .union(fio::MASK_KNOWN_PROTOCOLS)
32    .union(fio::PERM_READABLE)
33    .union(fio::PERM_EXECUTABLE)
34    .union(fio::Flags::PERM_INHERIT_EXECUTE)
35    .union(fio::Flags::FLAG_SEND_REPRESENTATION);
36
37#[derive(thiserror::Error, Debug)]
38pub enum Error {
39    #[error("the meta.far was not found")]
40    MissingMetaFar,
41
42    #[error("while opening the meta.far")]
43    OpenMetaFar(#[source] NonMetaStorageError),
44
45    #[error("while instantiating a fuchsia archive reader")]
46    ArchiveReader(#[source] fuchsia_archive::Error),
47
48    #[error("meta.far has a path that is not valid utf-8: {path:?}")]
49    NonUtf8MetaEntry {
50        #[source]
51        source: std::str::Utf8Error,
52        path: Vec<u8>,
53    },
54
55    #[error("while reading meta/contents")]
56    ReadMetaContents(#[source] fuchsia_archive::Error),
57
58    #[error("while deserializing meta/contents")]
59    DeserializeMetaContents(#[source] fuchsia_pkg::MetaContentsError),
60
61    #[error("collision between a file and a directory at path: '{:?}'", path)]
62    FileDirectoryCollision { path: String },
63
64    #[error("the supplied RootDir already has a dropper set")]
65    DropperAlreadySet,
66}
67
68impl From<&Error> for zx::Status {
69    fn from(e: &Error) -> Self {
70        use Error::*;
71        match e {
72            MissingMetaFar => zx::Status::NOT_FOUND,
73            OpenMetaFar(e) => e.into(),
74            DropperAlreadySet => zx::Status::INTERNAL,
75            ArchiveReader(fuchsia_archive::Error::Read(_)) => zx::Status::IO,
76            ArchiveReader(_) | ReadMetaContents(_) | DeserializeMetaContents(_) => {
77                zx::Status::INVALID_ARGS
78            }
79            FileDirectoryCollision { .. } | NonUtf8MetaEntry { .. } => zx::Status::INVALID_ARGS,
80        }
81    }
82}
83
84#[derive(thiserror::Error, Debug)]
85pub enum NonMetaStorageError {
86    #[error("while reading blob")]
87    ReadBlob(#[source] fuchsia_fs::file::ReadError),
88
89    #[error("while opening blob")]
90    OpenBlob(#[source] fuchsia_fs::node::OpenError),
91
92    #[error("while making FIDL call")]
93    Fidl(#[source] fidl::Error),
94
95    #[error("while calling GetBackingMemory")]
96    GetVmo(#[source] zx::Status),
97}
98
99impl NonMetaStorageError {
100    pub fn is_not_found_error(&self) -> bool {
101        match self {
102            NonMetaStorageError::ReadBlob(e) => e.is_not_found_error(),
103            NonMetaStorageError::OpenBlob(e) => e.is_not_found_error(),
104            NonMetaStorageError::GetVmo(status) => *status == zx::Status::NOT_FOUND,
105            _ => false,
106        }
107    }
108}
109
110impl From<&NonMetaStorageError> for zx::Status {
111    fn from(e: &NonMetaStorageError) -> Self {
112        if e.is_not_found_error() { zx::Status::NOT_FOUND } else { zx::Status::INTERNAL }
113    }
114}
115
116/// The storage that provides the non-meta files (accessed by hash) of a package-directory (e.g.
117/// blobfs).
118pub trait NonMetaStorage: Send + Sync + Sized + 'static {
119    /// Open a non-meta file by hash. `scope` may complete while there are still open connections.
120    fn open(
121        &self,
122        _blob: &fuchsia_hash::Hash,
123        _flags: fio::Flags,
124        _scope: ExecutionScope,
125        _object_request: ObjectRequestRef<'_>,
126    ) -> Result<(), zx::Status>;
127
128    /// Get a read-only VMO for the blob.
129    fn get_blob_vmo(
130        &self,
131        hash: &fuchsia_hash::Hash,
132    ) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
133
134    /// Reads the contents of a blob.
135    fn read_blob(
136        &self,
137        hash: &fuchsia_hash::Hash,
138    ) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
139}
140
141impl NonMetaStorage for blobfs::Client {
142    fn open(
143        &self,
144        blob: &fuchsia_hash::Hash,
145        flags: fio::Flags,
146        scope: ExecutionScope,
147        object_request: ObjectRequestRef<'_>,
148    ) -> Result<(), zx::Status> {
149        self.open_blob_for_read(blob, flags, scope, object_request)
150    }
151
152    async fn get_blob_vmo(
153        &self,
154        hash: &fuchsia_hash::Hash,
155    ) -> Result<zx::Vmo, NonMetaStorageError> {
156        self.get_blob_vmo(hash).await.map_err(|e| match e {
157            blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
158            blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
159            blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
160        })
161    }
162
163    async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
164        let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
165        let content_size = vmo.get_content_size().map_err(|e| {
166            NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
167        })?;
168        vmo.read_to_vec::<u8>(0, content_size)
169            .map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
170    }
171}
172
173/// Assumes the directory is a flat container and the files are named after their hashes.
174impl NonMetaStorage for fio::DirectoryProxy {
175    fn open(
176        &self,
177        blob: &fuchsia_hash::Hash,
178        flags: fio::Flags,
179        _scope: ExecutionScope,
180        object_request: ObjectRequestRef<'_>,
181    ) -> Result<(), zx::Status> {
182        // If the FIDL call passes, errors will be communicated via the `object_request` channel.
183        self.open(
184            blob.to_string().as_str(),
185            flags,
186            &object_request.options(),
187            object_request.take().into_channel(),
188        )
189        .map_err(|_fidl_error| zx::Status::PEER_CLOSED)
190    }
191
192    async fn get_blob_vmo(
193        &self,
194        hash: &fuchsia_hash::Hash,
195    ) -> Result<zx::Vmo, NonMetaStorageError> {
196        let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
197            .await
198            .map_err(NonMetaStorageError::OpenBlob)?;
199        proxy
200            .get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
201            .await
202            .map_err(NonMetaStorageError::Fidl)?
203            .map_err(|e| NonMetaStorageError::GetVmo(zx::Status::from_raw(e)))
204    }
205
206    async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
207        fuchsia_fs::directory::read_file(self, &hash.to_string())
208            .await
209            .map_err(NonMetaStorageError::ReadBlob)
210    }
211}
212
213/// Serves a package directory for the package with hash `meta_far` on `server_end`.
214/// The connection rights are set by `flags`, used the same as the `flags` parameter of
215///   fuchsia.io/Directory.Open.
216pub fn serve(
217    scope: vfs::execution_scope::ExecutionScope,
218    non_meta_storage: impl NonMetaStorage,
219    meta_far: fuchsia_hash::Hash,
220    flags: fio::Flags,
221    server_end: ServerEnd<fio::DirectoryMarker>,
222) -> impl futures::Future<Output = Result<(), Error>> {
223    serve_path(
224        scope,
225        non_meta_storage,
226        meta_far,
227        flags,
228        vfs::Path::dot(),
229        server_end.into_channel().into(),
230    )
231}
232
233/// Serves a sub-`path` of a package directory for the package with hash `meta_far` on `server_end`.
234///
235/// The connection rights are set by `flags`, used the same as the `flags` parameter of
236///   fuchsia.io/Directory.Open.
237/// On error while loading the package metadata, closes the provided server end, sending an OnOpen
238///   response with an error status if requested.
239pub async fn serve_path(
240    scope: vfs::execution_scope::ExecutionScope,
241    non_meta_storage: impl NonMetaStorage,
242    meta_far: fuchsia_hash::Hash,
243    flags: fio::Flags,
244    path: vfs::Path,
245    server_end: ServerEnd<fio::NodeMarker>,
246) -> Result<(), Error> {
247    let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
248        Ok(d) => d,
249        Err(e) => {
250            let () = send_on_open_with_error(
251                flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
252                server_end,
253                (&e).into(),
254            );
255            return Err(e);
256        }
257    };
258
259    ObjectRequest::new(flags, &fio::Options::default(), server_end.into_channel())
260        .handle(|request| root_dir.open(scope, path, flags, request));
261    Ok(())
262}
263
264fn usize_to_u64_safe(u: usize) -> u64 {
265    let ret: u64 = u.try_into().unwrap();
266    static_assertions::assert_eq_size_val!(u, ret);
267    ret
268}
269
270/// RootDir takes an optional `OnRootDirDrop` value that will be dropped when the RootDir is
271/// dropped.
272///
273/// This is useful because the VFS functions operate on `Arc<RootDir>`s (and create clones of the
274/// `Arc`s in response to e.g. `Directory::open` calls), so this allows clients to perform actions
275/// when the last clone of the `Arc<RootDir>` is dropped (which is frequently when the last
276/// fuchsia.io connection closes).
277///
278/// The `ExecutionScope` used to serve the connection could also be used to notice when all the
279/// `Arc<RootDir>`s are dropped, but only if the `Arc<RootDir>`s are only used by VFS. Tracking
280/// when the `RootDir` itself is dropped allows non VFS uses of the `Arc<RootDir>`s.
281pub trait OnRootDirDrop: Send + Sync + std::fmt::Debug {}
282impl<T> OnRootDirDrop for T where T: Send + Sync + std::fmt::Debug {}
283
284/// Takes a directory hierarchy and a directory in the hierarchy and returns all the directory's
285/// children in alphabetical order.
286///   `materialized_tree`: object relative path expressions of every file in a directory hierarchy
287///   `dir`: the empty string (signifies the root dir) or a path to a subdir (must be an object
288///          relative path expression plus a trailing slash)
289/// Returns an empty vec if `dir` isn't in `materialized_tree`.
290fn get_dir_children<'a>(
291    materialized_tree: impl IntoIterator<Item = &'a str>,
292    dir: &str,
293) -> Vec<(EntryInfo, String)> {
294    let mut added_entries = HashSet::new();
295    let mut res = vec![];
296
297    for path in materialized_tree {
298        if let Some(path) = path.strip_prefix(dir) {
299            match path.split_once('/') {
300                None => {
301                    // TODO(https://fxbug.dev/42161818) Replace .contains/.insert with .get_or_insert_owned when non-experimental.
302                    if !added_entries.contains(path) {
303                        res.push((
304                            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
305                            path.to_string(),
306                        ));
307                        added_entries.insert(path.to_string());
308                    }
309                }
310                Some((first, _)) => {
311                    if !added_entries.contains(first) {
312                        res.push((
313                            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
314                            first.to_string(),
315                        ));
316                        added_entries.insert(first.to_string());
317                    }
318                }
319            }
320        }
321    }
322
323    // TODO(https://fxbug.dev/42162840) Remove this sort
324    res.sort_by(|a, b| a.1.cmp(&b.1));
325    res
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use assert_matches::assert_matches;
332    use fuchsia_hash::Hash;
333    use fuchsia_pkg_testing::PackageBuilder;
334    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
335    use futures::StreamExt;
336    use vfs::directory::helper::DirectlyMutable;
337
338    #[fuchsia_async::run_singlethreaded(test)]
339    async fn serve() {
340        let (proxy, server_end) = fidl::endpoints::create_proxy();
341        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
342        let (metafar_blob, _) = package.contents();
343        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
344        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
345
346        crate::serve(
347            vfs::execution_scope::ExecutionScope::new(),
348            blobfs_client,
349            metafar_blob.merkle,
350            fio::PERM_READABLE,
351            server_end,
352        )
353        .await
354        .unwrap();
355
356        assert_eq!(
357            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
358            vec![fuchsia_fs::directory::DirEntry {
359                name: "meta".to_string(),
360                kind: fuchsia_fs::directory::DirentKind::Directory
361            }]
362        );
363    }
364
365    #[fuchsia_async::run_singlethreaded(test)]
366    async fn serve_path_open_root() {
367        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
368        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
369        let (metafar_blob, _) = package.contents();
370        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
371        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
372
373        crate::serve_path(
374            vfs::execution_scope::ExecutionScope::new(),
375            blobfs_client,
376            metafar_blob.merkle,
377            fio::PERM_READABLE,
378            vfs::Path::validate_and_split(".").unwrap(),
379            server_end.into_channel().into(),
380        )
381        .await
382        .unwrap();
383
384        assert_eq!(
385            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
386            vec![fuchsia_fs::directory::DirEntry {
387                name: "meta".to_string(),
388                kind: fuchsia_fs::directory::DirentKind::Directory
389            }]
390        );
391    }
392
393    #[fuchsia_async::run_singlethreaded(test)]
394    async fn serve_path_open_meta() {
395        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
396        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
397        let (metafar_blob, _) = package.contents();
398        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
399        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
400
401        crate::serve_path(
402            vfs::execution_scope::ExecutionScope::new(),
403            blobfs_client,
404            metafar_blob.merkle,
405            fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
406            vfs::Path::validate_and_split("meta").unwrap(),
407            server_end.into_channel().into(),
408        )
409        .await
410        .unwrap();
411
412        assert_eq!(
413            fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
414            metafar_blob.merkle.to_string(),
415        );
416    }
417
418    #[fuchsia_async::run_singlethreaded(test)]
419    async fn serve_path_open_missing_path_in_package() {
420        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
421        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
422        let (metafar_blob, _) = package.contents();
423        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
424        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
425
426        assert_matches!(
427            crate::serve_path(
428                vfs::execution_scope::ExecutionScope::new(),
429                blobfs_client,
430                metafar_blob.merkle,
431                fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
432                vfs::Path::validate_and_split("not-present").unwrap(),
433                server_end.into_channel().into(),
434            )
435            .await,
436            // serve_path succeeds in opening the package, but the forwarded open will discover
437            // that the requested path does not exist.
438            Ok(())
439        );
440
441        assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
442    }
443
444    #[fuchsia_async::run_singlethreaded(test)]
445    async fn serve_path_open_missing_package() {
446        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
447        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
448
449        assert_matches!(
450            crate::serve_path(
451                vfs::execution_scope::ExecutionScope::new(),
452                blobfs_client,
453                Hash::from([0u8; 32]),
454                fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
455                vfs::Path::validate_and_split(".").unwrap(),
456                server_end.into_channel().into(),
457            )
458            .await,
459            Err(Error::MissingMetaFar)
460        );
461
462        assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
463    }
464
465    async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
466        // Handle either an io1 OnOpen Status or an io2 epitaph status, though only one will be
467        // sent, determined by the open() API used.
468        let mut events = node.take_event_stream();
469        match events.next().await? {
470            Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => Some(zx::Status::from_raw(status)),
471            Ok(fio::NodeEvent::OnRepresentation { .. }) => Some(zx::Status::OK),
472            Err(fidl::Error::ClientChannelClosed { status, .. }) => Some(status),
473            other => panic!("unexpected stream event or error: {other:?}"),
474        }
475    }
476
477    fn file() -> EntryInfo {
478        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
479    }
480
481    fn dir() -> EntryInfo {
482        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
483    }
484
485    #[test]
486    fn get_dir_children_root() {
487        assert_eq!(get_dir_children([], ""), vec![]);
488        assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
489        assert_eq!(
490            get_dir_children(["a", "b"], ""),
491            vec![(file(), "a".to_string()), (file(), "b".to_string())]
492        );
493        assert_eq!(
494            get_dir_children(["b", "a"], ""),
495            vec![(file(), "a".to_string()), (file(), "b".to_string())]
496        );
497        assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
498        assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
499        assert_eq!(
500            get_dir_children(["a/b", "c"], ""),
501            vec![(dir(), "a".to_string()), (file(), "c".to_string())]
502        );
503        assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
504    }
505
506    #[test]
507    fn get_dir_children_subdir() {
508        assert_eq!(get_dir_children([], "a/"), vec![]);
509        assert_eq!(get_dir_children(["a"], "a/"), vec![]);
510        assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
511        assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
512        assert_eq!(
513            get_dir_children(["a/b", "a/c"], "a/"),
514            vec![(file(), "b".to_string()), (file(), "c".to_string())]
515        );
516        assert_eq!(
517            get_dir_children(["a/c", "a/b"], "a/"),
518            vec![(file(), "b".to_string()), (file(), "c".to_string())]
519        );
520        assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
521        assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
522        assert_eq!(
523            get_dir_children(["a/b/c", "a/d"], "a/"),
524            vec![(dir(), "b".to_string()), (file(), "d".to_string())]
525        );
526        assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
527    }
528
529    const BLOB_CONTENTS: &[u8] = b"blob-contents";
530
531    fn blob_contents_hash() -> Hash {
532        fuchsia_merkle::root_from_slice(BLOB_CONTENTS)
533    }
534
535    #[fuchsia_async::run_singlethreaded(test)]
536    async fn bootfs_get_vmo_blob() {
537        let directory = vfs::directory::immutable::simple();
538        directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
539        let proxy = vfs::directory::serve_read_only(directory);
540
541        let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
542        assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
543    }
544
545    #[fuchsia_async::run_singlethreaded(test)]
546    async fn bootfs_read_blob() {
547        let directory = vfs::directory::immutable::simple();
548        directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
549        let proxy = vfs::directory::serve_read_only(directory);
550
551        assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
552    }
553
554    #[fuchsia_async::run_singlethreaded(test)]
555    async fn bootfs_get_vmo_blob_missing_blob() {
556        let directory = vfs::directory::immutable::simple();
557        let proxy = vfs::directory::serve_read_only(directory);
558
559        let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
560        assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
561    }
562
563    #[fuchsia_async::run_singlethreaded(test)]
564    async fn bootfs_read_blob_missing_blob() {
565        let directory = vfs::directory::immutable::simple();
566        let proxy = vfs::directory::serve_read_only(directory);
567
568        let result = proxy.read_blob(&blob_contents_hash()).await;
569        assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
570    }
571
572    #[fuchsia_async::run_singlethreaded(test)]
573    async fn blobfs_get_vmo_blob() {
574        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
575        blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
576
577        let vmo =
578            NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
579        assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
580    }
581
582    #[fuchsia_async::run_singlethreaded(test)]
583    async fn blobfs_read_blob() {
584        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
585        blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
586
587        assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
588    }
589
590    #[fuchsia_async::run_singlethreaded(test)]
591    async fn blobfs_get_vmo_blob_missing_blob() {
592        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
593
594        let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
595        assert_matches!(result, Err(e) if e.is_not_found_error());
596    }
597
598    #[fuchsia_async::run_singlethreaded(test)]
599    async fn blobfs_read_blob_missing_blob() {
600        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
601
602        let result = blobfs_client.read_blob(&blob_contents_hash()).await;
603        assert_matches!(result, Err(e) if e.is_not_found_error());
604    }
605}