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