diff options
author | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2012-11-29 06:52:18 +0000 |
---|---|---|
committer | drbrain <drbrain@b2dd03c8-39d4-4d8f-98ff-823fe69b080e> | 2012-11-29 06:52:18 +0000 |
commit | 9694bb8cac12969300692dac5a1cf7aa4e3a46cd (patch) | |
tree | c3cb423d701f7049ba9382de052e2a937cd1302d /lib/rubygems/package.rb | |
parent | 3f606b7063fc7a8b191556365ad343a314719a8d (diff) |
* lib/rubygems*: Updated to RubyGems 2.0
* test/rubygems*: ditto.
* common.mk (prelude): Updated for RubyGems 2.0 source rearrangement.
* tool/change_maker.rb: Allow invalid UTF-8 characters in source
files.
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@37976 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
Diffstat (limited to 'lib/rubygems/package.rb')
-rw-r--r-- | lib/rubygems/package.rb | 556 |
1 files changed, 513 insertions, 43 deletions
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 2b50c588ee..51df43be93 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -3,16 +3,54 @@ # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. #++ +# +# Example using a Gem::Package +# +# Builds a .gem file given a Gem::Specification. A .gem file is a tarball +# which contains a data.tar.gz and metadata.gz, and possibly signatures. +# +# require 'rubygems' +# require 'rubygems/package' +# +# spec = Gem::Specification.new do |s| +# s.summary = "Ruby based make-like utility." +# s.name = 'rake' +# s.version = PKG_VERSION +# s.requirements << 'none' +# s.files = PKG_FILES +# s.description = <<-EOF +# Rake is a Make-like program implemented in Ruby. Tasks +# and dependencies are specified in standard Ruby syntax. +# EOF +# end +# +# Gem::Package.build spec +# +# Reads a .gem file. +# +# require 'rubygems' +# require 'rubygems/package' +# +# the_gem = Gem::Package.new(path_to_dot_gem) +# the_gem.contents # get the files in the gem +# the_gem.extract_files destination_directory # extract the gem into a directory +# the_gem.spec # get the spec out of the gem +# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive) +# +# #files are the files in the .gem tar file, not the ruby files in the gem +# #extract_files and #contents automatically call #verify +require 'rubygems/security' require 'rubygems/specification' +require 'rubygems/user_interaction' +require 'zlib' -module Gem::Package +class Gem::Package + + include Gem::UserInteraction + + class Error < Gem::Exception; end - class Error < StandardError; end - class NonSeekableIO < Error; end - class ClosedIO < Error; end - class BadCheckSum < Error; end - class TooLongFileName < Error; end class FormatError < Error attr_reader :path @@ -26,57 +64,489 @@ module Gem::Package end + class PathError < Error + def initialize destination, destination_dir + super "installing into parent path %s of %s is not allowed" % + [destination, destination_dir] + end + end + + class NonSeekableIO < Error; end + + class TooLongFileName < Error; end + ## # Raised when a tar file is corrupt class TarInvalidError < Error; end - # FIX: zenspider said: does it really take an IO? - # passed to a method called open?!? that seems stupid. - def self.open(io, mode = "r", signer = nil, &block) - tar_type = case mode - when 'r' then TarInput - when 'w' then TarOutput - else - raise "Unknown Package open mode" - end - - tar_type.open(io, signer, &block) - end - - def self.pack(src, destname, signer = nil) - TarOutput.open(destname, signer) do |outp| - dir_class.chdir(src) do - outp.metadata = (file_class.read("RPA/metadata") rescue nil) - find_class.find('.') do |entry| - case - when file_class.file?(entry) - entry.sub!(%r{\./}, "") - next if entry =~ /\ARPA\// - stat = File.stat(entry) - outp.add_file_simple(entry, stat.mode, stat.size) do |os| - file_class.open(entry, "rb") do |f| - os.write(f.read(4096)) until f.eof? - end - end - when file_class.dir?(entry) - entry.sub!(%r{\./}, "") - next if entry == "RPA" - outp.mkdir(entry, file_class.stat(entry).mode) - else - raise "Don't know how to pack this yet!" + attr_accessor :build_time # :nodoc: + + ## + # Checksums for the contents of the package + + attr_reader :checksums + + ## + # The files in this package. This is not the contents of the gem, just the + # files in the top-level container. + + attr_reader :files + + ## + # The security policy used for verifying the contents of this package. + + attr_accessor :security_policy + + ## + # Sets the Gem::Specification to use to build this package. + + attr_writer :spec + + def self.build spec, skip_validation=false + gem_file = spec.file_name + + package = new gem_file + package.spec = spec + package.build skip_validation + + gem_file + end + + ## + # Creates a new Gem::Package for the file at +gem+. + # + # If +gem+ is an existing file in the old format a Gem::Package::Old will be + # returned. + + def self.new gem + return super unless Gem::Package == self + return super unless File.exist? gem + + start = File.read gem, 20 + + return super unless start + return super unless start.include? 'MD5SUM =' + + Gem::Package::Old.new gem + end + + ## + # Creates a new package that will read or write to the file +gem+. + + def initialize gem # :notnew: + @gem = gem + + @build_time = Time.now + @checksums = {} + @contents = nil + @digests = Hash.new { |h, algorithm| h[algorithm] = {} } + @files = nil + @security_policy = nil + @signatures = {} + @signer = nil + @spec = nil + end + + ## + # Adds a checksum for each entry in the gem to checksums.yaml.gz. + + def add_checksums tar + Gem.load_yaml + + checksums_by_algorithm = Hash.new { |h, algorithm| h[algorithm] = {} } + + @checksums.each do |name, digests| + digests.each do |algorithm, digest| + checksums_by_algorithm[algorithm][name] = digest.hexdigest + end + end + + tar.add_file_signed 'checksums.yaml.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + YAML.dump checksums_by_algorithm, gz_io + end + end + end + + ## + # Adds the files listed in the packages's Gem::Specification to data.tar.gz + # and adds this file to the +tar+. + + def add_contents tar # :nodoc: + digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + Gem::Package::TarWriter.new gz_io do |data_tar| + add_files data_tar + end + end + end + + @checksums['data.tar.gz'] = digests + end + + ## + # Adds files included the package's Gem::Specification to the +tar+ file + + def add_files tar # :nodoc: + @spec.files.each do |file| + stat = File.stat file + + tar.add_file_simple file, stat.mode, stat.size do |dst_io| + open file, 'rb' do |src_io| + dst_io.write src_io.read 16384 until src_io.eof? + end + end + end + end + + ## + # Adds the package's Gem::Specification to the +tar+ file + + def add_metadata tar # :nodoc: + digests = tar.add_file_signed 'metadata.gz', 0444, @signer do |io| + gzip_to io do |gz_io| + gz_io.write @spec.to_yaml + end + end + + @checksums['metadata.gz'] = digests + end + + ## + # Builds this package based on the specification set by #spec= + + def build skip_validation = false + require 'rubygems/security' + + @spec.validate unless skip_validation + @spec.mark_version + + setup_signer + + open @gem, 'wb' do |gem_io| + Gem::Package::TarWriter.new gem_io do |gem| + add_metadata gem + add_contents gem + add_checksums gem + end + end + + say <<-EOM + Successfully built RubyGem + Name: #{@spec.name} + Version: #{@spec.version} + File: #{File.basename @spec.cache_file} +EOM + ensure + @signer = nil + end + + ## + # A list of file names contained in this gem + + def contents + return @contents if @contents + + verify unless @spec + + @contents = [] + + open @gem, 'rb' do |io| + gem_tar = Gem::Package::TarReader.new io + + gem_tar.each do |entry| + next unless entry.full_name == 'data.tar.gz' + + open_tar_gz entry do |pkg_tar| + pkg_tar.each do |contents_entry| + @contents << contents_entry.full_name end end + + return @contents + end + end + end + + ## + # Creates a digest of the TarEntry +entry+ from the digest algorithm set by + # the security policy. + + def digest entry # :nodoc: + return unless @checksums + + @checksums.each_key do |algorithm| + digester = OpenSSL::Digest.new algorithm + + digester << entry.read(16384) until entry.eof? + + entry.rewind + + @digests[algorithm][entry.full_name] = digester + end + + @digests + end + + ## + # Extracts the files in this package into +destination_dir+ + + def extract_files destination_dir + verify unless @spec + + FileUtils.mkdir_p destination_dir + + open @gem, 'rb' do |io| + reader = Gem::Package::TarReader.new io + + reader.each do |entry| + next unless entry.full_name == 'data.tar.gz' + + extract_tar_gz entry, destination_dir + + return # ignore further entries + end + end + end + + ## + # Extracts all the files in the gzipped tar archive +io+ into + # +destination_dir+. + # + # If an entry in the archive contains a relative path above + # +destination_dir+ or an absolute path is encountered an exception is + # raised. + + def extract_tar_gz io, destination_dir # :nodoc: + open_tar_gz io do |tar| + tar.each do |entry| + destination = install_location entry.full_name, destination_dir + + FileUtils.rm_rf destination + + FileUtils.mkdir_p File.dirname destination + + open destination, 'wb', entry.header.mode do |out| + out.write entry.read + out.fsync rescue nil # for filesystems without fsync(2) + end + + say destination if Gem.configuration.really_verbose + end + end + end + + ## + # Gzips content written to +gz_io+ to +io+. + #-- + # Also sets the gzip modification time to the package build time to ease + # testing. + + def gzip_to io # :yields: gz_io + gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION + gz_io.mtime = @build_time + + yield gz_io + ensure + gz_io.close + end + + ## + # Returns the full path for installing +filename+. + # + # If +filename+ is not inside +destination_dir+ an exception is raised. + + def install_location filename, destination_dir # :nodoc: + raise Gem::Package::PathError.new(filename, destination_dir) if + filename.start_with? '/' + + destination = File.join destination_dir, filename + destination = File.expand_path destination + + raise Gem::Package::PathError.new(destination, destination_dir) unless + destination.start_with? destination_dir + + destination.untaint + destination + end + + ## + # Loads a Gem::Specification from the TarEntry +entry+ + + def load_spec entry # :nodoc: + case entry.full_name + when 'metadata' then + @spec = Gem::Specification.from_yaml entry.read + when 'metadata.gz' then + args = [entry] + args << { :external_encoding => Encoding::UTF_8 } if + Object.const_defined? :Encoding + + Zlib::GzipReader.wrap(*args) do |gzio| + @spec = Gem::Specification.from_yaml gzio.read + end + end + end + + ## + # Opens +io+ as a gzipped tar archive + + def open_tar_gz io # :nodoc: + Zlib::GzipReader.wrap io do |gzio| + tar = Gem::Package::TarReader.new gzio + + yield tar + end + end + + ## + # Reads and loads checksums.yaml.gz from the tar file +gem+ + + def read_checksums gem + Gem.load_yaml + + @checksums = gem.seek 'checksums.yaml.gz' do |entry| + Zlib::GzipReader.wrap entry do |gz_io| + YAML.load gz_io.read + end + end + end + + ## + # Prepares the gem for signing and checksum generation. If a signing + # certificate and key are not present only checksum generation is set up. + + def setup_signer + if @spec.signing_key then + @signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain + @spec.signing_key = nil + @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s } + else + @signer = Gem::Security::Signer.new nil, nil + @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if + @signer.cert_chain + end + end + + ## + # The spec for this gem. + # + # If this is a package for a built gem the spec is loaded from the + # gem and returned. If this is a package for a gem being built the provided + # spec is returned. + + def spec + verify unless @spec + + @spec + end + + ## + # Verifies that this gem: + # + # * Contains a valid gem specification + # * Contains a contents archive + # * The contents archive is not corrupt + # + # After verification the gem specification from the gem is available from + # #spec + + def verify + @files = [] + @spec = nil + + open @gem, 'rb' do |io| + Gem::Package::TarReader.new io do |reader| + read_checksums reader + + verify_files reader + end + end + + verify_checksums @digests, @checksums + + @security_policy.verify_signatures @spec, @digests, @signatures if + @security_policy + + true + rescue Errno::ENOENT => e + raise Gem::Package::FormatError.new e.message + rescue Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem + end + + ## + # Verifies the +checksums+ against the +digests+. This check is not + # cryptographically secure. Missing checksums are ignored. + + def verify_checksums digests, checksums # :nodoc: + return unless checksums + + checksums.sort.each do |algorithm, gem_digests| + gem_digests.sort.each do |file_name, gem_hexdigest| + computed_digest = digests[algorithm][file_name] + + unless computed_digest.hexdigest == gem_hexdigest then + raise Gem::Package::FormatError.new \ + "#{algorithm} checksum mismatch for #{file_name}", @gem + end end end end + ## + # Verifies the files of the +gem+ + + def verify_files gem + gem.each do |entry| + file_name = entry.full_name + @files << file_name + + case file_name + when /\.sig$/ then + @signatures[$`] = entry.read if @security_policy + next + when 'checksums.yaml.gz' then + next # already handled + else + digest entry + end + + case file_name + when /^metadata(.gz)?$/ then + load_spec entry + when 'data.tar.gz' then + verify_gz entry + end + end + + unless @spec then + raise Gem::Package::FormatError.new 'package metadata is missing', @gem + end + + unless @files.include? 'data.tar.gz' then + raise Gem::Package::FormatError.new \ + 'package content (data.tar.gz) is missing', @gem + end + end + + ## + # Verifies that +entry+ is a valid gzipped file. + + def verify_gz entry # :nodoc: + Zlib::GzipReader.wrap entry do |gzio| + gzio.read 16384 until gzio.eof? # gzip checksum verification + end + rescue Zlib::GzipFile::Error => e + raise Gem::Package::FormatError.new(e.message, entry.full_name) + end + end -require 'rubygems/package/f_sync_dir' +require 'rubygems/package/digest_io' +require 'rubygems/package/old' require 'rubygems/package/tar_header' -require 'rubygems/package/tar_input' -require 'rubygems/package/tar_output' require 'rubygems/package/tar_reader' require 'rubygems/package/tar_reader/entry' require 'rubygems/package/tar_writer' |