Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

470 lines (364 sloc) 11.313 kb
# -*- coding: utf-8 -*-
#--
# 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(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
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
##
# 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
@contents = nil
@digest = Gem::Security::DIGEST_ALGORITHM
@files = nil
@security_policy = nil
@spec = nil
@signer = nil
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:
tar.add_file_signed 'data.tar.gz', 0444, @signer do |io|
Zlib::GzipWriter.wrap io do |gz_io|
Gem::Package::TarWriter.new gz_io do |data_tar|
add_files data_tar
end
end
end
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
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:
metadata = @spec.to_yaml
metadata_gz = Gem.gzip metadata
tar.add_file_signed 'metadata.gz', 0444, @signer do |io|
io.write metadata_gz
end
end
##
# Builds this package based on the specification set by #spec=
def build(skip_validation=false)
require 'rubygems/security'
@spec.validate unless skip_validation
@spec.mark_version
if @spec.signing_key then
@signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain
@spec.signing_key = nil
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_s }
else
@signer = Gem::Security::Signer.new nil, nil
@spec.cert_chain = @signer.cert_chain.map { |cert| cert.to_pem } if
@signer.cert_chain
end
open @gem, 'wb' do |gem_io|
Gem::Package::TarWriter.new gem_io do |gem|
add_metadata gem
add_contents 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:
digester = @digest.new
digester << entry.read(16384) until entry.eof?
entry.rewind
digester
end
##
# Extracts the files in this package into +destination_dir+
def extract_files destination_dir
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
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.
def extract_tar_gz io, destination_dir # :nodoc:
open_tar_gz io do |tar|
tar.each do |entry|
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
out.fsync rescue nil # for filesystems without fsync(2)
end
say destination if Gem.configuration.really_verbose
end
end
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 = 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.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
##
# 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
digests = {}
signatures = {}
checksums = {}
open @gem, 'rb' do |io|
reader = Gem::Package::TarReader.new io
reader.each do |entry|
file_name = entry.full_name
@files << file_name
case file_name
when /\.sig$/ then
signatures[$`] = entry.read if @security_policy
next
when /\.sum$/ then
checksums[$`] = entry.read
next
else
digests[file_name] = digest entry
end
case file_name
when /^metadata(.gz)?$/ then
load_spec entry
when 'data.tar.gz' then
verify_gz entry
end
end
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
verify_checksums digests, checksums
@security_policy.verify_signatures @spec, digests, signatures if
@security_policy
true
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 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
##
# Verifies the +checksums+ against the +digests+. This check is not
# cryptographically secure. Missing checksums are ignored.
def verify_checksums digests, checksums # :nodoc:
checksums.sort.each do |name, checksum|
digest = digests[name]
checksum =~ /#{digest.name}\t(.*)/
unless digest.hexdigest == $1 then
raise Gem::Package::FormatError.new("checksum mismatch for #{name}",
@gem)
end
end
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'
Jump to Line
Something went wrong with that request. Please try again.