///|
/// Whole-file gzip measures one complete Wasm file as one stream. Independent
/// section and function streams measure local compressibility: they are not
/// additive and do not attribute whole-file transfer bytes to individual pieces.
///|
/// Compress `data` as a gzip stream.
///
/// Keep the compressor's current defaults. A server may use another gzip
/// configuration, another content encoding, or no compression at all.
pub fn compress_gzip(data : Bytes) -> Result[Bytes, String] {
Ok(@gzip.compress(data)) catch {
error => Err(error.to_string())
}
}
///|
/// The compressed size of `data` in bytes, or why the compressor would not take
/// it.
///
/// The failure comes back as a failure rather than being swallowed into the raw
/// size. Returning `data.length()` would render as a ratio of 1.00x, which reads
/// as "this stream does not compress" when the truth is that nothing was measured
/// at all — a report that cannot tell those apart is worse than one that admits
/// it is missing a number.
pub fn compressed_size(data : Bytes) -> Result[Int, String] {
match compress_gzip(data) {
Ok(compressed) => Ok(compressed.length())
Err(message) => Err(message)
}
}
///|
/// A section's complete encoding (id, length prefix and payload), measured as
/// its own gzip stream. `raw` is the length of that same encoding.
pub struct SectionCompression {
label : String
raw : Int
/// The section's compressed size, or why the compressor refused it.
gzip : Result[Int, String]
}
///|
/// Compress each section of `parsed` on its own, out of the file it was read
/// from, heaviest raw section first.
///
/// Each section becomes its own gzip stream, so each pays its own container —
/// roughly twenty bytes of magic, header, CRC and length. That is invisible for a
/// section of kilobytes and dominant for one of a handful of bytes, which is why
/// a tiny section can come back larger than it went in.
///
/// This is what `render_compression` is handed instead of bare `Section`s: a
/// `Section` records where its bytes are, not what they are, and a section
/// cannot be compressed without them.
pub fn compress_sections(
data : Bytes,
parsed : WasmModule,
) -> Array[SectionCompression] {
let rows : Array[SectionCompression] = []
for section in ordered_sections(parsed) {
rows.push({
label: section_label(section),
raw: section.total_size,
gzip: compressed_span(data, section.offset, section.total_size),
})
}
rows
}
///|
/// The compressed size of `data[offset : offset + length]`, or why there is none.
///
/// This is how a complete section or encoded function (including its size
/// prefix, using `total_size`) is measured independently: the caller
/// has the offsets the decoder recorded, not a slice of its own. A range that
/// does not fit the file is reported too — measuring it as its own length would
/// be the same silent guess in a different place.
fn compressed_span(
data : Bytes,
offset : Int,
length : Int,
) -> Result[Int, String] {
// Validate by subtraction before adding: even a huge length cannot overflow.
if offset < 0 ||
length < 0 ||
offset > data.length() ||
length > data.length() - offset {
return Err(
"invalid compression range: offset " +
offset.to_string() +
", length " +
length.to_string() +
", file length " +
data.length().to_string(),
)
}
let end = offset + length
compressed_size(data[offset:end].to_owned())
}