summaryrefslogtreecommitdiff
path: root/lib/rubygems/package/tar_input.rb
blob: 9f901023b892c9755620d9788aedd248a61e4d25 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
#++
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
#--

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

          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
    @fileops = Gem::FileOperations.new

    raise Gem::Package::FormatError, "No metadata found!" unless has_meta
  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
        @fileops.chmod entry.header.mode, dest, :verbose => false
      else
        @fileops.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)
    @fileops.mkdir_p destdir, :mode => 0755, :verbose => false
    destfile = File.join destdir, File.basename(entry.full_name)
    @fileops.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

    @fileops.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.

  def zipped_stream(entry)
    if defined? Rubinius then
      zis = Zlib::GzipReader.new entry
      dis = zis.read
      is = StringIO.new(dis)
    else
      # This is Jamis Buck's Zlib workaround for some unknown issue
      entry.read(10) # skip the gzip header
      zis = Zlib::Inflate.new(-Zlib::MAX_WBITS)
      is = StringIO.new(zis.inflate(entry.read))
    end
  ensure
    zis.finish if zis
  end

end