///|
priv enum WriterMode {
Ready
Writing(PendingEntry)
Emitting
Finished
Failed
}
///|
priv struct PendingEntry {
name : Bytes
compression : Compression
offset : UInt64
expected_size : UInt64?
zip64 : Bool
crc : @checksum.Crc32
mut size : UInt64
mut compressed_size : UInt64
}
///|
/// An incremental ZIP writer over a synchronous byte sink. `add` handles an
/// already loaded file; `begin_entry`/`write`/`end_entry` handle bounded chunks.
/// Only central-directory records and reusable codec buffers are retained.
/// The sink must consume each view before returning. A sink exception poisons
/// the writer and is propagated unchanged; no further output can be emitted.
pub struct Writer {
priv sink : (BytesView) -> Unit raise
priv level : Int
priv fast_store : Bool
priv mut mode : WriterMode
priv mut offset : UInt64
priv mut directory : Array[Bytes]
priv mut has_zip64 : Bool
priv mut compressor : @flate.Compressor?
priv mut deflater : @flate.Deflater?
priv mut scratch : FixedArray[Byte]
}
///|
/// Create a ZIP writer. The sink supplies file/socket I/O or collects chunks;
/// this package itself performs no I/O. Calling `finish` writes the directory.
/// Optional `fast_store` applies to Deflate entries: whole-file detection for
/// `add`, per-block detection for chunked writes. It is off by default and may
/// increase output size. ZIP entry methods are unchanged.
pub fn Writer::Writer(
sink : (BytesView) -> Unit raise,
level? : Int = 6,
fast_store? : Bool = false,
) -> Writer {
{
sink,
level,
fast_store,
mode: Ready,
offset: 0UL,
directory: [],
has_zip64: false,
compressor: None,
deflater: None,
scratch: [],
}
}
///|
/// Number of successfully emitted bytes, including headers and descriptors.
pub fn Writer::position(self : Writer) -> UInt64 {
self.offset
}
///|
/// Whether the directory and archive trailer have been successfully emitted.
pub fn Writer::is_finished(self : Writer) -> Bool {
self.mode is Finished
}
///|
/// Whether a sink failure has made this writer unusable.
pub fn Writer::is_failed(self : Writer) -> Bool {
self.mode is Failed
}
///|
fn Writer::require_ready(self : Writer) -> Unit raise ZipError {
match self.mode {
Ready => ()
Emitting => raise ZipError(UnsupportedFeature, "zip: reentrant sink write")
_ =>
raise ZipError(
UnsupportedFeature,
"zip: writer is not ready for an entry",
)
}
}
///|
fn Writer::pending(self : Writer) -> PendingEntry raise ZipError {
guard self.mode is Writing(entry) else {
raise ZipError(UnsupportedFeature, "zip: no open entry")
}
entry
}
///|
fn Writer::emit(self : Writer, bytes : BytesView) -> Unit raise {
guard bytes.length() > 0 else { return }
let size = bytes.length().to_uint64()
guard size <= 0xffff_ffff_ffff_ffffUL - self.offset else {
self.mode = Failed
self.release_storage()
raise ZipError(UnsupportedFeature, "zip: output exceeds UInt64 range")
}
let mode = self.mode
// Reentrant writes from a sink and retries after partial I/O must fail.
self.mode = Emitting
errdefer {
self.mode = Failed
self.release_storage()
}
(self.sink)(bytes)
self.offset += size
self.mode = mode
}
///|
fn Writer::release_storage(self : Writer) -> Unit {
self.directory = []
self.compressor = None
self.deflater = None
self.scratch = []
}
///|
fn stream_name(name : String) -> Bytes raise ZipError {
let bytes = @encoding/utf8.encode(name)
guard bytes.length() <= 0xffff else {
raise ZipError(UnsupportedFeature, "zip: entry name too long")
}
bytes
}
///|
fn stream_method(compression : Compression) -> UInt16 {
match compression {
Store => (0).to_uint16()
Deflate => (8).to_uint16()
}
}
///|
fn stream_local_header(
name : Bytes,
compression : Compression,
crc : UInt,
size : UInt64,
compressed_size : UInt64,
zip64 : Bool,
descriptor : Bool,
) -> Bytes {
let out = Buffer(size_hint=30 + name.length() + (if zip64 { 20 } else { 0 }))
out.write_uint_le(LOCAL_HEADER_SIG)
out.write_uint16_le(if zip64 { (45).to_uint16() } else { (20).to_uint16() })
out.write_uint16_le(
(FLAG_UTF8 | (if descriptor { FLAG_DATA_DESCRIPTOR } else { 0 })).to_uint16(),
)
out.write_uint16_le(stream_method(compression))
out.write_uint_le(0U)
out.write_uint_le(crc)
out.write_uint_le(if zip64 { MAX_U32 } else { compressed_size.to_uint() })
out.write_uint_le(if zip64 { MAX_U32 } else { size.to_uint() })
out.write_uint16_le(name.length().to_uint16())
out.write_uint16_le(if zip64 { (20).to_uint16() } else { (0).to_uint16() })
out.write_bytes(name)
if zip64 {
out.write_uint16_le(ZIP64_EXTRA_ID.to_uint16())
out.write_uint16_le((16).to_uint16())
out.write_uint64_le(size)
out.write_uint64_le(compressed_size)
}
out.to_bytes()
}
///|
fn stream_central_record(
name : Bytes,
compression : Compression,
crc : UInt,
size : UInt64,
compressed_size : UInt64,
offset : UInt64,
zip64 : Bool,
descriptor : Bool,
) -> Bytes {
let large_offset = offset >= 0xffff_ffffUL
let extra_payload = (if zip64 { 16 } else { 0 }) +
(if large_offset { 8 } else { 0 })
let extra_size = if extra_payload == 0 { 0 } else { 4 + extra_payload }
let out = Buffer(size_hint=46 + name.length() + extra_size)
let version = if zip64 || large_offset {
(45).to_uint16()
} else {
(20).to_uint16()
}
out.write_uint_le(CENTRAL_HEADER_SIG)
out.write_uint16_le(version)
out.write_uint16_le(version)
out.write_uint16_le(
(FLAG_UTF8 | (if descriptor { FLAG_DATA_DESCRIPTOR } else { 0 })).to_uint16(),
)
out.write_uint16_le(stream_method(compression))
out.write_uint_le(0U)
out.write_uint_le(crc)
out.write_uint_le(if zip64 { MAX_U32 } else { compressed_size.to_uint() })
out.write_uint_le(if zip64 { MAX_U32 } else { size.to_uint() })
out.write_uint16_le(name.length().to_uint16())
out.write_uint16_le(extra_size.to_uint16())
out.write_uint16_le((0).to_uint16())
out.write_uint16_le((0).to_uint16())
out.write_uint16_le((0).to_uint16())
out.write_uint_le(0U)
out.write_uint_le(if large_offset { MAX_U32 } else { offset.to_uint() })
out.write_bytes(name)
if extra_payload > 0 {
out.write_uint16_le(ZIP64_EXTRA_ID.to_uint16())
out.write_uint16_le(extra_payload.to_uint16())
if zip64 {
out.write_uint64_le(size)
out.write_uint64_le(compressed_size)
}
if large_offset {
out.write_uint64_le(offset)
}
}
out.to_bytes()
}
///|
/// Compress and emit one complete file immediately. Reuses a whole-buffer
/// compressor, avoiding the staging overhead of chunked DEFLATE for small files.
/// Stored files are sent directly to the sink without an intermediate copy.
pub fn Writer::add(
self : Writer,
name : String,
data : Bytes,
compression? : Compression = Deflate,
) -> Unit raise {
self.require_ready()
let name = stream_name(name)
let crc = @checksum.crc32(data)
let compressed = match compression {
Store => data
Deflate => {
let compressor = self.compressor.unwrap_or_else(() => {
let codec = @flate.Compressor(
level=self.level,
fast_store=self.fast_store,
)
self.compressor = Some(codec)
codec
})
compressor.compress(data)
}
}
let size = data.length().to_uint64()
let compressed_size = compressed.length().to_uint64()
let offset = self.offset
self.emit(
stream_local_header(
name, compression, crc, size, compressed_size, false, false,
),
)
self.emit(compressed)
self.directory.push(
stream_central_record(
name, compression, crc, size, compressed_size, offset, false, false,
),
)
}
///|
/// Open a chunked entry. Supply the known uncompressed size to use classic
/// headers when it fits safely; unknown sizes reserve ZIP64 fields up front.
/// The declared size is checked before accepting input and before closing.
pub fn Writer::begin_entry(
self : Writer,
name : String,
compression? : Compression = Deflate,
size? : UInt64,
) -> Unit raise {
self.require_ready()
let name = stream_name(name)
// Below the sentinel, bound arithmetic fits UInt64 and covers stored blocks.
let zip64 = size
.map(n => {
n >= 0xffff_ffffUL || n + 5UL * (n / 16384UL + 1UL) + 1UL >= 0xffff_ffffUL
})
.unwrap_or(true)
if compression is Deflate {
if self.deflater is Some(deflater) {
deflater.reset()
} else {
self.deflater = Some(
@flate.Deflater(level=self.level, fast_store=self.fast_store),
)
self.scratch = FixedArray::make(65536, b'\x00')
}
}
let entry = {
name,
compression,
offset: self.offset,
expected_size: size,
zip64,
crc: @checksum.Crc32(),
size: 0UL,
compressed_size: 0UL,
}
self.mode = Writing(entry)
self.emit(stream_local_header(name, compression, 0U, 0UL, 0UL, zip64, true))
self.has_zip64 = self.has_zip64 || zip64
}
///|
fn Writer::pump(
self : Writer,
input : BytesView,
action : @flate.DeflateAction,
) -> Unit raise {
if self.deflater is Some(deflater) {
let mut consumed = 0
for ;; {
let status = deflater.step(
input[consumed:],
self.scratch.mut_view(),
action~,
)
consumed += deflater.last_consumed()
let produced = deflater.last_produced()
if produced > 0 {
self.emit(Bytes::from_array(self.scratch[:produced]))
}
guard status is @flate.NeedMoreOutput else { break }
}
}
}
///|
/// Consume a chunk of the current entry, updating CRC while its bytes are hot.
/// Produced compressed blocks are sent to the sink before this call returns.
pub fn Writer::write(self : Writer, bytes : BytesView) -> Unit raise {
let entry = self.pending()
let length = bytes.length().to_uint64()
guard length <= 0xffff_ffff_ffff_ffffUL - entry.size else {
raise ZipError(UnsupportedFeature, "zip: entry exceeds UInt64 range")
}
if entry.expected_size is Some(size) {
guard length <= size - entry.size else {
raise ZipError(UnsupportedFeature, "zip: input exceeds declared size")
}
}
let start = self.offset
entry.crc.update(bytes)
match entry.compression {
Store => self.emit(bytes)
Deflate => self.pump(bytes, @flate.Continue)
}
entry.size += length
entry.compressed_size += self.offset - start
}
///|
/// Finish the current entry and emit its data descriptor. The next file may
/// then be started; the writer retains only the completed directory record.
pub fn Writer::end_entry(self : Writer) -> Unit raise {
let entry = self.pending()
if entry.expected_size is Some(size) {
guard entry.size == size else {
raise ZipError(
UnsupportedFeature,
"zip: input is shorter than declared size",
)
}
}
let start = self.offset
if entry.compression is Deflate {
self.pump(b"", @flate.Finish)
}
entry.compressed_size += self.offset - start
let crc = entry.crc.finish()
let descriptor = Buffer(size_hint=if entry.zip64 { 24 } else { 16 })
descriptor.write_uint_le(DATA_DESCRIPTOR_SIG)
descriptor.write_uint_le(crc)
if entry.zip64 {
descriptor.write_uint64_le(entry.compressed_size)
descriptor.write_uint64_le(entry.size)
} else {
descriptor.write_uint_le(entry.compressed_size.to_uint())
descriptor.write_uint_le(entry.size.to_uint())
}
self.emit(descriptor.to_bytes())
self.directory.push(
stream_central_record(
entry.name,
entry.compression,
crc,
entry.size,
entry.compressed_size,
entry.offset,
entry.zip64,
true,
),
)
self.mode = Ready
}
///|
/// Write the central directory and close the ZIP. Further writes are rejected.
/// The optional archive comment must fit the ZIP 16-bit comment length.
pub fn Writer::finish(self : Writer, comment? : BytesView = b"") -> Unit raise {
self.require_ready()
guard comment.length() <= 0xffff else {
raise ZipError(UnsupportedFeature, "zip: archive comment too long")
}
let central_offset = self.offset
for record in self.directory {
self.emit(record)
}
let central_size = self.offset - central_offset
let count = self.directory.length()
let zip64 = self.has_zip64 ||
count >= 0xffff ||
central_size >= 0xffff_ffffUL ||
central_offset >= 0xffff_ffffUL
let out = Buffer(size_hint=98 + comment.length())
if zip64 {
out.write_uint_le(ZIP64_EOCD_SIG)
out.write_uint64_le(44UL)
out.write_uint16_le((45).to_uint16())
out.write_uint16_le((45).to_uint16())
out.write_uint_le(0U)
out.write_uint_le(0U)
out.write_uint64_le(count.to_uint64())
out.write_uint64_le(count.to_uint64())
out.write_uint64_le(central_size)
out.write_uint64_le(central_offset)
out.write_uint_le(ZIP64_LOCATOR_SIG)
out.write_uint_le(0U)
out.write_uint64_le(self.offset)
out.write_uint_le(1U)
}
out.write_uint_le(END_OF_CENTRAL_SIG)
out.write_uint_le(0U)
out.write_uint16_le(
if zip64 {
(0xffff).to_uint16()
} else {
count.to_uint16()
},
)
out.write_uint16_le(
if zip64 {
(0xffff).to_uint16()
} else {
count.to_uint16()
},
)
out.write_uint_le(if zip64 { MAX_U32 } else { central_size.to_uint() })
out.write_uint_le(if zip64 { MAX_U32 } else { central_offset.to_uint() })
out.write_uint16_le(comment.length().to_uint16())
out.write_bytes(comment)
self.emit(out.to_bytes())
self.mode = Finished
self.release_storage()
}