use std::collections::HashSet;
use std::fs::File;
use std::io::{self, BufWriter, Read, Write};
use std::path::{Path, PathBuf};
const DOS_TIME: u16 = 0;
const DOS_DATE: u16 = (1 << 5) | 1;
const FLAG_UTF8: u16 = 0x0800;
const VERSION_NEEDED: u16 = 20;
const VERSION_MADE_BY: u16 = (3 << 8) | 20;
const FILE_ATTRIBUTES: u32 = 0o100_644 << 16;
const DIR_ATTRIBUTES: u32 = (0o040_755 << 16) | 0x10;
const MAX_ENTRIES: usize = 0xFFFF;
const MAX_U32: u64 = 0xFFFF_FFFF;
const SIG_LOCAL: u32 = 0x0403_4b50;
const SIG_CENTRAL: u32 = 0x0201_4b50;
const SIG_END: u32 = 0x0605_4b50;
const CRC_TABLE: [u32; 256] = {
let mut table = [0u32; 256];
let mut n = 0;
while n < 256 {
let mut c = n as u32;
let mut k = 0;
while k < 8 {
c = if c & 1 != 0 {
0xEDB8_8320 ^ (c >> 1)
} else {
c >> 1
};
k += 1;
}
table[n] = c;
n += 1;
}
table
};
pub fn crc32(bytes: &[u8]) -> u32 {
let mut crc = 0xFFFF_FFFFu32;
for byte in bytes {
crc = CRC_TABLE[((crc ^ u32::from(*byte)) & 0xFF) as usize] ^ (crc >> 8);
}
!crc
}
fn invalid(message: impl Into<String>) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message.into())
}
fn check_name(name: &str, directory: bool) -> io::Result<()> {
let body = if directory {
name.strip_suffix('/').unwrap_or(name)
} else {
name
};
if body.is_empty() {
return Err(invalid("a ZIP entry name is empty"));
}
if name.len() > 0xFFFF {
return Err(invalid("a ZIP entry name is longer than 65535 bytes"));
}
if name.starts_with('/') || name.contains('\\') {
return Err(invalid(format!(
"ZIP entry name {name:?} is absolute or uses a backslash"
)));
}
if name.chars().any(|c| c.is_control()) {
return Err(invalid(format!(
"ZIP entry name {name:?} has a control character"
)));
}
let bytes = body.as_bytes();
if bytes.len() >= 2 && bytes[0].is_ascii_alphabetic() && bytes[1] == b':' {
return Err(invalid(format!(
"ZIP entry name {name:?} has a drive prefix"
)));
}
for component in body.split('/') {
if component.is_empty() || component == "." || component == ".." {
return Err(invalid(format!(
"ZIP entry name {name:?} has an empty, `.` or `..` component"
)));
}
}
Ok(())
}
struct Entry {
name: String,
crc: u32,
size: u32,
offset: u32,
directory: bool,
}
pub struct StoredZipWriter<W: Write> {
out: W,
written: u64,
entries: Vec<Entry>,
seen: HashSet<String>,
}
impl<W: Write> StoredZipWriter<W> {
pub fn new(out: W) -> Self {
Self {
out,
written: 0,
entries: Vec::new(),
seen: HashSet::new(),
}
}
fn put(&mut self, bytes: &[u8]) -> io::Result<()> {
self.out.write_all(bytes)?;
self.written += bytes.len() as u64;
Ok(())
}
fn register(&mut self, name: &str, directory: bool) -> io::Result<String> {
check_name(name, directory)?;
if self.entries.len() >= MAX_ENTRIES {
return Err(invalid(
"a ZIP archive without ZIP64 holds at most 65535 entries",
));
}
let stored = if directory && !name.ends_with('/') {
format!("{name}/")
} else {
name.to_string()
};
if stored.len() > 0xFFFF {
return Err(invalid("a ZIP entry name is longer than 65535 bytes"));
}
let twin = if directory {
stored.trim_end_matches('/').to_string()
} else {
format!("{stored}/")
};
if self.seen.contains(&stored) || self.seen.contains(&twin) {
return Err(invalid(format!("ZIP entry name {name:?} is already used")));
}
self.seen.insert(stored.clone());
Ok(stored)
}
pub fn add_file(&mut self, name: &str, data: &[u8]) -> io::Result<()> {
let size = u32::try_from(data.len())
.map_err(|_| invalid("a ZIP entry without ZIP64 is under 4 GiB"))?;
let stored = self.register(name, false)?;
self.write_local(stored, data, size, false)
}
pub fn add_dir(&mut self, name: &str) -> io::Result<()> {
let stored = self.register(name, true)?;
self.write_local(stored, &[], 0, true)
}
fn write_local(
&mut self,
name: String,
data: &[u8],
size: u32,
directory: bool,
) -> io::Result<()> {
let offset = u32::try_from(self.written)
.map_err(|_| invalid("a ZIP entry starts past 4 GiB, which needs ZIP64"))?;
let crc = crc32(data);
let name_bytes = name.as_bytes();
let mut header = Vec::with_capacity(30 + name_bytes.len());
header.extend_from_slice(&SIG_LOCAL.to_le_bytes());
header.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
header.extend_from_slice(&FLAG_UTF8.to_le_bytes());
header.extend_from_slice(&0u16.to_le_bytes()); header.extend_from_slice(&DOS_TIME.to_le_bytes());
header.extend_from_slice(&DOS_DATE.to_le_bytes());
header.extend_from_slice(&crc.to_le_bytes());
header.extend_from_slice(&size.to_le_bytes()); header.extend_from_slice(&size.to_le_bytes()); header.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
header.extend_from_slice(&0u16.to_le_bytes()); header.extend_from_slice(name_bytes);
self.put(&header)?;
self.put(data)?;
self.entries.push(Entry {
name,
crc,
size,
offset,
directory,
});
Ok(())
}
pub fn finish(mut self) -> io::Result<W> {
let directory_offset = u32::try_from(self.written).map_err(|_| {
invalid("the ZIP central directory starts past 4 GiB, which needs ZIP64")
})?;
let mut central = Vec::new();
for entry in &self.entries {
let name = entry.name.as_bytes();
central.extend_from_slice(&SIG_CENTRAL.to_le_bytes());
central.extend_from_slice(&VERSION_MADE_BY.to_le_bytes());
central.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
central.extend_from_slice(&FLAG_UTF8.to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&DOS_TIME.to_le_bytes());
central.extend_from_slice(&DOS_DATE.to_le_bytes());
central.extend_from_slice(&entry.crc.to_le_bytes());
central.extend_from_slice(&entry.size.to_le_bytes());
central.extend_from_slice(&entry.size.to_le_bytes());
central.extend_from_slice(&(name.len() as u16).to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); let attributes = if entry.directory {
DIR_ATTRIBUTES
} else {
FILE_ATTRIBUTES
};
central.extend_from_slice(&attributes.to_le_bytes());
central.extend_from_slice(&entry.offset.to_le_bytes());
central.extend_from_slice(name);
}
let directory_size = u32::try_from(central.len())
.ok()
.filter(|size| u64::from(*size) + u64::from(directory_offset) <= MAX_U32)
.ok_or_else(|| {
invalid("the ZIP central directory ends past 4 GiB, which needs ZIP64")
})?;
let count = self.entries.len() as u16;
let mut end = Vec::with_capacity(22);
end.extend_from_slice(&SIG_END.to_le_bytes());
end.extend_from_slice(&0u16.to_le_bytes()); end.extend_from_slice(&0u16.to_le_bytes()); end.extend_from_slice(&count.to_le_bytes());
end.extend_from_slice(&count.to_le_bytes());
end.extend_from_slice(&directory_size.to_le_bytes());
end.extend_from_slice(&directory_offset.to_le_bytes());
end.extend_from_slice(&0u16.to_le_bytes()); self.put(¢ral)?;
self.put(&end)?;
Ok(self.out)
}
}
pub fn stored_zip_bytes(entries: &[(&str, &[u8])]) -> io::Result<Vec<u8>> {
let mut writer = StoredZipWriter::new(Vec::new());
for (name, data) in entries {
if name.ends_with('/') {
if !data.is_empty() {
return Err(invalid(format!("directory entry {name:?} has content")));
}
writer.add_dir(name)?;
} else {
writer.add_file(name, data)?;
}
}
writer.finish()
}
const FILE_ZIP_MADE_BY: u16 = 0x031E;
fn io_context(context: String, error: io::Error) -> io::Error {
io::Error::new(error.kind(), format!("{context}: {error}"))
}
pub fn crc_table() -> [u32; 256] {
CRC_TABLE
}
pub fn crc32_update(table: &[u32; 256], crc: u32, bytes: &[u8]) -> u32 {
let mut c = !crc;
for &b in bytes {
c = table[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
}
!c
}
fn file_crc_size(path: &Path) -> io::Result<(u32, u64)> {
let table = crc_table();
let mut f =
File::open(path).map_err(|e| io_context(format!("opening {}", path.display()), e))?;
let mut crc = 0u32;
let mut size = 0u64;
let mut buf = vec![0u8; 1 << 16];
loop {
let n = f
.read(&mut buf)
.map_err(|e| io_context(format!("reading {}", path.display()), e))?;
if n == 0 {
break;
}
crc = crc32_update(&table, crc, &buf[..n]);
size += n as u64;
}
Ok((crc, size))
}
pub fn write_zip(dest: &Path, entries: &[(String, PathBuf)]) -> io::Result<()> {
if entries.len() >= 0xFFFF {
return Err(invalid("too many files for a zip"));
}
for (name, _) in entries {
if name.len() > u16::MAX as usize {
return Err(invalid("archive name exceeds 65535 UTF-8 bytes"));
}
}
let io = |e| io_context(format!("writing {}", dest.display()), e);
let mut out = BufWriter::new(File::create(dest).map_err(io)?);
let mut offset: u64 = 0;
let mut central: Vec<u8> = Vec::new();
for (name, src) in entries {
let (crc, size) = file_crc_size(src)?;
if size >= 0xFFFF_FFFF || offset >= 0xFFFF_FFFF {
return Err(invalid(format!(
"{} is too large for a zip (ZIP64 is not written)",
src.display()
)));
}
let name_bytes = name.as_bytes();
checked_zip32(offset + 30 + name_bytes.len() as u64 + size)?;
checked_zip32(central.len() as u64 + 46 + name_bytes.len() as u64)?;
let mut local = Vec::with_capacity(30 + name_bytes.len());
local.extend_from_slice(&0x0403_4b50u32.to_le_bytes());
local.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
local.extend_from_slice(&FLAG_UTF8.to_le_bytes());
local.extend_from_slice(&0u16.to_le_bytes()); local.extend_from_slice(&DOS_TIME.to_le_bytes());
local.extend_from_slice(&DOS_DATE.to_le_bytes());
local.extend_from_slice(&crc.to_le_bytes());
local.extend_from_slice(&(size as u32).to_le_bytes());
local.extend_from_slice(&(size as u32).to_le_bytes());
local.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
local.extend_from_slice(&0u16.to_le_bytes());
local.extend_from_slice(name_bytes);
out.write_all(&local).map_err(io)?;
let mut f =
File::open(src).map_err(|e| io_context(format!("opening {}", src.display()), e))?;
let (copied_crc, copied) = copy_counted_crc(&mut f, &mut out).map_err(io)?;
if copied != size || copied_crc != crc {
return Err(invalid(format!("{} changed while zipping", src.display())));
}
central.extend_from_slice(&0x0201_4b50u32.to_le_bytes());
central.extend_from_slice(&FILE_ZIP_MADE_BY.to_le_bytes());
central.extend_from_slice(&VERSION_NEEDED.to_le_bytes());
central.extend_from_slice(&FLAG_UTF8.to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes());
central.extend_from_slice(&DOS_TIME.to_le_bytes());
central.extend_from_slice(&DOS_DATE.to_le_bytes());
central.extend_from_slice(&crc.to_le_bytes());
central.extend_from_slice(&(size as u32).to_le_bytes());
central.extend_from_slice(&(size as u32).to_le_bytes());
central.extend_from_slice(&(name_bytes.len() as u16).to_le_bytes());
central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&0u16.to_le_bytes()); central.extend_from_slice(&FILE_ATTRIBUTES.to_le_bytes());
central.extend_from_slice(&(offset as u32).to_le_bytes());
central.extend_from_slice(name_bytes);
offset += local.len() as u64 + size;
}
let cd_offset = offset;
checked_zip32(cd_offset)?;
checked_zip32(central.len() as u64)?;
out.write_all(¢ral).map_err(io)?;
let mut end = Vec::with_capacity(22);
end.extend_from_slice(&0x0605_4b50u32.to_le_bytes());
end.extend_from_slice(&0u16.to_le_bytes());
end.extend_from_slice(&0u16.to_le_bytes());
end.extend_from_slice(&(entries.len() as u16).to_le_bytes());
end.extend_from_slice(&(entries.len() as u16).to_le_bytes());
end.extend_from_slice(&(central.len() as u32).to_le_bytes());
end.extend_from_slice(&(cd_offset as u32).to_le_bytes());
end.extend_from_slice(&0u16.to_le_bytes());
out.write_all(&end).map_err(io)?;
out.flush().map_err(io)?;
Ok(())
}
fn copy_counted_crc<R: Read, W: Write>(src: &mut R, out: &mut W) -> io::Result<(u32, u64)> {
let table = crc_table();
let mut crc = 0u32;
let mut size = 0u64;
let mut buf = vec![0u8; 1 << 16];
loop {
let n = src.read(&mut buf)?;
if n == 0 {
break;
}
crc = crc32_update(&table, crc, &buf[..n]);
out.write_all(&buf[..n])?;
size += n as u64;
}
Ok((crc, size))
}
fn checked_zip32(value: u64) -> io::Result<()> {
if value >= u32::MAX as u64 {
Err(invalid("archive exceeds ZIP32 limits"))
} else {
Ok(())
}
}
pub fn read_zip(path: &Path) -> io::Result<Vec<(String, Vec<u8>)>> {
let data = std::fs::read(path)?;
parse_zip(&data)
}
pub fn read_zip_limited(path: &Path, max_bytes: u64) -> io::Result<Vec<(String, Vec<u8>)>> {
let file =
File::open(path).map_err(|e| io_context(format!("opening {}", path.display()), e))?;
let mut data = Vec::new();
file.take(max_bytes.saturating_add(1))
.read_to_end(&mut data)
.map_err(|e| io_context(format!("reading {}", path.display()), e))?;
if data.len() as u64 > max_bytes {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"{} is larger than the {max_bytes} byte limit for a zip",
path.display()
),
));
}
parse_zip(&data)
}
fn parse_zip(data: &[u8]) -> io::Result<Vec<(String, Vec<u8>)>> {
let bad = || io::Error::new(io::ErrorKind::InvalidData, "invalid STORED zip");
let u16at = |o: usize| -> io::Result<usize> {
let bytes: [u8; 2] = data
.get(o..o.checked_add(2).ok_or_else(bad)?)
.ok_or_else(bad)?
.try_into()
.map_err(|_| bad())?;
Ok(u16::from_le_bytes(bytes) as usize)
};
let u32at = |o: usize| -> io::Result<u32> {
let bytes: [u8; 4] = data
.get(o..o.checked_add(4).ok_or_else(bad)?)
.ok_or_else(bad)?
.try_into()
.map_err(|_| bad())?;
Ok(u32::from_le_bytes(bytes))
};
let eocd = data.len().checked_sub(22).ok_or_else(bad)?;
if u32at(eocd)? != 0x0605_4b50
|| u16at(eocd + 4)? != 0
|| u16at(eocd + 6)? != 0
|| u16at(eocd + 20)? != 0
{
return Err(bad());
}
let count = u16at(eocd + 10)?;
if count >= 0xffff || u16at(eocd + 8)? != count {
return Err(bad());
}
let central_size = u32at(eocd + 12)? as usize;
let central_start = u32at(eocd + 16)? as usize;
if central_start.checked_add(central_size) != Some(eocd) {
return Err(bad());
}
let table = crc_table();
let mut p = central_start;
let mut local_end = 0;
let mut out = Vec::new();
for _ in 0..count {
let header_end = p.checked_add(46).filter(|&n| n <= eocd).ok_or_else(bad)?;
if u32at(p)? != 0x0201_4b50
|| u16at(p + 8)? != 0x0800
|| u16at(p + 10)? != 0
|| u16at(p + 30)? != 0
|| u16at(p + 32)? != 0
|| u16at(p + 34)? != 0
{
return Err(bad());
}
let crc = u32at(p + 16)?;
let size = u32at(p + 24)? as usize;
if u32at(p + 20)? as usize != size {
return Err(bad());
}
let name_len = u16at(p + 28)?;
let next = header_end
.checked_add(name_len)
.filter(|&n| n <= eocd)
.ok_or_else(bad)?;
let name_bytes = &data[header_end..next];
let name = std::str::from_utf8(name_bytes)
.map_err(|_| bad())?
.to_owned();
let local = u32at(p + 42)? as usize;
let start = local
.checked_add(30)
.and_then(|n| n.checked_add(name_len))
.filter(|&n| n <= central_start)
.ok_or_else(bad)?;
let end = start
.checked_add(size)
.filter(|&n| n <= central_start)
.ok_or_else(bad)?;
if local != local_end
|| u32at(local)? != 0x0403_4b50
|| u16at(local + 6)? != 0x0800
|| u16at(local + 8)? != 0
|| u32at(local + 14)? != crc
|| u32at(local + 18)? as usize != size
|| u32at(local + 22)? as usize != size
|| u16at(local + 26)? != name_len
|| u16at(local + 28)? != 0
|| &data[local + 30..start] != name_bytes
{
return Err(bad());
}
let bytes = &data[start..end];
if crc32_update(&table, 0, bytes) != crc {
return Err(bad());
}
out.push((name, bytes.to_vec()));
local_end = end;
p = next;
}
if p != eocd || local_end != central_start {
return Err(bad());
}
Ok(out)
}