summaryrefslogtreecommitdiff
path: root/lib/rubygems/package.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems/package.rb')
-rw-r--r--lib/rubygems/package.rb258
1 files changed, 141 insertions, 117 deletions
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index a4ae3e9ea5..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 "rubygems"
-require 'rubygems/security'
-require 'rubygems/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,8 +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 format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir)
end
end
@@ -139,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
@@ -170,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
@@ -221,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
@@ -233,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
@@ -241,7 +250,7 @@ class Gem::Package
end
end
- @checksums['data.tar.gz'] = digests
+ @checksums["data.tar.gz"] = digests
end
##
@@ -258,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
@@ -269,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
##
@@ -327,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|
@@ -338,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
##
@@ -346,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
@@ -373,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
##
@@ -400,46 +416,71 @@ EOM
# extracted.
def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
- directories = [] if dir_mode
+ 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 full_name, destination_dir
- destination = install_location entry.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(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
else
File.dirname destination
end
- directories << mkdir if directories
- mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name
-
- File.open destination, 'wb' do |out|
- out.write entry.read
- FileUtils.chmod file_mode(entry.header.mode), destination
- end if entry.file?
+ unless directories.include?(mkdir)
+ FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?)
+ directories << mkdir
+ end
- 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
- if directories
- directories.uniq!
+ 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
##
@@ -464,25 +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.expand_path(File.realpath(destination_dir))
- destination = File.expand_path(File.join(destination_dir, filename))
+ destination_dir = File.realpath(destination_dir)
+ destination = File.expand_path(filename, destination_dir)
raise Gem::Package::PathError.new(destination, destination_dir) unless
- destination.start_with? destination_dir + '/'
+ normalize_path(destination).start_with? normalize_path(destination_dir + "/")
- 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
-
- destination.tap(&Gem::UNTAINT)
destination
end
@@ -494,30 +524,14 @@ EOM
end
end
- def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name)
- destination_dir = File.realpath(File.expand_path(destination_dir))
- parts = mkdir.split(File::SEPARATOR)
- parts.reduce do |path, basename|
- path = File.realpath(path) unless path == ""
- path = File.expand_path(path + File::SEPARATOR + basename)
- lstat = File.lstat path rescue nil
- if !lstat || !lstat.directory?
- unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, **mkdir_options rescue false)
- raise Gem::Package::PathError.new(file_name, destination_dir)
- end
- end
- path
- end
- end
-
##
# Loads a Gem::Specification from the TarEntry +entry+
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
@@ -541,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
@@ -553,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(
@@ -564,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
@@ -609,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
@@ -619,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
@@ -660,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
@@ -677,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
@@ -695,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 'rubygems/package/digest_io'
-require 'rubygems/package/source'
-require 'rubygems/package/file_source'
-require 'rubygems/package/io_source'
-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'
+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"