Skip to main content

lzip_parallel/
reader.rs

1//! Streaming parallel ZIP decompressor for large files.
2//!
3//! Pipeline mirrors lbzip2-rs's StreamingBz2Read:
4//!   seek to tail → parse Central Directory → sort entries by file offset →
5//!   read compressed bytes sequentially in batches of BATCH_SIZE →
6//!   parallel DEFLATE decode → yield ZipEntry items.
7//!
8//! Only one batch of decompressed output is live at a time, bounding peak
9//! memory regardless of ZIP size.  The file is never fully loaded into RAM.
10//!
11//! For in-memory byte slices use `parallel::decompress_parallel` instead.
12
13use std::collections::VecDeque;
14use std::io::{Read, Seek, SeekFrom};
15
16use rayon::prelude::*;
17
18use crate::central_dir;
19use crate::entry::{self, ZipEntry, ZipError};
20
21/// Entries decoded per parallel batch.
22const BATCH_SIZE: usize = 64;
23
24const LOCAL_HEADER_SIG: u32 = 0x04034b50;
25
26/// Streaming parallel ZIP decompressor.
27///
28/// Parses the Central Directory by seeking to the file tail, sorts entries by
29/// their file offset for sequential I/O, then decodes them in parallel batches.
30/// Implements `Iterator<Item = Result<ZipEntry, ZipError>>`.
31///
32/// ```no_run
33/// use lzip::reader::StreamingZipRead;
34/// let f = std::fs::File::open("archive.zip").unwrap();
35/// for entry in StreamingZipRead::new(f).unwrap() {
36///     let e = entry.unwrap();
37///     println!("{}: {} bytes", e.name, e.data.len());
38/// }
39/// ```
40pub struct StreamingZipRead<R: Read + Seek> {
41    source: R,
42    /// File entries sorted by local_header_offset for sequential disk reads.
43    locations: Vec<crate::central_dir::EntryLocation>,
44    next_entry: usize,
45    buf: VecDeque<ZipEntry>,
46    error: bool,
47}
48
49impl<R: Read + Seek> StreamingZipRead<R> {
50    /// Open a streaming decompressor over any `Read + Seek` source.
51    ///
52    /// Seeks to the file tail to parse the Central Directory; no full-file
53    /// read is performed here.
54    pub fn new(mut source: R) -> Result<Self, ZipError> {
55        let all_locs = central_dir::read_central_directory_from(&mut source)?;
56        let mut locations: Vec<_> = all_locs.into_iter().filter(|l| !l.is_directory).collect();
57        locations.sort_by_key(|l| l.local_header_offset);
58        Ok(Self {
59            source,
60            locations,
61            next_entry: 0,
62            buf: VecDeque::new(),
63            error: false,
64        })
65    }
66
67    /// Open with a name filter — only entries containing `needle` are decompressed.
68    ///
69    /// Non-matching entries are excluded at the central directory level,
70    /// never read from disk or inflated.
71    pub fn with_filter(mut source: R, needle: &str) -> Result<Self, ZipError> {
72        let all_locs = central_dir::read_central_directory_from(&mut source)?;
73        let mut locations: Vec<_> = all_locs.into_iter()
74            .filter(|l| !l.is_directory && l.name.contains(needle))
75            .collect();
76        locations.sort_by_key(|l| l.local_header_offset);
77        Ok(Self {
78            source,
79            locations,
80            next_entry: 0,
81            buf: VecDeque::new(),
82            error: false,
83        })
84    }
85
86    fn fill_batch(&mut self) -> Result<(), ZipError> {
87        let remaining = self.locations.len() - self.next_entry;
88        if remaining == 0 {
89            return Ok(());
90        }
91
92        let batch_end = self.next_entry + remaining.min(BATCH_SIZE);
93        let batch = &self.locations[self.next_entry..batch_end];
94
95        // ── Read compressed bytes sequentially (entries sorted by offset) ──
96        let mut raw: Vec<(String, Vec<u8>, u16, usize)> = Vec::with_capacity(batch.len());
97        for loc in batch {
98            self.source.seek(SeekFrom::Start(loc.local_header_offset))
99                .map_err(|_| ZipError("seek to local header failed"))?;
100
101            let mut hdr = [0u8; 30];
102            self.source.read_exact(&mut hdr)
103                .map_err(|_| ZipError("read local header failed"))?;
104
105            if u32::from_le_bytes(hdr[0..4].try_into().unwrap()) != LOCAL_HEADER_SIG {
106                return Err(ZipError("invalid local file header signature"));
107            }
108
109            // Name and extra lengths in the local header may differ from the CD.
110            let skip = u16::from_le_bytes([hdr[26], hdr[27]]) as i64
111                     + u16::from_le_bytes([hdr[28], hdr[29]]) as i64;
112            self.source.seek(SeekFrom::Current(skip))
113                .map_err(|_| ZipError("seek past local header name/extra failed"))?;
114
115            let mut compressed = vec![0u8; loc.compressed_size as usize];
116            self.source.read_exact(&mut compressed)
117                .map_err(|_| ZipError("read compressed data failed"))?;
118
119            raw.push((loc.name.clone(), compressed, loc.compression_method, loc.uncompressed_size as usize));
120        }
121
122        // ── Parallel decode ────────────────────────────────────────────────
123        let results: Vec<Result<ZipEntry, ZipError>> = crate::thread_pool().install(|| {
124            raw.par_iter().map(|(name, compressed, method, expected_size)| {
125                let data = entry::decompress_entry_raw(compressed, *method, *expected_size)?;
126                Ok(ZipEntry { name: name.clone(), data })
127            }).collect()
128        });
129
130        // ── Buffer results ─────────────────────────────────────────────────
131        for r in results {
132            self.buf.push_back(r?);
133        }
134        self.next_entry = batch_end;
135        Ok(())
136    }
137}
138
139impl<R: Read + Seek> Iterator for StreamingZipRead<R> {
140    type Item = Result<ZipEntry, ZipError>;
141
142    fn next(&mut self) -> Option<Self::Item> {
143        if self.error {
144            return None;
145        }
146        if let Some(entry) = self.buf.pop_front() {
147            return Some(Ok(entry));
148        }
149        if self.next_entry >= self.locations.len() {
150            return None;
151        }
152        match self.fill_batch() {
153            Err(e) => {
154                self.error = true;
155                Some(Err(e))
156            }
157            Ok(()) => self.buf.pop_front().map(Ok),
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165    use std::io::{Cursor, Write};
166
167    fn make_jar(items: &[(&str, &[u8])]) -> Vec<u8> {
168        use zip::write::SimpleFileOptions;
169        let mut buf = Vec::new();
170        let mut zw = zip::ZipWriter::new(Cursor::new(&mut buf));
171        let opts = SimpleFileOptions::default()
172            .compression_method(zip::CompressionMethod::Deflated);
173        for (name, data) in items {
174            zw.start_file(*name, opts).unwrap();
175            zw.write_all(data).unwrap();
176        }
177        zw.finish().unwrap();
178        buf
179    }
180
181    #[test]
182    fn streaming_single() {
183        let jar = make_jar(&[("Hello.class", b"cafebabe")]);
184        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
185            .unwrap()
186            .collect::<Result<Vec<_>, _>>()
187            .unwrap();
188        assert_eq!(entries.len(), 1);
189        assert_eq!(entries[0].name, "Hello.class");
190        assert_eq!(entries[0].data, b"cafebabe");
191    }
192
193    #[test]
194    fn streaming_multi() {
195        let jar = make_jar(&[("a.txt", b"aaa"), ("b.txt", b"bbb"), ("c.txt", b"ccc")]);
196        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
197            .unwrap()
198            .collect::<Result<Vec<_>, _>>()
199            .unwrap();
200        assert_eq!(entries.len(), 3);
201    }
202
203    #[test]
204    fn streaming_skips_directories() {
205        let jar = make_jar(&[("META-INF/", b""), ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n")]);
206        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
207            .unwrap()
208            .collect::<Result<Vec<_>, _>>()
209            .unwrap();
210        assert_eq!(entries.len(), 1);
211        assert_eq!(entries[0].name, "META-INF/MANIFEST.MF");
212    }
213
214    #[test]
215    fn streaming_many_batches() {
216        let items: Vec<(String, Vec<u8>)> = (0..200)
217            .map(|i| (format!("Class{i}.class"), format!("data{i}").into_bytes()))
218            .collect();
219        let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
220        let jar = make_jar(&item_refs);
221
222        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
223            .unwrap()
224            .collect::<Result<Vec<_>, _>>()
225            .unwrap();
226        assert_eq!(entries.len(), 200);
227    }
228
229    #[test]
230    fn streaming_matches_parallel() {
231        let items: Vec<(String, Vec<u8>)> = (0..100)
232            .map(|i| (format!("{i}.txt"), format!("content {i}").into_bytes()))
233            .collect();
234        let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
235        let jar = make_jar(&item_refs);
236
237        let par = crate::parallel::decompress_parallel(&jar).unwrap();
238        let stream: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
239            .unwrap()
240            .collect::<Result<Vec<_>, _>>()
241            .unwrap();
242
243        // streaming returns entries in file-offset order (sorted); sort par to match
244        let mut par_sorted = par;
245        par_sorted.sort_by(|a, b| a.name.cmp(&b.name));
246        let mut stream_sorted = stream;
247        stream_sorted.sort_by(|a, b| a.name.cmp(&b.name));
248
249        assert_eq!(par_sorted.len(), stream_sorted.len());
250        for (p, s) in par_sorted.iter().zip(stream_sorted.iter()) {
251            assert_eq!(p.name, s.name);
252            assert_eq!(p.data, s.data);
253        }
254    }
255}