Skip to main content

fs_core/
slice.rs

1//! Slice adapters — view a byte sub-range of any `BlockRead` as its own
2//! device. Useful any time you want to feed a fragment of a larger
3//! device to a consumer that expects a whole block source — partition
4//! probes, image-file extents, mmap-style views, fuzzer harnesses.
5//!
6//! Three variants:
7//!
8//! - [`SliceReader`] borrows the parent, lifetime-tied. Cheaper when the
9//!   parent outlives the slice and you can express that statically.
10//! - [`OwnedSlice`] holds an `Arc` to the parent. Use when the parent's
11//!   lifetime can't be expressed in a borrow (FFI handles, slice handed
12//!   across thread boundaries, etc.).
13//! - [`OwnedRwSlice`] holds an `Arc<dyn BlockDevice>` and propagates
14//!   writes to the parent.
15//!
16//! The first two are strictly read-only: the default `Err(ReadOnly)`
17//! write path from [`BlockDevice`] applies.
18//!
19//! # Which error an out-of-range request gets
20//!
21//! All three share one range check — `SliceGeometry::rebase` — and it
22//! answers in two different currencies depending on the direction of the
23//! request:
24//!
25//! | request outside `[0, length)` | error |
26//! |---|---|
27//! | read  | [`Error::ShortRead`] with `got: 0` |
28//! | write | [`Error::OutOfBounds`] |
29//!
30//! The asymmetry is deliberate. A slice exists to be substitutable for a
31//! real device of size `length`, and a real device — [`FileDevice`] —
32//! answers a read that begins at or past its end with exactly
33//! `ShortRead { offset, want, got: 0 }`. A slice that answered
34//! `OutOfBounds` would be distinguishable from the thing it stands in
35//! for, and every caller that already handles end-of-device would need a
36//! second arm to cope with slices. Writes have no partial-write variant
37//! to stay consistent with, and a caller that overran a write needs the
38//! device size in order to clamp and retry — which is what
39//! [`Error::OutOfBounds`] carries and [`Error::ShortRead`] does not.
40//!
41//! The match is on the variant, not on `got`. A slice refuses an
42//! out-of-range read before it touches the parent, so it reports `got: 0`
43//! and leaves the buffer untouched — including for a read that begins
44//! inside the slice and runs off its end, where [`FileDevice`] would have
45//! copied the readable prefix and reported its length. `got` counts bytes
46//! actually delivered, and a slice delivers none.
47//!
48//! This governs the slice's own range only. A request that *is* inside
49//! `[0, length)` is forwarded to the parent, and whatever the parent says
50//! about it — including [`Error::OutOfBounds`] from a container reader
51//! that knows its virtual size — comes back unchanged.
52//!
53//! [`FileDevice`]: crate::FileDevice
54
55use crate::block::{BlockDevice, BlockRead};
56use crate::error::{Error, Result};
57use std::sync::Arc;
58
59/// Where a slice sits on its parent, and the one bounds rule the three
60/// slice types share.
61///
62/// The public slice types differ only in how they hold the parent and
63/// whether writes propagate. The geometry, the range check and the choice
64/// of error are identical across all of them, so they live here — one
65/// definition to read, one place to change.
66#[derive(Clone, Copy)]
67struct SliceGeometry {
68    start: u64,
69    length: u64,
70}
71
72impl SliceGeometry {
73    fn new(start: u64, length: u64) -> Self {
74        Self { start, length }
75    }
76
77    /// Parent offset corresponding to `offset`, or `None` when
78    /// `[offset, offset + len)` is not wholly inside `[0, length)`, or
79    /// when the rebased offset would not fit on the parent at all.
80    ///
81    /// Every one of these additions is checked, `start + offset`
82    /// included. That one used to be deliberate, on the argument that a
83    /// slice built with a nonsense `start` would "overflow here rather
84    /// than quietly reading some other part of the parent" -- which
85    /// holds only while `overflow-checks` is on, and it is off in the
86    /// release profile these crates ship. In release the addition
87    /// wrapped, and the wrap did precisely the thing the argument said
88    /// it avoided: a slice starting at 2^63 and 5000 bytes long
89    /// returned `Ok` and the parent's bytes from offset 5000.
90    ///
91    /// A slice's geometry comes from a partition table, which comes off
92    /// the disk, so "a nonsense start" is an ordinary thing to be
93    /// handed rather than a programming mistake.
94    fn rebase(&self, offset: u64, len: u64) -> Option<u64> {
95        let end = offset.checked_add(len)?;
96        if end > self.length {
97            return None;
98        }
99        self.start.checked_add(offset)
100    }
101
102    /// Bounds-check a read and rebase it onto the parent.
103    ///
104    /// Out of range is [`Error::ShortRead`] with `got: 0` — the same
105    /// answer a real device of size `length` gives for a read beginning
106    /// at or past its end. See the module docs for why.
107    fn rebase_read(&self, offset: u64, len: usize) -> Result<u64> {
108        self.rebase(offset, len as u64).ok_or(Error::ShortRead {
109            offset,
110            want: len,
111            got: 0,
112        })
113    }
114
115    /// Bounds-check a write and rebase it onto the parent.
116    ///
117    /// Out of range is [`Error::OutOfBounds`]: nothing was written, and
118    /// the caller is handed the slice's size so it can clamp and retry.
119    fn rebase_write(&self, offset: u64, len: usize) -> Result<u64> {
120        self.rebase(offset, len as u64).ok_or(Error::OutOfBounds {
121            offset,
122            len: len as u64,
123            size: self.length,
124        })
125    }
126}
127
128/// Borrowed slice of a parent `BlockRead`.
129///
130/// `read_at(0, …)` reads `start` of the parent. Reads outside
131/// `[0, length)` return [`Error::ShortRead`] with `got: 0`.
132pub struct SliceReader<'a> {
133    parent: &'a (dyn BlockRead + 'a),
134    geom: SliceGeometry,
135}
136
137impl<'a> SliceReader<'a> {
138    pub fn new(parent: &'a (dyn BlockRead + 'a), start: u64, length: u64) -> Self {
139        Self {
140            parent,
141            geom: SliceGeometry::new(start, length),
142        }
143    }
144
145    /// Byte offset of this slice on the parent device.
146    pub fn start(&self) -> u64 {
147        self.geom.start
148    }
149
150    /// Length of this slice in bytes (== `size_bytes()`).
151    pub fn length(&self) -> u64 {
152        self.geom.length
153    }
154}
155
156impl<'a> BlockRead for SliceReader<'a> {
157    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
158        let at = self.geom.rebase_read(offset, buf.len())?;
159        self.parent.read_at(at, buf)
160    }
161
162    fn size_bytes(&self) -> u64 {
163        self.geom.length
164    }
165}
166
167/// Slices are read-only by default — even where the parent is writable,
168/// slicing is almost always paired with a read-only inspection or
169/// dispatch workflow.
170impl<'a> BlockDevice for SliceReader<'a> {}
171
172/// Owned slice over an `Arc<dyn BlockRead>`. Use when the parent's
173/// lifetime can't be expressed in a borrow — e.g. when the slice is
174/// handed across an FFI boundary or stored in a long-lived struct.
175///
176/// Reads outside `[0, length)` return [`Error::ShortRead`] with `got: 0`.
177pub struct OwnedSlice {
178    parent: Arc<dyn BlockRead>,
179    geom: SliceGeometry,
180}
181
182impl OwnedSlice {
183    pub fn new(parent: Arc<dyn BlockRead>, start: u64, length: u64) -> Self {
184        Self {
185            parent,
186            geom: SliceGeometry::new(start, length),
187        }
188    }
189
190    /// Byte offset of this slice on the parent device.
191    pub fn start(&self) -> u64 {
192        self.geom.start
193    }
194
195    /// Length of this slice in bytes (== `size_bytes()`).
196    pub fn length(&self) -> u64 {
197        self.geom.length
198    }
199}
200
201impl BlockRead for OwnedSlice {
202    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
203        let at = self.geom.rebase_read(offset, buf.len())?;
204        self.parent.read_at(at, buf)
205    }
206
207    fn size_bytes(&self) -> u64 {
208        self.geom.length
209    }
210}
211
212/// Same rationale as `SliceReader`: read-only by default.
213impl BlockDevice for OwnedSlice {}
214
215/// Owned, read-WRITE slice over an `Arc<dyn BlockDevice>`. Use when the
216/// parent is writable and the slice should propagate writes (e.g. an
217/// individual partition handed to a filesystem driver).
218///
219/// Reads outside `[0, length)` return [`Error::ShortRead`] with `got: 0`;
220/// writes outside it return [`Error::OutOfBounds`]. The two directions
221/// differ on purpose — see the module docs.
222pub struct OwnedRwSlice {
223    parent: Arc<dyn BlockDevice>,
224    geom: SliceGeometry,
225}
226
227impl OwnedRwSlice {
228    pub fn new(parent: Arc<dyn BlockDevice>, start: u64, length: u64) -> Self {
229        Self {
230            parent,
231            geom: SliceGeometry::new(start, length),
232        }
233    }
234
235    /// Byte offset of this slice on the parent device.
236    pub fn start(&self) -> u64 {
237        self.geom.start
238    }
239
240    /// Length of this slice in bytes (== `size_bytes()`).
241    pub fn length(&self) -> u64 {
242        self.geom.length
243    }
244}
245
246impl BlockRead for OwnedRwSlice {
247    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<()> {
248        let at = self.geom.rebase_read(offset, buf.len())?;
249        self.parent.read_at(at, buf)
250    }
251
252    fn size_bytes(&self) -> u64 {
253        self.geom.length
254    }
255}
256
257impl BlockDevice for OwnedRwSlice {
258    /// Range first, writability second: a write that is both out of range
259    /// and aimed at a read-only parent reports [`Error::OutOfBounds`],
260    /// not [`Error::ReadOnly`]. The range is a property of this slice and
261    /// is knowable without asking the parent anything, so it is the more
262    /// specific of the two answers.
263    fn write_at(&self, offset: u64, buf: &[u8]) -> Result<()> {
264        let at = self.geom.rebase_write(offset, buf.len())?;
265        if !self.parent.is_writable() {
266            return Err(Error::ReadOnly);
267        }
268        self.parent.write_at(at, buf)
269    }
270
271    fn flush(&self) -> Result<()> {
272        self.parent.flush()
273    }
274
275    fn is_writable(&self) -> bool {
276        self.parent.is_writable()
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::test_device::{Bytes, RwBytes};
284    use std::sync::Mutex;
285
286    #[test]
287    fn slice_reader_rebases_offsets() {
288        let mut v = vec![0u8; 4096];
289        v[2000..2004].copy_from_slice(&[0xAB, 0xCD, 0xEF, 0x01]);
290        let dev = Bytes(Mutex::new(v));
291
292        let slice = SliceReader::new(&dev, 2000, 4);
293        assert_eq!(slice.size_bytes(), 4);
294        assert_eq!(slice.start(), 2000);
295        assert_eq!(slice.length(), 4);
296
297        let mut buf = [0u8; 4];
298        slice.read_at(0, &mut buf).unwrap();
299        assert_eq!(buf, [0xAB, 0xCD, 0xEF, 0x01]);
300    }
301
302    /// A slice's geometry comes from a partition table, and a partition
303    /// table comes off the disk. A start and a length that add up past
304    /// 2^64 are an ordinary thing to be handed.
305    ///
306    /// In a release build the rebasing addition wrapped, so a read at an
307    /// offset inside the slice's declared length landed somewhere else
308    /// on the parent entirely -- and came back `Ok`, with those bytes,
309    /// as though they were the slice's own.
310    #[test]
311    fn a_slice_whose_start_plus_offset_leaves_the_parent_reads_nothing() {
312        let mut v = vec![0u8; 64 * 1024];
313        v[5000..5008].copy_from_slice(b"SECRET!!");
314        let dev: Arc<dyn BlockRead> = Arc::new(Bytes(Mutex::new(v)));
315
316        // A GPT entry of starting_lba = 2^54 and ending_lba = 2^55 + 99
317        // produces exactly this.
318        let slice = OwnedSlice::new(dev, 1 << 63, (1 << 63) + 51200);
319        let mut buf = [0u8; 8];
320        let inside_the_declared_length = (1u64 << 63) + 5000;
321
322        let outcome = slice.read_at(inside_the_declared_length, &mut buf);
323        assert!(
324            outcome.is_err(),
325            "the read succeeded and returned {:?}, which is the parent's \
326             bytes from offset 5000",
327            std::str::from_utf8(&buf)
328        );
329        assert_ne!(&buf, b"SECRET!!");
330    }
331
332    #[test]
333    fn slice_reader_rejects_out_of_bounds() {
334        let dev = Bytes(Mutex::new(vec![0u8; 4096]));
335        let slice = SliceReader::new(&dev, 0, 16);
336        let mut buf = [0u8; 8];
337        match slice.read_at(12, &mut buf) {
338            Err(Error::ShortRead { .. }) => {}
339            other => panic!("expected ShortRead, got {other:?}"),
340        }
341    }
342
343    #[test]
344    fn owned_slice_works_through_arc() {
345        let mut v = vec![0u8; 4096];
346        v[100..104].copy_from_slice(&[0x11, 0x22, 0x33, 0x44]);
347        let dev: Arc<dyn BlockRead> = Arc::new(Bytes(Mutex::new(v)));
348
349        let slice = OwnedSlice::new(dev, 100, 4);
350        assert_eq!(slice.size_bytes(), 4);
351        let mut buf = [0u8; 4];
352        slice.read_at(0, &mut buf).unwrap();
353        assert_eq!(buf, [0x11, 0x22, 0x33, 0x44]);
354    }
355
356    #[test]
357    fn slices_reject_writes_via_blockdevice_default() {
358        let dev = Bytes(Mutex::new(vec![0u8; 16]));
359        let slice = SliceReader::new(&dev, 0, 8);
360        let err = BlockDevice::write_at(&slice, 0, &[1u8; 4]).unwrap_err();
361        assert!(matches!(err, Error::ReadOnly));
362    }
363
364    #[test]
365    fn owned_slice_accessors_report_geometry() {
366        let dev: Arc<dyn BlockRead> = Arc::new(Bytes(Mutex::new(vec![0u8; 4096])));
367        let slice = OwnedSlice::new(dev, 512, 256);
368        assert_eq!(slice.start(), 512);
369        assert_eq!(slice.length(), 256);
370        assert_eq!(slice.size_bytes(), 256);
371    }
372
373    #[test]
374    fn owned_rw_slice_accessors_report_geometry() {
375        let dev: Arc<dyn BlockDevice> = Arc::new(RwBytes(Mutex::new(vec![0u8; 64])));
376        let slice = OwnedRwSlice::new(dev, 16, 32);
377        assert_eq!(slice.start(), 16);
378        assert_eq!(slice.length(), 32);
379        assert_eq!(slice.size_bytes(), 32);
380        assert!(slice.is_writable());
381    }
382
383    #[test]
384    fn owned_rw_slice_rebases_reads_and_writes() {
385        let dev: Arc<dyn BlockDevice> = Arc::new(RwBytes(Mutex::new(vec![0u8; 64])));
386        let slice = OwnedRwSlice::new(dev.clone(), 16, 32);
387
388        // Write through the slice lands at parent offset 16.
389        slice.write_at(0, &[0xDE, 0xAD, 0xBE, 0xEF]).unwrap();
390        let mut buf = [0u8; 4];
391        slice.read_at(0, &mut buf).unwrap();
392        assert_eq!(buf, [0xDE, 0xAD, 0xBE, 0xEF]);
393
394        // Confirm rebasing against the parent directly.
395        let mut pbuf = [0u8; 4];
396        dev.read_at(16, &mut pbuf).unwrap();
397        assert_eq!(pbuf, [0xDE, 0xAD, 0xBE, 0xEF]);
398    }
399
400    #[test]
401    fn owned_rw_slice_rejects_out_of_bounds_write() {
402        let dev: Arc<dyn BlockDevice> = Arc::new(RwBytes(Mutex::new(vec![0u8; 64])));
403        let slice = OwnedRwSlice::new(dev, 0, 8);
404        match slice.write_at(6, &[0u8; 4]) {
405            Err(Error::OutOfBounds { .. }) => {}
406            other => panic!("expected OutOfBounds, got {other:?}"),
407        }
408    }
409
410    /// The bounds rule is direction-dependent by design: one slice, one
411    /// out-of-range span, two different errors. Pinned here so the
412    /// asymmetry cannot be "tidied up" into consistency without someone
413    /// deciding to — the reasoning is in the module docs.
414    #[test]
415    fn same_out_of_range_span_is_short_read_for_a_read_and_out_of_bounds_for_a_write() {
416        let dev: Arc<dyn BlockDevice> = Arc::new(RwBytes(Mutex::new(vec![0u8; 64])));
417        let slice = OwnedRwSlice::new(dev, 16, 8);
418
419        let mut buf = [0u8; 4];
420        match slice.read_at(6, &mut buf) {
421            Err(Error::ShortRead { offset, want, got }) => {
422                assert_eq!((offset, want, got), (6, 4, 0));
423            }
424            other => panic!("expected ShortRead, got {other:?}"),
425        }
426
427        match slice.write_at(6, &[0u8; 4]) {
428            Err(Error::OutOfBounds { offset, len, size }) => {
429                assert_eq!((offset, len, size), (6, 4, 8));
430            }
431            other => panic!("expected OutOfBounds, got {other:?}"),
432        }
433    }
434
435    #[test]
436    fn owned_rw_slice_flush_delegates_to_parent() {
437        let dev: Arc<dyn BlockDevice> = Arc::new(RwBytes(Mutex::new(vec![0u8; 8])));
438        let slice = OwnedRwSlice::new(dev, 0, 8);
439        // Default `flush` on RwBytes is a no-op success; the slice forwards it.
440        slice.flush().unwrap();
441    }
442}