///|
/// A byte sink used in two phases: first without storage to prove the exact
/// serialized size, then with one fixed backing array of that size. Keeping the
/// position and limit checks in one type makes the sizing and emission paths
/// obey identical overflow and output-ceiling rules.
priv struct FixedByteOutput {
storage : FixedArray[Byte]?
mut position : Int
limit : Int?
}
///|
fn FixedByteOutput::counting(limit? : Int) -> FixedByteOutput raise ZipError {
match limit {
Some(value) if value < 0 =>
raise UnsupportedFeature(msg="ZIP output limit must be non-negative")
_ => ()
}
{ storage: None, position: 0, limit }
}
///|
fn FixedByteOutput::allocated(size : Int) -> FixedByteOutput raise ZipError {
if size < 0 {
raise UnsupportedFeature(msg="negative ZIP output size")
}
{
// Every position is initialized by the exact-size emission pass before
// `finish`. Avoid an eager zero-fill proportional to attacker-bounded ZIP
// output; DEFLATE back-references only read positions below `position`.
storage: Some(uninitialized_fixed_byte_storage(size)),
position: 0,
limit: Some(size),
}
}
///|
/// Reinterprets primitive uninitialized storage for incremental initialization.
/// The sink must initialize every element before handing it to immutable Bytes.
fn uninitialized_fixed_byte_storage(size : Int) -> FixedArray[Byte] {
let storage : UninitializedArray[Byte] = UninitializedArray::make(size)
unsafe_uninitialized_byte_storage_to_fixed(storage)
}
///|
fn unsafe_uninitialized_byte_storage_to_fixed(
storage : UninitializedArray[Byte],
) -> FixedArray[Byte] = "%identity"
///|
fn FixedByteOutput::length(self : FixedByteOutput) -> Int {
self.position
}
///|
fn FixedByteOutput::is_counting(self : FixedByteOutput) -> Bool {
self.storage is None
}
///|
fn FixedByteOutput::remaining_limit(self : FixedByteOutput) -> Int? {
match self.limit {
Some(limit) if self.position <= limit => Some(limit - self.position)
Some(_) => Some(0)
None => None
}
}
///|
fn FixedByteOutput::check_growth(
self : FixedByteOutput,
additional : Int,
) -> Unit raise ZipError {
if additional < 0 ||
self.position < 0 ||
additional > 0x7fffffff - self.position {
raise UnsupportedFeature(msg="ZIP output size exceeds the Int range")
}
match self.limit {
Some(limit) =>
if limit < 0 ||
self.position > limit ||
additional > limit - self.position {
raise OutputLimitExceeded(limit~)
}
None => ()
}
match self.storage {
Some(storage) =>
if self.position > storage.length() ||
additional > storage.length() - self.position {
raise UnsupportedFeature(msg="fixed ZIP output plan was undersized")
}
None => ()
}
}
///|
fn FixedByteOutput::count_bytes(
self : FixedByteOutput,
additional : Int,
) -> Unit raise ZipError {
if !self.is_counting() {
raise UnsupportedFeature(msg="cannot skip bytes in allocated ZIP output")
}
self.check_growth(additional)
self.position = self.position + additional
}
///|
fn FixedByteOutput::write_byte(
self : FixedByteOutput,
value : Byte,
) -> Unit raise ZipError {
self.check_growth(1)
match self.storage {
Some(storage) => storage[self.position] = value
None => ()
}
self.position = self.position + 1
}
///|
fn FixedByteOutput::write_bytes(
self : FixedByteOutput,
bytes : BytesView,
) -> Unit raise ZipError {
let length = bytes.length()
self.check_growth(length)
match self.storage {
Some(storage) =>
if length > 0 {
storage.blit_from_bytesview(self.position, bytes)
}
None => ()
}
self.position = self.position + length
}
///|
/// Emits an overlapping DEFLATE back-reference. A counting sink needs only the
/// validated distance and length; an allocated sink copies from bytes already
/// written to the same exact-size destination.
fn FixedByteOutput::copy_back_reference(
self : FixedByteOutput,
distance : Int,
length : Int,
) -> Unit raise ZipError {
if distance <= 0 || distance > self.position {
raise UnsupportedFeature(msg="invalid back-reference distance")
}
self.check_growth(length)
match self.storage {
Some(storage) =>
for _ in 0.. self.position = self.position + length
}
}
///|
/// Reinterprets the one exact backing allocation as immutable bytes. Callers
/// must not retain or mutate the output sink after this ownership handoff.
fn FixedByteOutput::finish(self : FixedByteOutput) -> Bytes raise ZipError {
match self.storage {
Some(storage) => {
if self.position != storage.length() {
raise UnsupportedFeature(
msg="fixed ZIP output plan was not fully emitted",
)
}
storage.unsafe_reinterpret_as_bytes()
}
None =>
raise UnsupportedFeature(msg="counting ZIP output has no byte storage")
}
}