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