Skip to main content

fs_core/
file_device.rs

1//! File-backed `BlockDevice`. Used for disk images, raw `/dev/diskN` reads,
2//! anything that std::fs::File can address.
3
4use crate::block::{BlockDevice, BlockRead};
5use crate::error::{Error, Result};
6use std::fs::{File, OpenOptions};
7use std::io::{Seek, SeekFrom, Write};
8use std::path::Path;
9use std::sync::Mutex;
10
11/// A file opened as a block device.
12///
13/// # Reads do not take the lock; writes do
14///
15/// A read was `seek` then `read` under a mutex, which made the file's
16/// cursor shared state: two threads reading different offsets had to
17/// take turns, not because the device could not serve them at once but
18/// because one would have moved the other's cursor.
19///
20/// On Unix the cursor is not involved at all — `pread` takes the offset
21/// as an argument — so reads run without the lock and genuinely overlap.
22///
23/// On Windows the equivalent (`seek_read`) *does* move the file
24/// pointer, so the lock stays there. Same behaviour, one platform
25/// paying for it.
26///
27/// Writes keep the lock on both, because `write_at` is still `seek` plus
28/// `write_all` and a partial write must not have another writer's seek
29/// land in the middle of it.
30pub struct FileDevice {
31    file: File,
32    /// Held for writes only — see the type's own note. `()` rather than
33    /// the file, so that a reader physically cannot be made to wait on
34    /// it by a later edit.
35    write_lock: Mutex<()>,
36    size: u64,
37    writable: bool,
38}
39
40impl FileDevice {
41    /// Open read-only.
42    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
43        let file = File::open(path)?;
44        let size = file.metadata()?.len();
45        Ok(Self {
46            file,
47            write_lock: Mutex::new(()),
48            size,
49            writable: false,
50        })
51    }
52
53    /// Open read-write. Errors if the path is not writable.
54    pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
55        let file = OpenOptions::new().read(true).write(true).open(path)?;
56        let size = file.metadata()?.len();
57        Ok(Self {
58            file,
59            write_lock: Mutex::new(()),
60            size,
61            writable: true,
62        })
63    }
64
65    /// Open read-write if possible, fall back to read-only otherwise.
66    pub fn open_best_effort<P: AsRef<Path>>(path: P) -> Result<Self> {
67        let p = path.as_ref();
68        match Self::open_rw(p) {
69            Ok(d) => Ok(d),
70            Err(_) => Self::open(p),
71        }
72    }
73}
74
75impl FileDevice {
76    /// One positioned read, returning what it got.
77    ///
78    /// Unix: `pread`, which does not touch the file cursor, so this
79    /// needs no lock and concurrent readers overlap.
80    #[cfg(unix)]
81    fn read_once(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
82        use std::os::unix::fs::FileExt;
83        Ok(self.file.read_at(buf, offset)?)
84    }
85
86    /// Windows: `seek_read` DOES move the file pointer, so the lock is
87    /// still required here. The interface is the same and only this
88    /// platform pays.
89    #[cfg(windows)]
90    fn read_once(&self, offset: u64, buf: &mut [u8]) -> Result<usize> {
91        use std::os::windows::fs::FileExt;
92        let _guard = self.write_lock.lock().unwrap();
93        Ok(self.file.seek_read(buf, offset)?)
94    }
95}
96
97impl BlockRead for FileDevice {
98    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
99        // A SHORT READ IS AN ERROR NAMING WHAT WAS ASKED FOR AND WHAT
100        // ARRIVED, not a smaller answer: a caller that asked for a block
101        // and got half of one cannot tell the difference from bytes.
102        let mut total = 0usize;
103        while total < buf.len() {
104            let n = self.read_once(offset + total as u64, &mut buf[total..])?;
105            if n == 0 {
106                return Err(Error::ShortRead {
107                    offset,
108                    want: buf.len(),
109                    got: total,
110                });
111            }
112            total += n;
113        }
114        Ok(())
115    }
116
117    fn size_bytes(&self) -> u64 {
118        self.size
119    }
120}
121
122impl BlockDevice for FileDevice {
123    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
124        if !self.writable {
125            return Err(Error::ReadOnly);
126        }
127        let _guard = self.write_lock.lock().unwrap();
128        let mut f = &self.file;
129        f.seek(SeekFrom::Start(offset))?;
130        f.write_all(buf)?;
131        Ok(())
132    }
133
134    fn flush(&self) -> Result<()> {
135        if !self.writable {
136            return Ok(());
137        }
138        let _guard = self.write_lock.lock().unwrap();
139        let mut f = &self.file;
140        f.flush()?;
141        self.file.sync_data()?;
142        Ok(())
143    }
144
145    fn is_writable(&self) -> bool {
146        self.writable
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    /// Concurrent readers do not serialise, and none of them sees
155    /// another's offset.
156    ///
157    /// THE BUG THIS REPLACES: reads were `seek` then `read` under one
158    /// mutex, so the file cursor was shared state. Two threads reading
159    /// different parts of the same image took turns for no reason the
160    /// device imposed. Worse, the shape was one edit away from being
161    /// wrong rather than merely slow -- drop the lock without moving to
162    /// positioned reads and every reader corrupts every other reader's
163    /// offset.
164    ///
165    /// The assertion is on the BYTES rather than on timing: a test that
166    /// measured overlap would be a flake on a loaded machine, while a
167    /// reader that got another's offset returns the wrong bytes every
168    /// time.
169    #[test]
170    fn many_threads_reading_different_offsets_each_get_their_own_bytes() {
171        let path = temp_path("parallel_reads");
172        let _c = Cleanup(path.clone());
173        // Each 256-byte page filled with its own page number, so a read
174        // that landed at the wrong offset is obvious from one byte.
175        let mut bytes = Vec::with_capacity(64 * 256);
176        for page in 0..64u8 {
177            bytes.extend(std::iter::repeat_n(page, 256));
178        }
179        std::fs::write(&path, &bytes).expect("write the image");
180
181        let dev = std::sync::Arc::new(FileDevice::open(&path).expect("open"));
182        let mut handles = Vec::new();
183        for page in 0..64u8 {
184            let dev = dev.clone();
185            handles.push(std::thread::spawn(move || {
186                // Several times each, so a thread that raced would have
187                // many chances to read somebody else's page.
188                for _ in 0..50 {
189                    let mut buf = [0u8; 256];
190                    dev.read_at(u64::from(page) * 256, &mut buf).expect("read");
191                    assert!(
192                        buf.iter().all(|b| *b == page),
193                        "page {page} came back holding another page's bytes"
194                    );
195                }
196            }));
197        }
198        for h in handles {
199            h.join().expect("a reader panicked");
200        }
201    }
202
203    use std::sync::atomic::{AtomicU64, Ordering};
204
205    /// Unique temp path under the system temp dir (no extra dev-deps).
206    fn temp_path(tag: &str) -> std::path::PathBuf {
207        static N: AtomicU64 = AtomicU64::new(0);
208        let n = N.fetch_add(1, Ordering::Relaxed);
209        let pid = std::process::id();
210        std::env::temp_dir().join(format!("fs_core_{tag}_{pid}_{n}.bin"))
211    }
212
213    struct Cleanup(std::path::PathBuf);
214    impl Drop for Cleanup {
215        fn drop(&mut self) {
216            let _ = std::fs::remove_file(&self.0);
217        }
218    }
219
220    #[test]
221    fn open_rw_round_trips_write_then_read() {
222        let path = temp_path("rw");
223        let _g = Cleanup(path.clone());
224        std::fs::write(&path, vec![0u8; 32]).unwrap();
225
226        let dev = FileDevice::open_rw(&path).unwrap();
227        assert!(dev.is_writable());
228        assert_eq!(dev.size_bytes(), 32);
229
230        dev.write_at(8, &[0xAA, 0xBB, 0xCC, 0xDD]).unwrap();
231        dev.flush().unwrap();
232
233        let mut buf = [0u8; 4];
234        dev.read_at(8, &mut buf).unwrap();
235        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
236    }
237
238    #[test]
239    fn open_rw_errors_on_missing_path() {
240        let path = temp_path("missing");
241        assert!(FileDevice::open_rw(&path).is_err());
242    }
243
244    #[test]
245    fn open_best_effort_uses_rw_when_writable() {
246        let path = temp_path("best_rw");
247        let _g = Cleanup(path.clone());
248        std::fs::write(&path, vec![0u8; 16]).unwrap();
249
250        let dev = FileDevice::open_best_effort(&path).unwrap();
251        assert!(dev.is_writable());
252        dev.write_at(0, &[0x11; 4]).unwrap();
253    }
254
255    #[test]
256    #[cfg(unix)]
257    fn open_best_effort_falls_back_to_read_only() {
258        use std::os::unix::fs::PermissionsExt;
259
260        let path = temp_path("best_ro");
261        let _g = Cleanup(path.clone());
262        std::fs::write(&path, vec![0xEFu8; 16]).unwrap();
263        // Read-only permissions force `open_rw` to fail; fall back to `open`.
264        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444)).unwrap();
265
266        let dev = FileDevice::open_best_effort(&path).unwrap();
267        assert!(!dev.is_writable());
268        // Writes are rejected at the read-only layer.
269        assert!(matches!(dev.write_at(0, &[0u8; 4]), Err(Error::ReadOnly)));
270        // Read still works.
271        let mut buf = [0u8; 4];
272        dev.read_at(0, &mut buf).unwrap();
273        assert_eq!(buf, [0xEF; 4]);
274        // Flush on a read-only device is a no-op success.
275        dev.flush().unwrap();
276    }
277}