1#![deny(missing_docs)]
6
7use fidl::endpoints::ClientEnd;
10use fidl_fuchsia_fxfs as ffxfs;
11use fidl_fuchsia_io as fio;
12use fuchsia_hash::{Hash, ParseHashError};
13use futures::{StreamExt as _, stream};
14use log::{error, info};
15use std::collections::HashSet;
16use thiserror::Error;
17use vfs::execution_scope::ExecutionScope;
18use vfs::file::StreamIoConnection;
19use vfs::{ObjectRequest, ObjectRequestRef, ProtocolsExt};
20use zx::{self as zx, Status};
21
22pub mod mock;
23pub use mock::Mock;
24
25#[derive(Debug, Error)]
26#[allow(missing_docs)]
27pub enum BlobStatusError {
28 #[error("this client was not created with a blob creator so it cannot write blobs")]
29 WritingNotConfigured,
30
31 #[error("the fidl call returned an unexpected error")]
32 NeedsOverwrite(#[source] Status),
33}
34
35#[derive(Debug, Error)]
37#[allow(missing_docs)]
38pub enum BlobfsError {
39 #[error("while opening blobfs dir")]
40 OpenDir(#[from] fuchsia_fs::node::OpenError),
41
42 #[error("while cloning the blobfs dir")]
43 CloneDir(#[from] fuchsia_fs::node::CloneError),
44
45 #[error("while listing blobfs dir")]
46 ReadDir(#[source] fuchsia_fs::directory::EnumerateError),
47
48 #[error("while deleting blob")]
49 Unlink(#[source] Status),
50
51 #[error("while sync'ing")]
52 Sync(#[source] Status),
53
54 #[error("while parsing blob merkle hash")]
55 ParseHash(#[from] ParseHashError),
56
57 #[error("FIDL error")]
58 Fidl(#[from] fidl::Error),
59
60 #[error("while connecting to fuchsia.fxfs/BlobCreator")]
61 ConnectToBlobCreator(#[source] anyhow::Error),
62
63 #[error("while connecting to fuchsia.fxfs/BlobReader")]
64 ConnectToBlobReader(#[source] anyhow::Error),
65
66 #[error("while setting the VmexResource")]
67 InitVmexResource(#[source] anyhow::Error),
68
69 #[error("directory operation requested but blobfs directory was not configured")]
70 DirectoryNotConfigured,
71
72 #[error("while checking NeedsOverwrite for blob status")]
73 BlobStatus(BlobStatusError),
74}
75
76#[derive(Debug, Error)]
78#[allow(missing_docs)]
79pub enum CreateError {
80 #[error("the blob already exists or is being concurrently written")]
81 AlreadyExists,
82
83 #[error("while creating the blob")]
84 Io(#[source] fuchsia_fs::node::OpenError),
85
86 #[error("while converting the proxy into a client end")]
87 ConvertToClientEnd,
88
89 #[error("FIDL error")]
90 Fidl(#[from] fidl::Error),
91
92 #[error("while calling fuchsia.fxfs/BlobCreator.Create: {0:?}")]
93 BlobCreator(ffxfs::CreateBlobError),
94
95 #[error("this client was not created with a blob creator so it cannot write blobs")]
96 WritingNotConfigured,
97}
98
99pub enum BlobStatus {
101 UpToDate,
103
104 NeedsOverwrite,
106
107 Absent,
109}
110
111impl From<ffxfs::CreateBlobError> for CreateError {
112 fn from(e: ffxfs::CreateBlobError) -> Self {
113 match e {
114 ffxfs::CreateBlobError::AlreadyExists => CreateError::AlreadyExists,
115 e @ ffxfs::CreateBlobError::Internal => CreateError::BlobCreator(e),
116 }
117 }
118}
119
120#[derive(Default)]
122pub struct ClientBuilder {
123 readable: bool,
124 writable: bool,
125 executable: bool,
126 creator: bool,
127}
128
129impl ClientBuilder {
130 pub async fn build(self) -> Result<Client, BlobfsError> {
134 let mut flags = fio::Flags::empty();
135 if self.readable {
136 flags |= fio::PERM_READABLE
137 }
138 if self.writable {
139 flags |= fio::PERM_WRITABLE
140 }
141 if self.executable {
142 flags |= fio::PERM_EXECUTABLE
143 }
144
145 let dir = if !flags.is_empty() {
146 Some(fuchsia_fs::directory::open_in_namespace("/blob", flags)?)
147 } else {
148 None
149 };
150
151 if let Ok(client) = fuchsia_component::client::connect_to_protocol::<
152 fidl_fuchsia_kernel::VmexResourceMarker,
153 >() && let Ok(vmex) = client.get().await
154 {
155 info!("Got vmex resource");
156 vmo_blob::init_vmex_resource(vmex).map_err(BlobfsError::InitVmexResource)?;
157 }
158 let reader = fuchsia_component::client::connect_to_protocol::<ffxfs::BlobReaderMarker>()
159 .map_err(BlobfsError::ConnectToBlobReader)?;
160 let creator = if self.writable || self.creator {
161 Some(
162 fuchsia_component::client::connect_to_protocol::<ffxfs::BlobCreatorMarker>()
163 .map_err(BlobfsError::ConnectToBlobCreator)?,
164 )
165 } else {
166 None
167 };
168
169 Ok(Client { dir, creator, reader })
170 }
171
172 pub fn readable(self) -> Self {
175 Self { readable: true, ..self }
176 }
177
178 pub fn writable(self) -> Self {
182 Self { writable: true, ..self }
183 }
184
185 pub fn executable(self) -> Self {
188 Self { executable: true, ..self }
189 }
190
191 pub fn creator(self) -> Self {
194 Self { creator: true, ..self }
195 }
196}
197
198impl Client {
199 pub fn builder() -> ClientBuilder {
201 Default::default()
202 }
203}
204#[derive(Debug, Clone)]
206pub struct Client {
207 dir: Option<fio::DirectoryProxy>,
208 creator: Option<ffxfs::BlobCreatorProxy>,
209 reader: ffxfs::BlobReaderProxy,
210}
211
212impl Client {
213 pub fn new(
217 dir: fio::DirectoryProxy,
218 creator: Option<ffxfs::BlobCreatorProxy>,
219 reader: ffxfs::BlobReaderProxy,
220 vmex: Option<zx::Resource>,
221 ) -> Result<Self, anyhow::Error> {
222 if let Some(vmex) = vmex {
223 vmo_blob::init_vmex_resource(vmex)?;
224 }
225 Ok(Self { dir: Some(dir), creator, reader })
226 }
227
228 pub fn new_test() -> (
235 Self,
236 fio::DirectoryRequestStream,
237 ffxfs::BlobReaderRequestStream,
238 ffxfs::BlobCreatorRequestStream,
239 ) {
240 let (dir, dir_stream) = fidl::endpoints::create_proxy_and_stream::<fio::DirectoryMarker>();
241 let (reader, reader_stream) =
242 fidl::endpoints::create_proxy_and_stream::<ffxfs::BlobReaderMarker>();
243 let (creator, creator_stream) =
244 fidl::endpoints::create_proxy_and_stream::<ffxfs::BlobCreatorMarker>();
245
246 (
247 Self { dir: Some(dir), creator: Some(creator), reader },
248 dir_stream,
249 reader_stream,
250 creator_stream,
251 )
252 }
253
254 pub fn new_mock() -> (Self, mock::Mock) {
261 let (dir, stream) = fidl::endpoints::create_proxy_and_stream::<fio::DirectoryMarker>();
262 let (reader, reader_stream) =
263 fidl::endpoints::create_proxy_and_stream::<ffxfs::BlobReaderMarker>();
264 let (creator, creator_stream) =
265 fidl::endpoints::create_proxy_and_stream::<ffxfs::BlobCreatorMarker>();
266
267 (
268 Self { dir: Some(dir), creator: Some(creator), reader },
269 mock::Mock { stream, reader_stream, creator_stream },
270 )
271 }
272
273 pub async fn get_blob_vmo(&self, hash: &Hash) -> Result<zx::Vmo, GetBlobVmoError> {
275 self.reader
276 .get_vmo(hash)
277 .await
278 .map_err(GetBlobVmoError::Fidl)?
279 .map_err(|s| GetBlobVmoError::GetVmo(Status::from_raw(s)))
280 }
281
282 pub fn open_blob_for_read(
285 &self,
286 blob: &Hash,
287 flags: fio::Flags,
288 scope: ExecutionScope,
289 object_request: ObjectRequestRef<'_>,
290 ) -> Result<(), zx::Status> {
291 if flags.rights().is_some_and(|rights| rights.contains(fio::Operations::WRITE_BYTES)) {
292 return Err(zx::Status::ACCESS_DENIED);
293 }
294 if flags.creation_mode() != vfs::CreationMode::Never {
295 return Err(zx::Status::NOT_SUPPORTED);
296 }
297 let object_request = object_request.take();
299 let () = open_blob_with_reader(self.reader.clone(), *blob, scope, flags, object_request);
300 Ok(())
301 }
302
303 pub async fn list_known_blobs(&self) -> Result<HashSet<Hash>, BlobfsError> {
305 let dir = self.dir.as_ref().ok_or(BlobfsError::DirectoryNotConfigured)?;
311 let private_connection = fuchsia_fs::directory::clone(dir)?;
312 fuchsia_fs::directory::readdir(&private_connection)
313 .await
314 .map_err(BlobfsError::ReadDir)?
315 .into_iter()
316 .filter(|entry| entry.kind == fuchsia_fs::directory::DirentKind::File)
317 .map(|entry| entry.name.parse().map_err(BlobfsError::ParseHash))
318 .collect()
319 }
320
321 pub async fn delete_blob(&self, blob: &Hash) -> Result<(), BlobfsError> {
323 let dir = self.dir.as_ref().ok_or(BlobfsError::DirectoryNotConfigured)?;
324 dir.unlink(&blob.to_string(), &fio::UnlinkOptions::default())
325 .await?
326 .map_err(|s| BlobfsError::Unlink(Status::from_raw(s)))
327 }
328
329 pub async fn open_blob_for_write(
331 &self,
332 blob: &Hash,
333 allow_existing: bool,
334 ) -> Result<ClientEnd<ffxfs::BlobWriterMarker>, CreateError> {
335 let Some(creator) = &self.creator else {
336 return Err(CreateError::WritingNotConfigured);
337 };
338 Ok(creator.create(blob, allow_existing).await??)
339 }
340
341 pub async fn blob_present_and_up_to_date(&self, blob: &Hash) -> bool {
343 matches!(
346 self.creator.as_ref().expect("Missing BlobCreator access").needs_overwrite(blob).await,
347 Ok(Ok(false))
348 )
349 }
350
351 pub async fn blob_status(&self, blob: &Hash) -> Result<BlobStatus, BlobfsError> {
353 let Some(creator) = &self.creator else {
354 return Err(BlobfsError::BlobStatus(BlobStatusError::WritingNotConfigured));
355 };
356 match creator.needs_overwrite(blob).await? {
357 Ok(true) => Ok(BlobStatus::NeedsOverwrite),
358 Ok(false) => Ok(BlobStatus::UpToDate),
359 Err(status) if status == Status::NOT_FOUND.into_raw() => Ok(BlobStatus::Absent),
360 Err(s) => {
361 Err(BlobfsError::BlobStatus(BlobStatusError::NeedsOverwrite(Status::from_raw(s))))
362 }
363 }
364 }
365
366 pub async fn filter_to_missing_blobs(
377 &self,
378 candidates: impl IntoIterator<Item = Hash>,
379 ) -> HashSet<Hash> {
380 stream::iter(candidates)
388 .map(move |blob| async move {
389 if self.blob_present_and_up_to_date(&blob).await { None } else { Some(blob) }
390 })
391 .buffer_unordered(10)
394 .filter_map(|blob| async move { blob })
395 .collect()
396 .await
397 }
398
399 pub async fn sync(&self) -> Result<(), BlobfsError> {
401 let dir = self.dir.as_ref().ok_or(BlobfsError::DirectoryNotConfigured)?;
402 dir.sync().await?.map_err(zx::Status::from_raw).map_err(BlobfsError::Sync)
403 }
404}
405
406fn open_blob_with_reader<P: ProtocolsExt + Send>(
409 reader: ffxfs::BlobReaderProxy,
410 blob_hash: Hash,
411 scope: ExecutionScope,
412 protocols: P,
413 object_request: ObjectRequest,
414) {
415 scope.clone().spawn(object_request.handle_async(async move |object_request| {
416 let get_vmo_result = reader.get_vmo(&blob_hash.into()).await.map_err(|fidl_error| {
417 if let fidl::Error::ClientChannelClosed { epitaph, .. } = fidl_error {
418 error!("Blob reader channel closed: {epitaph:?}");
419 match epitaph.into() {
420 Err(status) => status,
421 Ok(()) => zx::Status::PEER_CLOSED,
422 }
423 } else {
424 error!("Transport error on get_vmo: {:?}", fidl_error);
425 zx::Status::INTERNAL
426 }
427 })?;
428 let vmo = get_vmo_result.map_err(zx::Status::from_raw)?;
429 let vmo_blob = vmo_blob::VmoBlob::new(vmo);
430 object_request
431 .create_connection::<StreamIoConnection<_>, _>(scope, vmo_blob, protocols)
432 .await
433 }));
434}
435
436#[derive(thiserror::Error, Debug)]
437#[allow(missing_docs)]
438pub enum GetBlobVmoError {
439 #[error("getting the vmo")]
440 GetVmo(#[source] Status),
441
442 #[error("opening the blob")]
443 OpenBlob(#[source] fuchsia_fs::node::OpenError),
444
445 #[error("making a fidl request")]
446 Fidl(#[source] fidl::Error),
447}
448
449#[cfg(test)]
450impl Client {
451 pub fn for_ramdisk(blobfs: &blobfs_ramdisk::BlobfsRamdisk) -> Self {
462 Self::new(
463 blobfs.root_dir_proxy().unwrap(),
464 Some(blobfs.blob_creator_proxy().unwrap()),
465 blobfs.blob_reader_proxy().unwrap(),
466 None,
467 )
468 .unwrap()
469 }
470}
471
472#[cfg(test)]
473#[allow(clippy::bool_assert_comparison)]
474mod tests {
475 use super::*;
476 use assert_matches::assert_matches;
477 use blobfs_ramdisk::BlobfsRamdisk;
478 use fuchsia_async as fasync;
479 use futures::stream::TryStreamExt as _;
480 use std::sync::Arc;
481 use test_case::test_case;
482
483 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
484 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
485 #[fuchsia::test]
486 async fn list_known_blobs_empty(blob_impl: blobfs_ramdisk::Implementation) {
487 let blobfs = BlobfsRamdisk::builder().implementation(blob_impl).start().await.unwrap();
488 let client = Client::for_ramdisk(&blobfs);
489
490 assert_eq!(client.list_known_blobs().await.unwrap(), HashSet::new());
491 blobfs.stop().await.unwrap();
492 }
493
494 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
495 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
496 #[fuchsia::test]
497 async fn list_known_blobs(blob_impl: blobfs_ramdisk::Implementation) {
498 let blobfs = BlobfsRamdisk::builder()
499 .implementation(blob_impl)
500 .with_blob(&b"blob 1"[..])
501 .with_blob(&b"blob 2"[..])
502 .start()
503 .await
504 .unwrap();
505 let client = Client::for_ramdisk(&blobfs);
506
507 let expected = blobfs.list_blobs().unwrap().into_iter().collect();
508 assert_eq!(client.list_known_blobs().await.unwrap(), expected);
509 blobfs.stop().await.unwrap();
510 }
511
512 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
513 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
514 #[fuchsia::test]
515 async fn delete_blob_and_then_list(blob_impl: blobfs_ramdisk::Implementation) {
516 let blobfs = BlobfsRamdisk::builder()
517 .implementation(blob_impl)
518 .with_blob(&b"blob 1"[..])
519 .with_blob(&b"blob 2"[..])
520 .start()
521 .await
522 .unwrap();
523 let client = Client::for_ramdisk(&blobfs);
524
525 let merkle = fuchsia_merkle::root_from_slice(b"blob 1");
526 assert_matches!(client.delete_blob(&merkle).await, Ok(()));
527
528 let expected = HashSet::from([fuchsia_merkle::root_from_slice(b"blob 2")]);
529 assert_eq!(client.list_known_blobs().await.unwrap(), expected);
530 blobfs.stop().await.unwrap();
531 }
532
533 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
534 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
535 #[fuchsia::test]
536 async fn delete_nonexistent_blob(blob_impl: blobfs_ramdisk::Implementation) {
537 let blobfs = BlobfsRamdisk::builder().implementation(blob_impl).start().await.unwrap();
538 let client = Client::for_ramdisk(&blobfs);
539 let blob_merkle = Hash::from([1; 32]);
540
541 assert_matches!(
542 client.delete_blob(&blob_merkle).await,
543 Err(BlobfsError::Unlink(Status::NOT_FOUND))
544 );
545 blobfs.stop().await.unwrap();
546 }
547
548 #[fuchsia::test]
549 async fn delete_blob_mock() {
550 let (client, mut stream, _, _) = Client::new_test();
551 let blob_merkle = Hash::from([1; 32]);
552 fasync::Task::spawn(async move {
553 match stream.try_next().await.unwrap().unwrap() {
554 fio::DirectoryRequest::Unlink { name, responder, .. } => {
555 assert_eq!(name, blob_merkle.to_string());
556 responder.send(Ok(())).unwrap();
557 }
558 other => panic!("unexpected request: {other:?}"),
559 }
560 })
561 .detach();
562
563 assert_matches!(client.delete_blob(&blob_merkle).await, Ok(()));
564 }
565
566 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
567 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
568 #[fuchsia::test]
569 async fn has_blob(blob_impl: blobfs_ramdisk::Implementation) {
570 let blobfs = BlobfsRamdisk::builder()
571 .implementation(blob_impl)
572 .with_blob(&b"blob 1"[..])
573 .start()
574 .await
575 .unwrap();
576 let client = Client::for_ramdisk(&blobfs);
577
578 assert!(
579 client.blob_present_and_up_to_date(&fuchsia_merkle::root_from_slice(b"blob 1")).await
580 );
581 assert!(!client.blob_present_and_up_to_date(&Hash::from([1; 32])).await);
582
583 blobfs.stop().await.unwrap();
584 }
585
586 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
587 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
588 #[fuchsia::test]
589 async fn has_blob_return_false_if_blob_is_partially_written(
590 blob_impl: blobfs_ramdisk::Implementation,
591 ) {
592 let blobfs = BlobfsRamdisk::builder().implementation(blob_impl).start().await.unwrap();
593 let client = Client::for_ramdisk(&blobfs);
594
595 let content = &[3; 1024];
596 let hash = fuchsia_merkle::root_from_slice(content);
597 let delivery_content =
598 delivery_blob::Type1Blob::generate(content, delivery_blob::CompressionMode::Always);
599
600 let writer = client.open_blob_for_write(&hash, false).await.unwrap().into_proxy();
601 assert!(!client.blob_present_and_up_to_date(&hash).await);
602
603 let n = delivery_content.len();
604 let vmo = writer.get_vmo(n.try_into().unwrap()).await.unwrap().unwrap();
605 assert!(!client.blob_present_and_up_to_date(&hash).await);
606
607 let () = vmo.write(&delivery_content[0..n - 1], 0).unwrap();
608 let () = writer.bytes_ready((n - 1).try_into().unwrap()).await.unwrap().unwrap();
609 assert!(!client.blob_present_and_up_to_date(&hash).await);
610
611 let () = vmo.write(&delivery_content[n - 1..], (n - 1).try_into().unwrap()).unwrap();
612 let () = writer.bytes_ready(1.try_into().unwrap()).await.unwrap().unwrap();
613 assert!(client.blob_present_and_up_to_date(&hash).await);
614
615 blobfs.stop().await.unwrap();
616 }
617
618 async fn fully_write_blob(client: &Client, content: &[u8]) -> Hash {
619 let hash = fuchsia_merkle::root_from_slice(content);
620 let delivery_content =
621 delivery_blob::Type1Blob::generate(content, delivery_blob::CompressionMode::Always);
622 let writer = client.open_blob_for_write(&hash, false).await.unwrap().into_proxy();
623 let vmo = writer
624 .get_vmo(delivery_content.len().try_into().unwrap())
625 .await
626 .expect("a")
627 .map_err(zx::Status::from_raw)
628 .expect("b");
629 let () = vmo.write(&delivery_content, 0).unwrap();
630 let () =
631 writer.bytes_ready(delivery_content.len().try_into().unwrap()).await.unwrap().unwrap();
632 hash
633 }
634
635 #[test_case(blobfs_ramdisk::Implementation::CppBlobfs; "cpp_blobfs")]
636 #[test_case(blobfs_ramdisk::Implementation::Fxblob; "fxblob")]
637 #[fuchsia::test]
638 async fn filter_to_missing_blobs(blob_impl: blobfs_ramdisk::Implementation) {
639 let blobfs = BlobfsRamdisk::builder().implementation(blob_impl).start().await.unwrap();
640 let client = Client::for_ramdisk(&blobfs);
641
642 let missing_hash0 = Hash::from([0; 32]);
643 let missing_hash1 = Hash::from([1; 32]);
644
645 let present_blob0 = fully_write_blob(&client, &[2; 1024]).await;
646 let present_blob1 = fully_write_blob(&client, &[3; 1024]).await;
647
648 assert_eq!(
649 client
650 .filter_to_missing_blobs([
651 missing_hash0,
652 missing_hash1,
653 present_blob0,
654 present_blob1
655 ])
656 .await,
657 HashSet::from([missing_hash0, missing_hash1])
658 );
659
660 blobfs.stop().await.unwrap();
661 }
662
663 #[fuchsia::test]
664 async fn sync() {
665 let counter = Arc::new(std::sync::atomic::AtomicU32::new(0));
666 let counter_clone = Arc::clone(&counter);
667 let (client, mut stream, _, _) = Client::new_test();
668 fasync::Task::spawn(async move {
669 match stream.try_next().await.unwrap().unwrap() {
670 fio::DirectoryRequest::Sync { responder } => {
671 counter_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
672 responder.send(Ok(())).unwrap();
673 }
674 other => panic!("unexpected request: {other:?}"),
675 }
676 })
677 .detach();
678
679 assert_matches!(client.sync().await, Ok(()));
680 assert_eq!(counter.load(std::sync::atomic::Ordering::SeqCst), 1);
681 }
682
683 #[fuchsia::test]
684 async fn open_blob_for_write_maps_already_exists() {
685 let (blob_creator, mut blob_creator_stream) =
686 fidl::endpoints::create_proxy_and_stream::<ffxfs::BlobCreatorMarker>();
687 let (blob_reader, _) = fidl::endpoints::create_proxy::<ffxfs::BlobReaderMarker>();
688
689 let client = Client::new(
690 fidl::endpoints::create_proxy::<fio::DirectoryMarker>().0,
691 Some(blob_creator),
692 blob_reader,
693 None,
694 )
695 .unwrap();
696
697 fuchsia_async::Task::spawn(async move {
698 match blob_creator_stream.next().await.unwrap().unwrap() {
699 ffxfs::BlobCreatorRequest::Create { hash, allow_existing, responder } => {
700 assert_eq!(hash, [0; 32]);
701 assert!(!allow_existing);
702 let () = responder.send(Err(ffxfs::CreateBlobError::AlreadyExists)).unwrap();
703 }
704 ffxfs::BlobCreatorRequest::NeedsOverwrite { .. } => {
705 unreachable!("This code path is not yet exercised.");
706 }
707 }
708 })
709 .detach();
710
711 assert_matches!(
712 client.open_blob_for_write(&[0; 32].into(), false).await,
713 Err(CreateError::AlreadyExists)
714 );
715 }
716
717 #[fuchsia::test]
718 async fn concurrent_list_known_blobs_all_return_full_contents() {
719 use futures::StreamExt;
720 let blobfs = BlobfsRamdisk::builder().start().await.unwrap();
721 let client = Client::for_ramdisk(&blobfs);
722
723 for i in 0..256u16 {
730 let _: Hash = fully_write_blob(&client, i.to_le_bytes().as_slice()).await;
731 }
732
733 let () = futures::stream::iter(0..100)
734 .for_each_concurrent(None, |_| async {
735 assert_eq!(client.list_known_blobs().await.unwrap().len(), 256);
736 })
737 .await;
738 }
739}