1use 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 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 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 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 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 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 assert!(matches!(dev.write_at(0, &[0u8; 4]), Err(Error::ReadOnly)));
171 let mut buf = [0u8; 4];
173 dev.read_at(0, &mut buf).unwrap();
174 assert_eq!(buf, [0xEF; 4]);
175 dev.flush().unwrap();
177 }
178}