use crate::container::Container;
use crate::crc32::crc32;
use crate::plan::{Config, compress_objects_checksummed};
use crate::unit::DeflateError;
const U32_MAX: u64 = u32::MAX as u64;
pub const MAX_ENTRIES: usize = u16::MAX as usize;
const METHOD_STORE: u16 = 0;
const METHOD_DEFLATE: u16 = 8;
#[derive(Debug, Clone, Copy)]
pub struct Entry<'a> {
pub name: &'a str,
pub data: &'a [u8],
}
impl<'a> Entry<'a> {
pub fn new(name: &'a str, data: &'a [u8]) -> Self {
Entry { name, data }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Placed {
pub offset: u64,
pub compressed: u64,
pub uncompressed: u64,
pub method: u16,
pub crc: u32,
}
fn validate_name(name: &str) -> Result<(), DeflateError> {
if name.is_empty() {
return Err(DeflateError::ZipLimit("entry name is empty"));
}
if name.len() > u16::MAX as usize {
return Err(DeflateError::ZipLimit("entry name exceeds 65535 bytes"));
}
if name.starts_with('/') || name.starts_with('\\') {
return Err(DeflateError::ZipLimit("entry name is absolute"));
}
if name.split(['/', '\\']).any(|c| c == "..") {
return Err(DeflateError::ZipLimit(
"entry name contains a `..` component",
));
}
let b = name.as_bytes();
if b.len() >= 2 && b[1] == b':' && b[0].is_ascii_alphabetic() {
return Err(DeflateError::ZipLimit(
"entry name has a drive-letter prefix",
));
}
Ok(())
}
fn le16(v: u16) -> [u8; 2] {
v.to_le_bytes()
}
fn le32(v: u32) -> [u8; 4] {
v.to_le_bytes()
}
pub fn zip_write(entries: &[Entry<'_>], cfg: &Config) -> Result<Vec<u8>, DeflateError> {
let (bytes, _) = zip_write_placed(entries, cfg)?;
Ok(bytes)
}
pub fn zip_write_placed(
entries: &[Entry<'_>],
cfg: &Config,
) -> Result<(Vec<u8>, Vec<Placed>), DeflateError> {
if entries.len() > MAX_ENTRIES {
return Err(DeflateError::ZipLimit(
"more than 65535 entries; this writer does not emit ZIP64",
));
}
for e in entries {
validate_name(e.name)?;
if e.data.len() as u64 > U32_MAX {
return Err(DeflateError::ZipLimit(
"entry larger than 4 GiB; this writer does not emit ZIP64",
));
}
}
let payloads: Vec<&[u8]> = entries.iter().map(|e| e.data).collect();
let compressed = compress_objects_checksummed(
&payloads,
&Config {
container: Container::Raw,
..*cfg
},
)?;
let mut out: Vec<u8> =
Vec::with_capacity(compressed.iter().map(|(b, _)| b.len() + 76).sum::<usize>() + 22);
let mut placed: Vec<Placed> = Vec::with_capacity(entries.len());
for (e, (body, crc)) in entries.iter().zip(&compressed) {
let raw_len = e.data.len();
let store = body.len() >= raw_len;
let (method, payload): (u16, &[u8]) = if store {
(METHOD_STORE, e.data)
} else {
(METHOD_DEFLATE, body)
};
let crc = if *crc == 0 && raw_len != 0 {
crc32(0, e.data)
} else {
*crc
};
let offset = out.len() as u64;
if offset > U32_MAX {
return Err(DeflateError::ZipLimit(
"archive exceeds 4 GiB; this writer does not emit ZIP64",
));
}
out.extend_from_slice(b"PK\x03\x04");
out.extend_from_slice(&le16(20)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(method));
out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0x21)); out.extend_from_slice(&le32(crc));
out.extend_from_slice(&le32(payload.len() as u32));
out.extend_from_slice(&le32(raw_len as u32));
out.extend_from_slice(&le16(e.name.len() as u16));
out.extend_from_slice(&le16(0)); out.extend_from_slice(e.name.as_bytes());
out.extend_from_slice(payload);
placed.push(Placed {
offset,
compressed: payload.len() as u64,
uncompressed: raw_len as u64,
method,
crc,
});
}
let cd_offset = out.len() as u64;
if cd_offset > U32_MAX {
return Err(DeflateError::ZipLimit(
"archive exceeds 4 GiB; this writer does not emit ZIP64",
));
}
for (e, p) in entries.iter().zip(&placed) {
out.extend_from_slice(b"PK\x01\x02");
out.extend_from_slice(&le16(0x031e)); out.extend_from_slice(&le16(20)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(p.method));
out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0x21)); out.extend_from_slice(&le32(p.crc));
out.extend_from_slice(&le32(p.compressed as u32));
out.extend_from_slice(&le32(p.uncompressed as u32));
out.extend_from_slice(&le16(e.name.len() as u16));
out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le32(0o100644 << 16));
out.extend_from_slice(&le32(p.offset as u32));
out.extend_from_slice(e.name.as_bytes());
}
let cd_size = out.len() as u64 - cd_offset;
if cd_size > U32_MAX {
return Err(DeflateError::ZipLimit("central directory exceeds 4 GiB"));
}
out.extend_from_slice(b"PK\x05\x06");
out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(0)); out.extend_from_slice(&le16(entries.len() as u16));
out.extend_from_slice(&le16(entries.len() as u16));
out.extend_from_slice(&le32(cd_size as u32));
out.extend_from_slice(&le32(cd_offset as u32));
out.extend_from_slice(&le16(0));
gatling::functional_status(
"ldeflate",
"zip_write",
true,
"entries fanned out over one pass",
);
Ok((out, placed))
}
pub fn jar_write(
manifest: &str,
entries: &[Entry<'_>],
cfg: &Config,
) -> Result<Vec<u8>, DeflateError> {
let mut body = manifest.to_string();
if !body.ends_with('\n') {
body.push('\n');
}
if !body.ends_with("\r\n\r\n") && !body.ends_with("\n\n") {
body.push('\n');
}
let mut all = Vec::with_capacity(entries.len() + 1);
all.push(Entry {
name: "META-INF/MANIFEST.MF",
data: body.as_bytes(),
});
all.extend_from_slice(entries);
zip_write(&all, cfg)
}
#[cfg(test)]
mod tests {
use super::*;
fn payload(tag: &str, n: usize) -> Vec<u8> {
let mut v = Vec::with_capacity(n);
while v.len() < n {
v.extend_from_slice(tag.as_bytes());
v.extend_from_slice(b" pub fn run() -> usize { 1 }\n");
}
v.truncate(n);
v
}
#[test]
fn entries_round_trip_and_offsets_point_at_local_headers() {
use std::io::Read;
let a = payload("alpha", 40);
let b = payload("beta", 200_000);
let c: Vec<u8> = Vec::new();
let entries = vec![
Entry::new("a.txt", &a),
Entry::new("dir/b.txt", &b),
Entry::new("empty.txt", &c),
];
let (zip, placed) = zip_write_placed(&entries, &Config::default()).unwrap();
assert_eq!(placed.len(), 3);
for (e, p) in entries.iter().zip(&placed) {
let o = p.offset as usize;
assert_eq!(
&zip[o..o + 4],
b"PK\x03\x04",
"{}: offset is not a local header",
e.name
);
let nlen = u16::from_le_bytes([zip[o + 26], zip[o + 27]]) as usize;
let elen = u16::from_le_bytes([zip[o + 28], zip[o + 29]]) as usize;
let name = std::str::from_utf8(&zip[o + 30..o + 30 + nlen]).unwrap();
assert_eq!(name, e.name, "name at the recorded offset is wrong");
let start = o + 30 + nlen + elen;
let body = &zip[start..start + p.compressed as usize];
let got = if p.method == METHOD_STORE {
body.to_vec()
} else {
let mut v = Vec::new();
flate2::read::DeflateDecoder::new(body)
.read_to_end(&mut v)
.unwrap();
v
};
assert_eq!(got, e.data, "{}: payload differs", e.name);
assert_eq!(
p.crc,
crc32(0, e.data),
"{}: CRC in the header is wrong",
e.name
);
}
}
#[test]
fn incompressible_entries_are_stored_not_deflated() {
let mut noise = Vec::with_capacity(200_000);
let mut s: u64 = 0x243F6A8885A308D3;
while noise.len() < 200_000 {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
noise.extend_from_slice(&s.to_le_bytes());
}
let text = payload("compressible", 200_000);
let entries = vec![
Entry::new("noise.bin", &noise),
Entry::new("text.txt", &text),
];
let (_, placed) = zip_write_placed(&entries, &Config::default()).unwrap();
assert_eq!(
placed[0].method, METHOD_STORE,
"high-entropy entry should be stored"
);
assert_eq!(
placed[0].compressed,
noise.len() as u64,
"stored entry must be verbatim"
);
assert_eq!(
placed[1].method, METHOD_DEFLATE,
"compressible entry should be deflated"
);
assert!(
placed[1].compressed < text.len() as u64,
"deflate must actually shrink it"
);
}
#[test]
fn archives_are_byte_identical_across_worker_counts() {
let big = payload("large", 9 << 20); let small: Vec<Vec<u8>> = (0..200)
.map(|i| payload(&format!("e{i}"), 100 + i))
.collect();
let mut entries = vec![Entry::new("big.bin", &big)];
let names: Vec<String> = (0..200).map(|i| format!("small/{i}.txt")).collect();
for i in 0..200 {
entries.push(Entry::new(&names[i], &small[i]));
}
let base = zip_write(&entries, &Config::default().workers(1)).unwrap();
for w in [2usize, 8, 32] {
assert_eq!(
zip_write(&entries, &Config::default().workers(w)).unwrap(),
base,
"workers={w}: archive bytes differ"
);
}
}
#[test]
fn hostile_entry_names_are_refused() {
let d = b"x";
for bad in [
"../escape.txt",
"a/../../etc/passwd",
"/absolute.txt",
"\\windows.txt",
"C:/drive.txt",
"",
] {
let e = [Entry::new(bad, d)];
assert!(
zip_write(&e, &Config::default()).is_err(),
"hostile name {bad:?} was accepted"
);
}
for good in [
"a.txt",
"dir/sub/file.txt",
"..hidden",
"a..b/c.txt",
"META-INF/MANIFEST.MF",
] {
let e = [Entry::new(good, d)];
assert!(
zip_write(&e, &Config::default()).is_ok(),
"legitimate name {good:?} was refused — the guard is too aggressive"
);
}
}
#[test]
fn too_many_entries_is_refused_not_truncated() {
let d = b"x";
let names: Vec<String> = (0..MAX_ENTRIES + 1).map(|i| format!("f{i}")).collect();
let entries: Vec<Entry> = names.iter().map(|n| Entry::new(n, d)).collect();
let err = zip_write(&entries, &Config::default()).unwrap_err();
assert!(
matches!(err, DeflateError::ZipLimit(_)),
"wrong error: {err}"
);
let ok = zip_write(&entries[..MAX_ENTRIES], &Config::default());
assert!(
ok.is_ok(),
"an archive of exactly 65535 entries must be accepted"
);
}
#[test]
fn jar_puts_the_manifest_first_and_terminates_it() {
let c = payload("class", 500);
let jar = jar_write(
"Manifest-Version: 1.0\nMain-Class: com.example.App",
&[Entry::new("com/example/App.class", &c)],
&Config::default(),
)
.unwrap();
let nlen = u16::from_le_bytes([jar[26], jar[27]]) as usize;
let name = std::str::from_utf8(&jar[30..30 + nlen]).unwrap();
assert_eq!(
name, "META-INF/MANIFEST.MF",
"the manifest is not the first entry"
);
let elen = u16::from_le_bytes([jar[28], jar[29]]) as usize;
let start = 30 + nlen + elen;
let clen = u32::from_le_bytes([jar[18], jar[19], jar[20], jar[21]]) as usize;
let method = u16::from_le_bytes([jar[8], jar[9]]);
let body = &jar[start..start + clen];
let text = if method == METHOD_STORE {
String::from_utf8(body.to_vec()).unwrap()
} else {
use std::io::Read;
let mut v = Vec::new();
flate2::read::DeflateDecoder::new(body)
.read_to_end(&mut v)
.unwrap();
String::from_utf8(v).unwrap()
};
assert!(
text.starts_with("Manifest-Version: 1.0"),
"manifest body wrong: {text:?}"
);
assert!(
text.ends_with("\n\n"),
"manifest must end with a blank line, got {text:?}"
);
}
#[test]
fn empty_archive_is_a_valid_eocd() {
let z = zip_write(&[], &Config::default()).unwrap();
assert_eq!(z.len(), 22, "an empty ZIP is exactly an EOCD record");
assert_eq!(&z[..4], b"PK\x05\x06");
assert_eq!(
u16::from_le_bytes([z[10], z[11]]),
0,
"entry count must be 0"
);
}
}