Skip to main content

lzip_parallel/
chunk.rs

1//! Chunk-level parallel DEFLATE decoder.
2//!
3//! Mirrors lbzip2-rs chunk.rs: the caller reads up to 200 MB of compressed data
4//! into a ring-buffer slot, prepends any carry from the previous slot, then calls
5//! `split_chunk` to find flush boundaries and `decode_chunk` to decode all segments
6//! in parallel.
7//!
8//! Full-flush boundaries (`00 00 FF FF`) guarantee no LZ77 back-reference crosses
9//! a segment boundary, so each segment is independently decompressible.
10//!
11//! Segments are decompressed using our custom DEFLATE decoder (`inflate_segment`)
12//! which handles non-final segments (no BFINAL=1 requirement). Falls back to
13//! miniz_oxide for robustness.
14
15use rayon::prelude::*;
16
17use crate::deflate_scan::{self, FlushBoundary};
18use crate::entry::ZipError;
19
20/// Result of splitting a chunk into independently-decompressible segments.
21pub struct ChunkSplit {
22    /// Byte offsets of segment starts (first entry is always 0).
23    pub segment_starts: Vec<usize>,
24    /// Number of segments to decode (all if `is_last`, else n−1).
25    pub decode_segments: usize,
26    /// Bytes consumed from the chunk (for carry computation).
27    pub consumed: usize,
28}
29
30/// Split a chunk of compressed DEFLATE data into segment boundaries.
31///
32/// Uses `split_boundaries_parallel` with `n_workers` splits (no oversplit —
33/// one segment per physical core, mirroring lbzip2-rs binary behaviour).
34///
35/// Returns `None` if no full-flush boundary is found (caller falls back to
36/// single-core decompression for this chunk).
37pub fn split_chunk(data: &[u8], n_workers: usize, is_last: bool) -> Option<ChunkSplit> {
38    let boundaries: Vec<FlushBoundary> =
39        deflate_scan::split_boundaries_parallel(data, n_workers);
40
41    if boundaries.is_empty() {
42        return None;
43    }
44
45    // Build deduplicated segment start list: always start at 0.
46    let mut segment_starts = Vec::with_capacity(boundaries.len() + 1);
47    segment_starts.push(0usize);
48    for b in &boundaries {
49        if *segment_starts.last().unwrap() != b.start {
50            segment_starts.push(b.start);
51        }
52    }
53
54    let n = segment_starts.len();
55    let decode_segments = if is_last {
56        n
57    } else if n > 1 {
58        n - 1
59    } else {
60        return None;
61    };
62
63    let consumed = if decode_segments < n {
64        segment_starts[decode_segments]
65    } else {
66        data.len()
67    };
68
69    Some(ChunkSplit { segment_starts, decode_segments, consumed })
70}
71
72/// Decompress one segment of DEFLATE data that may NOT end on BFINAL=1.
73///
74/// Uses linflate (shared fast SIMD inflate) for non-final segments.
75///
76/// Writes directly into the tail of `output` (zero per-segment allocation
77/// beyond the output vector itself).
78pub fn decode_segment_into(
79    compressed: &[u8],
80    output: &mut Vec<u8>,
81) -> Result<(), ZipError> {
82    let headroom = linflate::OVERWRITE_HEADROOM;
83    let out_start = output.len();
84    let mut buf_size = (compressed.len() * 4).max(4096) + headroom;
85
86    loop {
87        output.reserve(buf_size);
88        let spare_len = output.capacity() - out_start;
89        let out_buf = unsafe {
90            std::slice::from_raw_parts_mut(output.as_mut_ptr().add(out_start), spare_len)
91        };
92
93        match linflate::inflate_segment(compressed, out_buf) {
94            Ok(written) => {
95                unsafe { output.set_len(out_start + written) };
96                return Ok(());
97            }
98            Err(linflate::InflateError::OutputOverflow) => {
99                buf_size = buf_size.saturating_mul(2);
100                if buf_size > 4 * 1024 * 1024 * 1024 {
101                    return Err(ZipError("DEFLATE segment too large"));
102                }
103            }
104            Err(_) => return Err(ZipError("DEFLATE segment decompression failed")),
105        }
106    }
107}
108
109/// Decompress one segment and return the output as an owned `Vec<u8>`.
110pub fn decode_segment(compressed: &[u8]) -> Result<Vec<u8>, ZipError> {
111    let mut out = Vec::new();
112    decode_segment_into(compressed, &mut out)?;
113    Ok(out)
114}
115
116/// Decode a full chunk in parallel.
117///
118/// 1. Split the chunk at full-flush boundaries (N−1 rayon tasks).
119/// 2. If no boundaries found, fall back: returns `Err` with a sentinel so the
120///    caller can decompress the chunk single-core.
121/// 3. Otherwise decode `decode_segments` segments in parallel via rayon.
122///
123/// Returns `(segment_outputs, consumed_bytes)`.
124/// `consumed_bytes` is the byte offset up to which the chunk was decoded —
125/// bytes from `consumed_bytes..` must be carried into the next slot.
126pub fn decode_chunk(
127    data: &[u8],
128    n_workers: usize,
129    is_last: bool,
130) -> Result<(Vec<Vec<u8>>, usize), ZipError> {
131    let split = split_chunk(data, n_workers, is_last)
132        .ok_or(ZipError("no full-flush boundaries — fallback to single-core"))?;
133
134    let segments: Vec<&[u8]> = (0..split.decode_segments)
135        .map(|i| {
136            let start = split.segment_starts[i];
137            let end = if i + 1 < split.segment_starts.len() {
138                split.segment_starts[i + 1]
139            } else {
140                split.consumed
141            };
142            &data[start..end]
143        })
144        .collect();
145
146    let outputs: Vec<Result<Vec<u8>, ZipError>> = crate::thread_pool().install(|| {
147        segments.into_par_iter().map(|seg| decode_segment(seg)).collect()
148    });
149
150    let mut result = Vec::with_capacity(outputs.len());
151    for r in outputs {
152        result.push(r?);
153    }
154
155    Ok((result, split.consumed))
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    /// Build a valid stored DEFLATE stream with Z_FULL_FLUSH after `payload`.
163    ///
164    /// Layout: non-final stored block(s) carrying `payload`, then flush marker.
165    fn stored_block(payload: &[u8], bfinal: bool) -> Vec<u8> {
166        // Stored block: BFINAL | BTYPE=00 | pad bits | LEN | NLEN | data
167        let mut out = Vec::new();
168        let header: u8 = if bfinal { 0x01 } else { 0x00 };
169        out.push(header);
170        let len = payload.len() as u16;
171        out.extend_from_slice(&len.to_le_bytes());
172        out.extend_from_slice(&(!len).to_le_bytes());
173        out.extend_from_slice(payload);
174        out
175    }
176
177    fn flush_marker() -> Vec<u8> {
178        // Z_FULL_FLUSH: stored block header + zero-length LEN/NLEN
179        vec![0x00, 0x00, 0x00, 0xFF, 0xFF]
180    }
181
182    // ── split_chunk ──────────────────────────────────────────────────────────
183
184    #[test]
185    fn split_chunk_no_boundaries_returns_none() {
186        // Plain bytes with no 00 00 FF FF pattern — split_chunk returns None.
187        let data: Vec<u8> = (0u8..=255).cycle().take(128).collect();
188        assert!(split_chunk(&data, 4, true).is_none());
189    }
190
191    #[test]
192    fn split_chunk_single_boundary_is_last() {
193        // Flush boundary must appear AFTER buf.len()/2 so the forward scan
194        // from the nominal split point (midpoint) reaches it.
195        //
196        // Layout: 20-byte non-final stored block | 5-byte flush marker | 10-byte final stored block
197        //   Total = 35, midpoint = 17, pattern `00 00 FF FF` at position 21 > 17 ✓
198        let payload_a = vec![0xAA_u8; 15];
199        let mut buf = stored_block(&payload_a, false); // 20 bytes
200        buf.extend_from_slice(&flush_marker());         // 5 bytes
201        buf.extend_from_slice(&stored_block(b"world", true)); // 10 bytes
202
203        let split = split_chunk(&buf, 2, true).expect("should find boundary");
204        assert!(split.decode_segments >= 1);
205        assert_eq!(split.consumed, buf.len());
206    }
207
208    #[test]
209    fn split_chunk_single_boundary_not_last() {
210        // With is_last=false and flush boundary in the first half (small buffer,
211        // n_splits=2 nominal=midpoint > boundary position), split_chunk may
212        // return None — that is acceptable (caller falls back to single-core).
213        let mut buf = stored_block(b"hello", false);
214        buf.extend_from_slice(&flush_marker());
215        buf.extend_from_slice(&stored_block(b"world", false));
216
217        let result = split_chunk(&buf, 2, false);
218        if let Some(s) = result {
219            assert!(s.consumed <= buf.len());
220            assert!(s.decode_segments >= 1);
221        }
222        // None is also acceptable — caller falls back to single-core.
223    }
224
225    // ── decode_segment ───────────────────────────────────────────────────────
226
227    #[test]
228    fn decode_segment_stored_final() {
229        let data = stored_block(b"Hello, World!\n", true);
230        let out = decode_segment(&data).expect("decode stored block");
231        assert_eq!(out, b"Hello, World!\n");
232    }
233
234    #[test]
235    fn decode_segment_stored_non_final() {
236        // A non-final stored block (BFINAL=0) — decompress_to_vec would fail.
237        let data = stored_block(b"partial segment", false);
238        let out = decode_segment(&data).expect("decode non-final stored block");
239        assert_eq!(out, b"partial segment");
240    }
241
242    #[test]
243    fn decode_segment_real_deflate() {
244        // Compress with miniz_oxide and decompress a final segment.
245        use miniz_oxide::deflate::compress_to_vec;
246        let original = b"The quick brown fox jumps over the lazy dog".repeat(50);
247        let compressed = compress_to_vec(&original, 6);
248        let out = decode_segment(&compressed).expect("decode real deflate");
249        assert_eq!(out, original.to_vec());
250    }
251
252    // ── decode_chunk ─────────────────────────────────────────────────────────
253
254    #[test]
255    fn decode_chunk_no_boundaries_returns_err() {
256        // Data with no flush marker → fallback error.
257        let data = stored_block(b"no flush here", true);
258        assert!(decode_chunk(&data, 4, true).is_err());
259    }
260
261    #[test]
262    fn decode_chunk_two_segments() {
263        // Flush boundary must be past midpoint so parallel scan finds it.
264        //
265        // Layout: 27-byte non-final block | 5-byte flush | 10-byte final block
266        //   Total = 42, midpoint = 21, pattern at position 28 > 21 ✓
267        let payload_a = vec![0xAA_u8; 22]; // stored block = 27 bytes
268        let payload_b = vec![0xBB_u8; 5];  // stored block = 10 bytes
269        let mut buf = stored_block(&payload_a, false);
270        buf.extend_from_slice(&flush_marker());
271        buf.extend_from_slice(&stored_block(&payload_b, true));
272
273        let (outputs, consumed) = decode_chunk(&buf, 2, true).expect("decode two segments");
274        assert_eq!(consumed, buf.len());
275        let combined: Vec<u8> = outputs.into_iter().flatten().collect();
276        let expected: Vec<u8> = payload_a.iter().chain(payload_b.iter()).copied().collect();
277        assert_eq!(combined, expected);
278    }
279}