Skip to main content

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 packed::PackedMap;
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: PackedMap<str, MetaFileLocation>,
28    // The keys are object relative path expressions.
29    pub(crate) non_meta_files: PackedMap<str, 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        if let Some((k, _)) = self.meta_files.range(path.as_str()..).next()
175            && k.starts_with(&path)
176        {
177            return Some(MetaSubdir::new(self.clone(), path));
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        if let Some((k, _)) = self.non_meta_files.range(path.as_str()..).next()
189            && k.starts_with(&path)
190        {
191            return Some(NonMetaSubdir::new(self.clone(), path));
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(self.non_meta_files.keys().chain(["meta/placeholder"]), ""),
341            pos,
342            sink,
343        )
344    }
345
346    fn register_watcher(
347        self: Arc<Self>,
348        _: ExecutionScope,
349        _: fio::WatchMask,
350        _: vfs::directory::entry_container::DirectoryWatcher,
351    ) -> Result<(), zx::Status> {
352        Err(zx::Status::NOT_SUPPORTED)
353    }
354
355    // `register_watcher` is unsupported so no need to do anything here.
356    fn unregister_watcher(self: Arc<Self>, _: usize) {}
357}
358
359#[allow(clippy::type_complexity)]
360fn load_package_metadata(
361    meta_far_vmo: &zx::Vmo,
362) -> Result<(PackedMap<str, MetaFileLocation>, PackedMap<str, fuchsia_hash::Hash>), Error> {
363    let stream =
364        zx::Stream::create(zx::StreamOptions::MODE_READ, meta_far_vmo, 0).map_err(|e| {
365            Error::OpenMetaFar(NonMetaStorageError::ReadBlob(
366                fuchsia_fs::file::ReadError::ReadError(e),
367            ))
368        })?;
369
370    let mut reader = fuchsia_archive::Reader::new(stream).map_err(Error::ArchiveReader)?;
371    let mut meta_files = {
372        let reader_list = reader.list();
373        PackedMap::<str, MetaFileLocation>::with_capacity(
374            reader_list.len(),
375            reader_list.map(|entry| entry.path().len()).sum(),
376        )
377    };
378    let reader_list = reader.list();
379    // FAR files guarantee that entries are sorted by path, so we can use
380    // `append_or_update` directly to avoid `PackedMapBuilder`'s sorting overhead.
381    for entry in reader_list {
382        let path = std::str::from_utf8(entry.path())
383            .map_err(|source| Error::NonUtf8MetaEntry { source, path: entry.path().to_owned() })?;
384        if path.starts_with("meta/") {
385            for (i, _) in path.match_indices('/').skip(1) {
386                if meta_files.contains_key(&path[..i]) {
387                    return Err(Error::FileDirectoryCollision { path: path[..i].to_string() });
388                }
389            }
390            meta_files
391                .append_or_update(
392                    path,
393                    MetaFileLocation { offset: entry.offset(), length: entry.length() },
394                )
395                .expect("FAR entries are sorted");
396        }
397    }
398
399    let meta_contents_bytes =
400        reader.read_file(b"meta/contents").map_err(Error::ReadMetaContents)?;
401
402    let mut non_meta_files = MetaContents::deserialize(&meta_contents_bytes[..])
403        .map_err(Error::DeserializeMetaContents)?
404        .into_contents();
405
406    // We'll be keeping these packed files around for a while, so shrink to fit
407    // to avoid using too much memory.
408    non_meta_files.shrink_to_fit();
409
410    Ok((meta_files, non_meta_files))
411}
412
413/// Location of a meta file's contents within a meta.far
414#[derive(Clone, Copy, Debug, PartialEq, Eq)]
415pub(crate) struct MetaFileLocation {
416    offset: u64,
417    length: u64,
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use assert_matches::assert_matches;
424    use fidl::endpoints::create_proxy;
425    use fuchsia_fs::directory::{DirEntry, DirentKind};
426    use fuchsia_pkg_testing::PackageBuilder;
427    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
428    use futures::TryStreamExt as _;
429    use pretty_assertions::assert_eq;
430    use std::io::Cursor;
431
432    struct TestEnv {
433        _blobfs_fake: FakeBlobfs,
434        root_dir: Arc<RootDir<blobfs::Client>>,
435    }
436
437    impl TestEnv {
438        async fn with_subpackages(
439            subpackages_content: Option<&[u8]>,
440        ) -> (Self, Arc<RootDir<blobfs::Client>>) {
441            let mut pkg = PackageBuilder::new("base-package-0")
442                .add_resource_at("resource", "blob-contents".as_bytes())
443                .add_resource_at("dir/file", "bloblob".as_bytes())
444                .add_resource_at("meta/file", "meta-contents0".as_bytes())
445                .add_resource_at("meta/dir/file", "meta-contents1".as_bytes());
446            if let Some(subpackages_content) = subpackages_content {
447                pkg = pkg.add_resource_at(fuchsia_pkg::MetaSubpackages::PATH, subpackages_content);
448            }
449            let pkg = pkg.build().await.unwrap();
450            let (metafar_blob, content_blobs) = pkg.contents();
451            let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
452            blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
453            for (hash, bytes) in content_blobs {
454                blobfs_fake.add_blob(hash, bytes);
455            }
456
457            let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
458            (Self { _blobfs_fake: blobfs_fake, root_dir: root_dir.clone() }, root_dir)
459        }
460
461        async fn new() -> (Self, fio::DirectoryProxy) {
462            let (env, root) = Self::with_subpackages(None).await;
463            (env, vfs::directory::serve_read_only(root))
464        }
465    }
466
467    #[fuchsia::test]
468    async fn new_missing_meta_far_error() {
469        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
470        assert_matches!(
471            RootDir::new(blobfs_client, [0; 32].into()).await,
472            Err(Error::MissingMetaFar)
473        );
474    }
475
476    #[fuchsia::test]
477    async fn new_rejects_invalid_utf8() {
478        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
479        let mut meta_far = vec![];
480        let () = fuchsia_archive::write(
481            &mut meta_far,
482            std::collections::BTreeMap::from_iter([(
483                b"\xff",
484                (0, Box::new("".as_bytes()) as Box<dyn std::io::Read>),
485            )]),
486        )
487        .unwrap();
488        let hash = fuchsia_merkle::root_from_slice(&meta_far);
489        let () = blobfs_fake.add_blob(hash, meta_far);
490
491        assert_matches!(
492            RootDir::new(blobfs_client, hash).await,
493            Err(Error::NonUtf8MetaEntry{path, ..})
494                if path == vec![255]
495        );
496    }
497
498    #[fuchsia::test]
499    async fn new_initializes_maps() {
500        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
501
502        let meta_files = PackedMap::from([
503            ("meta/contents", MetaFileLocation { offset: 4096, length: 148 }),
504            ("meta/package", MetaFileLocation { offset: 20480, length: 39 }),
505            ("meta/file", MetaFileLocation { offset: 12288, length: 14 }),
506            ("meta/dir/file", MetaFileLocation { offset: 8192, length: 14 }),
507            ("meta/fuchsia.abi/abi-revision", MetaFileLocation { offset: 16384, length: 8 }),
508        ]);
509        assert_eq!(root_dir.meta_files, meta_files);
510
511        let non_meta_files = PackedMap::from([
512            (
513                "resource",
514                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15"
515                    .parse::<fuchsia_hash::Hash>()
516                    .unwrap(),
517            ),
518            (
519                "dir/file",
520                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201"
521                    .parse::<fuchsia_hash::Hash>()
522                    .unwrap(),
523            ),
524        ]);
525        assert_eq!(root_dir.non_meta_files, non_meta_files);
526    }
527
528    #[fuchsia::test]
529    async fn rejects_meta_file_collisions() {
530        let pkg = PackageBuilder::new("base-package-0")
531            .add_resource_at("meta/dir/file", "meta-contents0".as_bytes())
532            .build()
533            .await
534            .unwrap();
535
536        // Manually modify the meta.far to contain a "meta/dir" entry.
537        let (metafar_blob, _) = pkg.contents();
538        let mut metafar =
539            fuchsia_archive::Reader::new(Cursor::new(&metafar_blob.contents)).unwrap();
540        let mut entries = std::collections::BTreeMap::new();
541        let farentries =
542            metafar.list().map(|entry| (entry.path().to_vec(), entry.length())).collect::<Vec<_>>();
543        for (path, length) in farentries {
544            let contents = metafar.read_file(&path).unwrap();
545            entries
546                .insert(path, (length, Box::new(Cursor::new(contents)) as Box<dyn std::io::Read>));
547        }
548        let extra_contents = b"meta-contents1";
549        entries.insert(
550            b"meta/dir".to_vec(),
551            (
552                extra_contents.len() as u64,
553                Box::new(Cursor::new(extra_contents)) as Box<dyn std::io::Read>,
554            ),
555        );
556
557        let mut metafar: Vec<u8> = vec![];
558        let () = fuchsia_archive::write(&mut metafar, entries).unwrap();
559        let merkle = fuchsia_merkle::root_from_slice(&metafar);
560
561        // Verify it fails to load with the expected error.
562        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
563        blobfs_fake.add_blob(merkle, &metafar);
564
565        match RootDir::new(blobfs_client, merkle).await {
566            Ok(_) => panic!("this should not be reached!"),
567            Err(Error::FileDirectoryCollision { path }) => {
568                assert_eq!(path, "meta/dir".to_string());
569            }
570            Err(e) => panic!("Expected collision error, receieved {e:?}"),
571        };
572    }
573
574    #[fuchsia::test]
575    async fn read_file() {
576        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
577
578        assert_eq!(root_dir.read_file("resource").await.unwrap().as_slice(), b"blob-contents");
579        assert_eq!(root_dir.read_file("meta/file").await.unwrap().as_slice(), b"meta-contents0");
580        assert_matches!(
581            root_dir.read_file("missing").await.unwrap_err(),
582            ReadFileError::NoFileAtPath{path} if path == "missing"
583        );
584    }
585
586    #[fuchsia::test]
587    async fn has_file() {
588        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
589
590        assert!(root_dir.has_file("resource"));
591        assert!(root_dir.has_file("meta/file"));
592        assert_eq!(root_dir.has_file("missing"), false);
593    }
594
595    #[fuchsia::test]
596    async fn external_file_hashes() {
597        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
598
599        let mut actual = root_dir.external_file_hashes().copied().collect::<Vec<_>>();
600        actual.sort();
601        assert_eq!(
602            actual,
603            vec![
604                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201".parse().unwrap(),
605                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15".parse().unwrap()
606            ]
607        );
608    }
609
610    #[fuchsia::test]
611    async fn path() {
612        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
613
614        assert_eq!(
615            root_dir.path().await.unwrap(),
616            "base-package-0/0".parse::<fuchsia_pkg::PackagePath>().unwrap()
617        );
618    }
619
620    #[fuchsia::test]
621    async fn subpackages_present() {
622        let subpackages = fuchsia_pkg::MetaSubpackages::from_iter([(
623            fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
624            "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
625        )]);
626        let mut subpackages_bytes = vec![];
627        let () = subpackages.serialize(&mut subpackages_bytes).unwrap();
628        let (_env, root_dir) = TestEnv::with_subpackages(Some(&*subpackages_bytes)).await;
629
630        assert_eq!(root_dir.subpackages().await.unwrap(), subpackages);
631    }
632
633    #[fuchsia::test]
634    async fn subpackages_absent() {
635        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
636
637        assert_eq!(root_dir.subpackages().await.unwrap(), fuchsia_pkg::MetaSubpackages::default());
638    }
639
640    #[fuchsia::test]
641    async fn subpackages_error() {
642        let (_env, root_dir) = TestEnv::with_subpackages(Some(b"invalid-json")).await;
643
644        assert_matches!(root_dir.subpackages().await, Err(SubpackagesError::Parse(_)));
645    }
646
647    /// Ensure connections to a [`RootDir`] cannot be created as mutable (i.e. with
648    /// [`fio::PERM_WRITABLE`]). This ensures that the VFS will disallow any attempts to create a
649    /// new file/directory, modify the attributes of any nodes, open any files as writable.
650    #[fuchsia::test]
651    async fn root_dir_cannot_be_served_as_mutable() {
652        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
653        let proxy = vfs::directory::serve(root_dir, fio::PERM_WRITABLE);
654        assert_matches!(
655            proxy.take_event_stream().try_next().await,
656            Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
657        );
658    }
659
660    #[fuchsia::test]
661    async fn root_dir_readdir() {
662        let (_env, root_dir) = TestEnv::new().await;
663        assert_eq!(
664            fuchsia_fs::directory::readdir_inclusive(&root_dir).await.unwrap(),
665            vec![
666                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
667                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
668                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
669                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
670            ]
671        );
672    }
673
674    #[fuchsia::test]
675    async fn root_dir_get_attributes() {
676        let (_env, root_dir) = TestEnv::new().await;
677        let (mutable_attributes, immutable_attributes) =
678            root_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
679        assert_eq!(
680            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
681            immutable_attributes!(
682                fio::NodeAttributesQuery::all(),
683                Immutable {
684                    protocols: fio::NodeProtocolKinds::DIRECTORY,
685                    abilities: crate::DIRECTORY_ABILITIES,
686                    id: 1,
687                }
688            )
689        );
690    }
691
692    #[fuchsia::test]
693    async fn root_dir_watch_not_supported() {
694        let (_env, root_dir) = TestEnv::new().await;
695        let (_client, server) = fidl::endpoints::create_endpoints();
696        let status =
697            zx::Status::from_raw(root_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
698        assert_eq!(status, zx::Status::NOT_SUPPORTED);
699    }
700
701    #[fuchsia::test]
702    async fn root_dir_open_non_meta_file() {
703        let (_env, root_dir) = TestEnv::new().await;
704        let proxy = fuchsia_fs::directory::open_file(&root_dir, "resource", fio::PERM_READABLE)
705            .await
706            .unwrap();
707        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"blob-contents".to_vec());
708    }
709
710    #[fuchsia::test]
711    async fn root_dir_open_meta_as_file() {
712        let (env, root_dir) = TestEnv::new().await;
713        let proxy =
714            fuchsia_fs::directory::open_file(&root_dir, "meta", fio::PERM_READABLE).await.unwrap();
715        assert_eq!(
716            fuchsia_fs::file::read(&proxy).await.unwrap(),
717            env.root_dir.hash.to_string().as_bytes()
718        );
719        // Ensure the connection is cloned correctly (i.e. we don't get meta-as-dir).
720        let (cloned_proxy, server_end) = create_proxy::<fio::FileMarker>();
721        proxy.clone(server_end.into_channel().into()).unwrap();
722        assert_eq!(
723            fuchsia_fs::file::read(&cloned_proxy).await.unwrap(),
724            env.root_dir.hash.to_string().as_bytes()
725        );
726    }
727
728    #[fuchsia::test]
729    async fn root_dir_open_meta_as_dir() {
730        let (_env, root_dir) = TestEnv::new().await;
731        for path in ["meta", "meta/"] {
732            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
733                .await
734                .unwrap();
735            assert_eq!(
736                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
737                vec![
738                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
739                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
740                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
741                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
742                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
743                ]
744            );
745            // Ensure the connection is cloned correctly (i.e. we don't get meta-as-file).
746            let (cloned_proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
747            proxy.clone(server_end.into_channel().into()).unwrap();
748            assert_eq!(
749                fuchsia_fs::directory::readdir(&cloned_proxy).await.unwrap(),
750                vec![
751                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
752                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
753                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
754                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
755                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
756                ]
757            );
758        }
759    }
760
761    #[fuchsia::test]
762    async fn root_dir_open_meta_as_node() {
763        let (_env, root_dir) = TestEnv::new().await;
764        for path in ["meta", "meta/"] {
765            let proxy = fuchsia_fs::directory::open_node(
766                &root_dir,
767                path,
768                fio::Flags::PROTOCOL_NODE
769                    | fio::Flags::PROTOCOL_DIRECTORY
770                    | fio::Flags::PERM_GET_ATTRIBUTES,
771            )
772            .await
773            .unwrap();
774            let (mutable_attributes, immutable_attributes) = proxy
775                .get_attributes(
776                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
777                )
778                .await
779                .unwrap()
780                .unwrap();
781            assert_eq!(
782                fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
783                immutable_attributes!(
784                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
785                    Immutable {
786                        protocols: fio::NodeProtocolKinds::DIRECTORY,
787                        abilities: crate::DIRECTORY_ABILITIES
788                    }
789                )
790            );
791        }
792        // We should also be able to open the meta file as a node.
793        let proxy = fuchsia_fs::directory::open_node(
794            &root_dir,
795            "meta",
796            fio::Flags::PROTOCOL_NODE | fio::Flags::PERM_GET_ATTRIBUTES,
797        )
798        .await
799        .unwrap();
800        let (mutable_attributes, immutable_attributes) = proxy
801            .get_attributes(
802                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
803            )
804            .await
805            .unwrap()
806            .unwrap();
807        assert_eq!(
808            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
809            immutable_attributes!(
810                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
811                Immutable {
812                    protocols: fio::NodeProtocolKinds::FILE,
813                    abilities: fio::Abilities::READ_BYTES | fio::Abilities::GET_ATTRIBUTES,
814                }
815            )
816        );
817    }
818
819    #[fuchsia::test]
820    async fn root_dir_open_meta_file() {
821        let (_env, root_dir) = TestEnv::new().await;
822        let proxy = fuchsia_fs::directory::open_file(&root_dir, "meta/file", fio::PERM_READABLE)
823            .await
824            .unwrap();
825        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
826    }
827
828    #[fuchsia::test]
829    async fn root_dir_open_meta_subdir() {
830        let (_env, root_dir) = TestEnv::new().await;
831        for path in ["meta/dir", "meta/dir/"] {
832            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
833                .await
834                .unwrap();
835            assert_eq!(
836                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
837                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
838            );
839        }
840    }
841
842    #[fuchsia::test]
843    async fn root_dir_open_non_meta_subdir() {
844        let (_env, root_dir) = TestEnv::new().await;
845        for path in ["dir", "dir/"] {
846            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
847                .await
848                .unwrap();
849            assert_eq!(
850                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
851                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
852            );
853        }
854    }
855}