Skip to main content

fxfs_platform/fuchsia/fxblob/
directory.rs

1// Copyright 2023 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//! This module contains the [`BlobDirectory`] node type used to represent a directory of immutable
6//! content-addressable blobs.
7
8use crate::fuchsia::component::map_to_raw_status;
9use crate::fuchsia::directory::FxDirectory;
10use crate::fuchsia::dirent_cache::DirentCacheKey;
11use crate::fuchsia::fxblob::blob::FxBlob;
12use crate::fuchsia::fxblob::mapping_provider::BlobMappingProvider;
13use crate::fuchsia::fxblob::writer::DeliveryBlobWriter;
14use crate::fuchsia::node::{FxNode, GetResult, OpenedNode};
15use crate::fuchsia::volume::{FxVolume, RootDir};
16use anyhow::{Context as _, Error, anyhow, ensure};
17use fidl::endpoints::{ClientEnd, DiscoverableProtocolMarker, ServerEnd, create_request_stream};
18use fidl_fuchsia_fxfs::{
19    BlobCreatorMarker, BlobCreatorRequest, BlobCreatorRequestStream, BlobReaderMarker,
20    BlobReaderRequest, BlobReaderRequestStream, BlobWriterMarker, CreateBlobError,
21};
22use fidl_fuchsia_io::{self as fio, FilesystemInfo, NodeMarker, WatchMask};
23use fidl_fuchsia_storage_block as fblock;
24use fidl_fuchsia_storage_mapping::MappingProviderMarker;
25use fuchsia_hash::Hash;
26use futures::TryStreamExt;
27use fxfs::errors::FxfsError;
28use fxfs::object_store::transaction::{LockKey, lock_keys};
29use fxfs::object_store::{self, HandleOptions, ObjectDescriptor, ObjectStore, StoreObjectHandle};
30use fxfs_macros::ToWeakNode;
31use fxfs_trace::{TraceFutureExt, trace_future_args};
32use std::str::FromStr;
33use std::sync::Arc;
34use vfs::directory::dirents_sink;
35use vfs::directory::entry::{DirectoryEntry, EntryInfo, GetEntryInfo, OpenRequest};
36use vfs::directory::entry_container::{
37    Directory as VfsDirectory, DirectoryWatcher, MutableDirectory,
38};
39use vfs::directory::helper::DirectlyMutable;
40use vfs::directory::mutable::connection::MutableConnection;
41use vfs::directory::simple::Simple;
42use vfs::directory::traversal_position::TraversalPosition;
43use vfs::execution_scope::ExecutionScope;
44use vfs::path::Path;
45use vfs::{ObjectRequestRef, ProtocolsExt, ToObjectRequest};
46use zx::Status;
47
48/// A flat directory containing content-addressable blobs (names are their hashes).
49/// It is not possible to create sub-directories.
50/// It is not possible to write to an existing blob.
51/// It is not possible to open or read a blob until it is written and verified.
52#[derive(ToWeakNode)]
53pub struct BlobDirectory {
54    directory: Arc<FxDirectory>,
55}
56
57/// Instead of constantly switching back and forth between strings and hashes. Do it once and then
58/// just pass around a reference to that.
59pub(crate) struct Identifier {
60    pub string: String,
61    pub hash: Hash,
62}
63
64impl TryFrom<&str> for Identifier {
65    type Error = FxfsError;
66    fn try_from(value: &str) -> Result<Self, Self::Error> {
67        Ok(Self {
68            string: value.to_owned(),
69            hash: Hash::from_str(value).map_err(|_| FxfsError::InvalidArgs)?,
70        })
71    }
72}
73
74impl From<Hash> for Identifier {
75    fn from(hash: Hash) -> Self {
76        Self { string: hash.to_string(), hash }
77    }
78}
79
80impl RootDir for BlobDirectory {
81    fn as_directory_entry(self: Arc<Self>) -> Arc<dyn DirectoryEntry> {
82        self
83    }
84
85    fn serve(self: Arc<Self>, flags: fio::Flags, server_end: ServerEnd<fio::DirectoryMarker>) {
86        let scope = self.volume().scope().clone();
87        vfs::directory::serve_on(self, flags, scope, server_end);
88    }
89
90    fn as_node(self: Arc<Self>) -> Arc<dyn FxNode> {
91        self as Arc<dyn FxNode>
92    }
93
94    fn register_additional_volume_services(self: Arc<Self>, svc_dir: &Simple) -> Result<(), Error> {
95        let this = self.clone();
96        svc_dir.add_entry(
97            BlobCreatorMarker::PROTOCOL_NAME,
98            vfs::service::host(move |r| this.clone().handle_blob_creator_requests(r)),
99        )?;
100
101        let mapping_provider = Arc::new(BlobMappingProvider::new(self.clone())?);
102        svc_dir.add_entry(
103            MappingProviderMarker::PROTOCOL_NAME,
104            vfs::service::host(move |r| {
105                mapping_provider.clone().handle_mapping_provider_requests(r)
106            }),
107        )?;
108
109        let this = self.clone();
110        svc_dir.add_entry(
111            BlobReaderMarker::PROTOCOL_NAME,
112            vfs::service::host(move |r| this.clone().handle_blob_reader_requests(r)),
113        )?;
114
115        svc_dir.add_entry(
116            fblock::MapperMarker::PROTOCOL_NAME,
117            vfs::service::endpoint(move |scope, channel| {
118                let device = self.store().device().clone();
119                scope.spawn(async move {
120                    if let Err(status) =
121                        device.connect_mapper(channel.into_zx_channel().into()).await
122                    {
123                        log::warn!(status:?; "Failed to connect to Mapper");
124                    }
125                });
126            }),
127        )?;
128
129        Ok(())
130    }
131}
132
133impl BlobDirectory {
134    fn new(directory: FxDirectory) -> Self {
135        Self { directory: Arc::new(directory) }
136    }
137
138    pub fn directory(&self) -> &Arc<FxDirectory> {
139        &self.directory
140    }
141
142    pub fn volume(&self) -> &Arc<FxVolume> {
143        self.directory.volume()
144    }
145
146    fn store(&self) -> &ObjectStore {
147        self.directory.store()
148    }
149
150    /// Open blob and get the child vmo. This allows the creation of the child vmo to be atomic with
151    /// the open.
152    pub(crate) async fn open_blob_get_vmo(
153        self: &Arc<Self>,
154        id: &Identifier,
155    ) -> Result<(Arc<FxBlob>, zx::Vmo), Error> {
156        let store = self.store();
157        let fs = store.filesystem();
158        let keys = lock_keys![LockKey::object(store.store_object_id(), self.directory.object_id())];
159        // A lock needs to be held over searching the directory and incrementing the open count.
160        let _guard = fs.lock_manager().read_lock(keys.clone()).await;
161        let blob = self.open_blob_locked(id).await?.ok_or(FxfsError::NotFound)?;
162        let vmo = blob.create_child_vmo()?;
163        // Downgrade from an OpenedNode<Node> to a Node.
164        Ok((blob.clone(), vmo))
165    }
166
167    /// Wraps ['open_blob_locked'] while taking the locks.
168    pub(crate) async fn open_blob(
169        self: &Arc<Self>,
170        id: &Identifier,
171    ) -> Result<Option<OpenedNode<FxBlob>>, Error> {
172        let store = self.store();
173        let fs = store.filesystem();
174        let keys = lock_keys![LockKey::object(store.store_object_id(), self.directory.object_id())];
175        // A lock needs to be held over searching the directory and incrementing the open count.
176        let _guard = fs.lock_manager().read_lock(keys.clone()).await;
177        self.open_blob_locked(id).await
178    }
179
180    /// Attempt to open and cache the blob with `id` in this directory. Returns `Ok(None)` if no
181    /// blob matching `id` was found. Requires holding locks for at least the object store and
182    /// directory object.
183    async fn open_blob_locked(
184        self: &Arc<Self>,
185        id: &Identifier,
186    ) -> Result<Option<OpenedNode<FxBlob>>, Error> {
187        let node = match self.directory.directory().owner().dirent_cache().lookup(&(
188            self.directory.object_id(),
189            &id.string,
190            false,
191        )) {
192            Some(node) => Some(node),
193            None => {
194                if let Some((object_id, _, _)) =
195                    self.directory.directory().lookup(&id.string).await?
196                {
197                    let node = self.get_or_load_node(object_id, &id).await?;
198                    self.directory.directory().owner().dirent_cache().insert(
199                        DirentCacheKey::new(self.directory.object_id(), id.string.clone(), false),
200                        node.clone(),
201                    );
202                    Some(node)
203                } else {
204                    None
205                }
206            }
207        };
208        let Some(node) = node else {
209            return Ok(None);
210        };
211        if node.object_descriptor() != ObjectDescriptor::File {
212            return Err(FxfsError::Inconsistent)
213                .with_context(|| format!("Blob {} has invalid object descriptor!", id.string));
214        }
215        node.into_any()
216            .downcast::<FxBlob>()
217            .map(|node| Some(OpenedNode::new(node)))
218            .map_err(|_| FxfsError::Inconsistent)
219            .with_context(|| format!("Blob {} has incorrect node type!", id.string))
220    }
221
222    // Attempts to get a node from the node cache. If the node wasn't present in the cache, loads
223    // the object from the object store, installing the returned node into the cache and returns the
224    // newly created FxNode backed by the loaded object.
225    async fn get_or_load_node(
226        self: &Arc<Self>,
227        object_id: u64,
228        id: &Identifier,
229    ) -> Result<Arc<dyn FxNode>, Error> {
230        let volume = self.volume();
231        match volume.cache().get_or_reserve(object_id).await {
232            GetResult::Node(node) => {
233                // Protecting against the scenario where a directory entry points to another node
234                // which has already been loaded and verified with the correct hash. We need to
235                // verify that the hash for the blob that is cached here matches the requested hash.
236                let blob = node.into_any().downcast::<FxBlob>().map_err(|_| {
237                    anyhow!(FxfsError::Inconsistent).context("Loaded non-blob from cache")
238                })?;
239                ensure!(
240                    blob.root() == id.hash,
241                    anyhow!(FxfsError::Inconsistent)
242                        .context("Loaded blob by node that did not match the given hash")
243                );
244                Ok(blob as Arc<dyn FxNode>)
245            }
246            GetResult::Placeholder(placeholder) => {
247                let handle = StoreObjectHandle::new(
248                    volume.clone(),
249                    object_id,
250                    /*permanent_keys=*/ false,
251                    HandleOptions::default(),
252                    /*trace=*/ false,
253                );
254                let node = FxBlob::new(handle, id.hash).await? as Arc<dyn FxNode>;
255                placeholder.commit(&node);
256                Ok(node)
257            }
258        }
259    }
260
261    /// Creates a [`ClientEnd<BlobWriterMarker>`] to write the delivery blob identified by `hash`.
262    /// It is safe to create multiple writers for a given `hash`, however only one will succeed.
263    /// Requests are handled asynchronously on this volume's execution scope.
264    async fn create_blob_writer(
265        self: &Arc<Self>,
266        hash: Hash,
267        allow_existing: bool,
268    ) -> Result<ClientEnd<BlobWriterMarker>, CreateBlobError> {
269        let id = hash.into();
270        let blob_exists = self
271            .open_blob(&id)
272            .await
273            .map_err(|e| {
274                log::error!("Failed to lookup blob: {:?}", e);
275                CreateBlobError::Internal
276            })?
277            .is_some();
278        if blob_exists && !allow_existing {
279            return Err(CreateBlobError::AlreadyExists);
280        }
281        let (client_end, request_stream) = create_request_stream::<BlobWriterMarker>();
282        let writer = DeliveryBlobWriter::new(self, hash).await.map_err(|e| {
283            log::error!("Failed to create blob writer: {:?}", e);
284            CreateBlobError::Internal
285        })?;
286        self.volume().scope().spawn(async move {
287            if let Err(e) = writer.handle_requests(request_stream).await {
288                log::error!("Failed to handle BlobWriter requests: {}", e);
289            }
290        });
291        return Ok(client_end);
292    }
293
294    async fn needs_overwrite(&self, blob_hash: Identifier) -> Result<bool, Error> {
295        // We don't take a lock here because this will only look up existence for now. If we
296        // actually start fetching the blob or info about it after looking it up this will need to
297        // take a reader lock on the directory and maybe also the object.
298        if self
299            .volume()
300            .dirent_cache()
301            .lookup(&(self.object_id(), &blob_hash.string, false))
302            .is_some()
303        {
304            return Ok(false);
305        }
306        match self.directory.directory().lookup(&blob_hash.string).await? {
307            Some(_) => Ok(false),
308            None => Err(FxfsError::NotFound.into()),
309        }
310    }
311
312    async fn handle_blob_creator_requests(self: Arc<Self>, mut requests: BlobCreatorRequestStream) {
313        while let Ok(Some(request)) = requests.try_next().await {
314            match request {
315                BlobCreatorRequest::Create { responder, hash, allow_existing } => {
316                    async {
317                        responder
318                            .send(self.create_blob_writer(Hash::from(hash), allow_existing).await)
319                            .unwrap_or_else(|error| {
320                                log::error!(error:?; "failed to send Create response");
321                            });
322                    }
323                    .trace(trace_future_args!("BlobCreator::Create"))
324                    .await;
325                }
326                BlobCreatorRequest::NeedsOverwrite { blob_hash, responder } => {
327                    async {
328                        let _ = responder.send(
329                            self.needs_overwrite(Hash::from(blob_hash).into())
330                                .await
331                                .map_err(map_to_raw_status),
332                        );
333                    }
334                    .trace(trace_future_args!("BlobCreator::NeedsOverwrite"))
335                    .await;
336                }
337            }
338        }
339    }
340
341    async fn handle_blob_reader_requests(self: Arc<Self>, mut requests: BlobReaderRequestStream) {
342        while let Ok(Some(request)) = requests.try_next().await {
343            match request {
344                BlobReaderRequest::GetVmo { blob_hash, responder } => {
345                    async {
346                        responder
347                            .send(
348                                self.get_blob_vmo(blob_hash.into())
349                                    .await
350                                    .map_err(map_to_raw_status),
351                            )
352                            .unwrap_or_else(|error| {
353                                log::error!(error:?; "failed to send GetVmo response");
354                            });
355                    }
356                    .trace(trace_future_args!("BlobReader::GetVmo"))
357                    .await;
358                }
359            };
360        }
361    }
362
363    async fn open_impl(
364        self: Arc<Self>,
365        scope: ExecutionScope,
366        path: Path,
367        flags: impl ProtocolsExt,
368        object_request: ObjectRequestRef<'_>,
369    ) -> Result<(), Status> {
370        if path.is_empty() {
371            object_request
372                .create_connection::<MutableConnection<_>, _>(
373                    scope,
374                    OpenedNode::new(self).take(),
375                    flags,
376                )
377                .await
378        } else {
379            Err(Status::NOT_SUPPORTED)
380        }
381    }
382}
383
384impl FxNode for BlobDirectory {
385    fn object_id(&self) -> u64 {
386        self.directory.object_id()
387    }
388
389    fn parent(&self) -> Option<Arc<FxDirectory>> {
390        self.directory.parent()
391    }
392
393    fn set_parent(&self, _parent: Arc<FxDirectory>) {
394        // This directory can't be renamed.
395        unreachable!();
396    }
397
398    fn open_count_add_one(&self) {}
399    fn open_count_sub_one(self: Arc<Self>) {}
400
401    fn object_descriptor(&self) -> ObjectDescriptor {
402        ObjectDescriptor::Directory
403    }
404}
405
406impl MutableDirectory for BlobDirectory {
407    async fn unlink(self: Arc<Self>, name: &str, must_be_directory: bool) -> Result<(), Status> {
408        if must_be_directory {
409            return Err(Status::INVALID_ARGS);
410        }
411        self.directory.clone().unlink(name, must_be_directory).await
412    }
413
414    async fn update_attributes(
415        &self,
416        attributes: fio::MutableNodeAttributes,
417    ) -> Result<(), Status> {
418        self.directory.update_attributes(attributes).await
419    }
420
421    async fn sync(&self) -> Result<(), Status> {
422        self.directory.sync().await
423    }
424}
425
426/// Implementation of VFS pseudo-directory for blobs. Forks a task per connection.
427impl DirectoryEntry for BlobDirectory {
428    fn open_entry(self: Arc<Self>, request: OpenRequest<'_>) -> Result<(), Status> {
429        request.open_dir(self)
430    }
431
432    fn scope(&self) -> Option<ExecutionScope> {
433        Some(self.volume().scope().clone())
434    }
435}
436
437impl GetEntryInfo for BlobDirectory {
438    fn entry_info(&self) -> EntryInfo {
439        self.directory.entry_info()
440    }
441}
442
443impl vfs::node::Node for BlobDirectory {
444    async fn get_attributes(
445        &self,
446        requested_attributes: fio::NodeAttributesQuery,
447    ) -> Result<fio::NodeAttributes2, Status> {
448        self.directory.get_attributes(requested_attributes).await
449    }
450
451    fn query_filesystem(&self) -> Result<FilesystemInfo, Status> {
452        self.directory.query_filesystem()
453    }
454}
455
456/// Implements VFS entry container trait for directories, allowing manipulation of their contents.
457impl VfsDirectory for BlobDirectory {
458    fn deprecated_open(
459        self: Arc<Self>,
460        scope: ExecutionScope,
461        flags: fio::OpenFlags,
462        path: Path,
463        server_end: ServerEnd<NodeMarker>,
464    ) {
465        scope.clone().spawn(flags.to_object_request(server_end).handle_async(
466            async move |object_request| self.open_impl(scope, path, flags, object_request).await,
467        ));
468    }
469
470    fn open(
471        self: Arc<Self>,
472        scope: ExecutionScope,
473        path: Path,
474        flags: fio::Flags,
475        object_request: ObjectRequestRef<'_>,
476    ) -> Result<(), Status> {
477        scope.clone().spawn(object_request.take().handle_async(async move |object_request| {
478            self.open_impl(scope, path, flags, object_request).await
479        }));
480        Ok(())
481    }
482
483    async fn open_async(
484        self: Arc<Self>,
485        scope: ExecutionScope,
486        path: Path,
487        flags: fio::Flags,
488        object_request: ObjectRequestRef<'_>,
489    ) -> Result<(), Status> {
490        self.open_impl(scope, path, flags, object_request).await
491    }
492
493    async fn read_dirents(
494        &self,
495        pos: &TraversalPosition,
496        sink: Box<dyn dirents_sink::Sink>,
497    ) -> Result<(TraversalPosition, Box<dyn dirents_sink::Sealed>), Status> {
498        self.directory.read_dirents(pos, sink).await
499    }
500
501    fn register_watcher(
502        self: Arc<Self>,
503        scope: ExecutionScope,
504        mask: WatchMask,
505        watcher: DirectoryWatcher,
506    ) -> Result<(), Status> {
507        self.directory.clone().register_watcher(scope, mask, watcher)
508    }
509
510    fn unregister_watcher(self: Arc<Self>, key: usize) {
511        self.directory.clone().unregister_watcher(key)
512    }
513}
514
515impl From<object_store::Directory<FxVolume>> for BlobDirectory {
516    fn from(dir: object_store::Directory<FxVolume>) -> Self {
517        Self::new(dir.into())
518    }
519}
520
521#[cfg(test)]
522mod tests {
523    use super::*;
524    use crate::fuchsia::fxblob::testing::{BlobFixture, new_blob_fixture, open_blob_fixture};
525    use assert_matches::assert_matches;
526    use blob_writer::BlobWriter;
527    use delivery_blob::{CompressionMode, Type1Blob};
528    use fidl_fuchsia_fxfs::BlobReaderMarker;
529    use fuchsia_async::{DurationExt as _, TimeoutExt as _};
530    use fuchsia_component_client::connect_to_protocol_at_dir_svc;
531    use fuchsia_fs::directory::{
532        DirEntry, DirentKind, WatchEvent, WatchMessage, Watcher, readdir_inclusive,
533    };
534
535    use futures::StreamExt as _;
536    use std::path::PathBuf;
537
538    use crate::fuchsia::testing::{TestFixture, TestFixtureOptions};
539    use fidl_fuchsia_storage_mapping::MappingProviderMarker;
540    use fuchsia_fs::directory::open_directory_async;
541    use storage_device::DeviceHolder;
542    use storage_device::fake_device::FakeDevice;
543
544    #[fuchsia::test(threads = 10)]
545    async fn test_unlink() {
546        let fixture = new_blob_fixture().await;
547
548        let data = [1; 1000];
549
550        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
551
552        assert_eq!(fixture.read_blob(hash).await, data);
553
554        fixture
555            .root()
556            .unlink(&format!("{}", hash), &fio::UnlinkOptions::default())
557            .await
558            .expect("FIDL failed")
559            .expect("unlink failed");
560
561        fixture.close().await;
562    }
563
564    #[fuchsia::test(threads = 10)]
565    async fn test_readdir() {
566        let fixture = new_blob_fixture().await;
567
568        let data = [0xab; 2];
569        let hash;
570        {
571            hash = fuchsia_merkle::root_from_slice(&data);
572            let compressed_data: Vec<u8> = Type1Blob::generate(&data, CompressionMode::Always);
573
574            let blob_proxy =
575                connect_to_protocol_at_dir_svc::<fidl_fuchsia_fxfs::BlobCreatorMarker>(
576                    fixture.volume_out_dir(),
577                )
578                .expect("failed to connect to the Blob service");
579
580            let blob_writer_client_end = blob_proxy
581                .create(&hash.into(), false)
582                .await
583                .expect("transport error on create")
584                .expect("failed to create blob");
585
586            let writer = blob_writer_client_end.into_proxy();
587            let mut blob_writer = BlobWriter::create(writer, compressed_data.len() as u64)
588                .await
589                .expect("failed to create BlobWriter");
590            blob_writer.write(&compressed_data[..1]).await.unwrap();
591
592            // Before the blob is finished writing, it shouldn't appear in the directory.
593            assert_eq!(
594                readdir_inclusive(fixture.root()).await.ok(),
595                Some(vec![DirEntry { name: ".".to_string(), kind: DirentKind::Directory }])
596            );
597
598            blob_writer.write(&compressed_data[1..]).await.unwrap();
599        }
600
601        assert_eq!(
602            readdir_inclusive(fixture.root()).await.ok(),
603            Some(vec![
604                DirEntry { name: ".".to_string(), kind: DirentKind::Directory },
605                DirEntry { name: format! {"{}", hash}, kind: DirentKind::File },
606            ])
607        );
608
609        fixture.close().await;
610    }
611
612    #[fuchsia::test(threads = 10)]
613    async fn test_watchers() {
614        let fixture = new_blob_fixture().await;
615
616        let mut watcher = Watcher::new(fixture.root()).await.unwrap();
617        assert_eq!(
618            watcher.next().await,
619            Some(Ok(WatchMessage { event: WatchEvent::EXISTING, filename: PathBuf::from(".") }))
620        );
621        assert_matches!(
622            watcher.next().await,
623            Some(Ok(WatchMessage { event: WatchEvent::IDLE, .. }))
624        );
625
626        let data = vec![vec![0xab; 2], vec![0xcd; 65_536]];
627        let mut hashes = vec![];
628        let mut filenames = vec![];
629        for datum in data {
630            let hash = fuchsia_merkle::root_from_slice(&datum);
631            let filename = PathBuf::from(format!("{}", hash));
632            hashes.push(hash.clone());
633            filenames.push(filename.clone());
634
635            let compressed_data: Vec<u8> = Type1Blob::generate(&datum, CompressionMode::Always);
636
637            let blob_proxy =
638                connect_to_protocol_at_dir_svc::<fidl_fuchsia_fxfs::BlobCreatorMarker>(
639                    fixture.volume_out_dir(),
640                )
641                .expect("failed to connect to the Blob service");
642
643            let blob_writer_client_end = blob_proxy
644                .create(&hash.into(), false)
645                .await
646                .expect("transport error on create")
647                .expect("failed to create blob");
648
649            let writer = blob_writer_client_end.into_proxy();
650            let mut blob_writer = BlobWriter::create(writer, compressed_data.len() as u64)
651                .await
652                .expect("failed to create BlobWriter");
653            blob_writer.write(&compressed_data[..compressed_data.len() - 1]).await.unwrap();
654
655            // Before the blob is finished writing, we shouldn't see any watch events for it.
656            assert_matches!(
657                watcher
658                    .next()
659                    .on_timeout(zx::MonotonicDuration::from_millis(500).after_now(), || None)
660                    .await,
661                None
662            );
663
664            blob_writer.write(&compressed_data[compressed_data.len() - 1..]).await.unwrap();
665
666            assert_eq!(
667                watcher.next().await,
668                Some(Ok(WatchMessage { event: WatchEvent::ADD_FILE, filename }))
669            );
670        }
671
672        for (hash, filename) in hashes.iter().zip(filenames) {
673            fixture
674                .root()
675                .unlink(&format!("{}", hash), &fio::UnlinkOptions::default())
676                .await
677                .expect("FIDL call failed")
678                .expect("unlink failed");
679            assert_eq!(
680                watcher.next().await,
681                Some(Ok(WatchMessage { event: WatchEvent::REMOVE_FILE, filename }))
682            );
683        }
684
685        std::mem::drop(watcher);
686        fixture.close().await;
687    }
688
689    #[fuchsia::test(threads = 10)]
690    async fn test_rename_fails() {
691        let fixture = new_blob_fixture().await;
692
693        let data = vec![];
694        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
695
696        let (status, token) = fixture.root().get_token().await.expect("FIDL failed");
697        Status::ok(status).unwrap();
698        fixture
699            .root()
700            .rename(&format!("{}", hash), token.unwrap().into(), "foo")
701            .await
702            .expect("FIDL failed")
703            .expect_err("rename should fail");
704
705        fixture.close().await;
706    }
707
708    #[fuchsia::test(threads = 10)]
709    async fn test_link_fails() {
710        let fixture = new_blob_fixture().await;
711
712        let data = vec![];
713        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
714
715        let (status, token) = fixture.root().get_token().await.expect("FIDL failed");
716        Status::ok(status).unwrap();
717        let status = fixture
718            .root()
719            .link(&format!("{}", hash), token.unwrap().into(), "foo")
720            .await
721            .expect("FIDL failed");
722        assert_eq!(Status::ok(status), Err(Status::NOT_SUPPORTED));
723
724        fixture.close().await;
725    }
726
727    #[fuchsia::test(threads = 10)]
728    async fn test_verify_cached_hash_node() {
729        let fixture = new_blob_fixture().await;
730
731        let data = vec![];
732        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
733        let evil_hash =
734            Hash::from_str("2222222222222222222222222222222222222222222222222222222222222222")
735                .unwrap();
736
737        // Create a malicious link to the existing blob. This shouldn't be possible without special
738        // access either via internal apis or modifying the disk image.
739        {
740            let root = fixture
741                .volume()
742                .root()
743                .clone()
744                .as_node()
745                .into_any()
746                .downcast::<BlobDirectory>()
747                .unwrap()
748                .directory()
749                .clone();
750            root.clone()
751                .link(evil_hash.to_string(), root, &hash.to_string())
752                .await
753                .expect("Linking file");
754        }
755        let device = fixture.close().await;
756
757        let fixture = open_blob_fixture(device).await;
758        {
759            // Hold open a ref to keep it in the node cache.
760            let _vmo = fixture.get_blob_vmo(hash).await;
761
762            // Open the malicious link
763            let blob_reader =
764                connect_to_protocol_at_dir_svc::<BlobReaderMarker>(fixture.volume_out_dir())
765                    .expect("failed to connect to the BlobReader service");
766            blob_reader
767                .get_vmo(&evil_hash.into())
768                .await
769                .expect("transport error on BlobReader.GetVmo")
770                .expect_err("Hashes should mismatch");
771        }
772        fixture.close().await;
773    }
774
775    #[fuchsia::test(threads = 10)]
776    async fn test_blob_needs_overwrite_verifies_existence() {
777        let fixture = new_blob_fixture().await;
778        let data = vec![42u8; 32];
779        let hash = fixture.write_blob(&data, CompressionMode::Never).await;
780
781        let blob_creator =
782            connect_to_protocol_at_dir_svc::<BlobCreatorMarker>(fixture.volume_out_dir())
783                .expect("failed to connect to the BlobCreator service");
784        // Do it once to fetch it fresh after clearing the cache.
785        fixture.volume().volume().dirent_cache().clear();
786        assert!(
787            !blob_creator
788                .needs_overwrite(&hash.into())
789                .await
790                .expect("fidl transport")
791                .expect("Find new blob")
792        );
793
794        // Get it into the cache and then check again.
795        let blob_reader =
796            connect_to_protocol_at_dir_svc::<BlobReaderMarker>(fixture.volume_out_dir())
797                .expect("failed to connect to the BlobReader service");
798        blob_reader
799            .get_vmo(&hash.into())
800            .await
801            .expect("transport error on BlobReader.GetVmo")
802            .expect("Opening blob");
803        assert!(
804            !blob_creator
805                .needs_overwrite(&hash.into())
806                .await
807                .expect("fidl transport")
808                .expect("Find new blob")
809        );
810
811        // Fail to find one that is missing.
812        blob_creator
813            .needs_overwrite(&[1u8; 32].into())
814            .await
815            .expect("fidl transport")
816            .expect_err("Blob should not exist");
817        fixture.close().await;
818    }
819
820    #[fuchsia::test]
821    async fn test_mapping_provider_exposure() {
822        let fixture = TestFixture::open(
823            DeviceHolder::new(FakeDevice::new(16384, 512)),
824            TestFixtureOptions { encrypted: false, as_blob: true, ..Default::default() },
825        )
826        .await;
827
828        let svc_dir = open_directory_async(fixture.volume_out_dir(), "svc", fio::PERM_READABLE)
829            .expect("failed to open svc dir");
830
831        let entries = readdir_inclusive(&svc_dir).await.expect("readdir");
832        let is_exposed = entries.iter().any(|e| e.name == MappingProviderMarker::PROTOCOL_NAME);
833        assert!(is_exposed, "MappingProvider not exposed");
834        let mapper_exposed = entries.iter().any(|e| e.name == fblock::MapperMarker::PROTOCOL_NAME);
835        assert!(mapper_exposed, "Mapper not exposed");
836
837        let mapper =
838            connect_to_protocol_at_dir_svc::<fblock::MapperMarker>(fixture.volume_out_dir())
839                .expect("failed to connect to the Mapper service");
840        assert_matches::assert_matches!(
841            mapper.take_event_stream().try_next().await,
842            Err(fidl::Error::ClientChannelClosed {
843                epitaph: fidl::Epitaph::Explicit(Err(Status::NOT_SUPPORTED)),
844                ..
845            })
846        );
847
848        fixture.close().await;
849    }
850}