Skip to main content

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        if let Some(slice) = buffer.try_as_mut_slice() {
79            self.file.read_exact_at(slice, offset)?;
80        } else {
81            let mut data = vec![0u8; buffer.len()];
82            self.file.read_exact_at(&mut data, offset)?;
83            buffer.copy_from_slice(&data);
84        }
85        Ok(())
86    }
87
88    async fn write_with_opts(
89        &self,
90        offset: u64,
91        buffer: BufferRef<'_>,
92        _write_opts: WriteOptions,
93    ) -> Result<(), Error> {
94        assert_eq!(offset % self.block_size() as u64, 0);
95        assert_eq!(buffer.range().start % self.block_size() as usize, 0);
96        assert_eq!(buffer.len() % self.block_size() as usize, 0);
97        ensure!(offset + buffer.len() as u64 <= self.size(), "Writing past end of file");
98        // This isn't actually async, but that probably doesn't matter for host usage.
99        if let Some(slice) = buffer.try_as_slice() {
100            self.file.write_all_at(slice, offset)?;
101        } else {
102            let data = buffer.to_vec();
103            self.file.write_all_at(&data, offset)?;
104        }
105        Ok(())
106    }
107
108    async fn trim(&self, range: Range<u64>) -> Result<(), Error> {
109        assert_eq!(range.start % self.block_size() as u64, 0);
110        assert_eq!(range.end % self.block_size() as u64, 0);
111        // Blast over the range to simulate it being used for something else.
112        // This will help catch incorrect usage of trim, and since FileBackedDevice is not used in a
113        // production context, there should be no performance issues.
114        // Note that we could punch a hole in the file instead using platform-dependent operations
115        // (e.g. FALLOC_FL_PUNCH_HOLE on Linux) to speed this up if needed.
116        const BUF: [u8; 8192] = [0xab; 8192];
117        let mut offset = range.start;
118        while offset < range.end {
119            let len = std::cmp::min(BUF.len(), range.end as usize - offset as usize);
120            self.file.write_at(&BUF[..len], offset)?;
121            offset += len as u64;
122        }
123        Ok(())
124    }
125
126    async fn close(&self) -> Result<(), Error> {
127        // This isn't actually async, but that probably doesn't matter for host usage.
128        self.file.sync_all()?;
129        Ok(())
130    }
131
132    async fn flush(&self) -> Result<(), Error> {
133        self.file.sync_data().map_err(Into::into)
134    }
135
136    fn barrier(&self) {}
137
138    fn is_read_only(&self) -> bool {
139        false
140    }
141
142    fn supports_trim(&self) -> bool {
143        // We "support" trim insofar as Device::trim() can be called.  The actual implementation is,
144        // of course, simulated.
145        true
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::Device;
152    use crate::file_backed_device::FileBackedDevice;
153    use std::fs::{File, OpenOptions};
154    use std::path::PathBuf;
155
156    fn create_file() -> (PathBuf, File) {
157        let mut temp_path = std::env::temp_dir();
158        temp_path.push(format!("file_{:x}", rand::random::<u64>()));
159        let (pathbuf, file) = (
160            temp_path.clone(),
161            OpenOptions::new()
162                .read(true)
163                .write(true)
164                .create_new(true)
165                .open(temp_path.as_path())
166                .unwrap_or_else(|e| panic!("create {:?} failed: {:?}", temp_path.as_path(), e)),
167        );
168        file.set_len(1024 * 1024).expect("Failed to truncate file");
169        (pathbuf, file)
170    }
171
172    #[fuchsia::test]
173    async fn test_lifecycle() {
174        let (_path, file) = create_file();
175        let device = FileBackedDevice::new(file, 512);
176
177        {
178            let _buf = device.allocate_buffer(8192).await;
179        }
180
181        device.close().await.expect("Close failed");
182    }
183
184    #[fuchsia::test]
185    async fn test_read_write() {
186        let (_path, file) = create_file();
187        let device = FileBackedDevice::new(file, 512);
188
189        {
190            let mut buf1 = device.allocate_buffer(8192).await;
191            let mut buf2 = device.allocate_buffer(8192).await;
192            buf1.fill(0xaa);
193            buf2.fill(0xbb);
194            device.write(65536, buf1.as_ref()).await.expect("Write failed");
195            device.write(65536 + 8192, buf2.as_ref()).await.expect("Write failed");
196        }
197        {
198            let mut buf = device.allocate_buffer(16384).await;
199            device.read(65536, buf.as_mut()).await.expect("Read failed");
200            let mut data = vec![0u8; 16384];
201            buf.copy_to_slice(&mut data);
202            assert_eq!(data[..8192], vec![0xaa as u8; 8192]);
203            assert_eq!(data[8192..], vec![0xbb as u8; 8192]);
204        }
205
206        device.close().await.expect("Close failed");
207    }
208
209    #[fuchsia::test]
210    async fn test_read_write_past_end_of_file_fails() {
211        let (_path, file) = create_file();
212        let device = FileBackedDevice::new(file, 512);
213
214        {
215            let mut buf = device.allocate_buffer(8192).await;
216            let offset = (device.size() as usize - buf.len() + device.block_size() as usize) as u64;
217            buf.fill(0xaa);
218            device.write(offset, buf.as_ref()).await.expect_err("Write should have failed");
219            device.read(offset, buf.as_mut()).await.expect_err("Read should have failed");
220        }
221
222        device.close().await.expect("Close failed");
223    }
224
225    #[fuchsia::test]
226    async fn test_writes_persist() {
227        let (path, file) = create_file();
228        let device = FileBackedDevice::new(file, 512);
229
230        {
231            let mut buf1 = device.allocate_buffer(8192).await;
232            let mut buf2 = device.allocate_buffer(8192).await;
233            buf1.fill(0xaa);
234            buf2.fill(0xbb);
235            device.write(65536, buf1.as_ref()).await.expect("Write failed");
236            device.write(65536 + 8192, buf2.as_ref()).await.expect("Write failed");
237        }
238        device.close().await.expect("Close failed");
239
240        let file = File::open(path.as_path()).expect("Open failed");
241        let device = FileBackedDevice::new(file, 512);
242
243        {
244            let mut buf = device.allocate_buffer(16384).await;
245            device.read(65536, buf.as_mut()).await.expect("Read failed");
246            let mut data = vec![0u8; 16384];
247            buf.copy_to_slice(&mut data);
248            assert_eq!(data[..8192], vec![0xaa as u8; 8192]);
249            assert_eq!(data[8192..], vec![0xbb as u8; 8192]);
250        }
251        device.close().await.expect("Close failed");
252    }
253}