Skip to main content

ntfs_core/
source.rs

1//! A bounded sub-reader that re-bases a partition to offset zero.
2//!
3//! A whole-disk image (raw, EWF- or VMDK-backed) holds several partitions. The
4//! NTFS reader expects offset 0 to be the volume boot record, so opening a
5//! partition means presenting just that partition's byte window as if it began
6//! at zero. [`OffsetReader`] does exactly that — and refuses every read or seek
7//! that would escape the window, so the filesystem layer cannot wander into an
8//! adjacent partition no matter how corrupt the structures it follows.
9
10use std::io::{Read, Seek, SeekFrom};
11
12use crate::error::Result;
13
14/// A `Read + Seek` view of `[base, base + len)` within an underlying source,
15/// addressed as if it began at offset 0.
16#[derive(Debug)]
17pub struct OffsetReader<R> {
18    inner: R,
19    base: u64,
20    len: u64,
21    pos: u64,
22}
23
24impl<R: Read + Seek> OffsetReader<R> {
25    /// Create a window of `len` bytes starting at absolute byte `base`.
26    ///
27    /// # Errors
28    ///
29    /// [`crate::NtfsError::Io`] if the underlying source cannot seek to `base`.
30    pub fn new(mut inner: R, base: u64, len: u64) -> Result<Self> {
31        inner.seek(SeekFrom::Start(base))?;
32        Ok(Self {
33            inner,
34            base,
35            len,
36            pos: 0,
37        })
38    }
39
40    /// The partition length in bytes.
41    #[must_use]
42    pub fn len(&self) -> u64 {
43        self.len
44    }
45
46    /// Whether the partition window is empty.
47    #[must_use]
48    pub fn is_empty(&self) -> bool {
49        self.len == 0
50    }
51}
52
53impl<R: Read + Seek> Read for OffsetReader<R> {
54    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
55        let remaining = self.len.saturating_sub(self.pos);
56        if remaining == 0 {
57            return Ok(0);
58        }
59        // Never hand the inner reader more than the window has left.
60        let cap = remaining.min(buf.len() as u64) as usize;
61        // Re-anchor the inner reader: callers may have moved it elsewhere.
62        let abs = self.base.checked_add(self.pos).ok_or_else(|| {
63            std::io::Error::new(std::io::ErrorKind::InvalidInput, "offset overflow")
64        })?;
65        self.inner.seek(SeekFrom::Start(abs))?;
66        let n = self.inner.read(&mut buf[..cap])?;
67        self.pos += n as u64;
68        Ok(n)
69    }
70}
71
72impl<R: Read + Seek> Seek for OffsetReader<R> {
73    fn seek(&mut self, from: SeekFrom) -> std::io::Result<u64> {
74        // Resolve the requested position relative to the window, as a signed
75        // value so we can reject seeks before the start.
76        let target: i128 = match from {
77            SeekFrom::Start(n) => n as i128,
78            SeekFrom::Current(d) => self.pos as i128 + d as i128,
79            SeekFrom::End(d) => self.len as i128 + d as i128,
80        };
81        if target < 0 {
82            return Err(std::io::Error::new(
83                std::io::ErrorKind::InvalidInput,
84                "seek before partition start",
85            ));
86        }
87        // Position past the end is allowed (mirrors std semantics); reads there
88        // simply return EOF. Cap the stored value at u64.
89        self.pos = u64::try_from(target).unwrap_or(u64::MAX);
90        Ok(self.pos)
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use std::io::Cursor;
98
99    /// 64 bytes of disk: partition at offset 16, length 32.
100    fn disk() -> Cursor<Vec<u8>> {
101        Cursor::new((0u8..64).collect())
102    }
103
104    #[test]
105    fn reads_are_relative_to_base() {
106        let mut r = OffsetReader::new(disk(), 16, 32).unwrap();
107        let mut buf = [0u8; 4];
108        r.read_exact(&mut buf).unwrap();
109        assert_eq!(buf, [16, 17, 18, 19]); // partition byte 0 == disk byte 16
110    }
111
112    #[test]
113    fn seek_is_relative_to_base() {
114        let mut r = OffsetReader::new(disk(), 16, 32).unwrap();
115        r.seek(SeekFrom::Start(8)).unwrap();
116        let mut buf = [0u8; 2];
117        r.read_exact(&mut buf).unwrap();
118        assert_eq!(buf, [24, 25]); // disk byte 16 + 8
119    }
120
121    #[test]
122    fn seek_end_is_partition_length() {
123        let mut r = OffsetReader::new(disk(), 16, 32).unwrap();
124        let end = r.seek(SeekFrom::End(0)).unwrap();
125        assert_eq!(end, 32); // not 64 — the window ends at the partition
126    }
127
128    #[test]
129    fn read_is_clamped_at_partition_end() {
130        let mut r = OffsetReader::new(disk(), 16, 32).unwrap();
131        r.seek(SeekFrom::Start(30)).unwrap();
132        let mut buf = [0u8; 8];
133        let n = r.read(&mut buf).unwrap();
134        assert_eq!(n, 2); // only 2 bytes remain in the window
135        assert_eq!(&buf[..2], &[46, 47]); // disk bytes 46, 47
136                                          // A further read sees EOF, never disk bytes 48+.
137        assert_eq!(r.read(&mut buf).unwrap(), 0);
138    }
139
140    #[test]
141    fn rejects_seek_before_start() {
142        let mut r = OffsetReader::new(disk(), 16, 32).unwrap();
143        assert!(r.seek(SeekFrom::Current(-1)).is_err());
144    }
145
146    #[test]
147    fn len_reports_window_size() {
148        let r = OffsetReader::new(disk(), 16, 32).unwrap();
149        assert_eq!(r.len(), 32);
150        assert!(!r.is_empty());
151    }
152
153    #[test]
154    fn read_rejects_base_plus_position_overflow() {
155        // base near u64::MAX: a non-zero position makes base + pos overflow.
156        let mut r = OffsetReader::new(disk(), u64::MAX, u64::MAX).unwrap();
157        r.seek(SeekFrom::Start(1)).unwrap();
158        let mut buf = [0u8; 4];
159        let err = r.read(&mut buf).unwrap_err();
160        assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
161    }
162}