// The chunks a decoder was asked to report: gathered whole, within
// `max_chunk_bytes`, until their CRC has passed, then handed out as one
// block of `Metadata`. iCCP's profile, zTXt's text and iTXt's compressed text
// are inflated as they come by a zlib decoder of their own, made only when
// one of them is asked for: APNG allows text chunks between the fdAT chunks
// of one frame, in the middle of the image data's zlib stream.
///|
/// Where a block's data is in its chunk.
priv enum Mode {
/// Copied as it is.
Raw
/// The keyword and the fields before a compressed field (or iTXt's
/// text), checked byte by byte.
Prefix
/// The compressed field, through the zlib decoder.
Inflate
/// After the zlib stream: what follows is dropped.
Finished
/// The chunk is not what its type says; it will be left out.
Bad
}
///|
/// The block of a reported chunk.
priv struct Block {
max : Int32
inflater : @zlib.Decoder?
mut data : FixedArray[Byte]
mut len : Int32
mut mode : Mode
mut index : Int32
mut chunk : UInt32
// In the prefix: where the keyword's NUL is, how many fields have ended
// since, and iTXt's compression flag.
mut keyword_end : Int32
mut fields_seen : Int32
mut compressed : Bool
}
///|
/// A block for chunks of at most `max` bytes, inflated ones too, with a
/// zlib decoder when `inflate`.
fn Block::new(max : Int32, inflate : Bool) -> Block {
{
max,
inflater: if inflate {
Some(@zlib.Decoder::Decoder(until=EndOfMember))
} else {
None
},
data: FixedArray::make(0, b'\x00'),
len: 0,
mode: Raw,
index: -1,
chunk: 0,
keyword_end: -1,
fields_seen: 0,
compressed: false,
}
}
///|
/// Begins the block of a chunk of type `chunk`, `index` in `@rules.known`
/// (-1 for an unknown type), `length` bytes long.
fn Block::start(
self : Block,
chunk : UInt32,
index : Int32,
length : Int32,
) -> Unit {
self.chunk = chunk
self.index = index
self.len = 0
self.keyword_end = -1
self.fields_seen = 0
self.compressed = false
let packed = index is (@rules.ICCP | @rules.ZTXT | @rules.ITXT)
self.mode = if packed { Prefix } else { Raw }
if packed && self.inflater is Some(inflater) {
inflater.reset()
}
// A chunk copied as it is takes its length; one that inflates grows.
self.data = FixedArray::make(
if packed {
self.max.min(256)
} else {
length
},
b'\x00',
)
}
///|
/// Drops the block's data.
fn Block::release(self : Block) -> Unit {
self.data = FixedArray::make(0, b'\x00')
self.len = 0
}
///|
/// Bytes held: the block's data, and the zlib decoder when there is one.
fn Block::memory(self : Block) -> Int32 {
self.data.length() +
(match self.inflater {
Some(inflater) => inflater.memory_usage()
None => 0
})
}
///|
/// Makes room for `n` more bytes, which the caller has checked are within
/// `max`: a chunk copied as it is was checked against it from its length,
/// and the zlib decoder is given room only below it.
fn Block::grow(self : Block, n : Int32) -> Unit {
guard self.len + n > self.data.length() else { return }
let bigger = FixedArray::make(
(self.len + n).max(self.data.length() * 2).max(256).min(self.max),
b'\x00',
)
self.data.blit_to(bigger, len=self.len)
self.data = bigger
}
///|
/// Adds `data`, the chunk's next bytes, to the block.
fn Block::feed(self : Block, data : BytesView) -> Unit raise Stop {
let mut rest = data
while rest.length() > 0 {
match self.mode {
Raw => {
// The data was sized for the chunk's length, and an iTXt's
// uncompressed text fits in what its prefix leaves.
self.grow(rest.length())
self.data.blit_from_bytesview(self.len, rest)
self.len += rest.length()
return
}
Prefix => {
self.grow(1)
self.data[self.len] = rest[0]
self.len += 1
rest = rest[1:]
self.prefix_byte()
}
// The zlib decoder takes all its input or makes progress, given room;
// without room, `inflate` raises.
Inflate => rest = rest[self.inflate(rest):]
Finished | Bad => return
}
}
}
///|
/// Checks the prefix byte just added, at `len - 1`: a keyword of 1 to 79
/// bytes and its NUL; for iCCP and zTXt, a compression method of 0, which
/// the block leaves out; for iTXt, a compression flag of 0 or 1 and a method
/// of 0, both written as 0 in the block, and the language tag's and the
/// translated keyword's NULs.
fn Block::prefix_byte(self : Block) -> Unit {
let at = self.len - 1
let byte = self.data[at]
if self.keyword_end < 0 {
if byte == b'\x00' {
self.mode = if at == 0 { Bad } else { Prefix }
self.keyword_end = at
} else if at >= 79 {
self.mode = Bad
}
return
}
let after = at - self.keyword_end
if self.index != @rules.ITXT {
// The compression method, dropped from the block.
self.len -= 1
self.mode = if byte == b'\x00' { Inflate } else { Bad }
return
}
if after == 1 {
guard byte <= b'\x01' else {
self.mode = Bad
return
}
self.compressed = byte == b'\x01'
self.data[at] = b'\x00'
} else if after == 2 {
if byte != b'\x00' {
self.mode = Bad
}
} else if byte == b'\x00' {
// The language tag's NUL, then the translated keyword's.
self.fields_seen += 1
if self.fields_seen == 2 {
self.mode = if self.compressed { Inflate } else { Raw }
}
}
}
///|
/// One step of the zlib decoder over `input` into the block; returns the
/// bytes it took. A full block (`max` bytes) is given an empty slice, so that
/// a stream that ends there is still whole, and one that would write more is
/// refused.
fn Block::inflate(self : Block, input : BytesView) -> Int32 raise Stop {
// A block of a type that inflates exists only with the zlib decoder.
let inflater = self.inflater.unwrap()
if self.len < self.max {
self.grow(1)
}
let status = inflater.step(
input,
self.data.mut_view(start=self.len),
end=false,
) catch {
_ => {
self.mode = Bad
return input.length()
}
}
self.len += inflater.last_produced()
match status {
Done => self.mode = Finished
NeedMoreOutput if self.len == self.max => self.over()
_ => ()
}
inflater.last_consumed()
}
///|
/// Refuses a chunk that inflates to more than `max` bytes.
fn Block::over(self : Block) -> Unit raise Stop {
raise Fail(
LimitExceeded({
resource: Memory,
needed: None,
limit: self.max.to_uint64(),
}),
"\{@rules.type_name(self.chunk)} inflates to more than max_chunk_bytes",
)
}
///|
/// Ends the block once the chunk's data is all in, drawing what the zlib
/// decoder still holds, and says whether the chunk is what its type says.
fn Block::finish(self : Block) -> Bool raise Stop {
while self.mode is Inflate {
let before = self.len
let _ = self.inflate(b"")
if self.mode is Inflate && self.len == before {
// The zlib stream does not end in the chunk.
return false
}
}
self.mode is (Raw | Finished)
}