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