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