#![allow(clippy::let_unit_value)]
use fidl::endpoints::ServerEnd;
use fidl_fuchsia_io as fio;
use std::collections::HashSet;
use std::convert::TryInto as _;
use std::future::Future;
use tracing::error;
use vfs::common::send_on_open_with_error;
use vfs::directory::entry::EntryInfo;
use vfs::directory::entry_container::Directory;
use vfs::{ObjectRequest, ObjectRequestRef};
mod meta_as_dir;
mod meta_subdir;
mod non_meta_subdir;
mod root_dir;
mod root_dir_cache;
pub use root_dir::{PathError, ReadFileError, RootDir, SubpackagesError};
pub use root_dir_cache::RootDirCache;
pub use vfs::execution_scope::ExecutionScope;
pub use vfs::path::Path as VfsPath;
pub(crate) const DIRECTORY_ABILITIES: fio::Abilities =
fio::Abilities::GET_ATTRIBUTES.union(fio::Abilities::ENUMERATE).union(fio::Abilities::TRAVERSE);
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("the meta.far was not found")]
MissingMetaFar,
#[error("while opening the meta.far")]
OpenMetaFar(#[source] NonMetaStorageError),
#[error("while instantiating a fuchsia archive reader")]
ArchiveReader(#[source] fuchsia_archive::Error),
#[error("meta.far has a path that is not valid utf-8: {path:?}")]
NonUtf8MetaEntry {
#[source]
source: std::str::Utf8Error,
path: Vec<u8>,
},
#[error("while reading meta/contents")]
ReadMetaContents(#[source] fuchsia_archive::Error),
#[error("while deserializing meta/contents")]
DeserializeMetaContents(#[source] fuchsia_pkg::MetaContentsError),
#[error("collision between a file and a directory at path: '{:?}'", path)]
FileDirectoryCollision { path: String },
#[error("the supplied RootDir already has a dropper set")]
DropperAlreadySet,
}
impl From<&Error> for zx::Status {
fn from(e: &Error) -> Self {
use Error::*;
match e {
MissingMetaFar => zx::Status::NOT_FOUND,
OpenMetaFar(e) => e.into(),
DropperAlreadySet => zx::Status::INTERNAL,
ArchiveReader(fuchsia_archive::Error::Read(_)) => zx::Status::IO,
ArchiveReader(_) | ReadMetaContents(_) | DeserializeMetaContents(_) => {
zx::Status::INVALID_ARGS
}
FileDirectoryCollision { .. } | NonUtf8MetaEntry { .. } => zx::Status::INVALID_ARGS,
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum NonMetaStorageError {
#[error("while reading blob")]
ReadBlob(#[source] fuchsia_fs::file::ReadError),
#[error("while opening blob")]
OpenBlob(#[source] fuchsia_fs::node::OpenError),
#[error("while making FIDL call")]
Fidl(#[source] fidl::Error),
#[error("while calling GetBackingMemory")]
GetVmo(#[source] zx::Status),
}
impl NonMetaStorageError {
pub fn is_not_found_error(&self) -> bool {
match self {
NonMetaStorageError::ReadBlob(e) => e.is_not_found_error(),
NonMetaStorageError::OpenBlob(e) => e.is_not_found_error(),
NonMetaStorageError::GetVmo(status) => *status == zx::Status::NOT_FOUND,
_ => false,
}
}
}
impl From<&NonMetaStorageError> for zx::Status {
fn from(e: &NonMetaStorageError) -> Self {
if e.is_not_found_error() {
zx::Status::NOT_FOUND
} else {
zx::Status::INTERNAL
}
}
}
pub trait NonMetaStorage: Send + Sync + Sized + 'static {
fn open(
&self,
blob: &fuchsia_hash::Hash,
flags: fio::OpenFlags,
scope: ExecutionScope,
server_end: ServerEnd<fio::NodeMarker>,
) -> Result<(), NonMetaStorageError>;
fn open3(
&self,
_blob: &fuchsia_hash::Hash,
_flags: fio::Flags,
_scope: ExecutionScope,
_object_request: ObjectRequestRef<'_>,
) -> Result<(), zx::Status>;
fn get_blob_vmo(
&self,
hash: &fuchsia_hash::Hash,
) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
fn read_blob(
&self,
hash: &fuchsia_hash::Hash,
) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
}
impl NonMetaStorage for blobfs::Client {
fn open(
&self,
blob: &fuchsia_hash::Hash,
flags: fio::OpenFlags,
scope: ExecutionScope,
server_end: ServerEnd<fio::NodeMarker>,
) -> Result<(), NonMetaStorageError> {
self.open_blob_for_read(blob, flags, scope, server_end).map_err(|e| {
NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e))
})
}
fn open3(
&self,
blob: &fuchsia_hash::Hash,
flags: fio::Flags,
scope: ExecutionScope,
object_request: ObjectRequestRef<'_>,
) -> Result<(), zx::Status> {
self.open3_blob_for_read(blob, flags, scope, object_request)
}
async fn get_blob_vmo(
&self,
hash: &fuchsia_hash::Hash,
) -> Result<zx::Vmo, NonMetaStorageError> {
self.get_blob_vmo(hash).await.map_err(|e| match e {
blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
})
}
async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
let content_size = vmo.get_content_size().map_err(|e| {
NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
})?;
vmo.read_to_vec(0, content_size)
.map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
}
}
impl NonMetaStorage for fio::DirectoryProxy {
fn open(
&self,
blob: &fuchsia_hash::Hash,
flags: fio::OpenFlags,
_scope: ExecutionScope,
server_end: ServerEnd<fio::NodeMarker>,
) -> Result<(), NonMetaStorageError> {
self.open(flags, fio::ModeType::empty(), blob.to_string().as_str(), server_end).map_err(
|e| NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e)),
)
}
fn open3(
&self,
blob: &fuchsia_hash::Hash,
flags: fio::Flags,
_scope: ExecutionScope,
object_request: ObjectRequestRef<'_>,
) -> Result<(), zx::Status> {
self.open3(
blob.to_string().as_str(),
flags,
&object_request.options(),
object_request.take().into_channel(),
)
.map_err(|_fidl_error| zx::Status::PEER_CLOSED)
}
async fn get_blob_vmo(
&self,
hash: &fuchsia_hash::Hash,
) -> Result<zx::Vmo, NonMetaStorageError> {
let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
.await
.map_err(NonMetaStorageError::OpenBlob)?;
proxy
.get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
.await
.map_err(NonMetaStorageError::Fidl)?
.map_err(|e| NonMetaStorageError::GetVmo(zx::Status::from_raw(e)))
}
async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
fuchsia_fs::directory::read_file(self, &hash.to_string())
.await
.map_err(NonMetaStorageError::ReadBlob)
}
}
pub fn serve(
scope: vfs::execution_scope::ExecutionScope,
non_meta_storage: impl NonMetaStorage,
meta_far: fuchsia_hash::Hash,
flags: fio::Flags,
server_end: ServerEnd<fio::DirectoryMarker>,
) -> impl futures::Future<Output = Result<(), Error>> {
serve_path(
scope,
non_meta_storage,
meta_far,
flags,
VfsPath::dot(),
server_end.into_channel().into(),
)
}
pub async fn serve_path(
scope: vfs::execution_scope::ExecutionScope,
non_meta_storage: impl NonMetaStorage,
meta_far: fuchsia_hash::Hash,
flags: fio::Flags,
path: VfsPath,
server_end: ServerEnd<fio::NodeMarker>,
) -> Result<(), Error> {
let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
Ok(d) => d,
Err(e) => {
let () = send_on_open_with_error(
flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
server_end,
(&e).into(),
);
return Err(e);
}
};
ObjectRequest::new3(flags, &fio::Options::default(), server_end.into_channel())
.handle(|request| root_dir.open3(scope, path, flags, request));
Ok(())
}
fn usize_to_u64_safe(u: usize) -> u64 {
let ret: u64 = u.try_into().unwrap();
static_assertions::assert_eq_size_val!(u, ret);
ret
}
pub trait OnRootDirDrop: Send + Sync + std::fmt::Debug {}
impl<T> OnRootDirDrop for T where T: Send + Sync + std::fmt::Debug {}
fn get_dir_children<'a>(
materialized_tree: impl IntoIterator<Item = &'a str>,
dir: &str,
) -> Vec<(EntryInfo, String)> {
let mut added_entries = HashSet::new();
let mut res = vec![];
for path in materialized_tree {
if let Some(path) = path.strip_prefix(dir) {
match path.split_once('/') {
None => {
if !added_entries.contains(path) {
res.push((
EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
path.to_string(),
));
added_entries.insert(path.to_string());
}
}
Some((first, _)) => {
if !added_entries.contains(first) {
res.push((
EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
first.to_string(),
));
added_entries.insert(first.to_string());
}
}
}
}
}
res.sort_by(|a, b| a.1.cmp(&b.1));
res
}
#[cfg(test)]
async fn verify_open_adjusts_flags(
entry: std::sync::Arc<impl Directory>,
in_flags: fio::OpenFlags,
expected_flags: fio::OpenFlags,
) {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
entry.open(ExecutionScope::new(), in_flags, VfsPath::dot(), server_end);
let (status, flags) = proxy.get_flags().await.unwrap();
let () = zx::Status::ok(status).unwrap();
assert_eq!(flags, expected_flags);
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
use fuchsia_hash::Hash;
use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
use fuchsia_pkg_testing::PackageBuilder;
use futures::StreamExt;
use std::any::Any;
use std::sync::Arc;
use vfs::directory::dirents_sink::{self, AppendResult, Sealed, Sink};
use vfs::directory::helper::DirectlyMutable;
#[fuchsia_async::run_singlethreaded(test)]
async fn serve() {
let (proxy, server_end) = fidl::endpoints::create_proxy();
let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
let (metafar_blob, _) = package.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
crate::serve(
vfs::execution_scope::ExecutionScope::new(),
blobfs_client,
metafar_blob.merkle,
fio::PERM_READABLE,
server_end,
)
.await
.unwrap();
assert_eq!(
fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
vec![fuchsia_fs::directory::DirEntry {
name: "meta".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
}]
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn serve_path_open_root() {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
let (metafar_blob, _) = package.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
crate::serve_path(
vfs::execution_scope::ExecutionScope::new(),
blobfs_client,
metafar_blob.merkle,
fio::PERM_READABLE,
VfsPath::validate_and_split(".").unwrap(),
server_end.into_channel().into(),
)
.await
.unwrap();
assert_eq!(
fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
vec![fuchsia_fs::directory::DirEntry {
name: "meta".to_string(),
kind: fuchsia_fs::directory::DirentKind::Directory
}]
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn serve_path_open_meta() {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
let (metafar_blob, _) = package.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
crate::serve_path(
vfs::execution_scope::ExecutionScope::new(),
blobfs_client,
metafar_blob.merkle,
fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
VfsPath::validate_and_split("meta").unwrap(),
server_end.into_channel().into(),
)
.await
.unwrap();
assert_eq!(
fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
metafar_blob.merkle.to_string(),
);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn serve_path_open_missing_path_in_package() {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
let (metafar_blob, _) = package.contents();
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
assert_matches!(
crate::serve_path(
vfs::execution_scope::ExecutionScope::new(),
blobfs_client,
metafar_blob.merkle,
fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
VfsPath::validate_and_split("not-present").unwrap(),
server_end.into_channel().into(),
)
.await,
Ok(())
);
assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
}
#[fuchsia_async::run_singlethreaded(test)]
async fn serve_path_open_missing_package() {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
assert_matches!(
crate::serve_path(
vfs::execution_scope::ExecutionScope::new(),
blobfs_client,
Hash::from([0u8; 32]),
fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
VfsPath::validate_and_split(".").unwrap(),
server_end.into_channel().into(),
)
.await,
Err(Error::MissingMetaFar)
);
assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
}
async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
let mut events = node.take_event_stream();
match events.next().await? {
Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => Some(zx::Status::from_raw(status)),
Ok(fio::NodeEvent::OnRepresentation { .. }) => Some(zx::Status::OK),
Err(fidl::Error::ClientChannelClosed { status, .. }) => Some(status),
other => panic!("unexpected stream event or error: {other:?}"),
}
}
fn file() -> EntryInfo {
EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
}
fn dir() -> EntryInfo {
EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
}
#[test]
fn get_dir_children_root() {
assert_eq!(get_dir_children([], ""), vec![]);
assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
assert_eq!(
get_dir_children(["a", "b"], ""),
vec![(file(), "a".to_string()), (file(), "b".to_string())]
);
assert_eq!(
get_dir_children(["b", "a"], ""),
vec![(file(), "a".to_string()), (file(), "b".to_string())]
);
assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
assert_eq!(
get_dir_children(["a/b", "c"], ""),
vec![(dir(), "a".to_string()), (file(), "c".to_string())]
);
assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
}
#[test]
fn get_dir_children_subdir() {
assert_eq!(get_dir_children([], "a/"), vec![]);
assert_eq!(get_dir_children(["a"], "a/"), vec![]);
assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
assert_eq!(
get_dir_children(["a/b", "a/c"], "a/"),
vec![(file(), "b".to_string()), (file(), "c".to_string())]
);
assert_eq!(
get_dir_children(["a/c", "a/b"], "a/"),
vec![(file(), "b".to_string()), (file(), "c".to_string())]
);
assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
assert_eq!(
get_dir_children(["a/b/c", "a/d"], "a/"),
vec![(dir(), "b".to_string()), (file(), "d".to_string())]
);
assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
}
#[derive(Clone)]
pub(crate) struct FakeSink {
max_entries: usize,
pub(crate) entries: Vec<(String, EntryInfo)>,
sealed: bool,
}
impl FakeSink {
pub(crate) fn new(max_entries: usize) -> Self {
FakeSink { max_entries, entries: Vec::with_capacity(max_entries), sealed: false }
}
pub(crate) fn from_sealed(sealed: Box<dyn dirents_sink::Sealed>) -> Box<FakeSink> {
sealed.into()
}
}
impl From<Box<dyn dirents_sink::Sealed>> for Box<FakeSink> {
fn from(sealed: Box<dyn dirents_sink::Sealed>) -> Self {
sealed.open().downcast::<FakeSink>().unwrap()
}
}
impl Sink for FakeSink {
fn append(mut self: Box<Self>, entry: &EntryInfo, name: &str) -> AppendResult {
assert!(!self.sealed);
if self.entries.len() == self.max_entries {
AppendResult::Sealed(self.seal())
} else {
self.entries.push((name.to_owned(), entry.clone()));
AppendResult::Ok(self)
}
}
fn seal(mut self: Box<Self>) -> Box<dyn Sealed> {
self.sealed = true;
self
}
}
impl Sealed for FakeSink {
fn open(self: Box<Self>) -> Box<dyn Any> {
self
}
}
const BLOB_CONTENTS: &[u8] = b"blob-contents";
fn blob_contents_hash() -> Hash {
fuchsia_merkle::from_slice(BLOB_CONTENTS).root()
}
fn serve_directory(directory: Arc<vfs::directory::immutable::Simple>) -> fio::DirectoryProxy {
let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
vfs::directory::entry_container::Directory::open(
directory,
vfs::execution_scope::ExecutionScope::new(),
fio::OpenFlags::RIGHT_READABLE,
vfs::path::Path::dot(),
fidl::endpoints::ServerEnd::new(server_end.into_channel()),
);
proxy
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bootfs_get_vmo_blob() {
let directory = vfs::directory::immutable::simple();
directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
let proxy = serve_directory(directory);
let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
assert_eq!(vmo.read_to_vec(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bootfs_read_blob() {
let directory = vfs::directory::immutable::simple();
directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
let proxy = serve_directory(directory);
assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bootfs_get_vmo_blob_missing_blob() {
let directory = vfs::directory::immutable::simple();
let proxy = serve_directory(directory);
let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn bootfs_read_blob_missing_blob() {
let directory = vfs::directory::immutable::simple();
let proxy = serve_directory(directory);
let result = proxy.read_blob(&blob_contents_hash()).await;
assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blobfs_get_vmo_blob() {
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
let vmo =
NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
assert_eq!(vmo.read_to_vec(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blobfs_read_blob() {
let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blobfs_get_vmo_blob_missing_blob() {
let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
}
#[fuchsia_async::run_singlethreaded(test)]
async fn blobfs_read_blob_missing_blob() {
let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
let result = blobfs_client.read_blob(&blob_contents_hash()).await;
assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
}
}