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