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::{Read, Seek, SeekFrom, Write};
8use std::path::Path;
9use std::sync::Mutex;
10
11pub struct FileDevice {
12    file: Mutex<File>,
13    size: u64,
14    writable: bool,
15}
16
17impl FileDevice {
18    /// Open read-only.
19    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
20        let file = File::open(path)?;
21        let size = file.metadata()?.len();
22        Ok(Self {
23            file: Mutex::new(file),
24            size,
25            writable: false,
26        })
27    }
28
29    /// Open read-write. Errors if the path is not writable.
30    pub fn open_rw<P: AsRef<Path>>(path: P) -> Result<Self> {
31        let file = OpenOptions::new().read(true).write(true).open(path)?;
32        let size = file.metadata()?.len();
33        Ok(Self {
34            file: Mutex::new(file),
35            size,
36            writable: true,
37        })
38    }
39
40    /// Open read-write if possible, fall back to read-only otherwise.
41    pub fn open_best_effort<P: AsRef<Path>>(path: P) -> Result<Self> {
42        let p = path.as_ref();
43        match Self::open_rw(p) {
44            Ok(d) => Ok(d),
45            Err(_) => Self::open(p),
46        }
47    }
48}
49
50impl BlockRead for FileDevice {
51    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
52        let mut f = self.file.lock().unwrap();
53        f.seek(SeekFrom::Start(offset))?;
54        let mut total = 0usize;
55        while total < buf.len() {
56            match f.read(&mut buf[total..])? {
57                0 => {
58                    return Err(Error::ShortRead {
59                        offset,
60                        want: buf.len(),
61                        got: total,
62                    });
63                }
64                n => total += n,
65            }
66        }
67        Ok(())
68    }
69
70    fn size_bytes(&self) -> u64 {
71        self.size
72    }
73}
74
75impl BlockDevice for FileDevice {
76    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
77        if !self.writable {
78            return Err(Error::ReadOnly);
79        }
80        let mut f = self.file.lock().unwrap();
81        f.seek(SeekFrom::Start(offset))?;
82        f.write_all(buf)?;
83        Ok(())
84    }
85
86    fn flush(&self) -> Result<()> {
87        if !self.writable {
88            return Ok(());
89        }
90        let mut f = self.file.lock().unwrap();
91        f.flush()?;
92        f.sync_data()?;
93        Ok(())
94    }
95
96    fn is_writable(&self) -> bool {
97        self.writable
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use std::sync::atomic::{AtomicU64, Ordering};
105
106    /// Unique temp path under the system temp dir (no extra dev-deps).
107    fn temp_path(tag: &str) -> std::path::PathBuf {
108        static N: AtomicU64 = AtomicU64::new(0);
109        let n = N.fetch_add(1, Ordering::Relaxed);
110        let pid = std::process::id();
111        std::env::temp_dir().join(format!("fs_core_{tag}_{pid}_{n}.bin"))
112    }
113
114    struct Cleanup(std::path::PathBuf);
115    impl Drop for Cleanup {
116        fn drop(&mut self) {
117            let _ = std::fs::remove_file(&self.0);
118        }
119    }
120
121    #[test]
122    fn open_rw_round_trips_write_then_read() {
123        let path = temp_path("rw");
124        let _g = Cleanup(path.clone());
125        std::fs::write(&path, vec![0u8; 32]).unwrap();
126
127        let dev = FileDevice::open_rw(&path).unwrap();
128        assert!(dev.is_writable());
129        assert_eq!(dev.size_bytes(), 32);
130
131        dev.write_at(8, &[0xAA, 0xBB, 0xCC, 0xDD]).unwrap();
132        dev.flush().unwrap();
133
134        let mut buf = [0u8; 4];
135        dev.read_at(8, &mut buf).unwrap();
136        assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]);
137    }
138
139    #[test]
140    fn open_rw_errors_on_missing_path() {
141        let path = temp_path("missing");
142        assert!(FileDevice::open_rw(&path).is_err());
143    }
144
145    #[test]
146    fn open_best_effort_uses_rw_when_writable() {
147        let path = temp_path("best_rw");
148        let _g = Cleanup(path.clone());
149        std::fs::write(&path, vec![0u8; 16]).unwrap();
150
151        let dev = FileDevice::open_best_effort(&path).unwrap();
152        assert!(dev.is_writable());
153        dev.write_at(0, &[0x11; 4]).unwrap();
154    }
155
156    #[test]
157    #[cfg(unix)]
158    fn open_best_effort_falls_back_to_read_only() {
159        use std::os::unix::fs::PermissionsExt;
160
161        let path = temp_path("best_ro");
162        let _g = Cleanup(path.clone());
163        std::fs::write(&path, vec![0xEFu8; 16]).unwrap();
164        // Read-only permissions force `open_rw` to fail; fall back to `open`.
165        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444)).unwrap();
166
167        let dev = FileDevice::open_best_effort(&path).unwrap();
168        assert!(!dev.is_writable());
169        // Writes are rejected at the read-only layer.
170        assert!(matches!(dev.write_at(0, &[0u8; 4]), Err(Error::ReadOnly)));
171        // Read still works.
172        let mut buf = [0u8; 4];
173        dev.read_at(0, &mut buf).unwrap();
174        assert_eq!(buf, [0xEF; 4]);
175        // Flush on a read-only device is a no-op success.
176        dev.flush().unwrap();
177    }
178}