1use 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
21const BATCH_SIZE: usize = 64;
23
24const LOCAL_HEADER_SIG: u32 = 0x04034b50;
25
26pub struct StreamingZipRead<R: Read + Seek> {
41 source: R,
42 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 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 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 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 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 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 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 len = loc.compressed_size as usize;
156 let mut compressed = Vec::with_capacity(len);
157 #[allow(clippy::uninit_vec)]
160 unsafe {
161 compressed.set_len(len);
162 }
163 self.source.read_exact(&mut compressed)
164 .map_err(|_| ZipError("read compressed data failed"))?;
165
166 raw.push((loc.name.clone(), compressed, loc.compression_method, loc.uncompressed_size as usize));
167 }
168
169 let results: Vec<Result<ZipEntry, ZipError>> = crate::thread_pool().install(|| {
173 raw.into_par_iter().map(|(name, compressed, method, expected_size)| {
174 let data = entry::decompress_entry_raw(&compressed, method, expected_size)?;
175 Ok(ZipEntry { name, data })
176 }).collect()
177 });
178
179 for r in results {
181 self.buf.push_back(r?);
182 }
183 self.next_entry = batch_end;
184 Ok(())
185 }
186}
187
188impl<R: Read + Seek> Iterator for StreamingZipRead<R> {
189 type Item = Result<ZipEntry, ZipError>;
190
191 fn next(&mut self) -> Option<Self::Item> {
192 if self.error {
193 return None;
194 }
195 if let Some(entry) = self.buf.pop_front() {
196 return Some(Ok(entry));
197 }
198 if self.next_entry >= self.locations.len() {
199 return None;
200 }
201 match self.fill_batch() {
202 Err(e) => {
203 self.error = true;
204 Some(Err(e))
205 }
206 Ok(()) => self.buf.pop_front().map(Ok),
207 }
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use std::io::{Cursor, Write};
215
216 fn make_jar(items: &[(&str, &[u8])]) -> Vec<u8> {
217 use zip::write::SimpleFileOptions;
218 let mut buf = Vec::new();
219 let mut zw = zip::ZipWriter::new(Cursor::new(&mut buf));
220 let opts = SimpleFileOptions::default()
221 .compression_method(zip::CompressionMethod::Deflated);
222 for (name, data) in items {
223 zw.start_file(*name, opts).unwrap();
224 zw.write_all(data).unwrap();
225 }
226 zw.finish().unwrap();
227 buf
228 }
229
230 #[test]
231 fn streaming_single() {
232 let jar = make_jar(&[("Hello.class", b"cafebabe")]);
233 let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
234 .unwrap()
235 .collect::<Result<Vec<_>, _>>()
236 .unwrap();
237 assert_eq!(entries.len(), 1);
238 assert_eq!(entries[0].name, "Hello.class");
239 assert_eq!(entries[0].data, b"cafebabe");
240 }
241
242 #[test]
243 fn streaming_multi() {
244 let jar = make_jar(&[("a.txt", b"aaa"), ("b.txt", b"bbb"), ("c.txt", b"ccc")]);
245 let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
246 .unwrap()
247 .collect::<Result<Vec<_>, _>>()
248 .unwrap();
249 assert_eq!(entries.len(), 3);
250 }
251
252 #[test]
253 fn streaming_skips_directories() {
254 let jar = make_jar(&[("META-INF/", b""), ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n")]);
255 let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
256 .unwrap()
257 .collect::<Result<Vec<_>, _>>()
258 .unwrap();
259 assert_eq!(entries.len(), 1);
260 assert_eq!(entries[0].name, "META-INF/MANIFEST.MF");
261 }
262
263 #[test]
264 fn streaming_many_batches() {
265 let items: Vec<(String, Vec<u8>)> = (0..200)
266 .map(|i| (format!("Class{i}.class"), format!("data{i}").into_bytes()))
267 .collect();
268 let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
269 let jar = make_jar(&item_refs);
270
271 let entries: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
272 .unwrap()
273 .collect::<Result<Vec<_>, _>>()
274 .unwrap();
275 assert_eq!(entries.len(), 200);
276 }
277
278 #[test]
279 fn streaming_matches_parallel() {
280 let items: Vec<(String, Vec<u8>)> = (0..100)
281 .map(|i| (format!("{i}.txt"), format!("content {i}").into_bytes()))
282 .collect();
283 let item_refs: Vec<(&str, &[u8])> = items.iter().map(|(n, d)| (n.as_str(), d.as_slice())).collect();
284 let jar = make_jar(&item_refs);
285
286 let par = crate::parallel::decompress_parallel(&jar).unwrap();
287 let stream: Vec<_> = StreamingZipRead::new(Cursor::new(&jar))
288 .unwrap()
289 .collect::<Result<Vec<_>, _>>()
290 .unwrap();
291
292 let mut par_sorted = par;
294 par_sorted.sort_by(|a, b| a.name.cmp(&b.name));
295 let mut stream_sorted = stream;
296 stream_sorted.sort_by(|a, b| a.name.cmp(&b.name));
297
298 assert_eq!(par_sorted.len(), stream_sorted.len());
299 for (p, s) in par_sorted.iter().zip(stream_sorted.iter()) {
300 assert_eq!(p.name, s.name);
301 assert_eq!(p.data, s.data);
302 }
303 }
304}