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