// [rfc1952](https://datatracker.ietf.org/doc/html/rfc1952) compliant // gzip compression/decompression module gzip import compress import hash.crc32 // compresses an array of bytes using gzip and returns the compressed bytes in a new array // Example: compressed := gzip.compress(b)? pub fn compress(data []u8) ?[]u8 { compressed := compress.compress(data, 0)? // header mut result := [ u8(0x1f), // magic numbers (1F 8B) 0x8b, 0x08, // deflate 0x00, // header flags 0x00, // 4-byte timestamp, 0 = no timestamp (00 00 00 00) 0x00, 0x00, 0x00, 0x00, // extra flags 0xff, // operating system id (0xff = unknown) ] // 10 bytes result << compressed // trailer checksum := crc32.sum(data) length := data.len result << [ u8(checksum >> 24), u8(checksum >> 16), u8(checksum >> 8), u8(checksum), u8(length >> 24), u8(length >> 16), u8(length >> 8), u8(length), ] // 8 bytes return result } [params] pub struct DecompressParams { verify_header_checksum bool = true verify_length bool = true verify_checksum bool = true } // decompresses an array of bytes using zlib and returns the decompressed bytes in a new array // Example: decompressed := gzip.decompress(b)? pub fn decompress(data []u8, params DecompressParams) ?[]u8 { if data.len < 18 { return error('data is too short, not gzip compressed?') } else if data[0] != 0x1f || data[1] != 0x8b { return error('wrong magic numbers, not gzip compressed?') } else if data[2] != 0x08 { return error('gzip data is not compressed with DEFLATE') } mut header_length := 10 // parse flags, we ignore most of them, but we still need to parse them // correctly, so we dont accidently decompress something that belongs // to the header if data[4] & 0b1110_0000 > 0 { // reserved bits // rfc 1952 2.3.1.2 Compliance // A compliant decompressor must give an error indication if any // reserved bit is non-zero, since such a bit could indicate the // presence of a new field that would cause subsequent data to be // interpreted incorrectly. return error('reserved flags are set, unsupported field detected') } // if data[4] & 0b0000_0001 {} // FTEXT if data[4] & 0b0000_0100 > 0 { // FEXTRA, extra data xlen := data[header_length] header_length += xlen + 1 } if data[4] & 0b0000_1000 > 0 { // FNAME, file name // filename is zero-terminated, so skip until we hit a zero byte for header_length < data.len && data[header_length] != 0x00 { header_length++ } header_length++ } if data[4] & 0b0001_0000 > 0 { // FCOMMENT // comment is zero-terminated, so skip until we hit a zero byte for header_length < data.len && data[header_length] != 0x00 { header_length++ } header_length++ } if data[4] & 0b0000_0010 > 0 { // FHCRC, flag header crc if header_length + 12 > data.len { return error('data too short') } checksum_header := crc32.sum(data[..header_length]) checksum_header_expected := (u32(data[header_length]) << 24) | (u32(data[header_length + 1]) << 16) | (u32(data[ header_length + 2]) << 8) | data[header_length + 3] if params.verify_header_checksum && checksum_header != checksum_header_expected { return error('header checksum verification failed') } header_length += 4 } if header_length + 8 > data.len { return error('data too short') } decompressed := compress.decompress(data[header_length..data.len - 8], 0)? length_expected := (u32(data[data.len - 4]) << 24) | (u32(data[data.len - 3]) << 16) | (u32(data[data.len - 2]) << 8) | data[data.len - 1] if params.verify_length && decompressed.len != length_expected { return error('length verification failed, got $decompressed.len, expected $length_expected') } checksum := crc32.sum(decompressed) checksum_expected := (u32(data[data.len - 8]) << 24) | (u32(data[data.len - 7]) << 16) | (u32(data[data.len - 6]) << 8) | data[data.len - 5] if params.verify_checksum && checksum != checksum_expected { return error('checksum verification failed') } return decompressed }