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