// -*- compile-command: "moon test --target js > got.txt"; -*-
// This package is based on the Go implementation found here:
// https://cs.opensource.google/go/go/+/refs/tags/go1.23.0:src/compress/gzip/gzip.go
// which has the copyright notice:
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
///|
pub(all) struct CompressionLevel(Int) derive(Compare, Eq)
// pub let huffman_only : CompressionLevel = -2
// pub let default_compression : CompressionLevel = -1
// pub let no_compression : CompressionLevel = 0
///|
pub let best_speed : CompressionLevel = 1
// pub let best_compression : CompressionLevel = 9
///|
/// Writes to a Writer are compressed and written to w.
pub(all) struct Writer {
header : Header // written at first call to Write, Flush, or Close
w : &@io.Writer
level : CompressionLevel
mut wrote_header : Bool
mut closed : Bool
mut buf : Array[Byte] // [10]byte
mut compressor : &@io.WriteCloser
mut digest : UInt // CRC-32, IEEE polynomial (section 8)
mut size : UInt // Uncompressed size (section 2.3.1)
mut err : IOError?
}
///|
fn gzip_error(s : String) -> IOError {
IOError(s)
}
///|
/// Writer::new returns a new [IOWriter] using the optional compression level
/// (or `default_compression`).
/// Writes to the returned writer are compressed and written to w.
///
/// It is the caller's responsibility to call Close on the [Writer] when done.
/// Writes may be buffered and not flushed until Close.
///
/// Callers that wish to set the fields in Writer.Header must do so before
/// the first call to Write, Flush, or Close.
pub fn Writer::new(w : &@io.Writer) -> Writer {
let header = Header::new()
let buf = Array::make(10, b'\x00')
{
header,
w,
level: best_speed,
wrote_header: false,
closed: false,
buf,
compressor: @flate.Writer::new(w),
digest: 0U, // CRC-32, IEEE polynomial (section 8)
size: 0U, // Uncompressed size (section 2.3.1)
err: None,
}
}
///|
// write_bytes writes a length-prefixed byte slice to self.w.
fn Writer::write_bytes(self : Writer, b : Slice[Byte]) -> IOError? {
if b.length() > 0xffff {
return Some(gzip_error("gzip.Write: Extra data is too large"))
}
le_put_uint16(self.buf, 0, b.length().reinterpret_as_uint())
let le_len = [self.buf[0], self.buf[1]]
let (_, err) = self.w.write(Slice::new(le_len)[:])
guard err is None else { return err }
let (_, err) = self.w.write(b)
err
}
///|
fn le_put_uint16(b : Array[Byte], offset : Int, value : UInt) -> Unit {
b[offset] = (value & 0xff).to_byte()
b[offset + 1] = ((value >> 8) & 0xff).to_byte()
}
///|
fn le_put_uint32(b : Array[Byte], offset : Int, value : UInt) -> Unit {
b[offset] = (value & 0xff).to_byte()
b[offset + 1] = ((value >> 8) & 0xff).to_byte()
b[offset + 2] = ((value >> 16) & 0xff).to_byte()
b[offset + 3] = ((value >> 24) & 0xff).to_byte()
}
// fn le_put_uint64(b : Array[Byte], offset : Int, value : UInt64) -> Unit {
// le_put_uint32(b, offset, (value & 0xffffffff).to_uint())
// le_put_uint32(b, offset + 4, ((value >> 32) & 0xffffffff).to_uint())
// }
///|
/// write_string converts a UTF-16 String to UTF-8 and writes the UTF-8 string in GZIP's format to self.w.
/// GZIP (RFC 1952) specifies that strings are NUL-terminated ISO 8859-1 (Latin-1).
fn Writer::write_string(self : Writer, s : String) -> IOError? {
let ch = s.to_array()
// GZIP stores Latin-1 strings; error if non-Latin-1; convert if non-ASCII.
let mut needconv = false
for c in ch {
let v = c.to_int()
if v == 0 || v >= 0xff {
return Some(gzip_error("@gzip.write_string: non-Latin-1 header string"))
}
if v > 0x7f {
needconv = true
}
}
if needconv {
let b = Slice::new(Array::make(s.length(), b'\x00'))
for i in 0.. return (0, self.err)
_ => ()
}
// Write the GZIP header lazily.
if !self.wrote_header {
self.wrote_header = true
self.buf = Array::make(10, b'\x00')
self.buf[0] = gzip_id1
self.buf[1] = gzip_id2
self.buf[2] = gzip_deflate
match self.header.extra {
[] => ()
_ => self.buf[3] = b'\x04'
}
if self.header.name != "" {
self.buf[3] = self.buf[3] | b'\x08'
}
if self.header.comment != "" {
self.buf[3] = self.buf[3] | b'\x10'
}
match self.header.mod_time {
Some(t) => {
let t = t.to_unix_second().reinterpret_as_uint64().to_uint()
// Section 2.3.1, the zero value for MTIME means that the
// modified time is not set.
le_put_uint32(self.buf, 4, t)
}
None => ()
}
// This version only supports 'best_speed'.
// if self.level == best_compression {
// self.buf[8] = b'\x02'
// } else if self.level == best_speed {
self.buf[8] = b'\x04'
// }
self.buf[9] = self.header.os
let b = Slice::new(self.buf)
let (_, err) = self.w.write(b)
guard err is None else {
self.err = err
return (0, err)
}
match self.header.extra {
[] => ()
b => {
let err = self.write_bytes(Slice::new(b))
guard err is None else {
self.err = err
return (0, err)
}
}
}
if self.header.name != "" {
let err = self.write_string(self.header.name)
guard err is None else {
self.err = err
return (0, err)
}
}
if self.header.comment != "" {
let err = self.write_string(self.header.comment)
guard err is None else {
self.err = err
return (0, err)
}
}
// if z.compressor == nil {
// log.Printf("GML: gzip.go: Write: header: W")
// z.compressor, _ = flate.NewWriter(z.w, z.level)
// }
}
self.size += p.length().reinterpret_as_uint()
let sum = @crc32.Digest::new()
for b in p {
sum.write(b)
}
self.digest = sum.digest()
let (n, err) = @io.Writer::write(self.compressor, p)
(n, err)
}
///|
/// flush flushes any pending compressed data to the underlying writer.
///
/// It is useful mainly in compressed network protocols, to ensure that
/// a remote reader has enough data to reconstruct a packet. Flush does
/// not return until the data has been written. If the underlying
/// writer returns an error, Flush returns that error.
///
/// In the terminology of the zlib library, Flush is equivalent to Z_SYNC_FLUSH.
pub fn Writer::flush(self : Writer) -> IOError? {
guard self.err is None else { return self.err }
if self.closed {
return None
}
if !self.wrote_header {
let (_, err) = self.write(Slice::new([b'\x00']))
guard err is None else { return err }
}
// The simplified flate compressor has no flush method.
// self.err = self.compressor.flush()
None
}
///|
/// close closes the [Writer] by flushing any unwritten data to the underlying
/// [io.Writer] and writing the GZIP footer.
/// It does not close the underlying [io.Writer].
pub impl @io.Closer for Writer with fn close(self) {
guard self.err is None else { return self.err }
if self.closed {
return None
}
self.closed = true
if !self.wrote_header {
let (_, err) = self.write(Slice::new([]))
guard err is None else { return err }
}
self.err = @io.Closer::close(self.compressor)
le_put_uint32(self.buf, 0, self.digest)
le_put_uint32(self.buf, 4, self.size)
let footer = Slice::new(self.buf)[:8]
let (_, err) = self.w.write(footer)
err
}