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    /// Open with an exact-name filter — only entries whose name is in `names` are decompressed.
87    ///
88    /// Non-matching entries are excluded at the central directory level.
89    pub fn with_filter_set(mut source: R, names: &[&str]) -> Result<Self, ZipError> {
90        use std::collections::HashSet;
91        let name_set: HashSet<&str> = names.iter().copied().collect();
92        let all_locs = central_dir::read_central_directory_from(&mut source)?;
93        let mut locations: Vec<_> = all_locs.into_iter()
94            .filter(|l| !l.is_directory && name_set.contains(l.name.as_str()))
95            .collect();
96        locations.sort_by_key(|l| l.local_header_offset);
97        Ok(Self {
98            source,
99            locations,
100            next_entry: 0,
101            buf: VecDeque::new(),
102            error: false,
103        })
104    }
105
106    /// Open with a suffix filter — only entries whose name ends with one of `suffixes`.
107    ///
108    /// Non-matching entries are excluded at the central directory level.
109    pub fn with_filter_suffixes(mut source: R, suffixes: &[&str]) -> Result<Self, ZipError> {
110        let all_locs = central_dir::read_central_directory_from(&mut source)?;
111        let mut locations: Vec<_> = all_locs.into_iter()
112            .filter(|l| !l.is_directory && suffixes.iter().any(|s| l.name.ends_with(s)))
113            .collect();
114        locations.sort_by_key(|l| l.local_header_offset);
115        Ok(Self {
116            source,
117            locations,
118            next_entry: 0,
119            buf: VecDeque::new(),
120            error: false,
121        })
122    }
123
124    fn fill_batch(&mut self) -> Result<(), ZipError> {
125        let remaining = self.locations.len() - self.next_entry;
126        if remaining == 0 {
127            return Ok(());
128        }
129
130        let batch_end = self.next_entry + remaining.min(BATCH_SIZE);
131        let batch = &self.locations[self.next_entry..batch_end];
132
133        // ── Read compressed bytes sequentially (entries sorted by offset) ──
134        let mut raw: Vec<(String, Vec<u8>, u16, usize)> = Vec::with_capacity(batch.len());
135        for loc in batch {
136            self.source.seek(SeekFrom::Start(loc.local_header_offset))
137                .map_err(|_| ZipError("seek to local header failed"))?;
138
139            let mut hdr = [0u8; 30];
140            self.source.read_exact(&mut hdr)
141                .map_err(|_| ZipError("read local header failed"))?;
142
143            if u32::from_le_bytes(hdr[0..4].try_into().unwrap()) != LOCAL_HEADER_SIG {
144                return Err(ZipError("invalid local file header signature"));
145            }
146
147            // Name and extra lengths in the local header may differ from the CD.
148            let skip = u16::from_le_bytes([hdr[26], hdr[27]]) as i64
149                     + u16::from_le_bytes([hdr[28], hdr[29]]) as i64;
150            self.source.seek(SeekFrom::Current(skip))
151                .map_err(|_| ZipError("seek past local header name/extra failed"))?;
152
153            let mut compressed = vec![0u8; loc.compressed_size as usize];
154            self.source.read_exact(&mut compressed)
155                .map_err(|_| ZipError("read compressed data failed"))?;
156
157            raw.push((loc.name.clone(), compressed, loc.compression_method, loc.uncompressed_size as usize));
158        }
159
160        // ── Parallel decode ────────────────────────────────────────────────
161        let results: Vec<Result<ZipEntry, ZipError>> = crate::thread_pool().install(|| {
162            raw.par_iter().map(|(name, compressed, method, expected_size)| {
163                let data = entry::decompress_entry_raw(compressed, *method, *expected_size)?;
164                Ok(ZipEntry { name: name.clone(), data })
165            }).collect()
166        });
167
168        // ── Buffer results ─────────────────────────────────────────────────
169        for r in results {
170            self.buf.push_back(r?);
171        }
172        self.next_entry = batch_end;
173        Ok(())
174    }
175}
176
177impl<R: Read + Seek> Iterator for StreamingZipRead<R> {
178    type Item = Result<ZipEntry, ZipError>;
179
180    fn next(&mut self) -> Option<Self::Item> {
181        if self.error {
182            return None;
183        }
184        if let Some(entry) = self.buf.pop_front() {
185            return Some(Ok(entry));
186        }
187        if self.next_entry >= self.locations.len() {
188            return None;
189        }
190        match self.fill_batch() {
191            Err(e) => {
192                self.error = true;
193                Some(Err(e))
194            }
195            Ok(()) => self.buf.pop_front().map(Ok),
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203    use std::io::{Cursor, Write};
204
205    fn make_jar(items: &[(&str, &[u8])]) -> Vec<u8> {
206        use zip::write::SimpleFileOptions;
207        let mut buf = Vec::new();
208        let mut zw = zip::ZipWriter::new(Cursor::new(&mut buf));
209        let opts = SimpleFileOptions::default()
210            .compression_method(zip::CompressionMethod::Deflated);
211        for (name, data) in items {
212            zw.start_file(*name, opts).unwrap();
213            zw.write_all(data).unwrap();
214        }
215        zw.finish().unwrap();
216        buf
217    }
218
219    #[test]
220    fn streaming_single() {
221        let jar = make_jar(&[("Hello.class", b"cafebabe")]);
222        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
223            .unwrap()
224            .collect::<Result<Vec<_>, _>>()
225            .unwrap();
226        assert_eq!(entries.len(), 1);
227        assert_eq!(entries[0].name, "Hello.class");
228        assert_eq!(entries[0].data, b"cafebabe");
229    }
230
231    #[test]
232    fn streaming_multi() {
233        let jar = make_jar(&[("a.txt", b"aaa"), ("b.txt", b"bbb"), ("c.txt", b"ccc")]);
234        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
235            .unwrap()
236            .collect::<Result<Vec<_>, _>>()
237            .unwrap();
238        assert_eq!(entries.len(), 3);
239    }
240
241    #[test]
242    fn streaming_skips_directories() {
243        let jar = make_jar(&[("META-INF/", b""), ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n")]);
244        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
245            .unwrap()
246            .collect::<Result<Vec<_>, _>>()
247            .unwrap();
248        assert_eq!(entries.len(), 1);
249        assert_eq!(entries[0].name, "META-INF/MANIFEST.MF");
250    }
251
252    #[test]
253    fn streaming_many_batches() {
254        let items: Vec<(String, Vec<u8>)> = (0..200)
255            .map(|i| (format!("Class{i}.class"), format!("data{i}").into_bytes()))
256            .collect();
257        let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
258        let jar = make_jar(&item_refs);
259
260        let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
261            .unwrap()
262            .collect::<Result<Vec<_>, _>>()
263            .unwrap();
264        assert_eq!(entries.len(), 200);
265    }
266
267    #[test]
268    fn streaming_matches_parallel() {
269        let items: Vec<(String, Vec<u8>)> = (0..100)
270            .map(|i| (format!("{i}.txt"), format!("content {i}").into_bytes()))
271            .collect();
272        let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
273        let jar = make_jar(&item_refs);
274
275        let par = crate::parallel::decompress_parallel(&jar).unwrap();
276        let stream: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
277            .unwrap()
278            .collect::<Result<Vec<_>, _>>()
279            .unwrap();
280
281        // streaming returns entries in file-offset order (sorted); sort par to match
282        let mut par_sorted = par;
283        par_sorted.sort_by(|a, b| a.name.cmp(&b.name));
284        let mut stream_sorted = stream;
285        stream_sorted.sort_by(|a, b| a.name.cmp(&b.name));
286
287        assert_eq!(par_sorted.len(), stream_sorted.len());
288        for (p, s) in par_sorted.iter().zip(stream_sorted.iter()) {
289            assert_eq!(p.name, s.name);
290            assert_eq!(p.data, s.data);
291        }
292    }
293}