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