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 deprecated_open(
121        &self,
122        blob: &fuchsia_hash::Hash,
123        flags: fio::OpenFlags,
124        scope: ExecutionScope,
125        server_end: ServerEnd<fio::NodeMarker>,
126    ) -> Result<(), NonMetaStorageError>;
127
128    /// Open a non-meta file by hash. `scope` may complete while there are still open connections.
129    fn open(
130        &self,
131        _blob: &fuchsia_hash::Hash,
132        _flags: fio::Flags,
133        _scope: ExecutionScope,
134        _object_request: ObjectRequestRef<'_>,
135    ) -> Result<(), zx::Status>;
136
137    /// Get a read-only VMO for the blob.
138    fn get_blob_vmo(
139        &self,
140        hash: &fuchsia_hash::Hash,
141    ) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
142
143    /// Reads the contents of a blob.
144    fn read_blob(
145        &self,
146        hash: &fuchsia_hash::Hash,
147    ) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
148}
149
150impl NonMetaStorage for blobfs::Client {
151    fn deprecated_open(
152        &self,
153        blob: &fuchsia_hash::Hash,
154        flags: fio::OpenFlags,
155        scope: ExecutionScope,
156        server_end: ServerEnd<fio::NodeMarker>,
157    ) -> Result<(), NonMetaStorageError> {
158        self.deprecated_open_blob_for_read(blob, flags, scope, server_end).map_err(|e| {
159            NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e))
160        })
161    }
162
163    fn open(
164        &self,
165        blob: &fuchsia_hash::Hash,
166        flags: fio::Flags,
167        scope: ExecutionScope,
168        object_request: ObjectRequestRef<'_>,
169    ) -> Result<(), zx::Status> {
170        self.open_blob_for_read(blob, flags, scope, object_request)
171    }
172
173    async fn get_blob_vmo(
174        &self,
175        hash: &fuchsia_hash::Hash,
176    ) -> Result<zx::Vmo, NonMetaStorageError> {
177        self.get_blob_vmo(hash).await.map_err(|e| match e {
178            blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
179            blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
180            blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
181        })
182    }
183
184    async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
185        let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
186        let content_size = vmo.get_content_size().map_err(|e| {
187            NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
188        })?;
189        vmo.read_to_vec::<u8>(0, content_size)
190            .map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
191    }
192}
193
194/// Assumes the directory is a flat container and the files are named after their hashes.
195impl NonMetaStorage for fio::DirectoryProxy {
196    fn deprecated_open(
197        &self,
198        blob: &fuchsia_hash::Hash,
199        flags: fio::OpenFlags,
200        _scope: ExecutionScope,
201        server_end: ServerEnd<fio::NodeMarker>,
202    ) -> Result<(), NonMetaStorageError> {
203        self.deprecated_open(flags, fio::ModeType::empty(), blob.to_string().as_str(), server_end)
204            .map_err(|e| {
205                NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e))
206            })
207    }
208
209    fn open(
210        &self,
211        blob: &fuchsia_hash::Hash,
212        flags: fio::Flags,
213        _scope: ExecutionScope,
214        object_request: ObjectRequestRef<'_>,
215    ) -> Result<(), zx::Status> {
216        // If the FIDL call passes, errors will be communicated via the `object_request` channel.
217        self.open(
218            blob.to_string().as_str(),
219            flags,
220            &object_request.options(),
221            object_request.take().into_channel(),
222        )
223        .map_err(|_fidl_error| zx::Status::PEER_CLOSED)
224    }
225
226    async fn get_blob_vmo(
227        &self,
228        hash: &fuchsia_hash::Hash,
229    ) -> Result<zx::Vmo, NonMetaStorageError> {
230        let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
231            .await
232            .map_err(NonMetaStorageError::OpenBlob)?;
233        proxy
234            .get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
235            .await
236            .map_err(NonMetaStorageError::Fidl)?
237            .map_err(|e| NonMetaStorageError::GetVmo(zx::Status::from_raw(e)))
238    }
239
240    async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
241        fuchsia_fs::directory::read_file(self, &hash.to_string())
242            .await
243            .map_err(NonMetaStorageError::ReadBlob)
244    }
245}
246
247/// Serves a package directory for the package with hash `meta_far` on `server_end`.
248/// The connection rights are set by `flags`, used the same as the `flags` parameter of
249///   fuchsia.io/Directory.Open.
250pub fn serve(
251    scope: vfs::execution_scope::ExecutionScope,
252    non_meta_storage: impl NonMetaStorage,
253    meta_far: fuchsia_hash::Hash,
254    flags: fio::Flags,
255    server_end: ServerEnd<fio::DirectoryMarker>,
256) -> impl futures::Future<Output = Result<(), Error>> {
257    serve_path(
258        scope,
259        non_meta_storage,
260        meta_far,
261        flags,
262        vfs::Path::dot(),
263        server_end.into_channel().into(),
264    )
265}
266
267/// Serves a sub-`path` of a package directory for the package with hash `meta_far` on `server_end`.
268///
269/// The connection rights are set by `flags`, used the same as the `flags` parameter of
270///   fuchsia.io/Directory.Open.
271/// On error while loading the package metadata, closes the provided server end, sending an OnOpen
272///   response with an error status if requested.
273pub async fn serve_path(
274    scope: vfs::execution_scope::ExecutionScope,
275    non_meta_storage: impl NonMetaStorage,
276    meta_far: fuchsia_hash::Hash,
277    flags: fio::Flags,
278    path: vfs::Path,
279    server_end: ServerEnd<fio::NodeMarker>,
280) -> Result<(), Error> {
281    let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
282        Ok(d) => d,
283        Err(e) => {
284            let () = send_on_open_with_error(
285                flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
286                server_end,
287                (&e).into(),
288            );
289            return Err(e);
290        }
291    };
292
293    ObjectRequest::new(flags, &fio::Options::default(), server_end.into_channel())
294        .handle(|request| root_dir.open(scope, path, flags, request));
295    Ok(())
296}
297
298fn usize_to_u64_safe(u: usize) -> u64 {
299    let ret: u64 = u.try_into().unwrap();
300    static_assertions::assert_eq_size_val!(u, ret);
301    ret
302}
303
304/// RootDir takes an optional `OnRootDirDrop` value that will be dropped when the RootDir is
305/// dropped.
306///
307/// This is useful because the VFS functions operate on `Arc<RootDir>`s (and create clones of the
308/// `Arc`s in response to e.g. `Directory::open` calls), so this allows clients to perform actions
309/// when the last clone of the `Arc<RootDir>` is dropped (which is frequently when the last
310/// fuchsia.io connection closes).
311///
312/// The `ExecutionScope` used to serve the connection could also be used to notice when all the
313/// `Arc<RootDir>`s are dropped, but only if the `Arc<RootDir>`s are only used by VFS. Tracking
314/// when the `RootDir` itself is dropped allows non VFS uses of the `Arc<RootDir>`s.
315pub trait OnRootDirDrop: Send + Sync + std::fmt::Debug {}
316impl<T> OnRootDirDrop for T where T: Send + Sync + std::fmt::Debug {}
317
318/// Takes a directory hierarchy and a directory in the hierarchy and returns all the directory's
319/// children in alphabetical order.
320///   `materialized_tree`: object relative path expressions of every file in a directory hierarchy
321///   `dir`: the empty string (signifies the root dir) or a path to a subdir (must be an object
322///          relative path expression plus a trailing slash)
323/// Returns an empty vec if `dir` isn't in `materialized_tree`.
324fn get_dir_children<'a>(
325    materialized_tree: impl IntoIterator<Item = &'a str>,
326    dir: &str,
327) -> Vec<(EntryInfo, String)> {
328    let mut added_entries = HashSet::new();
329    let mut res = vec![];
330
331    for path in materialized_tree {
332        if let Some(path) = path.strip_prefix(dir) {
333            match path.split_once('/') {
334                None => {
335                    // TODO(https://fxbug.dev/42161818) Replace .contains/.insert with .get_or_insert_owned when non-experimental.
336                    if !added_entries.contains(path) {
337                        res.push((
338                            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
339                            path.to_string(),
340                        ));
341                        added_entries.insert(path.to_string());
342                    }
343                }
344                Some((first, _)) => {
345                    if !added_entries.contains(first) {
346                        res.push((
347                            EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
348                            first.to_string(),
349                        ));
350                        added_entries.insert(first.to_string());
351                    }
352                }
353            }
354        }
355    }
356
357    // TODO(https://fxbug.dev/42162840) Remove this sort
358    res.sort_by(|a, b| a.1.cmp(&b.1));
359    res
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use assert_matches::assert_matches;
366    use fuchsia_hash::Hash;
367    use fuchsia_pkg_testing::PackageBuilder;
368    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
369    use futures::StreamExt;
370    use vfs::directory::helper::DirectlyMutable;
371
372    #[fuchsia_async::run_singlethreaded(test)]
373    async fn serve() {
374        let (proxy, server_end) = fidl::endpoints::create_proxy();
375        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
376        let (metafar_blob, _) = package.contents();
377        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
378        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
379
380        crate::serve(
381            vfs::execution_scope::ExecutionScope::new(),
382            blobfs_client,
383            metafar_blob.merkle,
384            fio::PERM_READABLE,
385            server_end,
386        )
387        .await
388        .unwrap();
389
390        assert_eq!(
391            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
392            vec![fuchsia_fs::directory::DirEntry {
393                name: "meta".to_string(),
394                kind: fuchsia_fs::directory::DirentKind::Directory
395            }]
396        );
397    }
398
399    #[fuchsia_async::run_singlethreaded(test)]
400    async fn serve_path_open_root() {
401        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
402        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
403        let (metafar_blob, _) = package.contents();
404        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
405        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
406
407        crate::serve_path(
408            vfs::execution_scope::ExecutionScope::new(),
409            blobfs_client,
410            metafar_blob.merkle,
411            fio::PERM_READABLE,
412            vfs::Path::validate_and_split(".").unwrap(),
413            server_end.into_channel().into(),
414        )
415        .await
416        .unwrap();
417
418        assert_eq!(
419            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
420            vec![fuchsia_fs::directory::DirEntry {
421                name: "meta".to_string(),
422                kind: fuchsia_fs::directory::DirentKind::Directory
423            }]
424        );
425    }
426
427    #[fuchsia_async::run_singlethreaded(test)]
428    async fn serve_path_open_meta() {
429        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
430        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
431        let (metafar_blob, _) = package.contents();
432        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
433        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
434
435        crate::serve_path(
436            vfs::execution_scope::ExecutionScope::new(),
437            blobfs_client,
438            metafar_blob.merkle,
439            fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
440            vfs::Path::validate_and_split("meta").unwrap(),
441            server_end.into_channel().into(),
442        )
443        .await
444        .unwrap();
445
446        assert_eq!(
447            fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
448            metafar_blob.merkle.to_string(),
449        );
450    }
451
452    #[fuchsia_async::run_singlethreaded(test)]
453    async fn serve_path_open_missing_path_in_package() {
454        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
455        let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
456        let (metafar_blob, _) = package.contents();
457        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
458        blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
459
460        assert_matches!(
461            crate::serve_path(
462                vfs::execution_scope::ExecutionScope::new(),
463                blobfs_client,
464                metafar_blob.merkle,
465                fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
466                vfs::Path::validate_and_split("not-present").unwrap(),
467                server_end.into_channel().into(),
468            )
469            .await,
470            // serve_path succeeds in opening the package, but the forwarded open will discover
471            // that the requested path does not exist.
472            Ok(())
473        );
474
475        assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
476    }
477
478    #[fuchsia_async::run_singlethreaded(test)]
479    async fn serve_path_open_missing_package() {
480        let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
481        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
482
483        assert_matches!(
484            crate::serve_path(
485                vfs::execution_scope::ExecutionScope::new(),
486                blobfs_client,
487                Hash::from([0u8; 32]),
488                fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
489                vfs::Path::validate_and_split(".").unwrap(),
490                server_end.into_channel().into(),
491            )
492            .await,
493            Err(Error::MissingMetaFar)
494        );
495
496        assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
497    }
498
499    async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
500        // Handle either an io1 OnOpen Status or an io2 epitaph status, though only one will be
501        // sent, determined by the open() API used.
502        let mut events = node.take_event_stream();
503        match events.next().await? {
504            Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => Some(zx::Status::from_raw(status)),
505            Ok(fio::NodeEvent::OnRepresentation { .. }) => Some(zx::Status::OK),
506            Err(fidl::Error::ClientChannelClosed { status, .. }) => Some(status),
507            other => panic!("unexpected stream event or error: {other:?}"),
508        }
509    }
510
511    fn file() -> EntryInfo {
512        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
513    }
514
515    fn dir() -> EntryInfo {
516        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
517    }
518
519    #[test]
520    fn get_dir_children_root() {
521        assert_eq!(get_dir_children([], ""), vec![]);
522        assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
523        assert_eq!(
524            get_dir_children(["a", "b"], ""),
525            vec![(file(), "a".to_string()), (file(), "b".to_string())]
526        );
527        assert_eq!(
528            get_dir_children(["b", "a"], ""),
529            vec![(file(), "a".to_string()), (file(), "b".to_string())]
530        );
531        assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
532        assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
533        assert_eq!(
534            get_dir_children(["a/b", "c"], ""),
535            vec![(dir(), "a".to_string()), (file(), "c".to_string())]
536        );
537        assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
538    }
539
540    #[test]
541    fn get_dir_children_subdir() {
542        assert_eq!(get_dir_children([], "a/"), vec![]);
543        assert_eq!(get_dir_children(["a"], "a/"), vec![]);
544        assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
545        assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
546        assert_eq!(
547            get_dir_children(["a/b", "a/c"], "a/"),
548            vec![(file(), "b".to_string()), (file(), "c".to_string())]
549        );
550        assert_eq!(
551            get_dir_children(["a/c", "a/b"], "a/"),
552            vec![(file(), "b".to_string()), (file(), "c".to_string())]
553        );
554        assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
555        assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
556        assert_eq!(
557            get_dir_children(["a/b/c", "a/d"], "a/"),
558            vec![(dir(), "b".to_string()), (file(), "d".to_string())]
559        );
560        assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
561    }
562
563    const BLOB_CONTENTS: &[u8] = b"blob-contents";
564
565    fn blob_contents_hash() -> Hash {
566        fuchsia_merkle::from_slice(BLOB_CONTENTS).root()
567    }
568
569    #[fuchsia_async::run_singlethreaded(test)]
570    async fn bootfs_get_vmo_blob() {
571        let directory = vfs::directory::immutable::simple();
572        directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
573        let proxy = vfs::directory::serve_read_only(directory);
574
575        let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
576        assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
577    }
578
579    #[fuchsia_async::run_singlethreaded(test)]
580    async fn bootfs_read_blob() {
581        let directory = vfs::directory::immutable::simple();
582        directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
583        let proxy = vfs::directory::serve_read_only(directory);
584
585        assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
586    }
587
588    #[fuchsia_async::run_singlethreaded(test)]
589    async fn bootfs_get_vmo_blob_missing_blob() {
590        let directory = vfs::directory::immutable::simple();
591        let proxy = vfs::directory::serve_read_only(directory);
592
593        let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
594        assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
595    }
596
597    #[fuchsia_async::run_singlethreaded(test)]
598    async fn bootfs_read_blob_missing_blob() {
599        let directory = vfs::directory::immutable::simple();
600        let proxy = vfs::directory::serve_read_only(directory);
601
602        let result = proxy.read_blob(&blob_contents_hash()).await;
603        assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
604    }
605
606    #[fuchsia_async::run_singlethreaded(test)]
607    async fn blobfs_get_vmo_blob() {
608        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
609        blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
610
611        let vmo =
612            NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
613        assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
614    }
615
616    #[fuchsia_async::run_singlethreaded(test)]
617    async fn blobfs_read_blob() {
618        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
619        blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
620
621        assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
622    }
623
624    #[fuchsia_async::run_singlethreaded(test)]
625    async fn blobfs_get_vmo_blob_missing_blob() {
626        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
627
628        let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
629        assert_matches!(result, Err(e) if e.is_not_found_error());
630    }
631
632    #[fuchsia_async::run_singlethreaded(test)]
633    async fn blobfs_read_blob_missing_blob() {
634        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
635
636        let result = blobfs_client.read_blob(&blob_contents_hash()).await;
637        assert_matches!(result, Err(e) if e.is_not_found_error());
638    }
639}