fs_management/
filesystem.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//! Contains the asynchronous version of [`Filesystem`][`crate::Filesystem`].
6
7use crate::error::{QueryError, ShutdownError};
8use crate::{ComponentType, FSConfig, Options};
9use anyhow::{Context, Error, anyhow, bail, ensure};
10use fidl::endpoints::{ClientEnd, ServerEnd, create_endpoints, create_proxy};
11use fidl_fuchsia_component::{self as fcomponent, RealmMarker};
12use fidl_fuchsia_fs::AdminMarker;
13use fidl_fuchsia_fs_startup::{CheckOptions, CreateOptions, MountOptions, StartupMarker};
14use fidl_fuchsia_hardware_block_volume::VolumeMarker;
15use fuchsia_component_client::{
16    connect_to_named_protocol_at_dir_root, connect_to_protocol, connect_to_protocol_at_dir_root,
17    connect_to_protocol_at_dir_svc, open_childs_exposed_directory,
18};
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21use zx::{self as zx, AsHandleRef as _, Status};
22use {fidl_fuchsia_component_decl as fdecl, fidl_fuchsia_io as fio};
23
24/// Creates new connections to an instance of fuchsia.hardware.block.Block and similar protocols
25/// (Volume, Partition).
26///
27/// NOTE: It is important to understand the difference between `BlockConnector` and the actual
28/// protocols (e.g. a `ClientEnd<BlockMarker>` or `BlockProxy`): `BlockConnector` is used to *create
29/// new connections* to a Block.
30///
31/// It is not possible to directly convert a `ClientEnd<BlockMarker>` (or `BlockProxy`) into a
32/// `BlockConnector`, because Block is not cloneable.  To implement `BlockConnector`, you will need
33/// a way to generate new connections to a Block instance.  A few common implementations are
34/// provided below.
35pub trait BlockConnector: Send + Sync {
36    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error>;
37    fn connect_volume(&self) -> Result<ClientEnd<VolumeMarker>, Error> {
38        let (client, server) = fidl::endpoints::create_endpoints();
39        self.connect_channel_to_volume(server)?;
40        Ok(client)
41    }
42    fn connect_partition(
43        &self,
44    ) -> Result<ClientEnd<fidl_fuchsia_hardware_block_partition::PartitionMarker>, Error> {
45        self.connect_volume().map(|v| ClientEnd::new(v.into_channel()))
46    }
47    fn connect_block(&self) -> Result<ClientEnd<fidl_fuchsia_hardware_block::BlockMarker>, Error> {
48        self.connect_volume().map(|v| ClientEnd::new(v.into_channel()))
49    }
50}
51
52/// Implements `BlockConnector` via a service dir.  Wraps `connect_to_named_protocol_at_dir_root`.
53#[derive(Clone, Debug)]
54pub struct DirBasedBlockConnector(fio::DirectoryProxy, String);
55
56impl DirBasedBlockConnector {
57    pub fn new(dir: fio::DirectoryProxy, path: String) -> Self {
58        Self(dir, path)
59    }
60
61    pub fn path(&self) -> &str {
62        &self.1
63    }
64}
65
66impl BlockConnector for DirBasedBlockConnector {
67    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
68        self.0.open(
69            self.path(),
70            fio::Flags::PROTOCOL_SERVICE,
71            &fio::Options::default(),
72            server_end.into_channel(),
73        )?;
74        Ok(())
75    }
76}
77
78impl BlockConnector for fidl_fuchsia_device::ControllerProxy {
79    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
80        let () = self.connect_to_device_fidl(server_end.into_channel())?;
81        Ok(())
82    }
83}
84
85impl BlockConnector for fidl_fuchsia_storage_partitions::PartitionServiceProxy {
86    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
87        self.connect_channel_to_volume(server_end)?;
88        Ok(())
89    }
90}
91
92// NB: We have to be specific here; we cannot do a blanket impl for AsRef<T: BlockConnector> because
93// that would conflict with a downstream crate that implements AsRef for a concrete BlockConnector
94// defined here already.
95impl<T: BlockConnector> BlockConnector for Arc<T> {
96    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
97        self.as_ref().connect_channel_to_volume(server_end)
98    }
99}
100
101impl<F> BlockConnector for F
102where
103    F: Fn(ServerEnd<VolumeMarker>) -> Result<(), Error> + Send + Sync,
104{
105    fn connect_channel_to_volume(&self, server_end: ServerEnd<VolumeMarker>) -> Result<(), Error> {
106        self(server_end)
107    }
108}
109
110/// Asynchronously manages a block device for filesystem operations.
111pub struct Filesystem {
112    /// The filesystem struct keeps the FSConfig in a Box<dyn> instead of holding it directly for
113    /// code size reasons. Using a type parameter instead would make monomorphized versions of the
114    /// Filesystem impl block for each filesystem type, which duplicates several multi-kilobyte
115    /// functions (get_component_exposed_dir and serve in particular) that are otherwise quite
116    /// generic over config. Clients that want to be generic over filesystem type also pay the
117    /// monomorphization cost, with some, like fshost, paying a lot.
118    config: Box<dyn FSConfig>,
119    block_connector: Box<dyn BlockConnector>,
120    component: Option<Arc<DynamicComponentInstance>>,
121}
122
123// Used to disambiguate children in our component collection.
124static COLLECTION_COUNTER: AtomicU64 = AtomicU64::new(0);
125
126impl Filesystem {
127    pub fn config(&self) -> &dyn FSConfig {
128        self.config.as_ref()
129    }
130
131    pub fn into_config(self) -> Box<dyn FSConfig> {
132        self.config
133    }
134
135    /// Creates a new `Filesystem`.
136    pub fn new<B: BlockConnector + 'static, FSC: FSConfig>(
137        block_connector: B,
138        config: FSC,
139    ) -> Self {
140        Self::from_boxed_config(Box::new(block_connector), Box::new(config))
141    }
142
143    /// Creates a new `Filesystem`.
144    pub fn from_boxed_config(
145        block_connector: Box<dyn BlockConnector>,
146        config: Box<dyn FSConfig>,
147    ) -> Self {
148        Self { config, block_connector, component: None }
149    }
150
151    /// Returns the (relative) moniker of the filesystem component. This will start the component
152    /// instance if it is not running.
153    pub async fn get_component_moniker(&mut self) -> Result<String, Error> {
154        let _ = self.get_component_exposed_dir().await?;
155        Ok(match self.config.options().component_type {
156            ComponentType::StaticChild => self.config.options().component_name.to_string(),
157            ComponentType::DynamicChild { .. } => {
158                let component = self.component.as_ref().unwrap();
159                format!("{}:{}", component.collection, component.name)
160            }
161        })
162    }
163
164    async fn get_component_exposed_dir(&mut self) -> Result<fio::DirectoryProxy, Error> {
165        let options = self.config.options();
166        let component_name = options.component_name;
167        match options.component_type {
168            ComponentType::StaticChild => open_childs_exposed_directory(component_name, None).await,
169            ComponentType::DynamicChild { collection_name } => {
170                if let Some(component) = &self.component {
171                    return open_childs_exposed_directory(
172                        component.name.clone(),
173                        Some(component.collection.clone()),
174                    )
175                    .await;
176                }
177
178                // We need a unique name, so we pull in the process Koid here since it's possible
179                // for the same binary in a component to be launched multiple times and we don't
180                // want to collide with children created by other processes.
181                let name = format!(
182                    "{}-{}-{}",
183                    component_name,
184                    fuchsia_runtime::process_self().get_koid().unwrap().raw_koid(),
185                    COLLECTION_COUNTER.fetch_add(1, Ordering::Relaxed)
186                );
187
188                let collection_ref = fdecl::CollectionRef { name: collection_name };
189                let child_decls = vec![
190                    fdecl::Child {
191                        name: Some(format!("{}-relative", name)),
192                        url: Some(format!("#meta/{}.cm", component_name)),
193                        startup: Some(fdecl::StartupMode::Lazy),
194                        ..Default::default()
195                    },
196                    fdecl::Child {
197                        name: Some(name),
198                        url: Some(format!(
199                            "fuchsia-boot:///{}#meta/{}.cm",
200                            component_name, component_name
201                        )),
202                        startup: Some(fdecl::StartupMode::Lazy),
203                        ..Default::default()
204                    },
205                ];
206                let realm_proxy = connect_to_protocol::<RealmMarker>()?;
207                for child_decl in child_decls {
208                    // Launch a new component in our collection.
209                    realm_proxy
210                        .create_child(
211                            &collection_ref,
212                            &child_decl,
213                            fcomponent::CreateChildArgs::default(),
214                        )
215                        .await?
216                        .map_err(|e| anyhow!("create_child failed: {:?}", e))?;
217
218                    let component = Arc::new(DynamicComponentInstance {
219                        name: child_decl.name.unwrap(),
220                        collection: collection_ref.name.clone(),
221                        should_not_drop: AtomicBool::new(false),
222                    });
223
224                    if let Ok(proxy) = open_childs_exposed_directory(
225                        component.name.clone(),
226                        Some(component.collection.clone()),
227                    )
228                    .await
229                    {
230                        self.component = Some(component);
231                        return Ok(proxy);
232                    }
233                }
234                Err(anyhow!("Failed to open exposed directory"))
235            }
236        }
237    }
238
239    /// Calls fuchsia.fs.startup/Startup.Format on the configured filesystem component.
240    ///
241    /// Which component is used and the options passed to it are controlled by the config this
242    /// `Filesystem` was created with.
243    ///
244    /// See [`FSConfig`].
245    ///
246    /// # Errors
247    ///
248    /// Returns any errors from the Format method. Also returns an error if the startup protocol is
249    /// not found, if it couldn't launch or find the filesystem component, or if it couldn't get
250    /// the block device channel.
251    pub async fn format(&mut self) -> Result<(), Error> {
252        let channel = self.block_connector.connect_block()?;
253
254        let exposed_dir = self.get_component_exposed_dir().await?;
255        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
256        proxy
257            .format(channel, &self.config().options().format_options)
258            .await?
259            .map_err(Status::from_raw)?;
260
261        Ok(())
262    }
263
264    /// Calls fuchsia.fs.startup/Startup.Check on the configured filesystem component.
265    ///
266    /// Which component is used and the options passed to it are controlled by the config this
267    /// `Filesystem` was created with.
268    ///
269    /// See [`FSConfig`].
270    ///
271    /// # Errors
272    ///
273    /// Returns any errors from the Check method. Also returns an error if the startup protocol is
274    /// not found, if it couldn't launch or find the filesystem component, or if it couldn't get
275    /// the block device channel.
276    pub async fn fsck(&mut self) -> Result<(), Error> {
277        let channel = self.block_connector.connect_block()?;
278        let exposed_dir = self.get_component_exposed_dir().await?;
279        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
280        proxy.check(channel, CheckOptions::default()).await?.map_err(Status::from_raw)?;
281        Ok(())
282    }
283
284    /// Serves the filesystem on the block device and returns a [`ServingSingleVolumeFilesystem`]
285    /// representing the running filesystem component.
286    ///
287    /// # Errors
288    ///
289    /// Returns [`Err`] if serving the filesystem failed.
290    pub async fn serve(mut self) -> Result<ServingSingleVolumeFilesystem, Error> {
291        if self.config.is_multi_volume() {
292            bail!("Can't serve a multivolume filesystem; use serve_multi_volume");
293        }
294        let Options { start_options, reuse_component_after_serving, .. } = self.config.options();
295
296        let exposed_dir = self.get_component_exposed_dir().await?;
297        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
298        proxy
299            .start(self.block_connector.connect_block()?, &start_options)
300            .await?
301            .map_err(Status::from_raw)?;
302
303        let (root_dir, server_end) = create_endpoints::<fio::NodeMarker>();
304        exposed_dir.open(
305            "root",
306            fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE,
307            &Default::default(),
308            server_end.into_channel(),
309        )?;
310        let component = self.component.clone();
311        if !reuse_component_after_serving {
312            self.component = None;
313        }
314        Ok(ServingSingleVolumeFilesystem {
315            component,
316            exposed_dir: Some(exposed_dir),
317            root_dir: ClientEnd::<fio::DirectoryMarker>::new(root_dir.into_channel()).into_proxy(),
318            binding: None,
319        })
320    }
321
322    /// Serves the filesystem on the block device and returns a [`ServingMultiVolumeFilesystem`]
323    /// representing the running filesystem component.  No volumes are opened; clients have to do
324    /// that explicitly.
325    ///
326    /// # Errors
327    ///
328    /// Returns [`Err`] if serving the filesystem failed.
329    pub async fn serve_multi_volume(mut self) -> Result<ServingMultiVolumeFilesystem, Error> {
330        if !self.config.is_multi_volume() {
331            bail!("Can't serve_multi_volume a single-volume filesystem; use serve");
332        }
333
334        let exposed_dir = self.get_component_exposed_dir().await?;
335        let proxy = connect_to_protocol_at_dir_root::<StartupMarker>(&exposed_dir)?;
336        proxy
337            .start(self.block_connector.connect_block()?, &self.config.options().start_options)
338            .await?
339            .map_err(Status::from_raw)?;
340
341        Ok(ServingMultiVolumeFilesystem {
342            component: self.component,
343            exposed_dir: Some(exposed_dir),
344        })
345    }
346}
347
348// Destroys the child when dropped.
349struct DynamicComponentInstance {
350    name: String,
351    collection: String,
352    should_not_drop: AtomicBool,
353}
354
355impl DynamicComponentInstance {
356    fn forget(&self) {
357        self.should_not_drop.store(true, Ordering::Relaxed);
358    }
359}
360
361impl Drop for DynamicComponentInstance {
362    fn drop(&mut self) {
363        if self.should_not_drop.load(Ordering::Relaxed) {
364            return;
365        }
366        if let Ok(realm_proxy) = connect_to_protocol::<RealmMarker>() {
367            let _ = realm_proxy.destroy_child(&fdecl::ChildRef {
368                name: self.name.clone(),
369                collection: Some(self.collection.clone()),
370            });
371        }
372    }
373}
374
375/// Manages the binding of a `fuchsia_io::DirectoryProxy` into the local namespace.  When the object
376/// is dropped, the binding is removed.
377#[derive(Default)]
378pub struct NamespaceBinding(String);
379
380impl NamespaceBinding {
381    pub fn create(root_dir: &fio::DirectoryProxy, path: String) -> Result<NamespaceBinding, Error> {
382        let (client_end, server_end) = create_endpoints();
383        root_dir.clone(ServerEnd::new(server_end.into_channel()))?;
384        let namespace = fdio::Namespace::installed()?;
385        namespace.bind(&path, client_end)?;
386        Ok(Self(path))
387    }
388}
389
390impl std::ops::Deref for NamespaceBinding {
391    type Target = str;
392    fn deref(&self) -> &Self::Target {
393        &self.0
394    }
395}
396
397impl Drop for NamespaceBinding {
398    fn drop(&mut self) {
399        if let Ok(namespace) = fdio::Namespace::installed() {
400            let _ = namespace.unbind(&self.0);
401        }
402    }
403}
404
405// TODO(https://fxbug.dev/42174810): Soft migration; remove this after completion
406pub type ServingFilesystem = ServingSingleVolumeFilesystem;
407
408/// Asynchronously manages a serving filesystem. Created from [`Filesystem::serve()`].
409pub struct ServingSingleVolumeFilesystem {
410    component: Option<Arc<DynamicComponentInstance>>,
411    // exposed_dir will always be Some, except when the filesystem is shutting down.
412    exposed_dir: Option<fio::DirectoryProxy>,
413    root_dir: fio::DirectoryProxy,
414
415    // The path in the local namespace that this filesystem is bound to (optional).
416    binding: Option<NamespaceBinding>,
417}
418
419impl ServingSingleVolumeFilesystem {
420    /// Returns a proxy to the exposed directory of the serving filesystem.
421    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
422        self.exposed_dir.as_ref().unwrap()
423    }
424
425    /// Returns a proxy to the root directory of the serving filesystem.
426    pub fn root(&self) -> &fio::DirectoryProxy {
427        &self.root_dir
428    }
429
430    /// Binds the root directory being served by this filesystem to a path in the local namespace.
431    /// The path must be absolute, containing no "." nor ".." entries.  The binding will be dropped
432    /// when self is dropped.  Only one binding is supported.
433    ///
434    /// # Errors
435    ///
436    /// Returns [`Err`] if binding failed.
437    pub fn bind_to_path(&mut self, path: &str) -> Result<(), Error> {
438        ensure!(self.binding.is_none(), "Already bound");
439        self.binding = Some(NamespaceBinding::create(&self.root_dir, path.to_string())?);
440        Ok(())
441    }
442
443    pub fn bound_path(&self) -> Option<&str> {
444        self.binding.as_deref()
445    }
446
447    /// Returns a [`FilesystemInfo`] object containing information about the serving filesystem.
448    ///
449    /// # Errors
450    ///
451    /// Returns [`Err`] if querying the filesystem failed.
452    pub async fn query(&self) -> Result<Box<fio::FilesystemInfo>, QueryError> {
453        let (status, info) = self.root_dir.query_filesystem().await?;
454        Status::ok(status).map_err(QueryError::DirectoryQuery)?;
455        info.ok_or(QueryError::DirectoryEmptyResult)
456    }
457
458    /// Take the exposed dir from this filesystem instance, dropping the management struct without
459    /// shutting the filesystem down. This leaves the caller with the responsibility of shutting
460    /// down the filesystem, and the filesystem component if necessary.
461    pub fn take_exposed_dir(mut self) -> fio::DirectoryProxy {
462        self.component.take().expect("BUG: component missing").forget();
463        self.exposed_dir.take().expect("BUG: exposed dir missing")
464    }
465
466    /// Attempts to shutdown the filesystem using the
467    /// [`fidl_fuchsia_fs::AdminProxy::shutdown()`] FIDL method and waiting for the filesystem
468    /// process to terminate.
469    ///
470    /// # Errors
471    ///
472    /// Returns [`Err`] if the shutdown failed or the filesystem process did not terminate.
473    pub async fn shutdown(mut self) -> Result<(), ShutdownError> {
474        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(
475            &self.exposed_dir.take().expect("BUG: exposed dir missing"),
476        )?
477        .shutdown()
478        .await?;
479        Ok(())
480    }
481
482    /// Attempts to kill the filesystem process and waits for the process to terminate.
483    ///
484    /// # Errors
485    ///
486    /// Returns [`Err`] if the filesystem process could not be terminated. There is no way to
487    /// recover the [`Filesystem`] from this error.
488    pub async fn kill(self) -> Result<(), Error> {
489        // For components, just shut down the filesystem.
490        // TODO(https://fxbug.dev/293949323): Figure out a way to make this more abrupt - the use-cases are
491        // either testing or when the filesystem isn't responding.
492        self.shutdown().await?;
493        Ok(())
494    }
495}
496
497impl Drop for ServingSingleVolumeFilesystem {
498    fn drop(&mut self) {
499        // Make a best effort attempt to shut down to the filesystem, if we need to.
500        if let Some(exposed_dir) = self.exposed_dir.take() {
501            if let Ok(proxy) =
502                connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
503            {
504                let _ = proxy.shutdown();
505            }
506        }
507    }
508}
509
510/// Asynchronously manages a serving multivolume filesystem. Created from
511/// [`Filesystem::serve_multi_volume()`].
512pub struct ServingMultiVolumeFilesystem {
513    component: Option<Arc<DynamicComponentInstance>>,
514    // exposed_dir will always be Some, except in Self::shutdown.
515    exposed_dir: Option<fio::DirectoryProxy>,
516}
517
518/// Represents an opened volume in a [`ServingMultiVolumeFilesystem'] instance.
519pub struct ServingVolume {
520    root_dir: fio::DirectoryProxy,
521    binding: Option<NamespaceBinding>,
522    exposed_dir: fio::DirectoryProxy,
523}
524
525impl ServingVolume {
526    fn new(exposed_dir: fio::DirectoryProxy) -> Result<Self, Error> {
527        let (root_dir, server_end) = create_endpoints::<fio::NodeMarker>();
528        exposed_dir.open(
529            "root",
530            fio::PERM_READABLE | fio::Flags::PERM_INHERIT_WRITE | fio::Flags::PERM_INHERIT_EXECUTE,
531            &Default::default(),
532            server_end.into_channel(),
533        )?;
534        Ok(ServingVolume {
535            root_dir: ClientEnd::<fio::DirectoryMarker>::new(root_dir.into_channel()).into_proxy(),
536            binding: None,
537            exposed_dir,
538        })
539    }
540
541    /// Returns a proxy to the root directory of the serving volume.
542    pub fn root(&self) -> &fio::DirectoryProxy {
543        &self.root_dir
544    }
545
546    /// Returns a proxy to the exposed directory of the serving volume.
547    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
548        &self.exposed_dir
549    }
550
551    /// Binds the root directory being served by this filesystem to a path in the local namespace.
552    /// The path must be absolute, containing no "." nor ".." entries.  The binding will be dropped
553    /// when self is dropped, or when unbind_path is called.  Only one binding is supported.
554    ///
555    /// # Errors
556    ///
557    /// Returns [`Err`] if binding failed, or if a binding already exists.
558    pub fn bind_to_path(&mut self, path: &str) -> Result<(), Error> {
559        ensure!(self.binding.is_none(), "Already bound");
560        self.binding = Some(NamespaceBinding::create(&self.root_dir, path.to_string())?);
561        Ok(())
562    }
563
564    /// Remove the namespace binding to the root directory being served by this volume, if there is
565    /// one. If there is no binding, this function does nothing. After this, it is safe to call
566    /// bind_to_path again.
567    pub fn unbind_path(&mut self) {
568        let _ = self.binding.take();
569    }
570
571    pub fn bound_path(&self) -> Option<&str> {
572        self.binding.as_deref()
573    }
574
575    /// Returns a [`FilesystemInfo`] object containing information about the serving volume.
576    ///
577    /// # Errors
578    ///
579    /// Returns [`Err`] if querying the filesystem failed.
580    pub async fn query(&self) -> Result<Box<fio::FilesystemInfo>, QueryError> {
581        let (status, info) = self.root_dir.query_filesystem().await?;
582        Status::ok(status).map_err(QueryError::DirectoryQuery)?;
583        info.ok_or(QueryError::DirectoryEmptyResult)
584    }
585
586    /// Attempts to shutdown the filesystem using the [`fidl_fuchsia_fs::AdminProxy::shutdown()`]
587    /// FIDL method. Fails if the volume is not already open.
588    pub async fn shutdown(self) -> Result<(), Error> {
589        let admin_proxy = connect_to_protocol_at_dir_svc::<AdminMarker>(self.exposed_dir())?;
590        admin_proxy.shutdown().await.context("failed to shutdown volume")?;
591        Ok(())
592    }
593}
594
595impl ServingMultiVolumeFilesystem {
596    /// Returns whether the given volume exists.
597    pub async fn has_volume(&self, volume: &str) -> Result<bool, Error> {
598        let path = format!("volumes/{}", volume);
599        fuchsia_fs::directory::open_node(
600            self.exposed_dir.as_ref().unwrap(),
601            &path,
602            fio::Flags::PROTOCOL_NODE,
603        )
604        .await
605        .map(|_| true)
606        .or_else(|e| {
607            if let fuchsia_fs::node::OpenError::OpenError(status) = &e {
608                if *status == zx::Status::NOT_FOUND {
609                    return Ok(false);
610                }
611            }
612            Err(e.into())
613        })
614    }
615
616    /// Creates and mounts the volume.  Fails if the volume already exists.
617    /// If `options.crypt` is set, the volume will be encrypted using the provided Crypt instance.
618    /// If `options.as_blob` is set, creates a blob volume that is mounted as a blob filesystem.
619    pub async fn create_volume(
620        &self,
621        volume: &str,
622        create_options: CreateOptions,
623        options: MountOptions,
624    ) -> Result<ServingVolume, Error> {
625        let (exposed_dir, server) = create_proxy::<fio::DirectoryMarker>();
626        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
627            self.exposed_dir.as_ref().unwrap(),
628        )?
629        .create(volume, server, create_options, options)
630        .await?
631        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
632        ServingVolume::new(exposed_dir)
633    }
634
635    /// Deletes the volume. Fails if the volume is already mounted.
636    pub async fn remove_volume(&self, volume: &str) -> Result<(), Error> {
637        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumesMarker>(
638            self.exposed_dir.as_ref().unwrap(),
639        )?
640        .remove(volume)
641        .await?
642        .map_err(|e| anyhow!(zx::Status::from_raw(e)))
643    }
644
645    /// Mounts an existing volume.  Fails if the volume is already mounted or doesn't exist.
646    /// If `crypt` is set, the volume will be decrypted using the provided Crypt instance.
647    pub async fn open_volume(
648        &self,
649        volume: &str,
650        options: MountOptions,
651    ) -> Result<ServingVolume, Error> {
652        let (exposed_dir, server) = create_proxy::<fio::DirectoryMarker>();
653        let path = format!("volumes/{}", volume);
654        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
655            self.exposed_dir.as_ref().unwrap(),
656            &path,
657        )?
658        .mount(server, options)
659        .await?
660        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
661
662        ServingVolume::new(exposed_dir)
663    }
664
665    /// Returns volume info for `volume`.
666    pub async fn get_volume_info(
667        &self,
668        volume: &str,
669    ) -> Result<fidl_fuchsia_fs_startup::VolumeInfo, Error> {
670        let path = format!("volumes/{}", volume);
671        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
672            self.exposed_dir.as_ref().unwrap(),
673            &path,
674        )?
675        .get_info()
676        .await?
677        .map_err(|e| anyhow!(zx::Status::from_raw(e)))
678    }
679
680    /// Sets the max byte limit for a volume. Fails if the volume is not mounted.
681    pub async fn set_byte_limit(&self, volume: &str, byte_limit: u64) -> Result<(), Error> {
682        if byte_limit == 0 {
683            return Ok(());
684        }
685        let path = format!("volumes/{}", volume);
686        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
687            self.exposed_dir.as_ref().unwrap(),
688            &path,
689        )?
690        .set_limit(byte_limit)
691        .await?
692        .map_err(|e| anyhow!(zx::Status::from_raw(e)))
693    }
694
695    pub async fn check_volume(&self, volume: &str, options: CheckOptions) -> Result<(), Error> {
696        let path = format!("volumes/{}", volume);
697        connect_to_named_protocol_at_dir_root::<fidl_fuchsia_fs_startup::VolumeMarker>(
698            self.exposed_dir.as_ref().unwrap(),
699            &path,
700        )?
701        .check(options)
702        .await?
703        .map_err(|e| anyhow!(zx::Status::from_raw(e)))?;
704        Ok(())
705    }
706
707    /// Provides access to the internal |exposed_dir| for use in testing
708    /// callsites which need directory access.
709    pub fn exposed_dir(&self) -> &fio::DirectoryProxy {
710        self.exposed_dir.as_ref().expect("BUG: exposed dir missing")
711    }
712
713    /// Attempts to shutdown the filesystem using the [`fidl_fuchsia_fs::AdminProxy::shutdown()`]
714    /// FIDL method.
715    ///
716    /// # Errors
717    ///
718    /// Returns [`Err`] if the shutdown failed.
719    pub async fn shutdown(mut self) -> Result<(), ShutdownError> {
720        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(
721            // Take exposed_dir so we don't attempt to shut down again in Drop.
722            &self.exposed_dir.take().expect("BUG: exposed dir missing"),
723        )?
724        .shutdown()
725        .await?;
726        Ok(())
727    }
728
729    /// Take the exposed dir from this filesystem instance, dropping the management struct without
730    /// shutting the filesystem down. This leaves the caller with the responsibility of shutting
731    /// down the filesystem, and the filesystem component if necessary.
732    pub fn take_exposed_dir(mut self) -> fio::DirectoryProxy {
733        self.component.take().expect("BUG: missing component").forget();
734        self.exposed_dir.take().expect("BUG: exposed dir missing")
735    }
736
737    /// Returns a list of volumes found in the filesystem.
738    pub async fn list_volumes(&self) -> Result<Vec<String>, Error> {
739        let volumes_dir = fuchsia_fs::directory::open_async::<fio::DirectoryMarker>(
740            self.exposed_dir(),
741            "volumes",
742            fio::PERM_READABLE,
743        )
744        .unwrap();
745        fuchsia_fs::directory::readdir(&volumes_dir)
746            .await
747            .map(|entries| entries.into_iter().map(|e| e.name).collect())
748            .map_err(|e| anyhow!("failed to read volumes dir: {}", e))
749    }
750}
751
752impl Drop for ServingMultiVolumeFilesystem {
753    fn drop(&mut self) {
754        if let Some(exposed_dir) = self.exposed_dir.take() {
755            // Make a best effort attempt to shut down to the filesystem.
756            if let Ok(proxy) =
757                connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
758            {
759                let _ = proxy.shutdown();
760            }
761        }
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use crate::{BlobCompression, BlobEvictionPolicy, Blobfs, F2fs, Fxfs, Minfs};
769    use delivery_blob::{CompressionMode, Type1Blob};
770    use fidl_fuchsia_fxfs::{BlobCreatorMarker, BlobReaderMarker};
771    use ramdevice_client::RamdiskClient;
772    use std::io::{Read as _, Write as _};
773
774    async fn ramdisk(block_size: u64) -> RamdiskClient {
775        RamdiskClient::create(block_size, 1 << 16).await.unwrap()
776    }
777
778    async fn new_fs<FSC: FSConfig>(ramdisk: &RamdiskClient, config: FSC) -> Filesystem {
779        Filesystem::new(ramdisk.open_controller().unwrap(), config)
780    }
781
782    #[fuchsia::test]
783    async fn blobfs_custom_config() {
784        let block_size = 512;
785        let ramdisk = ramdisk(block_size).await;
786        let config = Blobfs {
787            verbose: true,
788            readonly: true,
789            write_compression_algorithm: BlobCompression::Uncompressed,
790            cache_eviction_policy_override: BlobEvictionPolicy::EvictImmediately,
791            ..Default::default()
792        };
793        let mut blobfs = new_fs(&ramdisk, config).await;
794
795        blobfs.format().await.expect("failed to format blobfs");
796        blobfs.fsck().await.expect("failed to fsck blobfs");
797        let _ = blobfs.serve().await.expect("failed to serve blobfs");
798
799        ramdisk.destroy().await.expect("failed to destroy ramdisk");
800    }
801
802    #[fuchsia::test]
803    async fn blobfs_format_fsck_success() {
804        let block_size = 512;
805        let ramdisk = ramdisk(block_size).await;
806        let mut blobfs = new_fs(&ramdisk, Blobfs::default()).await;
807
808        blobfs.format().await.expect("failed to format blobfs");
809        blobfs.fsck().await.expect("failed to fsck blobfs");
810
811        ramdisk.destroy().await.expect("failed to destroy ramdisk");
812    }
813
814    #[fuchsia::test]
815    async fn blobfs_format_serve_write_query_restart_read_shutdown() {
816        let block_size = 512;
817        let ramdisk = ramdisk(block_size).await;
818        let mut blobfs = new_fs(&ramdisk, Blobfs::default()).await;
819
820        blobfs.format().await.expect("failed to format blobfs");
821
822        let serving = blobfs.serve().await.expect("failed to serve blobfs the first time");
823
824        // snapshot of FilesystemInfo
825        let fs_info1 =
826            serving.query().await.expect("failed to query filesystem info after first serving");
827
828        // pre-generated merkle test fixture data
829        let content = b"test content";
830        let merkle = fuchsia_merkle::root_from_slice(content);
831        let delivery_blob = Type1Blob::generate(content, CompressionMode::Never);
832
833        {
834            let creator = fuchsia_component_client::connect_to_protocol_at_dir_root::<
835                BlobCreatorMarker,
836            >(serving.exposed_dir())
837            .unwrap();
838            let writer = creator.create(&merkle.into(), false).await.unwrap().unwrap();
839            let mut writer =
840                blob_writer::BlobWriter::create(writer.into_proxy(), delivery_blob.len() as u64)
841                    .await
842                    .unwrap();
843            writer.write(&delivery_blob).await.unwrap();
844        }
845
846        // check against the snapshot FilesystemInfo
847        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
848        assert_eq!(
849            fs_info2.used_bytes - fs_info1.used_bytes,
850            fs_info2.block_size as u64 // assuming content < 8K
851        );
852
853        serving.shutdown().await.expect("failed to shutdown blobfs the first time");
854        let blobfs = new_fs(&ramdisk, Blobfs::default()).await;
855        let serving = blobfs.serve().await.expect("failed to serve blobfs the second time");
856        {
857            let reader = fuchsia_component_client::connect_to_protocol_at_dir_root::<
858                BlobReaderMarker,
859            >(serving.exposed_dir())
860            .unwrap();
861            let vmo = reader.get_vmo(&merkle.into()).await.unwrap().unwrap();
862            let read_content = vmo.read_to_vec::<u8>(0, content.len() as u64).unwrap();
863            assert_eq!(read_content, content);
864        }
865
866        // once more check against the snapshot FilesystemInfo
867        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
868        assert_eq!(
869            fs_info3.used_bytes - fs_info1.used_bytes,
870            fs_info3.block_size as u64 // assuming content < 8K
871        );
872
873        serving.shutdown().await.expect("failed to shutdown blobfs the second time");
874
875        ramdisk.destroy().await.expect("failed to destroy ramdisk");
876    }
877
878    #[fuchsia::test]
879    async fn blobfs_bind_to_path() {
880        let block_size = 512;
881        let test_content = b"test content";
882        let merkle = fuchsia_merkle::root_from_slice(test_content);
883        let delivery_blob = Type1Blob::generate(test_content, CompressionMode::Never);
884        let ramdisk = ramdisk(block_size).await;
885        let mut blobfs = new_fs(&ramdisk, Blobfs::default()).await;
886
887        blobfs.format().await.expect("failed to format blobfs");
888        let mut serving = blobfs.serve().await.expect("failed to serve blobfs");
889        serving.bind_to_path("/test-blobfs-path").expect("bind_to_path failed");
890
891        {
892            let creator = fuchsia_component_client::connect_to_protocol_at_dir_root::<
893                BlobCreatorMarker,
894            >(serving.exposed_dir())
895            .unwrap();
896            let writer = creator.create(&merkle.into(), false).await.unwrap().unwrap();
897            let mut writer =
898                blob_writer::BlobWriter::create(writer.into_proxy(), delivery_blob.len() as u64)
899                    .await
900                    .unwrap();
901            writer.write(&delivery_blob).await.unwrap();
902        }
903
904        let entries = std::fs::read_dir("/test-blobfs-path")
905            .unwrap()
906            .map(|entry| entry.unwrap().file_name().into_string().unwrap())
907            .collect::<Vec<_>>();
908        assert_eq!(entries, &[merkle.to_string()]);
909
910        serving.shutdown().await.expect("failed to shutdown blobfs");
911    }
912
913    #[fuchsia::test]
914    async fn minfs_custom_config() {
915        let block_size = 512;
916        let ramdisk = ramdisk(block_size).await;
917        let config = Minfs {
918            verbose: true,
919            readonly: true,
920            fsck_after_every_transaction: true,
921            ..Default::default()
922        };
923        let mut minfs = new_fs(&ramdisk, config).await;
924
925        minfs.format().await.expect("failed to format minfs");
926        minfs.fsck().await.expect("failed to fsck minfs");
927        let _ = minfs.serve().await.expect("failed to serve minfs");
928
929        ramdisk.destroy().await.expect("failed to destroy ramdisk");
930    }
931
932    #[fuchsia::test]
933    async fn minfs_format_fsck_success() {
934        let block_size = 8192;
935        let ramdisk = ramdisk(block_size).await;
936        let mut minfs = new_fs(&ramdisk, Minfs::default()).await;
937
938        minfs.format().await.expect("failed to format minfs");
939        minfs.fsck().await.expect("failed to fsck minfs");
940
941        ramdisk.destroy().await.expect("failed to destroy ramdisk");
942    }
943
944    #[fuchsia::test]
945    async fn minfs_format_serve_write_query_restart_read_shutdown() {
946        let block_size = 8192;
947        let ramdisk = ramdisk(block_size).await;
948        let mut minfs = new_fs(&ramdisk, Minfs::default()).await;
949
950        minfs.format().await.expect("failed to format minfs");
951        let serving = minfs.serve().await.expect("failed to serve minfs the first time");
952
953        // snapshot of FilesystemInfo
954        let fs_info1 =
955            serving.query().await.expect("failed to query filesystem info after first serving");
956
957        let filename = "test_file";
958        let content = String::from("test content").into_bytes();
959
960        {
961            let test_file = fuchsia_fs::directory::open_file(
962                serving.root(),
963                filename,
964                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
965            )
966            .await
967            .expect("failed to create test file");
968            let _: u64 = test_file
969                .write(&content)
970                .await
971                .expect("failed to write to test file")
972                .map_err(Status::from_raw)
973                .expect("write error");
974        }
975
976        // check against the snapshot FilesystemInfo
977        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
978        assert_eq!(
979            fs_info2.used_bytes - fs_info1.used_bytes,
980            fs_info2.block_size as u64 // assuming content < 8K
981        );
982
983        serving.shutdown().await.expect("failed to shutdown minfs the first time");
984        let minfs = new_fs(&ramdisk, Minfs::default()).await;
985        let serving = minfs.serve().await.expect("failed to serve minfs the second time");
986
987        {
988            let test_file =
989                fuchsia_fs::directory::open_file(serving.root(), filename, fio::PERM_READABLE)
990                    .await
991                    .expect("failed to open test file");
992            let read_content =
993                fuchsia_fs::file::read(&test_file).await.expect("failed to read from test file");
994            assert_eq!(content, read_content);
995        }
996
997        // once more check against the snapshot FilesystemInfo
998        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
999        assert_eq!(
1000            fs_info3.used_bytes - fs_info1.used_bytes,
1001            fs_info3.block_size as u64 // assuming content < 8K
1002        );
1003
1004        let _ = serving.shutdown().await.expect("failed to shutdown minfs the second time");
1005
1006        ramdisk.destroy().await.expect("failed to destroy ramdisk");
1007    }
1008
1009    #[fuchsia::test]
1010    async fn minfs_bind_to_path() {
1011        let block_size = 8192;
1012        let test_content = b"test content";
1013        let ramdisk = ramdisk(block_size).await;
1014        let mut minfs = new_fs(&ramdisk, Minfs::default()).await;
1015
1016        minfs.format().await.expect("failed to format minfs");
1017        let mut serving = minfs.serve().await.expect("failed to serve minfs");
1018        serving.bind_to_path("/test-minfs-path").expect("bind_to_path failed");
1019        let test_path = "/test-minfs-path/test_file";
1020
1021        {
1022            let mut file = std::fs::File::create(test_path).expect("failed to create test file");
1023            file.write_all(test_content).expect("write bytes");
1024        }
1025
1026        {
1027            let mut file = std::fs::File::open(test_path).expect("failed to open test file");
1028            let mut buf = Vec::new();
1029            file.read_to_end(&mut buf).expect("failed to read test file");
1030            assert_eq!(buf, test_content);
1031        }
1032
1033        serving.shutdown().await.expect("failed to shutdown minfs");
1034
1035        std::fs::File::open(test_path).expect_err("test file was not unbound");
1036    }
1037
1038    #[fuchsia::test]
1039    async fn minfs_take_exposed_dir_does_not_drop() {
1040        let block_size = 512;
1041        let test_content = b"test content";
1042        let test_file_name = "test-file";
1043        let ramdisk = ramdisk(block_size).await;
1044        let mut minfs = new_fs(&ramdisk, Minfs::default()).await;
1045
1046        minfs.format().await.expect("failed to format fxfs");
1047
1048        let fs = minfs.serve().await.expect("failed to serve fxfs");
1049        let file = {
1050            let file = fuchsia_fs::directory::open_file(
1051                fs.root(),
1052                test_file_name,
1053                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
1054            )
1055            .await
1056            .unwrap();
1057            fuchsia_fs::file::write(&file, test_content).await.unwrap();
1058            file.close().await.expect("close fidl error").expect("close error");
1059            fuchsia_fs::directory::open_file(fs.root(), test_file_name, fio::PERM_READABLE)
1060                .await
1061                .unwrap()
1062        };
1063
1064        let exposed_dir = fs.take_exposed_dir();
1065
1066        assert_eq!(fuchsia_fs::file::read(&file).await.unwrap(), test_content);
1067
1068        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
1069            .expect("connecting to admin marker")
1070            .shutdown()
1071            .await
1072            .expect("shutdown failed");
1073    }
1074
1075    #[fuchsia::test]
1076    async fn f2fs_format_fsck_success() {
1077        let block_size = 4096;
1078        let ramdisk = ramdisk(block_size).await;
1079        let mut f2fs = new_fs(&ramdisk, F2fs::default()).await;
1080
1081        f2fs.format().await.expect("failed to format f2fs");
1082        f2fs.fsck().await.expect("failed to fsck f2fs");
1083
1084        ramdisk.destroy().await.expect("failed to destroy ramdisk");
1085    }
1086
1087    #[fuchsia::test]
1088    async fn f2fs_format_serve_write_query_restart_read_shutdown() {
1089        let block_size = 4096;
1090        let ramdisk = ramdisk(block_size).await;
1091        let mut f2fs = new_fs(&ramdisk, F2fs::default()).await;
1092
1093        f2fs.format().await.expect("failed to format f2fs");
1094        let serving = f2fs.serve().await.expect("failed to serve f2fs the first time");
1095
1096        // snapshot of FilesystemInfo
1097        let fs_info1 =
1098            serving.query().await.expect("failed to query filesystem info after first serving");
1099
1100        let filename = "test_file";
1101        let content = String::from("test content").into_bytes();
1102
1103        {
1104            let test_file = fuchsia_fs::directory::open_file(
1105                serving.root(),
1106                filename,
1107                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_WRITABLE,
1108            )
1109            .await
1110            .expect("failed to create test file");
1111            let _: u64 = test_file
1112                .write(&content)
1113                .await
1114                .expect("failed to write to test file")
1115                .map_err(Status::from_raw)
1116                .expect("write error");
1117        }
1118
1119        // check against the snapshot FilesystemInfo
1120        let fs_info2 = serving.query().await.expect("failed to query filesystem info after write");
1121        // With zx::stream, f2fs doesn't support the inline data feature allowing file
1122        // inode blocks to include small data. This way requires keeping two copies of VMOs
1123        // for the same inline data
1124        // assuming content < 4K and its inode block.
1125        let expected_size2 = fs_info2.block_size * 2;
1126        assert_eq!(fs_info2.used_bytes - fs_info1.used_bytes, expected_size2 as u64);
1127
1128        serving.shutdown().await.expect("failed to shutdown f2fs the first time");
1129        let f2fs = new_fs(&ramdisk, F2fs::default()).await;
1130        let serving = f2fs.serve().await.expect("failed to serve f2fs the second time");
1131
1132        {
1133            let test_file =
1134                fuchsia_fs::directory::open_file(serving.root(), filename, fio::PERM_READABLE)
1135                    .await
1136                    .expect("failed to open test file");
1137            let read_content =
1138                fuchsia_fs::file::read(&test_file).await.expect("failed to read from test file");
1139            assert_eq!(content, read_content);
1140        }
1141
1142        // once more check against the snapshot FilesystemInfo
1143        let fs_info3 = serving.query().await.expect("failed to query filesystem info after read");
1144        // assuming content < 4K and its inode block.
1145        let expected_size3 = fs_info3.block_size * 2;
1146        assert_eq!(fs_info3.used_bytes - fs_info1.used_bytes, expected_size3 as u64);
1147
1148        serving.shutdown().await.expect("failed to shutdown f2fs the second time");
1149        let mut f2fs = new_fs(&ramdisk, F2fs::default()).await;
1150        f2fs.fsck().await.expect("failed to fsck f2fs after shutting down the second time");
1151
1152        ramdisk.destroy().await.expect("failed to destroy ramdisk");
1153    }
1154
1155    #[fuchsia::test]
1156    async fn f2fs_bind_to_path() {
1157        let block_size = 4096;
1158        let test_content = b"test content";
1159        let ramdisk = ramdisk(block_size).await;
1160        let mut f2fs = new_fs(&ramdisk, F2fs::default()).await;
1161
1162        f2fs.format().await.expect("failed to format f2fs");
1163        let mut serving = f2fs.serve().await.expect("failed to serve f2fs");
1164        serving.bind_to_path("/test-f2fs-path").expect("bind_to_path failed");
1165        let test_path = "/test-f2fs-path/test_file";
1166
1167        {
1168            let mut file = std::fs::File::create(test_path).expect("failed to create test file");
1169            file.write_all(test_content).expect("write bytes");
1170        }
1171
1172        {
1173            let mut file = std::fs::File::open(test_path).expect("failed to open test file");
1174            let mut buf = Vec::new();
1175            file.read_to_end(&mut buf).expect("failed to read test file");
1176            assert_eq!(buf, test_content);
1177        }
1178
1179        serving.shutdown().await.expect("failed to shutdown f2fs");
1180
1181        std::fs::File::open(test_path).expect_err("test file was not unbound");
1182    }
1183
1184    #[fuchsia::test]
1185    async fn fxfs_open_volume() {
1186        let block_size = 512;
1187        let ramdisk = ramdisk(block_size).await;
1188        let mut fxfs = new_fs(&ramdisk, Fxfs::default()).await;
1189
1190        fxfs.format().await.expect("failed to format fxfs");
1191
1192        let fs = fxfs.serve_multi_volume().await.expect("failed to serve fxfs");
1193
1194        assert_eq!(fs.has_volume("foo").await.expect("has_volume"), false);
1195        assert!(
1196            fs.open_volume("foo", MountOptions::default()).await.is_err(),
1197            "Opening nonexistent volume should fail"
1198        );
1199
1200        let vol = fs
1201            .create_volume("foo", CreateOptions::default(), MountOptions::default())
1202            .await
1203            .expect("Create volume failed");
1204        vol.query().await.expect("Query volume failed");
1205        // TODO(https://fxbug.dev/42057878) Closing the volume is not synchronous. Immediately reopening the
1206        // volume will race with the asynchronous close and sometimes fail because the volume is
1207        // still mounted.
1208        // fs.open_volume("foo", MountOptions{crypt: None, as_blob: false}).await
1209        //    .expect("Open volume failed");
1210        assert_eq!(fs.has_volume("foo").await.expect("has_volume"), true);
1211    }
1212
1213    #[fuchsia::test]
1214    async fn fxfs_take_exposed_dir_does_not_drop() {
1215        let block_size = 512;
1216        let test_content = b"test content";
1217        let test_file_name = "test-file";
1218        let ramdisk = ramdisk(block_size).await;
1219        let mut fxfs = new_fs(&ramdisk, Fxfs::default()).await;
1220
1221        fxfs.format().await.expect("failed to format fxfs");
1222
1223        let fs = fxfs.serve_multi_volume().await.expect("failed to serve fxfs");
1224        let file = {
1225            let vol = fs
1226                .create_volume("foo", CreateOptions::default(), MountOptions::default())
1227                .await
1228                .expect("Create volume failed");
1229            let file = fuchsia_fs::directory::open_file(
1230                vol.root(),
1231                test_file_name,
1232                fio::Flags::FLAG_MAYBE_CREATE | fio::PERM_READABLE | fio::PERM_WRITABLE,
1233            )
1234            .await
1235            .unwrap();
1236            fuchsia_fs::file::write(&file, test_content).await.unwrap();
1237            file.close().await.expect("close fidl error").expect("close error");
1238            fuchsia_fs::directory::open_file(vol.root(), test_file_name, fio::PERM_READABLE)
1239                .await
1240                .unwrap()
1241        };
1242
1243        let exposed_dir = fs.take_exposed_dir();
1244
1245        assert_eq!(fuchsia_fs::file::read(&file).await.unwrap(), test_content);
1246
1247        connect_to_protocol_at_dir_root::<fidl_fuchsia_fs::AdminMarker>(&exposed_dir)
1248            .expect("connecting to admin marker")
1249            .shutdown()
1250            .await
1251            .expect("shutdown failed");
1252    }
1253}