Skip to main content

lzip_parallel/
entry.rs

1//! ZIP entry decompressor.
2//!
3//! Decompresses a single ZIP entry given the file data and an `EntryLocation`
4//! from the Central Directory.  Supports STORE (method 0) and DEFLATE (method 8).
5//!
6//! Designed for parallel use: each worker gets the full `data` slice (read-only)
7//! plus its own `EntryLocation`, decompresses independently, returns `Vec<u8>`.
8
9use crate::central_dir::EntryLocation;
10
11const LOCAL_HEADER_SIG: u32 = 0x04034b50;
12
13#[derive(Debug)]
14pub struct ZipError(pub &'static str);
15
16impl std::fmt::Display for ZipError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "ZIP error: {}", self.0)
19    }
20}
21
22impl std::error::Error for ZipError {}
23
24pub struct ZipEntry {
25    pub name: String,
26    pub data: Vec<u8>,
27}
28
29/// Decompress one ZIP entry.
30///
31/// `data` is the full ZIP file bytes.  `loc` comes from the Central Directory.
32/// We read name/extra lengths from the local file header to locate the compressed
33/// data, then use the CD's already-resolved `compressed_size` (ZIP64-aware).
34pub fn decompress_entry(data: &[u8], loc: &EntryLocation) -> Result<Vec<u8>, ZipError> {
35    let hdr = loc.local_header_offset as usize;
36
37    let sig = data.get(hdr..hdr + 4)
38        .and_then(|b| b.try_into().ok())
39        .map(u32::from_le_bytes)
40        .ok_or(ZipError("local file header out of bounds"))?;
41    if sig != LOCAL_HEADER_SIG {
42        return Err(ZipError("invalid local file header signature"));
43    }
44
45    // Name/extra lengths in the LOCAL header can differ from the CD — must read them here.
46    let name_len = data.get(hdr + 26..hdr + 28)
47        .and_then(|b| b.try_into().ok())
48        .map(u16::from_le_bytes)
49        .ok_or(ZipError("local header truncated at name len"))? as usize;
50
51    let extra_len = data.get(hdr + 28..hdr + 30)
52        .and_then(|b| b.try_into().ok())
53        .map(u16::from_le_bytes)
54        .ok_or(ZipError("local header truncated at extra len"))? as usize;
55
56    let data_start = hdr + 30 + name_len + extra_len;
57    let data_end   = data_start + loc.compressed_size as usize;
58
59    let compressed = data.get(data_start..data_end)
60        .ok_or(ZipError("compressed data out of bounds"))?;
61
62    decompress_entry_raw(compressed, loc.compression_method, loc.uncompressed_size as usize)
63}
64
65/// Decompress pre-read compressed bytes using the given ZIP compression method.
66///
67/// `expected_size` is the uncompressed size from the Central Directory (0 if unknown).
68///
69/// Used by the streaming reader which reads entry bytes into a buffer before
70/// handing them to rayon workers — no full-file slice needed.
71pub fn decompress_entry_raw(compressed: &[u8], method: u16, expected_size: usize) -> Result<Vec<u8>, ZipError> {
72    match method {
73        0 => Ok(compressed.to_vec()),
74        8 => inflate_raw(compressed, expected_size),
75        _ => Err(ZipError("unsupported compression method")),
76    }
77}
78
79#[cfg(not(feature = "zlib-ng"))]
80fn inflate_raw(compressed: &[u8], expected_size: usize) -> Result<Vec<u8>, ZipError> {
81    let size = if expected_size > 0 {
82        expected_size
83    } else {
84        (compressed.len() * 4).max(4096)
85    };
86    linflate::inflate_to_vec(compressed, size)
87        .map_err(|_| ZipError("DEFLATE decompression failed"))
88}
89
90#[cfg(feature = "zlib-ng")]
91fn inflate_raw(compressed: &[u8], expected_size: usize) -> Result<Vec<u8>, ZipError> {
92    use std::io::Read;
93    let mut decoder = flate2::read::DeflateDecoder::new(compressed);
94    let mut out = if expected_size > 0 {
95        Vec::with_capacity(expected_size)
96    } else {
97        Vec::new()
98    };
99    decoder
100        .read_to_end(&mut out)
101        .map_err(|_| ZipError("DEFLATE decompression failed (zlib-ng)"))?;
102    Ok(out)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::central_dir::EntryLocation;
109
110    /// Minimal STORE entry: "hello.txt" → "Hello, World!\n"
111    #[rustfmt::skip]
112    static HELLO_JAR: &[u8] = &[
113        // Local file header
114        0x50, 0x4B, 0x03, 0x04,
115        0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
116        0x00, 0x00, 0x00, 0x00,
117        0x00, 0x00, 0x00, 0x00,         // CRC32 (not validated)
118        0x0E, 0x00, 0x00, 0x00,         // compressed size: 14
119        0x0E, 0x00, 0x00, 0x00,         // uncompressed size: 14
120        0x09, 0x00,                     // name len: 9
121        0x00, 0x00,                     // extra len: 0
122        b'h', b'e', b'l', b'l', b'o', b'.', b't', b'x', b't',
123        b'H', b'e', b'l', b'l', b'o', b',', b' ',
124        b'W', b'o', b'r', b'l', b'd', b'!', b'\n',
125    ];
126
127    #[test]
128    fn store_entry() {
129        let loc = EntryLocation {
130            name: "hello.txt".into(),
131            local_header_offset: 0,
132            compressed_size: 14,
133            uncompressed_size: 14,
134            compression_method: 0,
135            crc32: 0,
136            is_directory: false,
137        };
138        let out = decompress_entry(HELLO_JAR, &loc).unwrap();
139        assert_eq!(out, b"Hello, World!\n");
140    }
141}