# A write-only `IO` object to compress data in the zlib format. # # Instances of this class wrap another IO object. When you write to this # instance, it compresses the data and writes it to the underlying IO. # # NOTE: unless created with a block, `close` must be invoked after all # data has been written to a Zlib::Writer instance. class Compress::Zlib::Writer < IO # Whether to close the enclosed `IO` when closing this writer. property? sync_close = false # Returns `true` if this writer is closed. getter? closed = false # Creates a new writer to the given *io*. def initialize(@io : IO, @level = Zlib::DEFAULT_COMPRESSION, @sync_close = false, @dict : Bytes? = nil) @wrote_header = false @adler32 = ::Digest::Adler32.initial @flate_io = Compress::Deflate::Writer.new(@io, level: level, dict: @dict) end # Creates a new writer to the given *filename*. def self.new(filename : String, level = Zlib::DEFAULT_COMPRESSION, dict : Bytes? = nil) new(::File.new(filename, "w"), level: level, sync_close: true, dict: dict) end # Creates a new writer to the given *io*, yields it to the given block, # and closes it at the end. def self.open(io : IO, level = Zlib::DEFAULT_COMPRESSION, sync_close = false, dict : Bytes? = nil, &) writer = new(io, level: level, sync_close: sync_close, dict: dict) yield writer ensure writer.close end # Creates a new writer to the given *filename*, yields it to the given block, # and closes it at the end. def self.open(filename : String, level = Zlib::DEFAULT_COMPRESSION, dict : Bytes? = nil, &) writer = new(filename, level: level, dict: dict) yield writer ensure writer.close end # Always raises `IO::Error` because this is a write-only `IO`. def read(slice : Bytes) : NoReturn raise IO::Error.new("Can't read from Gzip::Writer") end # See `IO#write`. def write(slice : Bytes) : Nil check_open return if slice.empty? write_header unless @wrote_header @flate_io.write(slice) @adler32 = ::Digest::Adler32.update(slice, @adler32) end # Flushes data, forcing writing the zlib header if no # data has been written yet. # # See `IO#flush`. def flush : Nil check_open write_header unless @wrote_header @flate_io.flush end # Closes this writer. Must be invoked after all data has been written. def close : Nil return if @closed @closed = true write_header unless @wrote_header @flate_io.close @io.write_bytes(@adler32, IO::ByteFormat::BigEndian) @io.close if @sync_close end private def write_header @wrote_header = true # CMF byte: 7 for window size, 8 for compression method (deflate) cmf = 0x78_u8 @io.write_byte cmf dict = @dict flg = 0_u8 if dict flg |= 1 << 5 end case @level when 0..1 flg |= 0 << 6 when 2..5 flg |= 1 << 6 when 6, -1 flg |= 2 << 6 else flg |= 3 << 6 end # CMF and FLG, when viewed as a 16-bit unsigned integer stored # in MSB order (CMF*256 + FLG), must be a multiple of 31 flg += 31 - (cmf.to_u16*256 + flg.to_u16).remainder(31) @io.write_byte flg if dict dict_checksum = ::Digest::Adler32.checksum(dict) @io.write_bytes(dict_checksum, IO::ByteFormat::BigEndian) end end end