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