diff options
Diffstat (limited to 'lib/rubygems/package')
| -rw-r--r-- | lib/rubygems/package/digest_io.rb | 12 | ||||
| -rw-r--r-- | lib/rubygems/package/file_source.rb | 14 | ||||
| -rw-r--r-- | lib/rubygems/package/io_source.rb | 10 | ||||
| -rw-r--r-- | lib/rubygems/package/old.rb | 43 | ||||
| -rw-r--r-- | lib/rubygems/package/source.rb | 2 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_header.rb | 236 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_reader.rb | 60 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_reader/entry.rb | 142 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_test_case.rb | 147 | ||||
| -rw-r--r-- | lib/rubygems/package/tar_writer.rb | 97 |
10 files changed, 358 insertions, 405 deletions
diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb index 4930c9aa7d..f04ab97462 100644 --- a/lib/rubygems/package/digest_io.rb +++ b/lib/rubygems/package/digest_io.rb @@ -1,9 +1,9 @@ # 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. # @@ -31,19 +31,19 @@ class Gem::Package::DigestIO # digests['SHA1'].hexdigest #=> "aaf4c61d[...]" # digests['SHA512'].hexdigest #=> "9b71d224[...]" - def self.wrap io, digests + def self.wrap(io, digests) digest_io = new io, digests yield digest_io - return digests + 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 + def initialize(io, digests) @io = io @digests = digests end @@ -51,7 +51,7 @@ class Gem::Package::DigestIO ## # Writes +data+ to the underlying IO and updates the digests - def write data + def write(data) result = @io.write data @digests.each do |_, digest| @@ -60,6 +60,4 @@ class Gem::Package::DigestIO result end - end - diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb index 1a4dc4c824..d9717e0f2a 100644 --- a/lib/rubygems/package/file_source.rb +++ b/lib/rubygems/package/file_source.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The primary source of gems is a file on disk, including all usages # internal to rubygems. @@ -7,10 +8,9 @@ # object to `Gem::Package.new`. class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all - attr_reader :path - def initialize path + def initialize(path) @path = path end @@ -22,13 +22,11 @@ class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all File.exist? path end - def with_write_io &block - open path, 'wb', &block + def with_write_io(&block) + File.open path, "wb", &block end - def with_read_io &block - open path, 'rb', &block + 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 index ee79a21083..227835dfce 100644 --- a/lib/rubygems/package/io_source.rb +++ b/lib/rubygems/package/io_source.rb @@ -1,4 +1,5 @@ # 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 @@ -8,10 +9,9 @@ # object to `Gem::Package.new`. class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all - attr_reader :io - def initialize io + def initialize(io) @io = io end @@ -33,14 +33,16 @@ class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all 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 index f6e6e67c38..1a13ac3e29 100644 --- a/lib/rubygems/package/old.rb +++ b/lib/rubygems/package/old.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -12,16 +13,15 @@ # 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' + def initialize(gem, security_policy) + require "fileutils" + require "zlib" Gem.load_yaml @contents = nil @@ -42,14 +42,14 @@ class Gem::Package::Old < Gem::Package read_until_dashes io # spec header = file_list io - @contents = header.map { |file| file['path'] } + @contents = header.map {|file| file["path"] } end end ## # Extracts the files in this package into +destination_dir+ - def extract_files destination_dir + def extract_files(destination_dir) verify errstr = "Error reading files from gem" @@ -60,7 +60,7 @@ class Gem::Package::Old < Gem::Package raise Gem::Exception, errstr unless header header.each do |entry| - full_name = entry['path'] + full_name = entry["path"] destination = install_location full_name, destination_dir @@ -70,17 +70,17 @@ class Gem::Package::Old < Gem::Package file_data << line end - file_data = file_data.strip.unpack("m")[0] + 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 + file_data.length != entry["size"].to_i FileUtils.rm_rf destination - FileUtils.mkdir_p File.dirname destination + FileUtils.mkdir_p File.dirname(destination), mode: dir_mode && 0o755 - open destination, 'wb', entry['mode'] do |out| + File.open destination, "wb", file_mode(entry["mode"]) do |out| out.write file_data end @@ -94,7 +94,7 @@ class Gem::Package::Old < Gem::Package ## # Reads the file list section from the old-format gem +io+ - def file_list io # :nodoc: + def file_list(io) # :nodoc: header = String.new read_until_dashes io do |line| @@ -107,7 +107,7 @@ class Gem::Package::Old < Gem::Package ## # Reads lines until a "---" separator is found - def read_until_dashes io # :nodoc: + def read_until_dashes(io) # :nodoc: while (line = io.gets) && line.chomp.strip != "---" do yield line if block_given? end @@ -116,11 +116,11 @@ class Gem::Package::Old < Gem::Package ## # Skips the Ruby self-install header in +io+. - def skip_ruby io # :nodoc: + def skip_ruby(io) # :nodoc: loop do line = io.gets - return if line.chomp == '__END__' + return if line.chomp == "__END__" break unless line end @@ -144,17 +144,9 @@ class Gem::Package::Old < Gem::Package 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 + rescue Psych::SyntaxError raise Gem::Exception, "Failed to parse gem specification out of gem file" end rescue ArgumentError @@ -169,10 +161,9 @@ class Gem::Package::Old < Gem::Package return true unless @security_policy raise Gem::Security::Exception, - 'old format gems do not contain signatures and cannot be verified' if + "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 index fe19776c38..8c44f8c305 100644 --- a/lib/rubygems/package/source.rb +++ b/lib/rubygems/package/source.rb @@ -1,4 +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 index c54bd14d57..dd20d65080 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -1,9 +1,11 @@ -# -*- coding: utf-8 -*- # 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 ## #-- @@ -29,7 +31,6 @@ # A header for a tar file class Gem::Package::TarHeader - ## # Fields in the tar header @@ -50,106 +51,143 @@ class Gem::Package::TarHeader :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' # prefix + 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' # prefix + 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 - empty = (header == "\0" * 512) + return EMPTY if header == EMPTY_HEADER 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 + 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] then + 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" 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 + @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? @@ -158,23 +196,23 @@ class Gem::Package::TarHeader 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 + 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: @@ -190,10 +228,21 @@ class Gem::Package::TarHeader @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.unpack("C*").inject { |a, b| a + b } + header.sum(0) end def header(checksum = @checksum) @@ -214,16 +263,15 @@ class Gem::Package::TarHeader gname, oct(devmajor, 7), oct(devminor, 7), - prefix + prefix, ] header = header.pack PACK_FORMAT - header << ("\0" * ((512 - header.size) % 512)) + header.ljust 512, "\0" end def oct(num, len) - "%0#{len}o" % num + format("%0#{len}o", num) end - end diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index 1098336e36..b66a8a62bc 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -1,23 +1,19 @@ -# -*- coding: utf-8 -*- # 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 ## - # 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) @@ -34,6 +30,8 @@ class Gem::Package::TarReader 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= @@ -56,47 +54,30 @@ class Gem::Package::TarReader 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 + 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 - @io.read skip # discard trailing zeros - - # make sure nobody can use #read, #getc or #rewind anymore + return if header.empty? + entry = Gem::Package::TarReader::Entry.new header, @io + yield entry entry.close end end - alias each_entry each + alias_method :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 + if @init_pos == 0 @io.rewind else - raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= @io.pos = @init_pos end end @@ -106,18 +87,17 @@ class Gem::Package::TarReader # yields it. Rewinds the tar file to the beginning when the block # terminates. - def seek name # :yields: entry + def seek(name) # :yields: entry found = find do |entry| entry.full_name == name end return unless found - return yield found + yield found ensure rewind end - end -require 'rubygems/package/tar_reader/entry' +require_relative "tar_reader/entry" diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb index 5f958edc2f..f837e86fd6 100644 --- a/lib/rubygems/package/tar_reader/entry.rb +++ b/lib/rubygems/package/tar_reader/entry.rb @@ -1,14 +1,29 @@ -# -*- coding: utf-8 -*- # 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 @@ -23,6 +38,7 @@ class Gem::Package::TarReader::Entry @header = header @io = io @orig_pos = @io.pos + @end_pos = @orig_pos + @header.size @read = 0 end @@ -41,7 +57,14 @@ class Gem::Package::TarReader::Entry # 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 ## @@ -64,24 +87,18 @@ class Gem::Package::TarReader::Entry # Full name of the tar entry def full_name - if @header.prefix != "" then - File.join @header.prefix, @header.name - else - @header.name - end + @header.full_name.force_encoding(Encoding::UTF_8) rescue ArgumentError => e - raise unless e.message == 'string contains null byte' + raise unless e.message == "string contains null byte" raise Gem::Package::TarInvalidError, - 'tar is corrupt, name contains null byte' + "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 + return nil if eof? ret = @io.getc @read += 1 if ret @@ -120,35 +137,108 @@ class Gem::Package::TarReader::Entry end ## - # Reads +len+ bytes from the tar file entry, or the rest of the entry if - # nil + # Seek to the position in the tar entry - def read(len = nil) - check_closed + def pos=(new_pos) + seek(new_pos, IO::SEEK_SET) + end + + def size + @header.size + end - return nil if @read >= @header.size + alias_method :length, :size - len ||= @header.size - @read - max_read = [len, @header.size - @read].min + ## + # 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 - alias readpartial read # :nodoc: + 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 ## - # Rewinds to the beginning of the tar file entry + # Seeks to +offset+ bytes into the tar file entry + # +whence+ can be IO::SEEK_SET, IO::SEEK_CUR, or IO::SEEK_END - def rewind + def seek(offset, whence = IO::SEEK_SET) check_closed - raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos= + 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 - @io.pos = @orig_pos - @read = 0 + 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_test_case.rb b/lib/rubygems/package/tar_test_case.rb deleted file mode 100644 index 46ac949587..0000000000 --- a/lib/rubygems/package/tar_test_case.rb +++ /dev/null @@ -1,147 +0,0 @@ -# frozen_string_literal: true -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, linkname = "") - 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" - ASCIIZ(linkname, 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 tar_symlink_header(fname, prefix, mode, mtime, linkname) - h = header("2", fname, prefix, 0, mode, mtime, nil, linkname) - checksum = calc_checksum(h) - header("2", fname, prefix, 0, mode, mtime, checksum, linkname) - 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 - - def util_symlink_entry - util_entry tar_symlink_header("foo", "bar", 0, Time.now, "link") - end - -end diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index f68b8d4c5e..39fed9e2af 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -1,24 +1,22 @@ -# -*- coding: utf-8 -*- # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ -require 'digest' +# 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 @@ -50,14 +48,12 @@ class Gem::Package::TarWriter @written += data.bytesize data.bytesize end - end ## # IO wrapper that provides only #write class RestrictedStream - ## # Creates a new RestrictedStream wrapping +io+ @@ -71,7 +67,6 @@ class Gem::Package::TarWriter def write(data) @io.write data end - end ## @@ -100,18 +95,17 @@ class Gem::Package::TarWriter end ## - # Adds file +name+ with permissions +mode+, and yields an IO for writing the - # file to + # 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) # :yields: io + def add_file(name, mode, mtime = nil) # :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 + @io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header yield RestrictedStream.new(@io) if block_given? @@ -123,9 +117,9 @@ class Gem::Package::TarWriter final_pos = @io.pos @io.pos = init_pos - header = Gem::Package::TarHeader.new :name => name, :mode => mode, - :size => size, :prefix => prefix, - :mtime => Time.now + 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 @@ -141,15 +135,14 @@ class Gem::Package::TarWriter # # The created digest object is returned. - def add_file_digest name, mode, digest_algorithms # :yields: io + 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 + if digest.respond_to? :name digest.name else - /::([^:]+)$/ =~ digest_algorithm.name - $1 + digest_algorithm.class.name[/::([^:]+)\z/, 1] end [digest_name, digest] @@ -174,10 +167,10 @@ class Gem::Package::TarWriter # # Returns the digest. - def add_file_signed name, mode, signer + def add_file_signed(name, mode, signer) digest_algorithms = [ signer.digest_algorithm, - Digest::SHA512, + Gem::Security.create_digest("SHA512"), ].compact.uniq digests = add_file_digest name, mode, digest_algorithms do |io| @@ -186,20 +179,21 @@ class Gem::Package::TarWriter signature_digest = digests.values.compact.find do |digest| digest_name = - if digest.respond_to? :name then + if digest.respond_to? :name digest.name else - /::([^:]+)$/ =~ digest.class.name - $1 + digest.class.name[/::([^:]+)\z/, 1] end digest_name == signer.digest_name end - if signer.key then + 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", 0444, signature.length do |io| + add_file_simple "#{name}.sig", 0o444, signature.length do |io| io.write signature end end @@ -216,9 +210,9 @@ class Gem::Package::TarWriter name, prefix = split_name name - header = Gem::Package::TarHeader.new(:name => name, :mode => mode, - :size => size, :prefix => prefix, - :mtime => Time.now).to_s + 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 @@ -242,11 +236,11 @@ class Gem::Package::TarWriter name, prefix = split_name name - header = Gem::Package::TarHeader.new(:name => name, :mode => mode, - :size => 0, :typeflag => "2", - :linkname => target, - :prefix => prefix, - :mtime => Time.now).to_s + 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 @@ -296,10 +290,10 @@ class Gem::Package::TarWriter name, prefix = split_name(name) - header = Gem::Package::TarHeader.new :name => name, :mode => mode, - :typeflag => "5", :size => 0, - :prefix => prefix, - :mtime => Time.now + header = Gem::Package::TarHeader.new name: name, mode: mode, + typeflag: "5", size: 0, + prefix: prefix, + mtime: Gem.source_date_epoch @io.write header @@ -310,30 +304,29 @@ class Gem::Package::TarWriter # Splits +name+ into a name and prefix that can fit in the TarHeader def split_name(name) # :nodoc: - if name.bytesize > 256 then + 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 then - parts = name.split('/', -1) # parts are never empty here + 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) + 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('/') + name = parts.pop + "/" + name + prefix = parts.join("/") end - if name.bytesize > 100 or prefix.empty? then + 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 then + 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 - return name, prefix + [name, prefix] end - end |
