# A read-only `IO` object to decompress data in the gzip format. # # Instances of this class wrap another IO object. When you read from this instance # instance, it reads data from the underlying IO, decompresses it, and returns # it to the caller. # # NOTE: A gzip stream can contain zero or more members. If it contains # no members, `header` will be `nil`. If it contains one or more # members, only the first header will be recorded here. This is # because gzipping multiple members is not common as one usually # combines gzip with tar. If, however, multiple members are present # then reading from this reader will return the concatenation of # all the members. # # ### Example: decompress a gzip file # # ``` # require "compress/gzip" # # File.write("file.gzip", Bytes[31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 76, 74, 6, 0, 194, 65, 36, 53, 3, 0, 0, 0]) # # string = File.open("file.gzip") do |file| # Compress::Gzip::Reader.open(file) do |gzip| # gzip.gets_to_end # end # end # string # => "abc" # ``` class Compress::Gzip::Reader < IO include IO::Buffered # Whether to close the enclosed `IO` when closing this reader. property? sync_close = false # Returns `true` if this reader is closed. getter? closed = false # Returns the first header in the gzip stream, if any. getter header : Header? @flate_io : Compress::Deflate::Reader? # Creates a new reader from the given *io*. def initialize(@io : IO, @sync_close = false) # CRC32 of written data @crc32 = ::Digest::CRC32.initial # Total size of the original (uncompressed) input data modulo 2^32. @isize = 0_u32 first_byte = @io.read_byte # A gzip file could be empty (have no members), so # we account for that case return unless first_byte @header = Header.new(first_byte, @io) @flate_io = Compress::Deflate::Reader.new(@io) end # Creates a new reader from the given *filename*. def self.new(filename : String) new(::File.new(filename), sync_close: true) end # Creates a new reader from the given *io*, yields it to the given block, # and closes it at the end. def self.open(io : IO, sync_close = false, &) reader = new(io, sync_close: sync_close) yield reader ensure reader.close end # Creates a new reader from the given *filename*, yields it to the given block, # and closes it at the end. def self.open(filename : String, &) reader = new(filename) yield reader ensure reader.close end # See `IO#read`. def unbuffered_read(slice : Bytes) : Int32 check_open return 0 if slice.empty? while true flate_io = @flate_io return 0 unless flate_io read_bytes = flate_io.read(slice) if read_bytes == 0 crc32 = @io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) isize = @io.read_bytes(UInt32, IO::ByteFormat::LittleEndian) if crc32 != @crc32 raise Compress::Gzip::Error.new("CRC32 checksum mismatch") end if isize != @isize raise Compress::Gzip::Error.new("isize mismatch") end # Reset checksum and total size for next entry @crc32 = ::Digest::CRC32.initial @isize = 0_u32 # Check if another header with data comes first_byte = @io.read_byte if first_byte Header.new(first_byte, @io) @flate_io = Compress::Deflate::Reader.new(@io) else @flate_io = nil break end else # Update CRC32 and total data size @crc32 = ::Digest::CRC32.update(slice[0, read_bytes], @crc32) # Using wrapping addition here because isize is only 32 bits wide but # uncompressed data size can be bigger. @isize &+= read_bytes break end end read_bytes end # Always raises `IO::Error` because this is a read-only `IO`. def unbuffered_write(slice : Bytes) : NoReturn raise IO::Error.new("Can't write to Compress::Gzip::Reader") end def unbuffered_flush : NoReturn raise IO::Error.new "Can't flush Compress::Gzip::Reader" end # Closes this reader. def unbuffered_close : Nil return if @closed @closed = true @flate_io.try &.close @io.close if @sync_close end def unbuffered_rewind : Nil check_open @io.rewind @header = nil @flate_io = nil initialize(@io, @sync_close) end end