use std::io::Write;
use flate2::Compression;
use flate2::write::DeflateEncoder;
use crate::ZipEntry;
mod signature {
pub const LOCAL_FILE_HEADER: u32 = 0x04034b50;
pub const CENTRAL_DIRECTORY_FILE_HEADER: u32 = 0x02014b50;
pub const END_OF_CENTRAL_DIRECTORY: u32 = 0x06054b50;
}
const LOCAL_HEADER_LEN: u32 = 30;
const CENTRAL_HEADER_LEN: u32 = 46;
struct LE<'a>(&'a mut Vec<u8>);
impl<'a> LE<'a> {
fn u16(self, v: u16) -> Self {
self.0.extend_from_slice(&v.to_le_bytes());
self
}
fn u32(self, v: u32) -> Self {
self.0.extend_from_slice(&v.to_le_bytes());
self
}
}
fn write_local_header(
out: &mut Vec<u8>,
name: &[u8],
crc: u32,
csize: u32,
usize_: u32,
method: u16,
) {
LE(out)
.u32(signature::LOCAL_FILE_HEADER)
.u16(20) .u16(0) .u16(method) .u16(0) .u16(0x21) .u32(crc)
.u32(csize)
.u32(usize_)
.u16(name.len() as u16) .u16(0); out.extend_from_slice(name);
}
fn write_central_header(
cd: &mut Vec<u8>,
name: &[u8],
offset: u32,
crc: u32,
csize: u32,
usize_: u32,
method: u16,
) {
LE(cd)
.u32(signature::CENTRAL_DIRECTORY_FILE_HEADER)
.u16(20) .u16(20) .u16(0) .u16(method) .u16(0) .u16(0x21) .u32(crc)
.u32(csize)
.u32(usize_)
.u16(name.len() as u16) .u16(0) .u16(0) .u16(0) .u16(0) .u32(0) .u32(offset); cd.extend_from_slice(name);
}
impl ZipEntry {
pub fn repack(&self) -> Result<Vec<u8>, crate::ZipError> {
let names = self.namelist().count();
let mut output: Vec<u8> = Vec::new();
let mut central_directory: Vec<u8> =
Vec::with_capacity(names * CENTRAL_HEADER_LEN as usize);
let mut offset = 0u32;
let mut count = 0u16;
for name in self.namelist() {
if name.ends_with('/') || name.is_empty() {
continue;
}
let data = self.read(name)?.0;
let crc32 = crc32fast::hash(&data);
let (compressed, uncompressed) = deflate(&data);
let entry_len = u32::try_from(compressed.len())
.ok()
.and_then(|len| len.checked_add(LOCAL_HEADER_LEN + name.len() as u32));
let (Some(entry_len), Some(next_count)) = (entry_len, count.checked_add(1)) else {
return Err(crate::ZipError::FileTooLarge);
};
write_local_header(
&mut output,
name.as_bytes(),
crc32,
compressed.len() as u32,
uncompressed,
8, );
output
.write_all(&compressed)
.expect("write to Vec can't fail");
write_central_header(
&mut central_directory,
name.as_bytes(),
offset,
crc32,
compressed.len() as u32,
uncompressed,
8, );
offset = offset
.checked_add(entry_len)
.ok_or(crate::ZipError::FileTooLarge)?;
count = next_count;
}
let central_dir_offset = offset;
let central_dir_len =
u32::try_from(central_directory.len()).map_err(|_| crate::ZipError::FileTooLarge)?;
output
.write_all(¢ral_directory)
.expect("write to Vec can't fail");
LE(&mut output)
.u32(signature::END_OF_CENTRAL_DIRECTORY)
.u16(0) .u16(0) .u16(count) .u16(count) .u32(central_dir_len) .u32(central_dir_offset) .u16(0);
Ok(output)
}
}
fn deflate(data: &[u8]) -> (Vec<u8>, u32) {
let mut encoder = DeflateEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(data)
.expect("write to encoder can't fail");
let compressed = encoder.finish().expect("finish can't fail");
(compressed, data.len() as u32)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::FileCompressionType;
fn build_test_zip() -> Vec<u8> {
let mut out = Vec::new();
let mut cd = Vec::new();
let mut offset: u32 = 0;
let files: &[(&[u8], &[u8], u16)] = &[
(b"hello.txt", b"hello world!", 0),
(b"AndroidManifest.xml", b"<manifest/>", 8),
];
for (name, data, method) in files {
let payload = if *method == 0 {
data.to_vec()
} else {
deflate(data).0
};
let crc = crc32fast::hash(data);
write_local_header(
&mut out,
name,
crc,
payload.len() as u32,
data.len() as u32,
*method,
);
out.extend_from_slice(&payload);
write_central_header(
&mut cd,
name,
offset,
crc,
payload.len() as u32,
data.len() as u32,
*method,
);
offset += LOCAL_HEADER_LEN + name.len() as u32 + payload.len() as u32;
}
out.extend_from_slice(&cd);
LE(&mut out)
.u32(signature::END_OF_CENTRAL_DIRECTORY)
.u16(0)
.u16(0)
.u16(files.len() as u16)
.u16(files.len() as u16)
.u32(cd.len() as u32)
.u32(offset)
.u16(0);
out
}
#[test]
fn header_lengths_match_consts() {
let mut v = Vec::new();
write_local_header(&mut v, b"x", 0, 0, 0, 8);
assert_eq!(
v.len() as u32,
LOCAL_HEADER_LEN + 1,
"update LOCAL_HEADER_LEN"
);
let mut c = Vec::new();
write_central_header(&mut c, b"x", 0, 0, 0, 0, 8);
assert_eq!(
c.len() as u32,
CENTRAL_HEADER_LEN + 1,
"update CENTRAL_HEADER_LEN"
);
}
#[test]
fn repack_roundtrip_preserves_data() {
let original = build_test_zip();
let zip = ZipEntry::new(original).unwrap();
let repacked = zip.repack().unwrap();
let reparsed = ZipEntry::new(repacked).unwrap();
let mut names: Vec<_> = reparsed.namelist().collect();
names.sort();
assert_eq!(names, vec!["AndroidManifest.xml", "hello.txt"]);
assert_eq!(reparsed.read("hello.txt").unwrap().0, b"hello world!");
assert_eq!(
reparsed.read("AndroidManifest.xml").unwrap().0,
b"<manifest/>"
);
}
#[test]
fn repack_output_is_wellformed() {
let zip = ZipEntry::new(build_test_zip()).unwrap();
let repacked = zip.repack().unwrap();
assert!(ZipEntry::new(repacked).is_ok());
}
#[test]
fn repack_normalizes_tampered_entry() {
let mut original = build_test_zip();
assert_eq!(&original[0..4], &0x04034b50u32.to_le_bytes()[..]);
original[8..10].copy_from_slice(&55914u16.to_le_bytes());
original[18..22].copy_from_slice(&12u32.to_le_bytes());
let zip = ZipEntry::new(original.clone()).unwrap();
let (_, compression) = zip.read("hello.txt").unwrap();
assert_eq!(compression, FileCompressionType::StoredTampered);
let repacked = zip.repack().unwrap();
assert!(ZipEntry::new(repacked.clone()).is_ok());
let reparsed = ZipEntry::new(repacked).unwrap();
assert_eq!(reparsed.read("hello.txt").unwrap().0, b"hello world!");
}
}