1use 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
31pub 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
41pub 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
53fn 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 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
87pub 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
101pub 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 #[rustfmt::skip]
145 static HELLO_JAR: &[u8] = &[
146 0x50, 0x4B, 0x03, 0x04,
148 0x14, 0x00, 0x00, 0x00, 0x00, 0x00,
149 0x00, 0x00, 0x00, 0x00,
150 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x00, 0x09, 0x00, 0x00, 0x00, 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}