mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
* craft-zip: ZIP preflight, bounded entry read and Budget (from craft-archive, #129) The ZIP half of the craft-archive proposal (#126, #129): an allocation-free check of the end records and central directory that runs before a ZIP reader indexes the archive, a bounded entry read that distrusts the declared size, and the named Budget both charge. The XML half of #129 is not carried: craft-xml bounds XML now. * craft-zip: Archive (entry-count guard, duplicate names, budgeted reads) and Writer check_entry_count is craft-opc's guard: the declared entry count, ZIP64 included, checked on the raw bytes before zip parses the central directory. Archive reads entries within a per-entry limit and a total Budget through read_entry. Duplicate names are found by counting the central directory headers against the entries zip kept. Writer is craft-opc's ZipOut: stored or deflated entries, ZIP64 for large ones, one fixed timestamp. --------- Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
59 lines
2.1 KiB
Rust
59 lines
2.1 KiB
Rust
//! A ZIP archive being written.
|
|
|
|
use std::io::{Cursor, Write};
|
|
|
|
use crate::Error;
|
|
|
|
/// How an entry is stored.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Compression {
|
|
/// Uncompressed: a `mimetype` entry that must be readable in place, or data that is
|
|
/// compressed already.
|
|
Stored,
|
|
/// Deflate at level 6.
|
|
Deflated,
|
|
/// Deflate at the given level (0 to 9; higher is clamped).
|
|
Level(u8),
|
|
}
|
|
|
|
/// A ZIP archive being written into memory. The output is deterministic: the same (1980)
|
|
/// timestamp on every entry. Entries are written in the order they are added.
|
|
pub struct Writer {
|
|
zip: zip::ZipWriter<Cursor<Vec<u8>>>,
|
|
}
|
|
|
|
impl Default for Writer {
|
|
fn default() -> Writer {
|
|
Writer { zip: zip::ZipWriter::new(Cursor::new(Vec::new())) }
|
|
}
|
|
}
|
|
|
|
impl Writer {
|
|
pub fn new() -> Writer {
|
|
Writer::default()
|
|
}
|
|
|
|
/// Adds an entry. A name the archive already has is an error.
|
|
pub fn add(&mut self, name: &str, data: &[u8], compression: Compression) -> Result<(), Error> {
|
|
let (method, level) = match compression {
|
|
Compression::Stored => (zip::CompressionMethod::Stored, None),
|
|
Compression::Deflated => (zip::CompressionMethod::Deflated, Some(6)),
|
|
Compression::Level(l) => (zip::CompressionMethod::Deflated, Some(i64::from(l.min(9)))),
|
|
};
|
|
let options = zip::write::SimpleFileOptions::default()
|
|
.compression_method(method)
|
|
.compression_level(level)
|
|
.last_modified_time(zip::DateTime::default())
|
|
// Deflate can grow incompressible data a little, so ZIP64 well before 4 GiB.
|
|
.large_file(data.len() as u64 >= u64::from(u32::MAX) / 2);
|
|
let fail = |e: &dyn std::fmt::Display| Error::Write(format!("{name}: {e}"));
|
|
self.zip.start_file(name, options).map_err(|e| fail(&e))?;
|
|
self.zip.write_all(data).map_err(|e| fail(&e))
|
|
}
|
|
|
|
/// The finished archive.
|
|
pub fn finish(self) -> Result<Vec<u8>, Error> {
|
|
self.zip.finish().map(Cursor::into_inner).map_err(|e| Error::Write(e.to_string()))
|
|
}
|
|
}
|