diff options
Diffstat (limited to 'lib/rubygems/package.rb')
-rw-r--r-- | lib/rubygems/package.rb | 212 |
1 files changed, 123 insertions, 89 deletions
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 94705914af..1d5d764237 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ -# + +# rubocop:enable Style/AsciiComments + +require_relative "../rubygems" +require_relative "security" +require_relative "user_interaction" + +## # Example using a Gem::Package # # Builds a .gem file given a Gem::Specification. A .gem file is a tarball @@ -41,10 +49,6 @@ # #files are the files in the .gem tar file, not the Ruby files in the gem # #extract_files and #contents automatically call #verify -require_relative "../rubygems" -require_relative 'security' -require_relative 'user_interaction' - class Gem::Package include Gem::UserInteraction @@ -55,9 +59,9 @@ class Gem::Package def initialize(message, source = nil) if source - @path = source.path + @path = source.is_a?(String) ? source : source.path - message = message + " in #{path}" if path + message += " in #{path}" if path end super message @@ -66,15 +70,13 @@ class Gem::Package class PathError < Error def initialize(destination, destination_dir) - super "installing into parent path %s of %s is not allowed" % - [destination, destination_dir] + super format("installing into parent path %s of %s is not allowed", destination, destination_dir) end end class SymlinkError < Error def initialize(name, destination, destination_dir) - super "installing symlink '%s' pointing to parent path %s of %s is not allowed" % - [name, destination, destination_dir] + super format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir) end end @@ -146,18 +148,18 @@ class Gem::Package def self.new(gem, security_policy = nil) 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 + gem + elsif gem.respond_to? :read + Gem::Package::IOSource.new gem + else + Gem::Package::FileSource.new gem + end - return super unless Gem::Package == self + return super unless self == Gem::Package return super unless gem.present? return super unless gem.start - return super unless gem.start.include? 'MD5SUM =' + return super unless gem.start.include? "MD5SUM =" Gem::Package::Old.new gem end @@ -177,22 +179,22 @@ class Gem::Package tar = Gem::Package::TarReader.new io tar.each_entry do |entry| case entry.full_name - when 'metadata' then + when "metadata" then metadata = entry.read - when 'metadata.gz' then + when "metadata.gz" then metadata = Gem::Util.gunzip entry.read end end end - return spec, metadata + [spec, metadata] end ## # Creates a new package that will read or write to the file +gem+. def initialize(gem, security_policy) # :notnew: - require 'zlib' + require "zlib" @gem = gem @@ -228,9 +230,9 @@ class Gem::Package end end - tar.add_file_signed 'checksums.yaml.gz', 0444, @signer do |io| + tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io| gzip_to io do |gz_io| - YAML.dump checksums_by_algorithm, gz_io + Psych.dump checksums_by_algorithm, gz_io end end end @@ -240,7 +242,7 @@ class Gem::Package # and adds this file to the +tar+. def add_contents(tar) # :nodoc: - digests = tar.add_file_signed 'data.tar.gz', 0444, @signer do |io| + digests = tar.add_file_signed "data.tar.gz", 0o444, @signer do |io| gzip_to io do |gz_io| Gem::Package::TarWriter.new gz_io do |data_tar| add_files data_tar @@ -248,7 +250,7 @@ class Gem::Package end end - @checksums['data.tar.gz'] = digests + @checksums["data.tar.gz"] = digests end ## @@ -265,8 +267,8 @@ class Gem::Package next unless stat.file? tar.add_file_simple file, stat.mode, stat.size do |dst_io| - File.open file, 'rb' do |src_io| - dst_io.write src_io.read 16384 until src_io.eof? + File.open file, "rb" do |src_io| + copy_stream(src_io, dst_io) end end end @@ -276,13 +278,13 @@ class Gem::Package # 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| + digests = tar.add_file_signed "metadata.gz", 0o444, @signer do |io| gzip_to io do |gz_io| gz_io.write @spec.to_yaml end end - @checksums['metadata.gz'] = digests + @checksums["metadata.gz"] = digests end ## @@ -334,7 +336,7 @@ EOM gem_tar = Gem::Package::TarReader.new io gem_tar.each do |entry| - next unless entry.full_name == 'data.tar.gz' + next unless entry.full_name == "data.tar.gz" open_tar_gz entry do |pkg_tar| pkg_tar.each do |contents_entry| @@ -345,6 +347,8 @@ EOM return @contents end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -353,18 +357,21 @@ EOM def digest(entry) # :nodoc: algorithms = if @checksums - @checksums.keys - else - [Gem::Security::DIGEST_NAME].compact - end - - algorithms.each do |algorithm| - digester = Gem::Security.create_digest(algorithm) + @checksums.to_h {|algorithm, _| [algorithm, Gem::Security.create_digest(algorithm)] } + elsif Gem::Security::DIGEST_NAME + { Gem::Security::DIGEST_NAME => Gem::Security.create_digest(Gem::Security::DIGEST_NAME) } + end - digester << entry.read(16384) until entry.eof? + return @digests if algorithms.nil? || algorithms.empty? - entry.rewind + buf = String.new(capacity: 16_384, encoding: Encoding::BINARY) + until entry.eof? + entry.readpartial(16_384, buf) + algorithms.each_value {|digester| digester << buf } + end + entry.rewind + algorithms.each do |algorithm, digester| @digests[algorithm][entry.full_name] = digester end @@ -380,19 +387,21 @@ EOM def extract_files(destination_dir, pattern = "*") verify unless @spec - FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755 @gem.with_read_io do |io| reader = Gem::Package::TarReader.new io reader.each do |entry| - next unless entry.full_name == 'data.tar.gz' + next unless entry.full_name == "data.tar.gz" extract_tar_gz entry, destination_dir, pattern - return # ignore further entries + break # ignore further entries end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -407,25 +416,30 @@ EOM # extracted. def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: + destination_dir = File.realpath(destination_dir) + directories = [] + symlinks = [] + open_tar_gz io do |tar| tar.each do |entry| - next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH + full_name = entry.full_name + next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH - destination = install_location entry.full_name, destination_dir + destination = install_location full_name, destination_dir if entry.symlink? link_target = entry.header.linkname real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination)) - raise Gem::Package::SymlinkError.new(entry.full_name, real_destination, destination_dir) unless - normalize_path(real_destination).start_with? normalize_path(destination_dir + '/') + raise Gem::Package::SymlinkError.new(full_name, real_destination, destination_dir) unless + normalize_path(real_destination).start_with? normalize_path(destination_dir + "/") + + symlinks << [full_name, link_target, destination, real_destination] end FileUtils.rm_rf destination - mkdir_options = {} - mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?) mkdir = if entry.directory? destination @@ -434,28 +448,39 @@ EOM end unless directories.include?(mkdir) - FileUtils.mkdir_p mkdir, **mkdir_options + FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?) directories << mkdir end - File.open destination, 'wb' do |out| - out.write entry.read - FileUtils.chmod file_mode(entry.header.mode), destination - end if entry.file? - - File.symlink(entry.header.linkname, destination) if entry.symlink? + if entry.file? + File.open(destination, "wb") {|out| copy_stream(entry, out) } + FileUtils.chmod file_mode(entry.header.mode) & ~File.umask, destination + end verbose destination end end + symlinks.each do |name, target, destination, real_destination| + if File.exist?(real_destination) + File.symlink(target, destination) + else + alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring" + end + end + if dir_mode File.chmod(dir_mode, *directories) end end def file_mode(mode) # :nodoc: - ((mode & 0111).zero? ? data_mode : prog_mode) || mode + ((mode & 0o111).zero? ? data_mode : prog_mode) || + # If we're not using one of the default modes, then we're going to fall + # back to the mode from the tarball. In this case we need to mask it down + # to fit into 2^16 bits (the maximum value for a mode in CRuby since it + # gets put into an unsigned short). + (mode & ((1 << 16) - 1)) end ## @@ -480,15 +505,14 @@ EOM def install_location(filename, destination_dir) # :nodoc: raise Gem::Package::PathError.new(filename, destination_dir) if - filename.start_with? '/' + filename.start_with? "/" destination_dir = File.realpath(destination_dir) destination = File.expand_path(filename, destination_dir) raise Gem::Package::PathError.new(destination, destination_dir) unless - normalize_path(destination).start_with? normalize_path(destination_dir + '/') + normalize_path(destination).start_with? normalize_path(destination_dir + "/") - destination.tap(&Gem::UNTAINT) destination end @@ -505,9 +529,9 @@ EOM def load_spec(entry) # :nodoc: case entry.full_name - when 'metadata' then + when "metadata" then @spec = Gem::Specification.from_yaml entry.read - when 'metadata.gz' then + when "metadata.gz" then Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| @spec = Gem::Specification.from_yaml gzio.read end @@ -531,7 +555,7 @@ EOM def read_checksums(gem) Gem.load_yaml - @checksums = gem.seek 'checksums.yaml.gz' do |entry| + @checksums = gem.seek "checksums.yaml.gz" do |entry| Zlib::GzipReader.wrap entry do |gz_io| Gem::SafeYAML.safe_load gz_io.read end @@ -543,7 +567,7 @@ EOM # certificate and key are not present only checksum generation is set up. def setup_signer(signer_options: {}) - passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] + passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] if @spec.signing_key @signer = Gem::Security::Signer.new( @@ -554,10 +578,10 @@ EOM ) @spec.signing_key = nil - @spec.cert_chain = @signer.cert_chain.map {|cert| cert.to_s } + @spec.cert_chain = @signer.cert_chain.map(&:to_s) else @signer = Gem::Security::Signer.new nil, nil, passphrase - @spec.cert_chain = @signer.cert_chain.map {|cert| cert.to_pem } if + @spec.cert_chain = @signer.cert_chain.map(&:to_pem) if @signer.cert_chain end end @@ -599,8 +623,7 @@ EOM verify_checksums @digests, @checksums - @security_policy.verify_signatures @spec, @digests, @signatures if - @security_policy + @security_policy&.verify_signatures @spec, @digests, @signatures true rescue Gem::Security::Exception @@ -609,7 +632,7 @@ EOM raise rescue Errno::ENOENT => e raise Gem::Package::FormatError.new e.message - rescue Gem::Package::TarInvalidError => e + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e raise Gem::Package::FormatError.new e.message, @gem end @@ -650,10 +673,10 @@ EOM case file_name when "metadata", "metadata.gz" then load_spec entry - when 'data.tar.gz' then + when "data.tar.gz" then verify_gz entry end - rescue + rescue StandardError warn "Exception while verifying #{@gem.path}" raise end @@ -667,16 +690,16 @@ EOM end unless @spec - raise Gem::Package::FormatError.new 'package metadata is missing', @gem + raise Gem::Package::FormatError.new "package metadata is missing", @gem end - unless @files.include? 'data.tar.gz' + unless @files.include? "data.tar.gz" raise Gem::Package::FormatError.new \ - 'package content (data.tar.gz) is missing', @gem + "package content (data.tar.gz) is missing", @gem end - 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(', ')})" + if (duplicates = @files.group_by {|f| f }.select {|_k,v| v.size > 1 }.map(&:first)) && duplicates.any? + raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(", ")})" end end @@ -685,19 +708,30 @@ EOM def verify_gz(entry) # :nodoc: Zlib::GzipReader.wrap entry do |gzio| - gzio.read 16384 until gzio.eof? # gzip checksum verification + # TODO: read into a buffer once zlib supports it + gzio.read 16_384 until gzio.eof? # gzip checksum verification end rescue Zlib::GzipFile::Error => e raise Gem::Package::FormatError.new(e.message, entry.full_name) end + + if RUBY_ENGINE == "truffleruby" + def copy_stream(src, dst) # :nodoc: + dst.write src.read + end + else + def copy_stream(src, dst) # :nodoc: + IO.copy_stream(src, dst) + end + end end -require_relative 'package/digest_io' -require_relative 'package/source' -require_relative 'package/file_source' -require_relative 'package/io_source' -require_relative 'package/old' -require_relative 'package/tar_header' -require_relative 'package/tar_reader' -require_relative 'package/tar_reader/entry' -require_relative 'package/tar_writer' +require_relative "package/digest_io" +require_relative "package/source" +require_relative "package/file_source" +require_relative "package/io_source" +require_relative "package/old" +require_relative "package/tar_header" +require_relative "package/tar_reader" +require_relative "package/tar_reader/entry" +require_relative "package/tar_writer" |