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.rb793
1 files changed, 17 insertions, 776 deletions
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
index f15e4feecb..9cb393b0c7 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -45,768 +45,15 @@ module Gem::Package
class TooLongFileName < Error; end
class FormatError < Error; end
- module FSyncDir
- private
- def fsync_dir(dirname)
- # make sure this hits the disc
- begin
- dir = open(dirname, "r")
- dir.fsync
- rescue # ignore IOError if it's an unpatched (old) Ruby
- ensure
- dir.close if dir rescue nil
- end
- end
- end
-
- class TarHeader
- FIELDS = [:name, :mode, :uid, :gid, :size, :mtime, :checksum, :typeflag,
- :linkname, :magic, :version, :uname, :gname, :devmajor,
- :devminor, :prefix]
- FIELDS.each {|x| attr_reader x}
-
- def self.new_from_stream(stream)
- data = stream.read(512)
- fields = data.unpack("A100" + # record name
- "A8A8A8" + # mode, uid, gid
- "A12A12" + # size, mtime
- "A8A" + # checksum, typeflag
- "A100" + # linkname
- "A6A2" + # magic, version
- "A32" + # uname
- "A32" + # gname
- "A8A8" + # devmajor, devminor
- "A155") # prefix
- name = fields.shift
- mode = fields.shift.oct
- uid = fields.shift.oct
- gid = fields.shift.oct
- size = fields.shift.oct
- mtime = fields.shift.oct
- checksum = fields.shift.oct
- typeflag = fields.shift
- linkname = fields.shift
- magic = fields.shift
- version = fields.shift.oct
- uname = fields.shift
- gname = fields.shift
- devmajor = fields.shift.oct
- devminor = fields.shift.oct
- prefix = fields.shift
-
- empty = (data == "\0" * 512)
-
- new(:name=>name, :mode=>mode, :uid=>uid, :gid=>gid, :size=>size,
- :mtime=>mtime, :checksum=>checksum, :typeflag=>typeflag,
- :magic=>magic, :version=>version, :uname=>uname, :gname=>gname,
- :devmajor=>devmajor, :devminor=>devminor, :prefix=>prefix,
- :empty => empty )
- end
-
- def initialize(vals)
- unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
- raise ArgumentError, ":name, :size, :prefix and :mode required"
- end
- vals[:uid] ||= 0
- vals[:gid] ||= 0
- vals[:mtime] ||= 0
- vals[:checksum] ||= ""
- vals[:typeflag] ||= "0"
- vals[:magic] ||= "ustar"
- vals[:version] ||= "00"
- vals[:uname] ||= "wheel"
- vals[:gname] ||= "wheel"
- vals[:devmajor] ||= 0
- vals[:devminor] ||= 0
- FIELDS.each {|x| instance_variable_set "@#{x.to_s}", vals[x]}
- @empty = vals[:empty]
- end
-
- def empty?
- @empty
- end
-
- def to_s
- update_checksum
- header(checksum)
- end
-
- def update_checksum
- h = header(" " * 8)
- @checksum = oct(calculate_checksum(h), 6)
- end
-
- private
- def oct(num, len)
- "%0#{len}o" % num
- end
-
- def calculate_checksum(hdr)
- #hdr.split('').map { |c| c[0] }.inject { |a, b| a + b } # HACK rubinius
- hdr.unpack("C*").inject{|a,b| a+b}
- end
-
- def header(chksum)
- # struct tarfile_entry_posix {
- # char name[100]; # ASCII + (Z unless filled)
- # char mode[8]; # 0 padded, octal, null
- # char uid[8]; # ditto
- # char gid[8]; # ditto
- # char size[12]; # 0 padded, octal, null
- # char mtime[12]; # 0 padded, octal, null
- # char checksum[8]; # 0 padded, octal, null, space
- # char typeflag[1]; # file: "0" dir: "5"
- # char linkname[100]; # ASCII + (Z unless filled)
- # char magic[6]; # "ustar\0"
- # char version[2]; # "00"
- # char uname[32]; # ASCIIZ
- # char gname[32]; # ASCIIZ
- # char devmajor[8]; # 0 padded, octal, null
- # char devminor[8]; # o padded, octal, null
- # char prefix[155]; # ASCII + (Z unless filled)
- # };
- arr = [name, oct(mode, 7), oct(uid, 7), oct(gid, 7), oct(size, 11),
- oct(mtime, 11), chksum, " ", typeflag, linkname, magic, version,
- uname, gname, oct(devmajor, 7), oct(devminor, 7), prefix]
- str = arr.pack("a100a8a8a8a12a12" + # name, mode, uid, gid, size, mtime
- "a7aaa100a6a2" + # chksum, typeflag, linkname, magic, version
- "a32a32a8a8a155") # uname, gname, devmajor, devminor, prefix
- str + "\0" * ((512 - str.size) % 512)
- end
- end
-
- class TarWriter
- class FileOverflow < StandardError; end
- class BlockNeeded < StandardError; end
-
- class BoundedStream
- attr_reader :limit, :written
- def initialize(io, limit)
- @io = io
- @limit = limit
- @written = 0
- end
-
- def write(data)
- if data.size + @written > @limit
- raise FileOverflow,
- "You tried to feed more data than fits in the file."
- end
- @io.write data
- @written += data.size
- data.size
- end
- end
-
- class RestrictedStream
- def initialize(anIO)
- @io = anIO
- end
-
- def write(data)
- @io.write data
- end
- end
-
- def self.new(anIO)
- writer = super(anIO)
- return writer unless block_given?
- begin
- yield writer
- ensure
- writer.close
- end
- nil
- end
-
- def initialize(anIO)
- @io = anIO
- @closed = false
- end
-
- def add_file_simple(name, mode, size)
- raise BlockNeeded unless block_given?
- raise ClosedIO if @closed
- name, prefix = split_name(name)
- header = TarHeader.new(:name => name, :mode => mode,
- :size => size, :prefix => prefix).to_s
- @io.write header
- os = BoundedStream.new(@io, size)
- yield os
- #FIXME: what if an exception is raised in the block?
- min_padding = size - os.written
- @io.write("\0" * min_padding)
- remainder = (512 - (size % 512)) % 512
- @io.write("\0" * remainder)
- end
-
- def add_file(name, mode)
- raise BlockNeeded unless block_given?
- raise ClosedIO if @closed
- raise NonSeekableIO unless @io.respond_to? :pos=
- name, prefix = split_name(name)
- init_pos = @io.pos
- @io.write "\0" * 512 # placeholder for the header
- yield RestrictedStream.new(@io)
- #FIXME: what if an exception is raised in the block?
- #FIXME: what if an exception is raised in the block?
- size = @io.pos - init_pos - 512
- remainder = (512 - (size % 512)) % 512
- @io.write("\0" * remainder)
- final_pos = @io.pos
- @io.pos = init_pos
- header = TarHeader.new(:name => name, :mode => mode,
- :size => size, :prefix => prefix).to_s
- @io.write header
- @io.pos = final_pos
- end
-
- def mkdir(name, mode)
- raise ClosedIO if @closed
- name, prefix = split_name(name)
- header = TarHeader.new(:name => name, :mode => mode, :typeflag => "5",
- :size => 0, :prefix => prefix).to_s
- @io.write header
- nil
- end
-
- def flush
- raise ClosedIO if @closed
- @io.flush if @io.respond_to? :flush
- end
-
- def close
- #raise ClosedIO if @closed
- return if @closed
- @io.write "\0" * 1024
- @closed = true
- end
-
- private
- def split_name name
- raise TooLongFileName if name.size > 256
- if name.size <= 100
- prefix = ""
- else
- parts = name.split(/\//)
- newname = parts.pop
- nxt = ""
- loop do
- nxt = parts.pop
- break if newname.size + 1 + nxt.size > 100
- newname = nxt + "/" + newname
- end
- prefix = (parts + [nxt]).join "/"
- name = newname
- raise TooLongFileName if name.size > 100 || prefix.size > 155
- end
- return name, prefix
- end
- end
-
- class TarReader
-
- include Gem::Package
-
- class UnexpectedEOF < StandardError; end
-
- module InvalidEntry
- def read(len=nil); raise ClosedIO; end
- def getc; raise ClosedIO; end
- def rewind; raise ClosedIO; end
- end
-
- class Entry
- TarHeader::FIELDS.each{|x| attr_reader x}
-
- def initialize(header, anIO)
- @io = anIO
- @name = header.name
- @mode = header.mode
- @uid = header.uid
- @gid = header.gid
- @size = header.size
- @mtime = header.mtime
- @checksum = header.checksum
- @typeflag = header.typeflag
- @linkname = header.linkname
- @magic = header.magic
- @version = header.version
- @uname = header.uname
- @gname = header.gname
- @devmajor = header.devmajor
- @devminor = header.devminor
- @prefix = header.prefix
- @read = 0
- @orig_pos = @io.pos
- end
-
- def read(len = nil)
- return nil if @read >= @size
- len ||= @size - @read
- max_read = [len, @size - @read].min
- ret = @io.read(max_read)
- @read += ret.size
- ret
- end
-
- def getc
- return nil if @read >= @size
- ret = @io.getc
- @read += 1 if ret
- ret
- end
-
- def is_directory?
- @typeflag == "5"
- end
-
- def is_file?
- @typeflag == "0"
- end
-
- def eof?
- @read >= @size
- end
-
- def pos
- @read
- end
-
- def rewind
- raise NonSeekableIO unless @io.respond_to? :pos=
- @io.pos = @orig_pos
- @read = 0
- end
-
- alias_method :is_directory, :is_directory?
- alias_method :is_file, :is_file?
-
- def bytes_read
- @read
- end
-
- def full_name
- if @prefix != ""
- File.join(@prefix, @name)
- else
- @name
- end
- end
-
- def close
- invalidate
- end
-
- private
- def invalidate
- extend InvalidEntry
- end
- end
-
- def self.new(anIO)
- reader = super(anIO)
- return reader unless block_given?
- begin
- yield reader
- ensure
- reader.close
- end
- nil
- end
-
- def initialize(anIO)
- @io = anIO
- @init_pos = anIO.pos
- end
-
- def each(&block)
- each_entry(&block)
- end
-
- # do not call this during a #each or #each_entry iteration
- def rewind
- if @init_pos == 0
- raise NonSeekableIO unless @io.respond_to? :rewind
- @io.rewind
- else
- raise NonSeekableIO unless @io.respond_to? :pos=
- @io.pos = @init_pos
- end
- end
-
- def each_entry
- loop do
- return if @io.eof?
- header = TarHeader.new_from_stream(@io)
- return if header.empty?
- entry = Entry.new header, @io
- size = entry.size
- yield entry
- skip = (512 - (size % 512)) % 512
- if @io.respond_to? :seek
- # avoid reading...
- @io.seek(size - entry.bytes_read, IO::SEEK_CUR)
- else
- pending = size - entry.bytes_read
- while pending > 0
- bread = @io.read([pending, 4096].min).size
- raise UnexpectedEOF if @io.eof?
- pending -= bread
- end
- end
- @io.read(skip) # discard trailing zeros
- # make sure nobody can use #read, #getc or #rewind anymore
- entry.close
- end
- end
-
- def close
- end
-
- end
-
- class TarInput
-
- include FSyncDir
- include Enumerable
-
- attr_reader :metadata
-
- class << self; private :new end
-
- def initialize(io, security_policy = nil)
- @io = io
- @tarreader = TarReader.new(@io)
- has_meta = false
- data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil
- dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil
-
- @tarreader.each do |entry|
- case entry.full_name
- when "metadata"
- @metadata = load_gemspec(entry.read)
- has_meta = true
- break
- when "metadata.gz"
- begin
- # if we have a security_policy, then pre-read the metadata file
- # and calculate it's digest
- sio = nil
- if security_policy
- Gem.ensure_ssl_available
- sio = StringIO.new(entry.read)
- meta_dgst = dgst_algo.digest(sio.string)
- sio.rewind
- end
-
- gzis = Zlib::GzipReader.new(sio || entry)
- # YAML wants an instance of IO
- @metadata = load_gemspec(gzis)
- has_meta = true
- ensure
- gzis.close unless gzis.nil?
- end
- when 'metadata.gz.sig'
- meta_sig = entry.read
- when 'data.tar.gz.sig'
- data_sig = entry.read
- when 'data.tar.gz'
- if security_policy
- Gem.ensure_ssl_available
- data_dgst = dgst_algo.digest(entry.read)
- end
- end
- end
-
- if security_policy then
- Gem.ensure_ssl_available
-
- # map trust policy from string to actual class (or a serialized YAML
- # file, if that exists)
- if String === security_policy then
- if Gem::Security::Policy.key? security_policy then
- # load one of the pre-defined security policies
- security_policy = Gem::Security::Policy[security_policy]
- elsif File.exist? security_policy then
- # FIXME: this doesn't work yet
- security_policy = YAML.load File.read(security_policy)
- else
- raise Gem::Exception, "Unknown trust policy '#{security_policy}'"
- end
- end
-
- if data_sig && data_dgst && meta_sig && meta_dgst then
- # the user has a trust policy, and we have a signed gem
- # file, so use the trust policy to verify the gem signature
-
- begin
- security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify data signature: #{e}"
- end
-
- begin
- security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify metadata signature: #{e}"
- end
- elsif security_policy.only_signed
- raise Gem::Exception, "Unsigned gem"
- else
- # FIXME: should display warning here (trust policy, but
- # either unsigned or badly signed gem file)
- end
- end
-
- @tarreader.rewind
- @fileops = Gem::FileOperations.new
- raise FormatError, "No metadata found!" unless has_meta
- end
-
- # Attempt to YAML-load a gemspec from the given _io_ parameter. Return
- # nil if it fails.
- def load_gemspec(io)
- Gem::Specification.from_yaml(io)
- rescue Gem::Exception
- nil
- end
-
- def self.open(filename, security_policy = nil, &block)
- open_from_io(File.open(filename, "rb"), security_policy, &block)
- end
-
- def self.open_from_io(io, security_policy = nil, &block)
- raise "Want a block" unless block_given?
- begin
- is = new(io, security_policy)
- yield is
- ensure
- is.close if is
- end
- end
-
- def each(&block)
- @tarreader.each do |entry|
- next unless entry.full_name == "data.tar.gz"
- is = zipped_stream(entry)
- begin
- TarReader.new(is) do |inner|
- inner.each(&block)
- end
- ensure
- is.close if is
- end
- end
- @tarreader.rewind
- end
-
- # Return an IO stream for the zipped entry.
- #
- # NOTE: Originally this method used two approaches, Return a GZipReader
- # directly, or read the GZipReader into a string and return a StringIO on
- # the string. The string IO approach was used for versions of ZLib before
- # 1.2.1 to avoid buffer errors on windows machines. Then we found that
- # errors happened with 1.2.1 as well, so we changed the condition. Then
- # we discovered errors occurred with versions as late as 1.2.3. At this
- # point (after some benchmarking to show we weren't seriously crippling
- # the unpacking speed) we threw our hands in the air and declared that
- # this method would use the String IO approach on all platforms at all
- # times. And that's the way it is.
- def zipped_stream(entry)
- if defined? Rubinius then
- zis = Zlib::GzipReader.new entry
- dis = zis.read
- is = StringIO.new(dis)
- else
- # This is Jamis Buck's ZLib workaround for some unknown issue
- entry.read(10) # skip the gzip header
- zis = Zlib::Inflate.new(-Zlib::MAX_WBITS)
- is = StringIO.new(zis.inflate(entry.read))
- end
- ensure
- zis.finish if zis
- end
-
- def extract_entry(destdir, entry, expected_md5sum = nil)
- if entry.is_directory?
- dest = File.join(destdir, entry.full_name)
- if file_class.dir? dest
- @fileops.chmod entry.mode, dest, :verbose=>false
- else
- @fileops.mkdir_p(dest, :mode => entry.mode, :verbose=>false)
- end
- fsync_dir dest
- fsync_dir File.join(dest, "..")
- return
- end
- # it's a file
- md5 = Digest::MD5.new if expected_md5sum
- destdir = File.join(destdir, File.dirname(entry.full_name))
- @fileops.mkdir_p(destdir, :mode => 0755, :verbose=>false)
- destfile = File.join(destdir, File.basename(entry.full_name))
- @fileops.chmod(0600, destfile, :verbose=>false) rescue nil # Errno::ENOENT
- file_class.open(destfile, "wb", entry.mode) do |os|
- loop do
- data = entry.read(4096)
- break unless data
- md5 << data if expected_md5sum
- os.write(data)
- end
- os.fsync
- end
- @fileops.chmod(entry.mode, destfile, :verbose=>false)
- fsync_dir File.dirname(destfile)
- fsync_dir File.join(File.dirname(destfile), "..")
- if expected_md5sum && expected_md5sum != md5.hexdigest
- raise BadCheckSum
- end
- end
-
- def close
- @io.close
- @tarreader.close
- end
-
- private
-
- def file_class
- File
- end
- end
-
- class TarOutput
-
- class << self; private :new end
-
- def initialize(io)
- @io = io
- @external = TarWriter.new @io
- end
-
- def external_handle
- @external
- end
-
- def self.open(filename, signer = nil, &block)
- io = File.open(filename, "wb")
- open_from_io(io, signer, &block)
- nil
- end
-
- def self.open_from_io(io, signer = nil, &block)
- outputter = new(io)
- metadata = nil
- set_meta = lambda{|x| metadata = x}
- raise "Want a block" unless block_given?
- begin
- data_sig, meta_sig = nil, nil
-
- outputter.external_handle.add_file("data.tar.gz", 0644) do |inner|
- begin
- sio = signer ? StringIO.new : nil
- os = Zlib::GzipWriter.new(sio || inner)
-
- TarWriter.new(os) do |inner_tar_stream|
- klass = class << inner_tar_stream; self end
- klass.send(:define_method, :metadata=, &set_meta)
- block.call inner_tar_stream
- end
- ensure
- os.flush
- os.finish
- #os.close
-
- # if we have a signing key, then sign the data
- # digest and return the signature
- data_sig = nil
- if signer
- dgst_algo = Gem::Security::OPT[:dgst_algo]
- dig = dgst_algo.digest(sio.string)
- data_sig = signer.sign(dig)
- inner.write(sio.string)
- end
- end
- end
-
- # if we have a data signature, then write it to the gem too
- if data_sig
- sig_file = 'data.tar.gz.sig'
- outputter.external_handle.add_file(sig_file, 0644) do |os|
- os.write(data_sig)
- end
- end
-
- outputter.external_handle.add_file("metadata.gz", 0644) do |os|
- begin
- sio = signer ? StringIO.new : nil
- gzos = Zlib::GzipWriter.new(sio || os)
- gzos.write metadata
- ensure
- gzos.flush
- gzos.finish
-
- # if we have a signing key, then sign the metadata
- # digest and return the signature
- if signer
- dgst_algo = Gem::Security::OPT[:dgst_algo]
- dig = dgst_algo.digest(sio.string)
- meta_sig = signer.sign(dig)
- os.write(sio.string)
- end
- end
- end
-
- # if we have a metadata signature, then write to the gem as
- # well
- if meta_sig
- sig_file = 'metadata.gz.sig'
- outputter.external_handle.add_file(sig_file, 0644) do |os|
- os.write(meta_sig)
- end
- end
-
- ensure
- outputter.close
- end
- nil
- end
-
- def close
- @external.close
- @io.close
- end
-
- end
-
- #FIXME: refactor the following 2 methods
-
- def self.open(dest, mode = "r", signer = nil, &block)
- raise "Block needed" unless block_given?
-
- case mode
- when "r"
- security_policy = signer
- TarInput.open(dest, security_policy, &block)
- when "w"
- TarOutput.open(dest, signer, &block)
- else
- raise "Unknown Package open mode"
- end
- end
-
- def self.open_from_io(io, mode = "r", signer = nil, &block)
- raise "Block needed" unless block_given?
-
- case mode
- when "r"
- security_policy = signer
- TarInput.open_from_io(io, security_policy, &block)
- when "w"
- TarOutput.open_from_io(io, signer, &block)
- else
- raise "Unknown Package open mode"
- end
+ def self.open(io, mode = "r", signer = nil, &block)
+ tar_type = case mode
+ when 'r' then TarInput
+ when 'w' then TarOutput
+ else
+ raise "Unknown Package open mode"
+ end
+
+ tar_type.open(io, signer, &block)
end
def self.pack(src, destname, signer = nil)
@@ -836,19 +83,13 @@ module Gem::Package
end
end
- class << self
- def file_class
- File
- end
-
- def dir_class
- Dir
- end
-
- def find_class # HACK kill me
- Find
- end
- end
-
end
+require 'rubygems/package/f_sync_dir'
+require 'rubygems/package/tar_header'
+require 'rubygems/package/tar_input'
+require 'rubygems/package/tar_output'
+require 'rubygems/package/tar_reader'
+require 'rubygems/package/tar_reader/entry'
+require 'rubygems/package/tar_writer'
+