Skip to main content

ntfs_core/
data.rs

1//! Reconstructing an attribute's bytes — resident inline, or non-resident by
2//! following its runlist across the volume.
3//!
4//! Sparse runs yield zeroes without touching the disk; real runs are read at
5//! `lcn × cluster_size`. The output is bounded by the attribute's real size and
6//! by the bytes its runs actually allocate, and every size is checked — a
7//! crafted runlist cannot trigger an unbounded allocation or an out-of-range
8//! seek.
9
10use std::io::{Read, Seek, SeekFrom};
11
12use crate::attribute::{Attribute, AttributeBody};
13use crate::error::{NtfsError, Result};
14use crate::runlist::{self, Run};
15
16/// Hard ceiling on a single reconstructed value (1 TiB) — far above any real
17/// artifact, but stops an allocation bomb from a crafted size.
18const MAX_VALUE_BYTES: u64 = 1 << 40;
19
20/// Read `real_size` bytes of a file described by `runs`, from `reader`.
21///
22/// The result is `min(real_size, bytes the runs allocate)` bytes long; sparse
23/// runs contribute zeroes.
24///
25/// # Errors
26///
27/// [`NtfsError::BadRunlist`] on arithmetic overflow, [`NtfsError::TooLarge`]
28/// when the requested size is implausible, or [`NtfsError::Io`] on read failure.
29pub fn read_runs<R: Read + Seek>(
30    reader: &mut R,
31    runs: &[Run],
32    cluster_size: u64,
33    real_size: u64,
34) -> Result<Vec<u8>> {
35    read_runs_capped(reader, runs, cluster_size, real_size, u64::MAX)
36}
37
38/// Like [`read_runs`], but materialize at most `max_bytes` — the cap is applied
39/// **during** the walk, so a crafted/huge runlist can never balloon a `Vec`
40/// before a caller truncates it. The result is the first
41/// `min(real_size, bytes the runs allocate, max_bytes)` bytes of the stream — a
42/// true prefix, with the final partial cluster sliced to the cap.
43///
44/// # Errors
45///
46/// As [`read_runs`].
47pub fn read_runs_capped<R: Read + Seek>(
48    reader: &mut R,
49    runs: &[Run],
50    cluster_size: u64,
51    real_size: u64,
52    max_bytes: u64,
53) -> Result<Vec<u8>> {
54    // Bytes the runs allocate (checked); the value can't exceed this.
55    let mut allocated = 0u64;
56    for r in runs {
57        let run_bytes = r
58            .length
59            .checked_mul(cluster_size)
60            .ok_or(NtfsError::BadRunlist("run byte length overflow"))?;
61        allocated = allocated
62            .checked_add(run_bytes)
63            .ok_or(NtfsError::BadRunlist("allocation overflow"))?;
64    }
65
66    let want = real_size.min(allocated).min(max_bytes);
67    if want > MAX_VALUE_BYTES {
68        return Err(NtfsError::TooLarge { bytes: want });
69    }
70    let want_usize = usize::try_from(want).map_err(|_| NtfsError::TooLarge { bytes: want })?;
71
72    let mut out: Vec<u8> = Vec::new();
73    out.try_reserve_exact(want_usize)
74        .map_err(|_| NtfsError::TooLarge { bytes: want })?;
75
76    let mut remaining = want;
77    for r in runs {
78        if remaining == 0 {
79            break;
80        }
81        let run_bytes = r.length * cluster_size; // already checked above
82        let take = run_bytes.min(remaining);
83        let take_usize = take as usize; // ≤ want ≤ MAX_VALUE_BYTES, fits usize
84
85        match r.lcn {
86            None => out.resize(out.len() + take_usize, 0), // sparse hole → zeroes
87            Some(lcn) => {
88                let byte_off = lcn
89                    .checked_mul(cluster_size)
90                    .ok_or(NtfsError::BadRunlist("LCN byte offset overflow"))?;
91                reader.seek(SeekFrom::Start(byte_off))?;
92                let start = out.len();
93                out.resize(start + take_usize, 0);
94                reader.read_exact(&mut out[start..])?;
95            }
96        }
97        remaining -= take;
98    }
99
100    Ok(out)
101}
102
103/// Read an attribute's value, dispatching on resident vs non-resident.
104///
105/// `record` is the (fixed-up) MFT record the attribute lives in; `reader` is the
106/// volume; `cluster_size` is from the boot sector.
107///
108/// # Errors
109///
110/// As [`read_runs`], plus [`NtfsError::BadAttribute`] when a resident value or
111/// the runlist slice is out of bounds.
112pub fn read_attribute_value<R: Read + Seek>(
113    reader: &mut R,
114    record: &[u8],
115    attribute: &Attribute,
116    cluster_size: u64,
117) -> Result<Vec<u8>> {
118    match attribute.body {
119        AttributeBody::Resident { .. } => attribute
120            .resident_content(record)
121            .map(<[u8]>::to_vec)
122            .ok_or(NtfsError::BadAttribute {
123                offset: attribute.offset,
124                detail: "resident content out of bounds",
125            }),
126        AttributeBody::NonResident { real_size, .. } => {
127            let runs = attribute_runlist(record, attribute)?;
128            read_nonresident(reader, &runs, cluster_size, real_size, attribute)
129        }
130    }
131}
132
133/// Read a non-resident `$DATA` value from its (already-assembled) runlist,
134/// LZNT1-decompressing when `attribute` is compressed.
135///
136/// This is the **single** dispatch point for both the single-record path
137/// ([`read_attribute_value`]) and the split-runlist path
138/// (`NtfsFs::read_data_stream`, which concatenates a `$DATA` spread across
139/// `$ATTRIBUTE_LIST` extension records) — so neither can read a compressed file
140/// as raw bytes. (A compressed `$DATA` stores data in `2^compression_unit`
141/// clusters; `checked_shl` rejects an implausible crafted unit before overflow.)
142///
143/// # Errors
144///
145/// As [`read_runs`] / [`read_compressed_runs`], plus [`NtfsError::BadAttribute`]
146/// for an implausible compression unit.
147pub(crate) fn read_nonresident<R: Read + Seek>(
148    reader: &mut R,
149    runs: &[Run],
150    cluster_size: u64,
151    real_size: u64,
152    attribute: &Attribute,
153) -> Result<Vec<u8>> {
154    read_nonresident_capped(reader, runs, cluster_size, real_size, attribute, u64::MAX)
155}
156
157/// Like [`read_nonresident`], but materialize at most `max_bytes` — the cap is
158/// honored on both the raw-runlist and the LZNT1-decompressed path, so neither
159/// can balloon memory past the cap before a caller truncates.
160///
161/// # Errors
162///
163/// As [`read_nonresident`].
164pub(crate) fn read_nonresident_capped<R: Read + Seek>(
165    reader: &mut R,
166    runs: &[Run],
167    cluster_size: u64,
168    real_size: u64,
169    attribute: &Attribute,
170    max_bytes: u64,
171) -> Result<Vec<u8>> {
172    let cu = attribute.compression_unit();
173    if attribute.is_compressed() && cu != 0 {
174        let unit_clusters = 1u64
175            .checked_shl(u32::from(cu))
176            .ok_or(NtfsError::BadAttribute {
177                offset: attribute.offset,
178                detail: "implausible compression unit",
179            })?;
180        read_compressed_runs_capped(
181            reader,
182            runs,
183            cluster_size,
184            real_size,
185            unit_clusters,
186            max_bytes,
187        )
188    } else {
189        read_runs_capped(reader, runs, cluster_size, real_size, max_bytes)
190    }
191}
192
193/// Read a COMPRESSED non-resident attribute, LZNT1-decompressing only enough
194/// units to satisfy `max_bytes` — the walk stops once the cap is reached, so a
195/// crafted huge `real_size` can't force the whole stream to be decompressed into
196/// memory. NTFS stores a compressed file in `unit_clusters`-cluster units; within
197/// a unit the data is either: fully allocated (stored uncompressed → copied
198/// verbatim), partially allocated then sparse-padded (the allocated clusters hold
199/// the LZNT1 stream → decompressed), or fully sparse (a unit of zeroes). The
200/// result is truncated to `min(real_size, max_bytes)`.
201///
202/// # Errors
203///
204/// [`NtfsError::TooLarge`] for an implausible size, [`NtfsError::BadRunlist`] on
205/// arithmetic overflow, [`NtfsError::BadCompression`] on a malformed LZNT1
206/// stream, or [`NtfsError::Io`] on read failure.
207fn read_compressed_runs_capped<R: Read + Seek>(
208    reader: &mut R,
209    runs: &[Run],
210    cluster_size: u64,
211    real_size: u64,
212    unit_clusters: u64,
213    max_bytes: u64,
214) -> Result<Vec<u8>> {
215    if real_size > MAX_VALUE_BYTES {
216        return Err(NtfsError::TooLarge { bytes: real_size });
217    }
218    // The byte target is the declared size clamped by the cap — every loop bound
219    // and tail computation below works against this, never the raw real_size.
220    let target = real_size.min(max_bytes);
221    let unit_bytes = unit_clusters
222        .checked_mul(cluster_size)
223        .ok_or(NtfsError::BadRunlist("compression unit byte size overflow"))?;
224
225    // Run cursor: (lcn, remaining_clusters), mutated as clusters are consumed.
226    let mut queue: std::collections::VecDeque<(Option<u64>, u64)> =
227        runs.iter().map(|r| (r.lcn, r.length)).collect();
228    let target_usize =
229        usize::try_from(target).map_err(|_| NtfsError::TooLarge { bytes: target })?;
230    let mut out: Vec<u8> = Vec::new();
231
232    while (out.len() as u64) < target {
233        // Gather exactly one compression unit (`unit_clusters` of VCN) from the
234        // runlist; the leading allocated clusters (if any) hold the unit's bytes.
235        let mut real_bytes: Vec<u8> = Vec::new();
236        let mut real_clusters = 0u64;
237        let mut got = 0u64;
238        while got < unit_clusters {
239            let Some((lcn, avail)) = queue.front_mut() else {
240                break;
241            };
242            let take = (unit_clusters - got).min(*avail);
243            if let Some(l) = *lcn {
244                let byte_off = l
245                    .checked_mul(cluster_size)
246                    .ok_or(NtfsError::BadRunlist("LCN byte offset overflow"))?;
247                let nbytes =
248                    usize::try_from(take * cluster_size).map_err(|_| NtfsError::TooLarge {
249                        bytes: take * cluster_size, // cov:unreachable: take*cluster_size ≤ unit_bytes (checked to fit u64); on 64-bit usize this conversion is infallible
250                    })?; // cov:unreachable: see the bytes line above — conversion never fails on 64-bit
251
252                reader.seek(SeekFrom::Start(byte_off))?;
253                let start = real_bytes.len();
254                real_bytes.resize(start + nbytes, 0);
255                reader.read_exact(&mut real_bytes[start..])?;
256                real_clusters += take;
257                *lcn = Some(l + take); // advance the LCN within this run
258            }
259            *avail -= take;
260            if *avail == 0 {
261                queue.pop_front();
262            }
263            got += take;
264        }
265        if got == 0 {
266            break; // runlist exhausted
267        }
268
269        if real_clusters == 0 {
270            // Fully sparse unit → a unit of zeroes (bounded by the file tail).
271            let want = unit_bytes.min(target - out.len() as u64);
272            let want = usize::try_from(want).map_err(|_| NtfsError::TooLarge { bytes: want })?;
273            out.resize(out.len() + want, 0);
274        } else if real_clusters == unit_clusters {
275            // Fully allocated → stored uncompressed; append verbatim.
276            out.extend_from_slice(&real_bytes);
277        } else {
278            // Partially allocated → the allocated clusters hold the LZNT1 stream.
279            // A unit decodes to at most unit_bytes; cap it so any trailing cluster
280            // slack the decoder ran into can't misalign later units (the final
281            // unit is bounded by the truncate to real_size below).
282            let mut decompressed = Vec::new();
283            lznt1::decompress(&real_bytes, &mut decompressed)
284                .map_err(|_| NtfsError::BadCompression("LZNT1 decode failed"))?;
285            decompressed.truncate(unit_bytes as usize);
286            out.extend_from_slice(&decompressed);
287        }
288    }
289
290    out.truncate(target_usize);
291    Ok(out)
292}
293
294/// Decode the data-run list of a non-resident attribute from its (fixed-up)
295/// record bytes.
296///
297/// Reused to assemble a split `$DATA` whose runlist spans several `$DATA`
298/// attributes in different MFT records (via `$ATTRIBUTE_LIST`).
299///
300/// # Errors
301///
302/// [`NtfsError::BadAttribute`] for a resident attribute or an out-of-bounds
303/// runlist; [`NtfsError::BadRunlist`] for a malformed runlist.
304pub fn attribute_runlist(record: &[u8], attribute: &Attribute) -> Result<Vec<Run>> {
305    let AttributeBody::NonResident { runs_offset, .. } = attribute.body else {
306        return Err(NtfsError::BadAttribute {
307            offset: attribute.offset,
308            detail: "attribute is resident (no runlist)",
309        });
310    };
311    let attr_end = attribute
312        .offset
313        .checked_add(attribute.length as usize)
314        .ok_or(NtfsError::BadAttribute {
315            offset: attribute.offset,
316            detail: "attribute length overflow",
317        })?;
318    let runs_start =
319        attribute
320            .offset
321            .checked_add(runs_offset as usize)
322            .ok_or(NtfsError::BadAttribute {
323                offset: attribute.offset,
324                detail: "runs offset overflow",
325            })?;
326    let runs_bytes = record
327        .get(runs_start..attr_end)
328        .ok_or(NtfsError::BadAttribute {
329            offset: attribute.offset,
330            detail: "runlist out of bounds",
331        })?;
332    runlist::decode(runs_bytes)
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use std::io::Cursor;
339
340    /// A volume where cluster `c` is filled with byte value `c as u8`.
341    fn volume(clusters: usize, cluster_size: usize) -> Cursor<Vec<u8>> {
342        let mut v = vec![0u8; clusters * cluster_size];
343        for c in 0..clusters {
344            let b = c as u8;
345            for x in &mut v[c * cluster_size..(c + 1) * cluster_size] {
346                *x = b;
347            }
348        }
349        Cursor::new(v)
350    }
351
352    #[test]
353    fn reads_single_run() {
354        let mut vol = volume(4, 512);
355        // One run: 2 clusters starting at LCN 1.
356        let runs = [Run {
357            length: 2,
358            lcn: Some(1),
359        }];
360        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
361        assert_eq!(out.len(), 1024);
362        assert!(out[..512].iter().all(|&b| b == 1));
363        assert!(out[512..].iter().all(|&b| b == 2));
364    }
365
366    #[test]
367    fn sparse_run_yields_zeroes_without_reading() {
368        let mut vol = volume(1, 512); // too small to read 2 clusters — proves no read
369        let runs = [Run {
370            length: 2,
371            lcn: None,
372        }];
373        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
374        assert_eq!(out.len(), 1024);
375        assert!(out.iter().all(|&b| b == 0));
376    }
377
378    #[test]
379    fn truncates_to_real_size() {
380        let mut vol = volume(4, 512);
381        let runs = [Run {
382            length: 2,
383            lcn: Some(0),
384        }]; // 1024 allocated
385        let out = read_runs(&mut vol, &runs, 512, 600).unwrap();
386        assert_eq!(out.len(), 600);
387    }
388
389    #[test]
390    fn mixed_data_and_sparse() {
391        let mut vol = volume(4, 512);
392        let runs = [
393            Run {
394                length: 1,
395                lcn: Some(3),
396            }, // cluster 3 → all 3s
397            Run {
398                length: 1,
399                lcn: None,
400            }, // sparse → zeros
401        ];
402        let out = read_runs(&mut vol, &runs, 512, 1024).unwrap();
403        assert!(out[..512].iter().all(|&b| b == 3));
404        assert!(out[512..].iter().all(|&b| b == 0));
405    }
406
407    #[test]
408    fn refuses_implausible_size() {
409        // A crafted runlist that *allocates* far more than the ceiling — a
410        // single sparse run of 2^40 clusters. (A huge real_size alone is
411        // harmless: it is clamped to what the runs actually allocate.)
412        let mut vol = volume(1, 512);
413        let runs = [Run {
414            length: 1 << 40,
415            lcn: None,
416        }];
417        assert!(matches!(
418            read_runs(&mut vol, &runs, 512, u64::MAX),
419            Err(NtfsError::TooLarge { .. })
420        ));
421    }
422
423    #[test]
424    fn rejects_cluster_size_overflow() {
425        let mut vol = volume(1, 512);
426        let runs = [Run {
427            length: u64::MAX,
428            lcn: Some(0),
429        }];
430        assert!(matches!(
431            read_runs(&mut vol, &runs, 512, 1024),
432            Err(NtfsError::BadRunlist(_))
433        ));
434    }
435
436    // ── read_attribute_value dispatch ─────────────────────────────────────────
437
438    #[test]
439    fn reads_resident_value() {
440        use forensicnomicon::ntfs::attr_types;
441        // Build a one-attribute record with resident $DATA content "hello".
442        let content = b"hello";
443        // Minimal resident attribute laid out by hand at record offset 0x10.
444        let attr_off = 0x10usize;
445        let mut record = vec![0u8; attr_off];
446        // header: type, length, resident, name_len 0, name_off, flags, id
447        let name_offset = 0x18u16;
448        let content_offset = 0x18u16;
449        let length = (content_offset as usize + content.len() + 7) & !7;
450        let mut a = vec![0u8; length];
451        a[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
452        a[0x04..0x08].copy_from_slice(&(length as u32).to_le_bytes());
453        a[0x0A..0x0C].copy_from_slice(&name_offset.to_le_bytes());
454        a[0x10..0x14].copy_from_slice(&(content.len() as u32).to_le_bytes());
455        a[0x14..0x16].copy_from_slice(&content_offset.to_le_bytes());
456        a[content_offset as usize..content_offset as usize + content.len()]
457            .copy_from_slice(content);
458        record.extend_from_slice(&a);
459        record.extend_from_slice(&attr_types::END.to_le_bytes());
460
461        let attrs = crate::attribute::parse_attributes(&record, attr_off).unwrap();
462        let mut vol = volume(1, 512);
463        let out = read_attribute_value(&mut vol, &record, &attrs[0], 512).unwrap();
464        assert_eq!(out, b"hello");
465    }
466
467    #[test]
468    fn reads_nonresident_value_via_runlist() {
469        use forensicnomicon::ntfs::attr_types;
470        // Non-resident $DATA: runlist of 1 cluster @ LCN 2, real size 512.
471        let runs_bytes = [0x11u8, 0x01, 0x02, 0x00]; // len 1, lcn delta +2
472        let attr_off = 0x10usize;
473        let mut record = vec![0u8; attr_off];
474        let runs_offset = 0x40u16;
475        let length = ((runs_offset as usize + runs_bytes.len()) + 7) & !7;
476        let mut a = vec![0u8; length];
477        a[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
478        a[0x04..0x08].copy_from_slice(&(length as u32).to_le_bytes());
479        a[0x08] = 1; // non-resident
480        a[0x0A..0x0C].copy_from_slice(&runs_offset.to_le_bytes()); // name offset (no name)
481        a[0x20..0x22].copy_from_slice(&runs_offset.to_le_bytes()); // runs offset
482        a[0x28..0x30].copy_from_slice(&512u64.to_le_bytes()); // allocated
483        a[0x30..0x38].copy_from_slice(&512u64.to_le_bytes()); // real size
484        a[runs_offset as usize..runs_offset as usize + runs_bytes.len()]
485            .copy_from_slice(&runs_bytes);
486        record.extend_from_slice(&a);
487        record.extend_from_slice(&attr_types::END.to_le_bytes());
488
489        let attrs = crate::attribute::parse_attributes(&record, attr_off).unwrap();
490        let mut vol = volume(4, 512); // cluster 2 → all 2s
491        let out = read_attribute_value(&mut vol, &record, &attrs[0], 512).unwrap();
492        assert_eq!(out.len(), 512);
493        assert!(out.iter().all(|&b| b == 2));
494    }
495
496    #[test]
497    fn reads_compressed_nonresident_value() {
498        use forensicnomicon::ntfs::attr_types;
499        // A COMPRESSED $DATA: one 16-cluster compression unit made of 1 real
500        // cluster (the LZNT1 stream) + 15 sparse clusters. real_size = 100 bytes.
501        // The stream is a single *uncompressed* LZNT1 chunk (header bit15=0,
502        // low-12 = size-1), which is valid LZNT1 the decompressor copies verbatim
503        // — lets us build a real compressed-unit fixture without a compressor.
504        let content = vec![0xABu8; 100];
505        let mut stream = Vec::new();
506        stream.extend_from_slice(&(content.len() as u16 - 1).to_le_bytes()); // 0x0063
507        stream.extend_from_slice(&content);
508
509        // runlist: 0x11 len=1 lcn+2 | 0x01 len=15 sparse | 0x00 end
510        let runs_bytes = [0x11u8, 0x01, 0x02, 0x01, 0x0F, 0x00];
511        let attr_off = 0x10usize;
512        let mut record = vec![0u8; attr_off];
513        let runs_offset = 0x40u16;
514        let length = ((runs_offset as usize + runs_bytes.len()) + 7) & !7;
515        let mut a = vec![0u8; length];
516        a[0x00..0x04].copy_from_slice(&attr_types::DATA.to_le_bytes());
517        a[0x04..0x08].copy_from_slice(&(length as u32).to_le_bytes());
518        a[0x08] = 1; // non-resident
519        a[0x0A..0x0C].copy_from_slice(&runs_offset.to_le_bytes()); // name offset (no name)
520        a[0x0C..0x0E].copy_from_slice(&0x0001u16.to_le_bytes()); // flags: COMPRESSED
521        a[0x20..0x22].copy_from_slice(&runs_offset.to_le_bytes()); // runs offset
522        a[0x22..0x24].copy_from_slice(&4u16.to_le_bytes()); // compression_unit = 4 → 16 clusters
523        a[0x28..0x30].copy_from_slice(&(16u64 * 512).to_le_bytes()); // allocated 8192
524        a[0x30..0x38].copy_from_slice(&(content.len() as u64).to_le_bytes()); // real size 100
525        a[runs_offset as usize..runs_offset as usize + runs_bytes.len()]
526            .copy_from_slice(&runs_bytes);
527        record.extend_from_slice(&a);
528        record.extend_from_slice(&attr_types::END.to_le_bytes());
529
530        let cluster_size = 512usize;
531        let mut disk = vec![0u8; 16 * cluster_size];
532        disk[2 * cluster_size..2 * cluster_size + stream.len()].copy_from_slice(&stream);
533        let mut vol = std::io::Cursor::new(disk);
534
535        let attrs = crate::attribute::parse_attributes(&record, attr_off).unwrap();
536        let out = read_attribute_value(&mut vol, &record, &attrs[0], 512).unwrap();
537        assert_eq!(
538            out, content,
539            "compressed $DATA must be LZNT1-decompressed, not returned raw"
540        );
541    }
542
543    #[test]
544    fn compressed_runs_fully_sparse_unit_is_zeroes() {
545        // A unit with no allocated clusters (real_clusters == 0) is a hole → zeroes.
546        let mut vol = std::io::Cursor::new(vec![0u8; 16 * 512]);
547        let runs = [Run {
548            length: 16,
549            lcn: None,
550        }];
551        let out = read_compressed_runs_capped(&mut vol, &runs, 512, 100, 16, u64::MAX).unwrap();
552        assert_eq!(out, vec![0u8; 100]);
553    }
554
555    #[test]
556    fn compressed_runs_fully_allocated_unit_is_verbatim() {
557        // A fully-allocated unit (real_clusters == unit_clusters) is stored
558        // uncompressed → copied verbatim, not run through the LZNT1 decoder.
559        let mut vol = std::io::Cursor::new(vec![0x5Au8; 16 * 512]);
560        let runs = [Run {
561            length: 16,
562            lcn: Some(0),
563        }];
564        let out =
565            read_compressed_runs_capped(&mut vol, &runs, 512, 16 * 512, 16, u64::MAX).unwrap();
566        assert_eq!(out.len(), 16 * 512);
567        assert!(out.iter().all(|&b| b == 0x5A));
568    }
569
570    #[test]
571    fn compressed_runs_stop_when_runlist_exhausted() {
572        // real_size claims two units but the runlist provides only one → the walk
573        // stops at the runlist end (no panic, no infinite loop) and returns what
574        // it has.
575        let mut vol = std::io::Cursor::new(vec![0x11u8; 16 * 512]);
576        let runs = [Run {
577            length: 16,
578            lcn: Some(0),
579        }];
580        let out =
581            read_compressed_runs_capped(&mut vol, &runs, 512, 2 * 16 * 512, 16, u64::MAX).unwrap();
582        assert_eq!(out.len(), 16 * 512, "only the available unit is returned");
583    }
584
585    #[test]
586    fn compressed_runs_reject_implausible_real_size() {
587        let mut vol = std::io::Cursor::new(vec![0u8; 16]);
588        let runs = [Run {
589            length: 1,
590            lcn: Some(0),
591        }];
592        let err =
593            read_compressed_runs_capped(&mut vol, &runs, 512, MAX_VALUE_BYTES + 1, 16, u64::MAX);
594        assert!(matches!(err, Err(NtfsError::TooLarge { .. })));
595    }
596
597    #[test]
598    fn compressed_runs_capped_below_real_size_is_a_true_prefix() {
599        // A fully-allocated 16-cluster unit (8192 bytes of 0x5A); a cap below the
600        // real size must bound the result to a true prefix.
601        let runs = [Run {
602            length: 16,
603            lcn: Some(0),
604        }];
605        let mut full_vol = std::io::Cursor::new(vec![0x5Au8; 16 * 512]);
606        let full =
607            read_compressed_runs_capped(&mut full_vol, &runs, 512, 16 * 512, 16, u64::MAX).unwrap();
608
609        let cap = 700u64;
610        let mut vol = std::io::Cursor::new(vec![0x5Au8; 16 * 512]);
611        let out = read_compressed_runs_capped(&mut vol, &runs, 512, 16 * 512, 16, cap).unwrap();
612        assert!(out.len() as u64 <= cap);
613        assert_eq!(out.len(), cap as usize);
614        assert_eq!(
615            out[..],
616            full[..cap as usize],
617            "capped bytes are a true prefix"
618        );
619    }
620
621    #[test]
622    fn stops_reading_once_real_size_is_met() {
623        // real_size covers only the first run; the second run must not be read.
624        let mut vol = volume(4, 512);
625        let runs = [
626            Run {
627                length: 1,
628                lcn: Some(0),
629            },
630            Run {
631                length: 1,
632                lcn: Some(1),
633            },
634        ];
635        let out = read_runs(&mut vol, &runs, 512, 512).unwrap();
636        assert_eq!(out.len(), 512); // only the first run's worth
637    }
638
639    #[test]
640    fn capped_read_stops_at_cap_and_is_a_true_prefix() {
641        // Three contiguous clusters (1,2,3) → 1536 bytes; cluster `c` holds byte
642        // `c`, so a cap that lands mid-stream must be a real prefix, not zeroes.
643        let runs = [Run {
644            length: 3,
645            lcn: Some(1),
646        }];
647        let mut full_vol = volume(4, 512);
648        let full = read_runs(&mut full_vol, &runs, 512, 1536).unwrap();
649        assert_eq!(full.len(), 1536);
650
651        let cap = 700usize; // crosses the first cluster boundary (512..1024)
652        let mut capped_vol = volume(4, 512);
653        let capped = read_runs_capped(&mut capped_vol, &runs, 512, 1536, cap as u64).unwrap();
654        assert!(capped.len() <= cap, "capped read must not exceed the cap");
655        assert_eq!(
656            capped.len(),
657            cap,
658            "cap is below the real size, so it bounds"
659        );
660        assert_eq!(capped[..], full[..cap], "capped bytes are a true prefix");
661    }
662
663    #[test]
664    fn rejects_runlist_region_out_of_bounds() {
665        use crate::attribute::{Attribute, AttributeBody};
666        // runs_offset points past the attribute, so the runlist slice is invalid.
667        let attr = Attribute {
668            type_code: forensicnomicon::ntfs::attr_types::DATA,
669            length: 0x48,
670            non_resident: true,
671            name: None,
672            flags: 0,
673            attribute_id: 0,
674            offset: 0,
675            body: AttributeBody::NonResident {
676                start_vcn: 0,
677                last_vcn: 0,
678                runs_offset: 0xFFFF,
679                compression_unit: 0,
680                allocated_size: 512,
681                real_size: 512,
682                initialized_size: 512,
683            },
684        };
685        let record = vec![0u8; 0x48];
686        let mut vol = volume(1, 512);
687        assert!(matches!(
688            read_attribute_value(&mut vol, &record, &attr, 512),
689            Err(NtfsError::BadAttribute { detail, .. }) if detail == "runlist out of bounds"
690        ));
691    }
692
693    #[test]
694    fn rejects_runs_offset_overflow() {
695        use crate::attribute::{Attribute, AttributeBody};
696        // offset + length stays in range, but offset + runs_offset overflows.
697        let attr = Attribute {
698            type_code: forensicnomicon::ntfs::attr_types::DATA,
699            length: 0x48,
700            non_resident: true,
701            name: None,
702            flags: 0,
703            attribute_id: 0,
704            offset: usize::MAX - 0x48,
705            body: AttributeBody::NonResident {
706                start_vcn: 0,
707                last_vcn: 0,
708                runs_offset: 0x49,
709                compression_unit: 0,
710                allocated_size: 512,
711                real_size: 512,
712                initialized_size: 512,
713            },
714        };
715        let record = vec![0u8; 1];
716        let mut vol = volume(1, 512);
717        assert!(matches!(
718            read_attribute_value(&mut vol, &record, &attr, 512),
719            Err(NtfsError::BadAttribute { detail, .. }) if detail == "runs offset overflow"
720        ));
721    }
722
723    #[test]
724    fn attribute_runlist_rejects_resident_attribute() {
725        let attr = Attribute {
726            type_code: forensicnomicon::ntfs::attr_types::DATA,
727            length: 0x20,
728            non_resident: false,
729            name: None,
730            flags: 0,
731            attribute_id: 0,
732            offset: 0,
733            body: AttributeBody::Resident {
734                content_offset: 0x18,
735                content_length: 4,
736            },
737        };
738        assert!(matches!(
739            attribute_runlist(&[0u8; 0x20], &attr),
740            Err(NtfsError::BadAttribute { detail, .. }) if detail.contains("resident")
741        ));
742    }
743}