1use crate::entry::ZipError;
11
12const EOCD_SIG: u32 = 0x06054b50;
13const EOCD64_LOC_SIG: u32 = 0x07064b50;
14const EOCD64_SIG: u32 = 0x06064b50;
15const CD_SIG: u32 = 0x02014b50;
16const MAX_COMMENT: usize = 0xFFFF;
17
18#[derive(Debug, Clone)]
20pub struct EntryLocation {
21 pub name: String,
22 pub local_header_offset: u64,
23 pub compressed_size: u64,
24 pub uncompressed_size: u64,
25 pub compression_method: u16,
26 pub crc32: u32,
27 pub is_directory: bool,
28}
29
30#[inline]
33fn u16_le(d: &[u8], off: usize) -> Option<u16> {
34 d.get(off..off + 2).map(|b| u16::from_le_bytes([b[0], b[1]]))
35}
36
37#[inline]
38fn u32_le(d: &[u8], off: usize) -> Option<u32> {
39 d.get(off..off + 4).map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
40}
41
42#[inline]
43fn u64_le(d: &[u8], off: usize) -> Option<u64> {
44 d.get(off..off + 8)
45 .map(|b| u64::from_le_bytes(b.try_into().unwrap()))
46}
47
48fn find_eocd(data: &[u8]) -> Option<usize> {
52 if data.len() < 22 { return None; }
53 let search_start = data.len().saturating_sub(22 + MAX_COMMENT);
54 for i in (search_start..=data.len() - 22).rev() {
55 if u32_le(data, i) == Some(EOCD_SIG) {
56 return Some(i);
57 }
58 }
59 None
60}
61
62fn parse_eocd(data: &[u8]) -> Result<(u64, u64, u64), ZipError> {
64 let eocd = find_eocd(data).ok_or(ZipError("EOCD not found — not a JAR/ZIP file"))?;
65
66 if eocd >= 20 {
68 if u32_le(data, eocd - 20) == Some(EOCD64_LOC_SIG) {
69 let eocd64_off = u64_le(data, eocd - 20 + 8)
70 .ok_or(ZipError("ZIP64 EOCD locator truncated"))? as usize;
71
72 if u32_le(data, eocd64_off) != Some(EOCD64_SIG) {
73 return Err(ZipError("invalid ZIP64 EOCD signature"));
74 }
75 let entry_count = u64_le(data, eocd64_off + 32)
80 .ok_or(ZipError("ZIP64 EOCD truncated at entry count"))?;
81 let cd_size = u64_le(data, eocd64_off + 40)
82 .ok_or(ZipError("ZIP64 EOCD truncated at CD size"))?;
83 let cd_offset = u64_le(data, eocd64_off + 48)
84 .ok_or(ZipError("ZIP64 EOCD truncated at CD offset"))?;
85 return Ok((entry_count, cd_offset, cd_size));
86 }
87 }
88
89 let entry_count = u16_le(data, eocd + 10).ok_or(ZipError("EOCD truncated"))? as u64;
95 let cd_size = u32_le(data, eocd + 12).ok_or(ZipError("EOCD truncated"))? as u64;
96 let cd_offset = u32_le(data, eocd + 16).ok_or(ZipError("EOCD truncated"))? as u64;
97 Ok((entry_count, cd_offset, cd_size))
98}
99
100fn walk_cd_entries(cd_data: &[u8], entry_count: u64) -> Result<Vec<EntryLocation>, ZipError> {
108 let mut entries = Vec::with_capacity(entry_count.min(65536) as usize);
109 let mut pos = 0usize;
110
111 while pos < cd_data.len() {
112 if u32_le(cd_data, pos) != Some(CD_SIG) { break; }
113
114 let method = u16_le(cd_data, pos + 10).ok_or(ZipError("CD entry truncated"))?;
124 let crc32 = u32_le(cd_data, pos + 16).ok_or(ZipError("CD entry truncated"))?;
125 let mut csize = u32_le(cd_data, pos + 20).ok_or(ZipError("CD entry truncated"))? as u64;
126 let mut ucsize = u32_le(cd_data, pos + 24).ok_or(ZipError("CD entry truncated"))? as u64;
127 let name_len = u16_le(cd_data, pos + 28).ok_or(ZipError("CD entry truncated"))? as usize;
128 let extra_len = u16_le(cd_data, pos + 30).ok_or(ZipError("CD entry truncated"))? as usize;
129 let comment_len = u16_le(cd_data, pos + 32).ok_or(ZipError("CD entry truncated"))? as usize;
130 let mut lhoff = u32_le(cd_data, pos + 42).ok_or(ZipError("CD entry truncated"))? as u64;
131
132 let name_start = pos + 46;
133 let name_end = name_start + name_len;
134 let extra_start = name_end;
135 let extra_end = extra_start + extra_len;
136
137 if extra_end > cd_data.len() {
138 return Err(ZipError("CD entry name/extra truncated"));
139 }
140 let name = String::from_utf8_lossy(&cd_data[name_start..name_end]).into_owned();
141
142 if csize == 0xFFFF_FFFF || ucsize == 0xFFFF_FFFF || lhoff == 0xFFFF_FFFF {
144 let mut epos = extra_start;
145 while epos + 4 <= extra_end {
146 let tag = u16_le(cd_data, epos).unwrap_or(0);
147 let size = u16_le(cd_data, epos + 2).unwrap_or(0) as usize;
148 if tag == 0x0001 {
149 let mut z = epos + 4;
150 if ucsize == 0xFFFF_FFFF {
151 ucsize = u64_le(cd_data, z).ok_or(ZipError("ZIP64 extra truncated"))?;
152 z += 8;
153 }
154 if csize == 0xFFFF_FFFF {
155 csize = u64_le(cd_data, z).ok_or(ZipError("ZIP64 extra truncated"))?;
156 z += 8;
157 }
158 if lhoff == 0xFFFF_FFFF {
159 lhoff = u64_le(cd_data, z).ok_or(ZipError("ZIP64 extra truncated"))?;
160 }
161 break;
162 }
163 epos += 4 + size;
164 }
165 }
166
167 entries.push(EntryLocation {
168 is_directory: name.ends_with('/'),
169 name,
170 local_header_offset: lhoff,
171 compressed_size: csize,
172 uncompressed_size: ucsize,
173 compression_method: method,
174 crc32,
175 });
176
177 pos = extra_end + comment_len;
178 }
179
180 Ok(entries)
181}
182
183pub fn read_central_directory(data: &[u8]) -> Result<Vec<EntryLocation>, ZipError> {
188 let (entry_count, cd_offset, cd_size) = parse_eocd(data)?;
189
190 let cd_start = cd_offset as usize;
191 let cd_end = cd_start.checked_add(cd_size as usize)
192 .filter(|&e| e <= data.len())
193 .ok_or(ZipError("central directory extends past end of file"))?;
194
195 walk_cd_entries(&data[cd_start..cd_end], entry_count)
196}
197
198fn parse_eocd_streaming<R: std::io::Read + std::io::Seek>(
204 source: &mut R,
205) -> Result<(u64, u64, u64), ZipError> {
206 use std::io::SeekFrom;
207
208 let file_size = source.seek(SeekFrom::End(0))
209 .map_err(|_| ZipError("seek failed"))?;
210
211 if file_size < 22 {
212 return Err(ZipError("file too small to be a JAR/ZIP"));
213 }
214
215 let scan_size = ((22 + MAX_COMMENT + 20 + 56) as u64).min(file_size);
218 let scan_start = file_size - scan_size;
219
220 source.seek(SeekFrom::Start(scan_start))
221 .map_err(|_| ZipError("seek to tail failed"))?;
222 let mut buf = vec![0u8; scan_size as usize];
223 source.read_exact(&mut buf)
224 .map_err(|_| ZipError("read tail failed"))?;
225
226 let eocd_in_buf = find_eocd(&buf)
227 .ok_or(ZipError("EOCD not found — not a JAR/ZIP file"))?;
228 let eocd_abs = scan_start + eocd_in_buf as u64;
229
230 if eocd_abs >= 20 {
232 let loc_abs = eocd_abs - 20;
233
234 let loc_sig = if loc_abs >= scan_start {
235 u32_le(&buf, (loc_abs - scan_start) as usize)
236 } else {
237 source.seek(SeekFrom::Start(loc_abs))
239 .map_err(|_| ZipError("seek to ZIP64 locator failed"))?;
240 let mut tmp = [0u8; 4];
241 source.read_exact(&mut tmp)
242 .map_err(|_| ZipError("read ZIP64 locator sig failed"))?;
243 Some(u32::from_le_bytes(tmp))
244 };
245
246 if loc_sig == Some(EOCD64_LOC_SIG) {
247 let eocd64_off: u64 = if loc_abs >= scan_start {
248 u64_le(&buf, (loc_abs - scan_start) as usize + 8)
249 .ok_or(ZipError("ZIP64 locator truncated"))?
250 } else {
251 source.seek(SeekFrom::Start(loc_abs + 8))
252 .map_err(|_| ZipError("seek to ZIP64 locator offset failed"))?;
253 let mut tmp = [0u8; 8];
254 source.read_exact(&mut tmp)
255 .map_err(|_| ZipError("read ZIP64 EOCD offset failed"))?;
256 u64::from_le_bytes(tmp)
257 };
258
259 let eocd64: [u8; 56] = if eocd64_off >= scan_start
261 && eocd64_off + 56 <= scan_start + scan_size
262 {
263 let s = (eocd64_off - scan_start) as usize;
264 buf[s..s + 56].try_into().unwrap()
265 } else {
266 source.seek(SeekFrom::Start(eocd64_off))
267 .map_err(|_| ZipError("seek to ZIP64 EOCD failed"))?;
268 let mut tmp = [0u8; 56];
269 source.read_exact(&mut tmp)
270 .map_err(|_| ZipError("read ZIP64 EOCD failed"))?;
271 tmp
272 };
273
274 if u32::from_le_bytes(eocd64[0..4].try_into().unwrap()) != EOCD64_SIG {
275 return Err(ZipError("invalid ZIP64 EOCD signature"));
276 }
277 let entry_count = u64::from_le_bytes(eocd64[32..40].try_into().unwrap());
278 let cd_size = u64::from_le_bytes(eocd64[40..48].try_into().unwrap());
279 let cd_offset = u64::from_le_bytes(eocd64[48..56].try_into().unwrap());
280 return Ok((entry_count, cd_offset, cd_size));
281 }
282 }
283
284 let entry_count = u16_le(&buf, eocd_in_buf + 10).ok_or(ZipError("EOCD truncated"))? as u64;
286 let cd_size = u32_le(&buf, eocd_in_buf + 12).ok_or(ZipError("EOCD truncated"))? as u64;
287 let cd_offset = u32_le(&buf, eocd_in_buf + 16).ok_or(ZipError("EOCD truncated"))? as u64;
288 Ok((entry_count, cd_offset, cd_size))
289}
290
291pub fn read_central_directory_from<R: std::io::Read + std::io::Seek>(
298 source: &mut R,
299) -> Result<Vec<EntryLocation>, ZipError> {
300 use std::io::SeekFrom;
301
302 let (entry_count, cd_offset, cd_size) = parse_eocd_streaming(source)?;
303
304 source.seek(SeekFrom::Start(cd_offset))
305 .map_err(|_| ZipError("seek to central directory failed"))?;
306 let mut cd_data = vec![0u8; cd_size as usize];
307 source.read_exact(&mut cd_data)
308 .map_err(|_| ZipError("read central directory failed"))?;
309
310 walk_cd_entries(&cd_data, entry_count)
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 fn make_jar(entries: &[(&str, &[u8])]) -> Vec<u8> {
318 use std::io::{Cursor, Write};
319 use zip::write::SimpleFileOptions;
320 let mut buf = Vec::new();
321 let cursor = Cursor::new(&mut buf);
322 let mut zw = zip::ZipWriter::new(cursor);
323 let opts = SimpleFileOptions::default()
324 .compression_method(zip::CompressionMethod::Deflated);
325 for (name, data) in entries {
326 zw.start_file(*name, opts).unwrap();
327 zw.write_all(data).unwrap();
328 }
329 zw.finish().unwrap();
330 buf
331 }
332
333 #[test]
334 fn single_entry() {
335 let jar = make_jar(&[("Hello.class", b"cafebabe")]);
336 let locs = read_central_directory(&jar).unwrap();
337 assert_eq!(locs.len(), 1);
338 assert_eq!(locs[0].name, "Hello.class");
339 assert_eq!(locs[0].uncompressed_size, 8);
340 }
341
342 #[test]
343 fn directory_entry() {
344 let jar = make_jar(&[("META-INF/", b""), ("META-INF/MANIFEST.MF", b"Manifest-Version: 1.0\n")]);
345 let locs = read_central_directory(&jar).unwrap();
346 assert_eq!(locs.len(), 2);
347 assert!(locs[0].is_directory);
348 assert!(!locs[1].is_directory);
349 }
350}