Skip to main content

ntfs_core/
runlist.rs

1//! Data-run (runlist) decoding.
2//!
3//! A non-resident attribute stores its content in clusters described by a
4//! *runlist*: a sequence of variable-length runs. Each run begins with a header
5//! byte whose low nibble is the byte-count of the run length and whose high
6//! nibble is the byte-count of the signed LCN delta. A run with a zero offset
7//! size is *sparse* (a hole — implicitly zero). A zero header byte ends the
8//! list.
9//!
10//! Every field width and span is validated, the running LCN is accumulated with
11//! checked arithmetic, and the run count is capped — a crafted runlist can
12//! never overflow, loop forever, or address a negative cluster.
13
14use crate::error::{NtfsError, Result};
15
16/// One data run: a contiguous span of clusters, or a sparse hole.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct Run {
19    /// Length of the run, in clusters.
20    pub length: u64,
21    /// Starting logical cluster number, or `None` for a sparse run.
22    pub lcn: Option<u64>,
23}
24
25/// Upper bound on runs in a single list — a belt-and-suspenders loop cap.
26const MAX_RUNS: usize = 1 << 20;
27
28/// Decode a runlist into its runs.
29///
30/// # Errors
31///
32/// [`NtfsError::BadRunlist`] for an invalid field width, a truncated run, a
33/// zero-length run, or an LCN that overflows or goes negative.
34pub fn decode(bytes: &[u8]) -> Result<Vec<Run>> {
35    let mut runs = Vec::new();
36    let mut pos = 0usize;
37    let mut current_lcn: i64 = 0;
38
39    for _ in 0..MAX_RUNS {
40        let Some(&header) = bytes.get(pos) else {
41            break; // ran off the end without a terminator — stop cleanly
42        };
43        if header == 0 {
44            break; // explicit end of list
45        }
46        pos += 1;
47
48        let len_bytes = (header & 0x0F) as usize;
49        let off_bytes = (header >> 4) as usize;
50        if len_bytes == 0 || len_bytes > 8 {
51            return Err(NtfsError::BadRunlist("invalid run length field width"));
52        }
53        if off_bytes > 8 {
54            return Err(NtfsError::BadRunlist("invalid run offset field width"));
55        }
56
57        let length = read_uint(bytes, pos, len_bytes)
58            .ok_or(NtfsError::BadRunlist("length runs past end"))?;
59        pos += len_bytes;
60        if length == 0 {
61            return Err(NtfsError::BadRunlist("zero-length run"));
62        }
63
64        let lcn = if off_bytes == 0 {
65            None // sparse run — the running LCN is left unchanged
66        } else {
67            let delta = read_sint(bytes, pos, off_bytes)
68                .ok_or(NtfsError::BadRunlist("offset runs past end"))?;
69            pos += off_bytes;
70            current_lcn = current_lcn
71                .checked_add(delta)
72                .ok_or(NtfsError::BadRunlist("LCN overflow"))?;
73            if current_lcn < 0 {
74                return Err(NtfsError::BadRunlist("negative LCN"));
75            }
76            Some(current_lcn as u64)
77        };
78
79        runs.push(Run { length, lcn });
80    }
81
82    Ok(runs)
83}
84
85/// Total length of all runs, in clusters (checked).
86///
87/// # Errors
88///
89/// [`NtfsError::BadRunlist`] if the lengths overflow `u64`.
90pub fn total_clusters(runs: &[Run]) -> Result<u64> {
91    let mut total = 0u64;
92    for r in runs {
93        total = total
94            .checked_add(r.length)
95            .ok_or(NtfsError::BadRunlist("total cluster count overflow"))?;
96    }
97    Ok(total)
98}
99
100/// Read an `n`-byte (1..=8) little-endian unsigned integer at `pos`.
101fn read_uint(bytes: &[u8], pos: usize, n: usize) -> Option<u64> {
102    let slice = bytes.get(pos..pos.checked_add(n)?)?;
103    let mut v = 0u64;
104    for (i, &b) in slice.iter().enumerate() {
105        v |= u64::from(b) << (8 * i);
106    }
107    Some(v)
108}
109
110/// Read an `n`-byte (1..=8) little-endian *signed* integer at `pos`,
111/// sign-extending from the top bit.
112fn read_sint(bytes: &[u8], pos: usize, n: usize) -> Option<i64> {
113    let slice = bytes.get(pos..pos.checked_add(n)?)?;
114    let mut v = 0i64;
115    for (i, &b) in slice.iter().enumerate() {
116        v |= i64::from(b) << (8 * i);
117    }
118    let bits = n * 8;
119    if bits < 64 {
120        let sign_bit = 1i64 << (bits - 1);
121        if v & sign_bit != 0 {
122            v |= -(1i64 << bits); // set the high bits
123        }
124    }
125    Some(v)
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn decodes_single_run() {
134        // 0x21: 1 length byte, 2 offset bytes. length=8, offset=0x0100=256.
135        let runs = decode(&[0x21, 0x08, 0x00, 0x01, 0x00]).unwrap();
136        assert_eq!(
137            runs,
138            vec![Run {
139                length: 8,
140                lcn: Some(256)
141            }]
142        );
143    }
144
145    #[test]
146    fn decodes_multiple_runs_with_delta() {
147        // run1: len 4 @ lcn 256; run2: len 4, delta +256 ⇒ lcn 512.
148        let bytes = [0x21, 0x04, 0x00, 0x01, 0x21, 0x04, 0x00, 0x01, 0x00];
149        let runs = decode(&bytes).unwrap();
150        assert_eq!(
151            runs,
152            vec![
153                Run {
154                    length: 4,
155                    lcn: Some(256)
156                },
157                Run {
158                    length: 4,
159                    lcn: Some(512)
160                },
161            ]
162        );
163    }
164
165    #[test]
166    fn decodes_sparse_run() {
167        // 0x01: 1 length byte, 0 offset bytes ⇒ sparse.
168        let runs = decode(&[0x01, 0x05, 0x00]).unwrap();
169        assert_eq!(
170            runs,
171            vec![Run {
172                length: 5,
173                lcn: None
174            }]
175        );
176    }
177
178    #[test]
179    fn decodes_negative_delta() {
180        // run1: len 4 @ lcn 512; run2: len 4, delta -2 (0xFE) ⇒ lcn 510.
181        let bytes = [0x21, 0x04, 0x00, 0x02, 0x11, 0x04, 0xFE, 0x00];
182        let runs = decode(&bytes).unwrap();
183        assert_eq!(runs[1].lcn, Some(510));
184    }
185
186    #[test]
187    fn sparse_run_does_not_shift_following_lcn() {
188        // real @ lcn 256, then a sparse hole, then real with delta +1 ⇒ lcn 257.
189        let bytes = [
190            0x21, 0x04, 0x00, 0x01, // lcn 256, len 4
191            0x01, 0x02, // sparse, len 2 (no offset)
192            0x11, 0x04, 0x01, // delta +1 ⇒ lcn 257
193            0x00,
194        ];
195        let runs = decode(&bytes).unwrap();
196        assert_eq!(runs[0].lcn, Some(256));
197        assert_eq!(runs[1].lcn, None);
198        assert_eq!(runs[2].lcn, Some(257));
199    }
200
201    #[test]
202    fn zero_header_ends_list() {
203        assert!(decode(&[0x00]).unwrap().is_empty());
204        assert!(decode(&[]).unwrap().is_empty());
205    }
206
207    #[test]
208    fn total_clusters_sums_lengths() {
209        let runs = vec![
210            Run {
211                length: 4,
212                lcn: Some(0),
213            },
214            Run {
215                length: 6,
216                lcn: None,
217            },
218        ];
219        assert_eq!(total_clusters(&runs).unwrap(), 10);
220    }
221
222    // ── Hardening ─────────────────────────────────────────────────────────────
223
224    #[test]
225    fn rejects_length_field_too_large() {
226        // low nibble 9 ⇒ 9-byte length, impossible for u64.
227        assert!(matches!(decode(&[0x09]), Err(NtfsError::BadRunlist(_))));
228    }
229
230    #[test]
231    fn rejects_offset_field_too_large() {
232        // high nibble 9 ⇒ 9-byte offset, impossible for u64.
233        assert!(matches!(
234            decode(&[0x91, 0x05]),
235            Err(NtfsError::BadRunlist(_))
236        ));
237    }
238
239    #[test]
240    fn decodes_full_width_eight_byte_offset() {
241        // off_bytes = 8 ⇒ the signed read takes the full-width (no sign-extend) path.
242        let rl = [0x81u8, 0x05, 0x10, 0, 0, 0, 0, 0, 0, 0];
243        let runs = decode(&rl).unwrap();
244        assert_eq!(runs[0].lcn, Some(0x10));
245    }
246
247    #[test]
248    fn rejects_truncated_run() {
249        // header wants 1 length + 2 offset bytes, but only the length is present.
250        assert!(matches!(
251            decode(&[0x21, 0x08]),
252            Err(NtfsError::BadRunlist(_))
253        ));
254    }
255
256    #[test]
257    fn rejects_zero_length_run() {
258        assert!(matches!(
259            decode(&[0x21, 0x00, 0x00, 0x01]),
260            Err(NtfsError::BadRunlist(_))
261        ));
262    }
263
264    #[test]
265    fn rejects_negative_lcn() {
266        // First (absolute) run with a negative offset is invalid.
267        assert!(matches!(
268            decode(&[0x11, 0x04, 0xFF]), // delta -1 from 0 ⇒ -1
269            Err(NtfsError::BadRunlist(_))
270        ));
271    }
272}