require 'zlib' require 'stringio' ## # Provides a unified callback interface to decompression libraries. module ZMachine::HttpDecoders class DecoderError < StandardError end class << self def accepted_encodings DECODERS.inject([]) { |r, d| r + d.encoding_names } end def decoder_for_encoding(encoding) DECODERS.each { |d| return d if d.encoding_names.include? encoding } nil end end class Base def self.encoding_names name = to_s.split('::').last.downcase [name] end ## # chunk_callback:: [Block] To handle a decompressed chunk def initialize(&chunk_callback) @chunk_callback = chunk_callback end def <<(compressed) return unless compressed && compressed.size > 0 decompressed = decompress(compressed) receive_decompressed decompressed end def finalize! decompressed = finalize receive_decompressed decompressed end private def receive_decompressed(decompressed) if decompressed && decompressed.size > 0 @chunk_callback.call(decompressed) end end protected ## # Must return a part of decompressed def decompress(compressed) nil end ## # May return last part def finalize nil end end class Deflate < Base def decompress(compressed) begin @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS) @zstream.inflate(compressed) rescue Zlib::Error raise DecoderError end end def finalize return nil unless @zstream begin r = @zstream.inflate(nil) @zstream.close r rescue Zlib::Error raise DecoderError end end end ## # Partial implementation of RFC 1952 to extract the deflate stream from a gzip file class GZipHeader def initialize @state = :begin @data = "" @pos = 0 end def finished? @state == :finish end def read(n, buffer) if (@pos + n) <= @data.size buffer << @data[@pos..(@pos + n - 1)] @pos += n return true else return false end end def readbyte if (@pos + 1) <= @data.size @pos += 1 @data.getbyte(@pos - 1) end end def eof? @pos >= @data.size end def extract_stream(compressed) @data << compressed pos = @pos while !eof? && !finished? buffer = "" case @state when :begin break if !read(10, buffer) if buffer.getbyte(0) != 0x1f || buffer.getbyte(1) != 0x8b raise DecoderError.new("magic header not found") end if buffer.getbyte(2) != 0x08 raise DecoderError.new("unknown compression method") end @flags = buffer.getbyte(3) if (@flags & 0xe0).nonzero? raise DecoderError.new("unknown header flags set") end # We don't care about these values, I'm leaving the code for reference # @time = buffer[4..7].unpack("V")[0] # little-endian uint32 # @extra_flags = buffer.getbyte(8) # @os = buffer.getbyte(9) @state = :extra_length when :extra_length if (@flags & 0x04).nonzero? break if !read(2, buffer) @extra_length = buffer.unpack("v")[0] # little-endian uint16 @state = :extra else @state = :extra end when :extra if (@flags & 0x04).nonzero? break if read(@extra_length, buffer) @state = :name else @state = :name end when :name if (@flags & 0x08).nonzero? while !(buffer = readbyte).nil? if buffer == 0 @state = :comment break end end else @state = :comment end when :comment if (@flags & 0x10).nonzero? while !(buffer = readbyte).nil? if buffer == 0 @state = :hcrc break end end else @state = :hcrc end when :hcrc if (@flags & 0x02).nonzero? break if !read(2, buffer) @state = :finish else @state = :finish end end end if finished? compressed[(@pos - pos)..-1] else "" end end end class GZip < Base def self.encoding_names %w(gzip compressed) end def decompress(compressed) compressed.force_encoding('BINARY') @header ||= GZipHeader.new if !@header.finished? compressed = @header.extract_stream(compressed) end @zstream ||= Zlib::Inflate.new(-Zlib::MAX_WBITS) @zstream.inflate(compressed) rescue Zlib::Error raise DecoderError end def finalize if @zstream if !@zstream.finished? r = @zstream.finish end @zstream.close r else nil end rescue Zlib::Error raise DecoderError end end DECODERS = [Deflate, GZip] end