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.
45use {
6crate::{
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?
16std::{ops::Range, os::unix::fs::FileExt},
17};
1819// TODO(csuter): Consider using an async file interface.
2021/// 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}
3031const TRANSFER_HEAP_SIZE: usize = 32 * 1024 * 1024;
3233impl FileBackedDevice {
34/// Creates a new FileBackedDevice over |file|. The size of the file will be used as the size of
35 /// the Device.
36pub fn new(file: std::fs::File, block_size: u32) -> Self {
37let size = file.metadata().unwrap().len();
38assert!(block_size > 0 && size > 0);
39Self::new_with_block_count(file, block_size, size / block_size as u64)
40 }
4142/// 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.
46pub 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.
50let allocator =
51 BufferAllocator::new(block_size as usize, BufferSource::new(TRANSFER_HEAP_SIZE));
52Self { allocator, file, block_count, block_size }
53 }
54}
5556#[async_trait]
57impl Device for FileBackedDevice {
58fn allocate_buffer(&self, size: usize) -> BufferFuture<'_> {
59self.allocator.allocate_buffer(size)
60 }
6162fn block_size(&self) -> u32 {
63self.block_size
64 }
6566fn block_count(&self) -> u64 {
67self.block_count
68 }
6970async fn read(&self, offset: u64, mut buffer: MutableBufferRef<'_>) -> Result<(), Error> {
71assert_eq!(offset % self.block_size() as u64, 0);
72assert_eq!(buffer.range().start % self.block_size() as usize, 0);
73assert_eq!(buffer.len() % self.block_size() as usize, 0);
74ensure!(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.
76self.file.read_exact_at(buffer.as_mut_slice(), offset)?;
77Ok(())
78 }
7980async fn write_with_opts(
81&self,
82 offset: u64,
83 buffer: BufferRef<'_>,
84 _opts: WriteOptions,
85 ) -> Result<(), Error> {
86assert_eq!(offset % self.block_size() as u64, 0);
87assert_eq!(buffer.range().start % self.block_size() as usize, 0);
88assert_eq!(buffer.len() % self.block_size() as usize, 0);
89ensure!(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.
91self.file.write_all_at(buffer.as_slice(), offset)?;
92Ok(())
93 }
9495async fn trim(&self, range: Range<u64>) -> Result<(), Error> {
96assert_eq!(range.start % self.block_size() as u64, 0);
97assert_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.
103const BUF: [u8; 8192] = [0xab; 8192];
104let mut offset = range.start;
105while offset < range.end {
106let len = std::cmp::min(BUF.len(), range.end as usize - offset as usize);
107self.file.write_at(&BUF[..len], offset)?;
108 offset += len as u64;
109 }
110Ok(())
111 }
112113async fn close(&self) -> Result<(), Error> {
114// This isn't actually async, but that probably doesn't matter for host usage.
115self.file.sync_all()?;
116Ok(())
117 }
118119async fn flush(&self) -> Result<(), Error> {
120self.file.sync_data().map_err(Into::into)
121 }
122123fn is_read_only(&self) -> bool {
124false
125}
126127fn supports_trim(&self) -> bool {
128// We "support" trim insofar as Device::trim() can be called. The actual implementation is,
129 // of course, simulated.
130true
131}
132}
133134#[cfg(test)]
135mod tests {
136use crate::file_backed_device::FileBackedDevice;
137use crate::Device;
138use std::fs::{File, OpenOptions};
139use std::path::PathBuf;
140141fn create_file() -> (PathBuf, File) {
142let mut temp_path = std::env::temp_dir();
143 temp_path.push(format!("file_{:x}", rand::random::<u64>()));
144let (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 }
156157#[fuchsia::test]
158async fn test_lifecycle() {
159let (_path, file) = create_file();
160let device = FileBackedDevice::new(file, 512);
161162 {
163let _buf = device.allocate_buffer(8192).await;
164 }
165166 device.close().await.expect("Close failed");
167 }
168169#[fuchsia::test]
170async fn test_read_write() {
171let (_path, file) = create_file();
172let device = FileBackedDevice::new(file, 512);
173174 {
175let mut buf1 = device.allocate_buffer(8192).await;
176let 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 {
183let mut buf = device.allocate_buffer(16384).await;
184 device.read(65536, buf.as_mut()).await.expect("Read failed");
185assert_eq!(buf.as_slice()[..8192], vec![0xaa as u8; 8192]);
186assert_eq!(buf.as_slice()[8192..], vec![0xbb as u8; 8192]);
187 }
188189 device.close().await.expect("Close failed");
190 }
191192#[fuchsia::test]
193async fn test_read_write_past_end_of_file_fails() {
194let (_path, file) = create_file();
195let device = FileBackedDevice::new(file, 512);
196197 {
198let mut buf = device.allocate_buffer(8192).await;
199let 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 }
204205 device.close().await.expect("Close failed");
206 }
207208#[fuchsia::test]
209async fn test_writes_persist() {
210let (path, file) = create_file();
211let device = FileBackedDevice::new(file, 512);
212213 {
214let mut buf1 = device.allocate_buffer(8192).await;
215let 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");
222223let file = File::open(path.as_path()).expect("Open failed");
224let device = FileBackedDevice::new(file, 512);
225226 {
227let mut buf = device.allocate_buffer(16384).await;
228 device.read(65536, buf.as_mut()).await.expect("Read failed");
229assert_eq!(buf.as_slice()[..8192], vec![0xaa as u8; 8192]);
230assert_eq!(buf.as_slice()[8192..], vec![0xbb as u8; 8192]);
231 }
232 device.close().await.expect("Close failed");
233 }
234}