storage_device/
file_backed_device.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
5use {
6    crate::{
7        Device,
8        buffer::{BufferFuture, BufferRef, MutableBufferRef},
9        buffer_allocator::{BufferAllocator, BufferSource},
10    },
11    anyhow::{Error, ensure},
12    async_trait::async_trait,
13    block_protocol::{ReadOptions, WriteOptions},
14    // Provides read_exact_at and write_all_at.
15    std::{ops::Range, os::unix::fs::FileExt},
16};
17
18/// FileBackedDevice is an implementation of Device backed by a std::fs::File. It is intended to be
19/// used for host tooling (to create or verify fxfs images), although it could also be used on
20/// Fuchsia builds if we wanted to do that for whatever reason.
21pub struct FileBackedDevice {
22    allocator: BufferAllocator,
23    file: std::fs::File,
24    block_count: u64,
25    block_size: u32,
26}
27
28const TRANSFER_HEAP_SIZE: usize = 32 * 1024 * 1024;
29
30impl FileBackedDevice {
31    /// Creates a new FileBackedDevice over |file|. The size of the file will be used as the size of
32    /// the Device.
33    pub fn new(file: std::fs::File, block_size: u32) -> Self {
34        let size = file.metadata().unwrap().len();
35        assert!(block_size > 0 && size > 0);
36        Self::new_with_block_count(file, block_size, size / block_size as u64)
37    }
38
39    /// Creates a new FileBackedDevice over |file| using an explicit size.  The underlying file is
40    /// *not* truncated to the target size, so the file size will be exactly as large as the
41    /// filesystem ends up using within the file.  With a sequential allocator, this makes the file
42    /// as big as it needs to be and no more.
43    pub fn new_with_block_count(file: std::fs::File, block_size: u32, block_count: u64) -> Self {
44        // NOTE: If file is S_ISBLK, we could (and probably should) use its block size. Rust does
45        // not appear to expose this information in a portable way, so we may need to dip into
46        // non-portable code to do so.
47        let allocator =
48            BufferAllocator::new(block_size as usize, BufferSource::new(TRANSFER_HEAP_SIZE));
49        Self { allocator, file, block_count, block_size }
50    }
51}
52
53#[async_trait]
54impl Device for FileBackedDevice {
55    fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
56        self.allocator.allocate_buffer(size)
57    }
58
59    fn block_size(&self) -> u32 {
60        self.block_size
61    }
62
63    fn block_count(&self) -> u64 {
64        self.block_count
65    }
66
67    async fn read_with_opts(
68        &self,
69        offset: u64,
70        mut buffer: MutableBufferRef<'_>,
71        _read_opts: ReadOptions,
72    ) -> Result<(), Error> {
73        assert_eq!(offset % self.block_size() as u64, 0);
74        assert_eq!(buffer.range().start % self.block_size() as usize, 0);
75        assert_eq!(buffer.len() % self.block_size() as usize, 0);
76        ensure!(offset + buffer.len() as u64 <= self.size(), "Reading past end of file");
77        // This isn't actually async, but that probably doesn't matter for host usage.
78        self.file.read_exact_at(buffer.as_mut_slice(), offset)?;
79        Ok(())
80    }
81
82    async fn write_with_opts(
83        &self,
84        offset: u64,
85        buffer: BufferRef<'_>,
86        _write_opts: WriteOptions,
87    ) -> Result<(), Error> {
88        assert_eq!(offset % self.block_size() as u64, 0);
89        assert_eq!(buffer.range().start % self.block_size() as usize, 0);
90        assert_eq!(buffer.len() % self.block_size() as usize, 0);
91        ensure!(offset + buffer.len() as u64 <= self.size(), "Writing past end of file");
92        // This isn't actually async, but that probably doesn't matter for host usage.
93        self.file.write_all_at(buffer.as_slice(), offset)?;
94        Ok(())
95    }
96
97    async fn trim(&self, range: Range<u64>) -> Result<(), Error> {
98        assert_eq!(range.start % self.block_size() as u64, 0);
99        assert_eq!(range.end % self.block_size() as u64, 0);
100        // Blast over the range to simulate it being used for something else.
101        // This will help catch incorrect usage of trim, and since FileBackedDevice is not used in a
102        // production context, there should be no performance issues.
103        // Note that we could punch a hole in the file instead using platform-dependent operations
104        // (e.g. FALLOC_FL_PUNCH_HOLE on Linux) to speed this up if needed.
105        const BUF: [u8; 8192] = [0xab; 8192];
106        let mut offset = range.start;
107        while offset < range.end {
108            let len = std::cmp::min(BUF.len(), range.end as usize - offset as usize);
109            self.file.write_at(&BUF[..len], offset)?;
110            offset += len as u64;
111        }
112        Ok(())
113    }
114
115    async fn close(&self) -> Result<(), Error> {
116        // This isn't actually async, but that probably doesn't matter for host usage.
117        self.file.sync_all()?;
118        Ok(())
119    }
120
121    async fn flush(&self) -> Result<(), Error> {
122        self.file.sync_data().map_err(Into::into)
123    }
124
125    fn barrier(&self) {}
126
127    fn is_read_only(&self) -> bool {
128        false
129    }
130
131    fn supports_trim(&self) -> bool {
132        // We "support" trim insofar as Device::trim() can be called.  The actual implementation is,
133        // of course, simulated.
134        true
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use crate::Device;
141    use crate::file_backed_device::FileBackedDevice;
142    use std::fs::{File, OpenOptions};
143    use std::path::PathBuf;
144
145    fn create_file() -> (PathBuf, File) {
146        let mut temp_path = std::env::temp_dir();
147        temp_path.push(format!("file_{:x}", rand::random::<u64>()));
148        let (pathbuf, file) = (
149            temp_path.clone(),
150            OpenOptions::new()
151                .read(true)
152                .write(true)
153                .create_new(true)
154                .open(temp_path.as_path())
155                .unwrap_or_else(|e| panic!("create {:?} failed: {:?}", temp_path.as_path(), e)),
156        );
157        file.set_len(1024 * 1024).expect("Failed to truncate file");
158        (pathbuf, file)
159    }
160
161    #[fuchsia::test]
162    async fn test_lifecycle() {
163        let (_path, file) = create_file();
164        let device = FileBackedDevice::new(file, 512);
165
166        {
167            let _buf = device.allocate_buffer(8192).await;
168        }
169
170        device.close().await.expect("Close failed");
171    }
172
173    #[fuchsia::test]
174    async fn test_read_write() {
175        let (_path, file) = create_file();
176        let device = FileBackedDevice::new(file, 512);
177
178        {
179            let mut buf1 = device.allocate_buffer(8192).await;
180            let mut buf2 = device.allocate_buffer(8192).await;
181            buf1.as_mut_slice().fill(0xaa as u8);
182            buf2.as_mut_slice().fill(0xbb as u8);
183            device.write(65536, buf1.as_ref()).await.expect("Write failed");
184            device.write(65536 + 8192, buf2.as_ref()).await.expect("Write failed");
185        }
186        {
187            let mut buf = device.allocate_buffer(16384).await;
188            device.read(65536, buf.as_mut()).await.expect("Read failed");
189            assert_eq!(buf.as_slice()[..8192], vec![0xaa as u8; 8192]);
190            assert_eq!(buf.as_slice()[8192..], vec![0xbb as u8; 8192]);
191        }
192
193        device.close().await.expect("Close failed");
194    }
195
196    #[fuchsia::test]
197    async fn test_read_write_past_end_of_file_fails() {
198        let (_path, file) = create_file();
199        let device = FileBackedDevice::new(file, 512);
200
201        {
202            let mut buf = device.allocate_buffer(8192).await;
203            let offset = (device.size() as usize - buf.len() + device.block_size() as usize) as u64;
204            buf.as_mut_slice().fill(0xaa as u8);
205            device.write(offset, buf.as_ref()).await.expect_err("Write should have failed");
206            device.read(offset, buf.as_mut()).await.expect_err("Read should have failed");
207        }
208
209        device.close().await.expect("Close failed");
210    }
211
212    #[fuchsia::test]
213    async fn test_writes_persist() {
214        let (path, file) = create_file();
215        let device = FileBackedDevice::new(file, 512);
216
217        {
218            let mut buf1 = device.allocate_buffer(8192).await;
219            let mut buf2 = device.allocate_buffer(8192).await;
220            buf1.as_mut_slice().fill(0xaa as u8);
221            buf2.as_mut_slice().fill(0xbb as u8);
222            device.write(65536, buf1.as_ref()).await.expect("Write failed");
223            device.write(65536 + 8192, buf2.as_ref()).await.expect("Write failed");
224        }
225        device.close().await.expect("Close failed");
226
227        let file = File::open(path.as_path()).expect("Open failed");
228        let device = FileBackedDevice::new(file, 512);
229
230        {
231            let mut buf = device.allocate_buffer(16384).await;
232            device.read(65536, buf.as_mut()).await.expect("Read failed");
233            assert_eq!(buf.as_slice()[..8192], vec![0xaa as u8; 8192]);
234            assert_eq!(buf.as_slice()[8192..], vec![0xbb as u8; 8192]);
235        }
236        device.close().await.expect("Close failed");
237    }
238}