Skip to main content

forensic_vfs/
adapters.rs

1//! Concrete [`crate::ImageSource`] adapters: a positioned-read OS file, a byte
2//! sub-range of a parent source, and a legacy `Read + Seek` cursor view.
3
4use std::fs::File;
5use std::io::{self, Read, Seek, SeekFrom};
6use std::path::Path;
7use std::sync::Mutex;
8
9use crate::error::{io_err, VfsResult};
10use crate::source::{DynSource, ImageSource, SourceId};
11
12/// A byte window `[base, base+len)` of a parent source, itself an
13/// [`ImageSource`]. How a partition, VSS store, embedded image, or decrypted
14/// volume is addressed. `len` is clamped to the parent's bounds at construction,
15/// so a `read_at` can never escape the window.
16pub struct SubRange {
17    parent: DynSource,
18    base: u64,
19    len: u64,
20}
21
22impl SubRange {
23    /// A window starting at `base` in `parent`, at most `len` bytes, clamped to
24    /// whatever the parent actually has from `base`.
25    #[must_use]
26    pub fn new(parent: DynSource, base: u64, len: u64) -> Self {
27        let available = parent.len().saturating_sub(base);
28        Self {
29            parent,
30            base,
31            len: len.min(available),
32        }
33    }
34}
35
36impl ImageSource for SubRange {
37    fn len(&self) -> u64 {
38        self.len
39    }
40
41    fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
42        if offset >= self.len {
43            return Ok(0);
44        }
45        let remaining = self.len - offset;
46        let want = (buf.len() as u64).min(remaining) as usize;
47        let Some(dst) = buf.get_mut(..want) else {
48            return Ok(0); // cov:unreachable: want <= buf.len() by the min above
49        };
50        let abs = self.base.saturating_add(offset);
51        self.parent.read_at(abs, dst)
52    }
53
54    fn source_id(&self) -> SourceId {
55        // Shares the parent's lineage so a block cache accounts by base source.
56        self.parent.source_id()
57    }
58}
59
60/// Wrap a raw OS file as an [`ImageSource`] using positioned reads
61/// (`pread`/`seek_read`) — NOT a `Mutex<Seek>`, so parallel workers never
62/// serialize on one cursor at the bottom of the stack.
63pub struct FileSource {
64    file: File,
65    len: u64,
66}
67
68impl FileSource {
69    /// Open `path` read-only as a base source.
70    pub fn open(path: impl AsRef<Path>) -> VfsResult<Self> {
71        let file = File::open(path).map_err(io_err("open"))?;
72        Self::from_file(file)
73    }
74
75    /// Wrap an already-open file.
76    pub fn from_file(file: File) -> VfsResult<Self> {
77        let len = file.metadata().map_err(io_err("metadata"))?.len();
78        Ok(Self { file, len })
79    }
80}
81
82impl ImageSource for FileSource {
83    fn len(&self) -> u64 {
84        self.len
85    }
86
87    fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
88        if offset >= self.len {
89            return Ok(0);
90        }
91        // Exactly one cfg block survives stripping and becomes the tail
92        // expression — positioned reads, no cursor lock.
93        #[cfg(unix)]
94        {
95            use std::os::unix::fs::FileExt;
96            self.file.read_at(buf, offset).map_err(io_err("read_at"))
97        }
98        #[cfg(windows)]
99        {
100            use std::os::windows::fs::FileExt;
101            self.file
102                .seek_read(buf, offset)
103                .map_err(io_err("seek_read"))
104        }
105        #[cfg(not(any(unix, windows)))]
106        {
107            let _ = buf;
108            Err(crate::error::VfsError::Unsupported {
109                layer: "FileSource",
110                scheme: "positioned read".to_string(),
111            })
112        }
113    }
114}
115
116/// A single-owner `Read + Seek` *view* over a [`DynSource`], for legacy
117/// `analyse(&mut R)` / `build_filesystem(R)` call sites during migration. Clamped
118/// to `[base, base+len)`; reads advance an internal cursor over positioned reads.
119pub struct SourceCursor {
120    src: DynSource,
121    base: u64,
122    len: u64,
123    pos: u64,
124}
125
126impl SourceCursor {
127    /// A cursor over the window `[base, base+len)` of `src` (clamped to the
128    /// source's bounds), positioned at the start.
129    #[must_use]
130    pub fn new(src: DynSource, base: u64, len: u64) -> Self {
131        let available = src.len().saturating_sub(base);
132        Self {
133            src,
134            base,
135            len: len.min(available),
136            pos: 0,
137        }
138    }
139}
140
141impl Read for SourceCursor {
142    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
143        if self.pos >= self.len {
144            return Ok(0);
145        }
146        let remaining = self.len - self.pos;
147        let want = (buf.len() as u64).min(remaining) as usize;
148        let Some(dst) = buf.get_mut(..want) else {
149            return Ok(0); // cov:unreachable: want <= buf.len() by the min above
150        };
151        let abs = self.base.saturating_add(self.pos);
152        let n = self.src.read_at(abs, dst).map_err(io::Error::other)?;
153        self.pos = self.pos.saturating_add(n as u64);
154        Ok(n)
155    }
156}
157
158impl Seek for SourceCursor {
159    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
160        // i128 spans u64+i64 without overflow.
161        let target: i128 = match pos {
162            SeekFrom::Start(o) => i128::from(o),
163            SeekFrom::End(o) => i128::from(self.len) + i128::from(o),
164            SeekFrom::Current(o) => i128::from(self.pos) + i128::from(o),
165        };
166        if target < 0 {
167            return Err(io::Error::new(
168                io::ErrorKind::InvalidInput,
169                "seek before start of window",
170            ));
171        }
172        self.pos = target.min(i128::from(u64::MAX)) as u64;
173        Ok(self.pos)
174    }
175}
176
177/// Wrap one or more legacy `Read + Seek` readers as an [`ImageSource`]. A
178/// `read_at` checks out a free cursor from the pool (blocking on one if all are
179/// busy), so parallel reads scale up to the pool size instead of serializing on
180/// a single lock. A single-reader pool is a plain mutex. This is how a container
181/// decoder (VHD/VMDK/QCOW2) hands its `Read + Seek` reader back as a `DynSource`.
182pub struct SeekPoolSource<R: Read + Seek + Send> {
183    pool: Vec<Mutex<R>>,
184    len: u64,
185}
186
187impl<R: Read + Seek + Send> SeekPoolSource<R> {
188    /// A pool of independent cursors over the same `len`-byte stream.
189    #[must_use]
190    pub fn new(readers: Vec<R>, len: u64) -> Self {
191        Self {
192            pool: readers.into_iter().map(Mutex::new).collect(),
193            len,
194        }
195    }
196
197    /// A single-cursor pool (a plain mutex).
198    #[must_use]
199    pub fn single(reader: R, len: u64) -> Self {
200        Self::new(vec![reader], len)
201    }
202
203    fn checkout(&self) -> Option<std::sync::MutexGuard<'_, R>> {
204        for m in &self.pool {
205            if let Ok(g) = m.try_lock() {
206                return Some(g);
207            }
208        }
209        // All busy (or a single-cursor pool): block on the first, recovering a
210        // poisoned lock instead of panicking.
211        self.pool
212            .first()
213            .map(|m| m.lock().unwrap_or_else(std::sync::PoisonError::into_inner))
214    }
215}
216
217impl<R: Read + Seek + Send> ImageSource for SeekPoolSource<R> {
218    fn len(&self) -> u64 {
219        self.len
220    }
221
222    fn read_at(&self, offset: u64, buf: &mut [u8]) -> VfsResult<usize> {
223        if offset >= self.len {
224            return Ok(0);
225        }
226        let Some(mut guard) = self.checkout() else {
227            return Ok(0); // cov:unreachable: the pool is non-empty by construction
228        };
229        guard
230            .seek(SeekFrom::Start(offset))
231            .map_err(io_err("seek"))?;
232        let remaining = self.len - offset;
233        let want = (buf.len() as u64).min(remaining) as usize;
234        let Some(dst) = buf.get_mut(..want) else {
235            return Ok(0); // cov:unreachable: want <= buf.len() by the min above
236        };
237        // Read::read may return short; loop to fill the window or hit EOF.
238        let mut total = 0;
239        while total < dst.len() {
240            let Some(slot) = dst.get_mut(total..) else {
241                break; // cov:unreachable: total < dst.len()
242            };
243            let n = guard.read(slot).map_err(io_err("read"))?;
244            if n == 0 {
245                break;
246            }
247            total += n;
248        }
249        Ok(total)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use std::io::{Read, Seek, SeekFrom};
256    use std::sync::Arc;
257
258    use crate::source::{DynSource, ImageSource, SourceId};
259
260    use super::{FileSource, SeekPoolSource, SourceCursor, SubRange};
261
262    /// A real tempfile-backed base source, so these tests exercise `FileSource`
263    /// (positioned reads) rather than a hand-rolled in-memory double.
264    fn mem(bytes: &[u8]) -> DynSource {
265        use std::io::Write;
266        let mut f = tempfile::tempfile().unwrap();
267        f.write_all(bytes).unwrap();
268        Arc::new(FileSource::from_file(f).unwrap())
269    }
270
271    #[test]
272    fn subrange_windows_the_parent() {
273        let base = mem(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
274        let sr = SubRange::new(base, 2, 5);
275        assert_eq!(sr.len(), 5);
276        let mut buf = [0u8; 5];
277        assert_eq!(sr.read_at(0, &mut buf).unwrap(), 5);
278        assert_eq!(buf, [2, 3, 4, 5, 6]);
279        // Offset within the window.
280        let mut two = [0u8; 2];
281        assert_eq!(sr.read_at(3, &mut two).unwrap(), 2);
282        assert_eq!(two, [5, 6]);
283    }
284
285    #[test]
286    fn subrange_clamps_reads_to_the_window() {
287        let base = mem(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
288        let sr = SubRange::new(base, 2, 5); // covers parent bytes [2,7)
289        let mut buf = [0xffu8; 8];
290        // Ask for 8 bytes from offset 3 — only 2 remain in the window.
291        assert_eq!(sr.read_at(3, &mut buf).unwrap(), 2);
292        assert_eq!(&buf[..2], &[5, 6]);
293        // Read at/after the window end yields 0.
294        assert_eq!(sr.read_at(5, &mut buf).unwrap(), 0);
295        assert_eq!(sr.read_at(99, &mut buf).unwrap(), 0);
296    }
297
298    #[test]
299    fn subrange_len_is_clamped_to_parent_bounds() {
300        let base = mem(&[0, 1, 2, 3]);
301        // Ask for a window longer than the parent has from base=2.
302        let sr = SubRange::new(base, 2, 100);
303        assert_eq!(sr.len(), 2); // clamped to parent.len()-base
304    }
305
306    #[test]
307    fn subrange_nests() {
308        let base = mem(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
309        let outer = Arc::new(SubRange::new(base, 2, 6)); // parent bytes [2,8)
310        let inner = SubRange::new(outer, 1, 3); // outer bytes [1,4) = parent [3,6)
311        let mut buf = [0u8; 3];
312        assert_eq!(inner.read_at(0, &mut buf).unwrap(), 3);
313        assert_eq!(buf, [3, 4, 5]);
314    }
315
316    #[test]
317    fn filesource_reads_by_position() {
318        let mut f = tempfile::NamedTempFile::new().unwrap();
319        std::io::Write::write_all(f.as_file_mut(), &[10, 20, 30, 40, 50]).unwrap();
320        let fs = FileSource::open(f.path()).unwrap();
321        assert_eq!(fs.len(), 5);
322        assert_eq!(fs.source_id(), SourceId::ROOT);
323        let mut buf = [0u8; 3];
324        assert_eq!(fs.read_at(1, &mut buf).unwrap(), 3);
325        assert_eq!(buf, [20, 30, 40]);
326        // Past EOF: short read.
327        let mut tail = [0u8; 4];
328        assert_eq!(fs.read_at(3, &mut tail).unwrap(), 2);
329        assert_eq!(&tail[..2], &[40, 50]);
330        // Entirely past EOF: zero.
331        assert_eq!(fs.read_at(100, &mut buf).unwrap(), 0);
332    }
333
334    #[test]
335    fn sourcecursor_bridges_read_and_seek() {
336        let base = mem(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
337        let mut cur = SourceCursor::new(base, 2, 6); // window over parent [2,8)
338        let mut first = [0u8; 3];
339        cur.read_exact(&mut first).unwrap();
340        assert_eq!(first, [2, 3, 4]);
341        // Seek within the window and read the rest.
342        assert_eq!(cur.seek(SeekFrom::Start(4)).unwrap(), 4);
343        let mut rest = Vec::new();
344        cur.read_to_end(&mut rest).unwrap();
345        assert_eq!(rest, vec![6, 7]);
346        // SeekFrom::End clamps to the window length.
347        assert_eq!(cur.seek(SeekFrom::End(0)).unwrap(), 6);
348    }
349    #[test]
350    fn seek_pool_source_bridges_read_seek_to_image_source() {
351        use std::io::Cursor;
352        let data: Vec<u8> = (0..=255).collect();
353        let len = data.len() as u64;
354        // Two independent cursors over the same bytes = a 2-reader pool.
355        let pool = SeekPoolSource::new(
356            vec![Cursor::new(data.clone()), Cursor::new(data.clone())],
357            len,
358        );
359        assert_eq!(pool.len(), 256);
360        let mut buf = [0u8; 4];
361        assert_eq!(pool.read_at(10, &mut buf).unwrap(), 4);
362        assert_eq!(buf, [10, 11, 12, 13]);
363        // read past EOF -> 0
364        assert_eq!(pool.read_at(256, &mut buf).unwrap(), 0);
365        // usable as a DynSource (single-reader pool)
366        let src: DynSource = Arc::new(SeekPoolSource::single(Cursor::new(data), len));
367        let mut b2 = [0u8; 2];
368        assert_eq!(src.read_at(254, &mut b2).unwrap(), 2);
369        assert_eq!(b2, [254, 255]);
370    }
371
372    #[test]
373    fn seek_pool_short_read_breaks_at_eof() {
374        use std::io::Cursor;
375        // The pool advertises 32 bytes but the cursor only has 4: a read of the
376        // full window fills 4 then the inner Read returns 0 -> the fill loop
377        // breaks at EOF rather than spinning.
378        let pool = SeekPoolSource::single(Cursor::new(vec![1u8, 2, 3, 4]), 32);
379        let mut buf = [0u8; 16];
380        assert_eq!(pool.read_at(0, &mut buf).unwrap(), 4);
381        assert_eq!(&buf[..4], &[1, 2, 3, 4]);
382    }
383
384    #[test]
385    fn seek_pool_checkout_blocks_when_the_only_cursor_is_busy() {
386        use std::io::{self, Read, Seek, SeekFrom};
387        use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
388        use std::sync::{Arc, Mutex};
389
390        // A reader whose first read signals "I am inside read (holding the pool
391        // guard)" then waits for a go-ahead before returning — so a concurrent
392        // read_at deterministically finds try_lock failing and must fall through
393        // to the blocking .first().lock() path (checkout's contention arm).
394        struct Parking {
395            data: Vec<u8>,
396            pos: usize,
397            entered: SyncSender<()>,
398            release: Arc<Mutex<Option<Receiver<()>>>>,
399        }
400        impl Read for Parking {
401            fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
402                // Only park on the very first read (before any bytes are served).
403                if let Some(rx) = self
404                    .release
405                    .lock()
406                    .unwrap_or_else(std::sync::PoisonError::into_inner)
407                    .take()
408                {
409                    let _ = self.entered.send(());
410                    let _ = rx.recv();
411                }
412                let n = (self.data.len() - self.pos).min(buf.len());
413                buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
414                self.pos += n;
415                Ok(n)
416            }
417        }
418        impl Seek for Parking {
419            fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
420                if let SeekFrom::Start(o) = pos {
421                    self.pos = o as usize;
422                }
423                Ok(self.pos as u64)
424            }
425        }
426
427        let (entered_tx, entered_rx) = sync_channel::<()>(0);
428        let (go_tx, go_rx) = sync_channel::<()>(0);
429        let src: DynSource = Arc::new(SeekPoolSource::single(
430            Parking {
431                data: (0..16).collect(),
432                pos: 0,
433                entered: entered_tx,
434                release: Arc::new(Mutex::new(Some(go_rx))),
435            },
436            16,
437        ));
438
439        // Thread A grabs the guard and parks inside read.
440        let a = {
441            let src = src.clone();
442            std::thread::spawn(move || {
443                let mut b = [0u8; 4];
444                src.read_at(0, &mut b).unwrap();
445                b
446            })
447        };
448        entered_rx.recv().unwrap(); // A now holds the single guard.
449
450        // Thread B's read_at finds try_lock failing on the only cursor and blocks
451        // on .first().lock() — the contention arm — until A releases.
452        let b = {
453            let src = src.clone();
454            std::thread::spawn(move || {
455                let mut b = [0u8; 4];
456                src.read_at(8, &mut b).unwrap();
457                b
458            })
459        };
460        // Give B a moment to reach the blocking lock, then release A.
461        std::thread::sleep(std::time::Duration::from_millis(50));
462        go_tx.send(()).unwrap();
463
464        assert_eq!(a.join().unwrap(), [0, 1, 2, 3]);
465        assert_eq!(b.join().unwrap(), [8, 9, 10, 11]);
466    }
467}