Skip to main content

blobfs/
lib.rs

1// Copyright 2021 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![deny(missing_docs)]
6
7//! Typesafe wrappers around the /blob filesystem.
8
9use 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/// Blobfs client errors.
36#[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/// An error encountered while creating a blob
77#[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
99/// The response to a `BlobCreator.NeedsOverwrite` call, excepting unexpected internal errors.
100pub enum BlobStatus {
101    /// The blob is present and considered up to date.
102    UpToDate,
103
104    /// The blob is present, but should be overwritten.
105    NeedsOverwrite,
106
107    /// The blob is not present.
108    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/// A builder for [`Client`]
121#[derive(Default)]
122pub struct ClientBuilder {
123    readable: bool,
124    writable: bool,
125    executable: bool,
126    creator: bool,
127}
128
129impl ClientBuilder {
130    /// Opens the /blob directory in the component's namespace with readable, writable, and/or
131    /// executable flags. Connects to the fuchsia.fxfs.BlobCreator and BlobReader if requested.
132    /// Connects to and initializes the VmexResource if `use_vmex` is set. Returns a `Client`.
133    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    /// If set, [`Client`] will connect to /blob in the current component's namespace with
173    /// [`fio::PERM_READABLE`].
174    pub fn readable(self) -> Self {
175        Self { readable: true, ..self }
176    }
177
178    /// If set, [`Client`] will connect to /blob in the current component's namespace with
179    /// [`fio::PERM_WRITABLE`] which needed so that [`Client::delete_blob`] can unlink the file,
180    /// and [`Client`] will connect to and use fuchsia.fxfs/BlobCreator for writes.
181    pub fn writable(self) -> Self {
182        Self { writable: true, ..self }
183    }
184
185    /// If set, [`Client`] will connect to /blob in the current component's namespace with
186    /// [`fio::PERM_EXECUTABLE`].
187    pub fn executable(self) -> Self {
188        Self { executable: true, ..self }
189    }
190
191    /// If set, [`Client`] will connect to and use fuchsia.fxfs/BlobCreator for writes.
192    /// This is independent of requiring writable directory access to `/blob`.
193    pub fn creator(self) -> Self {
194        Self { creator: true, ..self }
195    }
196}
197
198impl Client {
199    /// Create an empty `ClientBuilder`
200    pub fn builder() -> ClientBuilder {
201        Default::default()
202    }
203}
204/// Blobfs client
205#[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    /// Returns a client connected to the given blob directory, BlobCreatorProxy, and
214    /// BlobReaderProxy. If `vmex` is passed in, sets the VmexResource, which is used to mark blobs
215    /// as executable. If `creator` is not supplied, writes will fail.
216    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    /// Creates a new client backed by the returned request stream. This constructor should not be
229    /// used outside of tests.
230    ///
231    /// # Panics
232    ///
233    /// Panics on error
234    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    /// Creates a new client backed by the returned mock. This constructor should not be used
255    /// outside of tests.
256    ///
257    /// # Panics
258    ///
259    /// Panics on error
260    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    /// Returns the read-only VMO backing the blob.
274    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    /// Open a blob for read using open3. `scope` will only be used if the client was configured to
283    /// use fuchsia.fxfs.BlobReader.
284    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        // Errors below will be communicated via the `object_request` channel.
298        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    /// Returns the list of known blobs in blobfs.
304    pub async fn list_known_blobs(&self) -> Result<HashSet<Hash>, BlobfsError> {
305        // fuchsia.io.Directory.ReadDirents uses a per-connection index into the array of
306        // directory entries. To prevent contention over this index by concurrent calls (either
307        // from concurrent calls to list_known_blobs on this object, or on clones of this object,
308        // or other clones of the DirectoryProxy this object was made from), create a new
309        // connection which will have its own index.
310        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    /// Delete the blob with the given merkle hash.
322    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    /// Open a new blob for write.
330    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    /// Returns whether blobfs has a blob with the given hash and blobfs considers it up to date.
342    pub async fn blob_present_and_up_to_date(&self, blob: &Hash) -> bool {
343        // This call is only used when we're considering writing a blob, so we should have a
344        // creator.
345        matches!(
346            self.creator.as_ref().expect("Missing BlobCreator access").needs_overwrite(blob).await,
347            Ok(Ok(false))
348        )
349    }
350
351    /// Looks up the current status of a blob using `BlobCreator.NeedsOverwrite`.
352    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    /// Determines which blobs of `candidates` are missing from blobfs.
367    /// TODO(https://fxbug.dev/338477132) This fn is used during resolves after a meta.far is
368    /// fetched to determine which content blobs and subpackage meta.fars need to be fetched.
369    /// On c++blobfs, opening a partially written blob keeps that blob alive, creating the
370    /// following race condition:
371    /// 1. blob is partially written by resolve A
372    /// 2. blob is opened by this fn to check for presence by concurrent resolve B
373    /// 3. resolve A encounters an error and retries the fetch, which attempts to open the blob for
374    ///    write, which collides with the partially written blob from (1) that is being kept alive
375    ///    by (2) and so fails
376    pub async fn filter_to_missing_blobs(
377        &self,
378        candidates: impl IntoIterator<Item = Hash>,
379    ) -> HashSet<Hash> {
380        // Attempt to open each blob instead of using ReadDirents to catch more forms of filesystem
381        // metadata corruption.
382        // We don't use ReadDirents even as a pre-filter because emulator testing suggests
383        // ReadDirents on an fxblob with 1,000 blobs takes as long as ~60 sequential has_blob calls
384        // on missing blobs, and it's about 5x worse on c++blobfs (on which both ReadDirents is
385        // slower and has_blob is faster). The minor speedup on packages with a great number of
386        // missing blobs is not worth a rarely taken branch deep within package resolution.
387        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            // Emulator testing suggests both c++blobfs and fxblob show diminishing returns after
392            // even three concurrent `has_blob` calls.
393            .buffer_unordered(10)
394            .filter_map(|blob| async move { blob })
395            .collect()
396            .await
397    }
398
399    /// Call fuchsia.io/Node.Sync on the blobfs directory.
400    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
406/// Spawns a task on `scope` to attempt opening `blob` via `reader`. Creates a file connection to
407/// the blob using [`vmo_blob::VmoBlob`]. Errors will be sent via `object_request` asynchronously.
408fn 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    /// Constructs a new [`Client`] connected to the provided [`BlobfsRamdisk`]. Tests in this
452    /// crate should use this constructor rather than [`BlobfsRamdisk::client`], which returns
453    /// the non-cfg(test) build of this crate's [`blobfs::Client`]. While tests could use the
454    /// [`blobfs::Client`] returned by [`BlobfsRamdisk::client`], it will be a different type than
455    /// [`super::Client`], and the tests could not access its private members or any cfg(test)
456    /// specific functionality.
457    ///
458    /// # Panics
459    ///
460    /// Panics on error.
461    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        // ReadDirents returns an 8,192 byte buffer, and each entry is 74 bytes [0] (including 64
724        // bytes of filename), so use more than 110 entries to guarantee that listing all contents
725        // requires multiple ReadDirents calls. This isn't necessary to cause conflict, because
726        // each successful listing requires a call to Rewind as well, but it does make conflict
727        // more likely.
728        // [0] https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.io/directory.fidl;l=261;drc=9e84e19d3f42240c46d2b0c3c132c2f0b5a3343f
729        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}