package_directory/
root_dir.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
5use crate::meta_as_dir::MetaAsDir;
6use crate::meta_subdir::MetaSubdir;
7use crate::non_meta_subdir::NonMetaSubdir;
8use crate::{Error, NonMetaStorageError, usize_to_u64_safe};
9use fidl_fuchsia_io as fio;
10use fuchsia_pkg::MetaContents;
11use log::error;
12use std::collections::HashMap;
13use std::sync::Arc;
14use vfs::directory::entry::{EntryInfo, OpenRequest};
15use vfs::directory::immutable::connection::ImmutableConnection;
16use vfs::directory::traversal_position::TraversalPosition;
17use vfs::execution_scope::ExecutionScope;
18use vfs::file::vmo::VmoFile;
19use vfs::{ObjectRequestRef, ProtocolsExt as _, immutable_attributes};
20
21/// The root directory of Fuchsia package.
22#[derive(Debug)]
23pub struct RootDir<S> {
24    pub(crate) non_meta_storage: S,
25    pub(crate) hash: fuchsia_hash::Hash,
26    // The keys are object relative path expressions.
27    pub(crate) meta_files: HashMap<String, MetaFileLocation>,
28    // The keys are object relative path expressions.
29    pub(crate) non_meta_files: HashMap<String, fuchsia_hash::Hash>,
30    pub(crate) meta_far_vmo: zx::Vmo,
31    dropper: Option<Box<dyn crate::OnRootDirDrop>>,
32}
33
34impl<S: crate::NonMetaStorage> RootDir<S> {
35    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
36    /// representing the package, backed by `non_meta_storage`.
37    pub async fn new(non_meta_storage: S, hash: fuchsia_hash::Hash) -> Result<Arc<Self>, Error> {
38        Ok(Arc::new(Self::new_raw(non_meta_storage, hash, None).await?))
39    }
40
41    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
42    /// representing the package, backed by `non_meta_storage`.
43    /// Takes `dropper`, which will be dropped when the returned `RootDir` is dropped.
44    pub async fn new_with_dropper(
45        non_meta_storage: S,
46        hash: fuchsia_hash::Hash,
47        dropper: Box<dyn crate::OnRootDirDrop>,
48    ) -> Result<Arc<Self>, Error> {
49        Ok(Arc::new(Self::new_raw(non_meta_storage, hash, Some(dropper)).await?))
50    }
51
52    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
53    /// representing the package, backed by `non_meta_storage`.
54    /// Takes `dropper`, which will be dropped when the returned `RootDir` is dropped.
55    /// Like `new_with_dropper` except the returned `RootDir` is not in an `Arc`.
56    pub async fn new_raw(
57        non_meta_storage: S,
58        hash: fuchsia_hash::Hash,
59        dropper: Option<Box<dyn crate::OnRootDirDrop>>,
60    ) -> Result<Self, Error> {
61        let meta_far_vmo = non_meta_storage.get_blob_vmo(&hash).await.map_err(|e| {
62            if e.is_not_found_error() { Error::MissingMetaFar } else { Error::OpenMetaFar(e) }
63        })?;
64        let (meta_files, non_meta_files) = load_package_metadata(&meta_far_vmo)?;
65
66        Ok(RootDir { non_meta_storage, hash, meta_files, non_meta_files, meta_far_vmo, dropper })
67    }
68
69    /// Sets the dropper. If the dropper was already set, returns `dropper` in the error.
70    pub fn set_dropper(
71        &mut self,
72        dropper: Box<dyn crate::OnRootDirDrop>,
73    ) -> Result<(), Box<dyn crate::OnRootDirDrop>> {
74        match self.dropper {
75            Some(_) => Err(dropper),
76            None => {
77                self.dropper = Some(dropper);
78                Ok(())
79            }
80        }
81    }
82
83    /// Returns the contents, if present, of the file at object relative path expression `path`.
84    /// https://fuchsia.dev/fuchsia-src/concepts/process/namespaces?hl=en#object_relative_path_expressions
85    pub async fn read_file(&self, path: &str) -> Result<Vec<u8>, ReadFileError> {
86        if let Some(hash) = self.non_meta_files.get(path) {
87            self.non_meta_storage.read_blob(hash).await.map_err(ReadFileError::ReadBlob)
88        } else if let Some(location) = self.meta_files.get(path) {
89            self.meta_far_vmo
90                .read_to_vec(location.offset, location.length)
91                .map_err(ReadFileError::ReadMetaFile)
92        } else {
93            Err(ReadFileError::NoFileAtPath { path: path.to_string() })
94        }
95    }
96
97    /// Returns `true` iff there is a file at `path`, an object relative path expression.
98    /// https://fuchsia.dev/fuchsia-src/concepts/process/namespaces?hl=en#object_relative_path_expressions
99    pub fn has_file(&self, path: &str) -> bool {
100        self.non_meta_files.contains_key(path) || self.meta_files.contains_key(path)
101    }
102
103    /// Returns the hash of the package.
104    pub fn hash(&self) -> &fuchsia_hash::Hash {
105        &self.hash
106    }
107
108    /// Returns an iterator of the hashes of files stored externally to the package meta.far.
109    /// May return duplicates.
110    pub fn external_file_hashes(&self) -> impl ExactSizeIterator<Item = &fuchsia_hash::Hash> {
111        self.non_meta_files.values()
112    }
113
114    /// Returns the path of the package as indicated by the "meta/package" file.
115    pub async fn path(&self) -> Result<fuchsia_pkg::PackagePath, PathError> {
116        Ok(fuchsia_pkg::MetaPackage::deserialize(&self.read_file("meta/package").await?[..])?
117            .into_path())
118    }
119
120    /// Returns the subpackages of the package.
121    pub async fn subpackages(&self) -> Result<fuchsia_pkg::MetaSubpackages, SubpackagesError> {
122        let contents = match self.read_file(fuchsia_pkg::MetaSubpackages::PATH).await {
123            Ok(contents) => contents,
124            Err(ReadFileError::NoFileAtPath { .. }) => {
125                return Ok(fuchsia_pkg::MetaSubpackages::default());
126            }
127            Err(e) => Err(e)?,
128        };
129
130        Ok(fuchsia_pkg::MetaSubpackages::deserialize(&*contents)?)
131    }
132
133    /// Creates a file that contains the package's hash.
134    fn create_meta_as_file(&self) -> Result<Arc<VmoFile>, zx::Status> {
135        let file_contents = self.hash.to_string();
136        let vmo = zx::Vmo::create(usize_to_u64_safe(file_contents.len()))?;
137        let () = vmo.write(file_contents.as_bytes(), 0)?;
138        Ok(VmoFile::new_with_inode(vmo, /*inode*/ 1))
139    }
140
141    /// Creates and returns a meta file if one exists at `path`.
142    pub(crate) fn get_meta_file(&self, path: &str) -> Result<Option<Arc<VmoFile>>, zx::Status> {
143        // The FAR spec requires 4 KiB alignment of content chunks [1], so offset will
144        // always be page-aligned, because pages are required [2] to be a power of 2 and at
145        // least 4 KiB.
146        // [1] https://fuchsia.dev/fuchsia-src/concepts/source_code/archive_format#content_chunk
147        // [2] https://fuchsia.dev/fuchsia-src/reference/syscalls/system_get_page_size
148        // TODO(https://fxbug.dev/42162525) Need to manually zero the end of the VMO if
149        // zx_system_get_page_size() > 4K.
150        assert_eq!(zx::system_get_page_size(), 4096);
151
152        let location = match self.meta_files.get(path) {
153            Some(location) => location,
154            None => return Ok(None),
155        };
156        let vmo = self
157            .meta_far_vmo
158            .create_child(
159                zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE,
160                location.offset,
161                location.length,
162            )
163            .map_err(|e| {
164                error!("Error creating child vmo for meta file {:?}", e);
165                zx::Status::INTERNAL
166            })?;
167
168        Ok(Some(VmoFile::new_with_inode(vmo, /*inode*/ 1)))
169    }
170
171    /// Creates and returns a `MetaSubdir` if one exists at `path`. `path` must end in '/'.
172    pub(crate) fn get_meta_subdir(self: &Arc<Self>, path: String) -> Option<Arc<MetaSubdir<S>>> {
173        debug_assert!(path.ends_with("/"));
174        for k in self.meta_files.keys() {
175            if k.starts_with(&path) {
176                return Some(MetaSubdir::new(self.clone(), path));
177            }
178        }
179        None
180    }
181
182    /// Creates and returns a `NonMetaSubdir` if one exists at `path`. `path` must end in '/'.
183    pub(crate) fn get_non_meta_subdir(
184        self: &Arc<Self>,
185        path: String,
186    ) -> Option<Arc<NonMetaSubdir<S>>> {
187        debug_assert!(path.ends_with("/"));
188        for k in self.non_meta_files.keys() {
189            if k.starts_with(&path) {
190                return Some(NonMetaSubdir::new(self.clone(), path));
191            }
192        }
193        None
194    }
195}
196
197#[derive(thiserror::Error, Debug)]
198pub enum ReadFileError {
199    #[error("reading blob")]
200    ReadBlob(#[source] NonMetaStorageError),
201
202    #[error("reading meta file")]
203    ReadMetaFile(#[source] zx::Status),
204
205    #[error("no file exists at path: {path:?}")]
206    NoFileAtPath { path: String },
207}
208
209#[derive(thiserror::Error, Debug)]
210pub enum SubpackagesError {
211    #[error("reading manifest")]
212    Read(#[from] ReadFileError),
213
214    #[error("parsing manifest")]
215    Parse(#[from] fuchsia_pkg::MetaSubpackagesError),
216}
217
218#[derive(thiserror::Error, Debug)]
219pub enum PathError {
220    #[error("reading meta/package")]
221    Read(#[from] ReadFileError),
222
223    #[error("parsing meta/package")]
224    Parse(#[from] fuchsia_pkg::MetaPackageError),
225}
226
227impl<S: crate::NonMetaStorage> vfs::directory::entry::DirectoryEntry for RootDir<S> {
228    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
229        request.open_dir(self)
230    }
231}
232
233impl<S: crate::NonMetaStorage> vfs::directory::entry::GetEntryInfo for RootDir<S> {
234    fn entry_info(&self) -> EntryInfo {
235        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
236    }
237}
238
239impl<S: crate::NonMetaStorage> vfs::node::Node for RootDir<S> {
240    async fn get_attributes(
241        &self,
242        requested_attributes: fio::NodeAttributesQuery,
243    ) -> Result<fio::NodeAttributes2, zx::Status> {
244        Ok(immutable_attributes!(
245            requested_attributes,
246            Immutable {
247                protocols: fio::NodeProtocolKinds::DIRECTORY,
248                abilities: crate::DIRECTORY_ABILITIES,
249                id: 1,
250            }
251        ))
252    }
253}
254
255impl<S: crate::NonMetaStorage> vfs::directory::entry_container::Directory for RootDir<S> {
256    fn open(
257        self: Arc<Self>,
258        scope: ExecutionScope,
259        path: vfs::Path,
260        flags: fio::Flags,
261        object_request: ObjectRequestRef<'_>,
262    ) -> Result<(), zx::Status> {
263        if !flags.difference(crate::ALLOWED_FLAGS).is_empty() {
264            return Err(zx::Status::NOT_SUPPORTED);
265        }
266
267        // Handle case where the request is for this directory itself (e.g. ".").
268        if path.is_empty() {
269            // `ImmutableConnection` checks that only directory flags are specified.
270            object_request
271                .take()
272                .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
273            return Ok(());
274        }
275
276        // `path` is relative, and may include a trailing slash.
277        let canonical_path = path.as_ref().strip_suffix('/').unwrap_or_else(|| path.as_ref());
278
279        if canonical_path == "meta" {
280            // This branch is done here instead of in MetaAsDir so that Clone'ing MetaAsDir yields
281            // MetaAsDir. See the MetaAsDir::open impl for more.
282
283            // TODO(https://fxbug.dev/328485661): consider retrieving the merkle root by retrieving
284            // the attribute instead of opening as a file to read the merkle root content.
285
286            // To remain POSIX compliant, we must default to opening meta as a file unless the
287            // directory protocol (i.e. O_DIRECTORY) is explicitly requested.
288            let open_meta_as_dir = flags.is_dir_allowed() && !flags.is_file_allowed();
289            if !open_meta_as_dir {
290                if path.is_dir() {
291                    return Err(zx::Status::NOT_DIR);
292                }
293                let file = self.create_meta_as_file().map_err(|e| {
294                    error!("Error creating the meta file: {:?}", e);
295                    zx::Status::INTERNAL
296                })?;
297                return vfs::file::serve(file, scope, &flags, object_request);
298            }
299            return MetaAsDir::new(self).open(scope, vfs::Path::dot(), flags, object_request);
300        }
301
302        if canonical_path.starts_with("meta/") {
303            if let Some(file) = self.get_meta_file(canonical_path)? {
304                if path.is_dir() {
305                    return Err(zx::Status::NOT_DIR);
306                }
307                return vfs::file::serve(file, scope, &flags, object_request);
308            }
309
310            if let Some(subdir) = self.get_meta_subdir(canonical_path.to_string() + "/") {
311                return subdir.open(scope, vfs::Path::dot(), flags, object_request);
312            }
313            return Err(zx::Status::NOT_FOUND);
314        }
315
316        if let Some(blob) = self.non_meta_files.get(canonical_path) {
317            if path.is_dir() {
318                return Err(zx::Status::NOT_DIR);
319            }
320            return self.non_meta_storage.open(blob, flags, scope, object_request);
321        }
322
323        if let Some(subdir) = self.get_non_meta_subdir(canonical_path.to_string() + "/") {
324            return subdir.open(scope, vfs::Path::dot(), flags, object_request);
325        }
326
327        Err(zx::Status::NOT_FOUND)
328    }
329
330    async fn read_dirents(
331        &self,
332        pos: &TraversalPosition,
333        sink: Box<dyn vfs::directory::dirents_sink::Sink + 'static>,
334    ) -> Result<
335        (TraversalPosition, Box<dyn vfs::directory::dirents_sink::Sealed + 'static>),
336        zx::Status,
337    > {
338        vfs::directory::read_dirents::read_dirents(
339            // Add "meta/placeholder" file so the "meta" dir is included in the results
340            &crate::get_dir_children(
341                self.non_meta_files.keys().map(|s| s.as_str()).chain(["meta/placeholder"]),
342                "",
343            ),
344            pos,
345            sink,
346        )
347    }
348
349    fn register_watcher(
350        self: Arc<Self>,
351        _: ExecutionScope,
352        _: fio::WatchMask,
353        _: vfs::directory::entry_container::DirectoryWatcher,
354    ) -> Result<(), zx::Status> {
355        Err(zx::Status::NOT_SUPPORTED)
356    }
357
358    // `register_watcher` is unsupported so no need to do anything here.
359    fn unregister_watcher(self: Arc<Self>, _: usize) {}
360}
361
362#[allow(clippy::type_complexity)]
363fn load_package_metadata(
364    meta_far_vmo: &zx::Vmo,
365) -> Result<(HashMap<String, MetaFileLocation>, HashMap<String, fuchsia_hash::Hash>), Error> {
366    let stream =
367        zx::Stream::create(zx::StreamOptions::MODE_READ, meta_far_vmo, 0).map_err(|e| {
368            Error::OpenMetaFar(NonMetaStorageError::ReadBlob(
369                fuchsia_fs::file::ReadError::ReadError(e),
370            ))
371        })?;
372
373    let mut reader = fuchsia_archive::Reader::new(stream).map_err(Error::ArchiveReader)?;
374    let reader_list = reader.list();
375    let mut meta_files = HashMap::with_capacity(reader_list.len());
376    for entry in reader_list {
377        let path = std::str::from_utf8(entry.path())
378            .map_err(|source| Error::NonUtf8MetaEntry { source, path: entry.path().to_owned() })?
379            .to_owned();
380        if path.starts_with("meta/") {
381            for (i, _) in path.match_indices('/').skip(1) {
382                if meta_files.contains_key(&path[..i]) {
383                    return Err(Error::FileDirectoryCollision { path: path[..i].to_string() });
384                }
385            }
386            meta_files
387                .insert(path, MetaFileLocation { offset: entry.offset(), length: entry.length() });
388        }
389    }
390
391    let meta_contents_bytes =
392        reader.read_file(b"meta/contents").map_err(Error::ReadMetaContents)?;
393
394    let non_meta_files = MetaContents::deserialize(&meta_contents_bytes[..])
395        .map_err(Error::DeserializeMetaContents)?
396        .into_contents();
397
398    Ok((meta_files, non_meta_files))
399}
400
401/// Location of a meta file's contents within a meta.far
402#[derive(Clone, Copy, Debug, PartialEq, Eq)]
403pub(crate) struct MetaFileLocation {
404    offset: u64,
405    length: u64,
406}
407
408#[cfg(test)]
409mod tests {
410    use super::*;
411    use assert_matches::assert_matches;
412    use fidl::endpoints::create_proxy;
413    use fuchsia_fs::directory::{DirEntry, DirentKind};
414    use fuchsia_pkg_testing::PackageBuilder;
415    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
416    use futures::TryStreamExt as _;
417    use pretty_assertions::assert_eq;
418    use std::io::Cursor;
419
420    struct TestEnv {
421        _blobfs_fake: FakeBlobfs,
422        root_dir: Arc<RootDir<blobfs::Client>>,
423    }
424
425    impl TestEnv {
426        async fn with_subpackages(
427            subpackages_content: Option<&[u8]>,
428        ) -> (Self, Arc<RootDir<blobfs::Client>>) {
429            let mut pkg = PackageBuilder::new("base-package-0")
430                .add_resource_at("resource", "blob-contents".as_bytes())
431                .add_resource_at("dir/file", "bloblob".as_bytes())
432                .add_resource_at("meta/file", "meta-contents0".as_bytes())
433                .add_resource_at("meta/dir/file", "meta-contents1".as_bytes());
434            if let Some(subpackages_content) = subpackages_content {
435                pkg = pkg.add_resource_at(fuchsia_pkg::MetaSubpackages::PATH, subpackages_content);
436            }
437            let pkg = pkg.build().await.unwrap();
438            let (metafar_blob, content_blobs) = pkg.contents();
439            let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
440            blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
441            for (hash, bytes) in content_blobs {
442                blobfs_fake.add_blob(hash, bytes);
443            }
444
445            let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
446            (Self { _blobfs_fake: blobfs_fake, root_dir: root_dir.clone() }, root_dir)
447        }
448
449        async fn new() -> (Self, fio::DirectoryProxy) {
450            let (env, root) = Self::with_subpackages(None).await;
451            (env, vfs::directory::serve_read_only(root))
452        }
453    }
454
455    #[fuchsia_async::run_singlethreaded(test)]
456    async fn new_missing_meta_far_error() {
457        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
458        assert_matches!(
459            RootDir::new(blobfs_client, [0; 32].into()).await,
460            Err(Error::MissingMetaFar)
461        );
462    }
463
464    #[fuchsia_async::run_singlethreaded(test)]
465    async fn new_rejects_invalid_utf8() {
466        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
467        let mut meta_far = vec![];
468        let () = fuchsia_archive::write(
469            &mut meta_far,
470            std::collections::BTreeMap::from_iter([(
471                b"\xff",
472                (0, Box::new("".as_bytes()) as Box<dyn std::io::Read>),
473            )]),
474        )
475        .unwrap();
476        let hash = fuchsia_merkle::root_from_slice(&meta_far);
477        let () = blobfs_fake.add_blob(hash, meta_far);
478
479        assert_matches!(
480            RootDir::new(blobfs_client, hash).await,
481            Err(Error::NonUtf8MetaEntry{path, ..})
482                if path == vec![255]
483        );
484    }
485
486    #[fuchsia_async::run_singlethreaded(test)]
487    async fn new_initializes_maps() {
488        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
489
490        let meta_files = HashMap::from([
491            (String::from("meta/contents"), MetaFileLocation { offset: 4096, length: 148 }),
492            (String::from("meta/package"), MetaFileLocation { offset: 20480, length: 39 }),
493            (String::from("meta/file"), MetaFileLocation { offset: 12288, length: 14 }),
494            (String::from("meta/dir/file"), MetaFileLocation { offset: 8192, length: 14 }),
495            (
496                String::from("meta/fuchsia.abi/abi-revision"),
497                MetaFileLocation { offset: 16384, length: 8 },
498            ),
499        ]);
500        assert_eq!(root_dir.meta_files, meta_files);
501
502        let non_meta_files: HashMap<String, fuchsia_hash::Hash> = [
503            (
504                String::from("resource"),
505                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15"
506                    .parse::<fuchsia_hash::Hash>()
507                    .unwrap(),
508            ),
509            (
510                String::from("dir/file"),
511                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201"
512                    .parse::<fuchsia_hash::Hash>()
513                    .unwrap(),
514            ),
515        ]
516        .iter()
517        .cloned()
518        .collect();
519        assert_eq!(root_dir.non_meta_files, non_meta_files);
520    }
521
522    #[fuchsia_async::run_singlethreaded(test)]
523    async fn rejects_meta_file_collisions() {
524        let pkg = PackageBuilder::new("base-package-0")
525            .add_resource_at("meta/dir/file", "meta-contents0".as_bytes())
526            .build()
527            .await
528            .unwrap();
529
530        // Manually modify the meta.far to contain a "meta/dir" entry.
531        let (metafar_blob, _) = pkg.contents();
532        let mut metafar =
533            fuchsia_archive::Reader::new(Cursor::new(&metafar_blob.contents)).unwrap();
534        let mut entries = std::collections::BTreeMap::new();
535        let farentries =
536            metafar.list().map(|entry| (entry.path().to_vec(), entry.length())).collect::<Vec<_>>();
537        for (path, length) in farentries {
538            let contents = metafar.read_file(&path).unwrap();
539            entries
540                .insert(path, (length, Box::new(Cursor::new(contents)) as Box<dyn std::io::Read>));
541        }
542        let extra_contents = b"meta-contents1";
543        entries.insert(
544            b"meta/dir".to_vec(),
545            (
546                extra_contents.len() as u64,
547                Box::new(Cursor::new(extra_contents)) as Box<dyn std::io::Read>,
548            ),
549        );
550
551        let mut metafar: Vec<u8> = vec![];
552        let () = fuchsia_archive::write(&mut metafar, entries).unwrap();
553        let merkle = fuchsia_merkle::root_from_slice(&metafar);
554
555        // Verify it fails to load with the expected error.
556        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
557        blobfs_fake.add_blob(merkle, &metafar);
558
559        match RootDir::new(blobfs_client, merkle).await {
560            Ok(_) => panic!("this should not be reached!"),
561            Err(Error::FileDirectoryCollision { path }) => {
562                assert_eq!(path, "meta/dir".to_string());
563            }
564            Err(e) => panic!("Expected collision error, receieved {e:?}"),
565        };
566    }
567
568    #[fuchsia_async::run_singlethreaded(test)]
569    async fn read_file() {
570        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
571
572        assert_eq!(root_dir.read_file("resource").await.unwrap().as_slice(), b"blob-contents");
573        assert_eq!(root_dir.read_file("meta/file").await.unwrap().as_slice(), b"meta-contents0");
574        assert_matches!(
575            root_dir.read_file("missing").await.unwrap_err(),
576            ReadFileError::NoFileAtPath{path} if path == "missing"
577        );
578    }
579
580    #[fuchsia_async::run_singlethreaded(test)]
581    async fn has_file() {
582        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
583
584        assert!(root_dir.has_file("resource"));
585        assert!(root_dir.has_file("meta/file"));
586        assert_eq!(root_dir.has_file("missing"), false);
587    }
588
589    #[fuchsia_async::run_singlethreaded(test)]
590    async fn external_file_hashes() {
591        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
592
593        let mut actual = root_dir.external_file_hashes().copied().collect::<Vec<_>>();
594        actual.sort();
595        assert_eq!(
596            actual,
597            vec![
598                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201".parse().unwrap(),
599                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15".parse().unwrap()
600            ]
601        );
602    }
603
604    #[fuchsia_async::run_singlethreaded(test)]
605    async fn path() {
606        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
607
608        assert_eq!(
609            root_dir.path().await.unwrap(),
610            "base-package-0/0".parse::<fuchsia_pkg::PackagePath>().unwrap()
611        );
612    }
613
614    #[fuchsia_async::run_singlethreaded(test)]
615    async fn subpackages_present() {
616        let subpackages = fuchsia_pkg::MetaSubpackages::from_iter([(
617            fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
618            "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
619        )]);
620        let mut subpackages_bytes = vec![];
621        let () = subpackages.serialize(&mut subpackages_bytes).unwrap();
622        let (_env, root_dir) = TestEnv::with_subpackages(Some(&*subpackages_bytes)).await;
623
624        assert_eq!(root_dir.subpackages().await.unwrap(), subpackages);
625    }
626
627    #[fuchsia_async::run_singlethreaded(test)]
628    async fn subpackages_absent() {
629        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
630
631        assert_eq!(root_dir.subpackages().await.unwrap(), fuchsia_pkg::MetaSubpackages::default());
632    }
633
634    #[fuchsia_async::run_singlethreaded(test)]
635    async fn subpackages_error() {
636        let (_env, root_dir) = TestEnv::with_subpackages(Some(b"invalid-json")).await;
637
638        assert_matches!(root_dir.subpackages().await, Err(SubpackagesError::Parse(_)));
639    }
640
641    /// Ensure connections to a [`RootDir`] cannot be created as mutable (i.e. with
642    /// [`fio::PERM_WRITABLE`]). This ensures that the VFS will disallow any attempts to create a
643    /// new file/directory, modify the attributes of any nodes, open any files as writable.
644    #[fuchsia_async::run_singlethreaded(test)]
645    async fn root_dir_cannot_be_served_as_mutable() {
646        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
647        let proxy = vfs::directory::serve(root_dir, fio::PERM_WRITABLE);
648        assert_matches!(
649            proxy.take_event_stream().try_next().await,
650            Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
651        );
652    }
653
654    #[fuchsia_async::run_singlethreaded(test)]
655    async fn root_dir_readdir() {
656        let (_env, root_dir) = TestEnv::new().await;
657        assert_eq!(
658            fuchsia_fs::directory::readdir_inclusive(&root_dir).await.unwrap(),
659            vec![
660                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
661                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
662                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
663                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
664            ]
665        );
666    }
667
668    #[fuchsia_async::run_singlethreaded(test)]
669    async fn root_dir_get_attributes() {
670        let (_env, root_dir) = TestEnv::new().await;
671        let (mutable_attributes, immutable_attributes) =
672            root_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
673        assert_eq!(
674            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
675            immutable_attributes!(
676                fio::NodeAttributesQuery::all(),
677                Immutable {
678                    protocols: fio::NodeProtocolKinds::DIRECTORY,
679                    abilities: crate::DIRECTORY_ABILITIES,
680                    id: 1,
681                }
682            )
683        );
684    }
685
686    #[fuchsia_async::run_singlethreaded(test)]
687    async fn root_dir_watch_not_supported() {
688        let (_env, root_dir) = TestEnv::new().await;
689        let (_client, server) = fidl::endpoints::create_endpoints();
690        let status =
691            zx::Status::from_raw(root_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
692        assert_eq!(status, zx::Status::NOT_SUPPORTED);
693    }
694
695    #[fuchsia_async::run_singlethreaded(test)]
696    async fn root_dir_open_non_meta_file() {
697        let (_env, root_dir) = TestEnv::new().await;
698        let proxy = fuchsia_fs::directory::open_file(&root_dir, "resource", fio::PERM_READABLE)
699            .await
700            .unwrap();
701        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"blob-contents".to_vec());
702    }
703
704    #[fuchsia_async::run_singlethreaded(test)]
705    async fn root_dir_open_meta_as_file() {
706        let (env, root_dir) = TestEnv::new().await;
707        let proxy =
708            fuchsia_fs::directory::open_file(&root_dir, "meta", fio::PERM_READABLE).await.unwrap();
709        assert_eq!(
710            fuchsia_fs::file::read(&proxy).await.unwrap(),
711            env.root_dir.hash.to_string().as_bytes()
712        );
713        // Ensure the connection is cloned correctly (i.e. we don't get meta-as-dir).
714        let (cloned_proxy, server_end) = create_proxy::<fio::FileMarker>();
715        proxy.clone(server_end.into_channel().into()).unwrap();
716        assert_eq!(
717            fuchsia_fs::file::read(&cloned_proxy).await.unwrap(),
718            env.root_dir.hash.to_string().as_bytes()
719        );
720    }
721
722    #[fuchsia_async::run_singlethreaded(test)]
723    async fn root_dir_open_meta_as_dir() {
724        let (_env, root_dir) = TestEnv::new().await;
725        for path in ["meta", "meta/"] {
726            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
727                .await
728                .unwrap();
729            assert_eq!(
730                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
731                vec![
732                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
733                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
734                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
735                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
736                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
737                ]
738            );
739            // Ensure the connection is cloned correctly (i.e. we don't get meta-as-file).
740            let (cloned_proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
741            proxy.clone(server_end.into_channel().into()).unwrap();
742            assert_eq!(
743                fuchsia_fs::directory::readdir(&cloned_proxy).await.unwrap(),
744                vec![
745                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
746                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
747                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
748                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
749                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
750                ]
751            );
752        }
753    }
754
755    #[fuchsia_async::run_singlethreaded(test)]
756    async fn root_dir_open_meta_as_node() {
757        let (_env, root_dir) = TestEnv::new().await;
758        for path in ["meta", "meta/"] {
759            let proxy = fuchsia_fs::directory::open_node(
760                &root_dir,
761                path,
762                fio::Flags::PROTOCOL_NODE
763                    | fio::Flags::PROTOCOL_DIRECTORY
764                    | fio::Flags::PERM_GET_ATTRIBUTES,
765            )
766            .await
767            .unwrap();
768            let (mutable_attributes, immutable_attributes) = proxy
769                .get_attributes(
770                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
771                )
772                .await
773                .unwrap()
774                .unwrap();
775            assert_eq!(
776                fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
777                immutable_attributes!(
778                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
779                    Immutable {
780                        protocols: fio::NodeProtocolKinds::DIRECTORY,
781                        abilities: crate::DIRECTORY_ABILITIES
782                    }
783                )
784            );
785        }
786        // We should also be able to open the meta file as a node.
787        let proxy = fuchsia_fs::directory::open_node(
788            &root_dir,
789            "meta",
790            fio::Flags::PROTOCOL_NODE | fio::Flags::PERM_GET_ATTRIBUTES,
791        )
792        .await
793        .unwrap();
794        let (mutable_attributes, immutable_attributes) = proxy
795            .get_attributes(
796                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
797            )
798            .await
799            .unwrap()
800            .unwrap();
801        assert_eq!(
802            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
803            immutable_attributes!(
804                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
805                Immutable {
806                    protocols: fio::NodeProtocolKinds::FILE,
807                    abilities: fio::Abilities::READ_BYTES | fio::Abilities::GET_ATTRIBUTES,
808                }
809            )
810        );
811    }
812
813    #[fuchsia_async::run_singlethreaded(test)]
814    async fn root_dir_open_meta_file() {
815        let (_env, root_dir) = TestEnv::new().await;
816        let proxy = fuchsia_fs::directory::open_file(&root_dir, "meta/file", fio::PERM_READABLE)
817            .await
818            .unwrap();
819        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
820    }
821
822    #[fuchsia_async::run_singlethreaded(test)]
823    async fn root_dir_open_meta_subdir() {
824        let (_env, root_dir) = TestEnv::new().await;
825        for path in ["meta/dir", "meta/dir/"] {
826            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
827                .await
828                .unwrap();
829            assert_eq!(
830                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
831                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
832            );
833        }
834    }
835
836    #[fuchsia_async::run_singlethreaded(test)]
837    async fn root_dir_open_non_meta_subdir() {
838        let (_env, root_dir) = TestEnv::new().await;
839        for path in ["dir", "dir/"] {
840            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
841                .await
842                .unwrap();
843            assert_eq!(
844                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
845                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
846            );
847        }
848    }
849}