1use super::{
2    central_header_to_zip_file_inner, make_symlink, read_zipfile_from_stream, ZipCentralEntryBlock,
3    ZipFile, ZipFileData, ZipResult,
4};
5use crate::spec::FixedSizeBlock;
6use indexmap::IndexMap;
7use std::fs;
8use std::fs::create_dir_all;
9use std::io::{self, Read};
10use std::path::{Path, PathBuf};
11
12#[derive(Debug)]
14pub struct ZipStreamReader<R>(R);
15
16impl<R> ZipStreamReader<R> {
17    pub const fn new(reader: R) -> Self {
19        Self(reader)
20    }
21}
22
23impl<R: Read> ZipStreamReader<R> {
24    fn parse_central_directory(&mut self) -> ZipResult<ZipStreamFileMetadata> {
25        let archive_offset = 0;
28        let central_header_start = 0;
29
30        let block = ZipCentralEntryBlock::parse(&mut self.0)?;
32        let file = central_header_to_zip_file_inner(
33            &mut self.0,
34            archive_offset,
35            central_header_start,
36            block,
37        )?;
38        Ok(ZipStreamFileMetadata(file))
39    }
40
41    pub fn visit<V: ZipStreamVisitor>(mut self, visitor: &mut V) -> ZipResult<()> {
44        while let Some(mut file) = read_zipfile_from_stream(&mut self.0)? {
45            visitor.visit_file(&mut file)?;
46        }
47
48        while let Ok(metadata) = self.parse_central_directory() {
49            visitor.visit_additional_metadata(&metadata)?;
50        }
51
52        Ok(())
53    }
54
55    pub fn extract<P: AsRef<Path>>(self, directory: P) -> ZipResult<()> {
61        create_dir_all(&directory)?;
62        let directory = directory.as_ref().canonicalize()?;
63        struct Extractor(PathBuf, IndexMap<Box<str>, ()>);
64        impl ZipStreamVisitor for Extractor {
65            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
66                self.1.insert(file.name().into(), ());
67                let mut outpath = self.0.clone();
68                file.safe_prepare_path(&self.0, &mut outpath, None::<&(_, fn(&Path) -> bool)>)?;
69
70                if file.is_symlink() {
71                    let mut target = Vec::with_capacity(file.size() as usize);
72                    file.read_to_end(&mut target)?;
73                    make_symlink(&outpath, &target, &self.1)?;
74                    return Ok(());
75                }
76
77                if file.is_dir() {
78                    fs::create_dir_all(&outpath)?;
79                } else {
80                    let mut outfile = fs::File::create(&outpath)?;
81                    io::copy(file, &mut outfile)?;
82                }
83
84                Ok(())
85            }
86
87            #[allow(unused)]
88            fn visit_additional_metadata(
89                &mut self,
90                metadata: &ZipStreamFileMetadata,
91            ) -> ZipResult<()> {
92                #[cfg(unix)]
93                {
94                    use super::ZipError;
95                    let filepath = metadata
96                        .enclosed_name()
97                        .ok_or(ZipError::InvalidArchive("Invalid file path"))?;
98
99                    let outpath = self.0.join(filepath);
100
101                    use std::os::unix::fs::PermissionsExt;
102                    if let Some(mode) = metadata.unix_mode() {
103                        fs::set_permissions(outpath, fs::Permissions::from_mode(mode))?;
104                    }
105                }
106
107                Ok(())
108            }
109        }
110
111        self.visit(&mut Extractor(directory, IndexMap::new()))
112    }
113}
114
115pub trait ZipStreamVisitor {
117    fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()>;
123
124    fn visit_additional_metadata(&mut self, metadata: &ZipStreamFileMetadata) -> ZipResult<()>;
128}
129
130#[derive(Debug)]
132pub struct ZipStreamFileMetadata(ZipFileData);
133
134impl ZipStreamFileMetadata {
135    pub fn name(&self) -> &str {
148        &self.0.file_name
149    }
150
151    pub fn name_raw(&self) -> &[u8] {
155        &self.0.file_name_raw
156    }
157
158    pub fn mangled_name(&self) -> PathBuf {
169        self.0.file_name_sanitized()
170    }
171
172    pub fn enclosed_name(&self) -> Option<PathBuf> {
183        self.0.enclosed_name()
184    }
185
186    pub fn is_dir(&self) -> bool {
188        self.name()
189            .chars()
190            .next_back()
191            .is_some_and(|c| c == '/' || c == '\\')
192    }
193
194    pub fn is_file(&self) -> bool {
196        !self.is_dir()
197    }
198
199    pub fn comment(&self) -> &str {
201        &self.0.file_comment
202    }
203
204    pub const fn unix_mode(&self) -> Option<u32> {
206        self.0.unix_mode()
207    }
208}
209
210#[cfg(test)]
211mod test {
212    use tempfile::TempDir;
213
214    use super::*;
215    use crate::write::SimpleFileOptions;
216    use crate::ZipWriter;
217    use std::collections::BTreeSet;
218    use std::io::Cursor;
219
220    struct DummyVisitor;
221    impl ZipStreamVisitor for DummyVisitor {
222        fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
223            Ok(())
224        }
225
226        fn visit_additional_metadata(
227            &mut self,
228            _metadata: &ZipStreamFileMetadata,
229        ) -> ZipResult<()> {
230            Ok(())
231        }
232    }
233
234    #[allow(dead_code)]
235    #[derive(Default, Debug, Eq, PartialEq)]
236    struct CounterVisitor(u64, u64);
237    impl ZipStreamVisitor for CounterVisitor {
238        fn visit_file(&mut self, _file: &mut ZipFile<'_>) -> ZipResult<()> {
239            self.0 += 1;
240            Ok(())
241        }
242
243        fn visit_additional_metadata(
244            &mut self,
245            _metadata: &ZipStreamFileMetadata,
246        ) -> ZipResult<()> {
247            self.1 += 1;
248            Ok(())
249        }
250    }
251
252    #[test]
253    fn invalid_offset() {
254        ZipStreamReader::new(io::Cursor::new(include_bytes!(
255            "../../tests/data/invalid_offset.zip"
256        )))
257        .visit(&mut DummyVisitor)
258        .unwrap_err();
259    }
260
261    #[test]
262    fn invalid_offset2() {
263        ZipStreamReader::new(io::Cursor::new(include_bytes!(
264            "../../tests/data/invalid_offset2.zip"
265        )))
266        .visit(&mut DummyVisitor)
267        .unwrap_err();
268    }
269
270    #[test]
271    fn zip_read_streaming() {
272        let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
273            "../../tests/data/mimetype.zip"
274        )));
275
276        #[derive(Default)]
277        struct V {
278            filenames: BTreeSet<Box<str>>,
279        }
280        impl ZipStreamVisitor for V {
281            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
282                if file.is_file() {
283                    self.filenames.insert(file.name().into());
284                }
285
286                Ok(())
287            }
288            fn visit_additional_metadata(
289                &mut self,
290                metadata: &ZipStreamFileMetadata,
291            ) -> ZipResult<()> {
292                if metadata.is_file() {
293                    assert!(
294                        self.filenames.contains(metadata.name()),
295                        "{} is missing its file content",
296                        metadata.name()
297                    );
298                }
299
300                Ok(())
301            }
302        }
303
304        reader.visit(&mut V::default()).unwrap();
305    }
306
307    #[test]
308    fn file_and_dir_predicates() {
309        let reader = ZipStreamReader::new(io::Cursor::new(include_bytes!(
310            "../../tests/data/files_and_dirs.zip"
311        )));
312
313        #[derive(Default)]
314        struct V {
315            filenames: BTreeSet<Box<str>>,
316        }
317        impl ZipStreamVisitor for V {
318            fn visit_file(&mut self, file: &mut ZipFile<'_>) -> ZipResult<()> {
319                let full_name = file.enclosed_name().unwrap();
320                let file_name = full_name.file_name().unwrap().to_str().unwrap();
321                assert!(
322                    (file_name.starts_with("dir") && file.is_dir())
323                        || (file_name.starts_with("file") && file.is_file())
324                );
325
326                if file.is_file() {
327                    self.filenames.insert(file.name().into());
328                }
329
330                Ok(())
331            }
332            fn visit_additional_metadata(
333                &mut self,
334                metadata: &ZipStreamFileMetadata,
335            ) -> ZipResult<()> {
336                if metadata.is_file() {
337                    assert!(
338                        self.filenames.contains(metadata.name()),
339                        "{} is missing its file content",
340                        metadata.name()
341                    );
342                }
343
344                Ok(())
345            }
346        }
347
348        reader.visit(&mut V::default()).unwrap();
349    }
350
351    #[test]
355    fn invalid_cde_number_of_files_allocation_smaller_offset() {
356        ZipStreamReader::new(io::Cursor::new(include_bytes!(
357            "../../tests/data/invalid_cde_number_of_files_allocation_smaller_offset.zip"
358        )))
359        .visit(&mut DummyVisitor)
360        .unwrap_err();
361    }
362
363    #[test]
367    fn invalid_cde_number_of_files_allocation_greater_offset() {
368        ZipStreamReader::new(io::Cursor::new(include_bytes!(
369            "../../tests/data/invalid_cde_number_of_files_allocation_greater_offset.zip"
370        )))
371        .visit(&mut DummyVisitor)
372        .unwrap_err();
373    }
374
375    #[test]
377    fn test_cannot_symlink_outside_destination() -> ZipResult<()> {
378        use std::fs::create_dir;
379
380        let mut writer = ZipWriter::new(Cursor::new(Vec::new()));
381        writer.add_symlink("symlink/", "../dest-sibling/", SimpleFileOptions::default())?;
382        writer.start_file("symlink/dest-file", SimpleFileOptions::default())?;
383        let reader = ZipStreamReader::new(writer.finish()?);
384        let dest_parent = TempDir::with_prefix("stream__cannot_symlink_outside_destination")?;
385        let dest_sibling = dest_parent.path().join("dest-sibling");
386        create_dir(&dest_sibling)?;
387        let dest = dest_parent.path().join("dest");
388        create_dir(&dest)?;
389        assert!(reader.extract(dest).is_err());
390        assert!(!dest_sibling.join("dest-file").exists());
391        Ok(())
392    }
393
394    #[test]
395    fn test_can_create_destination() -> ZipResult<()> {
396        let mut v = Vec::new();
397        v.extend_from_slice(include_bytes!("../../tests/data/mimetype.zip"));
398        let reader = ZipStreamReader::new(v.as_slice());
399        let dest = TempDir::with_prefix("stream_test_can_create_destination").unwrap();
400        reader.extract(&dest)?;
401        assert!(dest.path().join("mimetype").exists());
402        Ok(())
403    }
404}