diff options
Diffstat (limited to 'lib/rubygems/package')
-rw-r--r-- | lib/rubygems/package/digest_io.rb | 64 | ||||
-rw-r--r-- | lib/rubygems/package/f_sync_dir.rb | 23 | ||||
-rw-r--r-- | lib/rubygems/package/old.rb | 147 | ||||
-rw-r--r-- | lib/rubygems/package/tar_header.rb | 73 | ||||
-rw-r--r-- | lib/rubygems/package/tar_input.rb | 235 | ||||
-rw-r--r-- | lib/rubygems/package/tar_output.rb | 146 | ||||
-rw-r--r-- | lib/rubygems/package/tar_reader.rb | 23 | ||||
-rw-r--r-- | lib/rubygems/package/tar_writer.rb | 70 |
8 files changed, 312 insertions, 469 deletions
diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb new file mode 100644 index 0000000000..f8bde0f557 --- /dev/null +++ b/lib/rubygems/package/digest_io.rb @@ -0,0 +1,64 @@ +## +# IO wrapper that creates digests of contents written to the IO it wraps. + +class Gem::Package::DigestIO + + ## + # Collected digests for wrapped writes. + # + # { + # 'SHA1' => #<OpenSSL::Digest: [...]>, + # 'SHA512' => #<OpenSSL::Digest: [...]>, + # } + + attr_reader :digests + + ## + # Wraps +io+ and updates digest for each of the digest algorithms in + # the +digests+ Hash. Returns the digests hash. Example: + # + # io = StringIO.new + # digests = { + # 'SHA1' => OpenSSL::Digest.new('SHA1'), + # 'SHA512' => OpenSSL::Digest.new('SHA512'), + # } + # + # Gem::Package::DigestIO.wrap io, digests do |digest_io| + # digest_io.write "hello" + # end + # + # digests['SHA1'].hexdigest #=> "aaf4c61d[...]" + # digests['SHA512'].hexdigest #=> "9b71d224[...]" + + def self.wrap io, digests + digest_io = new io, digests + + yield digest_io + + return digests + end + + ## + # Creates a new DigestIO instance. Using ::wrap is recommended, see the + # ::wrap documentation for documentation of +io+ and +digests+. + + def initialize io, digests + @io = io + @digests = digests + end + + ## + # Writes +data+ to the underlying IO and updates the digests + + def write data + result = @io.write data + + @digests.each do |_, digest| + digest << data + end + + result + end + +end + diff --git a/lib/rubygems/package/f_sync_dir.rb b/lib/rubygems/package/f_sync_dir.rb deleted file mode 100644 index f7eb7f3ce3..0000000000 --- a/lib/rubygems/package/f_sync_dir.rb +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -#-- -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#++ - -module Gem::Package::FSyncDir - - private - - ## - # make sure this hits the disc - - def fsync_dir(dirname) - dir = open dirname, 'r' - dir.fsync - rescue # ignore IOError if it's an unpatched (old) Ruby - ensure - dir.close if dir rescue nil - end - -end - diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb new file mode 100644 index 0000000000..552a5f3591 --- /dev/null +++ b/lib/rubygems/package/old.rb @@ -0,0 +1,147 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +## +# The format class knows the guts of the ancient .gem file format and provides +# the capability to read such ancient gems. +# +# Please pretend this doesn't exist. + +class Gem::Package::Old < Gem::Package + + undef_method :spec= + + ## + # Creates a new old-format package reader for +gem+. Old-format packages + # cannot be written. + + def initialize gem + require 'fileutils' + require 'zlib' + Gem.load_yaml + + @gem = gem + @contents = nil + @spec = nil + end + + ## + # A list of file names contained in this gem + + def contents + return @contents if @contents + + open @gem, 'rb' do |io| + read_until_dashes io # spec + header = file_list io + + @contents = header.map { |file| file['path'] } + end + end + + ## + # Extracts the files in this package into +destination_dir+ + + def extract_files destination_dir + errstr = "Error reading files from gem" + + open @gem, 'rb' do |io| + read_until_dashes io # spec + header = file_list io + raise Gem::Exception, errstr unless header + + header.each do |entry| + full_name = entry['path'] + + destination = install_location full_name, destination_dir + + file_data = '' + + read_until_dashes io do |line| + file_data << line + end + + file_data = file_data.strip.unpack("m")[0] + file_data = Zlib::Inflate.inflate file_data + + raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if + file_data.length != entry['size'].to_i + + FileUtils.rm_rf destination + + FileUtils.mkdir_p File.dirname destination + + open destination, 'wb', entry['mode'] do |out| + out.write file_data + end + + say destination if Gem.configuration.really_verbose + end + end + rescue Zlib::DataError + raise Gem::Exception, errstr + end + + ## + # Reads the file list section from the old-format gem +io+ + + def file_list io # :nodoc: + header = '' + + read_until_dashes io do |line| + header << line + end + + YAML.load header + end + + ## + # Reads lines until a "---" separator is found + + def read_until_dashes io # :nodoc: + while (line = io.gets) && line.chomp.strip != "---" do + yield line if block_given? + end + end + + ## + # Skips the Ruby self-install header in +io+. + + def skip_ruby io # :nodoc: + loop do + line = io.gets + + return if line.chomp == '__END__' + break unless line + end + + raise Gem::Exception, "Failed to find end of ruby script while reading gem" + end + + ## + # The specification for this gem + + def spec + return @spec if @spec + + yaml = '' + + open @gem, 'rb' do |io| + skip_ruby io + read_until_dashes io do |line| + yaml << line + end + end + + @spec = Gem::Specification.from_yaml yaml + rescue YAML::SyntaxError => e + raise Gem::Exception, "Failed to parse gem specification out of gem file" + rescue ArgumentError => e + raise Gem::Exception, "Failed to parse gem specification out of gem file" + end + +end + diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 4f923b9b5e..28da1db0b5 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -102,61 +102,24 @@ class Gem::Package::TarHeader fields = header.unpack UNPACK_FORMAT - name = fields.shift - mode = fields.shift.oct - uid = fields.shift.oct - gid = fields.shift.oct - size = fields.shift.oct - mtime = fields.shift.oct - checksum = fields.shift.oct - typeflag = fields.shift - linkname = fields.shift - magic = fields.shift - version = fields.shift.oct - uname = fields.shift - gname = fields.shift - devmajor = fields.shift.oct - devminor = fields.shift.oct - prefix = fields.shift - - new :name => name, - :mode => mode, - :uid => uid, - :gid => gid, - :size => size, - :mtime => mtime, - :checksum => checksum, - :typeflag => typeflag, - :linkname => linkname, - :magic => magic, - :version => version, - :uname => uname, - :gname => gname, - :devmajor => devmajor, - :devminor => devminor, - :prefix => prefix, - - :empty => empty - - # HACK unfactor for Rubinius - #new :name => fields.shift, - # :mode => fields.shift.oct, - # :uid => fields.shift.oct, - # :gid => fields.shift.oct, - # :size => fields.shift.oct, - # :mtime => fields.shift.oct, - # :checksum => fields.shift.oct, - # :typeflag => fields.shift, - # :linkname => fields.shift, - # :magic => fields.shift, - # :version => fields.shift.oct, - # :uname => fields.shift, - # :gname => fields.shift, - # :devmajor => fields.shift.oct, - # :devminor => fields.shift.oct, - # :prefix => fields.shift, - - # :empty => empty + new :name => fields.shift, + :mode => fields.shift.oct, + :uid => fields.shift.oct, + :gid => fields.shift.oct, + :size => fields.shift.oct, + :mtime => fields.shift.oct, + :checksum => fields.shift.oct, + :typeflag => fields.shift, + :linkname => fields.shift, + :magic => fields.shift, + :version => fields.shift.oct, + :uname => fields.shift, + :gname => fields.shift, + :devmajor => fields.shift.oct, + :devminor => fields.shift.oct, + :prefix => fields.shift, + + :empty => empty end ## diff --git a/lib/rubygems/package/tar_input.rb b/lib/rubygems/package/tar_input.rb deleted file mode 100644 index 77b4d698da..0000000000 --- a/lib/rubygems/package/tar_input.rb +++ /dev/null @@ -1,235 +0,0 @@ -# -*- coding: iso-8859-1 -*- -#++ -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#-- - -require 'zlib' -Gem.load_yaml - -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) - - gzis = Zlib::GzipReader.new(*args) - - # 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 - diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb deleted file mode 100644 index fdc8f4fb7c..0000000000 --- a/lib/rubygems/package/tar_output.rb +++ /dev/null @@ -1,146 +0,0 @@ -# -*- coding: utf-8 -*- -#-- -# Copyright (C) 2004 Mauricio Julio Fernández Pradier -# See LICENSE.txt for additional licensing information. -#++ - -## -# TarOutput is a wrapper to TarWriter that builds gem-format tar file. -# -# Gem-format tar files contain the following files: -# [data.tar.gz] A gzipped tar file containing the files that compose the gem -# which will be extracted into the gem/ dir on installation. -# [metadata.gz] A YAML format Gem::Specification. -# [data.tar.gz.sig] A signature for the gem's data.tar.gz. -# [metadata.gz.sig] A signature for the gem's metadata.gz. -# -# See TarOutput::open for usage details. - -class Gem::Package::TarOutput - - ## - # Creates a new TarOutput which will yield a TarWriter object for the - # data.tar.gz portion of a gem-format tar file. - # - # See #initialize for details on +io+ and +signer+. - # - # See #add_gem_contents for details on adding metadata to the tar file. - - def self.open(io, signer = nil, &block) # :yield: data_tar_writer - tar_outputter = new io, signer - tar_outputter.add_gem_contents(&block) - tar_outputter.add_metadata - tar_outputter.add_signatures - - ensure - tar_outputter.close - end - - ## - # Creates a new TarOutput that will write a gem-format tar file to +io+. If - # +signer+ is given, the data.tar.gz and metadata.gz will be signed and - # the signatures will be added to the tar file. - - def initialize(io, signer) - @io = io - @signer = signer - - @tar_writer = Gem::Package::TarWriter.new @io - - @metadata = nil - - @data_signature = nil - @meta_signature = nil - end - - ## - # Yields a TarWriter for the data.tar.gz inside a gem-format tar file. - # The yielded TarWriter has been extended with a #metadata= method for - # attaching a YAML format Gem::Specification which will be written by - # add_metadata. - - def add_gem_contents - @tar_writer.add_file "data.tar.gz", 0644 do |inner| - sio = @signer ? StringIO.new : nil - Zlib::GzipWriter.wrap(sio || inner) do |os| - - Gem::Package::TarWriter.new os do |data_tar_writer| - # :stopdoc: - def data_tar_writer.metadata() @metadata end - def data_tar_writer.metadata=(metadata) @metadata = metadata end - # :startdoc: - - yield data_tar_writer - - @metadata = data_tar_writer.metadata - end - end - - # if we have a signing key, then sign the data - # digest and return the signature - if @signer then - require 'rubygems/security' - digest = Gem::Security::OPT[:dgst_algo].digest sio.string - @data_signature = @signer.sign digest - inner.write sio.string - end - end - - self - end - - ## - # Adds metadata.gz to the gem-format tar file which was saved from a - # previous #add_gem_contents call. - - def add_metadata - return if @metadata.nil? - - @tar_writer.add_file "metadata.gz", 0644 do |io| - begin - sio = @signer ? StringIO.new : nil - gzos = Zlib::GzipWriter.new(sio || io) - gzos.write @metadata - ensure - gzos.flush - gzos.finish - - # if we have a signing key, then sign the metadata digest and return - # the signature - if @signer then - require 'rubygems/security' - digest = Gem::Security::OPT[:dgst_algo].digest sio.string - @meta_signature = @signer.sign digest - io.write sio.string - end - end - end - end - - ## - # Adds data.tar.gz.sig and metadata.gz.sig to the gem-format tar files if - # a Gem::Security::Signer was sent to initialize. - - def add_signatures - if @data_signature then - @tar_writer.add_file 'data.tar.gz.sig', 0644 do |io| - io.write @data_signature - end - end - - if @meta_signature then - @tar_writer.add_file 'metadata.gz.sig', 0644 do |io| - io.write @meta_signature - end - end - end - - ## - # Closes the TarOutput. - - def close - @tar_writer.close - end - -end - diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index e6a71d386c..e257fdd846 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -9,7 +9,7 @@ class Gem::Package::TarReader - include Gem::Package + include Enumerable ## # Raised if the tar IO is not seekable @@ -52,9 +52,9 @@ class Gem::Package::TarReader # Iterates over files in the tarball yielding each entry def each - loop do - return if @io.eof? + return enum_for __method__ unless block_given? + until @io.eof? do header = Gem::Package::TarHeader.from @io return if header.empty? @@ -100,6 +100,23 @@ class Gem::Package::TarReader end end + ## + # Seeks through the tar file until it finds the +entry+ with +name+ and + # yields it. Rewinds the tar file to the beginning when the block + # terminates. + + def seek name # :yields: entry + found = find do |entry| + entry.full_name == name + end + + return unless found + + return yield found + ensure + rewind + end + end require 'rubygems/package/tar_reader/entry' diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index a73b5e5cab..f2c11e3544 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -40,12 +40,12 @@ class Gem::Package::TarWriter # number of bytes will be more than #limit def write(data) - if data.size + @written > @limit + if data.bytesize + @written > @limit raise FileOverflow, "You tried to feed more data than fits in the file." end @io.write data - @written += data.size - data.size + @written += data.bytesize + data.bytesize end end @@ -130,6 +130,62 @@ class Gem::Package::TarWriter end ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +digest_algorithm+ is written to a read-only +name+.sum + # file following the given file contents containing the digest name and + # hexdigest separated by a tab. + # + # The created digest object is returned. + + def add_file_digest name, mode, digest_algorithms # :yields: io + digests = digest_algorithms.map do |digest_algorithm| + digest = digest_algorithm.new + [digest.name, digest] + end + + digests = Hash[*digests.flatten] + + add_file name, mode do |io| + Gem::Package::DigestIO.wrap io, digests do |digest_io| + yield digest_io + end + end + + digests + end + + ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +signer+ is used to add a digest file using its + # digest_algorithm per add_file_digest and a cryptographic signature in + # +name+.sig. If the signer has no key only the checksum file is added. + # + # Returns the digest. + + def add_file_signed name, mode, signer + digest_algorithms = [ + signer.digest_algorithm, + OpenSSL::Digest::SHA512, + ].uniq + + digests = add_file_digest name, mode, digest_algorithms do |io| + yield io + end + + signature_digest = digests.values.find do |digest| + digest.name == signer.digest_name + end + + signature = signer.sign signature_digest.digest + + add_file_simple "#{name}.sig", 0444, signature.length do |io| + io.write signature + end if signature + + digests + end + + ## # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO # to write the file to. @@ -211,9 +267,9 @@ class Gem::Package::TarWriter # Splits +name+ into a name and prefix that can fit in the TarHeader def split_name(name) # :nodoc: - raise Gem::Package::TooLongFileName if name.size > 256 + raise Gem::Package::TooLongFileName if name.bytesize > 256 - if name.size <= 100 then + if name.bytesize <= 100 then prefix = "" else parts = name.split(/\//) @@ -222,14 +278,14 @@ class Gem::Package::TarWriter loop do nxt = parts.pop - break if newname.size + 1 + nxt.size > 100 + break if newname.bytesize + 1 + nxt.bytesize > 100 newname = nxt + "/" + newname end prefix = (parts + [nxt]).join "/" name = newname - if name.size > 100 or prefix.size > 155 then + if name.bytesize > 100 or prefix.bytesize > 155 then raise Gem::Package::TooLongFileName end end |