const LOCAL_FILE_HEADER_SIG: u32 = 0x04034b50;
const CENTRAL_DIR_SIG: u32 = 0x02014b50;
const END_OF_CENTRAL_DIR_SIG: u32 = 0x06054b50;
const VERSION: u16 = 20;
fn put_u16(buf: &mut Vec<u8>, v: u16) {
buf.extend_from_slice(&v.to_le_bytes());
}
fn put_u32(buf: &mut Vec<u8>, v: u32) {
buf.extend_from_slice(&v.to_le_bytes());
}
fn put_zero_u16s(buf: &mut Vec<u8>, n: usize) {
for _ in 0..n {
put_u16(buf, 0);
}
}
struct Entry {
name: Vec<u8>,
offset: u32,
crc32: u32,
size: u32,
}
pub(crate) struct ZipWriter {
buf: Vec<u8>,
entries: Vec<Entry>,
}
impl ZipWriter {
pub fn new() -> Self {
Self {
buf: Vec::new(),
entries: Vec::new(),
}
}
pub fn add_file(&mut self, name: &str, data: &[u8]) {
let name_bytes = name.as_bytes();
let crc = crc32(data);
let size = data.len() as u32;
let offset = self.buf.len() as u32;
put_u32(&mut self.buf, LOCAL_FILE_HEADER_SIG);
put_u16(&mut self.buf, VERSION);
put_zero_u16s(&mut self.buf, 3); put_u16(&mut self.buf, 0); put_u32(&mut self.buf, crc);
put_u32(&mut self.buf, size); put_u32(&mut self.buf, size); put_u16(&mut self.buf, name_bytes.len() as u16);
put_u16(&mut self.buf, 0); self.buf.extend_from_slice(name_bytes);
self.buf.extend_from_slice(data);
self.entries.push(Entry {
name: name_bytes.to_vec(),
offset,
crc32: crc,
size,
});
}
pub fn finish(mut self) -> Vec<u8> {
let cd_offset = self.buf.len() as u32;
for e in &self.entries {
put_u32(&mut self.buf, CENTRAL_DIR_SIG);
put_u16(&mut self.buf, VERSION); put_u16(&mut self.buf, VERSION); put_zero_u16s(&mut self.buf, 2); put_zero_u16s(&mut self.buf, 2); put_u32(&mut self.buf, e.crc32);
put_u32(&mut self.buf, e.size); put_u32(&mut self.buf, e.size); put_u16(&mut self.buf, e.name.len() as u16);
put_zero_u16s(&mut self.buf, 4); put_u32(&mut self.buf, 0); put_u32(&mut self.buf, e.offset);
self.buf.extend_from_slice(&e.name);
}
let cd_size = self.buf.len() as u32 - cd_offset;
let count = self.entries.len() as u16;
put_u32(&mut self.buf, END_OF_CENTRAL_DIR_SIG);
put_zero_u16s(&mut self.buf, 2); put_u16(&mut self.buf, count);
put_u16(&mut self.buf, count);
put_u32(&mut self.buf, cd_size);
put_u32(&mut self.buf, cd_offset);
put_u16(&mut self.buf, 0);
self.buf
}
}
fn crc32(data: &[u8]) -> u32 {
static TABLE: std::sync::LazyLock<[u32; 256]> = std::sync::LazyLock::new(|| {
let mut table = [0u32; 256];
for i in 0..256u32 {
let mut crc = i;
for _ in 0..8 {
if crc & 1 != 0 {
crc = 0xEDB88320 ^ (crc >> 1);
} else {
crc >>= 1;
}
}
table[i as usize] = crc;
}
table
});
let mut crc = 0xFFFF_FFFFu32;
for &byte in data {
crc = TABLE[((crc ^ byte as u32) & 0xFF) as usize] ^ (crc >> 8);
}
!crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn crc32_known_values() {
assert_eq!(crc32(b""), 0x00000000);
assert_eq!(crc32(b"123456789"), 0xCBF43926);
}
#[test]
fn zip_single_file() {
let mut w = ZipWriter::new();
w.add_file("hello.txt", b"Hello, world!");
let zip = w.finish();
assert_eq!(&zip[0..4], &LOCAL_FILE_HEADER_SIG.to_le_bytes());
let eocd_pos = zip.len() - 22;
assert_eq!(
&zip[eocd_pos..eocd_pos + 4],
&END_OF_CENTRAL_DIR_SIG.to_le_bytes()
);
assert_eq!(
u16::from_le_bytes([zip[eocd_pos + 8], zip[eocd_pos + 9]]),
1
);
}
#[test]
fn zip_multiple_files() {
let mut w = ZipWriter::new();
w.add_file("a.bin", &[0xAA; 100]);
w.add_file("b.bin", &[0xBB; 200]);
w.add_file("c.bin", &[0xCC; 50]);
let zip = w.finish();
let eocd_pos = zip.len() - 22;
assert_eq!(
u16::from_le_bytes([zip[eocd_pos + 8], zip[eocd_pos + 9]]),
3
);
}
#[test]
fn central_directory_offsets_valid() {
let data = b"test content here";
let mut w = ZipWriter::new();
w.add_file("test.bin", data);
let zip = w.finish();
let eocd_pos = zip.len() - 22;
let cd_offset =
u32::from_le_bytes(zip[eocd_pos + 16..eocd_pos + 20].try_into().unwrap()) as usize;
assert_eq!(
&zip[cd_offset..cd_offset + 4],
&CENTRAL_DIR_SIG.to_le_bytes()
);
let local_offset =
u32::from_le_bytes(zip[cd_offset + 42..cd_offset + 46].try_into().unwrap()) as usize;
assert_eq!(local_offset, 0);
let name_len = u16::from_le_bytes(
zip[local_offset + 26..local_offset + 28]
.try_into()
.unwrap(),
) as usize;
let data_start = local_offset + 30 + name_len;
assert_eq!(&zip[data_start..data_start + data.len()], data);
}
}