Skip to main content

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