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 std::borrow::Cow;
10
11use crate::central_dir::EntryLocation;
12
13const LOCAL_HEADER_SIG: u32 = 0x04034b50;
14
15#[derive(Debug)]
16pub struct ZipError(pub &'static str);
17
18impl std::fmt::Display for ZipError {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        write!(f, "ZIP error: {}", self.0)
21    }
22}
23
24impl std::error::Error for ZipError {}
25
26pub struct ZipEntry {
27    pub name: String,
28    pub data: Vec<u8>,
29}
30
31/// Decompress one ZIP entry.
32///
33/// `data` is the full ZIP file bytes.  `loc` comes from the Central Directory.
34/// We read name/extra lengths from the local file header to locate the compressed
35/// data, then use the CD's already-resolved `compressed_size` (ZIP64-aware).
36pub fn decompress_entry(data: &[u8], loc: &EntryLocation) -> Result<Vec<u8>, ZipError> {
37    let compressed = locate_compressed(data, loc)?;
38    decompress_entry_raw(compressed, loc.compression_method, loc.uncompressed_size as usize)
39}
40
41/// Borrowing decode: same as `decompress_entry` but returns `Cow` so that
42/// STORE entries (method 0) borrow directly from `data` instead of copying.
43///
44/// The returned `Cow::Borrowed` aliases the input slice, so the caller must
45/// keep `data` alive for the lifetime of the result (true for the in-memory
46/// parallel/batch paths where the source buffer outlives the call). Avoids the
47/// `.to_vec()` clone of already-stored bytes (.png, nested .jar, .so) — Law 1.
48pub fn decompress_entry_cow<'a>(data: &'a [u8], loc: &EntryLocation) -> Result<Cow<'a, [u8]>, ZipError> {
49    let compressed = locate_compressed(data, loc)?;
50    decompress_entry_raw_cow(compressed, loc.compression_method, loc.uncompressed_size as usize)
51}
52
53/// Locate the compressed-data slice of an entry within the full ZIP `data`.
54///
55/// Reads name/extra lengths from the LOCAL header (which can differ from the
56/// Central Directory) to find where the compressed bytes start, then uses the
57/// CD's ZIP64-resolved `compressed_size` for the length.
58fn locate_compressed<'a>(data: &'a [u8], loc: &EntryLocation) -> Result<&'a [u8], ZipError> {
59    let hdr = loc.local_header_offset as usize;
60
61    let sig = data.get(hdr..hdr + 4)
62        .and_then(|b| b.try_into().ok())
63        .map(u32::from_le_bytes)
64        .ok_or(ZipError("local file header out of bounds"))?;
65    if sig != LOCAL_HEADER_SIG {
66        return Err(ZipError("invalid local file header signature"));
67    }
68
69    // Name/extra lengths in the LOCAL header can differ from the CD — must read them here.
70    let name_len = data.get(hdr + 26..hdr + 28)
71        .and_then(|b| b.try_into().ok())
72        .map(u16::from_le_bytes)
73        .ok_or(ZipError("local header truncated at name len"))? as usize;
74
75    let extra_len = data.get(hdr + 28..hdr + 30)
76        .and_then(|b| b.try_into().ok())
77        .map(u16::from_le_bytes)
78        .ok_or(ZipError("local header truncated at extra len"))? as usize;
79
80    let data_start = hdr + 30 + name_len + extra_len;
81    let data_end   = data_start + loc.compressed_size as usize;
82
83    data.get(data_start..data_end)
84        .ok_or(ZipError("compressed data out of bounds"))
85}
86
87/// Decompress pre-read compressed bytes using the given ZIP compression method.
88///
89/// `expected_size` is the uncompressed size from the Central Directory (0 if unknown).
90///
91/// Used by the streaming reader which reads entry bytes into a buffer before
92/// handing them to rayon workers — no full-file slice needed.
93pub fn decompress_entry_raw(compressed: &[u8], method: u16, expected_size: usize) -> Result<Vec<u8>, ZipError> {
94    match method {
95        0 => Ok(compressed.to_vec()),
96        8 => inflate_raw(compressed, expected_size),
97        _ => Err(ZipError("unsupported compression method")),
98    }
99}
100
101/// Borrowing variant of `decompress_entry_raw`: STORE (method 0) returns
102/// `Cow::Borrowed(compressed)` — no copy — while DEFLATE returns `Cow::Owned`.
103/// The borrow lives as long as the `compressed` slice the caller passed in.
104pub fn decompress_entry_raw_cow(compressed: &[u8], method: u16, expected_size: usize) -> Result<Cow<'_, [u8]>, ZipError> {
105    match method {
106        0 => Ok(Cow::Borrowed(compressed)),
107        8 => inflate_raw(compressed, expected_size).map(Cow::Owned),
108        _ => Err(ZipError("unsupported compression method")),
109    }
110}
111
112#[cfg(not(feature = "zlib-ng"))]
113fn inflate_raw(compressed: &[u8], expected_size: usize) -> Result<Vec<u8>, ZipError> {
114    let size = if expected_size > 0 {
115        expected_size
116    } else {
117        (compressed.len() * 4).max(4096)
118    };
119    linflate::inflate_to_vec(compressed, size)
120        .map_err(|_| ZipError("DEFLATE decompression failed"))
121}
122
123#[cfg(feature = "zlib-ng")]
124fn inflate_raw(compressed: &[u8], expected_size: usize) -> Result<Vec<u8>, ZipError> {
125    use std::io::Read;
126    let mut decoder = flate2::read::DeflateDecoder::new(compressed);
127    let mut out = if expected_size > 0 {
128        Vec::with_capacity(expected_size)
129    } else {
130        Vec::new()
131    };
132    decoder
133        .read_to_end(&mut out)
134        .map_err(|_| ZipError("DEFLATE decompression failed (zlib-ng)"))?;
135    Ok(out)
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::central_dir::EntryLocation;
142
143    /// Minimal STORE entry: "hello.txt" → "Hello, World!\n"
144    #[rustfmt::skip]
145    static HELLO_JAR: &[u8] = &[
146        // Local file header
147        0x50, 0x4B, 0x03, 0x04,
148        0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
149        0x00, 0x00, 0x00, 0x00,
150        0x00, 0x00, 0x00, 0x00,         // CRC32 (not validated)
151        0x0E, 0x00, 0x00, 0x00,         // compressed size: 14
152        0x0E, 0x00, 0x00, 0x00,         // uncompressed size: 14
153        0x09, 0x00,                     // name len: 9
154        0x00, 0x00,                     // extra len: 0
155        b'h', b'e', b'l', b'l', b'o', b'.', b't', b'x', b't',
156        b'H', b'e', b'l', b'l', b'o', b',', b' ',
157        b'W', b'o', b'r', b'l', b'd', b'!', b'\n',
158    ];
159
160    #[test]
161    fn store_entry() {
162        let loc = EntryLocation {
163            name: "hello.txt".into(),
164            local_header_offset: 0,
165            compressed_size: 14,
166            uncompressed_size: 14,
167            compression_method: 0,
168            crc32: 0,
169            is_directory: false,
170        };
171        let out = decompress_entry(HELLO_JAR, &loc).unwrap();
172        assert_eq!(out, b"Hello, World!\n");
173    }
174}