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::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/// Blobfs client errors.
38#[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/// An error encountered while creating a blob
79#[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
101/// The response to a `BlobCreator.NeedsOverwrite` call, excepting unexpected internal errors.
102pub enum BlobStatus {
103    /// The blob is present and considered up to date.
104    UpToDate,
105
106    /// The blob is present, but should be overwritten.
107    NeedsOverwrite,
108
109    /// The blob is not present.
110    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/// A builder for [`Client`]
123#[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    /// Opens the /blob directory in the component's namespace with readable, writable, and/or
134    /// executable flags. Connects to the fuchsia.fxfs.BlobCreator and BlobReader if requested.
135    /// Connects to and initializes the VmexResource if `use_vmex` is set. Returns a `Client`.
136    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    /// If set, [`Client`] will use this reader implementation instead of connecting to the
183    /// `fuchsia.fxfs.BlobReader` protocol in the component's namespace.
184    pub fn reader(self, reader: impl IntoBlobReader) -> Self {
185        Self { reader: Some(reader.into_blob_reader()), ..self }
186    }
187
188    /// If set, [`Client`] will connect to /blob in the current component's namespace with
189    /// [`fio::PERM_READABLE`].
190    pub fn readable(self) -> Self {
191        Self { readable: true, ..self }
192    }
193
194    /// If set, [`Client`] will connect to /blob in the current component's namespace with
195    /// [`fio::PERM_WRITABLE`] which needed so that [`Client::delete_blob`] can unlink the file,
196    /// and [`Client`] will connect to and use fuchsia.fxfs/BlobCreator for writes.
197    pub fn writable(self) -> Self {
198        Self { writable: true, ..self }
199    }
200
201    /// If set, [`Client`] will connect to /blob in the current component's namespace with
202    /// [`fio::PERM_EXECUTABLE`].
203    pub fn executable(self) -> Self {
204        Self { executable: true, ..self }
205    }
206
207    /// If set, [`Client`] will connect to and use fuchsia.fxfs/BlobCreator for writes.
208    /// This is independent of requiring writable directory access to `/blob`.
209    pub fn creator(self) -> Self {
210        Self { creator: true, ..self }
211    }
212}
213
214impl Client {
215    /// Create an empty `ClientBuilder`
216    pub fn builder() -> ClientBuilder {
217        Default::default()
218    }
219}
220/// Blobfs client
221#[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    /// Returns a client connected to the given blob directory, BlobCreatorProxy, and
230    /// BlobReader. If `vmex` is passed in, sets the VmexResource, which is used to mark blobs
231    /// as executable. If `creator` is not supplied, writes will fail.
232    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    /// Creates a new client backed by the returned request stream. This constructor should not be
245    /// used outside of tests.
246    ///
247    /// # Panics
248    ///
249    /// Panics on error
250    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    /// Creates a new client backed by the returned mock. This constructor should not be used
271    /// outside of tests.
272    ///
273    /// # Panics
274    ///
275    /// Panics on error
276    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    /// Returns the read-only VMO backing the blob.
290    pub async fn get_blob_vmo(&self, hash: &Hash) -> Result<zx::Vmo, GetBlobVmoError> {
291        self.reader.get_vmo(hash).await
292    }
293
294    /// Open a blob for read using open3. `scope` will only be used if the client was configured to
295    /// use fuchsia.fxfs.BlobReader.
296    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        // Errors below will be communicated via the `object_request` channel.
310        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    /// Returns the list of known blobs in blobfs.
316    pub async fn list_known_blobs(&self) -> Result<HashSet<Hash>, BlobfsError> {
317        // fuchsia.io.Directory.ReadDirents uses a per-connection index into the array of
318        // directory entries. To prevent contention over this index by concurrent calls (either
319        // from concurrent calls to list_known_blobs on this object, or on clones of this object,
320        // or other clones of the DirectoryProxy this object was made from), create a new
321        // connection which will have its own index.
322        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    /// Delete the blob with the given merkle hash.
334    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    /// Open a new blob for write.
342    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    /// Returns whether blobfs has a blob with the given hash and blobfs considers it up to date.
354    pub async fn blob_present_and_up_to_date(&self, blob: &Hash) -> bool {
355        // This call is only used when we're considering writing a blob, so we should have a
356        // creator.
357        matches!(
358            self.creator.as_ref().expect("Missing BlobCreator access").needs_overwrite(blob).await,
359            Ok(Ok(false))
360        )
361    }
362
363    /// Looks up the current status of a blob using `BlobCreator.NeedsOverwrite`.
364    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    /// Determines which blobs of `candidates` are missing from blobfs.
379    /// TODO(https://fxbug.dev/338477132) This fn is used during resolves after a meta.far is
380    /// fetched to determine which content blobs and subpackage meta.fars need to be fetched.
381    /// On c++blobfs, opening a partially written blob keeps that blob alive, creating the
382    /// following race condition:
383    /// 1. blob is partially written by resolve A
384    /// 2. blob is opened by this fn to check for presence by concurrent resolve B
385    /// 3. resolve A encounters an error and retries the fetch, which attempts to open the blob for
386    ///    write, which collides with the partially written blob from (1) that is being kept alive
387    ///    by (2) and so fails
388    pub async fn filter_to_missing_blobs(
389        &self,
390        candidates: impl IntoIterator<Item = Hash>,
391    ) -> HashSet<Hash> {
392        // Attempt to open each blob instead of using ReadDirents to catch more forms of filesystem
393        // metadata corruption.
394        // We don't use ReadDirents even as a pre-filter because emulator testing suggests
395        // ReadDirents on an fxblob with 1,000 blobs takes as long as ~60 sequential has_blob calls
396        // on missing blobs, and it's about 5x worse on c++blobfs (on which both ReadDirents is
397        // slower and has_blob is faster). The minor speedup on packages with a great number of
398        // missing blobs is not worth a rarely taken branch deep within package resolution.
399        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            // Emulator testing suggests both c++blobfs and fxblob show diminishing returns after
404            // even three concurrent `has_blob` calls.
405            .buffer_unordered(10)
406            .filter_map(|blob| async move { blob })
407            .collect()
408            .await
409    }
410
411    /// Call fuchsia.io/Node.Sync on the blobfs directory.
412    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
418/// Spawns a task on `scope` to attempt opening `blob` via `reader`. Creates a file connection to
419/// the blob using [`vmo_blob::VmoBlob`]. Errors will be sent via `object_request` asynchronously.
420fn 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
464/// A reader for retrieving blob VMOs by their Merkle root hash.
465pub trait BlobReader: Send + Sync + std::fmt::Debug {
466    /// Returns the read-only VMO backing the blob identified by `blob`.
467    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
482/// Helper trait for types that can be converted into an `Arc<dyn BlobReader>`.
483pub trait IntoBlobReader {
484    /// Converts `self` into an `Arc<dyn BlobReader>`.
485    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    /// Constructs a new [`Client`] connected to the provided [`BlobfsRamdisk`]. Tests in this
503    /// crate should use this constructor rather than [`BlobfsRamdisk::client`], which returns
504    /// the non-cfg(test) build of this crate's [`blobfs::Client`]. While tests could use the
505    /// [`blobfs::Client`] returned by [`BlobfsRamdisk::client`], it will be a different type than
506    /// [`super::Client`], and the tests could not access its private members or any cfg(test)
507    /// specific functionality.
508    ///
509    /// # Panics
510    ///
511    /// Panics on error.
512    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        // ReadDirents returns an 8,192 byte buffer, and each entry is 74 bytes [0] (including 64
775        // bytes of filename), so use more than 110 entries to guarantee that listing all contents
776        // requires multiple ReadDirents calls. This isn't necessary to cause conflict, because
777        // each successful listing requires a call to Rewind as well, but it does make conflict
778        // more likely.
779        // [0] https://cs.opensource.google/fuchsia/fuchsia/+/main:sdk/fidl/fuchsia.io/directory.fidl;l=261;drc=9e84e19d3f42240c46d2b0c3c132c2f0b5a3343f
780        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}