fxfs/
virtual_device.rs

1// Copyright 2025 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//! Implementation of a virtual storage devices backed by a [`ReadObjectHandle`]. Allows using
6//! files within an existing fxfs as a virtual storage device (e.g. to mount an inner filesystem).
7
8use crate::errors::FxfsError;
9use crate::object_handle::ReadObjectHandle;
10use anyhow::{Context as _, Error, bail};
11use async_trait::async_trait;
12use std::ops::Range;
13use storage_device::buffer::MutableBufferRef;
14use storage_device::buffer_allocator::BufferFuture;
15use storage_device::{Device, ReadOptions};
16
17/// Allows using anything that implements [`ReadObjectHandle`] as a read-only storage [`Device`].
18pub struct ReadOnlyDevice<H: ReadObjectHandle> {
19    handle: H,
20}
21
22impl<H: ReadObjectHandle> ReadOnlyDevice<H> {
23    pub fn new(handle: H) -> Result<Self, Error> {
24        let device = Self { handle };
25        // Prevent division by zero when calculating the block count.
26        if device.block_size() == 0 {
27            bail!("Expected non-zero block size.");
28        }
29        Ok(device)
30    }
31}
32
33#[async_trait]
34impl<H: ReadObjectHandle> Device for ReadOnlyDevice<H> {
35    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
36        self.handle.allocate_buffer(size)
37    }
38
39    fn block_size(&self) -> u32 {
40        self.handle.block_size() as u32
41    }
42
43    fn block_count(&self) -> u64 {
44        self.handle.get_size() / self.handle.block_size()
45    }
46
47    async fn read_with_opts(
48        &self,
49        offset: u64,
50        buffer: MutableBufferRef<'_>,
51        _read_opts: ReadOptions,
52    ) -> Result<(), Error> {
53        let len = buffer.len();
54        let amount = self.handle.read(offset, buffer).await?;
55        if amount != len {
56            return Err(FxfsError::OutOfRange).context("short read from underlying object");
57        }
58        Ok(())
59    }
60
61    async fn close(&self) -> Result<(), Error> {
62        Ok(())
63    }
64
65    fn is_read_only(&self) -> bool {
66        true
67    }
68
69    fn supports_trim(&self) -> bool {
70        false
71    }
72
73    async fn write_with_opts(
74        &self,
75        _offset: u64,
76        _buffer: storage_device::buffer::BufferRef<'_>,
77        _write_opts: storage_device::WriteOptions,
78    ) -> Result<(), Error> {
79        unreachable!()
80    }
81
82    async fn flush(&self) -> Result<(), Error> {
83        unreachable!()
84    }
85
86    async fn trim(&self, _range: Range<u64>) -> Result<(), Error> {
87        unreachable!()
88    }
89
90    fn barrier(&self) {
91        unreachable!()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98    use crate::filesystem::FxFilesystem;
99    use crate::object_handle::ObjectHandle as _;
100    use crate::object_store::transaction::{LockKey, lock_keys};
101    use crate::object_store::volume::root_volume;
102    use crate::object_store::{DataObjectHandle, Directory, NewChildStoreOptions, ObjectStore};
103    use std::sync::Arc;
104    use storage_device::DeviceHolder;
105    use storage_device::fake_device::FakeDevice;
106
107    /// Helper function that creates a test file filled with a known byte pattern.
108    /// Each block in the file will be filled with the block offset mod 0xFF.
109    async fn create_test_file(
110        fs: &Arc<FxFilesystem>,
111        num_blocks: usize,
112    ) -> DataObjectHandle<ObjectStore> {
113        let root_vol = root_volume(fs.clone()).await.unwrap();
114        let store = root_vol.new_volume("test", NewChildStoreOptions::default()).await.unwrap();
115        let test_vol_root =
116            Directory::open(&store, store.root_directory_object_id()).await.unwrap();
117
118        let mut transaction = fs
119            .clone()
120            .new_transaction(
121                lock_keys![LockKey::object(
122                    store.store_object_id(),
123                    store.root_directory_object_id()
124                )],
125                Default::default(),
126            )
127            .await
128            .unwrap();
129
130        let object = test_vol_root.create_child_file(&mut transaction, "test_file").await.unwrap();
131        transaction.commit().await.unwrap();
132
133        {
134            let mut transaction = object.new_transaction().await.unwrap();
135            let block_size = object.block_size() as usize;
136            let mut buffer = object.allocate_buffer(block_size * num_blocks).await;
137            for i in 0..num_blocks {
138                let buff_range = (i * block_size)..((i + 1) * block_size);
139                buffer.as_mut_slice()[buff_range].fill((i % 0xFF) as u8);
140            }
141            object.txn_write(&mut transaction, 0, buffer.as_ref()).await.unwrap();
142
143            transaction.commit().await.unwrap();
144        }
145
146        object
147    }
148
149    #[fuchsia::test]
150    async fn test_read_only_virtual_device() {
151        const BLOCK_SIZE: usize = 4096;
152        const TEST_FILE_BLOCK_COUNT: usize = 64;
153        let device = DeviceHolder::new(FakeDevice::new(512, BLOCK_SIZE as u32));
154        let fs = FxFilesystem::new_empty(device).await.unwrap();
155        let handle = create_test_file(&fs, TEST_FILE_BLOCK_COUNT).await;
156        let handle_as_device = ReadOnlyDevice::new(handle).unwrap();
157        assert_eq!(handle_as_device.block_size(), BLOCK_SIZE as u32);
158        assert_eq!(handle_as_device.block_count(), TEST_FILE_BLOCK_COUNT as u64);
159
160        // We should be able to read the whole file through our virtual read-only device.
161        let mut buffer = handle_as_device.allocate_buffer(BLOCK_SIZE * TEST_FILE_BLOCK_COUNT).await;
162        handle_as_device.read(0, buffer.as_mut()).await.unwrap();
163        for i in 0..TEST_FILE_BLOCK_COUNT {
164            let buff_range = (i * BLOCK_SIZE)..((i + 1) * BLOCK_SIZE);
165            assert_eq!(buffer.as_slice()[buff_range], [(i % 0xFF) as u8; BLOCK_SIZE]);
166        }
167
168        // Test reading from an offset.
169        let mut buffer = handle_as_device.allocate_buffer(BLOCK_SIZE).await;
170        handle_as_device.read((BLOCK_SIZE * 4) as u64, buffer.as_mut()).await.unwrap();
171        assert_eq!(buffer.as_slice(), [4u8; BLOCK_SIZE]);
172    }
173}