Class: Gem::Package
| Relationships & Source Files | |
| Namespace Children | |
|
Classes:
| |
|
Exceptions:
| |
| Extension / Inclusion / Inheritance Descendants | |
|
Subclasses:
|
|
| Super Chains via Extension / Inclusion / Inheritance | |
|
Instance Chain:
|
|
| Inherits: | Object |
| Defined in: | lib/rubygems/package.rb |
Overview
Example using a Package
Builds a #gem file given a Specification. A #gem file is a tarball
which contains a data.tar.gz, metadata.gz, checksums.yaml.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
Class Method Summary
- .build(spec, skip_validation = false, strict_validation = false, file_name = nil)
-
.new(gem, security_policy = nil) ⇒ Package
constructor
Creates a new
Packagefor the file at #gem. -
.raw_spec(path, security_policy = nil)
Extracts the
Specificationand raw metadata from the #gem file at path.
Instance Attribute Summary
-
#checksums
readonly
Checksums for the contents of the package.
-
#data_mode
rw
Permission for other files.
-
#dir_mode
rw
Permission for directories.
-
#files
readonly
The files in this package.
-
#gem
readonly
Reference to the gem being packaged.
-
#prog_mode
rw
Permission for program files.
-
#security_policy
rw
The security policy used for verifying the contents of this package.
-
#spec
rw
The spec for this gem.
-
#spec=(value)
rw
Sets the
Specificationto use to build this package. - #build_time rw Internal use only
DefaultUserInteraction - Included
Instance Method Summary
-
#add_checksums(tar)
Adds a checksum for each entry in the gem to checksums.yaml.gz.
-
#build(skip_validation = false, strict_validation = false)
Builds this package based on the specification set by #spec=
-
#contents
A list of file names contained in this gem.
-
#copy_to(path)
Copies this package to path (if possible).
-
#extract_files(destination_dir, pattern = "*")
Extracts the files in this package into
destination_dir -
#gzip_to(io)
Gzips content written to
gz_iotoio. -
#initialize(gem, security_policy) ⇒ Package
constructor
Creates a new package that will read or write to the file #gem.
-
#read_checksums(gem)
Reads and loads checksums.yaml.gz from the tar file #gem
-
#setup_signer(signer_options: {})
Prepares the gem for signing and checksum generation.
-
#verify
Verifies that this gem:
-
#create_symlink(old_name, new_name)
private
Create a symlink and fallback to copy the file or directory on Windows, where symlink creation needs special privileges in form of the Developer Mode.
- #limit_read(io, name, limit) private
-
#verify_entry(entry)
private
Verifies
entryin a #gem file. -
#verify_files(gem)
private
Verifies the files of the #gem
-
#add_contents(tar)
Internal use only
Adds the files listed in the packages's
Specificationto data.tar.gz and adds this file to thetar. -
#add_files(tar)
Internal use only
Adds files included the package's
Specificationto thetarfile. -
#add_metadata(tar)
Internal use only
Adds the package's
Specificationto thetarfile. -
#digest(entry)
Internal use only
Creates a digest of the TarEntry
entryfrom the digest algorithm set by the security policy. -
#extract_tar_gz(io, destination_dir, pattern = "*")
Internal use only
Extracts all the files in the gzipped tar archive
iointodestination_dir. - #file_mode(mode) Internal use only
-
#install_location(filename, destination_dir)
Internal use only
Returns the full path for installing
filename. -
#load_spec_from_metadata(entry)
Internal use only
Loads a
Specificationfrom the TarEntryentry -
#normalize_path(pathname)
Internal use only
See additional method definition at line 532.
-
#open_tar_gz(io)
Internal use only
Opens
ioas a gzipped tar archive. -
#copy_stream(src, dst, size)
private
Internal use only
See additional method definition at line 728.
-
#verify_checksums(digests, checksums)
private
Internal use only
Verifies the #checksums against the
digests.
UserInteraction - Included
| #alert | Displays an alert |
| #alert_error | Displays an error |
| #alert_warning | Displays a warning |
| #ask | Asks a |
| #ask_for_password | Asks for a password with a |
| #ask_yes_no | Asks a yes or no |
| #choose_from_list | Asks the user to answer |
| #say | Displays the given |
| #terminate_interaction | Terminates the RubyGems process with the given |
| #verbose | Calls |
DefaultUserInteraction - Included
Text - Included
| #clean_text | Remove any non-printable characters and make the text suitable for printing. |
| #format_text | Wraps |
| #levenshtein_distance | Returns a value representing the "cost" of transforming str1 into str2 Vendored version of |
| #truncate_text, #min3 | |
Constructor Details
.new(gem, security_policy = nil) ⇒ Package
Creates a new Package for the file at #gem. #gem can also be
provided as an IO object.
If #gem is an existing file in the old format a Package::Old will be
returned.
# File 'lib/rubygems/package.rb', line 149
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 return super unless self == Gem::Package return super unless gem.present? return super unless gem.start return super unless gem.start.include? "MD5SUM =" Gem::Package::Old.new gem end
#initialize(gem, security_policy) ⇒ Package
Creates a new package that will read or write to the file #gem.
# File 'lib/rubygems/package.rb', line 196
def initialize(gem, security_policy) # :notnew: require "zlib" @gem = gem @build_time = Gem.source_date_epoch @checksums = {} @contents = nil @digests = Hash.new {|h, algorithm| h[algorithm] = {} } @files = nil @security_policy = security_policy @signatures = {} @signer = nil @spec = nil end
Class Method Details
.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
[ GitHub ].raw_spec(path, security_policy = nil)
Extracts the Specification and raw metadata from the #gem file at
Gem.path.
# File 'lib/rubygems/package.rb', line 172
def self.raw_spec(path, security_policy = nil) format = new(path, security_policy) spec = format.spec = 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 = entry.read when "metadata.gz" then = Gem::Util.gunzip entry.read end end end [spec, ] end
Instance Attribute Details
#build_time (rw)
# File 'lib/rubygems/package.rb', line 92
attr_accessor :build_time # :nodoc:
#checksums (readonly)
Checksums for the contents of the package
# File 'lib/rubygems/package.rb', line 97
attr_reader :checksums
#data_mode (rw)
Permission for other files
# File 'lib/rubygems/package.rb', line 130
attr_accessor :data_mode
#dir_mode (rw)
Permission for directories
# File 'lib/rubygems/package.rb', line 122
attr_accessor :dir_mode
#files (readonly)
The files in this package. This is not the contents of the gem, just the files in the top-level container.
# File 'lib/rubygems/package.rb', line 103
attr_reader :files
#gem (readonly)
Reference to the gem being packaged.
# File 'lib/rubygems/package.rb', line 108
attr_reader :gem
#prog_mode (rw)
Permission for program files
# File 'lib/rubygems/package.rb', line 126
attr_accessor :prog_mode
#security_policy (rw)
The security policy used for verifying the contents of this package.
# File 'lib/rubygems/package.rb', line 113
attr_accessor :security_policy
#spec (rw)
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.
# File 'lib/rubygems/package.rb', line 620
def spec verify unless @spec @spec end
#spec=(value) (rw)
Sets the Specification to use to build this package.
# File 'lib/rubygems/package.rb', line 118
attr_writer :spec
Instance Method Details
#add_checksums(tar)
Adds a checksum for each entry in the gem to checksums.yaml.gz.
# File 'lib/rubygems/package.rb', line 222
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", 0o444, @signer do |io| gzip_to io do |gz_io| if Gem.use_psych? Psych.dump checksums_by_algorithm, gz_io else gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm) end end end end
#add_contents(tar)
Adds the files listed in the packages's Specification to data.tar.gz
and adds this file to the tar.
#add_files(tar)
Adds files included the package's Specification to the tar file
# File 'lib/rubygems/package.rb', line 263
def add_files(tar) # :nodoc: @spec.files.each do |file| stat = File.lstat file if stat.symlink? tar.add_symlink file, File.readlink(file), stat.mode end next unless stat.file? tar.add_file_simple file, stat.mode, stat.size do |dst_io| File.open file, "rb" do |src_io| copy_stream(src_io, dst_io, stat.size) end end end end
#add_metadata(tar)
Adds the package's Specification to the tar file
# File 'lib/rubygems/package.rb', line 284
def (tar) # :nodoc: 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 end
#build(skip_validation = false, strict_validation = false)
Builds this package based on the specification set by #spec=
# File 'lib/rubygems/package.rb', line 297
def build(skip_validation = false, strict_validation = false) raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation Gem.load_yaml @spec.validate true, strict_validation unless skip_validation setup_signer( signer_options: { expiration_length_days: Gem.configuration.cert_expiration_length_days, } ) @gem.with_write_io do |gem_io| Gem::Package::TarWriter.new gem_io do |gem| gem add_contents gem add_checksums gem end end say <<-EOM Successfully built RubyGem Name: #{@spec.name} Version: #{@spec.version} File: #{File.basename @gem.path} EOM ensure @signer = nil end
#contents
A list of file names contained in this gem
# File 'lib/rubygems/package.rb', line 331
def contents return @contents if @contents verify unless @spec @contents = [] @gem.with_read_io 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 rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e raise Gem::Package::FormatError.new e., @gem end
#copy_stream(src, dst, size) (private)
See additional method definition at line 728.
# File 'lib/rubygems/package.rb', line 732
def copy_stream(src, dst, size) # :nodoc: dst.write src.read(size) end
#copy_to(path)
Copies this package to Gem.path (if possible)
#create_symlink(old_name, new_name) (private)
Create a symlink and fallback to copy the file or directory on Windows, where symlink creation needs special privileges in form of the Developer Mode. JRuby on Windows raises TypeError from the wincode path-conversion helper when it cannot create the symlink, so fall back to copy in that case too.
See additional method definition at line 748.
# File 'lib/rubygems/package.rb', line 755
def create_symlink(old_name, new_name) File.symlink(old_name, new_name) rescue Errno::EACCES, TypeError from = File.(old_name, File.dirname(new_name)) FileUtils.cp_r(from, new_name) end
#digest(entry)
Creates a digest of the TarEntry entry from the digest algorithm set by
the security policy.
# File 'lib/rubygems/package.rb', line 361
def digest(entry) # :nodoc: algorithms = if @checksums @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 return @digests if algorithms.nil? || algorithms.empty? 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 @digests end
#extract_files(destination_dir, pattern = "*")
Extracts the files in this package into destination_dir
If pattern is specified, only entries matching that glob will be
extracted.
# File 'lib/rubygems/package.rb', line 390
def extract_files(destination_dir, pattern = "*") verify unless @spec 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" extract_tar_gz entry, destination_dir, pattern break # ignore further entries end end rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e raise Gem::Package::FormatError.new e., @gem end
#extract_tar_gz(io, destination_dir, pattern = "*")
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.
# File 'lib/rubygems/package.rb', line 421
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| full_name = entry.full_name next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH destination = install_location full_name, destination_dir if entry.symlink? link_target = entry.header.linkname real_destination = link_target.start_with?("/") ? link_target : File.(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 mkdir = if entry.directory? destination else File.dirname destination end unless directories.include?(mkdir) FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?) directories << mkdir end real_mkdir = File.realpath(mkdir) unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/")) raise Gem::Package::PathError.new(real_mkdir, destination_dir) end if entry.file? File.open(destination, "wb") do |out| copy_stream(tar.io, out, entry.size) # Flush needs to happen before chmod because there could be data # in the IO buffer that needs to be written, and that could be # written after the chmod (on close) which would mess up the perms out.flush out.chmod file_mode(entry.header.mode) & ~File.umask end end verbose destination end end symlinks.each do |name, target, destination, real_destination| if File.exist?(real_destination) create_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
#file_mode(mode)
# File 'lib/rubygems/package.rb', line 489
def file_mode(mode) # :nodoc: ((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
#gzip_to(io)
Gzips content written to gz_io to io.
# File 'lib/rubygems/package.rb', line 504
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
#install_location(filename, destination_dir)
Returns the full path for installing filename.
If filename is not inside destination_dir an exception is raised.
# File 'lib/rubygems/package.rb', line 518
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) destination = File.(filename, destination_dir) raise Gem::Package::PathError.new(destination, destination_dir) unless normalize_path(destination).start_with? normalize_path(destination_dir + "/") destination end
#limit_read(io, name, limit) (private)
# File 'lib/rubygems/package.rb', line 737
def limit_read(io, name, limit) bytes = io.read(limit + 1) raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit bytes end
#load_spec_from_metadata(entry)
Loads a Specification from the TarEntry entry
# File 'lib/rubygems/package.rb', line 544
def (entry) # :nodoc: limit = 10 * 1024 * 1024 case entry.full_name when "metadata" then @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit) when "metadata.gz" then Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit) end end end
#normalize_path(pathname)
See additional method definition at line 532.
# File 'lib/rubygems/package.rb', line 536
def normalize_path(pathname) # :nodoc: pathname.downcase end
#open_tar_gz(io)
Opens io as a gzipped tar archive
# File 'lib/rubygems/package.rb', line 559
def open_tar_gz(io) # :nodoc: Zlib::GzipReader.wrap io do |gzio| tar = Gem::Package::TarReader.new gzio yield tar ensure # Consume remaining gzip data to prevent the # "attempt to close unfinished zstream; reset forced" warning # when the GzipReader is closed with unconsumed compressed data. begin IO.copy_stream(gzio, IO::NULL) rescue Zlib::GzipFile::Error, IOError nil end end end
#read_checksums(gem)
Reads and loads checksums.yaml.gz from the tar file #gem
#setup_signer(signer_options: {})
Prepares the gem for signing and checksum generation. If a signing certificate and key are not present only checksum generation is set up.
# File 'lib/rubygems/package.rb', line 593
def setup_signer(signer_options: {}) passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] if @spec.signing_key @signer = Gem::Security::Signer.new( @spec.signing_key, @spec.cert_chain, passphrase, ) @spec.signing_key = nil @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(&:to_pem) if @signer.cert_chain end end
#verify
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
# File 'lib/rubygems/package.rb', line 636
def verify @files = [] @spec = nil @gem.with_read_io 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 true rescue Gem::Security::Exception @spec = nil @files = [] raise rescue Errno::ENOENT => e raise Gem::Package::FormatError.new e. rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e raise Gem::Package::FormatError.new e., @gem end
#verify_checksums(digests, checksums) (private)
Verifies the #checksums against the digests. This check is not
cryptographically secure. Missing checksums are ignored.
# File 'lib/rubygems/package.rb', line 669
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 raise Gem::Package::FormatError.new \ "#{algorithm} checksum mismatch for #{file_name}", @gem end end end end
#verify_entry(entry) (private)
Verifies entry in a #gem file.
# File 'lib/rubygems/package.rb', line 687
def verify_entry(entry) file_name = entry.full_name @files << file_name case file_name when /\.sig$/ then @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy return else digest entry end entry rescue StandardError warn "Exception while verifying #{@gem.path}" raise end
#verify_files(gem) (private)
Verifies the files of the #gem
# File 'lib/rubygems/package.rb', line 708
def verify_files(gem) gem.each do |entry| verify_entry entry end unless @spec raise Gem::Package::FormatError.new "package metadata is missing", @gem end unless @files.include? "data.tar.gz" raise Gem::Package::FormatError.new \ "package content (data.tar.gz) is missing", @gem end 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