Skip to main content

fs_ext4/
fs_core_bridge.rs

1//! Bidirectional bridge between ext4's local [`BlockDevice`] trait and the
2//! shared [`fs_core::BlockDevice`] trait.
3//!
4//! Two directions:
5//!
6//! 1. **Outbound** — ext4's [`FileDevice`] / [`CallbackDevice`] also
7//!    implement [`fs_core::BlockDevice`], so anything that takes a
8//!    `&dyn fs_core::BlockDevice` (a partition probe, a slice adapter)
9//!    can drive an ext4-flavoured device directly.
10//!
11//! 2. **Inbound** — [`CoreDevice<T>`] wraps any `fs_core::BlockDevice` and
12//!    presents it as ext4's local `BlockDevice`, with the journal-cache
13//!    hooks (`populate_cache` / `unpin_all`) defaulting to no-ops. This is
14//!    how an external image reader (a [`Qcow2Reader`][qcow]), or a
15//!    partition slice, is fed into ext4's mount path.
16//!
17//! Strictly additive — does not touch the existing [`BlockDevice`] trait
18//! or any of its implementors. Risk-bounded: removing `pub mod
19//! fs_core_bridge;` from `lib.rs` reverts the entire change.
20//!
21//! [qcow]: https://crates.io/crates/rust-img-qcow2
22
23use crate::block_io::{BlockDevice, CallbackDevice, FileDevice};
24use crate::error::Error;
25
26/// Lift an ext4 error into the unified `fs_core::Error` shape so trait
27/// methods can return the framework's error type. Lossy on variants
28/// without a direct counterpart — those flatten to `Custom(String)`.
29///
30/// `Error::OutOfBounds` deliberately flattens to `Custom` rather than
31/// `fs_core::Error::OutOfBounds { offset, len, size }`: ext4's error type
32/// doesn't carry the offset/len/size context, so synthesising zeros here
33/// would silently mislead consumers pattern-matching on those fields.
34fn ext4_to_fs_core_error(e: Error) -> fs_core::Error {
35    match e {
36        Error::Io(io) => fs_core::Error::Io(io),
37        Error::ReadOnly => fs_core::Error::ReadOnly,
38        other => fs_core::Error::Custom(format!("{other:?}")),
39    }
40}
41
42/// Drop a `fs_core::Error` back into ext4's error type for the inbound
43/// adapter path. `Custom` and `OutOfBounds` flatten to a synthetic
44/// `io::Error` so ext4's existing `From<io::Error>` lifts them naturally.
45fn fs_core_to_ext4_error(e: fs_core::Error) -> Error {
46    match e {
47        fs_core::Error::Io(io) => Error::Io(io),
48        fs_core::Error::ReadOnly => Error::ReadOnly,
49        fs_core::Error::OutOfBounds { .. } => Error::OutOfBounds,
50        fs_core::Error::ShortRead { offset, want, got } => Error::Io(std::io::Error::other(
51            format!("short read at {offset}: wanted {want} got {got}"),
52        )),
53        fs_core::Error::Custom(s) => Error::Io(std::io::Error::other(s)),
54    }
55}
56
57// ---------------------------------------------------------------------------
58// Outbound: ext4's adapters also satisfy fs_core::BlockDevice.
59// ---------------------------------------------------------------------------
60
61impl fs_core::BlockRead for FileDevice {
62    fn read_at(&self, offset: u64, buf: &mut [u8]) -> fs_core::Result<()> {
63        BlockDevice::read_at(self, offset, buf).map_err(ext4_to_fs_core_error)
64    }
65    fn size_bytes(&self) -> u64 {
66        BlockDevice::size_bytes(self)
67    }
68}
69
70impl fs_core::BlockDevice for FileDevice {
71    fn write_at(&self, offset: u64, buf: &[u8]) -> fs_core::Result<()> {
72        // Short-circuit on the writable bit so RO devices surface
73        // `fs_core::Error::ReadOnly` consistently. The inner write_at
74        // would otherwise return `Error::Corrupt("FileDevice opened
75        // read-only")`, which our error map flattens to `Custom`,
76        // breaking the `is_writable()` / `write_at` contract.
77        if !BlockDevice::is_writable(self) {
78            return Err(fs_core::Error::ReadOnly);
79        }
80        BlockDevice::write_at(self, offset, buf).map_err(ext4_to_fs_core_error)
81    }
82    fn flush(&self) -> fs_core::Result<()> {
83        BlockDevice::flush(self).map_err(ext4_to_fs_core_error)
84    }
85    fn is_writable(&self) -> bool {
86        BlockDevice::is_writable(self)
87    }
88}
89
90impl fs_core::BlockRead for CallbackDevice {
91    fn read_at(&self, offset: u64, buf: &mut [u8]) -> fs_core::Result<()> {
92        BlockDevice::read_at(self, offset, buf).map_err(ext4_to_fs_core_error)
93    }
94    fn size_bytes(&self) -> u64 {
95        BlockDevice::size_bytes(self)
96    }
97}
98
99impl fs_core::BlockDevice for CallbackDevice {
100    fn write_at(&self, offset: u64, buf: &[u8]) -> fs_core::Result<()> {
101        // Same RO short-circuit as the FileDevice impl above — without
102        // it, callback devices configured with `write: None` surface
103        // their `Error::Corrupt("CallbackDevice has no write
104        // callback")` as `fs_core::Error::Custom`, which breaks
105        // `is_writable()` / `write_at` consistency.
106        if !BlockDevice::is_writable(self) {
107            return Err(fs_core::Error::ReadOnly);
108        }
109        BlockDevice::write_at(self, offset, buf).map_err(ext4_to_fs_core_error)
110    }
111    fn flush(&self) -> fs_core::Result<()> {
112        BlockDevice::flush(self).map_err(ext4_to_fs_core_error)
113    }
114    fn is_writable(&self) -> bool {
115        BlockDevice::is_writable(self)
116    }
117}
118
119// ---------------------------------------------------------------------------
120// Inbound: any fs_core::BlockDevice can drive ext4 via this wrapper.
121// ---------------------------------------------------------------------------
122
123/// Wraps any [`fs_core::BlockDevice`] and presents it as an ext4
124/// [`BlockDevice`]. Journal-cache hooks default to no-ops (the underlying
125/// device has no notion of "pinned" pages), which is the same as ext4's
126/// behaviour against a raw `FileDevice` — safe because un-cached devices
127/// imply no separate journal log either.
128pub struct CoreDevice<T: fs_core::BlockDevice> {
129    inner: T,
130}
131
132impl<T: fs_core::BlockDevice> CoreDevice<T> {
133    pub fn new(inner: T) -> Self {
134        Self { inner }
135    }
136
137    /// Borrow the wrapped device, e.g. for diagnostics.
138    pub fn inner(&self) -> &T {
139        &self.inner
140    }
141
142    /// Consume the wrapper and return the inner device.
143    pub fn into_inner(self) -> T {
144        self.inner
145    }
146}
147
148impl<T: fs_core::BlockDevice> BlockDevice for CoreDevice<T> {
149    fn read_at(&self, offset: u64, buf: &mut [u8]) -> crate::error::Result<()> {
150        fs_core::BlockRead::read_at(&self.inner, offset, buf).map_err(fs_core_to_ext4_error)
151    }
152    fn size_bytes(&self) -> u64 {
153        fs_core::BlockRead::size_bytes(&self.inner)
154    }
155    fn write_at(&self, offset: u64, buf: &[u8]) -> crate::error::Result<()> {
156        fs_core::BlockDevice::write_at(&self.inner, offset, buf).map_err(fs_core_to_ext4_error)
157    }
158    fn flush(&self) -> crate::error::Result<()> {
159        fs_core::BlockDevice::flush(&self.inner).map_err(fs_core_to_ext4_error)
160    }
161    fn is_writable(&self) -> bool {
162        fs_core::BlockDevice::is_writable(&self.inner)
163    }
164    // populate_cache / unpin_all use the trait's default no-op impls — see
165    // module-level docs for why that's safe for un-journaled inbound devices.
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::sync::Mutex;
172
173    /// Trivial in-memory `fs_core::BlockDevice` for testing the inbound
174    /// adapter without dragging in a real qcow2 fixture.
175    struct InMemoryFsCore(Mutex<Vec<u8>>);
176
177    impl fs_core::BlockRead for InMemoryFsCore {
178        fn read_at(&self, offset: u64, buf: &mut [u8]) -> fs_core::Result<()> {
179            let b = self.0.lock().unwrap();
180            let start = offset as usize;
181            let end = start + buf.len();
182            if end > b.len() {
183                return Err(fs_core::Error::ShortRead {
184                    offset,
185                    want: buf.len(),
186                    got: b.len().saturating_sub(start),
187                });
188            }
189            buf.copy_from_slice(&b[start..end]);
190            Ok(())
191        }
192        fn size_bytes(&self) -> u64 {
193            self.0.lock().unwrap().len() as u64
194        }
195    }
196
197    impl fs_core::BlockDevice for InMemoryFsCore {
198        fn write_at(&self, offset: u64, buf: &[u8]) -> fs_core::Result<()> {
199            let mut b = self.0.lock().unwrap();
200            let start = offset as usize;
201            let end = start + buf.len();
202            // Honest BlockDevice impl: out-of-range writes return
203            // `fs_core::Error::OutOfBounds` rather than panicking from
204            // a slice copy_from_slice. Without this guard, future
205            // tests that exercise the error path would panic instead.
206            if end > b.len() {
207                return Err(fs_core::Error::OutOfBounds {
208                    offset,
209                    len: buf.len() as u64,
210                    size: b.len() as u64,
211                });
212            }
213            b[start..end].copy_from_slice(buf);
214            Ok(())
215        }
216        fn is_writable(&self) -> bool {
217            true
218        }
219    }
220
221    #[test]
222    fn core_device_round_trip() {
223        let mem = InMemoryFsCore(Mutex::new(vec![0u8; 4096]));
224        let dev = CoreDevice::new(mem);
225        assert_eq!(BlockDevice::size_bytes(&dev), 4096);
226
227        // Inbound write-then-read through the ext4 trait.
228        BlockDevice::write_at(&dev, 100, &[0x11, 0x22, 0x33, 0x44]).unwrap();
229        let mut buf = [0u8; 4];
230        BlockDevice::read_at(&dev, 100, &mut buf).unwrap();
231        assert_eq!(buf, [0x11, 0x22, 0x33, 0x44]);
232    }
233
234    #[test]
235    fn core_device_propagates_short_read_as_oob() {
236        let mem = InMemoryFsCore(Mutex::new(vec![0u8; 64]));
237        let dev = CoreDevice::new(mem);
238        let mut buf = [0u8; 16];
239        // Read past the end -> fs_core::Error::ShortRead -> ext4::Error::Io
240        // (we surface short reads as io errors, not OutOfBounds, because
241        // the offset/len/size context is lost in translation).
242        let err = BlockDevice::read_at(&dev, 60, &mut buf).unwrap_err();
243        assert!(matches!(err, Error::Io(_)), "got: {err:?}");
244    }
245}