blob: 807460c51fbf5409772c78bf3553ac642c5a536e [file] [edit]
// Copyright 2025 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//! Implementation of a virtual storage devices backed by a [`ReadObjectHandle`]. Allows using
//! files within an existing fxfs as a virtual storage device (e.g. to mount an inner filesystem).
use crate::errors::FxfsError;
use crate::object_handle::ReadObjectHandle;
use anyhow::{Context as _, Error, bail};
use async_trait::async_trait;
use std::ops::Range;
use storage_device::buffer::MutableBufferRef;
use storage_device::buffer_allocator::BufferFuture;
use storage_device::{Device, ReadOptions};
/// Allows using anything that implements [`ReadObjectHandle`] as a read-only storage [`Device`].
pub struct ReadOnlyDevice<H: ReadObjectHandle> {
handle: H,
}
impl<H: ReadObjectHandle> ReadOnlyDevice<H> {
pub fn new(handle: H) -> Result<Self, Error> {
let device = Self { handle };
// Prevent division by zero when calculating the block count.
if device.block_size() == 0 {
bail!("Expected non-zero block size.");
}
Ok(device)
}
}
#[async_trait]
impl<H: ReadObjectHandle> Device for ReadOnlyDevice<H> {
fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
self.handle.allocate_buffer(size)
}
fn block_size(&self) -> u32 {
self.handle.block_size().get() as u32
}
fn block_count(&self) -> u64 {
self.handle.get_size() / self.handle.block_size()
}
async fn read_with_opts(
&self,
offset: u64,
buffer: MutableBufferRef<'_>,
_read_opts: ReadOptions,
) -> Result<(), Error> {
let len = buffer.len();
let amount = self.handle.read(offset, buffer).await?;
if amount != len {
return Err(FxfsError::OutOfRange).context("short read from underlying object");
}
Ok(())
}
async fn close(&self) -> Result<(), Error> {
Ok(())
}
fn is_read_only(&self) -> bool {
true
}
fn supports_trim(&self) -> bool {
false
}
async fn write_with_opts(
&self,
_offset: u64,
_buffer: storage_device::buffer::BufferRef<'_>,
_write_opts: storage_device::WriteOptions,
) -> Result<(), Error> {
unreachable!()
}
async fn flush(&self) -> Result<(), Error> {
unreachable!()
}
async fn trim(&self, _range: Range<u64>) -> Result<(), Error> {
unreachable!()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::filesystem::FxFilesystem;
use crate::object_handle::ObjectHandle as _;
use crate::object_store::transaction::{LockKey, lock_keys};
use crate::object_store::volume::root_volume;
use crate::object_store::{DataObjectHandle, Directory, NewChildStoreOptions, ObjectStore};
use std::sync::Arc;
use storage_device::DeviceHolder;
use storage_device::fake_device::FakeDevice;
/// Helper function that creates a test file filled with a known byte pattern.
/// Each block in the file will be filled with the block offset mod 0xFF.
async fn create_test_file(
fs: &Arc<FxFilesystem>,
num_blocks: usize,
) -> DataObjectHandle<ObjectStore> {
let root_vol = root_volume(fs.clone()).await.unwrap();
let store = root_vol.new_volume("test", NewChildStoreOptions::default()).await.unwrap();
let test_vol_root =
Directory::open(&store, store.root_directory_object_id()).await.unwrap();
let mut transaction = fs
.root_store()
.new_transaction(
lock_keys![LockKey::object(
store.store_object_id(),
store.root_directory_object_id()
)],
Default::default(),
)
.await
.unwrap();
let object = test_vol_root.create_child_file(&mut transaction, "test_file").await.unwrap();
transaction.commit().await.unwrap();
{
let mut transaction = object.new_transaction().await.unwrap();
let block_size = object.block_size().get() as usize;
let mut buffer = object.allocate_buffer(block_size * num_blocks).await;
for i in 0..num_blocks {
let buff_range = (i * block_size)..((i + 1) * block_size);
buffer.subslice_mut(buff_range).fill((i % 0xFF) as u8);
}
object.txn_write(&mut transaction, 0, buffer.as_ref()).await.unwrap();
transaction.commit().await.unwrap();
}
object
}
#[fuchsia::test]
async fn test_read_only_virtual_device() {
const BLOCK_SIZE: usize = 4096;
const TEST_FILE_BLOCK_COUNT: usize = 64;
let device = DeviceHolder::new(FakeDevice::new(512, BLOCK_SIZE as u32));
let fs = FxFilesystem::new_empty(device).await.unwrap();
let handle = create_test_file(&fs, TEST_FILE_BLOCK_COUNT).await;
let handle_as_device = ReadOnlyDevice::new(handle).unwrap();
assert_eq!(handle_as_device.block_size(), BLOCK_SIZE as u32);
assert_eq!(handle_as_device.block_count(), TEST_FILE_BLOCK_COUNT as u64);
// We should be able to read the whole file through our virtual read-only device.
let mut buffer = handle_as_device.allocate_buffer(BLOCK_SIZE * TEST_FILE_BLOCK_COUNT).await;
handle_as_device.read(0, buffer.as_mut()).await.unwrap();
let data = buffer.to_vec();
for i in 0..TEST_FILE_BLOCK_COUNT {
let buff_range = (i * BLOCK_SIZE)..((i + 1) * BLOCK_SIZE);
assert_eq!(data[buff_range], [(i % 0xFF) as u8; BLOCK_SIZE]);
}
// Test reading from an offset.
let mut buffer = handle_as_device.allocate_buffer(BLOCK_SIZE).await;
handle_as_device.read((BLOCK_SIZE * 4) as u64, buffer.as_mut()).await.unwrap();
assert_eq!(buffer.to_vec(), [4u8; BLOCK_SIZE]);
}
}