2010-04-22 08:24:42 +00:00
|
|
|
# -*- coding: utf-8 -*-
|
2016-02-02 00:11:34 +00:00
|
|
|
# frozen_string_literal: true
|
2007-11-10 07:48:56 +00:00
|
|
|
#--
|
2010-04-22 08:24:42 +00:00
|
|
|
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
|
|
|
|
# See LICENSE.txt for additional licensing information.
|
|
|
|
#++
|
2012-11-29 06:52:18 +00:00
|
|
|
#
|
|
|
|
# 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)
|
|
|
|
#
|
2013-09-14 08:59:02 +00:00
|
|
|
# #files are the files in the .gem tar file, not the Ruby files in the gem
|
2012-11-29 06:52:18 +00:00
|
|
|
# #extract_files and #contents automatically call #verify
|
2007-11-10 07:48:56 +00:00
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
require 'rubygems/security'
|
2007-11-10 07:48:56 +00:00
|
|
|
require 'rubygems/specification'
|
2012-11-29 06:52:18 +00:00
|
|
|
require 'rubygems/user_interaction'
|
|
|
|
require 'zlib'
|
2007-11-10 07:48:56 +00:00
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
class Gem::Package
|
|
|
|
|
|
|
|
include Gem::UserInteraction
|
|
|
|
|
|
|
|
class Error < Gem::Exception; end
|
2007-11-10 07:48:56 +00:00
|
|
|
|
2011-02-01 03:11:34 +00:00
|
|
|
class FormatError < Error
|
2019-02-14 12:59:03 +00:00
|
|
|
|
2011-02-01 03:11:34 +00:00
|
|
|
attr_reader :path
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def initialize(message, source = nil)
|
2014-09-14 03:30:02 +00:00
|
|
|
if source
|
|
|
|
@path = source.path
|
2011-02-01 03:11:34 +00:00
|
|
|
|
2016-02-01 12:43:26 +00:00
|
|
|
message = message + " in #{path}" if path
|
2014-09-14 03:30:02 +00:00
|
|
|
end
|
2011-02-01 03:11:34 +00:00
|
|
|
|
|
|
|
super message
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
class PathError < Error
|
2019-02-14 12:59:03 +00:00
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def initialize(destination, destination_dir)
|
2012-11-29 06:52:18 +00:00
|
|
|
super "installing into parent path %s of %s is not allowed" %
|
|
|
|
[destination, destination_dir]
|
|
|
|
end
|
2019-02-14 12:59:03 +00:00
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
class NonSeekableIO < Error; end
|
|
|
|
|
|
|
|
class TooLongFileName < Error; end
|
|
|
|
|
2011-02-01 03:11:34 +00:00
|
|
|
##
|
|
|
|
# Raised when a tar file is corrupt
|
|
|
|
|
|
|
|
class TarInvalidError < Error; end
|
2007-11-10 07:48:56 +00:00
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
|
|
|
|
2019-07-11 09:20:43 -04:00
|
|
|
##
|
|
|
|
# Reference to the gem being packaged.
|
|
|
|
|
|
|
|
attr_reader :gem
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
##
|
|
|
|
# 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
|
|
|
|
|
2018-05-30 13:01:35 +00:00
|
|
|
##
|
|
|
|
# Permission for directories
|
|
|
|
attr_accessor :dir_mode
|
|
|
|
|
|
|
|
##
|
|
|
|
# Permission for program files
|
|
|
|
attr_accessor :prog_mode
|
|
|
|
|
|
|
|
##
|
|
|
|
# Permission for other files
|
|
|
|
attr_accessor :data_mode
|
|
|
|
|
2018-12-12 05:07:50 +00:00
|
|
|
def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
|
|
|
|
gem_file = file_name || spec.file_name
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
package = new gem_file
|
|
|
|
package.spec = spec
|
2018-08-27 10:05:04 +00:00
|
|
|
package.build skip_validation, strict_validation
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
gem_file
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
2014-09-14 03:30:02 +00:00
|
|
|
# Creates a new Gem::Package for the file at +gem+. +gem+ can also be
|
|
|
|
# provided as an IO object.
|
2012-11-29 06:52:18 +00:00
|
|
|
#
|
|
|
|
# If +gem+ is an existing file in the old format a Gem::Package::Old will be
|
|
|
|
# returned.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def self.new(gem, security_policy = nil)
|
2014-09-14 03:30:02 +00:00
|
|
|
gem = if gem.is_a?(Gem::Package::Source)
|
|
|
|
gem
|
|
|
|
elsif gem.respond_to? :read
|
|
|
|
Gem::Package::IOSource.new gem
|
|
|
|
else
|
|
|
|
Gem::Package::FileSource.new gem
|
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2015-07-01 21:50:14 +00:00
|
|
|
return super unless Gem::Package == self
|
2014-09-14 03:30:02 +00:00
|
|
|
return super unless gem.present?
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
return super unless gem.start
|
|
|
|
return super unless gem.start.include? 'MD5SUM ='
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
Gem::Package::Old.new gem
|
|
|
|
end
|
|
|
|
|
2019-06-23 20:03:50 -06:00
|
|
|
##
|
|
|
|
# Extracts the Gem::Specification and raw metadata from the .gem file at
|
|
|
|
# +path+.
|
|
|
|
#--
|
|
|
|
|
2019-06-23 20:08:34 -06:00
|
|
|
def self.raw_spec(path, security_policy = nil)
|
2019-06-23 20:03:50 -06:00
|
|
|
format = new(path, security_policy)
|
|
|
|
spec = format.spec
|
|
|
|
|
|
|
|
metadata = nil
|
|
|
|
|
|
|
|
File.open path, Gem.binary_mode do |io|
|
|
|
|
tar = Gem::Package::TarReader.new io
|
|
|
|
tar.each_entry do |entry|
|
|
|
|
case entry.full_name
|
|
|
|
when 'metadata' then
|
|
|
|
metadata = entry.read
|
|
|
|
when 'metadata.gz' then
|
|
|
|
metadata = Gem::Util.gunzip entry.read
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
return spec, metadata
|
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
##
|
|
|
|
# Creates a new package that will read or write to the file +gem+.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def initialize(gem, security_policy) # :notnew:
|
2012-11-29 06:52:18 +00:00
|
|
|
@gem = gem
|
|
|
|
|
2019-08-17 04:45:09 +00:00
|
|
|
@build_time = Gem.source_date_epoch
|
2012-11-29 06:52:18 +00:00
|
|
|
@checksums = {}
|
|
|
|
@contents = nil
|
|
|
|
@digests = Hash.new { |h, algorithm| h[algorithm] = {} }
|
|
|
|
@files = nil
|
2015-07-01 21:50:14 +00:00
|
|
|
@security_policy = security_policy
|
2012-11-29 06:52:18 +00:00
|
|
|
@signatures = {}
|
|
|
|
@signer = nil
|
|
|
|
@spec = nil
|
|
|
|
end
|
|
|
|
|
2015-07-01 21:50:14 +00:00
|
|
|
##
|
|
|
|
# Copies this package to +path+ (if possible)
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def copy_to(path)
|
2015-07-01 21:50:14 +00:00
|
|
|
FileUtils.cp @gem.path, path unless File.exist? path
|
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
##
|
|
|
|
# Adds a checksum for each entry in the gem to checksums.yaml.gz.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def add_checksums(tar)
|
2012-11-29 06:52:18 +00:00
|
|
|
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+.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def add_contents(tar) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def add_files(tar) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
@spec.files.each do |file|
|
2015-07-01 21:50:14 +00:00
|
|
|
stat = File.lstat file
|
|
|
|
|
|
|
|
if stat.symlink?
|
2018-12-12 05:07:50 +00:00
|
|
|
target_path = File.readlink(file)
|
|
|
|
|
|
|
|
unless target_path.start_with? '.'
|
|
|
|
relative_dir = File.dirname(file).sub("#{Dir.pwd}/", '')
|
|
|
|
target_path = File.join(relative_dir, target_path)
|
|
|
|
end
|
|
|
|
|
2016-06-18 05:11:55 +00:00
|
|
|
tar.add_symlink file, target_path, stat.mode
|
2015-07-01 21:50:14 +00:00
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2012-12-10 00:13:42 +00:00
|
|
|
next unless stat.file?
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
tar.add_file_simple file, stat.mode, stat.size do |dst_io|
|
2018-02-06 02:58:35 +00:00
|
|
|
File.open file, 'rb' do |src_io|
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def add_metadata(tar) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
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=
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def build(skip_validation = false, strict_validation = false)
|
2018-08-27 10:05:04 +00:00
|
|
|
raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation
|
|
|
|
|
2012-11-30 06:20:34 +00:00
|
|
|
Gem.load_yaml
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
@spec.mark_version
|
2018-08-27 10:05:04 +00:00
|
|
|
@spec.validate true, strict_validation unless skip_validation
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2018-10-22 00:27:02 +00:00
|
|
|
setup_signer(
|
|
|
|
signer_options: {
|
|
|
|
expiration_length_days: Gem.configuration.cert_expiration_length_days
|
|
|
|
}
|
|
|
|
)
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
@gem.with_write_io do |gem_io|
|
2012-11-29 06:52:18 +00:00
|
|
|
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}
|
2018-12-12 05:07:50 +00:00
|
|
|
File: #{File.basename @gem.path}
|
2012-11-29 06:52:18 +00:00
|
|
|
EOM
|
|
|
|
ensure
|
|
|
|
@signer = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# A list of file names contained in this gem
|
|
|
|
|
|
|
|
def contents
|
|
|
|
return @contents if @contents
|
|
|
|
|
|
|
|
verify unless @spec
|
|
|
|
|
|
|
|
@contents = []
|
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
@gem.with_read_io do |io|
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
2007-11-10 07:48:56 +00:00
|
|
|
end
|
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
return @contents
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Creates a digest of the TarEntry +entry+ from the digest algorithm set by
|
|
|
|
# the security policy.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def digest(entry) # :nodoc:
|
|
|
|
algorithms = if @checksums
|
2013-02-07 05:56:53 +00:00
|
|
|
@checksums.keys
|
|
|
|
else
|
2013-09-14 08:59:02 +00:00
|
|
|
[Gem::Security::DIGEST_NAME].compact
|
2013-02-07 05:56:53 +00:00
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2013-02-07 05:56:53 +00:00
|
|
|
algorithms.each do |algorithm|
|
2013-09-14 08:59:02 +00:00
|
|
|
digester =
|
2018-11-21 10:20:47 +00:00
|
|
|
if defined?(OpenSSL::Digest)
|
2013-09-14 08:59:02 +00:00
|
|
|
OpenSSL::Digest.new algorithm
|
|
|
|
else
|
|
|
|
Digest.const_get(algorithm).new
|
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
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+
|
2013-09-14 08:59:02 +00:00
|
|
|
#
|
|
|
|
# If +pattern+ is specified, only entries matching that glob will be
|
|
|
|
# extracted.
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def extract_files(destination_dir, pattern = "*")
|
2012-11-29 06:52:18 +00:00
|
|
|
verify unless @spec
|
|
|
|
|
2018-12-23 00:20:49 +00:00
|
|
|
FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
@gem.with_read_io do |io|
|
2012-11-29 06:52:18 +00:00
|
|
|
reader = Gem::Package::TarReader.new io
|
|
|
|
|
|
|
|
reader.each do |entry|
|
|
|
|
next unless entry.full_name == 'data.tar.gz'
|
|
|
|
|
2013-09-14 08:59:02 +00:00
|
|
|
extract_tar_gz entry, destination_dir, pattern
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
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.
|
2013-09-14 08:59:02 +00:00
|
|
|
#
|
|
|
|
# If +pattern+ is specified, only entries matching that glob will be
|
|
|
|
# extracted.
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
|
2018-05-30 13:01:35 +00:00
|
|
|
directories = [] if dir_mode
|
2012-11-29 06:52:18 +00:00
|
|
|
open_tar_gz io do |tar|
|
|
|
|
tar.each do |entry|
|
2013-10-16 00:14:16 +00:00
|
|
|
next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH
|
2013-09-14 08:59:02 +00:00
|
|
|
|
2013-10-16 00:14:16 +00:00
|
|
|
destination = install_location entry.full_name, destination_dir
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
FileUtils.rm_rf destination
|
|
|
|
|
2013-09-18 21:29:41 +00:00
|
|
|
mkdir_options = {}
|
2018-12-23 00:20:49 +00:00
|
|
|
mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?)
|
2013-09-18 21:29:41 +00:00
|
|
|
mkdir =
|
2018-11-21 10:20:47 +00:00
|
|
|
if entry.directory?
|
2013-09-18 21:29:41 +00:00
|
|
|
destination
|
|
|
|
else
|
|
|
|
File.dirname destination
|
|
|
|
end
|
2018-05-30 13:01:35 +00:00
|
|
|
directories << mkdir if directories
|
2013-09-18 21:29:41 +00:00
|
|
|
|
2018-02-16 08:08:06 +00:00
|
|
|
mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2018-02-06 02:58:35 +00:00
|
|
|
File.open destination, 'wb' do |out|
|
2012-11-29 06:52:18 +00:00
|
|
|
out.write entry.read
|
2018-05-30 13:01:35 +00:00
|
|
|
FileUtils.chmod file_mode(entry.header.mode), destination
|
2013-09-18 21:29:41 +00:00
|
|
|
end if entry.file?
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2016-02-01 12:43:26 +00:00
|
|
|
File.symlink(entry.header.linkname, destination) if entry.symlink?
|
2015-07-01 21:50:14 +00:00
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
verbose destination
|
2012-11-29 06:52:18 +00:00
|
|
|
end
|
|
|
|
end
|
2018-05-30 13:01:35 +00:00
|
|
|
|
|
|
|
if directories
|
|
|
|
directories.uniq!
|
|
|
|
File.chmod(dir_mode, *directories)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def file_mode(mode) # :nodoc:
|
|
|
|
((mode & 0111).zero? ? data_mode : prog_mode) || mode
|
2012-11-29 06:52:18 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Gzips content written to +gz_io+ to +io+.
|
|
|
|
#--
|
|
|
|
# Also sets the gzip modification time to the package build time to ease
|
|
|
|
# testing.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def gzip_to(io) # :yields: gz_io
|
2012-11-29 06:52:18 +00:00
|
|
|
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.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def install_location(filename, destination_dir) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
raise Gem::Package::PathError.new(filename, destination_dir) if
|
|
|
|
filename.start_with? '/'
|
|
|
|
|
2018-05-30 13:01:35 +00:00
|
|
|
destination_dir = File.expand_path(File.realpath(destination_dir))
|
|
|
|
destination = File.expand_path(File.join(destination_dir, filename))
|
2012-11-29 06:52:18 +00:00
|
|
|
|
|
|
|
raise Gem::Package::PathError.new(destination, destination_dir) unless
|
2018-02-16 08:08:06 +00:00
|
|
|
destination.start_with? destination_dir + '/'
|
2012-11-29 06:52:18 +00:00
|
|
|
|
2019-03-05 03:32:58 +00:00
|
|
|
begin
|
|
|
|
real_destination = File.expand_path(File.realpath(destination))
|
|
|
|
rescue
|
|
|
|
# it's fine if the destination doesn't exist, because rm -rf'ing it can't cause any damage
|
|
|
|
nil
|
|
|
|
else
|
|
|
|
raise Gem::Package::PathError.new(real_destination, destination_dir) unless
|
|
|
|
real_destination.start_with? destination_dir + '/'
|
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
destination.untaint
|
|
|
|
destination
|
|
|
|
end
|
|
|
|
|
2018-05-18 01:39:13 +00:00
|
|
|
def normalize_path(pathname)
|
|
|
|
if Gem.win_platform?
|
|
|
|
pathname.downcase
|
|
|
|
else
|
|
|
|
pathname
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name)
|
2018-05-30 13:01:35 +00:00
|
|
|
destination_dir = File.realpath(File.expand_path(destination_dir))
|
2018-02-16 08:08:06 +00:00
|
|
|
parts = mkdir.split(File::SEPARATOR)
|
|
|
|
parts.reduce do |path, basename|
|
2018-05-30 13:01:35 +00:00
|
|
|
path = File.realpath(path) unless path == ""
|
2018-02-16 08:08:06 +00:00
|
|
|
path = File.expand_path(path + File::SEPARATOR + basename)
|
|
|
|
lstat = File.lstat path rescue nil
|
|
|
|
if !lstat || !lstat.directory?
|
2019-04-07 16:44:49 -07:00
|
|
|
unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, **mkdir_options rescue false)
|
2018-02-16 08:08:06 +00:00
|
|
|
raise Gem::Package::PathError.new(file_name, destination_dir)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
path
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
##
|
|
|
|
# Loads a Gem::Specification from the TarEntry +entry+
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def load_spec(entry) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
case entry.full_name
|
|
|
|
when 'metadata' then
|
|
|
|
@spec = Gem::Specification.from_yaml entry.read
|
|
|
|
when 'metadata.gz' then
|
Make rb_scan_args handle keywords more similar to Ruby methods (#2460)
Cfuncs that use rb_scan_args with the : entry suffer similar keyword
argument separation issues that Ruby methods suffer if the cfuncs
accept optional or variable arguments.
This makes the following changes to : handling.
* Treats as **kw, prompting keyword argument separation warnings
if called with a positional hash.
* Do not look for an option hash if empty keywords are provided.
For backwards compatibility, treat an empty keyword splat as a empty
mandatory positional hash argument, but emit a a warning, as this
behavior will be removed in Ruby 3. The argument number check
needs to be moved lower so it can correctly handle an empty
positional argument being added.
* If the last argument is nil and it is necessary to treat it as an option
hash in order to make sure all arguments are processed, continue to
treat the last argument as the option hash. Emit a warning in this case,
as this behavior will be removed in Ruby 3.
* If splitting the keyword hash into two hashes, issue a warning, as we
will not be splitting hashes in Ruby 3.
* If the keyword argument is required to fill a mandatory positional
argument, continue to do so, but emit a warning as this behavior will
be going away in Ruby 3.
* If keyword arguments are provided and the last argument is not a hash,
that indicates something wrong. This can happen if a cfunc is calling
rb_scan_args multiple times, and providing arguments that were not
passed to it from Ruby. Callers need to switch to the new
rb_scan_args_kw function, which allows passing of whether keywords
were provided.
This commit fixes all warnings caused by the changes above.
It switches some function calls to *_kw versions with appropriate
kw_splat flags. If delegating arguments, RB_PASS_CALLED_KEYWORDS
is used. If creating new arguments, RB_PASS_KEYWORDS is used if
the last argument is a hash to be treated as keywords.
In open_key_args in io.c, use rb_scan_args_kw.
In this case, the arguments provided come from another C
function, not Ruby. The last argument may or may not be a hash,
so we can't set keyword argument mode. However, if it is a
hash, we don't want to warn when treating it as keywords.
In Ruby files, make sure to appropriately use keyword splats
or literal keywords when calling Cfuncs that now issue keyword
argument separation warnings through rb_scan_args. Also, make
sure not to pass nil in place of an option hash.
Work around Kernel#warn warnings due to problems in the Rubygems
override of the method. There is an open pull request to fix
these issues in Rubygems, but part of the Rubygems tests for
their override fail on ruby-head due to rb_scan_args not
recognizing empty keyword splats, which this commit fixes.
Implementation wise, adding rb_scan_args_kw is kind of a pain,
because rb_scan_args takes a variable number of arguments.
In order to not duplicate all the code, the function internals need
to be split into two functions taking a va_list, and to avoid passing
in a ton of arguments, a single struct argument is used to handle
the variables previously local to the function.
2019-09-25 11:18:49 -07:00
|
|
|
opts = {}
|
|
|
|
opts[:external_encoding] = Encoding::UTF_8 if
|
2018-05-30 13:01:35 +00:00
|
|
|
Zlib::GzipReader.method(:wrap).arity != 1
|
2012-11-29 06:52:18 +00:00
|
|
|
|
Make rb_scan_args handle keywords more similar to Ruby methods (#2460)
Cfuncs that use rb_scan_args with the : entry suffer similar keyword
argument separation issues that Ruby methods suffer if the cfuncs
accept optional or variable arguments.
This makes the following changes to : handling.
* Treats as **kw, prompting keyword argument separation warnings
if called with a positional hash.
* Do not look for an option hash if empty keywords are provided.
For backwards compatibility, treat an empty keyword splat as a empty
mandatory positional hash argument, but emit a a warning, as this
behavior will be removed in Ruby 3. The argument number check
needs to be moved lower so it can correctly handle an empty
positional argument being added.
* If the last argument is nil and it is necessary to treat it as an option
hash in order to make sure all arguments are processed, continue to
treat the last argument as the option hash. Emit a warning in this case,
as this behavior will be removed in Ruby 3.
* If splitting the keyword hash into two hashes, issue a warning, as we
will not be splitting hashes in Ruby 3.
* If the keyword argument is required to fill a mandatory positional
argument, continue to do so, but emit a warning as this behavior will
be going away in Ruby 3.
* If keyword arguments are provided and the last argument is not a hash,
that indicates something wrong. This can happen if a cfunc is calling
rb_scan_args multiple times, and providing arguments that were not
passed to it from Ruby. Callers need to switch to the new
rb_scan_args_kw function, which allows passing of whether keywords
were provided.
This commit fixes all warnings caused by the changes above.
It switches some function calls to *_kw versions with appropriate
kw_splat flags. If delegating arguments, RB_PASS_CALLED_KEYWORDS
is used. If creating new arguments, RB_PASS_KEYWORDS is used if
the last argument is a hash to be treated as keywords.
In open_key_args in io.c, use rb_scan_args_kw.
In this case, the arguments provided come from another C
function, not Ruby. The last argument may or may not be a hash,
so we can't set keyword argument mode. However, if it is a
hash, we don't want to warn when treating it as keywords.
In Ruby files, make sure to appropriately use keyword splats
or literal keywords when calling Cfuncs that now issue keyword
argument separation warnings through rb_scan_args. Also, make
sure not to pass nil in place of an option hash.
Work around Kernel#warn warnings due to problems in the Rubygems
override of the method. There is an open pull request to fix
these issues in Rubygems, but part of the Rubygems tests for
their override fail on ruby-head due to rb_scan_args not
recognizing empty keyword splats, which this commit fixes.
Implementation wise, adding rb_scan_args_kw is kind of a pain,
because rb_scan_args takes a variable number of arguments.
In order to not duplicate all the code, the function internals need
to be split into two functions taking a va_list, and to avoid passing
in a ton of arguments, a single struct argument is used to handle
the variables previously local to the function.
2019-09-25 11:18:49 -07:00
|
|
|
Zlib::GzipReader.wrap(entry, **opts) do |gzio|
|
2012-11-29 06:52:18 +00:00
|
|
|
@spec = Gem::Specification.from_yaml gzio.read
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Opens +io+ as a gzipped tar archive
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def open_tar_gz(io) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
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+
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def read_checksums(gem)
|
2012-11-29 06:52:18 +00:00
|
|
|
Gem.load_yaml
|
|
|
|
|
|
|
|
@checksums = gem.seek 'checksums.yaml.gz' do |entry|
|
|
|
|
Zlib::GzipReader.wrap entry do |gz_io|
|
2017-10-10 08:58:22 +00:00
|
|
|
Gem::SafeYAML.safe_load gz_io.read
|
2012-11-29 06:52:18 +00:00
|
|
|
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.
|
|
|
|
|
2018-10-22 00:27:02 +00:00
|
|
|
def setup_signer(signer_options: {})
|
2013-09-14 08:59:02 +00:00
|
|
|
passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE']
|
2018-11-21 10:20:47 +00:00
|
|
|
if @spec.signing_key
|
2018-10-22 00:27:02 +00:00
|
|
|
@signer =
|
|
|
|
Gem::Security::Signer.new(
|
|
|
|
@spec.signing_key,
|
|
|
|
@spec.cert_chain,
|
|
|
|
passphrase,
|
|
|
|
signer_options
|
|
|
|
)
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
@spec.signing_key = nil
|
|
|
|
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s }
|
|
|
|
else
|
2013-09-14 08:59:02 +00:00
|
|
|
@signer = Gem::Security::Signer.new nil, nil, passphrase
|
2012-11-29 06:52:18 +00:00
|
|
|
@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
|
|
|
|
|
2014-09-14 03:30:02 +00:00
|
|
|
@gem.with_read_io do |io|
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
2013-02-05 02:37:35 +00:00
|
|
|
rescue Gem::Security::Exception
|
|
|
|
@spec = nil
|
|
|
|
@files = []
|
|
|
|
raise
|
2012-11-29 06:52:18 +00:00
|
|
|
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.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def verify_checksums(digests, checksums) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
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]
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
unless computed_digest.hexdigest == gem_hexdigest
|
2012-11-29 06:52:18 +00:00
|
|
|
raise Gem::Package::FormatError.new \
|
|
|
|
"#{algorithm} checksum mismatch for #{file_name}", @gem
|
|
|
|
end
|
2007-11-10 07:48:56 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2013-09-14 08:59:02 +00:00
|
|
|
##
|
|
|
|
# Verifies +entry+ in a .gem file.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def verify_entry(entry)
|
2013-09-14 08:59:02 +00:00
|
|
|
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
|
2018-05-18 01:39:13 +00:00
|
|
|
when "metadata", "metadata.gz" then
|
2013-09-14 08:59:02 +00:00
|
|
|
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
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
##
|
|
|
|
# Verifies the files of the +gem+
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def verify_files(gem)
|
2012-11-29 06:52:18 +00:00
|
|
|
gem.each do |entry|
|
2013-09-14 08:59:02 +00:00
|
|
|
verify_entry entry
|
2012-11-29 06:52:18 +00:00
|
|
|
end
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
unless @spec
|
2012-11-29 06:52:18 +00:00
|
|
|
raise Gem::Package::FormatError.new 'package metadata is missing', @gem
|
|
|
|
end
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
unless @files.include? 'data.tar.gz'
|
2012-11-29 06:52:18 +00:00
|
|
|
raise Gem::Package::FormatError.new \
|
|
|
|
'package content (data.tar.gz) is missing', @gem
|
|
|
|
end
|
2018-02-16 08:08:06 +00:00
|
|
|
|
|
|
|
if duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first) and duplicates.any?
|
|
|
|
raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})"
|
|
|
|
end
|
2012-11-29 06:52:18 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
##
|
|
|
|
# Verifies that +entry+ is a valid gzipped file.
|
|
|
|
|
2018-11-21 10:20:47 +00:00
|
|
|
def verify_gz(entry) # :nodoc:
|
2012-11-29 06:52:18 +00:00
|
|
|
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
|
2019-06-23 20:08:34 -06:00
|
|
|
|
2007-11-10 07:48:56 +00:00
|
|
|
end
|
|
|
|
|
2012-11-29 06:52:18 +00:00
|
|
|
require 'rubygems/package/digest_io'
|
2014-09-14 03:30:02 +00:00
|
|
|
require 'rubygems/package/source'
|
|
|
|
require 'rubygems/package/file_source'
|
|
|
|
require 'rubygems/package/io_source'
|
2012-11-29 06:52:18 +00:00
|
|
|
require 'rubygems/package/old'
|
2008-03-31 22:40:06 +00:00
|
|
|
require 'rubygems/package/tar_header'
|
|
|
|
require 'rubygems/package/tar_reader'
|
|
|
|
require 'rubygems/package/tar_reader/entry'
|
|
|
|
require 'rubygems/package/tar_writer'
|