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}