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() { zx::Status::NOT_FOUND } else { zx::Status::INTERNAL }
114 }
115}
116
117pub trait NonMetaStorage: Send + Sync + Sized + 'static {
120 fn deprecated_open(
122 &self,
123 blob: &fuchsia_hash::Hash,
124 flags: fio::OpenFlags,
125 scope: ExecutionScope,
126 server_end: ServerEnd<fio::NodeMarker>,
127 ) -> Result<(), NonMetaStorageError>;
128
129 fn open(
131 &self,
132 _blob: &fuchsia_hash::Hash,
133 _flags: fio::Flags,
134 _scope: ExecutionScope,
135 _object_request: ObjectRequestRef<'_>,
136 ) -> Result<(), zx::Status>;
137
138 fn get_blob_vmo(
140 &self,
141 hash: &fuchsia_hash::Hash,
142 ) -> impl Future<Output = Result<zx::Vmo, NonMetaStorageError>> + Send;
143
144 fn read_blob(
146 &self,
147 hash: &fuchsia_hash::Hash,
148 ) -> impl Future<Output = Result<Vec<u8>, NonMetaStorageError>> + Send;
149}
150
151impl NonMetaStorage for blobfs::Client {
152 fn deprecated_open(
153 &self,
154 blob: &fuchsia_hash::Hash,
155 flags: fio::OpenFlags,
156 scope: ExecutionScope,
157 server_end: ServerEnd<fio::NodeMarker>,
158 ) -> Result<(), NonMetaStorageError> {
159 self.deprecated_open_blob_for_read(blob, flags, scope, server_end).map_err(|e| {
160 NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e))
161 })
162 }
163
164 fn open(
165 &self,
166 blob: &fuchsia_hash::Hash,
167 flags: fio::Flags,
168 scope: ExecutionScope,
169 object_request: ObjectRequestRef<'_>,
170 ) -> Result<(), zx::Status> {
171 self.open_blob_for_read(blob, flags, scope, object_request)
172 }
173
174 async fn get_blob_vmo(
175 &self,
176 hash: &fuchsia_hash::Hash,
177 ) -> Result<zx::Vmo, NonMetaStorageError> {
178 self.get_blob_vmo(hash).await.map_err(|e| match e {
179 blobfs::GetBlobVmoError::OpenBlob(e) => NonMetaStorageError::OpenBlob(e),
180 blobfs::GetBlobVmoError::GetVmo(e) => NonMetaStorageError::GetVmo(e),
181 blobfs::GetBlobVmoError::Fidl(e) => NonMetaStorageError::Fidl(e),
182 })
183 }
184
185 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
186 let vmo = NonMetaStorage::get_blob_vmo(self, hash).await?;
187 let content_size = vmo.get_content_size().map_err(|e| {
188 NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e))
189 })?;
190 vmo.read_to_vec(0, content_size)
191 .map_err(|e| NonMetaStorageError::ReadBlob(fuchsia_fs::file::ReadError::ReadError(e)))
192 }
193}
194
195impl NonMetaStorage for fio::DirectoryProxy {
197 fn deprecated_open(
198 &self,
199 blob: &fuchsia_hash::Hash,
200 flags: fio::OpenFlags,
201 _scope: ExecutionScope,
202 server_end: ServerEnd<fio::NodeMarker>,
203 ) -> Result<(), NonMetaStorageError> {
204 self.deprecated_open(flags, fio::ModeType::empty(), blob.to_string().as_str(), server_end)
205 .map_err(|e| {
206 NonMetaStorageError::OpenBlob(fuchsia_fs::node::OpenError::SendOpenRequest(e))
207 })
208 }
209
210 fn open(
211 &self,
212 blob: &fuchsia_hash::Hash,
213 flags: fio::Flags,
214 _scope: ExecutionScope,
215 object_request: ObjectRequestRef<'_>,
216 ) -> Result<(), zx::Status> {
217 self.open(
219 blob.to_string().as_str(),
220 flags,
221 &object_request.options(),
222 object_request.take().into_channel(),
223 )
224 .map_err(|_fidl_error| zx::Status::PEER_CLOSED)
225 }
226
227 async fn get_blob_vmo(
228 &self,
229 hash: &fuchsia_hash::Hash,
230 ) -> Result<zx::Vmo, NonMetaStorageError> {
231 let proxy = fuchsia_fs::directory::open_file(self, &hash.to_string(), fio::PERM_READABLE)
232 .await
233 .map_err(NonMetaStorageError::OpenBlob)?;
234 proxy
235 .get_backing_memory(fio::VmoFlags::PRIVATE_CLONE | fio::VmoFlags::READ)
236 .await
237 .map_err(NonMetaStorageError::Fidl)?
238 .map_err(|e| NonMetaStorageError::GetVmo(zx::Status::from_raw(e)))
239 }
240
241 async fn read_blob(&self, hash: &fuchsia_hash::Hash) -> Result<Vec<u8>, NonMetaStorageError> {
242 fuchsia_fs::directory::read_file(self, &hash.to_string())
243 .await
244 .map_err(NonMetaStorageError::ReadBlob)
245 }
246}
247
248pub fn serve(
252 scope: vfs::execution_scope::ExecutionScope,
253 non_meta_storage: impl NonMetaStorage,
254 meta_far: fuchsia_hash::Hash,
255 flags: fio::Flags,
256 server_end: ServerEnd<fio::DirectoryMarker>,
257) -> impl futures::Future<Output = Result<(), Error>> {
258 serve_path(
259 scope,
260 non_meta_storage,
261 meta_far,
262 flags,
263 vfs::Path::dot(),
264 server_end.into_channel().into(),
265 )
266}
267
268pub async fn serve_path(
275 scope: vfs::execution_scope::ExecutionScope,
276 non_meta_storage: impl NonMetaStorage,
277 meta_far: fuchsia_hash::Hash,
278 flags: fio::Flags,
279 path: vfs::Path,
280 server_end: ServerEnd<fio::NodeMarker>,
281) -> Result<(), Error> {
282 let root_dir = match RootDir::new(non_meta_storage, meta_far).await {
283 Ok(d) => d,
284 Err(e) => {
285 let () = send_on_open_with_error(
286 flags.contains(fio::Flags::FLAG_SEND_REPRESENTATION),
287 server_end,
288 (&e).into(),
289 );
290 return Err(e);
291 }
292 };
293
294 ObjectRequest::new(flags, &fio::Options::default(), server_end.into_channel())
295 .handle(|request| root_dir.open(scope, path, flags, request));
296 Ok(())
297}
298
299fn usize_to_u64_safe(u: usize) -> u64 {
300 let ret: u64 = u.try_into().unwrap();
301 static_assertions::assert_eq_size_val!(u, ret);
302 ret
303}
304
305pub trait OnRootDirDrop: Send + Sync + std::fmt::Debug {}
317impl<T> OnRootDirDrop for T where T: Send + Sync + std::fmt::Debug {}
318
319fn get_dir_children<'a>(
326 materialized_tree: impl IntoIterator<Item = &'a str>,
327 dir: &str,
328) -> Vec<(EntryInfo, String)> {
329 let mut added_entries = HashSet::new();
330 let mut res = vec![];
331
332 for path in materialized_tree {
333 if let Some(path) = path.strip_prefix(dir) {
334 match path.split_once('/') {
335 None => {
336 if !added_entries.contains(path) {
338 res.push((
339 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File),
340 path.to_string(),
341 ));
342 added_entries.insert(path.to_string());
343 }
344 }
345 Some((first, _)) => {
346 if !added_entries.contains(first) {
347 res.push((
348 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory),
349 first.to_string(),
350 ));
351 added_entries.insert(first.to_string());
352 }
353 }
354 }
355 }
356 }
357
358 res.sort_by(|a, b| a.1.cmp(&b.1));
360 res
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
366 use assert_matches::assert_matches;
367 use fuchsia_hash::Hash;
368 use fuchsia_pkg_testing::PackageBuilder;
369 use fuchsia_pkg_testing::blobfs::Fake as FakeBlobfs;
370 use futures::StreamExt;
371 use vfs::directory::helper::DirectlyMutable;
372
373 #[fuchsia_async::run_singlethreaded(test)]
374 async fn serve() {
375 let (proxy, server_end) = fidl::endpoints::create_proxy();
376 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
377 let (metafar_blob, _) = package.contents();
378 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
379 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
380
381 crate::serve(
382 vfs::execution_scope::ExecutionScope::new(),
383 blobfs_client,
384 metafar_blob.merkle,
385 fio::PERM_READABLE,
386 server_end,
387 )
388 .await
389 .unwrap();
390
391 assert_eq!(
392 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
393 vec![fuchsia_fs::directory::DirEntry {
394 name: "meta".to_string(),
395 kind: fuchsia_fs::directory::DirentKind::Directory
396 }]
397 );
398 }
399
400 #[fuchsia_async::run_singlethreaded(test)]
401 async fn serve_path_open_root() {
402 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::DirectoryMarker>();
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 crate::serve_path(
409 vfs::execution_scope::ExecutionScope::new(),
410 blobfs_client,
411 metafar_blob.merkle,
412 fio::PERM_READABLE,
413 vfs::Path::validate_and_split(".").unwrap(),
414 server_end.into_channel().into(),
415 )
416 .await
417 .unwrap();
418
419 assert_eq!(
420 fuchsia_fs::directory::readdir(&proxy).await.unwrap(),
421 vec![fuchsia_fs::directory::DirEntry {
422 name: "meta".to_string(),
423 kind: fuchsia_fs::directory::DirentKind::Directory
424 }]
425 );
426 }
427
428 #[fuchsia_async::run_singlethreaded(test)]
429 async fn serve_path_open_meta() {
430 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::FileMarker>();
431 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
432 let (metafar_blob, _) = package.contents();
433 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
434 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
435
436 crate::serve_path(
437 vfs::execution_scope::ExecutionScope::new(),
438 blobfs_client,
439 metafar_blob.merkle,
440 fio::PERM_READABLE | fio::Flags::PROTOCOL_FILE,
441 vfs::Path::validate_and_split("meta").unwrap(),
442 server_end.into_channel().into(),
443 )
444 .await
445 .unwrap();
446
447 assert_eq!(
448 fuchsia_fs::file::read_to_string(&proxy).await.unwrap(),
449 metafar_blob.merkle.to_string(),
450 );
451 }
452
453 #[fuchsia_async::run_singlethreaded(test)]
454 async fn serve_path_open_missing_path_in_package() {
455 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
456 let package = PackageBuilder::new("just-meta-far").build().await.expect("created pkg");
457 let (metafar_blob, _) = package.contents();
458 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
459 blobfs_fake.add_blob(metafar_blob.merkle, metafar_blob.contents);
460
461 assert_matches!(
462 crate::serve_path(
463 vfs::execution_scope::ExecutionScope::new(),
464 blobfs_client,
465 metafar_blob.merkle,
466 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
467 vfs::Path::validate_and_split("not-present").unwrap(),
468 server_end.into_channel().into(),
469 )
470 .await,
471 Ok(())
474 );
475
476 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
477 }
478
479 #[fuchsia_async::run_singlethreaded(test)]
480 async fn serve_path_open_missing_package() {
481 let (proxy, server_end) = fidl::endpoints::create_proxy::<fio::NodeMarker>();
482 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
483
484 assert_matches!(
485 crate::serve_path(
486 vfs::execution_scope::ExecutionScope::new(),
487 blobfs_client,
488 Hash::from([0u8; 32]),
489 fio::PERM_READABLE | fio::Flags::FLAG_SEND_REPRESENTATION,
490 vfs::Path::validate_and_split(".").unwrap(),
491 server_end.into_channel().into(),
492 )
493 .await,
494 Err(Error::MissingMetaFar)
495 );
496
497 assert_eq!(node_into_on_open_status(proxy).await, Some(zx::Status::NOT_FOUND));
498 }
499
500 async fn node_into_on_open_status(node: fio::NodeProxy) -> Option<zx::Status> {
501 let mut events = node.take_event_stream();
504 match events.next().await? {
505 Ok(fio::NodeEvent::OnOpen_ { s: status, .. }) => Some(zx::Status::from_raw(status)),
506 Ok(fio::NodeEvent::OnRepresentation { .. }) => Some(zx::Status::OK),
507 Err(fidl::Error::ClientChannelClosed { status, .. }) => Some(status),
508 other => panic!("unexpected stream event or error: {other:?}"),
509 }
510 }
511
512 fn file() -> EntryInfo {
513 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::File)
514 }
515
516 fn dir() -> EntryInfo {
517 EntryInfo::new(fio::INO_UNKNOWN, fio::DirentType::Directory)
518 }
519
520 #[test]
521 fn get_dir_children_root() {
522 assert_eq!(get_dir_children([], ""), vec![]);
523 assert_eq!(get_dir_children(["a"], ""), vec![(file(), "a".to_string())]);
524 assert_eq!(
525 get_dir_children(["a", "b"], ""),
526 vec![(file(), "a".to_string()), (file(), "b".to_string())]
527 );
528 assert_eq!(
529 get_dir_children(["b", "a"], ""),
530 vec![(file(), "a".to_string()), (file(), "b".to_string())]
531 );
532 assert_eq!(get_dir_children(["a", "a"], ""), vec![(file(), "a".to_string())]);
533 assert_eq!(get_dir_children(["a/b"], ""), vec![(dir(), "a".to_string())]);
534 assert_eq!(
535 get_dir_children(["a/b", "c"], ""),
536 vec![(dir(), "a".to_string()), (file(), "c".to_string())]
537 );
538 assert_eq!(get_dir_children(["a/b/c"], ""), vec![(dir(), "a".to_string())]);
539 }
540
541 #[test]
542 fn get_dir_children_subdir() {
543 assert_eq!(get_dir_children([], "a/"), vec![]);
544 assert_eq!(get_dir_children(["a"], "a/"), vec![]);
545 assert_eq!(get_dir_children(["a", "b"], "a/"), vec![]);
546 assert_eq!(get_dir_children(["a/b"], "a/"), vec![(file(), "b".to_string())]);
547 assert_eq!(
548 get_dir_children(["a/b", "a/c"], "a/"),
549 vec![(file(), "b".to_string()), (file(), "c".to_string())]
550 );
551 assert_eq!(
552 get_dir_children(["a/c", "a/b"], "a/"),
553 vec![(file(), "b".to_string()), (file(), "c".to_string())]
554 );
555 assert_eq!(get_dir_children(["a/b", "a/b"], "a/"), vec![(file(), "b".to_string())]);
556 assert_eq!(get_dir_children(["a/b/c"], "a/"), vec![(dir(), "b".to_string())]);
557 assert_eq!(
558 get_dir_children(["a/b/c", "a/d"], "a/"),
559 vec![(dir(), "b".to_string()), (file(), "d".to_string())]
560 );
561 assert_eq!(get_dir_children(["a/b/c/d"], "a/"), vec![(dir(), "b".to_string())]);
562 }
563
564 const BLOB_CONTENTS: &[u8] = b"blob-contents";
565
566 fn blob_contents_hash() -> Hash {
567 fuchsia_merkle::from_slice(BLOB_CONTENTS).root()
568 }
569
570 #[fuchsia_async::run_singlethreaded(test)]
571 async fn bootfs_get_vmo_blob() {
572 let directory = vfs::directory::immutable::simple();
573 directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
574 let proxy = vfs::directory::serve_read_only(directory);
575
576 let vmo = proxy.get_blob_vmo(&blob_contents_hash()).await.unwrap();
577 assert_eq!(vmo.read_to_vec(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
578 }
579
580 #[fuchsia_async::run_singlethreaded(test)]
581 async fn bootfs_read_blob() {
582 let directory = vfs::directory::immutable::simple();
583 directory.add_entry(blob_contents_hash(), vfs::file::read_only(BLOB_CONTENTS)).unwrap();
584 let proxy = vfs::directory::serve_read_only(directory);
585
586 assert_eq!(proxy.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
587 }
588
589 #[fuchsia_async::run_singlethreaded(test)]
590 async fn bootfs_get_vmo_blob_missing_blob() {
591 let directory = vfs::directory::immutable::simple();
592 let proxy = vfs::directory::serve_read_only(directory);
593
594 let result = proxy.get_blob_vmo(&blob_contents_hash()).await;
595 assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
596 }
597
598 #[fuchsia_async::run_singlethreaded(test)]
599 async fn bootfs_read_blob_missing_blob() {
600 let directory = vfs::directory::immutable::simple();
601 let proxy = vfs::directory::serve_read_only(directory);
602
603 let result = proxy.read_blob(&blob_contents_hash()).await;
604 assert_matches!(result, Err(NonMetaStorageError::ReadBlob(e)) if e.is_not_found_error());
605 }
606
607 #[fuchsia_async::run_singlethreaded(test)]
608 async fn blobfs_get_vmo_blob() {
609 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
610 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
611
612 let vmo =
613 NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await.unwrap();
614 assert_eq!(vmo.read_to_vec(0, BLOB_CONTENTS.len() as u64).unwrap(), BLOB_CONTENTS);
615 }
616
617 #[fuchsia_async::run_singlethreaded(test)]
618 async fn blobfs_read_blob() {
619 let (blobfs_fake, blobfs_client) = FakeBlobfs::new();
620 blobfs_fake.add_blob(blob_contents_hash(), BLOB_CONTENTS);
621
622 assert_eq!(blobfs_client.read_blob(&blob_contents_hash()).await.unwrap(), BLOB_CONTENTS);
623 }
624
625 #[fuchsia_async::run_singlethreaded(test)]
626 async fn blobfs_get_vmo_blob_missing_blob() {
627 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
628
629 let result = NonMetaStorage::get_blob_vmo(&blobfs_client, &blob_contents_hash()).await;
630 assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
631 }
632
633 #[fuchsia_async::run_singlethreaded(test)]
634 async fn blobfs_read_blob_missing_blob() {
635 let (_blobfs_fake, blobfs_client) = FakeBlobfs::new();
636
637 let result = blobfs_client.read_blob(&blob_contents_hash()).await;
638 assert_matches!(result, Err(NonMetaStorageError::OpenBlob(e)) if e.is_not_found_error());
639 }
640}