Skip to main content

fs_ext4/
block_io.rs

1//! Abstract block-device I/O.
2//!
3//! The driver doesn't care if blocks come from a file, raw device, or a
4//! callback into Swift — it just needs `read_at(offset, buf) -> Result<()>`.
5//!
6//! `write_at` is an optional trait method: it defaults to returning
7//! `Error::Corrupt("read-only device")` so every existing read-only caller
8//! keeps working. `FileDevice` and the callback-with-writer device override
9//! it when the underlying resource allows writes.
10
11use crate::error::{Error, Result};
12use std::fs::{File, OpenOptions};
13use std::io::{Read, Seek, SeekFrom, Write};
14use std::path::Path;
15use std::sync::Mutex;
16
17/// Random-access block device. Reads required; writes optional.
18pub trait BlockDevice: Send + Sync {
19    /// Read exactly `buf.len()` bytes starting at `offset` (bytes from start of device).
20    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()>;
21
22    /// Total device size in bytes (for bounds-checking).
23    fn size_bytes(&self) -> u64;
24
25    /// Write exactly `buf.len()` bytes at `offset`. Default: returns an error
26    /// for read-only devices. Writable devices override this.
27    fn write_at(&self, _offset: u64, _buf: &[u8]) -> Result<()> {
28        Err(Error::Corrupt(
29            "block device is read-only (no write_at impl)",
30        ))
31    }
32
33    /// Flush any pending writes to stable storage. Default: no-op for
34    /// read-only devices; writable devices should implement fsync semantics.
35    fn flush(&self) -> Result<()> {
36        Ok(())
37    }
38
39    /// Reports whether `write_at` is likely to succeed. Used by the mount
40    /// path to decide whether journal replay is possible.
41    fn is_writable(&self) -> bool {
42        false
43    }
44
45    /// Buffer-cache hook: stash `bytes` for `block` so a subsequent
46    /// `read_at` returns those bytes instead of reading from physical
47    /// storage. Used by a read-only mount's journal replay to make
48    /// committed metadata visible to readers without writing it back to
49    /// the data area on disk.
50    ///
51    /// Pinned entries inserted via `populate_cache` MUST NOT be evicted
52    /// — they're the only place those bytes exist until `unpin_all`
53    /// runs (typically after journal replay). Devices without a cache
54    /// (raw `FileDevice`, etc.) implement this as a no-op and the
55    /// caller's bytes simply have no in-memory shadow; that's safe
56    /// because un-cached devices imply no separate journal log either.
57    fn populate_cache(&self, _block: u64, _bytes: Vec<u8>) {}
58
59    /// Buffer-cache hook: tell the device the journal has been
60    /// checkpointed, so any blocks pinned via `populate_cache` are now
61    /// consistent with disk and can be evicted under normal LRU
62    /// pressure. No-op for un-cached devices.
63    fn unpin_all(&self) {}
64
65    /// Discard clean read-cache entries. Pinned journal metadata must be
66    /// checkpointed and unpinned first; cached implementations reject otherwise.
67    /// This does not flush writes and requires exclusive filesystem ownership.
68    fn invalidate_cache(&self) -> Result<()> {
69        Ok(())
70    }
71}
72
73/// File-backed device — used for disk images and `/dev/diskN`.
74pub struct FileDevice {
75    file: Mutex<File>,
76    size: u64,
77    writable: bool,
78}
79
80impl FileDevice {
81    /// Open read-only. Matches pre-existing behaviour.
82    pub fn open(path: &str) -> Result<Self> {
83        Self::open_path(Path::new(path))
84    }
85
86    /// Open read-only by a path that need not be UTF-8: a device or image
87    /// name is bytes on Unix, and a lossy rendering of it can name another
88    /// file.
89    pub fn open_path(path: &Path) -> Result<Self> {
90        let file = File::open(path)?;
91        let size = file.metadata()?.len();
92        Ok(Self {
93            file: Mutex::new(file),
94            size,
95            writable: false,
96        })
97    }
98
99    /// Open read-write. Prefer this when the caller needs to journal-replay
100    /// or apply Phase 4 mutations. Falls back to an error if the path is
101    /// not writable.
102    pub fn open_rw(path: &str) -> Result<Self> {
103        Self::open_path_rw(Path::new(path))
104    }
105
106    /// Open read-write by a path that need not be UTF-8; see
107    /// [`FileDevice::open_path`].
108    pub fn open_path_rw(path: &Path) -> Result<Self> {
109        let file = OpenOptions::new().read(true).write(true).open(path)?;
110        let size = file.metadata()?.len();
111        Ok(Self {
112            file: Mutex::new(file),
113            size,
114            writable: true,
115        })
116    }
117
118    /// Open read-write if possible; otherwise fall back to read-only. Useful
119    /// for the mount path so read-only images on e.g. a locked volume still
120    /// mount, just without replay.
121    pub fn open_best_effort(path: &str) -> Result<Self> {
122        match Self::open_rw(path) {
123            Ok(d) => Ok(d),
124            Err(_) => Self::open(path),
125        }
126    }
127}
128
129impl BlockDevice for FileDevice {
130    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
131        let mut f = self.file.lock().unwrap();
132        f.seek(SeekFrom::Start(offset))?;
133        f.read_exact(buf)?;
134        Ok(())
135    }
136
137    fn size_bytes(&self) -> u64 {
138        self.size
139    }
140
141    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
142        if !self.writable {
143            return Err(Error::Corrupt("FileDevice opened read-only"));
144        }
145        let mut f = self.file.lock().unwrap();
146        f.seek(SeekFrom::Start(offset))?;
147        f.write_all(buf)?;
148        Ok(())
149    }
150
151    fn flush(&self) -> Result<()> {
152        if !self.writable {
153            return Ok(());
154        }
155        let mut f = self.file.lock().unwrap();
156        f.flush()?;
157        f.sync_data()?;
158        Ok(())
159    }
160
161    fn is_writable(&self) -> bool {
162        self.writable
163    }
164}
165
166/// Read callback: fill `buf` starting at byte `offset`.
167pub type ReadCb = Box<dyn Fn(u64, &mut [u8]) -> std::io::Result<()> + Send + Sync>;
168/// Write callback: write `buf` starting at byte `offset`.
169pub type WriteCb = Box<dyn Fn(u64, &[u8]) -> std::io::Result<()> + Send + Sync>;
170/// Flush callback.
171pub type FlushCb = Box<dyn Fn() -> std::io::Result<()> + Send + Sync>;
172
173/// Callback-backed device — used when the host process owns the fd
174/// (e.g. FSBlockDeviceResource via the C bridge). Optional write callback;
175/// set to `None` for read-only.
176pub struct CallbackDevice {
177    pub size: u64,
178    pub read: ReadCb,
179    pub write: Option<WriteCb>,
180    pub flush: Option<FlushCb>,
181}
182
183impl BlockDevice for CallbackDevice {
184    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
185        (self.read)(offset, buf)?;
186        Ok(())
187    }
188
189    fn size_bytes(&self) -> u64 {
190        self.size
191    }
192
193    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
194        match &self.write {
195            Some(f) => {
196                f(offset, buf)?;
197                Ok(())
198            }
199            None => Err(Error::Corrupt("CallbackDevice has no write callback")),
200        }
201    }
202
203    fn flush(&self) -> Result<()> {
204        match &self.flush {
205            Some(f) => {
206                f()?;
207                Ok(())
208            }
209            None => Ok(()),
210        }
211    }
212
213    fn is_writable(&self) -> bool {
214        self.write.is_some()
215    }
216}
217
218// ---------------------------------------------------------------------------
219// AlignedDevice — sector-aligned I/O over a device that requires it
220// ---------------------------------------------------------------------------
221
222/// Presents a byte-granular [`BlockDevice`] over one that accepts only
223/// requests whose offset and length are multiples of `sector` (#373).
224///
225/// The engine reads 1024 bytes at offset 1024 for the superblock, and its
226/// write path passes sub-block writes through; a host block resource that
227/// only takes sector-aligned I/O refuses both. Here:
228///
229/// - an aligned request passes straight through, with no copy;
230/// - an unaligned read reads the enclosing aligned span into a bounce
231///   buffer and copies the requested slice out;
232/// - an unaligned write reads the enclosing span's first and last sectors,
233///   patches the caller's bytes in, and writes the whole span back as one
234///   aligned request.
235///
236/// Every write holds one lock, so a read-modify-write can neither lose nor
237/// be lost to a concurrent write into the same sector.
238pub struct AlignedDevice<D: BlockDevice> {
239    inner: D,
240    sector: u64,
241    write_lock: Mutex<()>,
242}
243
244impl<D: BlockDevice> AlignedDevice<D> {
245    /// Wrap `inner`, whose requests must be multiples of `sector` bytes.
246    /// `sector` must be a power of two.
247    pub fn new(inner: D, sector: u32) -> Self {
248        assert!(
249            sector.is_power_of_two(),
250            "sector size {sector} is not a power of two"
251        );
252        AlignedDevice {
253            inner,
254            sector: sector as u64,
255            write_lock: Mutex::new(()),
256        }
257    }
258
259    fn is_aligned(&self, offset: u64, len: usize) -> bool {
260        let mask = self.sector - 1;
261        offset & mask == 0 && (len as u64) & mask == 0
262    }
263
264    /// The aligned span `[start, end)` enclosing `len` bytes at `offset`.
265    fn span(&self, offset: u64, len: usize) -> Result<(u64, u64)> {
266        let mask = self.sector - 1;
267        let end = offset
268            .checked_add(len as u64)
269            .and_then(|e| e.checked_add(mask))
270            .ok_or(Error::Corrupt("device request overflows u64"))?
271            & !mask;
272        if end > self.inner.size_bytes() {
273            return Err(Error::Corrupt("device request past the end of the device"));
274        }
275        Ok((offset & !mask, end))
276    }
277}
278
279impl<D: BlockDevice> BlockDevice for AlignedDevice<D> {
280    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
281        if self.is_aligned(offset, buf.len()) {
282            return self.inner.read_at(offset, buf);
283        }
284        let (start, end) = self.span(offset, buf.len())?;
285        let mut bounce = vec![0u8; (end - start) as usize];
286        self.inner.read_at(start, &mut bounce)?;
287        let head = (offset - start) as usize;
288        buf.copy_from_slice(&bounce[head..head + buf.len()]);
289        Ok(())
290    }
291
292    fn size_bytes(&self) -> u64 {
293        self.inner.size_bytes()
294    }
295
296    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
297        let _guard = self.write_lock.lock().unwrap_or_else(|p| p.into_inner());
298        if self.is_aligned(offset, buf.len()) {
299            return self.inner.write_at(offset, buf);
300        }
301        let (start, end) = self.span(offset, buf.len())?;
302        let sector = self.sector as usize;
303        let mut bounce = vec![0u8; (end - start) as usize];
304        let head = (offset - start) as usize;
305        let tail = head + buf.len();
306        // Only the first and last sectors hold bytes the caller is not
307        // replacing; every sector between them is overwritten whole.
308        if head != 0 {
309            self.inner.read_at(start, &mut bounce[..sector])?;
310        }
311        let last = bounce.len() - sector;
312        if !tail.is_multiple_of(sector) && (last != 0 || head == 0) {
313            self.inner
314                .read_at(start + last as u64, &mut bounce[last..])?;
315        }
316        bounce[head..tail].copy_from_slice(buf);
317        self.inner.write_at(start, &bounce)
318    }
319
320    fn flush(&self) -> Result<()> {
321        self.inner.flush()
322    }
323
324    fn is_writable(&self) -> bool {
325        self.inner.is_writable()
326    }
327
328    fn populate_cache(&self, block: u64, bytes: Vec<u8>) {
329        self.inner.populate_cache(block, bytes)
330    }
331
332    fn unpin_all(&self) {
333        self.inner.unpin_all()
334    }
335
336    fn invalidate_cache(&self) -> Result<()> {
337        self.inner.invalidate_cache()
338    }
339}
340
341// ---------------------------------------------------------------------------
342// CachingDevice — small LRU block cache decorator
343// ---------------------------------------------------------------------------
344
345#[cfg(test)]
346mod aligned_device_tests {
347    use super::*;
348
349    const SECTOR: u64 = 512;
350
351    /// An in-memory device that refuses any request not aligned to SECTOR.
352    struct Strict(Mutex<Vec<u8>>);
353
354    impl Strict {
355        fn check(offset: u64, len: usize) -> Result<()> {
356            if !offset.is_multiple_of(SECTOR) || !(len as u64).is_multiple_of(SECTOR) {
357                return Err(Error::Corrupt("unaligned request"));
358            }
359            Ok(())
360        }
361    }
362
363    impl BlockDevice for Strict {
364        fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
365            Self::check(offset, buf.len())?;
366            let b = self.0.lock().unwrap();
367            buf.copy_from_slice(&b[offset as usize..offset as usize + buf.len()]);
368            Ok(())
369        }
370        fn size_bytes(&self) -> u64 {
371            self.0.lock().unwrap().len() as u64
372        }
373        fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
374            Self::check(offset, buf.len())?;
375            let mut b = self.0.lock().unwrap();
376            b[offset as usize..offset as usize + buf.len()].copy_from_slice(buf);
377            Ok(())
378        }
379        fn is_writable(&self) -> bool {
380            true
381        }
382    }
383
384    fn pattern(len: usize) -> Vec<u8> {
385        (0..len).map(|i| (i * 7 + 3) as u8).collect()
386    }
387
388    /// Every (offset, length) shape against a 4-sector device: inside one
389    /// sector, touching either edge, straddling one boundary, spanning
390    /// whole sectors between two partial ones, and fully aligned.
391    #[test]
392    fn unaligned_reads_and_writes_match_a_byte_granular_model() {
393        let size = 4 * SECTOR as usize;
394        let cases: &[(u64, usize)] = &[
395            (0, 1),
396            (1, 10),
397            (500, 12),
398            (511, 1),
399            (510, 4),
400            (0, 512),
401            (512, 1024),
402            (100, 1500),
403            (1, 2046),
404            (1024, 1),
405            (2047, 1),
406            (0, 2048),
407            (3, 0),
408        ];
409        for &(off, len) in cases {
410            let base = pattern(size);
411            let dev = AlignedDevice::new(Strict(Mutex::new(base.clone())), SECTOR as u32);
412            let data: Vec<u8> = (0..len).map(|i| !(i as u8)).collect();
413            dev.write_at(off, &data)
414                .unwrap_or_else(|e| panic!("write {len}@{off}: {e:?}"));
415            let mut model = base;
416            model[off as usize..off as usize + len].copy_from_slice(&data);
417            assert!(
418                *dev.inner.0.lock().unwrap() == model,
419                "write {len}@{off} disturbed bytes outside it"
420            );
421            let mut back = vec![0u8; len];
422            dev.read_at(off, &mut back)
423                .unwrap_or_else(|e| panic!("read {len}@{off}: {e:?}"));
424            assert_eq!(back, data, "read {len}@{off}");
425        }
426    }
427
428    #[test]
429    fn a_request_past_the_end_is_refused_not_wrapped() {
430        let dev = AlignedDevice::new(Strict(Mutex::new(vec![0; 1024])), SECTOR as u32);
431        let mut buf = [0u8; 4];
432        assert!(dev.read_at(1022, &mut buf).is_err());
433        assert!(dev.write_at(1022, &buf).is_err());
434        assert!(dev.read_at(u64::MAX - 1, &mut buf).is_err());
435    }
436}