use pith_digest::Error;
use pith_digest::crc32;
use pith_inflate::{Limits, inflate_raw};
use pith_zip::ZipArchive;
fn le16(out: &mut Vec<u8>, v: u16) {
out.extend_from_slice(&v.to_le_bytes());
}
fn le32(out: &mut Vec<u8>, v: u32) {
out.extend_from_slice(&v.to_le_bytes());
}
fn stored_deflate(data: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len() + 10);
if data.len() <= 65_535 {
out.push(0x01);
le16(&mut out, data.len() as u16);
le16(&mut out, !(data.len() as u16));
out.extend_from_slice(data);
} else {
let split = 65_535usize;
out.push(0x00);
le16(&mut out, split as u16);
le16(&mut out, !(split as u16));
out.extend_from_slice(&data[..split]);
out.push(0x01);
let rest = (data.len() - split) as u16;
le16(&mut out, rest);
le16(&mut out, !rest);
out.extend_from_slice(&data[split..]);
}
out
}
struct Spec<'a> {
name: &'a str,
body: &'a [u8],
method: u16,
descriptor: bool,
descriptor_sig: bool,
extra_flags: u16,
crc_override: Option<u32>,
}
fn stored<'a>(name: &'a str, body: &'a [u8]) -> Spec<'a> {
Spec {
name,
body,
method: 0,
descriptor: false,
descriptor_sig: false,
extra_flags: 0,
crc_override: None,
}
}
fn deflated<'a>(name: &'a str, body: &'a [u8]) -> Spec<'a> {
Spec {
name,
body,
method: 8,
descriptor: false,
descriptor_sig: false,
extra_flags: 0,
crc_override: None,
}
}
fn descriptor<'a>(name: &'a str, body: &'a [u8], sig: bool) -> Spec<'a> {
Spec {
name,
body,
method: 8,
descriptor: true,
descriptor_sig: sig,
extra_flags: 0,
crc_override: None,
}
}
fn build_zip(specs: &[Spec], comment: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut directory = Vec::new();
for spec in specs {
let payload = if spec.method == 8 {
stored_deflate(spec.body)
} else {
spec.body.to_vec()
};
let crc = spec.crc_override.unwrap_or_else(|| crc32(spec.body));
let flags = spec.extra_flags | if spec.descriptor { 1 << 3 } else { 0 };
let cs = payload.len() as u32;
let us = spec.body.len() as u32;
let name = spec.name.as_bytes();
let local_offset = out.len() as u32;
le32(&mut out, 0x0403_4b50);
le16(&mut out, 20); le16(&mut out, flags);
le16(&mut out, spec.method);
le16(&mut out, 0x7e21); le16(&mut out, 0x0121); if spec.descriptor {
le32(&mut out, 0);
le32(&mut out, 0);
le32(&mut out, 0);
} else {
le32(&mut out, crc);
le32(&mut out, cs);
le32(&mut out, us);
}
le16(&mut out, name.len() as u16);
le16(&mut out, 0);
out.extend_from_slice(name);
out.extend_from_slice(&payload);
if spec.descriptor {
if spec.descriptor_sig {
le32(&mut out, 0x0807_4b50);
}
le32(&mut out, crc);
le32(&mut out, cs);
le32(&mut out, us);
}
directory.push((flags, spec.method, crc, cs, us, name, local_offset));
}
let cd_offset = out.len() as u32;
for (flags, method, crc, cs, us, name, local_offset) in &directory {
le32(&mut out, 0x0201_4b50);
le16(&mut out, 20); le16(&mut out, 20); le16(&mut out, *flags);
le16(&mut out, *method);
le16(&mut out, 0x7e21);
le16(&mut out, 0x0121);
le32(&mut out, *crc);
le32(&mut out, *cs);
le32(&mut out, *us);
le16(&mut out, name.len() as u16);
le16(&mut out, 0); le16(&mut out, 0); le16(&mut out, 0); le16(&mut out, 0); le32(&mut out, 0); le32(&mut out, *local_offset);
out.extend_from_slice(name);
}
let cd_size = out.len() as u32 - cd_offset;
le32(&mut out, 0x0605_4b50);
le16(&mut out, 0);
le16(&mut out, 0);
le16(&mut out, specs.len() as u16);
le16(&mut out, specs.len() as u16);
le32(&mut out, cd_size);
le32(&mut out, cd_offset);
le16(&mut out, comment.len() as u16);
out.extend_from_slice(comment);
out
}
fn first_entry_field(bytes: &[u8], field: usize) -> usize {
let eocd = bytes
.windows(4)
.rposition(|w| w == [0x50, 0x4b, 0x05, 0x06])
.expect("test archive has an eocd");
let cd =
u32::from_le_bytes(bytes[eocd + 16..eocd + 20].try_into().expect("cd offset")) as usize;
cd + field
}
#[test]
fn stored_and_deflated_entries_round_trip() {
let a = b"stored payload, no compression at all";
let b: Vec<u8> = (0..3000).map(|i| (i % 251) as u8).collect();
let zip = build_zip(&[stored("a.txt", a), deflated("b.bin", &b)], b"");
let zip = ZipArchive::new(&zip).expect("parse");
assert_eq!(zip.len(), 2);
assert_eq!(zip.entries()[0].name(), "a.txt");
assert_eq!(zip.entries()[0].method(), 0);
assert_eq!(zip.entries()[1].name(), "b.bin");
assert_eq!(zip.entries()[1].method(), 8);
assert_eq!(zip.extract_by_name("a.txt").unwrap(), a);
assert_eq!(zip.extract_by_name("b.bin").unwrap(), b);
}
#[test]
fn stored_block_deflate_is_legal_deflate() {
let data: Vec<u8> = (0..70_000).map(|i| (i % 253) as u8).collect();
assert_eq!(
inflate_raw(&stored_deflate(&data), &Limits::default()).unwrap(),
data
);
}
#[test]
fn multi_block_deflated_entry_extracts() {
let data: Vec<u8> = (0..70_000).map(|i| (i % 253) as u8).collect();
let zip = build_zip(&[deflated("big.bin", &data)], b"");
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(zip.extract_by_name("big.bin").unwrap(), data);
}
#[test]
fn data_descriptor_entries_extract() {
let body = b"descriptor-backed deflate entry";
for sig in [true, false] {
let zip = build_zip(&[descriptor("d.bin", body, sig)], b"");
let zip = ZipArchive::new(&zip).unwrap();
let entry = zip.by_name("d.bin").unwrap();
assert!(entry.uses_data_descriptor());
assert_eq!(zip.extract(entry).unwrap(), body);
}
}
#[test]
fn data_descriptor_mismatch_is_bad_value() {
let body = b"descriptor-backed deflate entry";
let mut zip = build_zip(&[descriptor("d.bin", body, true)], b"");
let eocd = zip
.windows(4)
.rposition(|w| w == [0x50, 0x4b, 0x05, 0x06])
.unwrap();
let cd = u32::from_le_bytes(zip[eocd + 16..eocd + 20].try_into().unwrap()) as usize;
let dd_us = cd - 4;
zip[dd_us] ^= 0xFF;
let zip = ZipArchive::new(&zip).unwrap();
let entry = zip.by_name("d.bin").unwrap();
assert_eq!(
zip.extract(entry),
Err(Error::BadValue("zip data descriptor mismatch"))
);
}
#[test]
fn eocd_is_found_past_a_comment() {
let comment: Vec<u8> = (0..100).map(|i| b' ' + (i % 90) as u8).collect();
let zip = build_zip(&[stored("a.txt", b"with comment")], &comment);
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(zip.extract_by_name("a.txt").unwrap(), b"with comment");
}
#[test]
fn eocd_signature_inside_comment_is_ignored() {
let mut comment = vec![0u8; 64];
comment[10..14].copy_from_slice(&[0x50, 0x4b, 0x05, 0x06]);
let zip = build_zip(&[stored("a.txt", b"pk in comment")], &comment);
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(zip.extract_by_name("a.txt").unwrap(), b"pk in comment");
}
#[test]
fn empty_archive_parses() {
let zip = build_zip(&[], b"");
let zip = ZipArchive::new(&zip).unwrap();
assert!(zip.is_empty());
assert!(zip.by_name("anything").is_none());
assert_eq!(
zip.extract_by_name("anything"),
Err(Error::BadValue("no such zip entry"))
);
}
#[test]
fn crc_mismatch_is_a_named_error() {
let mut zip = build_zip(&[stored("a.txt", b"check me")], b"");
let at = first_entry_field(&zip, 16);
zip[at] ^= 0xFF;
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::BadValue("zip entry crc32 mismatch"))
);
}
#[test]
fn size_mismatch_is_a_named_error() {
let mut zip = build_zip(&[stored("a.txt", b"check me")], b"");
let at = first_entry_field(&zip, 24); zip[at] ^= 0xFF;
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::BadValue("zip stored entry size"))
);
}
#[test]
fn deflated_size_mismatch_is_a_named_error() {
let mut zip = build_zip(&[deflated("b.bin", b"check me")], b"");
let at = first_entry_field(&zip, 24); zip[at] ^= 0xFF;
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("b.bin"),
Err(Error::BadValue("zip entry size mismatch"))
);
}
#[test]
fn zip64_eocd_is_refused() {
let mut zip = build_zip(&[stored("a.txt", b"x")], b"");
let eocd = zip
.windows(4)
.rposition(|w| w == [0x50, 0x4b, 0x05, 0x06])
.unwrap();
for i in 0..4 {
zip[eocd + 8 + i] = 0xFF;
}
assert_eq!(
ZipArchive::new(&zip).unwrap_err(),
Error::Unsupported("zip64 archive")
);
}
#[test]
fn local_flags_mismatch_is_a_named_error() {
let mut zip = build_zip(&[stored("a.txt", b"flags")], b"");
zip[6] = 0xFF; let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::BadValue("zip flags differ local vs central"))
);
}
#[test]
fn local_method_mismatch_is_a_named_error() {
let mut zip = build_zip(&[stored("a.txt", b"method")], b"");
zip[8] = 0x08; let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::BadValue("zip method differs local vs central"))
);
}
#[test]
fn multi_disk_is_refused() {
let mut zip = build_zip(&[stored("a.txt", b"x")], b"");
let eocd = zip
.windows(4)
.rposition(|w| w == [0x50, 0x4b, 0x05, 0x06])
.unwrap();
zip[eocd + 8] = 0; zip[eocd + 9] = 0;
assert_eq!(
ZipArchive::new(&zip).unwrap_err(),
Error::Unsupported("multi-disk zip archive")
);
}
#[test]
fn encrypted_entry_is_refused() {
let mut spec = stored("a.txt", b"secret");
spec.extra_flags = 1;
let zip = build_zip(&[spec], b"");
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::Unsupported("encrypted zip entry"))
);
}
#[test]
fn unsupported_method_is_refused() {
let mut spec = stored("a.txt", b"shrunk");
spec.method = 9; let zip = build_zip(&[spec], b"");
let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::Unsupported("zip compression method"))
);
}
#[test]
fn bad_local_signature_is_a_magic_error() {
let mut zip = build_zip(&[stored("a.txt", b"payload")], b"");
zip[0] = 0x00; let zip = ZipArchive::new(&zip).unwrap();
assert_eq!(
zip.extract_by_name("a.txt"),
Err(Error::InvalidMagic {
what: "zip local file header"
})
);
}
#[test]
fn every_prefix_errors_and_never_panics() {
let content = b"prefix fuzz payload without PK markers";
let zip = build_zip(
&[
stored("a.txt", content),
deflated("b.bin", b"deflated prefix payload"),
descriptor("d.bin", b"descriptor payload", true),
],
b"trailing comment",
);
assert!(!content.windows(4).any(|w| w == [0x50, 0x4b, 0x05, 0x06]));
for i in 0..zip.len() {
let result = std::panic::catch_unwind(|| ZipArchive::new(&zip[..i]).map(|_| ()));
match result {
Ok(Ok(())) => panic!("prefix of {i} bytes parsed as a zip"),
Ok(Err(_)) => {}
Err(_) => panic!("prefix of {i} bytes panicked"),
}
}
}
#[test]
fn corrupted_bytes_never_yield_wrong_data() {
let zip = build_zip(
&[stored("a.txt", b"corrupt me"), deflated("b.bin", b"or me")],
b"",
);
let want = b"corrupt me".to_vec();
for i in 0..zip.len() {
let mut bad = zip.clone();
bad[i] ^= 0xA5;
match ZipArchive::new(&bad) {
Err(_) => {}
Ok(arc) => {
for entry in arc.entries() {
if let Ok(bytes) = arc.extract(entry) {
if entry.name() == "a.txt" {
assert_eq!(bytes, want, "mutation at byte {i} corrupted output");
}
}
}
}
}
}
}