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