text: # -*- coding: iso-8859-1 -*-
  #++
  # Copyright (C) 2004 Mauricio Julio Fernández Pradier
  # See LICENSE.txt for additional licensing information.
  #--
  
  require 'zlib'
  
  class Gem::Package::TarInput
  
    include Gem::Package::FSyncDir
    include Enumerable
  
    attr_reader :metadata
  
    private_class_method :new
  
    def self.open(io, security_policy = nil,  &block)
      is = new io, security_policy
  
      yield is
    ensure
      is.close if is
    end
  
    def initialize(io, security_policy = nil)
      @io = io
      @tarreader = Gem::Package::TarReader.new @io
      has_meta = false
  
      data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil
      dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil
  
      @tarreader.each do |entry|
        case entry.full_name
        when "metadata"
          @metadata = load_gemspec entry.read
          has_meta = true
        when "metadata.gz"
          begin
            # if we have a security_policy, then pre-read the metadata file
            # and calculate it's digest
            sio = nil
            if security_policy
              Gem.ensure_ssl_available
              sio = StringIO.new(entry.read)
              meta_dgst = dgst_algo.digest(sio.string)
              sio.rewind
            end
  
            # Ruby 1.8 doesn't have encoding and YAML is UTF-8
            args = [sio || entry]
            args << { :external_encoding => Encoding::UTF_8 } if
              Object.const_defined?(:Encoding)
  
            # XXX MACRUBY our Zlib::GzipReader.new can't accept variable length arguments
            #gzis = Zlib::GzipReader.new(*args)
            gzis = Zlib::GzipReader.new(sio || entry)
  
            # YAML wants an instance of IO
            @metadata = load_gemspec(gzis)
            has_meta = true
          ensure
            gzis.close unless gzis.nil?
          end
        when 'metadata.gz.sig'
          meta_sig = entry.read
        when 'data.tar.gz.sig'
          data_sig = entry.read
        when 'data.tar.gz'
          if security_policy
            Gem.ensure_ssl_available
            data_dgst = dgst_algo.digest(entry.read)
          end
        end
      end
  
      if security_policy then
        Gem.ensure_ssl_available
  
        # map trust policy from string to actual class (or a serialized YAML
        # file, if that exists)
        if String === security_policy then
          if Gem::Security::Policies.key? security_policy then
            # load one of the pre-defined security policies
            security_policy = Gem::Security::Policies[security_policy]
          elsif File.exist? security_policy then
            # FIXME: this doesn't work yet
            security_policy = YAML.load File.read(security_policy)
          else
            raise Gem::Exception, "Unknown trust policy '#{security_policy}'"
          end
        end
  
        if data_sig && data_dgst && meta_sig && meta_dgst then
          # the user has a trust policy, and we have a signed gem
          # file, so use the trust policy to verify the gem signature
  
          begin
            security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain)
          rescue Exception => e
            raise "Couldn't verify data signature: #{e}"
          end
  
          begin
            security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain)
          rescue Exception => e
            raise "Couldn't verify metadata signature: #{e}"
          end
        elsif security_policy.only_signed
          raise Gem::Exception, "Unsigned gem"
        else
          # FIXME: should display warning here (trust policy, but
          # either unsigned or badly signed gem file)
        end
      end
  
      @tarreader.rewind
  
      unless has_meta then
        path = io.path if io.respond_to? :path
        error = Gem::Package::FormatError.new 'no metadata found', path
        raise error
      end
    end
  
    def close
      @io.close
      @tarreader.close
    end
  
    def each(&block)
      @tarreader.each do |entry|
        next unless entry.full_name == "data.tar.gz"
        is = zipped_stream entry
  
        begin
          Gem::Package::TarReader.new is do |inner|
            inner.each(&block)
          end
        ensure
          is.close if is
        end
      end
  
      @tarreader.rewind
    end
  
    def extract_entry(destdir, entry, expected_md5sum = nil)
      if entry.directory? then
        dest = File.join destdir, entry.full_name
  
        if File.directory? dest then
          FileUtils.chmod entry.header.mode, dest, :verbose => false
        else
          FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false
        end
  
        fsync_dir dest
        fsync_dir File.join(dest, "..")
  
        return
      end
  
      # it's a file
      md5 = Digest::MD5.new if expected_md5sum
      destdir = File.join destdir, File.dirname(entry.full_name)
      FileUtils.mkdir_p destdir, :mode => 0755, :verbose => false
      destfile = File.join destdir, File.basename(entry.full_name)
      FileUtils.chmod 0600, destfile, :verbose => false rescue nil # Errno::ENOENT
  
      open destfile, "wb", entry.header.mode do |os|
        loop do
          data = entry.read 4096
          break unless data
          # HACK shouldn't we check the MD5 before writing to disk?
          md5 << data if expected_md5sum
          os.write(data)
        end
  
        os.fsync
      end
  
      FileUtils.chmod entry.header.mode, destfile, :verbose => false
      fsync_dir File.dirname(destfile)
      fsync_dir File.join(File.dirname(destfile), "..")
  
      if expected_md5sum && expected_md5sum != md5.hexdigest then
        raise Gem::Package::BadCheckSum
      end
    end
  
    # Attempt to YAML-load a gemspec from the given _io_ parameter.  Return
    # nil if it fails.
    def load_gemspec(io)
      Gem::Specification.from_yaml io
    rescue Gem::Exception
      nil
    end
  
    ##
    # Return an IO stream for the zipped entry.
    #
    # NOTE:  Originally this method used two approaches, Return a GZipReader
    # directly, or read the GZipReader into a string and return a StringIO on
    # the string.  The string IO approach was used for versions of ZLib before
    # 1.2.1 to avoid buffer errors on windows machines.  Then we found that
    # errors happened with 1.2.1 as well, so we changed the condition.  Then
    # we discovered errors occurred with versions as late as 1.2.3.  At this
    # point (after some benchmarking to show we weren't seriously crippling
    # the unpacking speed) we threw our hands in the air and declared that
    # this method would use the String IO approach on all platforms at all
    # times.  And that's the way it is.
    #
    # Revisited.  Here's the beginning of the long story.
    # http://osdir.com/ml/lang.ruby.gems.devel/2007-06/msg00045.html
    #
    # StringIO wraping has never worked as a workaround by definition.  Skipping
    # initial 10 bytes and passing -MAX_WBITS to Zlib::Inflate luckily works as
    # gzip reader, but it only works if the GZip header is 10 bytes long (see
    # below) and it does not check inflated stream consistency (CRC value in the
    # Gzip trailer.)
    #
    #   RubyGems generated Gzip Header: 10 bytes
    #     magic(2) + method(1) + flag(1) + mtime(4) + exflag(1) + os(1) +
    #     orig_name(0) + comment(0)
    #
    # Ideally, it must return a GZipReader without meaningless buffering.  We
    # have lots of CRuby committers around so let's fix windows build when we
    # received an error.
    def zipped_stream(entry)
      Zlib::GzipReader.new entry
    end
  
  end
  
  