diff options
Diffstat (limited to 'lib/rubygems/package')
| -rw-r--r-- | lib/rubygems/package/digest_io.rb | 63 | ||||
| -rw-r--r-- | lib/rubygems/package/file_source.rb | 32 | ||||
| -rw-r--r-- | lib/rubygems/package/io_source.rb | 48 | ||||
| -rw-r--r-- | lib/rubygems/package/old.rb | 169 | ||||
| -rw-r--r-- | lib/rubygems/package/source.rb | 4 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_header.rb | 277 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_reader.rb | 103 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_reader/entry.rb | 244 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_writer.rb | 332 |
9 files changed, 1272 insertions, 0 deletions
diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb new file mode 100644 index 0000000000..f04ab97462 --- /dev/null +++ b/lib/rubygems/package/digest_io.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +## +# 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 + + 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/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb new file mode 100644 index 0000000000..d9717e0f2a --- /dev/null +++ b/lib/rubygems/package/file_source.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +## +# 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) + File.open path, "wb", &block + end + + def with_read_io(&block) + File.open path, "rb", &block + end +end diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb new file mode 100644 index 0000000000..227835dfce --- /dev/null +++ b/lib/rubygems/package/io_source.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +## +# 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 + ensure + io.rewind + end + + def with_write_io + yield io + ensure + io.rewind + end + + def path + end +end diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb new file mode 100644 index 0000000000..1a13ac3e29 --- /dev/null +++ b/lib/rubygems/package/old.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +#-- +# 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, security_policy) + require "fileutils" + require "zlib" + Gem.load_yaml + + @contents = nil + @gem = gem + @security_policy = security_policy + @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 = String.new + + read_until_dashes io do |line| + file_data << line + end + + file_data = file_data.strip.unpack1("m") + 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), mode: dir_mode && 0o755 + + File.open destination, "wb", file_mode(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 = String.new + + read_until_dashes io do |line| + header << line + end + + Gem::SafeYAML.safe_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 = String.new + + @gem.with_read_io do |io| + skip_ruby io + read_until_dashes io do |line| + yaml << line + end + end + + begin + @spec = Gem::Specification.from_yaml yaml + rescue Psych::SyntaxError + 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/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb new file mode 100644 index 0000000000..8c44f8c305 --- /dev/null +++ b/lib/rubygems/package/source.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class Gem::Package::Source # :nodoc: +end diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb new file mode 100644 index 0000000000..dd20d65080 --- /dev/null +++ b/lib/rubygems/package/tar_header.rb @@ -0,0 +1,277 @@ +# 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 + +## +#-- +# 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, + ].freeze + + ## + # 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").freeze # 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").freeze # prefix + + attr_reader(*FIELDS) + + EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc: + + ## + # Creates a tar header from IO +stream+ + + def self.from(stream) + header = stream.read 512 + return EMPTY if header == EMPTY_HEADER + + fields = header.unpack UNPACK_FORMAT + + new name: fields.shift, + mode: strict_oct(fields.shift), + uid: oct_or_256based(fields.shift), + gid: oct_or_256based(fields.shift), + size: strict_oct(fields.shift), + mtime: strict_oct(fields.shift), + checksum: strict_oct(fields.shift), + typeflag: fields.shift, + linkname: fields.shift, + magic: fields.shift, + version: strict_oct(fields.shift), + uname: fields.shift, + gname: fields.shift, + devmajor: strict_oct(fields.shift), + devminor: strict_oct(fields.shift), + prefix: fields.shift, + + empty: false + end + + def self.strict_oct(str) + str.strip! + return str.oct if /\A[0-7]*\z/.match?(str) + + raise ArgumentError, "#{str.inspect} is not an octal string" + end + + def self.oct_or_256based(str) + # \x80 flags a positive 256-based number + # \ff flags a negative 256-based number + # In case we have a match, parse it as a signed binary value + # in big-endian order, except that the high-order bit is ignored. + + return str.unpack1("@4N") if /\A[\x80\xff]/n.match?(str) + strict_oct(str) + end + + ## + # Creates a new TarHeader using +vals+ + + def initialize(vals) + unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode] + raise ArgumentError, ":name, :size, :prefix and :mode required" + end + + @checksum = vals[:checksum] || "" + @devmajor = vals[:devmajor] || 0 + @devminor = vals[:devminor] || 0 + @gid = vals[:gid] || 0 + @gname = vals[:gname] || "wheel" + @linkname = vals[:linkname] + @magic = vals[:magic] || "ustar" + @mode = vals[:mode] + @mtime = vals[:mtime] || 0 + @name = vals[:name] + @prefix = vals[:prefix] + @size = vals[:size] + @typeflag = vals[:typeflag] + @typeflag = "0" if @typeflag.nil? || @typeflag.empty? + @uid = vals[:uid] || 0 + @uname = vals[:uname] || "wheel" + @version = vals[:version] || "00" + + @empty = vals[:empty] + end + + EMPTY = new({ # :nodoc: + checksum: 0, + gname: "", + linkname: "", + magic: "", + mode: 0, + name: "", + prefix: "", + size: 0, + uname: "", + version: 0, + + empty: true, + }).freeze + private_constant :EMPTY + + ## + # Is the tar entry empty? + + def empty? + @empty + end + + def ==(other) # :nodoc: + self.class === other && + @checksum == other.checksum && + @devmajor == other.devmajor && + @devminor == other.devminor && + @gid == other.gid && + @gname == other.gname && + @linkname == other.linkname && + @magic == other.magic && + @mode == other.mode && + @mtime == other.mtime && + @name == other.name && + @prefix == other.prefix && + @size == other.size && + @typeflag == other.typeflag && + @uid == other.uid && + @uname == other.uname && + @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 + + ## + # Header's full name, including prefix + + def full_name + if prefix != "" + File.join prefix, name + else + name + end + end + + private + + def calculate_checksum(header) + header.sum(0) + 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.ljust 512, "\0" + end + + def oct(num, len) + format("%0#{len}o", num) + end +end diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb new file mode 100644 index 0000000000..b66a8a62bc --- /dev/null +++ b/lib/rubygems/package/tar_reader.rb @@ -0,0 +1,103 @@ +# 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 + +## +# TarReader reads tar files and allows iteration over their items + +class Gem::Package::TarReader + include Enumerable + + ## + # 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 + + attr_reader :io # :nodoc: + + ## + # 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 + begin + header = Gem::Package::TarHeader.from @io + rescue ArgumentError => e + # Specialize only exceptions from Gem::Package::TarHeader.strict_oct + raise e unless e.message.match?(/ is not an octal string$/) + raise Gem::Package::TarInvalidError, e.message + end + + return if header.empty? + entry = Gem::Package::TarReader::Entry.new header, @io + yield entry + entry.close + end + end + + alias_method :each_entry, :each + + ## + # NOTE: Do not call #rewind during #each + + def rewind + if @init_pos == 0 + @io.rewind + else + @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 + + yield found + ensure + rewind + end +end + +require_relative "tar_reader/entry" diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb new file mode 100644 index 0000000000..f837e86fd6 --- /dev/null +++ b/lib/rubygems/package/tar_reader/entry.rb @@ -0,0 +1,244 @@ +# 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 + +## +# Class for reading entries out of a tar file + +class Gem::Package::TarReader::Entry + ## + # Creates a new tar entry for +header+ that will be read from +io+ + # If a block is given, the entry is yielded and then closed. + + def self.open(header, io, &block) + entry = new header, io + return entry unless block_given? + begin + yield entry + ensure + entry.close + end + end + + ## + # 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 + @end_pos = @orig_pos + @header.size + @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 + return if closed? + # Seek to the end of the entry if it wasn't fully read + seek(0, IO::SEEK_END) + # discard trailing zeros + skip = (512 - (@header.size % 512)) % 512 + @io.read(skip) + @closed = true + nil + 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 + @header.full_name.force_encoding(Encoding::UTF_8) + 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 + return nil if eof? + + 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 + + ## + # Is this tar entry a symlink? + + def symlink? + @header.typeflag == "2" + end + + ## + # The position in the tar entry + + def pos + check_closed + + bytes_read + end + + ## + # Seek to the position in the tar entry + + def pos=(new_pos) + seek(new_pos, IO::SEEK_SET) + end + + def size + @header.size + end + + alias_method :length, :size + + ## + # Reads +maxlen+ bytes from the tar file entry, or the rest of the entry if nil + + def read(maxlen = nil) + if eof? + return maxlen.to_i.zero? ? "" : nil + end + + max_read = [maxlen, @header.size - @read].compact.min + + ret = @io.read max_read + if ret.nil? + return maxlen ? nil : "" # IO.read returns nil on EOF with len argument + end + @read += ret.size + + ret + end + + def readpartial(maxlen, outbuf = "".b) + if eof? && maxlen > 0 + raise EOFError, "end of file reached" + end + + max_read = [maxlen, @header.size - @read].min + + @io.readpartial(max_read, outbuf) + @read += outbuf.size + + outbuf + end + + ## + # Seeks to +offset+ bytes into the tar file entry + # +whence+ can be IO::SEEK_SET, IO::SEEK_CUR, or IO::SEEK_END + + def seek(offset, whence = IO::SEEK_SET) + check_closed + + new_pos = + case whence + when IO::SEEK_SET then @orig_pos + offset + when IO::SEEK_CUR then @io.pos + offset + when IO::SEEK_END then @end_pos + offset + else + raise ArgumentError, "invalid whence" + end + + if new_pos < @orig_pos + new_pos = @orig_pos + elsif new_pos > @end_pos + new_pos = @end_pos + end + + pending = new_pos - @io.pos + + return 0 if pending == 0 + + if @io.respond_to?(:seek) + begin + # avoid reading if the @io supports seeking + @io.seek new_pos, IO::SEEK_SET + pending = 0 + rescue Errno::EINVAL + end + end + + # if seeking isn't supported or failed + # negative seek requires that we rewind and read + if pending < 0 + @io.rewind + pending = new_pos + end + + while pending > 0 do + size_read = @io.read([pending, 4096].min)&.size + raise(EOFError, "end of file reached") if size_read.nil? + pending -= size_read + end + + @read = @io.pos - @orig_pos + + 0 + end + + ## + # Rewinds to the beginning of the tar file entry + + def rewind + check_closed + seek(0, IO::SEEK_SET) + end +end diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb new file mode 100644 index 0000000000..39fed9e2af --- /dev/null +++ b/lib/rubygems/package/tar_writer.rb @@ -0,0 +1,332 @@ +# 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 + +## +# 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 mtime +mtime+ (sets + # Gem.source_date_epoch if not specified), and yields an IO for + # writing the file to + + def add_file(name, mode, mtime = nil) # :yields: io + check_closed + + name, prefix = split_name name + + init_pos = @io.pos + @io.write Gem::Package::TarHeader::EMPTY_HEADER # 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: mtime || Gem.source_date_epoch + + @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 + digest.name + else + digest_algorithm.class.name[/::([^:]+)\z/, 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, + Gem::Security.create_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 + digest.name + else + digest.class.name[/::([^:]+)\z/, 1] + end + + digest_name == signer.digest_name + end + + raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest + + if signer.key + signature = signer.sign signature_digest.digest + + add_file_simple "#{name}.sig", 0o444, 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: Gem.source_date_epoch).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 + + ## + # Adds symlink +name+ with permissions +mode+, linking to +target+. + + def add_symlink(name, target, mode) + check_closed + + name, prefix = split_name name + + header = Gem::Package::TarHeader.new(name: name, mode: mode, + size: 0, typeflag: "2", + linkname: target, + prefix: prefix, + mtime: Gem.source_date_epoch).to_s + + @io.write header + + 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: Gem.source_date_epoch + + @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 + + prefix = "" + if name.bytesize > 100 + parts = name.split("/", -1) # parts are never empty here + name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/") + prefix = parts.join("/") # if empty, then it's impossible to split (parts is empty too) + while !parts.empty? && (prefix.bytesize > 155 || name.empty?) + name = parts.pop + "/" + name + prefix = parts.join("/") + end + + if name.bytesize > 100 || prefix.empty? + raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)") + end + + if prefix.bytesize > 155 + raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)") + end + end + + [name, prefix] + end +end |
