Skip to main content

archive_core/
reassemble.rs

1//! Phase 4 of two-phase archive access (ADR 0008): **`SegmentSet` reassembly**.
2//! A multi-segment image split across archive members (`.E01/.E02/.E03`, split
3//! raw `.001/.002`, split VMDK `-s001/-s002`) is presented as ONE logical
4//! seekable source, built from the per-segment [`PeeledSource`] handles the
5//! phase-2/3 executor produces for each member's own [`crate::Access`].
6//!
7//! Two reassembly regimes, chosen by [`crate::SegmentKind`]:
8//!
9//! - [`SegmentKind::SplitRaw`] (`.001/.002/.003`, a raw dd split) — the segments
10//!   ARE a byte-for-byte concatenation, so [`ConcatSource`] stitches the ordered
11//!   per-segment handles into one `Read + Seek`: logical offset `O` maps to
12//!   `(segment k, local offset)`, and `seek`/`read` dispatch to the owning
13//!   segment. Fully reassembled in archive-core.
14//! - [`SegmentKind::Ewf`] / [`SegmentKind::SplitVmdk`] — the segments are NOT a
15//!   raw concatenation: each `.E0N` carries its own EWF header/section structure,
16//!   and split-VMDK extents are described by the descriptor. Stitching needs the
17//!   container reader's multi-segment logic, so archive-core's job here is to hand
18//!   back the **ordered per-segment byte sources** via [`segment_sources`]; the
19//!   container reader consumes them through its own segment backing.
20//!
21//! ## The EWF `SegmentBacking` seam
22//!
23//! The `ewf` crate accepts explicit segment backings through
24//! `EwfReader::open_from_sources(Vec<ewf::SegmentSource>)`, where
25//! `ewf::SegmentSource` is a closed enum of `File` (a loose segment file), `Sub`
26//! (a `[base, base+len)` sub-range of a shared **on-disk** `File`), and `Mem`
27//! (an in-RAM `Arc<[u8]>`). archive-core's [`PeeledSource`]s are in-RAM
28//! `Read + Seek` handles (a zero-copy [`SubRange`](crate::SubRange) window, a
29//! zran checkpoint index, or a temp spill) — not `File`s — so the only lossless
30//! adapter available today is materializing each segment to an `Arc<[u8]>` and
31//! handing `ewf::SegmentSource::Mem`, which defeats the zero-spill zran path for
32//! a Deflate-compressed E01-in-zip set.
33//!
34//! A *clean*, zero-copy wiring therefore needs an `ewf`-side change: a
35//! `SegmentSource` variant that accepts an arbitrary boxed positioned reader
36//! (`read_at` + `len`), so a zran-backed or windowed source flows in without a
37//! full inflate. That is a cross-repo change to `ewf`; per the phase plan
38//! archive-core STOPS at exposing the ordered [`PeeledSource`]s here, and the
39//! adapter into a container reader lives in the consumer (disk-forensic /
40//! forensic-vfs-engine), never inside this leaf (which would invert the layer
41//! direction — the archive layer must not depend on a CONTAINER crate).
42
43use std::io::{self, Read, Seek, SeekFrom};
44use std::sync::Arc;
45
46use crate::detect::Format;
47use crate::error::Result;
48use crate::execute::{execute_member_access, offset_from, PeeledSource};
49use crate::plan::{detect, AccessPlan, SegmentKind};
50use crate::resolve::Limits;
51
52/// The ordered per-segment byte sources of a segmented image, each materialized
53/// via its own [`crate::Access`] (phase-2/3 executor) and ordered by segment
54/// number. For [`SegmentKind::SplitRaw`] these concatenate ([`ConcatSource`]);
55/// for [`SegmentKind::Ewf`] / [`SegmentKind::SplitVmdk`] they feed a container
56/// reader's multi-segment backing (the `SegmentBacking` seam above).
57#[derive(Debug)]
58#[non_exhaustive]
59pub struct SegmentSources {
60    /// The archive format the segments live in.
61    pub format: Format,
62    /// How the segments reassemble.
63    pub kind: SegmentKind,
64    /// The per-segment seekable handles, ordered by segment number.
65    pub sources: Vec<PeeledSource>,
66}
67
68/// One logical, seekable image reassembled by **concatenating** the ordered
69/// per-segment handles of a raw split set (`.001/.002/.003`). A logical offset
70/// maps to `(segment, local offset)`; a single `read` never crosses a segment
71/// boundary (the caller's `read_exact` loops across it), and `seek` is
72/// bounded — a position past the end clamps to the total length.
73#[derive(Debug)]
74pub struct ConcatSource {
75    segs: Vec<ConcatSeg>,
76    total: u64,
77    pos: u64,
78}
79
80/// One segment inside a [`ConcatSource`]: its handle plus its logical window.
81#[derive(Debug)]
82struct ConcatSeg {
83    source: PeeledSource,
84    /// Logical start offset of this segment in the reassembled image.
85    start: u64,
86    /// Length of this segment in bytes.
87    len: u64,
88}
89
90impl ConcatSource {
91    /// Stitch `sources` (ordered by segment number) into one logical image, each
92    /// segment placed at its running logical offset.
93    pub(crate) fn new(sources: Vec<PeeledSource>) -> Self {
94        let mut segs = Vec::with_capacity(sources.len());
95        let mut start = 0u64;
96        for source in sources {
97            let len = source.len();
98            segs.push(ConcatSeg { source, start, len });
99            start = start.saturating_add(len);
100        }
101        ConcatSource {
102            segs,
103            total: start,
104            pos: 0,
105        }
106    }
107
108    /// The reassembled image's total length, in bytes.
109    #[must_use]
110    pub fn len(&self) -> u64 {
111        self.total
112    }
113
114    /// Whether the reassembled image is empty.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.total == 0
118    }
119
120    /// Index of the segment containing logical `pos`, or `None` at/after the end.
121    /// Contiguous, ordered segments make this the first segment whose end exceeds
122    /// `pos` — a zero-length segment (`start == end`) is never selected.
123    fn seg_at(&self, pos: u64) -> Option<usize> {
124        let idx = self.segs.partition_point(|s| s.start + s.len <= pos);
125        (idx < self.segs.len()).then_some(idx)
126    }
127}
128
129impl Read for ConcatSource {
130    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
131        if buf.is_empty() || self.pos >= self.total {
132            return Ok(0);
133        }
134        let pos = self.pos;
135        let Some(i) = self.seg_at(pos) else {
136            return Ok(0); // cov:unreachable: pos < total ⇒ a segment contains it
137        };
138        let seg = &mut self.segs[i];
139        let local = pos - seg.start;
140        seg.source.seek(SeekFrom::Start(local))?;
141        // Cap the read at this segment's boundary so one `read` never crosses
142        // into the next segment; a caller's `read_exact` loops across it.
143        let avail = seg.len - local;
144        let want = (buf.len() as u64).min(avail) as usize;
145        let n = seg.source.read(&mut buf[..want])?;
146        self.pos = self.pos.saturating_add(n as u64);
147        Ok(n)
148    }
149}
150
151impl Seek for ConcatSource {
152    fn seek(&mut self, from: SeekFrom) -> io::Result<u64> {
153        let target = match from {
154            SeekFrom::Start(o) => o,
155            SeekFrom::End(o) => offset_from(self.total, o)?,
156            SeekFrom::Current(o) => offset_from(self.pos, o)?,
157        };
158        // A reassembled image is bounded: clamp so reads stop at its end.
159        self.pos = target.min(self.total);
160        Ok(self.pos)
161    }
162}
163
164/// The outcome of reassembling a segmented image (or the finding that the input
165/// is not a segment set).
166#[derive(Debug)]
167#[non_exhaustive]
168pub enum Reassembled {
169    /// A raw split set (`.001/.002/.003`) stitched into one logical `Read + Seek`.
170    Concat(ConcatSource),
171    /// An EWF / split-VMDK set: the ordered per-segment sources for a container
172    /// reader's multi-segment backing (archive-core does not stitch these).
173    Segments(SegmentSources),
174    /// The input is not a segment set (a single member, wrapper, collection, or
175    /// raw stream) — nothing to reassemble.
176    NotSegmented,
177}
178
179/// Produce the ordered per-segment [`PeeledSource`] handles for a segmented
180/// image, or `Ok(None)` when `data` is not a segment set. Each segment is
181/// materialized via its own [`crate::Access`] (in-place window / zran index /
182/// temp spill), in segment-number order — the primitive both [`reassemble`] and
183/// a container reader's `SegmentBacking` build on.
184///
185/// # Errors
186/// Propagates a detect/executor failure ([`crate::ArchiveError`]).
187pub fn segment_sources(data: Vec<u8>, limits: &Limits) -> Result<Option<SegmentSources>> {
188    let AccessPlan::SegmentSet {
189        format,
190        members,
191        kind,
192    } = detect(&data)?
193    else {
194        return Ok(None);
195    };
196    // Share the archive bytes across every segment window (no per-segment clone).
197    let data = Arc::new(data);
198    let mut sources = Vec::with_capacity(members.len());
199    for seg in &members {
200        sources.push(execute_member_access(
201            format,
202            &data,
203            seg.index,
204            &seg.name,
205            &seg.access,
206            limits,
207        )?);
208    }
209    Ok(Some(SegmentSources {
210        format,
211        kind,
212        sources,
213    }))
214}
215
216/// Reassemble a segmented image: a [`SegmentKind::SplitRaw`] set becomes a
217/// stitched [`ConcatSource`]; an [`SegmentKind::Ewf`] / [`SegmentKind::SplitVmdk`]
218/// set yields its ordered [`SegmentSources`] for a container reader's backing;
219/// anything else is [`Reassembled::NotSegmented`].
220///
221/// # Errors
222/// Propagates a detect/executor failure ([`crate::ArchiveError`]).
223pub fn reassemble(data: Vec<u8>, limits: &Limits) -> Result<Reassembled> {
224    let Some(ss) = segment_sources(data, limits)? else {
225        return Ok(Reassembled::NotSegmented);
226    };
227    Ok(match ss.kind {
228        // A raw split IS a byte-for-byte concatenation — stitch it here.
229        SegmentKind::SplitRaw => Reassembled::Concat(ConcatSource::new(ss.sources)),
230        // EWF / split-VMDK need the container reader's multi-segment logic;
231        // hand back the ordered sources for its backing (the seam above).
232        SegmentKind::Ewf | SegmentKind::SplitVmdk => Reassembled::Segments(ss),
233    })
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    /// A deterministic, non-repeating byte pattern of length `n` seeded by
241    /// `seed`, so a wrong segment/offset yields visibly wrong bytes.
242    fn pattern(n: usize, seed: usize) -> Vec<u8> {
243        (0..n).map(|i| ((i + seed) % 251) as u8).collect()
244    }
245
246    /// Hand-assemble a multi-member ZIP whose members are each STORED (method 0),
247    /// so every member's [`crate::Access`] is `InPlace` (a zero-copy window). The
248    /// members appear in the given order — pass them out of segment order to
249    /// prove reassembly reorders by segment number.
250    fn stored_zip(members: &[(&str, &[u8])]) -> Vec<u8> {
251        let mut z = Vec::new();
252        let mut central = Vec::new();
253        let mut count = 0u16;
254        for (name, payload) in members {
255            let nb = name.as_bytes();
256            let mut crc = flate2::Crc::new();
257            crc.update(payload);
258            let crc = crc.sum();
259            let lho = z.len() as u32;
260            let (sz, nlen) = (payload.len() as u32, nb.len() as u16);
261            // Local file header (method 0 = stored).
262            z.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
263            z.extend_from_slice(&20u16.to_le_bytes()); // version needed
264            z.extend_from_slice(&0u16.to_le_bytes()); // flags
265            z.extend_from_slice(&0u16.to_le_bytes()); // method: stored
266            z.extend_from_slice(&0u16.to_le_bytes()); // mod time
267            z.extend_from_slice(&0u16.to_le_bytes()); // mod date
268            z.extend_from_slice(&crc.to_le_bytes());
269            z.extend_from_slice(&sz.to_le_bytes()); // compressed size
270            z.extend_from_slice(&sz.to_le_bytes()); // uncompressed size
271            z.extend_from_slice(&nlen.to_le_bytes());
272            z.extend_from_slice(&0u16.to_le_bytes()); // extra len
273            z.extend_from_slice(nb);
274            z.extend_from_slice(payload);
275            // Central directory header.
276            central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
277            central.extend_from_slice(&20u16.to_le_bytes()); // version made by
278            central.extend_from_slice(&20u16.to_le_bytes()); // version needed
279            central.extend_from_slice(&0u16.to_le_bytes()); // flags
280            central.extend_from_slice(&0u16.to_le_bytes()); // method
281            central.extend_from_slice(&0u16.to_le_bytes()); // mod time
282            central.extend_from_slice(&0u16.to_le_bytes()); // mod date
283            central.extend_from_slice(&crc.to_le_bytes());
284            central.extend_from_slice(&sz.to_le_bytes());
285            central.extend_from_slice(&sz.to_le_bytes());
286            central.extend_from_slice(&nlen.to_le_bytes());
287            central.extend_from_slice(&0u16.to_le_bytes()); // extra len
288            central.extend_from_slice(&0u16.to_le_bytes()); // comment len
289            central.extend_from_slice(&0u16.to_le_bytes()); // disk number start
290            central.extend_from_slice(&0u16.to_le_bytes()); // internal attrs
291            central.extend_from_slice(&0u32.to_le_bytes()); // external attrs
292            central.extend_from_slice(&lho.to_le_bytes()); // local header offset
293            central.extend_from_slice(nb);
294            count += 1;
295        }
296        let cd_offset = z.len() as u32;
297        let cd_size = central.len() as u32;
298        z.extend_from_slice(&central);
299        // End of central directory.
300        z.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
301        z.extend_from_slice(&0u16.to_le_bytes()); // disk num
302        z.extend_from_slice(&0u16.to_le_bytes()); // disk with cd
303        z.extend_from_slice(&count.to_le_bytes()); // entries this disk
304        z.extend_from_slice(&count.to_le_bytes()); // total entries
305        z.extend_from_slice(&cd_size.to_le_bytes());
306        z.extend_from_slice(&cd_offset.to_le_bytes());
307        z.extend_from_slice(&0u16.to_le_bytes()); // comment len
308        z
309    }
310
311    const FX: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../tests/data/fixtures/");
312
313    fn load(name: &str) -> Vec<u8> {
314        std::fs::read(format!("{FX}{name}")).unwrap()
315    }
316
317    // (a) A raw split set (`.001/.002/.003`), stored OUT OF ORDER in the zip, is
318    // reassembled by concatenation in segment-number order; random reads across
319    // the segment boundaries return the correct logical bytes, and a full read
320    // reproduces `p1 ++ p2 ++ p3`.
321    #[test]
322    fn split_raw_concatenates_in_order_with_random_access() {
323        let p1 = pattern(1000, 0);
324        let p2 = pattern(1500, 100);
325        let p3 = pattern(777, 200);
326        // Out of order in the archive: 002, 003, 001.
327        let data = stored_zip(&[("img.002", &p2), ("img.003", &p3), ("img.001", &p1)]);
328        let Reassembled::Concat(mut c) = reassemble(data, &Limits::default()).unwrap() else {
329            panic!("expected a SplitRaw ConcatSource");
330        };
331        let total = (p1.len() + p2.len() + p3.len()) as u64;
332        assert_eq!(c.len(), total);
333        assert!(!c.is_empty());
334
335        // Full read reproduces the ordered concatenation.
336        let mut whole = Vec::new();
337        c.seek(SeekFrom::Start(0)).unwrap();
338        c.read_to_end(&mut whole).unwrap();
339        let mut expected = Vec::new();
340        expected.extend_from_slice(&p1);
341        expected.extend_from_slice(&p2);
342        expected.extend_from_slice(&p3);
343        assert_eq!(whole, expected);
344
345        // A read straddling the p1|p2 boundary (offset 1000).
346        c.seek(SeekFrom::Start(995)).unwrap();
347        let mut span = [0u8; 12];
348        c.read_exact(&mut span).unwrap();
349        assert_eq!(&span[..5], &p1[995..1000]);
350        assert_eq!(&span[5..], &p2[..7]);
351
352        // A read straddling the p2|p3 boundary (offset 2500).
353        c.seek(SeekFrom::Start(2495)).unwrap();
354        let mut span2 = [0u8; 10];
355        c.read_exact(&mut span2).unwrap();
356        assert_eq!(&span2[..5], &p2[p2.len() - 5..]);
357        assert_eq!(&span2[5..], &p3[..5]);
358
359        // End-relative seek reads the tail of the last segment.
360        c.seek(SeekFrom::End(-4)).unwrap();
361        let mut tail = [0u8; 4];
362        c.read_exact(&mut tail).unwrap();
363        assert_eq!(tail, p3[p3.len() - 4..]);
364
365        // A seek past the end clamps to the total length.
366        assert_eq!(c.seek(SeekFrom::Start(u64::MAX)).unwrap(), total);
367        assert_eq!(c.read(&mut [0u8; 8]).unwrap(), 0);
368    }
369
370    // (b) Each segment of a raw split uses its OWN access — a stored member is a
371    // zero-copy `InPlace` window, never a copy — and `segment_sources` orders
372    // them by segment number, each reading the exact segment bytes.
373    #[test]
374    fn split_raw_segment_sources_are_inplace_and_ordered() {
375        let p1 = pattern(300, 1);
376        let p2 = pattern(400, 2);
377        let data = stored_zip(&[("d.002", &p2), ("d.001", &p1)]);
378        let ss = segment_sources(data, &Limits::default())
379            .unwrap()
380            .expect("a segment set");
381        assert_eq!(ss.kind, SegmentKind::SplitRaw);
382        assert_eq!(ss.sources.len(), 2);
383        let expected = [p1, p2];
384        for (src, want) in ss.sources.into_iter().zip(expected) {
385            assert!(
386                matches!(src, PeeledSource::InPlace(_)),
387                "a stored segment is a zero-copy InPlace window"
388            );
389            let mut got = Vec::new();
390            let mut src = src;
391            src.read_to_end(&mut got).unwrap();
392            assert_eq!(got, want, "segment bytes read in order");
393        }
394    }
395
396    // (c) An EWF `.E0N` set: archive-core hands back the ORDERED per-segment
397    // sources (the `SegmentBacking` seam) — E01, E02, E03 — each reading the exact
398    // segment bytes. Uses the committed `seg_ewf.zip` (members stored out of order).
399    #[test]
400    fn ewf_segment_sources_are_ordered_per_segment_handles() {
401        let data = load("seg_ewf.zip");
402        // Oracle: the ordered member bytes, straight from the archive reader.
403        let mut a = crate::Archive::open(&data, Some("seg_ewf.zip"))
404            .unwrap()
405            .unwrap();
406        let order = ["img.E01", "img.E02", "img.E03"];
407        let oracle: Vec<Vec<u8>> = order
408            .iter()
409            .map(|n| {
410                let idx = a.entries().iter().position(|e| e.name == *n).unwrap();
411                a.read(idx).unwrap()
412            })
413            .collect();
414
415        let ss = segment_sources(data, &Limits::default())
416            .unwrap()
417            .expect("a segment set");
418        assert_eq!(ss.kind, SegmentKind::Ewf);
419        assert_eq!(ss.sources.len(), 3);
420        for (src, want) in ss.sources.into_iter().zip(oracle) {
421            let mut got = Vec::new();
422            let mut src = src;
423            src.read_to_end(&mut got).unwrap();
424            assert_eq!(got, want, "ordered E0N segment bytes");
425        }
426
427        // `reassemble` routes an EWF set to the Segments seam, not a ConcatSource.
428        let data = load("seg_ewf.zip");
429        match reassemble(data, &Limits::default()).unwrap() {
430            Reassembled::Segments(s) => {
431                assert_eq!(s.kind, SegmentKind::Ewf);
432                assert_eq!(s.sources.len(), 3);
433            }
434            other => panic!("expected Segments seam, got {other:?}"),
435        }
436    }
437
438    // A non-segmented input (a single stored member) is `NotSegmented`, and
439    // `segment_sources` returns `None`.
440    #[test]
441    fn single_member_is_not_segmented() {
442        let data = load("stored_one.zip");
443        assert!(matches!(
444            reassemble(data.clone(), &Limits::default()).unwrap(),
445            Reassembled::NotSegmented
446        ));
447        assert!(segment_sources(data, &Limits::default()).unwrap().is_none());
448    }
449}