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::{CachedRootDir, 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
65impl From<&Error> for zx::Status {
66 fn from(e: &Error) -> Self {
67 use Error::*;
68 match e {
69 MissingMetaFar => zx::Status::NOT_FOUND,
70 OpenMetaFar(e) => e.into(),
71 ArchiveReader(fuchsia_archive::Error::Read(_)) => zx::Status::IO,
72 ArchiveReader(_) | ReadMetaContents(_) | DeserializeMetaContents(_) => {
73 zx::Status::INVALID_ARGS
74 }
75 FileDirectoryCollision { .. } | NonUtf8MetaEntry { .. } => zx::Status::INVALID_ARGS,
76 }
77 }
78}
79
80#[derive(thiserror::Error, Debug)]
81pub enum NonMetaStorageError {
82 #[error("while reading blob")]
83 ReadBlob(#[source] fuchsia_fs::file::ReadError),
84
85 #[error("while opening blob")]
86 OpenBlob(#[source] fuchsia_fs::node::OpenError),
87
88 #[error("while making FIDL call")]
89 Fidl(#[source] fidl::Error),
90
91 #[error("while calling GetBackingMemory")]
92 GetVmo(#[source] zx::Status),
93}
94
95impl NonMetaStorageError {
96 pub fn is_not_found_error(&self) -> bool {
97 match self {
98 NonMetaStorageError::ReadBlob(e) => e.is_not_found_error(),
99 NonMetaStorageError::OpenBlob(e) => e.is_not_found_error(),
100 NonMetaStorageError::GetVmo(status) => *status == zx::Status::NOT_FOUND,
101 _ => false,
102 }
103 }
104}
105
106impl From<&NonMetaStorageError> for zx::Status {
107 fn from(e: &NonMetaStorageError) -> Self {
108 if e.is_not_found_error() { zx::Status::NOT_FOUND } else { zx::Status::INTERNAL }
109 }
110}
111
112pub trait NonMetaStorage: Send + Sync + Sized + 'static {
115 fn open(
117 &self,
118 _blob: &fuchsia_hash::Hash,
119 _flags: fio::Flags,
120 _scope: ExecutionScope,
121 _object_request: ObjectRequestRef<'_>,
122 ) -> Result<(), zx::Status>;
123
124 fn get_blob_vmo(
126 &self,
127 hash: &fuchsia_hash::Hash,
128 ) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
129
130 fn read_blob(
132 &self,
133 hash: &fuchsia_hash::Hash,
134 ) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
135}
136
137impl NonMetaStorage for blobfs::Client {
138 fn open(
139 &self,
140 blob: &fuchsia_hash::Hash,
141 flags: fio::Flags,
142 scope: ExecutionScope,
143 object_request: ObjectRequestRef<'_>,
144 ) -> Result<(), zx::Status> {
145 self.open_blob_for_read(blob, flags, scope, object_request)
146 }
147
148 async fn get_blob_vmo(
149 &self,
150 hash: &fuchsia_hash::Hash,
151 ) -> Result<zx::Vmo, NonMetaStorageError> {
152 self.get_blob_vmo(hash).await.map_err(|e| match e {
153 blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
154 blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
155 blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
156 })
157 }
158
159 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
160 let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
161 let content_size = vmo.get_content_size().map_err(|e| {
162 NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
163 })?;
164 vmo.read_to_vec::<u8>(0, content_size)
165 .map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
166 }
167}
168
169impl NonMetaStorage for fio::DirectoryProxy {
171 fn open(
172 &self,
173 blob: &fuchsia_hash::Hash,
174 flags: fio::Flags,
175 _scope: ExecutionScope,
176 object_request: ObjectRequestRef<'_>,
177 ) -> Result<(), zx::Status> {
178 self.open(
180 blob.to_string().as_str(),
181 flags,
182 &object_request.options(),
183 object_request.take().into_channel(),
184 )
185 .map_err(|_fidl_error| zx::Status::PEER_CLOSED)
186 }
187
188 async fn get_blob_vmo(
189 &self,
190 hash: &fuchsia_hash::Hash,
191 ) -> Result<zx::Vmo, NonMetaStorageError> {
192 let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
193 .await
194 .map_err(NonMetaStorageError::OpenBlob)?;
195 proxy
196 .get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
197 .await
198 .map_err(NonMetaStorageError::Fidl)?
199 .map_err(|e| NonMetaStorageError::GetVmo(zx::Status::err_from_raw(e)))
200 }
201
202 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
203 fuchsia_fs::directory::read_file(self, &hash.to_string())
204 .await
205 .map_err(NonMetaStorageError::ReadBlob)
206 }
207}
208
209pub fn serve(
213 scope: vfs::execution_scope::ExecutionScope,
214 non_meta_storage: impl NonMetaStorage,
215 meta_far: fuchsia_hash::Hash,
216 flags: fio::Flags,
217 server_end: ServerEnd<fio::DirectoryMarker>,
218) -> impl futures::Future<Output = Result<(), Error>> {
219 serve_path(
220 scope,
221 non_meta_storage,
222 meta_far,
223 flags,
224 vfs::Path::dot(),
225 server_end.into_channel().into(),
226 )
227}
228
229pub async fn serve_path(
236 scope: vfs::execution_scope::ExecutionScope,
237 non_meta_storage: impl NonMetaStorage,
238 meta_far: fuchsia_hash::Hash,
239 flags: fio::Flags,
240 path: vfs::Path,
241 server_end: ServerEnd<fio::NodeMarker>,
242) -> Result<(), Error> {
243 let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
244 Ok(d) => d,
245 Err(e) => {
246 let () = send_on_open_with_error(
247 flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
248 server_end,
249 (&e).into(),
250 );
251 return Err(e);
252 }
253 };
254
255 ObjectRequest::new(flags, &fio::Options::default(), server_end.into_channel())
256 .handle(|request| root_dir.open(scope, path, flags, request));
257 Ok(())
258}
259
260fn usize_to_u64_safe(u: usize) -> u64 {
261 let ret: u64 = u.try_into().unwrap();
262 static_assertions::assert_eq_size_val!(u, ret);
263 ret
264}
265
266fn get_dir_children<'a>(
273 materialized_tree: impl IntoIterator<Item = &'a str>,
274 dir: &str,
275) -> Vec<(EntryInfo, String)> {
276 let mut added_entries = HashSet::new();
277 let mut res = vec![];
278
279 for path in materialized_tree {
280 if let Some(path) = path.strip_prefix(dir) {
281 match path.split_once('/') {
282 None => {
283 if !added_entries.contains(path) {
285 res.push((
286 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
287 path.to_string(),
288 ));
289 added_entries.insert(path.to_string());
290 }
291 }
292 Some((first, _)) => {
293 if !added_entries.contains(first) {
294 res.push((
295 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
296 first.to_string(),
297 ));
298 added_entries.insert(first.to_string());
299 }
300 }
301 }
302 }
303 }
304
305 res.sort_by(|a, b| a.1.cmp(&b.1));
307 res
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 use assert_matches::assert_matches;
314 use fuchsia_hash::Hash;
315 use fuchsia_pkg_testing::PackageBuilder;
316 use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
317 use futures::StreamExt;
318 use vfs::directory::helper::DirectlyMutable;
319
320 #[fuchsia::test]
321 async fn serve() {
322 let (proxy, server_end) = fidl::endpoints::create_proxy();
323 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
324 let (metafar_blob, _) = package.contents();
325 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
326 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
327
328 crate::serve(
329 vfs::execution_scope::ExecutionScope::new(),
330 blobfs_client,
331 metafar_blob.merkle,
332 fio::PERM_READABLE,
333 server_end,
334 )
335 .await
336 .unwrap();
337
338 assert_eq!(
339 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
340 vec![fuchsia_fs::directory::DirEntry {
341 name: "meta".to_string(),
342 kind: fuchsia_fs::directory::DirentKind::Directory
343 }]
344 );
345 }
346
347 #[fuchsia::test]
348 async fn serve_path_open_root() {
349 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
350 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
351 let (metafar_blob, _) = package.contents();
352 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
353 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
354
355 crate::serve_path(
356 vfs::execution_scope::ExecutionScope::new(),
357 blobfs_client,
358 metafar_blob.merkle,
359 fio::PERM_READABLE,
360 vfs::Path::validate_and_split(".").unwrap(),
361 server_end.into_channel().into(),
362 )
363 .await
364 .unwrap();
365
366 assert_eq!(
367 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
368 vec![fuchsia_fs::directory::DirEntry {
369 name: "meta".to_string(),
370 kind: fuchsia_fs::directory::DirentKind::Directory
371 }]
372 );
373 }
374
375 #[fuchsia::test]
376 async fn serve_path_open_meta() {
377 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
378 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
379 let (metafar_blob, _) = package.contents();
380 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
381 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
382
383 crate::serve_path(
384 vfs::execution_scope::ExecutionScope::new(),
385 blobfs_client,
386 metafar_blob.merkle,
387 fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
388 vfs::Path::validate_and_split("meta").unwrap(),
389 server_end.into_channel().into(),
390 )
391 .await
392 .unwrap();
393
394 assert_eq!(
395 fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
396 metafar_blob.merkle.to_string(),
397 );
398 }
399
400 #[fuchsia::test]
401 async fn serve_path_open_missing_path_in_package() {
402 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
403 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
404 let (metafar_blob, _) = package.contents();
405 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
406 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
407
408 assert_matches!(
409 crate::serve_path(
410 vfs::execution_scope::ExecutionScope::new(),
411 blobfs_client,
412 metafar_blob.merkle,
413 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
414 vfs::Path::validate_and_split("not-present").unwrap(),
415 server_end.into_channel().into(),
416 )
417 .await,
418 Ok(())
421 );
422
423 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
424 }
425
426 #[fuchsia::test]
427 async fn serve_path_open_missing_package() {
428 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
429 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
430
431 assert_matches!(
432 crate::serve_path(
433 vfs::execution_scope::ExecutionScope::new(),
434 blobfs_client,
435 Hash::from([0u8; 32]),
436 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
437 vfs::Path::validate_and_split(".").unwrap(),
438 server_end.into_channel().into(),
439 )
440 .await,
441 Err(Error::MissingMetaFar)
442 );
443
444 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
445 }
446
447 async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
448 let mut events = node.take_event_stream();
451 match events.next().await? {
452 Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => zx::Status::try_from_raw(status),
453 Ok(fio::NodeEvent::OnRepresentation { .. }) => None,
454 Err(fidl::Error::ClientChannelClosed { epitaph, .. }) => match epitaph.into() {
455 Err(s) => Some(s),
456 Ok(()) => Some(zx::Status::PEER_CLOSED),
457 },
458 other => panic!("unexpected stream event or error: {other:?}"),
459 }
460 }
461
462 fn file() -> EntryInfo {
463 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
464 }
465
466 fn dir() -> EntryInfo {
467 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
468 }
469
470 #[test]
471 fn get_dir_children_root() {
472 assert_eq!(get_dir_children([], ""), vec![]);
473 assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
474 assert_eq!(
475 get_dir_children(["a", "b"], ""),
476 vec![(file(), "a".to_string()), (file(), "b".to_string())]
477 );
478 assert_eq!(
479 get_dir_children(["b", "a"], ""),
480 vec![(file(), "a".to_string()), (file(), "b".to_string())]
481 );
482 assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
483 assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
484 assert_eq!(
485 get_dir_children(["a/b", "c"], ""),
486 vec![(dir(), "a".to_string()), (file(), "c".to_string())]
487 );
488 assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
489 }
490
491 #[test]
492 fn get_dir_children_subdir() {
493 assert_eq!(get_dir_children([], "a/"), vec![]);
494 assert_eq!(get_dir_children(["a"], "a/"), vec![]);
495 assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
496 assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
497 assert_eq!(
498 get_dir_children(["a/b", "a/c"], "a/"),
499 vec![(file(), "b".to_string()), (file(), "c".to_string())]
500 );
501 assert_eq!(
502 get_dir_children(["a/c", "a/b"], "a/"),
503 vec![(file(), "b".to_string()), (file(), "c".to_string())]
504 );
505 assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
506 assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
507 assert_eq!(
508 get_dir_children(["a/b/c", "a/d"], "a/"),
509 vec![(dir(), "b".to_string()), (file(), "d".to_string())]
510 );
511 assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
512 }
513
514 const BLOB_CONTENTS: &[u8] = b"blob-contents";
515
516 fn blob_contents_hash() -> Hash {
517 fuchsia_merkle::root_from_slice(BLOB_CONTENTS)
518 }
519
520 #[fuchsia::test]
521 async fn bootfs_get_vmo_blob() {
522 let directory = vfs::directory::immutable::simple();
523 directory
524 .add_entry(blob_contents_hash().to_string(), vfs::file::read_only(BLOB_CONTENTS))
525 .unwrap();
526 let proxy = vfs::directory::serve_read_only(directory, ExecutionScope::new());
527
528 let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
529 assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
530 }
531
532 #[fuchsia::test]
533 async fn bootfs_read_blob() {
534 let directory = vfs::directory::immutable::simple();
535 directory
536 .add_entry(blob_contents_hash().to_string(), vfs::file::read_only(BLOB_CONTENTS))
537 .unwrap();
538 let proxy = vfs::directory::serve_read_only(directory, ExecutionScope::new());
539
540 assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
541 }
542
543 #[fuchsia::test]
544 async fn bootfs_get_vmo_blob_missing_blob() {
545 let directory = vfs::directory::immutable::simple();
546 let proxy = vfs::directory::serve_read_only(directory, ExecutionScope::new());
547
548 let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
549 assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
550 }
551
552 #[fuchsia::test]
553 async fn bootfs_read_blob_missing_blob() {
554 let directory = vfs::directory::immutable::simple();
555 let proxy = vfs::directory::serve_read_only(directory, ExecutionScope::new());
556
557 let result = proxy.read_blob(&blob_contents_hash()).await;
558 assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
559 }
560
561 #[fuchsia::test]
562 async fn blobfs_get_vmo_blob() {
563 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
564 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
565
566 let vmo =
567 NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
568 assert_eq!(vmo.read_to_vec::<u8>(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
569 }
570
571 #[fuchsia::test]
572 async fn blobfs_read_blob() {
573 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
574 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
575
576 assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
577 }
578
579 #[fuchsia::test]
580 async fn blobfs_get_vmo_blob_missing_blob() {
581 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
582
583 let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
584 assert_matches!(result, Err(e) if e.is_not_found_error());
585 }
586
587 #[fuchsia::test]
588 async fn blobfs_read_blob_missing_blob() {
589 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
590
591 let result = blobfs_client.read_blob(&blob_contents_hash()).await;
592 assert_matches!(result, Err(e) if e.is_not_found_error());
593 }
594}