1#![allow(clippy::let_unit_value)]
6
7use fidl::endpoints::ServerEnd;
8use fidl_fuchsia_io as fio;
9use std::collections::HashSet;
10use std::convert::TryInto as _;
11use std::future::Future;
12use vfs::common::send_on_open_with_error;
13use vfs::directory::entry::EntryInfo;
14use vfs::directory::entry_container::Directory;
15use vfs::{ObjectRequest, ObjectRequestRef};
16
17mod meta_as_dir;
18mod meta_subdir;
19mod non_meta_subdir;
20mod root_dir;
21mod root_dir_cache;
22
23pub use root_dir::{PathError, ReadFileError, RootDir, SubpackagesError};
24pub use root_dir_cache::RootDirCache;
25pub use vfs::execution_scope::ExecutionScope;
26
27pub(crate) const DIRECTORY_ABILITIES: fio::Abilities =
28 fio::Abilities::GET_ATTRIBUTES.union(fio::Abilities::ENUMERATE).union(fio::Abilities::TRAVERSE);
29
30pub(crate) const ALLOWED_FLAGS: fio::Flags = fio::Flags::empty()
31 .union(fio::MASK_KNOWN_PROTOCOLS)
32 .union(fio::PERM_READABLE)
33 .union(fio::PERM_EXECUTABLE)
34 .union(fio::Flags::PERM_INHERIT_EXECUTE)
35 .union(fio::Flags::FLAG_SEND_REPRESENTATION);
36
37#[derive(thiserror::Error, Debug)]
38pub enum Error {
39 #[error("the meta.far was not found")]
40 MissingMetaFar,
41
42 #[error("while opening the meta.far")]
43 OpenMetaFar(#[source] NonMetaStorageError),
44
45 #[error("while instantiating a fuchsia archive reader")]
46 ArchiveReader(#[source] fuchsia_archive::Error),
47
48 #[error("meta.far has a path that is not valid utf-8: {path:?}")]
49 NonUtf8MetaEntry {
50 #[source]
51 source: std::str::Utf8Error,
52 path: Vec<u8>,
53 },
54
55 #[error("while reading meta/contents")]
56 ReadMetaContents(#[source] fuchsia_archive::Error),
57
58 #[error("while deserializing meta/contents")]
59 DeserializeMetaContents(#[source] fuchsia_pkg::MetaContentsError),
60
61 #[error("collision between a file and a directory at path: '{:?}'", path)]
62 FileDirectoryCollision { path: String },
63
64 #[error("the supplied RootDir already has a dropper set")]
65 DropperAlreadySet,
66}
67
68impl From<&Error> for zx::Status {
69 fn from(e: &Error) -> Self {
70 use Error::*;
71 match e {
72 MissingMetaFar => zx::Status::NOT_FOUND,
73 OpenMetaFar(e) => e.into(),
74 DropperAlreadySet => zx::Status::INTERNAL,
75 ArchiveReader(fuchsia_archive::Error::Read(_)) => zx::Status::IO,
76 ArchiveReader(_) | ReadMetaContents(_) | DeserializeMetaContents(_) => {
77 zx::Status::INVALID_ARGS
78 }
79 FileDirectoryCollision { .. } | NonUtf8MetaEntry { .. } => zx::Status::INVALID_ARGS,
80 }
81 }
82}
83
84#[derive(thiserror::Error, Debug)]
85pub enum NonMetaStorageError {
86 #[error("while reading blob")]
87 ReadBlob(#[source] fuchsia_fs::file::ReadError),
88
89 #[error("while opening blob")]
90 OpenBlob(#[source] fuchsia_fs::node::OpenError),
91
92 #[error("while making FIDL call")]
93 Fidl(#[source] fidl::Error),
94
95 #[error("while calling GetBackingMemory")]
96 GetVmo(#[source] zx::Status),
97}
98
99impl NonMetaStorageError {
100 pub fn is_not_found_error(&self) -> bool {
101 match self {
102 NonMetaStorageError::ReadBlob(e) => e.is_not_found_error(),
103 NonMetaStorageError::OpenBlob(e) => e.is_not_found_error(),
104 NonMetaStorageError::GetVmo(status) => *status == zx::Status::NOT_FOUND,
105 _ => false,
106 }
107 }
108}
109
110impl From<&NonMetaStorageError> for zx::Status {
111 fn from(e: &NonMetaStorageError) -> Self {
112 if e.is_not_found_error() { zx::Status::NOT_FOUND } else { zx::Status::INTERNAL }
113 }
114}
115
116pub trait NonMetaStorage: Send + Sync + Sized + 'static {
119 fn open(
121 &self,
122 _blob: &fuchsia_hash::Hash,
123 _flags: fio::Flags,
124 _scope: ExecutionScope,
125 _object_request: ObjectRequestRef<'_>,
126 ) -> Result<(), zx::Status>;
127
128 fn get_blob_vmo(
130 &self,
131 hash: &fuchsia_hash::Hash,
132 ) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
133
134 fn read_blob(
136 &self,
137 hash: &fuchsia_hash::Hash,
138 ) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
139}
140
141impl NonMetaStorage for blobfs::Client {
142 fn open(
143 &self,
144 blob: &fuchsia_hash::Hash,
145 flags: fio::Flags,
146 scope: ExecutionScope,
147 object_request: ObjectRequestRef<'_>,
148 ) -> Result<(), zx::Status> {
149 self.open_blob_for_read(blob, flags, scope, object_request)
150 }
151
152 async fn get_blob_vmo(
153 &self,
154 hash: &fuchsia_hash::Hash,
155 ) -> Result<zx::Vmo, NonMetaStorageError> {
156 self.get_blob_vmo(hash).await.map_err(|e| match e {
157 blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
158 blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
159 blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
160 })
161 }
162
163 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
164 let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
165 let content_size = vmo.get_content_size().map_err(|e| {
166 NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
167 })?;
168 vmo.read_to_vec::<u8>(0, content_size)
169 .map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
170 }
171}
172
173impl NonMetaStorage for fio::DirectoryProxy {
175 fn open(
176 &self,
177 blob: &fuchsia_hash::Hash,
178 flags: fio::Flags,
179 _scope: ExecutionScope,
180 object_request: ObjectRequestRef<'_>,
181 ) -> Result<(), zx::Status> {
182 self.open(
184 blob.to_string().as_str(),
185 flags,
186 &object_request.options(),
187 object_request.take().into_channel(),
188 )
189 .map_err(|_fidl_error| zx::Status::PEER_CLOSED)
190 }
191
192 async fn get_blob_vmo(
193 &self,
194 hash: &fuchsia_hash::Hash,
195 ) -> Result<zx::Vmo, NonMetaStorageError> {
196 let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
197 .await
198 .map_err(NonMetaStorageError::OpenBlob)?;
199 proxy
200 .get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
201 .await
202 .map_err(NonMetaStorageError::Fidl)?
203 .map_err(|e| NonMetaStorageError::GetVmo(zx::Status::from_raw(e)))
204 }
205
206 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
207 fuchsia_fs::directory::read_file(self, &hash.to_string())
208 .await
209 .map_err(NonMetaStorageError::ReadBlob)
210 }
211}
212
213pub fn serve(
217 scope: vfs::execution_scope::ExecutionScope,
218 non_meta_storage: impl NonMetaStorage,
219 meta_far: fuchsia_hash::Hash,
220 flags: fio::Flags,
221 server_end: ServerEnd<fio::DirectoryMarker>,
222) -> impl futures::Future<Output = Result<(), Error>> {
223 serve_path(
224 scope,
225 non_meta_storage,
226 meta_far,
227 flags,
228 vfs::Path::dot(),
229 server_end.into_channel().into(),
230 )
231}
232
233pub async fn serve_path(
240 scope: vfs::execution_scope::ExecutionScope,
241 non_meta_storage: impl NonMetaStorage,
242 meta_far: fuchsia_hash::Hash,
243 flags: fio::Flags,
244 path: vfs::Path,
245 server_end: ServerEnd<fio::NodeMarker>,
246) -> Result<(), Error> {
247 let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
248 Ok(d) => d,
249 Err(e) => {
250 let () = send_on_open_with_error(
251 flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
252 server_end,
253 (&e).into(),
254 );
255 return Err(e);
256 }
257 };
258
259 ObjectRequest::new(flags, &fio::Options::default(), server_end.into_channel())
260 .handle(|request| root_dir.open(scope, path, flags, request));
261 Ok(())
262}
263
264fn usize_to_u64_safe(u: usize) -> u64 {
265 let ret: u64 = u.try_into().unwrap();
266 static_assertions::assert_eq_size_val!(u, ret);
267 ret
268}
269
270pub trait OnRootDirDrop: Send + Sync + std::fmt::Debug {}
282impl<T> OnRootDirDrop for T where T: Send + Sync + std::fmt::Debug {}
283
284fn get_dir_children<'a>(
291 materialized_tree: impl IntoIterator<Item = &'a str>,
292 dir: &str,
293) -> Vec<(EntryInfo, String)> {
294 let mut added_entries = HashSet::new();
295 let mut res = vec![];
296
297 for path in materialized_tree {
298 if let Some(path) = path.strip_prefix(dir) {
299 match path.split_once('/') {
300 None => {
301 if !added_entries.contains(path) {
303 res.push((
304 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
305 path.to_string(),
306 ));
307 added_entries.insert(path.to_string());
308 }
309 }
310 Some((first, _)) => {
311 if !added_entries.contains(first) {
312 res.push((
313 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
314 first.to_string(),
315 ));
316 added_entries.insert(first.to_string());
317 }
318 }
319 }
320 }
321 }
322
323 res.sort_by(|a, b| a.1.cmp(&b.1));
325 res
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331 use assert_matches::assert_matches;
332 use fuchsia_hash::Hash;
333 use fuchsia_pkg_testing::PackageBuilder;
334 use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
335 use futures::StreamExt;
336 use vfs::directory::helper::DirectlyMutable;
337
338 #[fuchsia_async::run_singlethreaded(test)]
339 async fn serve() {
340 let (proxy, server_end) = fidl::endpoints::create_proxy();
341 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
342 let (metafar_blob, _) = package.contents();
343 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
344 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
345
346 crate::serve(
347 vfs::execution_scope::ExecutionScope::new(),
348 blobfs_client,
349 metafar_blob.merkle,
350 fio::PERM_READABLE,
351 server_end,
352 )
353 .await
354 .unwrap();
355
356 assert_eq!(
357 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
358 vec![fuchsia_fs::directory::DirEntry {
359 name: "meta".to_string(),
360 kind: fuchsia_fs::directory::DirentKind::Directory
361 }]
362 );
363 }
364
365 #[fuchsia_async::run_singlethreaded(test)]
366 async fn serve_path_open_root() {
367 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
368 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
369 let (metafar_blob, _) = package.contents();
370 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
371 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
372
373 crate::serve_path(
374 vfs::execution_scope::ExecutionScope::new(),
375 blobfs_client,
376 metafar_blob.merkle,
377 fio::PERM_READABLE,
378 vfs::Path::validate_and_split(".").unwrap(),
379 server_end.into_channel().into(),
380 )
381 .await
382 .unwrap();
383
384 assert_eq!(
385 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
386 vec![fuchsia_fs::directory::DirEntry {
387 name: "meta".to_string(),
388 kind: fuchsia_fs::directory::DirentKind::Directory
389 }]
390 );
391 }
392
393 #[fuchsia_async::run_singlethreaded(test)]
394 async fn serve_path_open_meta() {
395 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
396 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
397 let (metafar_blob, _) = package.contents();
398 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
399 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
400
401 crate::serve_path(
402 vfs::execution_scope::ExecutionScope::new(),
403 blobfs_client,
404 metafar_blob.merkle,
405 fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
406 vfs::Path::validate_and_split("meta").unwrap(),
407 server_end.into_channel().into(),
408 )
409 .await
410 .unwrap();
411
412 assert_eq!(
413 fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
414 metafar_blob.merkle.to_string(),
415 );
416 }
417
418 #[fuchsia_async::run_singlethreaded(test)]
419 async fn serve_path_open_missing_path_in_package() {
420 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
421 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
422 let (metafar_blob, _) = package.contents();
423 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
424 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
425
426 assert_matches!(
427 crate::serve_path(
428 vfs::execution_scope::ExecutionScope::new(),
429 blobfs_client,
430 metafar_blob.merkle,
431 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
432 vfs::Path::validate_and_split("not-present").unwrap(),
433 server_end.into_channel().into(),
434 )
435 .await,
436 Ok(())
439 );
440
441 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
442 }
443
444 #[fuchsia_async::run_singlethreaded(test)]
445 async fn serve_path_open_missing_package() {
446 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
447 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
448
449 assert_matches!(
450 crate::serve_path(
451 vfs::execution_scope::ExecutionScope::new(),
452 blobfs_client,
453 Hash::from([0u8; 32]),
454 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
455 vfs::Path::validate_and_split(".").unwrap(),
456 server_end.into_channel().into(),
457 )
458 .await,
459 Err(Error::MissingMetaFar)
460 );
461
462 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
463 }
464
465 async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
466 let mut events = node.take_event_stream();
469 match events.next().await? {
470 Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => Some(zx::Status::from_raw(status)),
471 Ok(fio::NodeEvent::OnRepresentation { .. }) => Some(zx::Status::OK),
472 Err(fidl::Error::ClientChannelClosed { status, .. }) => Some(status),
473 other => panic!("unexpected stream event or error: {other:?}"),
474 }
475 }
476
477 fn file() -> EntryInfo {
478 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
479 }
480
481 fn dir() -> EntryInfo {
482 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
483 }
484
485 #[test]
486 fn get_dir_children_root() {
487 assert_eq!(get_dir_children([], ""), vec![]);
488 assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
489 assert_eq!(
490 get_dir_children(["a", "b"], ""),
491 vec![(file(), "a".to_string()), (file(), "b".to_string())]
492 );
493 assert_eq!(
494 get_dir_children(["b", "a"], ""),
495 vec![(file(), "a".to_string()), (file(), "b".to_string())]
496 );
497 assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
498 assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
499 assert_eq!(
500 get_dir_children(["a/b", "c"], ""),
501 vec![(dir(), "a".to_string()), (file(), "c".to_string())]
502 );
503 assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
504 }
505
506 #[test]
507 fn get_dir_children_subdir() {
508 assert_eq!(get_dir_children([], "a/"), vec![]);
509 assert_eq!(get_dir_children(["a"], "a/"), vec![]);
510 assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
511 assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
512 assert_eq!(
513 get_dir_children(["a/b", "a/c"], "a/"),
514 vec![(file(), "b".to_string()), (file(), "c".to_string())]
515 );
516 assert_eq!(
517 get_dir_children(["a/c", "a/b"], "a/"),
518 vec![(file(), "b".to_string()), (file(), "c".to_string())]
519 );
520 assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
521 assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
522 assert_eq!(
523 get_dir_children(["a/b/c", "a/d"], "a/"),
524 vec![(dir(), "b".to_string()), (file(), "d".to_string())]
525 );
526 assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
527 }
528
529 const BLOB_CONTENTS: &[u8] = b"blob-contents";
530
531 fn blob_contents_hash() -> Hash {
532 fuchsia_merkle::root_from_slice(BLOB_CONTENTS)
533 }
534
535 #[fuchsia_async::run_singlethreaded(test)]
536 async fn bootfs_get_vmo_blob() {
537 let directory = vfs::directory::immutable::simple();
538 directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
539 let proxy = vfs::directory::serve_read_only(directory);
540
541 let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
542 assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
543 }
544
545 #[fuchsia_async::run_singlethreaded(test)]
546 async fn bootfs_read_blob() {
547 let directory = vfs::directory::immutable::simple();
548 directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
549 let proxy = vfs::directory::serve_read_only(directory);
550
551 assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
552 }
553
554 #[fuchsia_async::run_singlethreaded(test)]
555 async fn bootfs_get_vmo_blob_missing_blob() {
556 let directory = vfs::directory::immutable::simple();
557 let proxy = vfs::directory::serve_read_only(directory);
558
559 let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
560 assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
561 }
562
563 #[fuchsia_async::run_singlethreaded(test)]
564 async fn bootfs_read_blob_missing_blob() {
565 let directory = vfs::directory::immutable::simple();
566 let proxy = vfs::directory::serve_read_only(directory);
567
568 let result = proxy.read_blob(&blob_contents_hash()).await;
569 assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
570 }
571
572 #[fuchsia_async::run_singlethreaded(test)]
573 async fn blobfs_get_vmo_blob() {
574 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
575 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
576
577 let vmo =
578 NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
579 assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
580 }
581
582 #[fuchsia_async::run_singlethreaded(test)]
583 async fn blobfs_read_blob() {
584 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
585 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
586
587 assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
588 }
589
590 #[fuchsia_async::run_singlethreaded(test)]
591 async fn blobfs_get_vmo_blob_missing_blob() {
592 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
593
594 let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
595 assert_matches!(result, Err(e) if e.is_not_found_error());
596 }
597
598 #[fuchsia_async::run_singlethreaded(test)]
599 async fn blobfs_read_blob_missing_blob() {
600 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
601
602 let result = blobfs_client.read_blob(&blob_contents_hash()).await;
603 assert_matches!(result, Err(e) if e.is_not_found_error());
604 }
605}