diff options
Diffstat (limited to 'jni/ruby/lib/rubygems/package')
-rw-r--r-- | jni/ruby/lib/rubygems/package/digest_io.rb | 64 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/file_source.rb | 33 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/io_source.rb | 45 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/old.rb | 178 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/source.rb | 3 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/tar_header.rb | 229 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/tar_reader.rb | 123 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/tar_reader/entry.rb | 147 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/tar_test_case.rb | 137 | ||||
-rw-r--r-- | jni/ruby/lib/rubygems/package/tar_writer.rb | 326 |
10 files changed, 1285 insertions, 0 deletions
diff --git a/jni/ruby/lib/rubygems/package/digest_io.rb b/jni/ruby/lib/rubygems/package/digest_io.rb new file mode 100644 index 0000000..f8bde0f --- /dev/null +++ b/jni/ruby/lib/rubygems/package/digest_io.rb @@ -0,0 +1,64 @@ +## +# IO wrapper that creates digests of contents written to the IO it wraps. + +class Gem::Package::DigestIO + + ## + # Collected digests for wrapped writes. + # + # { + # 'SHA1' => #<OpenSSL::Digest: [...]>, + # 'SHA512' => #<OpenSSL::Digest: [...]>, + # } + + attr_reader :digests + + ## + # Wraps +io+ and updates digest for each of the digest algorithms in + # the +digests+ Hash. Returns the digests hash. Example: + # + # io = StringIO.new + # digests = { + # 'SHA1' => OpenSSL::Digest.new('SHA1'), + # 'SHA512' => OpenSSL::Digest.new('SHA512'), + # } + # + # Gem::Package::DigestIO.wrap io, digests do |digest_io| + # digest_io.write "hello" + # end + # + # digests['SHA1'].hexdigest #=> "aaf4c61d[...]" + # digests['SHA512'].hexdigest #=> "9b71d224[...]" + + def self.wrap io, digests + digest_io = new io, digests + + yield digest_io + + return digests + end + + ## + # Creates a new DigestIO instance. Using ::wrap is recommended, see the + # ::wrap documentation for documentation of +io+ and +digests+. + + def initialize io, digests + @io = io + @digests = digests + end + + ## + # Writes +data+ to the underlying IO and updates the digests + + def write data + result = @io.write data + + @digests.each do |_, digest| + digest << data + end + + result + end + +end + diff --git a/jni/ruby/lib/rubygems/package/file_source.rb b/jni/ruby/lib/rubygems/package/file_source.rb new file mode 100644 index 0000000..85316f6 --- /dev/null +++ b/jni/ruby/lib/rubygems/package/file_source.rb @@ -0,0 +1,33 @@ +## +# The primary source of gems is a file on disk, including all usages +# internal to rubygems. +# +# This is a private class, do not depend on it directly. Instead, pass a path +# object to `Gem::Package.new`. + +class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all + + attr_reader :path + + def initialize path + @path = path + end + + def start + @start ||= File.read path, 20 + end + + def present? + File.exist? path + end + + def with_write_io &block + open path, 'wb', &block + end + + def with_read_io &block + open path, 'rb', &block + end + +end + diff --git a/jni/ruby/lib/rubygems/package/io_source.rb b/jni/ruby/lib/rubygems/package/io_source.rb new file mode 100644 index 0000000..f89593d --- /dev/null +++ b/jni/ruby/lib/rubygems/package/io_source.rb @@ -0,0 +1,45 @@ +## +# Supports reading and writing gems from/to a generic IO object. This is +# useful for other applications built on top of rubygems, such as +# rubygems.org. +# +# This is a private class, do not depend on it directly. Instead, pass an IO +# object to `Gem::Package.new`. + +class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all + + attr_reader :io + + def initialize io + @io = io + end + + def start + @start ||= begin + if io.pos > 0 + raise Gem::Package::Error, "Cannot read start unless IO is at start" + end + + value = io.read 20 + io.rewind + value + end + end + + def present? + true + end + + def with_read_io + yield io + end + + def with_write_io + yield io + end + + def path + end + +end + diff --git a/jni/ruby/lib/rubygems/package/old.rb b/jni/ruby/lib/rubygems/package/old.rb new file mode 100644 index 0000000..d7b228d --- /dev/null +++ b/jni/ruby/lib/rubygems/package/old.rb @@ -0,0 +1,178 @@ +#-- +# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. +# All rights reserved. +# See LICENSE.txt for permissions. +#++ + +## +# The format class knows the guts of the ancient .gem file format and provides +# the capability to read such ancient gems. +# +# Please pretend this doesn't exist. + +class Gem::Package::Old < Gem::Package + + undef_method :spec= + + ## + # Creates a new old-format package reader for +gem+. Old-format packages + # cannot be written. + + def initialize gem + require 'fileutils' + require 'zlib' + Gem.load_yaml + + @contents = nil + @gem = gem + @security_policy = nil + @spec = nil + end + + ## + # A list of file names contained in this gem + + def contents + verify + + return @contents if @contents + + @gem.with_read_io do |io| + read_until_dashes io # spec + header = file_list io + + @contents = header.map { |file| file['path'] } + end + end + + ## + # Extracts the files in this package into +destination_dir+ + + def extract_files destination_dir + verify + + errstr = "Error reading files from gem" + + @gem.with_read_io do |io| + read_until_dashes io # spec + header = file_list io + raise Gem::Exception, errstr unless header + + header.each do |entry| + full_name = entry['path'] + + destination = install_location full_name, destination_dir + + file_data = '' + + read_until_dashes io do |line| + file_data << line + end + + file_data = file_data.strip.unpack("m")[0] + file_data = Zlib::Inflate.inflate file_data + + raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if + file_data.length != entry['size'].to_i + + FileUtils.rm_rf destination + + FileUtils.mkdir_p File.dirname destination + + open destination, 'wb', entry['mode'] do |out| + out.write file_data + end + + verbose destination + end + end + rescue Zlib::DataError + raise Gem::Exception, errstr + end + + ## + # Reads the file list section from the old-format gem +io+ + + def file_list io # :nodoc: + header = '' + + read_until_dashes io do |line| + header << line + end + + YAML.load header + end + + ## + # Reads lines until a "---" separator is found + + def read_until_dashes io # :nodoc: + while (line = io.gets) && line.chomp.strip != "---" do + yield line if block_given? + end + end + + ## + # Skips the Ruby self-install header in +io+. + + def skip_ruby io # :nodoc: + loop do + line = io.gets + + return if line.chomp == '__END__' + break unless line + end + + raise Gem::Exception, "Failed to find end of ruby script while reading gem" + end + + ## + # The specification for this gem + + def spec + verify + + return @spec if @spec + + yaml = '' + + @gem.with_read_io do |io| + skip_ruby io + read_until_dashes io do |line| + yaml << line + end + end + + yaml_error = if RUBY_VERSION < '1.9' then + YAML::ParseError + elsif YAML.const_defined?(:ENGINE) && YAML::ENGINE.yamler == 'syck' then + YAML::ParseError + else + YAML::SyntaxError + end + + begin + @spec = Gem::Specification.from_yaml yaml + rescue yaml_error + raise Gem::Exception, "Failed to parse gem specification out of gem file" + end + rescue ArgumentError + raise Gem::Exception, "Failed to parse gem specification out of gem file" + end + + ## + # Raises an exception if a security policy that verifies data is active. + # Old format gems cannot be verified as signed. + + def verify + return true unless @security_policy + + raise Gem::Security::Exception, + 'old format gems do not contain signatures and cannot be verified' if + @security_policy.verify_data + + true + end + +end + diff --git a/jni/ruby/lib/rubygems/package/source.rb b/jni/ruby/lib/rubygems/package/source.rb new file mode 100644 index 0000000..1f18d47 --- /dev/null +++ b/jni/ruby/lib/rubygems/package/source.rb @@ -0,0 +1,3 @@ +class Gem::Package::Source # :nodoc: +end + diff --git a/jni/ruby/lib/rubygems/package/tar_header.rb b/jni/ruby/lib/rubygems/package/tar_header.rb new file mode 100644 index 0000000..f9ab13a --- /dev/null +++ b/jni/ruby/lib/rubygems/package/tar_header.rb @@ -0,0 +1,229 @@ +# -*- coding: utf-8 -*- +#-- +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#++ + +## +#-- +# 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) +# }; +#++ +# A header for a tar file + +class Gem::Package::TarHeader + + ## + # Fields in the tar header + + FIELDS = [ + :checksum, + :devmajor, + :devminor, + :gid, + :gname, + :linkname, + :magic, + :mode, + :mtime, + :name, + :prefix, + :size, + :typeflag, + :uid, + :uname, + :version, + ] + + ## + # Pack format for a tar header + + PACK_FORMAT = 'a100' + # name + 'a8' + # mode + 'a8' + # uid + 'a8' + # gid + 'a12' + # size + 'a12' + # mtime + 'a7a' + # chksum + 'a' + # typeflag + 'a100' + # linkname + 'a6' + # magic + 'a2' + # version + 'a32' + # uname + 'a32' + # gname + 'a8' + # devmajor + 'a8' + # devminor + 'a155' # prefix + + ## + # Unpack format for a tar header + + UNPACK_FORMAT = 'A100' + # name + 'A8' + # mode + 'A8' + # uid + 'A8' + # gid + 'A12' + # size + 'A12' + # mtime + 'A8' + # checksum + 'A' + # typeflag + 'A100' + # linkname + 'A6' + # magic + 'A2' + # version + 'A32' + # uname + 'A32' + # gname + 'A8' + # devmajor + 'A8' + # devminor + 'A155' # prefix + + attr_reader(*FIELDS) + + ## + # Creates a tar header from IO +stream+ + + def self.from(stream) + header = stream.read 512 + empty = (header == "\0" * 512) + + fields = header.unpack UNPACK_FORMAT + + new :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 => empty + end + + ## + # Creates a new TarHeader using +vals+ + + def initialize(vals) + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] then + raise ArgumentError, ":name, :size, :prefix and :mode required" + end + + vals[:uid] ||= 0 + vals[:gid] ||= 0 + vals[:mtime] ||= 0 + vals[:checksum] ||= "" + vals[:typeflag] = "0" if vals[:typeflag].nil? || vals[:typeflag].empty? + vals[:magic] ||= "ustar" + vals[:version] ||= "00" + vals[:uname] ||= "wheel" + vals[:gname] ||= "wheel" + vals[:devmajor] ||= 0 + vals[:devminor] ||= 0 + + FIELDS.each do |name| + instance_variable_set "@#{name}", vals[name] + end + + @empty = vals[:empty] + end + + ## + # Is the tar entry empty? + + def empty? + @empty + end + + def ==(other) # :nodoc: + self.class === other and + @checksum == other.checksum and + @devmajor == other.devmajor and + @devminor == other.devminor and + @gid == other.gid and + @gname == other.gname and + @linkname == other.linkname and + @magic == other.magic and + @mode == other.mode and + @mtime == other.mtime and + @name == other.name and + @prefix == other.prefix and + @size == other.size and + @typeflag == other.typeflag and + @uid == other.uid and + @uname == other.uname and + @version == other.version + end + + def to_s # :nodoc: + update_checksum + header + end + + ## + # Updates the TarHeader's checksum + + def update_checksum + header = header " " * 8 + @checksum = oct calculate_checksum(header), 6 + end + + private + + def calculate_checksum(header) + header.unpack("C*").inject { |a, b| a + b } + end + + def header(checksum = @checksum) + header = [ + name, + oct(mode, 7), + oct(uid, 7), + oct(gid, 7), + oct(size, 11), + oct(mtime, 11), + checksum, + " ", + typeflag, + linkname, + magic, + oct(version, 2), + uname, + gname, + oct(devmajor, 7), + oct(devminor, 7), + prefix + ] + + header = header.pack PACK_FORMAT + + header << ("\0" * ((512 - header.size) % 512)) + end + + def oct(num, len) + "%0#{len}o" % num + end + +end + diff --git a/jni/ruby/lib/rubygems/package/tar_reader.rb b/jni/ruby/lib/rubygems/package/tar_reader.rb new file mode 100644 index 0000000..e257fdd --- /dev/null +++ b/jni/ruby/lib/rubygems/package/tar_reader.rb @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +#-- +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#++ + +## +# TarReader reads tar files and allows iteration over their items + +class Gem::Package::TarReader + + include Enumerable + + ## + # Raised if the tar IO is not seekable + + class UnexpectedEOF < StandardError; end + + ## + # Creates a new TarReader on +io+ and yields it to the block, if given. + + def self.new(io) + reader = super + + return reader unless block_given? + + begin + yield reader + ensure + reader.close + end + + nil + end + + ## + # Creates a new tar file reader on +io+ which needs to respond to #pos, + # #eof?, #read, #getc and #pos= + + def initialize(io) + @io = io + @init_pos = io.pos + end + + ## + # Close the tar file + + def close + end + + ## + # Iterates over files in the tarball yielding each entry + + def each + return enum_for __method__ unless block_given? + + until @io.eof? do + header = Gem::Package::TarHeader.from @io + return if header.empty? + + entry = Gem::Package::TarReader::Entry.new header, @io + size = entry.header.size + + yield entry + + skip = (512 - (size % 512)) % 512 + pending = size - entry.bytes_read + + begin + # avoid reading... + @io.seek pending, IO::SEEK_CUR + pending = 0 + rescue Errno::EINVAL, NameError + while pending > 0 do + bytes_read = @io.read([pending, 4096].min).size + raise UnexpectedEOF if @io.eof? + pending -= bytes_read + end + end + + @io.read skip # discard trailing zeros + + # make sure nobody can use #read, #getc or #rewind anymore + entry.close + end + end + + alias each_entry each + + ## + # NOTE: Do not call #rewind during #each + + def rewind + if @init_pos == 0 then + raise Gem::Package::NonSeekableIO unless @io.respond_to? :rewind + @io.rewind + else + raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= + @io.pos = @init_pos + end + end + + ## + # Seeks through the tar file until it finds the +entry+ with +name+ and + # yields it. Rewinds the tar file to the beginning when the block + # terminates. + + def seek name # :yields: entry + found = find do |entry| + entry.full_name == name + end + + return unless found + + return yield found + ensure + rewind + end + +end + +require 'rubygems/package/tar_reader/entry' + diff --git a/jni/ruby/lib/rubygems/package/tar_reader/entry.rb b/jni/ruby/lib/rubygems/package/tar_reader/entry.rb new file mode 100644 index 0000000..737c763 --- /dev/null +++ b/jni/ruby/lib/rubygems/package/tar_reader/entry.rb @@ -0,0 +1,147 @@ +# -*- coding: utf-8 -*- +#++ +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#-- + +## +# Class for reading entries out of a tar file + +class Gem::Package::TarReader::Entry + + ## + # Header for this tar entry + + attr_reader :header + + ## + # Creates a new tar entry for +header+ that will be read from +io+ + + def initialize(header, io) + @closed = false + @header = header + @io = io + @orig_pos = @io.pos + @read = 0 + end + + def check_closed # :nodoc: + raise IOError, "closed #{self.class}" if closed? + end + + ## + # Number of bytes read out of the tar entry + + def bytes_read + @read + end + + ## + # Closes the tar entry + + def close + @closed = true + end + + ## + # Is the tar entry closed? + + def closed? + @closed + end + + ## + # Are we at the end of the tar entry? + + def eof? + check_closed + + @read >= @header.size + end + + ## + # Full name of the tar entry + + def full_name + if @header.prefix != "" then + File.join @header.prefix, @header.name + else + @header.name + end + rescue ArgumentError => e + raise unless e.message == 'string contains null byte' + raise Gem::Package::TarInvalidError, + 'tar is corrupt, name contains null byte' + end + + ## + # Read one byte from the tar entry + + def getc + check_closed + + return nil if @read >= @header.size + + ret = @io.getc + @read += 1 if ret + + ret + end + + ## + # Is this tar entry a directory? + + def directory? + @header.typeflag == "5" + end + + ## + # Is this tar entry a file? + + def file? + @header.typeflag == "0" + end + + ## + # The position in the tar entry + + def pos + check_closed + + bytes_read + end + + ## + # Reads +len+ bytes from the tar file entry, or the rest of the entry if + # nil + + def read(len = nil) + check_closed + + return nil if @read >= @header.size + + len ||= @header.size - @read + max_read = [len, @header.size - @read].min + + ret = @io.read max_read + @read += ret.size + + ret + end + + alias readpartial read # :nodoc: + + ## + # Rewinds to the beginning of the tar file entry + + def rewind + check_closed + + raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= + + @io.pos = @orig_pos + @read = 0 + end + +end + diff --git a/jni/ruby/lib/rubygems/package/tar_test_case.rb b/jni/ruby/lib/rubygems/package/tar_test_case.rb new file mode 100644 index 0000000..5253e32 --- /dev/null +++ b/jni/ruby/lib/rubygems/package/tar_test_case.rb @@ -0,0 +1,137 @@ +require 'rubygems/test_case' +require 'rubygems/package' + +## +# A test case for Gem::Package::Tar* classes + +class Gem::Package::TarTestCase < Gem::TestCase + + def ASCIIZ(str, length) + str + "\0" * (length - str.length) + end + + def SP(s) + s + " " + end + + def SP_Z(s) + s + " \0" + end + + def Z(s) + s + "\0" + end + + def assert_headers_equal(expected, actual) + expected = expected.to_s unless String === expected + actual = actual.to_s unless String === actual + + fields = %w[ + name 100 + mode 8 + uid 8 + gid 8 + size 12 + mtime 12 + checksum 8 + typeflag 1 + linkname 100 + magic 6 + version 2 + uname 32 + gname 32 + devmajor 8 + devminor 8 + prefix 155 + ] + + offset = 0 + + until fields.empty? do + name = fields.shift + length = fields.shift.to_i + + if name == "checksum" then + chksum_off = offset + offset += length + next + end + + assert_equal expected[offset, length], actual[offset, length], + "Field #{name} of the tar header differs." + + offset += length + end + + assert_equal expected[chksum_off, 8], actual[chksum_off, 8] + end + + def calc_checksum(header) + sum = header.unpack("C*").inject{|s,a| s + a} + SP(Z(to_oct(sum, 6))) + end + + def header(type, fname, dname, length, mode, mtime, checksum = nil) + checksum ||= " " * 8 + + arr = [ # struct tarfile_entry_posix + ASCIIZ(fname, 100), # char name[100]; ASCII + (Z unless filled) + Z(to_oct(mode, 7)), # char mode[8]; 0 padded, octal null + Z(to_oct(0, 7)), # char uid[8]; ditto + Z(to_oct(0, 7)), # char gid[8]; ditto + Z(to_oct(length, 11)), # char size[12]; 0 padded, octal, null + Z(to_oct(mtime, 11)), # char mtime[12]; 0 padded, octal, null + checksum, # char checksum[8]; 0 padded, octal, null, space + type, # char typeflag[1]; file: "0" dir: "5" + "\0" * 100, # char linkname[100]; ASCII + (Z unless filled) + "ustar\0", # char magic[6]; "ustar\0" + "00", # char version[2]; "00" + ASCIIZ("wheel", 32), # char uname[32]; ASCIIZ + ASCIIZ("wheel", 32), # char gname[32]; ASCIIZ + Z(to_oct(0, 7)), # char devmajor[8]; 0 padded, octal, null + Z(to_oct(0, 7)), # char devminor[8]; 0 padded, octal, null + ASCIIZ(dname, 155) # char prefix[155]; ASCII + (Z unless filled) + ] + + format = "C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155" + h = if RUBY_VERSION >= "1.9" then + arr.join + else + arr = arr.join("").split(//).map{|x| x[0]} + arr.pack format + end + ret = h + "\0" * (512 - h.size) + assert_equal(512, ret.size) + ret + end + + def tar_dir_header(name, prefix, mode, mtime) + h = header("5", name, prefix, 0, mode, mtime) + checksum = calc_checksum(h) + header("5", name, prefix, 0, mode, mtime, checksum) + end + + def tar_file_header(fname, dname, mode, length, mtime) + h = header("0", fname, dname, length, mode, mtime) + checksum = calc_checksum(h) + header("0", fname, dname, length, mode, mtime, checksum) + end + + def to_oct(n, pad_size) + "%0#{pad_size}o" % n + end + + def util_entry(tar) + io = TempIO.new tar + + header = Gem::Package::TarHeader.from io + + Gem::Package::TarReader::Entry.new header, io + end + + def util_dir_entry + util_entry tar_dir_header("foo", "bar", 0, Time.now) + end + +end + diff --git a/jni/ruby/lib/rubygems/package/tar_writer.rb b/jni/ruby/lib/rubygems/package/tar_writer.rb new file mode 100644 index 0000000..dfd6357 --- /dev/null +++ b/jni/ruby/lib/rubygems/package/tar_writer.rb @@ -0,0 +1,326 @@ +# -*- coding: utf-8 -*- +#-- +# Copyright (C) 2004 Mauricio Julio Fernández Pradier +# See LICENSE.txt for additional licensing information. +#++ + +require 'digest' + +## +# Allows writing of tar files + +class Gem::Package::TarWriter + + class FileOverflow < StandardError; end + + ## + # IO wrapper that allows writing a limited amount of data + + class BoundedStream + + ## + # Maximum number of bytes that can be written + + attr_reader :limit + + ## + # Number of bytes written + + attr_reader :written + + ## + # Wraps +io+ and allows up to +limit+ bytes to be written + + def initialize(io, limit) + @io = io + @limit = limit + @written = 0 + end + + ## + # Writes +data+ onto the IO, raising a FileOverflow exception if the + # number of bytes will be more than #limit + + def write(data) + if data.bytesize + @written > @limit + raise FileOverflow, "You tried to feed more data than fits in the file." + end + @io.write data + @written += data.bytesize + data.bytesize + end + + end + + ## + # IO wrapper that provides only #write + + class RestrictedStream + + ## + # Creates a new RestrictedStream wrapping +io+ + + def initialize(io) + @io = io + end + + ## + # Writes +data+ onto the IO + + def write(data) + @io.write data + end + + end + + ## + # Creates a new TarWriter, yielding it if a block is given + + def self.new(io) + writer = super + + return writer unless block_given? + + begin + yield writer + ensure + writer.close + end + + nil + end + + ## + # Creates a new TarWriter that will write to +io+ + + def initialize(io) + @io = io + @closed = false + end + + ## + # Adds file +name+ with permissions +mode+, and yields an IO for writing the + # file to + + def add_file(name, mode) # :yields: io + check_closed + + raise Gem::Package::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) if block_given? + + size = @io.pos - init_pos - 512 + + remainder = (512 - (size % 512)) % 512 + @io.write "\0" * remainder + + final_pos = @io.pos + @io.pos = init_pos + + header = Gem::Package::TarHeader.new :name => name, :mode => mode, + :size => size, :prefix => prefix, + :mtime => Time.now + + @io.write header + @io.pos = final_pos + + self + end + + ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +digest_algorithm+ is written to a read-only +name+.sum + # file following the given file contents containing the digest name and + # hexdigest separated by a tab. + # + # The created digest object is returned. + + def add_file_digest name, mode, digest_algorithms # :yields: io + digests = digest_algorithms.map do |digest_algorithm| + digest = digest_algorithm.new + digest_name = + if digest.respond_to? :name then + digest.name + else + /::([^:]+)$/ =~ digest_algorithm.name + $1 + end + + [digest_name, digest] + end + + digests = Hash[*digests.flatten] + + add_file name, mode do |io| + Gem::Package::DigestIO.wrap io, digests do |digest_io| + yield digest_io + end + end + + digests + end + + ## + # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing + # the file. The +signer+ is used to add a digest file using its + # digest_algorithm per add_file_digest and a cryptographic signature in + # +name+.sig. If the signer has no key only the checksum file is added. + # + # Returns the digest. + + def add_file_signed name, mode, signer + digest_algorithms = [ + signer.digest_algorithm, + Digest::SHA512, + ].compact.uniq + + digests = add_file_digest name, mode, digest_algorithms do |io| + yield io + end + + signature_digest = digests.values.compact.find do |digest| + digest_name = + if digest.respond_to? :name then + digest.name + else + /::([^:]+)$/ =~ digest.class.name + $1 + end + + digest_name == signer.digest_name + end + + if signer.key then + signature = signer.sign signature_digest.digest + + add_file_simple "#{name}.sig", 0444, signature.length do |io| + io.write signature + end + end + + digests + end + + ## + # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO + # to write the file to. + + def add_file_simple(name, mode, size) # :yields: io + check_closed + + name, prefix = split_name name + + header = Gem::Package::TarHeader.new(:name => name, :mode => mode, + :size => size, :prefix => prefix, + :mtime => Time.now).to_s + + @io.write header + os = BoundedStream.new @io, size + + yield os if block_given? + + min_padding = size - os.written + @io.write("\0" * min_padding) + + remainder = (512 - (size % 512)) % 512 + @io.write("\0" * remainder) + + self + end + + ## + # Raises IOError if the TarWriter is closed + + def check_closed + raise IOError, "closed #{self.class}" if closed? + end + + ## + # Closes the TarWriter + + def close + check_closed + + @io.write "\0" * 1024 + flush + + @closed = true + end + + ## + # Is the TarWriter closed? + + def closed? + @closed + end + + ## + # Flushes the TarWriter's IO + + def flush + check_closed + + @io.flush if @io.respond_to? :flush + end + + ## + # Creates a new directory in the tar file +name+ with +mode+ + + def mkdir(name, mode) + check_closed + + name, prefix = split_name(name) + + header = Gem::Package::TarHeader.new :name => name, :mode => mode, + :typeflag => "5", :size => 0, + :prefix => prefix, + :mtime => Time.now + + @io.write header + + self + end + + ## + # Splits +name+ into a name and prefix that can fit in the TarHeader + + def split_name(name) # :nodoc: + if name.bytesize > 256 + raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)") + end + + if name.bytesize <= 100 then + prefix = "" + else + parts = name.split(/\//) + newname = parts.pop + nxt = "" + + loop do + nxt = parts.pop + break if newname.bytesize + 1 + nxt.bytesize > 100 + newname = nxt + "/" + newname + end + + prefix = (parts + [nxt]).join "/" + name = newname + + if name.bytesize > 100 + raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)") + end + + if prefix.bytesize > 155 then + raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)") + end + end + + return name, prefix + end + +end + |