1const LOCAL_HEADER: u32 = 0x0403_4B50;
20const CENTRAL_HEADER: u32 = 0x0201_4B50;
21const END_OF_CENTRAL_DIRECTORY: u32 = 0x0605_4B50;
22const VERSION: u16 = 20;
24pub const STORED: u16 = 0;
26pub const EPOCH_TIME: u16 = 0;
29pub const EPOCH_DATE: u16 = 0x0021;
30
31const fn crc_table() -> [u32; 256] {
32 let mut table = [0u32; 256];
33 let mut n = 0;
34 while n < 256 {
35 let mut c = n as u32;
36 let mut k = 0;
37 while k < 8 {
38 c = if c & 1 != 0 {
39 0xEDB8_8320 ^ (c >> 1)
40 } else {
41 c >> 1
42 };
43 k += 1;
44 }
45 table[n] = c;
46 n += 1;
47 }
48 table
49}
50const CRC_TABLE: [u32; 256] = crc_table();
51
52pub fn crc32(data: &[u8]) -> u32 {
55 let mut c = 0xFFFF_FFFFu32;
56 for &b in data {
57 c = CRC_TABLE[((c ^ u32::from(b)) & 0xFF) as usize] ^ (c >> 8);
58 }
59 c ^ 0xFFFF_FFFF
60}
61
62fn put_u16(out: &mut Vec<u8>, v: u16) {
63 out.extend_from_slice(&v.to_le_bytes());
64}
65
66fn put_u32(out: &mut Vec<u8>, v: u32) {
67 out.extend_from_slice(&v.to_le_bytes());
68}
69
70pub struct RawEntry<'a> {
76 pub name: &'a str,
77 pub method: u16,
78 pub crc: u32,
79 pub size: u32,
80 pub bytes: &'a [u8],
81}
82
83pub fn write_stored(parts: &[(String, Vec<u8>)]) -> Option<Vec<u8>> {
86 let mut raws = Vec::with_capacity(parts.len());
87 for (name, data) in parts {
88 raws.push(RawEntry {
89 name,
90 method: STORED,
91 crc: crc32(data),
92 size: u32::try_from(data.len()).ok()?,
93 bytes: data,
94 });
95 }
96 write_entries(&raws)
97}
98
99pub fn write_entries(entries: &[RawEntry]) -> Option<Vec<u8>> {
103 let count = u16::try_from(entries.len()).ok()?;
104 let mut out: Vec<u8> = Vec::new();
105 let mut central: Vec<u8> = Vec::new();
106 for e in entries {
107 let name_len = u16::try_from(e.name.len()).ok()?;
108 let compressed = u32::try_from(e.bytes.len()).ok()?;
109 let offset = u32::try_from(out.len()).ok()?;
110 put_u32(&mut out, LOCAL_HEADER);
112 put_u16(&mut out, VERSION);
113 put_u16(&mut out, 0); put_u16(&mut out, e.method);
115 put_u16(&mut out, EPOCH_TIME);
116 put_u16(&mut out, EPOCH_DATE);
117 put_u32(&mut out, e.crc);
118 put_u32(&mut out, compressed);
119 put_u32(&mut out, e.size);
120 put_u16(&mut out, name_len);
121 put_u16(&mut out, 0); out.extend_from_slice(e.name.as_bytes());
123 out.extend_from_slice(e.bytes);
124 put_u32(&mut central, CENTRAL_HEADER);
126 put_u16(&mut central, VERSION); put_u16(&mut central, VERSION); put_u16(&mut central, 0);
129 put_u16(&mut central, e.method);
130 put_u16(&mut central, EPOCH_TIME);
131 put_u16(&mut central, EPOCH_DATE);
132 put_u32(&mut central, e.crc);
133 put_u32(&mut central, compressed);
134 put_u32(&mut central, e.size);
135 put_u16(&mut central, name_len);
136 put_u16(&mut central, 0); put_u16(&mut central, 0); put_u16(&mut central, 0); put_u16(&mut central, 0); put_u32(&mut central, 0); put_u32(&mut central, offset);
142 central.extend_from_slice(e.name.as_bytes());
143 }
144 let cd_offset = u32::try_from(out.len()).ok()?;
145 let cd_size = u32::try_from(central.len()).ok()?;
146 out.extend_from_slice(¢ral);
147 put_u32(&mut out, END_OF_CENTRAL_DIRECTORY);
148 put_u16(&mut out, 0); put_u16(&mut out, 0); put_u16(&mut out, count);
151 put_u16(&mut out, count);
152 put_u32(&mut out, cd_size);
153 put_u32(&mut out, cd_offset);
154 put_u16(&mut out, 0); Some(out)
156}
157
158#[derive(Clone, Debug, PartialEq, Eq)]
160pub struct Entry {
161 pub name: String,
162 pub method: u16,
163 pub time: u16,
164 pub date: u16,
165 pub crc: u32,
166 pub compressed_size: u32,
167 pub size: u32,
168 pub data_offset: usize,
170}
171
172fn read_u16(b: &[u8], at: usize) -> Option<u16> {
173 Some(u16::from_le_bytes([*b.get(at)?, *b.get(at + 1)?]))
174}
175
176fn read_u32(b: &[u8], at: usize) -> Option<u32> {
177 Some(u32::from_le_bytes([
178 *b.get(at)?,
179 *b.get(at + 1)?,
180 *b.get(at + 2)?,
181 *b.get(at + 3)?,
182 ]))
183}
184
185const ZIP64_LOCATOR: u32 = 0x0706_4B50;
186
187fn find_eocd(bytes: &[u8]) -> Option<usize> {
191 if bytes.len() < 22 {
192 return None;
193 }
194 let last = bytes.len() - 22;
195 let first = last.saturating_sub(65_535);
196 (first..=last).rev().find(|&i| {
197 read_u32(bytes, i) == Some(END_OF_CENTRAL_DIRECTORY)
198 && read_u16(bytes, i + 20).map(usize::from) == Some(bytes.len() - 22 - i)
199 })
200}
201
202pub fn why_not_an_archive(bytes: &[u8]) -> Option<&'static str> {
205 if bytes.starts_with(&[0xD0, 0xCF, 0x11, 0xE0]) {
206 return Some("it is an older .xls or an encrypted workbook, not an .xlsx package");
207 }
208 let Some(eocd) = find_eocd(bytes) else {
209 return Some("it has no end-of-central-directory record, so it is not a zip archive");
210 };
211 if read_u16(bytes, eocd + 10) == Some(0xFFFF)
212 || read_u32(bytes, eocd + 16) == Some(0xFFFF_FFFF)
213 || (eocd >= 20 && read_u32(bytes, eocd - 20) == Some(ZIP64_LOCATOR))
214 {
215 return Some("it is a zip64 archive, which this version does not read");
216 }
217 if entries(bytes).is_none() {
218 return Some("its central directory does not read whole");
219 }
220 None
221}
222
223pub fn entries(bytes: &[u8]) -> Option<Vec<Entry>> {
229 let eocd = find_eocd(bytes)?;
230 let count = usize::from(read_u16(bytes, eocd + 10)?);
231 let cd_size = read_u32(bytes, eocd + 12)? as usize;
232 let cd_offset = read_u32(bytes, eocd + 16)? as usize;
233 if count == 0xFFFF || cd_offset == 0xFFFF_FFFF {
234 return None; }
236 if cd_offset.checked_add(cd_size)? > eocd {
237 return None;
238 }
239 let mut pos = cd_offset;
240 let mut list = Vec::with_capacity(count);
241 for _ in 0..count {
242 if read_u32(bytes, pos)? != CENTRAL_HEADER {
243 return None;
244 }
245 let method = read_u16(bytes, pos + 10)?;
246 let time = read_u16(bytes, pos + 12)?;
247 let date = read_u16(bytes, pos + 14)?;
248 let crc = read_u32(bytes, pos + 16)?;
249 let compressed_size = read_u32(bytes, pos + 20)?;
250 let size = read_u32(bytes, pos + 24)?;
251 let name_len = usize::from(read_u16(bytes, pos + 28)?);
252 let extra_len = usize::from(read_u16(bytes, pos + 30)?);
253 let comment_len = usize::from(read_u16(bytes, pos + 32)?);
254 let local = read_u32(bytes, pos + 42)? as usize;
255 let name = std::str::from_utf8(bytes.get(pos + 46..pos + 46 + name_len)?)
256 .ok()?
257 .to_string();
258 if read_u32(bytes, local)? != LOCAL_HEADER {
259 return None;
260 }
261 let local_name_len = usize::from(read_u16(bytes, local + 26)?);
262 let local_extra_len = usize::from(read_u16(bytes, local + 28)?);
263 let data_offset = local + 30 + local_name_len + local_extra_len;
264 if data_offset.checked_add(compressed_size as usize)? > bytes.len() {
265 return None;
266 }
267 list.push(Entry {
268 name,
269 method,
270 time,
271 date,
272 crc,
273 compressed_size,
274 size,
275 data_offset,
276 });
277 pos += 46 + name_len + extra_len + comment_len;
278 }
279 Some(list)
280}
281
282pub fn stored_data<'a>(bytes: &'a [u8], e: &Entry) -> Option<&'a [u8]> {
284 if e.method != STORED {
285 return None;
286 }
287 bytes.get(e.data_offset..e.data_offset + e.size as usize)
288}
289
290pub fn raw_data<'a>(bytes: &'a [u8], e: &Entry) -> Option<&'a [u8]> {
293 bytes.get(e.data_offset..e.data_offset + e.compressed_size as usize)
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn the_crc_check_value() {
302 assert_eq!(crc32(b"123456789"), 0xCBF4_3926);
303 assert_eq!(crc32(b""), 0);
304 }
305
306 fn two_parts() -> Vec<(String, Vec<u8>)> {
307 vec![
308 ("[Content_Types].xml".to_string(), b"<Types/>".to_vec()),
309 ("xl/workbook.xml".to_string(), b"<workbook/>".to_vec()),
310 ]
311 }
312
313 #[test]
314 fn the_archive_walks_back_through_its_own_directory() {
315 let bytes = write_stored(&two_parts()).expect("two small parts fit");
316 assert_eq!(&bytes[..4], &[0x50, 0x4B, 0x03, 0x04]);
317 let list = entries(&bytes).expect("a central directory");
318 assert_eq!(list.len(), 2);
319 assert_eq!(list[0].name, "[Content_Types].xml");
320 assert_eq!(list[1].name, "xl/workbook.xml");
321 for (e, (_, data)) in list.iter().zip(two_parts().iter()) {
322 assert_eq!(e.method, STORED);
323 assert_eq!((e.time, e.date), (EPOCH_TIME, EPOCH_DATE));
324 assert_eq!(e.size, data.len() as u32);
325 assert_eq!(e.compressed_size, data.len() as u32);
326 assert_eq!(e.crc, crc32(data));
327 assert_eq!(stored_data(&bytes, e).unwrap(), data.as_slice());
328 }
329 }
330
331 #[test]
332 fn the_same_parts_give_the_same_bytes() {
333 assert_eq!(write_stored(&two_parts()), write_stored(&two_parts()));
334 }
335
336 #[test]
337 fn an_entry_copies_as_it_is_and_a_comment_or_zip64_is_told_apart() {
338 let raw = RawEntry {
341 name: "xl/styles.xml",
342 method: 8,
343 crc: 0x1234_5678,
344 size: 999,
345 bytes: &[0x4b, 0x4c, 0x4a, 0x06, 0x00],
346 };
347 let own = RawEntry {
348 name: "own.xml",
349 method: STORED,
350 crc: crc32(b"<a/>"),
351 size: 4,
352 bytes: b"<a/>",
353 };
354 let bytes = write_entries(&[raw, own]).unwrap();
355 let list = entries(&bytes).unwrap();
356 assert_eq!(list[0].method, 8);
357 assert_eq!(list[0].crc, 0x1234_5678);
358 assert_eq!(list[0].size, 999);
359 assert_eq!(list[0].compressed_size, 5);
360 assert_eq!(
361 raw_data(&bytes, &list[0]).unwrap(),
362 &[0x4b, 0x4c, 0x4a, 0x06, 0x00]
363 );
364 assert_eq!(stored_data(&bytes, &list[0]), None);
365 assert_eq!(stored_data(&bytes, &list[1]).unwrap(), b"<a/>");
366 assert_eq!(why_not_an_archive(&bytes), None);
367 let mut commented = bytes.clone();
369 let n = commented.len();
370 commented[n - 2] = 5;
371 commented.extend_from_slice(b"hello");
372 assert_eq!(entries(&commented).unwrap().len(), 2);
373 assert_eq!(
375 why_not_an_archive(b"not a zip"),
376 Some("it has no end-of-central-directory record, so it is not a zip archive")
377 );
378 assert_eq!(
379 why_not_an_archive(&[0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]),
380 Some("it is an older .xls or an encrypted workbook, not an .xlsx package")
381 );
382 let mut zip64 = bytes.clone();
383 let n = zip64.len();
384 zip64[n - 12] = 0xFF;
385 zip64[n - 11] = 0xFF;
386 zip64[n - 10] = 0xFF;
387 zip64[n - 9] = 0xFF;
388 assert_eq!(
389 why_not_an_archive(&zip64),
390 Some("it is a zip64 archive, which this version does not read")
391 );
392 assert!(entries(&zip64).is_none());
393 }
394
395 #[test]
396 fn what_does_not_fit_is_refused_not_truncated() {
397 let many: Vec<(String, Vec<u8>)> =
398 (0..65_536).map(|_| ("a".to_string(), Vec::new())).collect();
399 assert!(write_stored(&many).is_none());
400 assert!(entries(b"not a zip at all, not even twenty-two").is_none());
401 assert!(entries(b"").is_none());
402 }
403}