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::{usize_to_u64_safe, Error, NonMetaStorageError};
9use fidl::endpoints::ServerEnd;
10use fidl_fuchsia_io as fio;
11use fuchsia_pkg::MetaContents;
12use log::error;
13use std::collections::HashMap;
14use std::sync::Arc;
15use vfs::common::send_on_open_with_error;
16use vfs::directory::entry::{EntryInfo, OpenRequest};
17use vfs::directory::immutable::connection::ImmutableConnection;
18use vfs::directory::traversal_position::TraversalPosition;
19use vfs::execution_scope::ExecutionScope;
20use vfs::file::vmo::VmoFile;
21use vfs::{immutable_attributes, ObjectRequestRef, ProtocolsExt as _, ToObjectRequest as _};
22
23/// The root directory of Fuchsia package.
24#[derive(Debug)]
25pub struct RootDir<S> {
26    pub(crate) non_meta_storage: S,
27    pub(crate) hash: fuchsia_hash::Hash,
28    // The keys are object relative path expressions.
29    pub(crate) meta_files: HashMap<String, MetaFileLocation>,
30    // The keys are object relative path expressions.
31    pub(crate) non_meta_files: HashMap<String, fuchsia_hash::Hash>,
32    pub(crate) meta_far_vmo: zx::Vmo,
33    dropper: Option<Box<dyn crate::OnRootDirDrop>>,
34}
35
36impl<S: crate::NonMetaStorage> RootDir<S> {
37    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
38    /// representing the package, backed by `non_meta_storage`.
39    pub async fn new(non_meta_storage: S, hash: fuchsia_hash::Hash) -> Result<Arc<Self>, Error> {
40        Ok(Arc::new(Self::new_raw(non_meta_storage, hash, None).await?))
41    }
42
43    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
44    /// representing the package, backed by `non_meta_storage`.
45    /// Takes `dropper`, which will be dropped when the returned `RootDir` is dropped.
46    pub async fn new_with_dropper(
47        non_meta_storage: S,
48        hash: fuchsia_hash::Hash,
49        dropper: Box<dyn crate::OnRootDirDrop>,
50    ) -> Result<Arc<Self>, Error> {
51        Ok(Arc::new(Self::new_raw(non_meta_storage, hash, Some(dropper)).await?))
52    }
53
54    /// Loads the package metadata given by `hash` from `non_meta_storage`, returning an object
55    /// representing the package, backed by `non_meta_storage`.
56    /// Takes `dropper`, which will be dropped when the returned `RootDir` is dropped.
57    /// Like `new_with_dropper` except the returned `RootDir` is not in an `Arc`.
58    pub async fn new_raw(
59        non_meta_storage: S,
60        hash: fuchsia_hash::Hash,
61        dropper: Option<Box<dyn crate::OnRootDirDrop>>,
62    ) -> Result<Self, Error> {
63        let meta_far_vmo = non_meta_storage.get_blob_vmo(&hash).await.map_err(|e| {
64            if e.is_not_found_error() {
65                Error::MissingMetaFar
66            } else {
67                Error::OpenMetaFar(e)
68            }
69        })?;
70        let (meta_files, non_meta_files) = load_package_metadata(&meta_far_vmo)?;
71
72        Ok(RootDir { non_meta_storage, hash, meta_files, non_meta_files, meta_far_vmo, dropper })
73    }
74
75    /// Sets the dropper. If the dropper was already set, returns `dropper` in the error.
76    pub fn set_dropper(
77        &mut self,
78        dropper: Box<dyn crate::OnRootDirDrop>,
79    ) -> Result<(), Box<dyn crate::OnRootDirDrop>> {
80        match self.dropper {
81            Some(_) => Err(dropper),
82            None => {
83                self.dropper = Some(dropper);
84                Ok(())
85            }
86        }
87    }
88
89    /// Returns the contents, if present, of the file at object relative path expression `path`.
90    /// https://fuchsia.dev/fuchsia-src/concepts/process/namespaces?hl=en#object_relative_path_expressions
91    pub async fn read_file(&self, path: &str) -> Result<Vec<u8>, ReadFileError> {
92        if let Some(hash) = self.non_meta_files.get(path) {
93            self.non_meta_storage.read_blob(hash).await.map_err(ReadFileError::ReadBlob)
94        } else if let Some(location) = self.meta_files.get(path) {
95            self.meta_far_vmo
96                .read_to_vec(location.offset, location.length)
97                .map_err(ReadFileError::ReadMetaFile)
98        } else {
99            Err(ReadFileError::NoFileAtPath { path: path.to_string() })
100        }
101    }
102
103    /// Returns `true` iff there is a file at `path`, an object relative path expression.
104    /// https://fuchsia.dev/fuchsia-src/concepts/process/namespaces?hl=en#object_relative_path_expressions
105    pub fn has_file(&self, path: &str) -> bool {
106        self.non_meta_files.contains_key(path) || self.meta_files.contains_key(path)
107    }
108
109    /// Returns the hash of the package.
110    pub fn hash(&self) -> &fuchsia_hash::Hash {
111        &self.hash
112    }
113
114    /// Returns an iterator of the hashes of files stored externally to the package meta.far.
115    /// May return duplicates.
116    pub fn external_file_hashes(&self) -> impl ExactSizeIterator<Item = &fuchsia_hash::Hash> {
117        self.non_meta_files.values()
118    }
119
120    /// Returns the path of the package as indicated by the "meta/package" file.
121    pub async fn path(&self) -> Result<fuchsia_pkg::PackagePath, PathError> {
122        Ok(fuchsia_pkg::MetaPackage::deserialize(&self.read_file("meta/package").await?[..])?
123            .into_path())
124    }
125
126    /// Returns the subpackages of the package.
127    pub async fn subpackages(&self) -> Result<fuchsia_pkg::MetaSubpackages, SubpackagesError> {
128        let contents = match self.read_file(fuchsia_pkg::MetaSubpackages::PATH).await {
129            Ok(contents) => contents,
130            Err(ReadFileError::NoFileAtPath { .. }) => {
131                return Ok(fuchsia_pkg::MetaSubpackages::default())
132            }
133            Err(e) => Err(e)?,
134        };
135
136        Ok(fuchsia_pkg::MetaSubpackages::deserialize(&*contents)?)
137    }
138
139    /// Creates a file that contains the package's hash.
140    fn create_meta_as_file(&self) -> Result<Arc<VmoFile>, zx::Status> {
141        let file_contents = self.hash.to_string();
142        let vmo = zx::Vmo::create(usize_to_u64_safe(file_contents.len()))?;
143        let () = vmo.write(file_contents.as_bytes(), 0)?;
144        Ok(VmoFile::new_with_inode(
145            vmo, /*readable*/ true, /*writable*/ false, /*executable*/ false,
146            /*inode*/ 1,
147        ))
148    }
149
150    /// Creates and returns a meta file if one exists at `path`.
151    pub(crate) fn get_meta_file(&self, path: &str) -> Result<Option<Arc<VmoFile>>, zx::Status> {
152        // The FAR spec requires 4 KiB alignment of content chunks [1], so offset will
153        // always be page-aligned, because pages are required [2] to be a power of 2 and at
154        // least 4 KiB.
155        // [1] https://fuchsia.dev/fuchsia-src/concepts/source_code/archive_format#content_chunk
156        // [2] https://fuchsia.dev/fuchsia-src/reference/syscalls/system_get_page_size
157        // TODO(https://fxbug.dev/42162525) Need to manually zero the end of the VMO if
158        // zx_system_get_page_size() > 4K.
159        assert_eq!(zx::system_get_page_size(), 4096);
160
161        let location = match self.meta_files.get(path) {
162            Some(location) => location,
163            None => return Ok(None),
164        };
165        let vmo = self
166            .meta_far_vmo
167            .create_child(
168                zx::VmoChildOptions::SNAPSHOT_AT_LEAST_ON_WRITE | zx::VmoChildOptions::NO_WRITE,
169                location.offset,
170                location.length,
171            )
172            .map_err(|e| {
173                error!("Error creating child vmo for meta file {:?}", e);
174                zx::Status::INTERNAL
175            })?;
176
177        Ok(Some(VmoFile::new_with_inode(
178            vmo, /*readable*/ true, /*writable*/ false, /*executable*/ false,
179            /*inode*/ 1,
180        )))
181    }
182
183    /// Creates and returns a `MetaSubdir` if one exists at `path`. `path` must end in '/'.
184    pub(crate) fn get_meta_subdir(self: &Arc<Self>, path: String) -> Option<Arc<MetaSubdir<S>>> {
185        debug_assert!(path.ends_with("/"));
186        for k in self.meta_files.keys() {
187            if k.starts_with(&path) {
188                return Some(MetaSubdir::new(self.clone(), path));
189            }
190        }
191        None
192    }
193
194    /// Creates and returns a `NonMetaSubdir` if one exists at `path`. `path` must end in '/'.
195    pub(crate) fn get_non_meta_subdir(
196        self: &Arc<Self>,
197        path: String,
198    ) -> Option<Arc<NonMetaSubdir<S>>> {
199        debug_assert!(path.ends_with("/"));
200        for k in self.non_meta_files.keys() {
201            if k.starts_with(&path) {
202                return Some(NonMetaSubdir::new(self.clone(), path));
203            }
204        }
205        None
206    }
207}
208
209#[derive(thiserror::Error, Debug)]
210pub enum ReadFileError {
211    #[error("reading blob")]
212    ReadBlob(#[source] NonMetaStorageError),
213
214    #[error("reading meta file")]
215    ReadMetaFile(#[source] zx::Status),
216
217    #[error("no file exists at path: {path:?}")]
218    NoFileAtPath { path: String },
219}
220
221#[derive(thiserror::Error, Debug)]
222pub enum SubpackagesError {
223    #[error("reading manifest")]
224    Read(#[from] ReadFileError),
225
226    #[error("parsing manifest")]
227    Parse(#[from] fuchsia_pkg::MetaSubpackagesError),
228}
229
230#[derive(thiserror::Error, Debug)]
231pub enum PathError {
232    #[error("reading meta/package")]
233    Read(#[from] ReadFileError),
234
235    #[error("parsing meta/package")]
236    Parse(#[from] fuchsia_pkg::MetaPackageError),
237}
238
239impl<S: crate::NonMetaStorage> vfs::directory::entry::DirectoryEntry for RootDir<S> {
240    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), zx::Status> {
241        request.open_dir(self)
242    }
243}
244
245impl<S: crate::NonMetaStorage> vfs::directory::entry::GetEntryInfo for RootDir<S> {
246    fn entry_info(&self) -> EntryInfo {
247        EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
248    }
249}
250
251impl<S: crate::NonMetaStorage> vfs::node::Node for RootDir<S> {
252    async fn get_attributes(
253        &self,
254        requested_attributes: fio::NodeAttributesQuery,
255    ) -> Result<fio::NodeAttributes2, zx::Status> {
256        Ok(immutable_attributes!(
257            requested_attributes,
258            Immutable {
259                protocols: fio::NodeProtocolKinds::DIRECTORY,
260                abilities: crate::DIRECTORY_ABILITIES,
261                id: 1,
262            }
263        ))
264    }
265}
266
267impl<S: crate::NonMetaStorage> vfs::directory::entry_container::Directory for RootDir<S> {
268    fn deprecated_open(
269        self: Arc<Self>,
270        scope: ExecutionScope,
271        flags: fio::OpenFlags,
272        path: vfs::Path,
273        server_end: ServerEnd<fio::NodeMarker>,
274    ) {
275        let flags = flags & !fio::OpenFlags::POSIX_WRITABLE;
276        let describe = flags.contains(fio::OpenFlags::DESCRIBE);
277        // Disallow creating a writable connection to this node or any children. We also disallow
278        // file flags which do not apply. Note that the latter is not required for Open3, as we
279        // require writable rights for the latter flags already.
280        if flags.intersects(fio::OpenFlags::RIGHT_WRITABLE | fio::OpenFlags::TRUNCATE) {
281            let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_SUPPORTED);
282            return;
283        }
284        // The VFS should disallow file creation since we cannot serve a mutable connection.
285        assert!(!flags.intersects(fio::OpenFlags::CREATE | fio::OpenFlags::CREATE_IF_ABSENT));
286
287        // Handle case where the request is for this directory itself (e.g. ".").
288        if path.is_empty() {
289            flags.to_object_request(server_end).handle(|object_request| {
290                // NOTE: Some older CTF tests still rely on being able to use the APPEND flag in
291                // some cases, so we cannot check this flag above. Appending is still not possible.
292                // As we plan to remove this method entirely, we can just leave this for now.
293                if flags.intersects(fio::OpenFlags::APPEND) {
294                    return Err(zx::Status::NOT_SUPPORTED);
295                }
296                object_request
297                    .take()
298                    .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
299                Ok(())
300            });
301            return;
302        }
303
304        // `path` is relative, and may include a trailing slash.
305        let canonical_path = path.as_ref().strip_suffix('/').unwrap_or_else(|| path.as_ref());
306
307        if canonical_path == "meta" {
308            // This branch is done here instead of in MetaAsDir so that Clone'ing MetaAsDir yields
309            // MetaAsDir. See the MetaAsDir::open impl for more.
310
311            // To remain POSIX compliant, we must default to opening meta as a file unless the
312            // DIRECTORY flag (which maps to O_DIRECTORY) is specified. Otherwise, it would be
313            // impossible to open as a directory, as there is no POSIX equivalent for NOT_DIRECTORY.
314            let open_meta_as_file =
315                !flags.intersects(fio::OpenFlags::DIRECTORY | fio::OpenFlags::NODE_REFERENCE);
316            if open_meta_as_file {
317                flags.to_object_request(server_end).handle(|object_request| {
318                    let file = self.create_meta_as_file().map_err(|e| {
319                        error!("Error creating the meta file: {:?}", e);
320                        zx::Status::INTERNAL
321                    })?;
322                    vfs::file::serve(file, scope, &flags, object_request)
323                });
324            } else {
325                let () = MetaAsDir::new(self).deprecated_open(
326                    scope,
327                    flags,
328                    vfs::Path::dot(),
329                    server_end,
330                );
331            }
332            return;
333        }
334
335        if canonical_path.starts_with("meta/") {
336            match self.get_meta_file(canonical_path) {
337                Ok(Some(meta_file)) => {
338                    flags.to_object_request(server_end).handle(|object_request| {
339                        vfs::file::serve(meta_file, scope, &flags, object_request)
340                    });
341                    return;
342                }
343                Ok(None) => {}
344                Err(status) => {
345                    let () = send_on_open_with_error(describe, server_end, status);
346                    return;
347                }
348            }
349
350            if let Some(subdir) = self.get_meta_subdir(canonical_path.to_string() + "/") {
351                let () = subdir.deprecated_open(scope, flags, vfs::Path::dot(), server_end);
352                return;
353            }
354
355            let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_FOUND);
356            return;
357        }
358
359        if let Some(blob) = self.non_meta_files.get(canonical_path) {
360            let () = self
361                .non_meta_storage
362                .deprecated_open(blob, flags, scope, server_end)
363                .unwrap_or_else(|e| {
364                    error!("Error forwarding content blob open to blobfs: {:#}", anyhow::anyhow!(e))
365                });
366            return;
367        }
368
369        if let Some(subdir) = self.get_non_meta_subdir(canonical_path.to_string() + "/") {
370            let () = subdir.deprecated_open(scope, flags, vfs::Path::dot(), server_end);
371            return;
372        }
373
374        let () = send_on_open_with_error(describe, server_end, zx::Status::NOT_FOUND);
375    }
376
377    fn open(
378        self: Arc<Self>,
379        scope: ExecutionScope,
380        path: vfs::Path,
381        flags: fio::Flags,
382        object_request: ObjectRequestRef<'_>,
383    ) -> Result<(), zx::Status> {
384        if !flags.difference(crate::ALLOWED_FLAGS).is_empty() {
385            return Err(zx::Status::NOT_SUPPORTED);
386        }
387
388        // Handle case where the request is for this directory itself (e.g. ".").
389        if path.is_empty() {
390            // `ImmutableConnection` checks that only directory flags are specified.
391            object_request
392                .take()
393                .create_connection_sync::<ImmutableConnection<_>, _>(scope, self, flags);
394            return Ok(());
395        }
396
397        // `path` is relative, and may include a trailing slash.
398        let canonical_path = path.as_ref().strip_suffix('/').unwrap_or_else(|| path.as_ref());
399
400        if canonical_path == "meta" {
401            // This branch is done here instead of in MetaAsDir so that Clone'ing MetaAsDir yields
402            // MetaAsDir. See the MetaAsDir::open impl for more.
403
404            // TODO(https://fxbug.dev/328485661): consider retrieving the merkle root by retrieving
405            // the attribute instead of opening as a file to read the merkle root content.
406
407            // To remain POSIX compliant, we must default to opening meta as a file unless the
408            // directory protocol (i.e. O_DIRECTORY) is explicitly requested.
409            let open_meta_as_dir = flags.is_dir_allowed() && !flags.is_file_allowed();
410            if !open_meta_as_dir {
411                if path.is_dir() {
412                    return Err(zx::Status::NOT_DIR);
413                }
414                let file = self.create_meta_as_file().map_err(|e| {
415                    error!("Error creating the meta file: {:?}", e);
416                    zx::Status::INTERNAL
417                })?;
418                return vfs::file::serve(file, scope, &flags, object_request);
419            }
420            return MetaAsDir::new(self).open(scope, vfs::Path::dot(), flags, object_request);
421        }
422
423        if canonical_path.starts_with("meta/") {
424            if let Some(file) = self.get_meta_file(canonical_path)? {
425                if path.is_dir() {
426                    return Err(zx::Status::NOT_DIR);
427                }
428                return vfs::file::serve(file, scope, &flags, object_request);
429            }
430
431            if let Some(subdir) = self.get_meta_subdir(canonical_path.to_string() + "/") {
432                return subdir.open(scope, vfs::Path::dot(), flags, object_request);
433            }
434            return Err(zx::Status::NOT_FOUND);
435        }
436
437        if let Some(blob) = self.non_meta_files.get(canonical_path) {
438            if path.is_dir() {
439                return Err(zx::Status::NOT_DIR);
440            }
441            return self.non_meta_storage.open(blob, flags, scope, object_request);
442        }
443
444        if let Some(subdir) = self.get_non_meta_subdir(canonical_path.to_string() + "/") {
445            return subdir.open(scope, vfs::Path::dot(), flags, object_request);
446        }
447
448        Err(zx::Status::NOT_FOUND)
449    }
450
451    async fn read_dirents<'a>(
452        &'a self,
453        pos: &'a TraversalPosition,
454        sink: Box<(dyn vfs::directory::dirents_sink::Sink + 'static)>,
455    ) -> Result<
456        (TraversalPosition, Box<(dyn vfs::directory::dirents_sink::Sealed + 'static)>),
457        zx::Status,
458    > {
459        vfs::directory::read_dirents::read_dirents(
460            // Add "meta/placeholder" file so the "meta" dir is included in the results
461            &crate::get_dir_children(
462                self.non_meta_files.keys().map(|s| s.as_str()).chain(["meta/placeholder"]),
463                "",
464            ),
465            pos,
466            sink,
467        )
468        .await
469    }
470
471    fn register_watcher(
472        self: Arc<Self>,
473        _: ExecutionScope,
474        _: fio::WatchMask,
475        _: vfs::directory::entry_container::DirectoryWatcher,
476    ) -> Result<(), zx::Status> {
477        Err(zx::Status::NOT_SUPPORTED)
478    }
479
480    // `register_watcher` is unsupported so no need to do anything here.
481    fn unregister_watcher(self: Arc<Self>, _: usize) {}
482}
483
484#[allow(clippy::type_complexity)]
485fn load_package_metadata(
486    meta_far_vmo: &zx::Vmo,
487) -> Result<(HashMap<String, MetaFileLocation>, HashMap<String, fuchsia_hash::Hash>), Error> {
488    let stream =
489        zx::Stream::create(zx::StreamOptions::MODE_READ, meta_far_vmo, 0).map_err(|e| {
490            Error::OpenMetaFar(NonMetaStorageError::ReadBlob(
491                fuchsia_fs::file::ReadError::ReadError(e),
492            ))
493        })?;
494
495    let mut reader = fuchsia_archive::Reader::new(stream).map_err(Error::ArchiveReader)?;
496    let reader_list = reader.list();
497    let mut meta_files = HashMap::with_capacity(reader_list.len());
498    for entry in reader_list {
499        let path = std::str::from_utf8(entry.path())
500            .map_err(|source| Error::NonUtf8MetaEntry { source, path: entry.path().to_owned() })?
501            .to_owned();
502        if path.starts_with("meta/") {
503            for (i, _) in path.match_indices('/').skip(1) {
504                if meta_files.contains_key(&path[..i]) {
505                    return Err(Error::FileDirectoryCollision { path: path[..i].to_string() });
506                }
507            }
508            meta_files
509                .insert(path, MetaFileLocation { offset: entry.offset(), length: entry.length() });
510        }
511    }
512
513    let meta_contents_bytes =
514        reader.read_file(b"meta/contents").map_err(Error::ReadMetaContents)?;
515
516    let non_meta_files = MetaContents::deserialize(&meta_contents_bytes[..])
517        .map_err(Error::DeserializeMetaContents)?
518        .into_contents();
519
520    Ok((meta_files, non_meta_files))
521}
522
523/// Location of a meta file's contents within a meta.far
524#[derive(Clone, Copy, Debug, PartialEq, Eq)]
525pub(crate) struct MetaFileLocation {
526    offset: u64,
527    length: u64,
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533    use assert_matches::assert_matches;
534    use fidl::endpoints::{create_proxy, Proxy as _};
535    use fuchsia_fs::directory::{DirEntry, DirentKind};
536    use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
537    use fuchsia_pkg_testing::PackageBuilder;
538    use futures::TryStreamExt as _;
539    use pretty_assertions::assert_eq;
540    use std::io::Cursor;
541
542    struct TestEnv {
543        _blobfs_fake: FakeBlobfs,
544        root_dir: Arc<RootDir<blobfs::Client>>,
545    }
546
547    impl TestEnv {
548        async fn with_subpackages(
549            subpackages_content: Option<&[u8]>,
550        ) -> (Self, Arc<RootDir<blobfs::Client>>) {
551            let mut pkg = PackageBuilder::new("base-package-0")
552                .add_resource_at("resource", "blob-contents".as_bytes())
553                .add_resource_at("dir/file", "bloblob".as_bytes())
554                .add_resource_at("meta/file", "meta-contents0".as_bytes())
555                .add_resource_at("meta/dir/file", "meta-contents1".as_bytes());
556            if let Some(subpackages_content) = subpackages_content {
557                pkg = pkg.add_resource_at(fuchsia_pkg::MetaSubpackages::PATH, subpackages_content);
558            }
559            let pkg = pkg.build().await.unwrap();
560            let (metafar_blob, content_blobs) = pkg.contents();
561            let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
562            blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
563            for (hash, bytes) in content_blobs {
564                blobfs_fake.add_blob(hash, bytes);
565            }
566
567            let root_dir = RootDir::new(blobfs_client, metafar_blob.merkle).await.unwrap();
568            (Self { _blobfs_fake: blobfs_fake, root_dir: root_dir.clone() }, root_dir)
569        }
570
571        async fn new() -> (Self, fio::DirectoryProxy) {
572            let (env, root) = Self::with_subpackages(None).await;
573            (env, vfs::directory::serve_read_only(root))
574        }
575    }
576
577    #[fuchsia_async::run_singlethreaded(test)]
578    async fn new_missing_meta_far_error() {
579        let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
580        assert_matches!(
581            RootDir::new(blobfs_client, [0; 32].into()).await,
582            Err(Error::MissingMetaFar)
583        );
584    }
585
586    #[fuchsia_async::run_singlethreaded(test)]
587    async fn new_rejects_invalid_utf8() {
588        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
589        let mut meta_far = vec![];
590        let () = fuchsia_archive::write(
591            &mut meta_far,
592            std::collections::BTreeMap::from_iter([(
593                b"\xff",
594                (0, Box::new("".as_bytes()) as Box<dyn std::io::Read>),
595            )]),
596        )
597        .unwrap();
598        let hash = fuchsia_merkle::from_slice(&meta_far).root();
599        let () = blobfs_fake.add_blob(hash, meta_far);
600
601        assert_matches!(
602            RootDir::new(blobfs_client, hash).await,
603            Err(Error::NonUtf8MetaEntry{path, ..})
604                if path == vec![255]
605        );
606    }
607
608    #[fuchsia_async::run_singlethreaded(test)]
609    async fn new_initializes_maps() {
610        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
611
612        let meta_files = HashMap::from([
613            (String::from("meta/contents"), MetaFileLocation { offset: 4096, length: 148 }),
614            (String::from("meta/package"), MetaFileLocation { offset: 20480, length: 39 }),
615            (String::from("meta/file"), MetaFileLocation { offset: 12288, length: 14 }),
616            (String::from("meta/dir/file"), MetaFileLocation { offset: 8192, length: 14 }),
617            (
618                String::from("meta/fuchsia.abi/abi-revision"),
619                MetaFileLocation { offset: 16384, length: 8 },
620            ),
621        ]);
622        assert_eq!(root_dir.meta_files, meta_files);
623
624        let non_meta_files: HashMap<String, fuchsia_hash::Hash> = [
625            (
626                String::from("resource"),
627                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15"
628                    .parse::<fuchsia_hash::Hash>()
629                    .unwrap(),
630            ),
631            (
632                String::from("dir/file"),
633                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201"
634                    .parse::<fuchsia_hash::Hash>()
635                    .unwrap(),
636            ),
637        ]
638        .iter()
639        .cloned()
640        .collect();
641        assert_eq!(root_dir.non_meta_files, non_meta_files);
642    }
643
644    #[fuchsia_async::run_singlethreaded(test)]
645    async fn rejects_meta_file_collisions() {
646        let pkg = PackageBuilder::new("base-package-0")
647            .add_resource_at("meta/dir/file", "meta-contents0".as_bytes())
648            .build()
649            .await
650            .unwrap();
651
652        // Manually modify the meta.far to contain a "meta/dir" entry.
653        let (metafar_blob, _) = pkg.contents();
654        let mut metafar =
655            fuchsia_archive::Reader::new(Cursor::new(&metafar_blob.contents)).unwrap();
656        let mut entries = std::collections::BTreeMap::new();
657        let farentries =
658            metafar.list().map(|entry| (entry.path().to_vec(), entry.length())).collect::<Vec<_>>();
659        for (path, length) in farentries {
660            let contents = metafar.read_file(&path).unwrap();
661            entries
662                .insert(path, (length, Box::new(Cursor::new(contents)) as Box<dyn std::io::Read>));
663        }
664        let extra_contents = b"meta-contents1";
665        entries.insert(
666            b"meta/dir".to_vec(),
667            (
668                extra_contents.len() as u64,
669                Box::new(Cursor::new(extra_contents)) as Box<dyn std::io::Read>,
670            ),
671        );
672
673        let mut metafar: Vec<u8> = vec![];
674        let () = fuchsia_archive::write(&mut metafar, entries).unwrap();
675        let merkle = fuchsia_merkle::from_slice(&metafar).root();
676
677        // Verify it fails to load with the expected error.
678        let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
679        blobfs_fake.add_blob(merkle, &metafar);
680
681        match RootDir::new(blobfs_client, merkle).await {
682            Ok(_) => panic!("this should not be reached!"),
683            Err(Error::FileDirectoryCollision { path }) => {
684                assert_eq!(path, "meta/dir".to_string());
685            }
686            Err(e) => panic!("Expected collision error, receieved {e:?}"),
687        };
688    }
689
690    #[fuchsia_async::run_singlethreaded(test)]
691    async fn read_file() {
692        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
693
694        assert_eq!(root_dir.read_file("resource").await.unwrap().as_slice(), b"blob-contents");
695        assert_eq!(root_dir.read_file("meta/file").await.unwrap().as_slice(), b"meta-contents0");
696        assert_matches!(
697            root_dir.read_file("missing").await.unwrap_err(),
698            ReadFileError::NoFileAtPath{path} if path == "missing"
699        );
700    }
701
702    #[fuchsia_async::run_singlethreaded(test)]
703    async fn has_file() {
704        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
705
706        assert!(root_dir.has_file("resource"));
707        assert!(root_dir.has_file("meta/file"));
708        assert_eq!(root_dir.has_file("missing"), false);
709    }
710
711    #[fuchsia_async::run_singlethreaded(test)]
712    async fn external_file_hashes() {
713        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
714
715        let mut actual = root_dir.external_file_hashes().copied().collect::<Vec<_>>();
716        actual.sort();
717        assert_eq!(
718            actual,
719            vec![
720                "5f615dd575994fcbcc174974311d59de258d93cd523d5cb51f0e139b53c33201".parse().unwrap(),
721                "bd905f783ceae4c5ba8319703d7505ab363733c2db04c52c8405603a02922b15".parse().unwrap()
722            ]
723        );
724    }
725
726    #[fuchsia_async::run_singlethreaded(test)]
727    async fn path() {
728        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
729
730        assert_eq!(
731            root_dir.path().await.unwrap(),
732            "base-package-0/0".parse::<fuchsia_pkg::PackagePath>().unwrap()
733        );
734    }
735
736    #[fuchsia_async::run_singlethreaded(test)]
737    async fn subpackages_present() {
738        let subpackages = fuchsia_pkg::MetaSubpackages::from_iter([(
739            fuchsia_url::RelativePackageUrl::parse("subpackage-name").unwrap(),
740            "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap(),
741        )]);
742        let mut subpackages_bytes = vec![];
743        let () = subpackages.serialize(&mut subpackages_bytes).unwrap();
744        let (_env, root_dir) = TestEnv::with_subpackages(Some(&*subpackages_bytes)).await;
745
746        assert_eq!(root_dir.subpackages().await.unwrap(), subpackages);
747    }
748
749    #[fuchsia_async::run_singlethreaded(test)]
750    async fn subpackages_absent() {
751        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
752
753        assert_eq!(root_dir.subpackages().await.unwrap(), fuchsia_pkg::MetaSubpackages::default());
754    }
755
756    #[fuchsia_async::run_singlethreaded(test)]
757    async fn subpackages_error() {
758        let (_env, root_dir) = TestEnv::with_subpackages(Some(b"invalid-json")).await;
759
760        assert_matches!(root_dir.subpackages().await, Err(SubpackagesError::Parse(_)));
761    }
762
763    /// Ensure connections to a [`RootDir`] cannot be created as mutable (i.e. with
764    /// [`fio::PERM_WRITABLE`]). This ensures that the VFS will disallow any attempts to create a
765    /// new file/directory, modify the attributes of any nodes, open any files as writable.
766    #[fuchsia_async::run_singlethreaded(test)]
767    async fn root_dir_cannot_be_served_as_mutable() {
768        let (_env, root_dir) = TestEnv::with_subpackages(None).await;
769        let proxy = vfs::directory::serve(root_dir, fio::PERM_WRITABLE);
770        assert_matches!(
771            proxy.take_event_stream().try_next().await,
772            Err(fidl::Error::ClientChannelClosed { status: zx::Status::NOT_SUPPORTED, .. })
773        );
774    }
775
776    #[fuchsia_async::run_singlethreaded(test)]
777    async fn root_dir_readdir() {
778        let (_env, root_dir) = TestEnv::new().await;
779        assert_eq!(
780            fuchsia_fs::directory::readdir_inclusive(&root_dir).await.unwrap(),
781            vec![
782                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
783                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
784                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
785                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
786            ]
787        );
788    }
789
790    #[fuchsia_async::run_singlethreaded(test)]
791    async fn root_dir_get_attributes() {
792        let (_env, root_dir) = TestEnv::new().await;
793        let (mutable_attributes, immutable_attributes) =
794            root_dir.get_attributes(fio::NodeAttributesQuery::all()).await.unwrap().unwrap();
795        assert_eq!(
796            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
797            immutable_attributes!(
798                fio::NodeAttributesQuery::all(),
799                Immutable {
800                    protocols: fio::NodeProtocolKinds::DIRECTORY,
801                    abilities: crate::DIRECTORY_ABILITIES,
802                    id: 1,
803                }
804            )
805        );
806    }
807
808    #[fuchsia_async::run_singlethreaded(test)]
809    async fn root_dir_watch_not_supported() {
810        let (_env, root_dir) = TestEnv::new().await;
811        let (_client, server) = fidl::endpoints::create_endpoints();
812        let status =
813            zx::Status::from_raw(root_dir.watch(fio::WatchMask::empty(), 0, server).await.unwrap());
814        assert_eq!(status, zx::Status::NOT_SUPPORTED);
815    }
816
817    #[fuchsia_async::run_singlethreaded(test)]
818    async fn root_dir_open_non_meta_file() {
819        let (_env, root_dir) = TestEnv::new().await;
820        let proxy = fuchsia_fs::directory::open_file(&root_dir, "resource", fio::PERM_READABLE)
821            .await
822            .unwrap();
823        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"blob-contents".to_vec());
824    }
825
826    #[fuchsia_async::run_singlethreaded(test)]
827    async fn root_dir_open_meta_as_file() {
828        let (env, root_dir) = TestEnv::new().await;
829        let proxy =
830            fuchsia_fs::directory::open_file(&root_dir, "meta", fio::PERM_READABLE).await.unwrap();
831        assert_eq!(
832            fuchsia_fs::file::read(&proxy).await.unwrap(),
833            env.root_dir.hash.to_string().as_bytes()
834        );
835        // Ensure the connection is cloned correctly (i.e. we don't get meta-as-dir).
836        let (cloned_proxy, server_end) = create_proxy::<fio::FileMarker>();
837        proxy.clone(server_end.into_channel().into()).unwrap();
838        assert_eq!(
839            fuchsia_fs::file::read(&cloned_proxy).await.unwrap(),
840            env.root_dir.hash.to_string().as_bytes()
841        );
842    }
843
844    #[fuchsia_async::run_singlethreaded(test)]
845    async fn root_dir_open_meta_as_dir() {
846        let (_env, root_dir) = TestEnv::new().await;
847        for path in ["meta", "meta/"] {
848            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
849                .await
850                .unwrap();
851            assert_eq!(
852                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
853                vec![
854                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
855                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
856                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
857                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
858                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
859                ]
860            );
861            // Ensure the connection is cloned correctly (i.e. we don't get meta-as-file).
862            let (cloned_proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
863            proxy.clone(server_end.into_channel().into()).unwrap();
864            assert_eq!(
865                fuchsia_fs::directory::readdir(&cloned_proxy).await.unwrap(),
866                vec![
867                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
868                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
869                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
870                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
871                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
872                ]
873            );
874        }
875    }
876
877    #[fuchsia_async::run_singlethreaded(test)]
878    async fn root_dir_open_meta_as_node() {
879        let (_env, root_dir) = TestEnv::new().await;
880        for path in ["meta", "meta/"] {
881            let proxy = fuchsia_fs::directory::open_node(
882                &root_dir,
883                path,
884                fio::Flags::PROTOCOL_NODE
885                    | fio::Flags::PROTOCOL_DIRECTORY
886                    | fio::Flags::PERM_GET_ATTRIBUTES,
887            )
888            .await
889            .unwrap();
890            let (mutable_attributes, immutable_attributes) = proxy
891                .get_attributes(
892                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
893                )
894                .await
895                .unwrap()
896                .unwrap();
897            assert_eq!(
898                fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
899                immutable_attributes!(
900                    fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
901                    Immutable {
902                        protocols: fio::NodeProtocolKinds::DIRECTORY,
903                        abilities: crate::DIRECTORY_ABILITIES
904                    }
905                )
906            );
907        }
908        // We should also be able to open the meta file as a node.
909        let proxy = fuchsia_fs::directory::open_node(
910            &root_dir,
911            "meta",
912            fio::Flags::PROTOCOL_NODE | fio::Flags::PERM_GET_ATTRIBUTES,
913        )
914        .await
915        .unwrap();
916        let (mutable_attributes, immutable_attributes) = proxy
917            .get_attributes(
918                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
919            )
920            .await
921            .unwrap()
922            .unwrap();
923        assert_eq!(
924            fio::NodeAttributes2 { mutable_attributes, immutable_attributes },
925            immutable_attributes!(
926                fio::NodeAttributesQuery::PROTOCOLS | fio::NodeAttributesQuery::ABILITIES,
927                Immutable {
928                    protocols: fio::NodeProtocolKinds::FILE,
929                    abilities: fio::Abilities::READ_BYTES | fio::Abilities::GET_ATTRIBUTES,
930                }
931            )
932        );
933    }
934
935    #[fuchsia_async::run_singlethreaded(test)]
936    async fn root_dir_open_meta_file() {
937        let (_env, root_dir) = TestEnv::new().await;
938        let proxy = fuchsia_fs::directory::open_file(&root_dir, "meta/file", fio::PERM_READABLE)
939            .await
940            .unwrap();
941        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
942    }
943
944    #[fuchsia_async::run_singlethreaded(test)]
945    async fn root_dir_open_meta_subdir() {
946        let (_env, root_dir) = TestEnv::new().await;
947        for path in ["meta/dir", "meta/dir/"] {
948            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
949                .await
950                .unwrap();
951            assert_eq!(
952                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
953                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
954            );
955        }
956    }
957
958    #[fuchsia_async::run_singlethreaded(test)]
959    async fn root_dir_open_non_meta_subdir() {
960        let (_env, root_dir) = TestEnv::new().await;
961        for path in ["dir", "dir/"] {
962            let proxy = fuchsia_fs::directory::open_directory(&root_dir, path, fio::PERM_READABLE)
963                .await
964                .unwrap();
965            assert_eq!(
966                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
967                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
968            );
969        }
970    }
971
972    #[fuchsia_async::run_singlethreaded(test)]
973    async fn root_dir_deprecated_open_self() {
974        let (_env, root_dir) = TestEnv::new().await;
975        let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
976        root_dir
977            .deprecated_open(
978                fio::OpenFlags::RIGHT_READABLE,
979                Default::default(),
980                ".",
981                server_end.into_channel().into(),
982            )
983            .unwrap();
984        assert_eq!(
985            fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
986            vec![
987                DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
988                DirEntry { name: "meta".to_string(), kind: DirentKind::Directory },
989                DirEntry { name: "resource".to_string(), kind: DirentKind::File }
990            ]
991        );
992    }
993
994    #[fuchsia_async::run_singlethreaded(test)]
995    async fn root_dir_deprecated_open_non_meta_file() {
996        let (_env, root_dir) = TestEnv::new().await;
997        let (proxy, server_end) = create_proxy();
998        root_dir
999            .deprecated_open(
1000                fio::OpenFlags::RIGHT_READABLE,
1001                Default::default(),
1002                "resource",
1003                server_end,
1004            )
1005            .unwrap();
1006        assert_eq!(
1007            fuchsia_fs::file::read(&fio::FileProxy::from_channel(proxy.into_channel().unwrap()))
1008                .await
1009                .unwrap(),
1010            b"blob-contents".to_vec()
1011        );
1012    }
1013
1014    #[fuchsia_async::run_singlethreaded(test)]
1015    async fn root_dir_deprecated_open_meta_as_file() {
1016        let (env, root_dir) = TestEnv::new().await;
1017        let (proxy, server_end) = create_proxy::<fio::FileMarker>();
1018        root_dir
1019            .deprecated_open(
1020                fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NOT_DIRECTORY,
1021                Default::default(),
1022                "meta",
1023                server_end.into_channel().into(),
1024            )
1025            .unwrap();
1026        assert_eq!(
1027            fuchsia_fs::file::read(&proxy).await.unwrap(),
1028            env.root_dir.hash.to_string().as_bytes()
1029        );
1030    }
1031
1032    #[fuchsia_async::run_singlethreaded(test)]
1033    async fn root_dir_deprecated_open_meta_as_dir() {
1034        let (_env, root_dir) = TestEnv::new().await;
1035        for path in ["meta", "meta/"] {
1036            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
1037            root_dir
1038                .deprecated_open(
1039                    fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::DIRECTORY,
1040                    Default::default(),
1041                    path,
1042                    server_end.into_channel().into(),
1043                )
1044                .unwrap();
1045            assert_eq!(
1046                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
1047                vec![
1048                    DirEntry { name: "contents".to_string(), kind: DirentKind::File },
1049                    DirEntry { name: "dir".to_string(), kind: DirentKind::Directory },
1050                    DirEntry { name: "file".to_string(), kind: DirentKind::File },
1051                    DirEntry { name: "fuchsia.abi".to_string(), kind: DirentKind::Directory },
1052                    DirEntry { name: "package".to_string(), kind: DirentKind::File },
1053                ]
1054            );
1055        }
1056    }
1057
1058    #[fuchsia_async::run_singlethreaded(test)]
1059    async fn root_dir_deprecated_open_meta_as_node_reference() {
1060        let (_env, root_dir) = TestEnv::new().await;
1061        for path in ["meta", "meta/"] {
1062            let (proxy, server_end) = create_proxy::<fio::NodeMarker>();
1063            root_dir
1064                .deprecated_open(
1065                    fio::OpenFlags::RIGHT_READABLE | fio::OpenFlags::NODE_REFERENCE,
1066                    Default::default(),
1067                    path,
1068                    server_end.into_channel().into(),
1069                )
1070                .unwrap();
1071            // Check that open as a node reference passed by calling `get_attr()` on the proxy.
1072            // The returned attributes should indicate the meta is a directory.
1073            let (status, attr) = proxy.get_attr().await.expect("get_attr failed");
1074            assert_eq!(zx::Status::from_raw(status), zx::Status::OK);
1075            assert_eq!(attr.mode & fio::MODE_TYPE_MASK, fio::MODE_TYPE_DIRECTORY);
1076        }
1077    }
1078
1079    #[fuchsia_async::run_singlethreaded(test)]
1080    async fn root_dir_deprecated_open_meta_file() {
1081        let (_env, root_dir) = TestEnv::new().await;
1082        let (proxy, server_end) = create_proxy::<fio::FileMarker>();
1083        root_dir
1084            .deprecated_open(
1085                fio::OpenFlags::RIGHT_READABLE,
1086                Default::default(),
1087                "meta/file",
1088                server_end.into_channel().into(),
1089            )
1090            .unwrap();
1091        assert_eq!(fuchsia_fs::file::read(&proxy).await.unwrap(), b"meta-contents0".to_vec());
1092    }
1093
1094    #[fuchsia_async::run_singlethreaded(test)]
1095    async fn root_dir_deprecated_open_meta_subdir() {
1096        let (_env, root_dir) = TestEnv::new().await;
1097        for path in ["meta/dir", "meta/dir/"] {
1098            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
1099            root_dir
1100                .deprecated_open(
1101                    fio::OpenFlags::RIGHT_READABLE,
1102                    Default::default(),
1103                    path,
1104                    server_end.into_channel().into(),
1105                )
1106                .unwrap();
1107            assert_eq!(
1108                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
1109                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
1110            );
1111        }
1112    }
1113
1114    #[fuchsia_async::run_singlethreaded(test)]
1115    async fn root_dir_deprecated_open_non_meta_subdir() {
1116        let (_env, root_dir) = TestEnv::new().await;
1117        for path in ["dir", "dir/"] {
1118            let (proxy, server_end) = create_proxy::<fio::DirectoryMarker>();
1119            root_dir
1120                .deprecated_open(
1121                    fio::OpenFlags::RIGHT_READABLE,
1122                    Default::default(),
1123                    path,
1124                    server_end.into_channel().into(),
1125                )
1126                .unwrap();
1127            assert_eq!(
1128                fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
1129                vec![DirEntry { name: "file".to_string(), kind: DirentKind::File }]
1130            );
1131        }
1132    }
1133}