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