Skip to main content

lzip_parallel/
parallel.rs

1//! Parallel ZIP decompression for small files (< SMALL_FILE_THRESHOLD).
2//!
3//! Reads the Central Directory once, then decompresses all entries
4//! concurrently on the rayon thread pool.  Results are returned in
5//! Central Directory order.
6//!
7//! For large files use `reader::decompress_batched` instead.
8
9use rayon::prelude::*;
10
11use crate::central_dir;
12use crate::entry::{self, ZipEntry, ZipError};
13
14/// Decompress all file entries in `data` in parallel.
15///
16/// `data` must be a complete ZIP file in memory.
17/// Directory entries are skipped.  All file entries are decoded
18/// concurrently and returned in Central Directory order.
19pub fn decompress_parallel(data: &[u8]) -> Result<Vec<ZipEntry>, ZipError> {
20    decompress_parallel_filter(data, "")
21}
22
23/// Decompress only entries whose name contains `needle`.
24///
25/// Empty needle matches all entries (same as `decompress_parallel`).
26/// Filtering happens before decompression — non-matching entries are never inflated.
27pub fn decompress_parallel_filter(data: &[u8], needle: &str) -> Result<Vec<ZipEntry>, ZipError> {
28    let locations = central_dir::read_central_directory(data)?;
29
30    let results: Vec<Result<Option<ZipEntry>, ZipError>> = crate::thread_pool().install(|| {
31        locations
32            .par_iter()
33            .map(|loc| {
34                if loc.is_directory {
35                    return Ok(None);
36                }
37                if !needle.is_empty() && !loc.name.contains(needle) {
38                    return Ok(None);
39                }
40                let bytes = entry::decompress_entry(data, loc)?;
41                Ok(Some(ZipEntry { name: loc.name.clone(), data: bytes }))
42            })
43            .collect()
44    });
45
46    let mut entries = Vec::with_capacity(locations.len());
47    for r in results {
48        if let Some(e) = r? {
49            entries.push(e);
50        }
51    }
52    Ok(entries)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58    use std::io::Write;
59
60    fn make_jar(items: &[(&str, &[u8])]) -> Vec<u8> {
61        use std::io::Cursor;
62        use zip::write::SimpleFileOptions;
63        let mut buf = Vec::new();
64        let mut zw = zip::ZipWriter::new(Cursor::new(&mut buf));
65        let opts = SimpleFileOptions::default()
66            .compression_method(zip::CompressionMethod::Deflated);
67        for (name, data) in items {
68            zw.start_file(*name, opts).unwrap();
69            zw.write_all(data).unwrap();
70        }
71        zw.finish().unwrap();
72        buf
73    }
74
75    #[test]
76    fn parallel_single() {
77        let jar = make_jar(&[("Hello.class", b"cafebabe deadbeef")]);
78        let entries = decompress_parallel(&jar).unwrap();
79        assert_eq!(entries.len(), 1);
80        assert_eq!(entries[0].name, "Hello.class");
81        assert_eq!(entries[0].data, b"cafebabe deadbeef");
82    }
83
84    #[test]
85    fn parallel_multi() {
86        let jar = make_jar(&[
87            ("a.txt", b"aaa"),
88            ("b.txt", b"bbb"),
89            ("c.txt", b"ccc"),
90        ]);
91        let entries = decompress_parallel(&jar).unwrap();
92        assert_eq!(entries.len(), 3);
93        assert_eq!(entries[0].data, b"aaa");
94        assert_eq!(entries[1].data, b"bbb");
95        assert_eq!(entries[2].data, b"ccc");
96    }
97
98    #[test]
99    fn parallel_skips_directories() {
100        let jar = make_jar(&[
101            ("META-INF/", b""),
102            ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n"),
103        ]);
104        let entries = decompress_parallel(&jar).unwrap();
105        assert_eq!(entries.len(), 1);
106        assert_eq!(entries[0].name, "META-INF/MANIFEST.MF");
107    }
108
109    #[test]
110    fn filter_by_needle() {
111        let jar = make_jar(&[
112            ("com/example/App.class", b"bytecode"),
113            ("META-INF/maven/com.example/app/pom.xml", b"<project/>"),
114            ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n"),
115        ]);
116        let entries = decompress_parallel_filter(&jar, "pom.xml").unwrap();
117        assert_eq!(entries.len(), 1);
118        assert_eq!(entries[0].name, "META-INF/maven/com.example/app/pom.xml");
119        assert_eq!(entries[0].data, b"<project/>");
120    }
121
122    #[test]
123    fn filter_empty_matches_all() {
124        let jar = make_jar(&[("a.txt", b"a"), ("b.txt", b"b")]);
125        let entries = decompress_parallel_filter(&jar, "").unwrap();
126        assert_eq!(entries.len(), 2);
127    }
128}