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