#– # 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 # 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'
class Gem::Package
include Gem::UserInteraction
class Error < Gem::Exception; end
class FormatError < Error
attr_reader :path
def initialize message, path = nil
@path = path
message << " in #{path}" if path
super message
end
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
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
next unless 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
Gem.load_yaml
require 'rubygems/security'
@spec.mark_version
@spec.validate unless skip_validation
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: algorithms = if @checksums then @checksums.keys else [Gem::Security::DIGEST_NAME].compact end algorithms.each do |algorithm| digester = if defined?(OpenSSL::Digest) then OpenSSL::Digest.new algorithm else Digest.const_get(algorithm).new end 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+ # # If +pattern+ is specified, only entries matching that glob will be # extracted. def extract_files destination_dir, pattern = "*" 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, pattern 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. # # If +pattern+ is specified, only entries matching that glob will be # extracted. def extract_tar_gz io, destination_dir, pattern = "*" # :nodoc: open_tar_gz io do |tar| tar.each do |entry| next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH 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 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_dir = File.realpath destination_dir if File.respond_to? :realpath destination_dir = File.expand_path destination_dir 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.method(:wrap).arity != 1 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 passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] if @spec.signing_key then @signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain, passphrase @spec.signing_key = nil @spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s } else @signer = Gem::Security::Signer.new nil, nil, passphrase @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 Gem::Security::Exception @spec = nil @files = [] raise 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 +entry+ in a .gem file. def verify_entry entry file_name = entry.full_name @files << file_name case file_name when /\.sig$/ then @signatures[$`] = entry.read if @security_policy return else digest entry end case file_name when /^metadata(.gz)?$/ then load_spec entry when 'data.tar.gz' then verify_gz entry end rescue => e message = "package is corrupt, exception while verifying: " + "#{e.message} (#{e.class})" raise Gem::Package::FormatError.new message, @gem end ## # Verifies the files of the +gem+ def verify_files gem gem.each do |entry| verify_entry entry 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/digest_io' require 'rubygems/package/old' require 'rubygems/package/tar_header' require 'rubygems/package/tar_reader' require 'rubygems/package/tar_reader/entry' require 'rubygems/package/tar_writer'