mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
bbcf8f87ac
If we explicitly disallow the creation of symlinks that point to files outside of the destination directory, we can avoid any other safety checks while creating directories, because we can be sure they will always fall under the destination directory as well. https://github.com/rubygems/rubygems/commit/555692b8de
1180 lines
29 KiB
Ruby
1180 lines
29 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative 'package/tar_test_case'
|
|
require 'digest'
|
|
|
|
class TestGemPackage < Gem::Package::TarTestCase
|
|
def setup
|
|
super
|
|
|
|
@spec = quick_gem 'a' do |s|
|
|
s.description = 'π'
|
|
s.files = %w[lib/code.rb]
|
|
end
|
|
|
|
util_build_gem @spec
|
|
|
|
@gem = @spec.cache_file
|
|
|
|
@destination = File.join @tempdir, 'extract'
|
|
|
|
FileUtils.mkdir_p @destination
|
|
end
|
|
|
|
def test_class_new_old_format
|
|
pend "jruby can't require the simple_gem file" if Gem.java_platform?
|
|
require_relative "simple_gem"
|
|
File.open 'old_format.gem', 'wb' do |io|
|
|
io.write SIMPLE_GEM
|
|
end
|
|
|
|
package = Gem::Package.new 'old_format.gem'
|
|
|
|
assert package.spec
|
|
end
|
|
|
|
def test_add_checksums
|
|
gem_io = StringIO.new
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.date = Time.at 0
|
|
spec.rubygems_version = Gem::Version.new '0'
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
package.build_time = 1 # 0 uses current time
|
|
package.setup_signer
|
|
|
|
Gem::Package::TarWriter.new gem_io do |gem|
|
|
package.add_metadata gem
|
|
package.add_contents gem
|
|
package.add_checksums gem
|
|
end
|
|
|
|
gem_io.rewind
|
|
|
|
reader = Gem::Package::TarReader.new gem_io
|
|
|
|
checksums = nil
|
|
tar = nil
|
|
|
|
reader.each_entry do |entry|
|
|
case entry.full_name
|
|
when 'checksums.yaml.gz' then
|
|
Zlib::GzipReader.wrap entry do |io|
|
|
checksums = io.read
|
|
end
|
|
when 'data.tar.gz' then
|
|
tar = entry.read
|
|
end
|
|
end
|
|
|
|
s = StringIO.new
|
|
|
|
package.gzip_to s do |io|
|
|
io.write spec.to_yaml
|
|
end
|
|
|
|
metadata_sha256 = Digest::SHA256.hexdigest s.string
|
|
metadata_sha512 = Digest::SHA512.hexdigest s.string
|
|
|
|
expected = {
|
|
'SHA512' => {
|
|
'metadata.gz' => metadata_sha512,
|
|
'data.tar.gz' => Digest::SHA512.hexdigest(tar),
|
|
},
|
|
'SHA256' => {
|
|
'metadata.gz' => metadata_sha256,
|
|
'data.tar.gz' => Digest::SHA256.hexdigest(tar),
|
|
},
|
|
}
|
|
|
|
assert_equal expected, load_yaml(checksums)
|
|
end
|
|
|
|
def test_build_time_uses_source_date_epoch
|
|
epoch = ENV["SOURCE_DATE_EPOCH"]
|
|
ENV["SOURCE_DATE_EPOCH"] = "123456789"
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.date = Time.at 0
|
|
spec.rubygems_version = Gem::Version.new '0'
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
|
|
assert_equal Time.at(ENV["SOURCE_DATE_EPOCH"].to_i).utc, package.build_time
|
|
ensure
|
|
ENV["SOURCE_DATE_EPOCH"] = epoch
|
|
end
|
|
|
|
def test_build_time_without_source_date_epoch
|
|
epoch = ENV["SOURCE_DATE_EPOCH"]
|
|
ENV["SOURCE_DATE_EPOCH"] = nil
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.rubygems_version = Gem::Version.new '0'
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
|
|
assert_kind_of Time, package.build_time
|
|
|
|
build_time = package.build_time.to_i
|
|
|
|
assert_equal Gem.source_date_epoch.to_i, build_time
|
|
ensure
|
|
ENV["SOURCE_DATE_EPOCH"] = epoch
|
|
end
|
|
|
|
def test_add_files
|
|
spec = Gem::Specification.new
|
|
spec.files = %w[lib/code.rb lib/empty]
|
|
|
|
FileUtils.mkdir_p 'lib/empty'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
File.open 'lib/extra.rb', 'w' do |io|
|
|
io.write '# lib/extra.rb'
|
|
end
|
|
|
|
package = Gem::Package.new 'bogus.gem'
|
|
package.spec = spec
|
|
|
|
tar = util_tar do |tar_io|
|
|
package.add_files tar_io
|
|
end
|
|
|
|
tar.rewind
|
|
|
|
files = []
|
|
|
|
Gem::Package::TarReader.new tar do |tar_io|
|
|
tar_io.each_entry do |entry|
|
|
files << entry.full_name
|
|
end
|
|
end
|
|
|
|
assert_equal %w[lib/code.rb], files
|
|
end
|
|
|
|
def test_add_files_symlink
|
|
spec = Gem::Specification.new
|
|
spec.files = %w[lib/code.rb lib/code_sym.rb lib/code_sym2.rb]
|
|
|
|
FileUtils.mkdir_p 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
# NOTE: 'code.rb' is correct, because it's relative to lib/code_sym.rb
|
|
begin
|
|
File.symlink('code.rb', 'lib/code_sym.rb')
|
|
File.symlink('../lib/code.rb', 'lib/code_sym2.rb')
|
|
rescue Errno::EACCES => e
|
|
if win_platform?
|
|
pend "symlink - must be admin with no UAC on Windows"
|
|
else
|
|
raise e
|
|
end
|
|
end
|
|
|
|
package = Gem::Package.new 'bogus.gem'
|
|
package.spec = spec
|
|
|
|
tar = util_tar do |tar_io|
|
|
package.add_files tar_io
|
|
end
|
|
|
|
tar.rewind
|
|
|
|
files, symlinks = [], []
|
|
|
|
Gem::Package::TarReader.new tar do |tar_io|
|
|
tar_io.each_entry do |entry|
|
|
if entry.symlink?
|
|
symlinks << { entry.full_name => entry.header.linkname }
|
|
else
|
|
files << entry.full_name
|
|
end
|
|
end
|
|
end
|
|
|
|
assert_equal %w[lib/code.rb], files
|
|
assert_equal [{'lib/code_sym.rb' => 'code.rb'}, {'lib/code_sym2.rb' => '../lib/code.rb'}], symlinks
|
|
end
|
|
|
|
def test_build
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.rubygems_version = :junk
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
package.build
|
|
|
|
assert_equal Gem::VERSION, spec.rubygems_version
|
|
assert_path_exist spec.file_name
|
|
|
|
reader = Gem::Package.new spec.file_name
|
|
assert_equal spec, reader.spec
|
|
|
|
assert_equal %w[metadata.gz data.tar.gz checksums.yaml.gz],
|
|
reader.files
|
|
|
|
assert_equal %w[lib/code.rb], reader.contents
|
|
end
|
|
|
|
def test_build_auto_signed
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
FileUtils.mkdir_p File.join(Gem.user_home, '.gem')
|
|
|
|
private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem'
|
|
Gem::Security.write PRIVATE_KEY, private_key_path
|
|
|
|
public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem'
|
|
FileUtils.cp PUBLIC_CERT_PATH, public_cert_path
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
package.build
|
|
|
|
assert_equal Gem::VERSION, spec.rubygems_version
|
|
assert_path_exist spec.file_name
|
|
|
|
reader = Gem::Package.new spec.file_name
|
|
assert reader.verify
|
|
|
|
assert_equal [PUBLIC_CERT.to_pem], reader.spec.cert_chain
|
|
|
|
assert_equal %w[metadata.gz metadata.gz.sig
|
|
data.tar.gz data.tar.gz.sig
|
|
checksums.yaml.gz checksums.yaml.gz.sig],
|
|
reader.files
|
|
|
|
assert_equal %w[lib/code.rb], reader.contents
|
|
end
|
|
|
|
def test_build_auto_signed_encrypted_key
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
FileUtils.mkdir_p File.join(Gem.user_home, '.gem')
|
|
|
|
private_key_path = File.join Gem.user_home, '.gem', 'gem-private_key.pem'
|
|
FileUtils.cp ENCRYPTED_PRIVATE_KEY_PATH, private_key_path
|
|
|
|
public_cert_path = File.join Gem.user_home, '.gem', 'gem-public_cert.pem'
|
|
Gem::Security.write PUBLIC_CERT, public_cert_path
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
package.build
|
|
|
|
assert_equal Gem::VERSION, spec.rubygems_version
|
|
assert_path_exist spec.file_name
|
|
|
|
reader = Gem::Package.new spec.file_name
|
|
assert reader.verify
|
|
|
|
assert_equal [PUBLIC_CERT.to_pem], reader.spec.cert_chain
|
|
|
|
assert_equal %w[metadata.gz metadata.gz.sig
|
|
data.tar.gz data.tar.gz.sig
|
|
checksums.yaml.gz checksums.yaml.gz.sig],
|
|
reader.files
|
|
|
|
assert_equal %w[lib/code.rb], reader.contents
|
|
end
|
|
|
|
def test_build_invalid
|
|
spec = Gem::Specification.new 'build', '1'
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
e = assert_raise Gem::InvalidSpecificationException do
|
|
package.build
|
|
end
|
|
|
|
assert_equal 'missing value for attribute summary', e.message
|
|
end
|
|
|
|
def test_build_invalid_arguments
|
|
spec = Gem::Specification.new 'build', '1'
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
e = assert_raise ArgumentError do
|
|
package.build true, true
|
|
end
|
|
|
|
assert_equal "skip_validation = true and strict_validation = true are incompatible", e.message
|
|
end
|
|
|
|
def test_build_signed
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.cert_chain = [PUBLIC_CERT.to_pem]
|
|
spec.signing_key = PRIVATE_KEY
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
package.build
|
|
|
|
assert_equal Gem::VERSION, spec.rubygems_version
|
|
assert_path_exist spec.file_name
|
|
|
|
reader = Gem::Package.new spec.file_name
|
|
assert reader.verify
|
|
|
|
assert_equal spec, reader.spec
|
|
|
|
assert_equal %w[metadata.gz metadata.gz.sig
|
|
data.tar.gz data.tar.gz.sig
|
|
checksums.yaml.gz checksums.yaml.gz.sig],
|
|
reader.files
|
|
|
|
assert_equal %w[lib/code.rb], reader.contents
|
|
end
|
|
|
|
def test_build_signed_encrypted_key
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
spec = Gem::Specification.new 'build', '1'
|
|
spec.summary = 'build'
|
|
spec.authors = 'build'
|
|
spec.files = ['lib/code.rb']
|
|
spec.cert_chain = [PUBLIC_CERT.to_pem]
|
|
spec.signing_key = ENCRYPTED_PRIVATE_KEY
|
|
|
|
FileUtils.mkdir 'lib'
|
|
|
|
File.open 'lib/code.rb', 'w' do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
|
|
package = Gem::Package.new spec.file_name
|
|
package.spec = spec
|
|
|
|
package.build
|
|
|
|
assert_equal Gem::VERSION, spec.rubygems_version
|
|
assert_path_exist spec.file_name
|
|
|
|
reader = Gem::Package.new spec.file_name
|
|
assert reader.verify
|
|
|
|
assert_equal spec, reader.spec
|
|
|
|
assert_equal %w[metadata.gz metadata.gz.sig
|
|
data.tar.gz data.tar.gz.sig
|
|
checksums.yaml.gz checksums.yaml.gz.sig],
|
|
reader.files
|
|
|
|
assert_equal %w[lib/code.rb], reader.contents
|
|
end
|
|
|
|
def test_raw_spec
|
|
data_tgz = util_tar_gz {}
|
|
|
|
gem = util_tar do |tar|
|
|
tar.add_file 'data.tar.gz', 0644 do |io|
|
|
io.write data_tgz.string
|
|
end
|
|
|
|
tar.add_file 'metadata.gz', 0644 do |io|
|
|
Zlib::GzipWriter.wrap io do |gzio|
|
|
gzio.write @spec.to_yaml
|
|
end
|
|
end
|
|
end
|
|
|
|
gem_path = "#{@destination}/test.gem"
|
|
|
|
File.open gem_path, "wb" do |io|
|
|
io.write gem.string
|
|
end
|
|
|
|
spec, metadata = Gem::Package.raw_spec(gem_path)
|
|
|
|
assert_equal @spec, spec
|
|
assert_match @spec.to_yaml, metadata.force_encoding("UTF-8")
|
|
end
|
|
|
|
def test_contents
|
|
package = Gem::Package.new @gem
|
|
|
|
assert_equal %w[lib/code.rb], package.contents
|
|
end
|
|
|
|
def test_extract_files
|
|
package = Gem::Package.new @gem
|
|
|
|
package.extract_files @destination
|
|
|
|
extracted = File.join @destination, 'lib/code.rb'
|
|
assert_path_exist extracted
|
|
|
|
mask = 0100666 & (~File.umask)
|
|
|
|
assert_equal mask.to_s(8), File.stat(extracted).mode.to_s(8) unless
|
|
win_platform?
|
|
end
|
|
|
|
def test_extract_files_empty
|
|
data_tgz = util_tar_gz {}
|
|
|
|
gem = util_tar do |tar|
|
|
tar.add_file 'data.tar.gz', 0644 do |io|
|
|
io.write data_tgz.string
|
|
end
|
|
|
|
tar.add_file 'metadata.gz', 0644 do |io|
|
|
Zlib::GzipWriter.wrap io do |gzio|
|
|
gzio.write @spec.to_yaml
|
|
end
|
|
end
|
|
end
|
|
|
|
File.open 'empty.gem', 'wb' do |io|
|
|
io.write gem.string
|
|
end
|
|
|
|
package = Gem::Package.new 'empty.gem'
|
|
|
|
package.extract_files @destination
|
|
|
|
assert_path_exist @destination
|
|
end
|
|
|
|
def test_extract_tar_gz_absolute
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_file '/absolute.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
end
|
|
|
|
e = assert_raise Gem::Package::PathError do
|
|
package.extract_tar_gz tgz_io, @destination
|
|
end
|
|
|
|
assert_equal("installing into parent path /absolute.rb of " +
|
|
"#{@destination} is not allowed", e.message)
|
|
end
|
|
|
|
def test_extract_tar_gz_symlink_relative_path
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_file 'relative.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
|
|
tar.mkdir 'lib', 0755
|
|
tar.add_symlink 'lib/foo.rb', '../relative.rb', 0644
|
|
end
|
|
|
|
begin
|
|
package.extract_tar_gz tgz_io, @destination
|
|
rescue Errno::EACCES => e
|
|
if win_platform?
|
|
pend "symlink - must be admin with no UAC on Windows"
|
|
else
|
|
raise e
|
|
end
|
|
end
|
|
|
|
extracted = File.join @destination, 'lib/foo.rb'
|
|
assert_path_exist extracted
|
|
assert_equal '../relative.rb',
|
|
File.readlink(extracted)
|
|
assert_equal 'hi',
|
|
File.read(extracted)
|
|
end
|
|
|
|
def test_extract_symlink_parent
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.mkdir 'lib', 0755
|
|
tar.add_symlink 'lib/link', '../..', 0644
|
|
tar.add_file 'lib/link/outside.txt', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
end
|
|
|
|
# Extract into a subdirectory of @destination; if this test fails it writes
|
|
# a file outside destination_subdir, but we want the file to remain inside
|
|
# @destination so it will be cleaned up.
|
|
destination_subdir = File.join @destination, 'subdir'
|
|
FileUtils.mkdir_p destination_subdir
|
|
|
|
expected_exceptions = win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError]
|
|
|
|
e = assert_raise(*expected_exceptions) do
|
|
package.extract_tar_gz tgz_io, destination_subdir
|
|
end
|
|
|
|
pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e
|
|
|
|
assert_equal("installing symlink 'lib/link' pointing to parent path #{@destination} of " +
|
|
"#{destination_subdir} is not allowed", e.message)
|
|
|
|
assert_path_not_exist File.join(@destination, "outside.txt")
|
|
assert_path_not_exist File.join(destination_subdir, "lib/link")
|
|
end
|
|
|
|
def test_extract_symlink_parent_doesnt_delete_user_dir
|
|
package = Gem::Package.new @gem
|
|
|
|
# Extract into a subdirectory of @destination; if this test fails it writes
|
|
# a file outside destination_subdir, but we want the file to remain inside
|
|
# @destination so it will be cleaned up.
|
|
destination_subdir = File.join @destination, 'subdir'
|
|
FileUtils.mkdir_p destination_subdir
|
|
|
|
destination_user_dir = File.join @destination, 'user'
|
|
destination_user_subdir = File.join destination_user_dir, 'dir'
|
|
FileUtils.mkdir_p destination_user_subdir
|
|
|
|
pend "TMPDIR seems too long to add it as symlink into tar" if destination_user_dir.size > 90
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_symlink 'link', destination_user_dir, 16877
|
|
tar.add_symlink 'link/dir', '.', 16877
|
|
end
|
|
|
|
expected_exceptions = win_platform? ? [Gem::Package::SymlinkError, Errno::EACCES] : [Gem::Package::SymlinkError]
|
|
|
|
e = assert_raise(*expected_exceptions) do
|
|
package.extract_tar_gz tgz_io, destination_subdir
|
|
end
|
|
|
|
pend "symlink - must be admin with no UAC on Windows" if Errno::EACCES === e
|
|
|
|
assert_equal("installing symlink 'link' pointing to parent path #{destination_user_dir} of " +
|
|
"#{destination_subdir} is not allowed", e.message)
|
|
|
|
assert_path_exist destination_user_subdir
|
|
assert_path_not_exist File.join(destination_subdir, "link/dir")
|
|
assert_path_not_exist File.join(destination_subdir, "link")
|
|
end
|
|
|
|
def test_extract_tar_gz_directory
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.mkdir 'lib', 0755
|
|
tar.add_file 'lib/foo.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
tar.mkdir 'lib/foo', 0755
|
|
end
|
|
|
|
package.extract_tar_gz tgz_io, @destination
|
|
|
|
extracted = File.join @destination, 'lib/foo.rb'
|
|
assert_path_exist extracted
|
|
|
|
extracted = File.join @destination, 'lib/foo'
|
|
assert_path_exist extracted
|
|
end
|
|
|
|
def test_extract_tar_gz_dot_slash
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_file './dot_slash.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
end
|
|
|
|
package.extract_tar_gz tgz_io, @destination
|
|
|
|
extracted = File.join @destination, 'dot_slash.rb'
|
|
assert_path_exist extracted
|
|
end
|
|
|
|
def test_extract_tar_gz_dot_file
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_file '.dot_file.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
end
|
|
|
|
package.extract_tar_gz tgz_io, @destination
|
|
|
|
extracted = File.join @destination, '.dot_file.rb'
|
|
assert_path_exist extracted
|
|
end
|
|
|
|
if Gem.win_platform?
|
|
def test_extract_tar_gz_case_insensitive
|
|
package = Gem::Package.new @gem
|
|
|
|
tgz_io = util_tar_gz do |tar|
|
|
tar.add_file 'foo/file.rb', 0644 do |io|
|
|
io.write 'hi'
|
|
end
|
|
end
|
|
|
|
package.extract_tar_gz tgz_io, @destination.upcase
|
|
|
|
extracted = File.join @destination, 'foo/file.rb'
|
|
assert_path_exist extracted
|
|
end
|
|
end
|
|
|
|
def test_install_location
|
|
package = Gem::Package.new @gem
|
|
|
|
file = 'file.rb'.dup
|
|
file.taint if RUBY_VERSION < '2.7'
|
|
|
|
destination = package.install_location file, @destination
|
|
|
|
assert_equal File.join(@destination, 'file.rb'), destination
|
|
refute destination.tainted? if RUBY_VERSION < '2.7'
|
|
end
|
|
|
|
def test_install_location_absolute
|
|
package = Gem::Package.new @gem
|
|
|
|
e = assert_raise Gem::Package::PathError do
|
|
package.install_location '/absolute.rb', @destination
|
|
end
|
|
|
|
assert_equal("installing into parent path /absolute.rb of " +
|
|
"#{@destination} is not allowed", e.message)
|
|
end
|
|
|
|
def test_install_location_dots
|
|
package = Gem::Package.new @gem
|
|
|
|
file = 'file.rb'
|
|
|
|
destination = File.join @destination, 'foo', '..', 'bar'
|
|
|
|
FileUtils.mkdir_p File.join @destination, 'foo'
|
|
FileUtils.mkdir_p File.expand_path destination
|
|
|
|
destination = package.install_location file, destination
|
|
|
|
# this test only fails on ruby missing File.realpath
|
|
assert_equal File.join(@destination, 'bar', 'file.rb'), destination
|
|
end
|
|
|
|
def test_install_location_extra_slash
|
|
package = Gem::Package.new @gem
|
|
|
|
file = 'foo//file.rb'.dup
|
|
file.taint if RUBY_VERSION < '2.7'
|
|
|
|
destination = @destination.sub '/', '//'
|
|
|
|
destination = package.install_location file, destination
|
|
|
|
assert_equal File.join(@destination, 'foo', 'file.rb'), destination
|
|
refute destination.tainted? if RUBY_VERSION < '2.7'
|
|
end
|
|
|
|
def test_install_location_relative
|
|
package = Gem::Package.new @gem
|
|
|
|
e = assert_raise Gem::Package::PathError do
|
|
package.install_location '../relative.rb', @destination
|
|
end
|
|
|
|
parent = File.expand_path File.join @destination, "../relative.rb"
|
|
|
|
assert_equal("installing into parent path #{parent} of " +
|
|
"#{@destination} is not allowed", e.message)
|
|
end
|
|
|
|
def test_install_location_suffix
|
|
package = Gem::Package.new @gem
|
|
|
|
filename = "../#{File.basename(@destination)}suffix.rb"
|
|
|
|
e = assert_raise Gem::Package::PathError do
|
|
package.install_location filename, @destination
|
|
end
|
|
|
|
parent = File.expand_path File.join @destination, filename
|
|
|
|
assert_equal("installing into parent path #{parent} of " +
|
|
"#{@destination} is not allowed", e.message)
|
|
end
|
|
|
|
def test_load_spec
|
|
entry = StringIO.new Gem::Util.gzip @spec.to_yaml
|
|
def entry.full_name() 'metadata.gz' end
|
|
|
|
package = Gem::Package.new 'nonexistent.gem'
|
|
|
|
spec = package.load_spec entry
|
|
|
|
assert_equal @spec, spec
|
|
end
|
|
|
|
def test_verify
|
|
package = Gem::Package.new @gem
|
|
|
|
package.verify
|
|
|
|
assert_equal @spec, package.spec
|
|
assert_equal %w[checksums.yaml.gz data.tar.gz metadata.gz],
|
|
package.files.sort
|
|
end
|
|
|
|
def test_verify_checksum_bad
|
|
data_tgz = util_tar_gz do |tar|
|
|
tar.add_file 'lib/code.rb', 0444 do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
end
|
|
|
|
data_tgz = data_tgz.string
|
|
|
|
gem = util_tar do |tar|
|
|
metadata_gz = Gem::Util.gzip @spec.to_yaml
|
|
|
|
tar.add_file 'metadata.gz', 0444 do |io|
|
|
io.write metadata_gz
|
|
end
|
|
|
|
tar.add_file 'data.tar.gz', 0444 do |io|
|
|
io.write data_tgz
|
|
end
|
|
|
|
bogus_checksums = {
|
|
'SHA1' => {
|
|
'data.tar.gz' => 'bogus',
|
|
'metadata.gz' => 'bogus',
|
|
},
|
|
}
|
|
tar.add_file 'checksums.yaml.gz', 0444 do |io|
|
|
Zlib::GzipWriter.wrap io do |gz_io|
|
|
gz_io.write YAML.dump bogus_checksums
|
|
end
|
|
end
|
|
end
|
|
|
|
File.open 'mismatch.gem', 'wb' do |io|
|
|
io.write gem.string
|
|
end
|
|
|
|
package = Gem::Package.new 'mismatch.gem'
|
|
|
|
e = assert_raise Gem::Package::FormatError do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'SHA1 checksum mismatch for data.tar.gz in mismatch.gem',
|
|
e.message
|
|
end
|
|
|
|
def test_verify_checksum_missing
|
|
data_tgz = util_tar_gz do |tar|
|
|
tar.add_file 'lib/code.rb', 0444 do |io|
|
|
io.write '# lib/code.rb'
|
|
end
|
|
end
|
|
|
|
data_tgz = data_tgz.string
|
|
|
|
gem = util_tar do |tar|
|
|
metadata_gz = Gem::Util.gzip @spec.to_yaml
|
|
|
|
tar.add_file 'metadata.gz', 0444 do |io|
|
|
io.write metadata_gz
|
|
end
|
|
|
|
digest = Digest::SHA1.new
|
|
digest << metadata_gz
|
|
|
|
checksums = {
|
|
'SHA1' => {
|
|
'metadata.gz' => digest.hexdigest,
|
|
},
|
|
}
|
|
|
|
tar.add_file 'checksums.yaml.gz', 0444 do |io|
|
|
Zlib::GzipWriter.wrap io do |gz_io|
|
|
gz_io.write YAML.dump checksums
|
|
end
|
|
end
|
|
|
|
tar.add_file 'data.tar.gz', 0444 do |io|
|
|
io.write data_tgz
|
|
end
|
|
end
|
|
|
|
File.open 'data_checksum_missing.gem', 'wb' do |io|
|
|
io.write gem.string
|
|
end
|
|
|
|
package = Gem::Package.new 'data_checksum_missing.gem'
|
|
|
|
assert package.verify
|
|
end
|
|
|
|
def test_verify_corrupt
|
|
pend "jruby strips the null byte and does not think it's corrupt" if Gem.java_platform?
|
|
tf = Tempfile.open 'corrupt' do |io|
|
|
data = Gem::Util.gzip 'a' * 10
|
|
io.write \
|
|
tar_file_header('metadata.gz', "\000x", 0644, data.length, Time.now)
|
|
io.write data
|
|
io.rewind
|
|
|
|
package = Gem::Package.new io.path
|
|
|
|
e = assert_raise Gem::Package::FormatError do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal "tar is corrupt, name contains null byte in #{io.path}",
|
|
e.message
|
|
io
|
|
end
|
|
tf.close!
|
|
end
|
|
|
|
def test_verify_empty
|
|
FileUtils.touch 'empty.gem'
|
|
|
|
package = Gem::Package.new 'empty.gem'
|
|
|
|
e = assert_raise Gem::Package::FormatError do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'package metadata is missing in empty.gem', e.message
|
|
end
|
|
|
|
def test_verify_nonexistent
|
|
package = Gem::Package.new 'nonexistent.gem'
|
|
|
|
e = assert_raise Gem::Package::FormatError do
|
|
package.verify
|
|
end
|
|
|
|
assert_match %r{^No such file or directory}, e.message
|
|
assert_match %r{nonexistent.gem$}, e.message
|
|
end
|
|
|
|
def test_verify_duplicate_file
|
|
FileUtils.mkdir_p 'lib'
|
|
FileUtils.touch 'lib/code.rb'
|
|
|
|
build = Gem::Package.new @gem
|
|
build.spec = @spec
|
|
build.setup_signer
|
|
File.open @gem, 'wb' do |gem_io|
|
|
Gem::Package::TarWriter.new gem_io do |gem|
|
|
build.add_metadata gem
|
|
build.add_contents gem
|
|
|
|
gem.add_file_simple 'a.sig', 0444, 0
|
|
gem.add_file_simple 'a.sig', 0444, 0
|
|
end
|
|
end
|
|
|
|
package = Gem::Package.new @gem
|
|
|
|
e = assert_raise Gem::Security::Exception do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'duplicate files in the package: ("a.sig")', e.message
|
|
end
|
|
|
|
def test_verify_security_policy
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
package = Gem::Package.new @gem
|
|
package.security_policy = Gem::Security::HighSecurity
|
|
|
|
e = assert_raise Gem::Security::Exception do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'unsigned gems are not allowed by the High Security policy',
|
|
e.message
|
|
|
|
refute package.instance_variable_get(:@spec), '@spec must not be loaded'
|
|
assert_empty package.instance_variable_get(:@files), '@files must empty'
|
|
end
|
|
|
|
def test_verify_security_policy_low_security
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
@spec.cert_chain = [PUBLIC_CERT.to_pem]
|
|
@spec.signing_key = PRIVATE_KEY
|
|
|
|
FileUtils.mkdir_p 'lib'
|
|
FileUtils.touch 'lib/code.rb'
|
|
|
|
build = Gem::Package.new @gem
|
|
build.spec = @spec
|
|
|
|
build.build
|
|
|
|
package = Gem::Package.new @gem
|
|
package.security_policy = Gem::Security::LowSecurity
|
|
|
|
assert package.verify
|
|
end
|
|
|
|
def test_verify_security_policy_checksum_missing
|
|
pend 'openssl is missing' unless Gem::HAVE_OPENSSL
|
|
|
|
@spec.cert_chain = [PUBLIC_CERT.to_pem]
|
|
@spec.signing_key = PRIVATE_KEY
|
|
|
|
build = Gem::Package.new @gem
|
|
build.spec = @spec
|
|
build.setup_signer
|
|
|
|
FileUtils.mkdir 'lib'
|
|
FileUtils.touch 'lib/code.rb'
|
|
|
|
File.open @gem, 'wb' do |gem_io|
|
|
Gem::Package::TarWriter.new gem_io do |gem|
|
|
build.add_metadata gem
|
|
build.add_contents gem
|
|
|
|
# write bogus data.tar.gz to foil signature
|
|
bogus_data = Gem::Util.gzip 'hello'
|
|
fake_signer = Class.new do
|
|
def digest_name; 'SHA512'; end
|
|
def digest_algorithm; Digest(:SHA512).new; end
|
|
def key; 'key'; end
|
|
def sign(*); 'fake_sig'; end
|
|
end
|
|
gem.add_file_signed 'data2.tar.gz', 0444, fake_signer.new do |io|
|
|
io.write bogus_data
|
|
end
|
|
|
|
# pre rubygems 2.0 gems do not add checksums
|
|
end
|
|
end
|
|
|
|
Gem::Security.trust_dir.trust_cert PUBLIC_CERT
|
|
|
|
package = Gem::Package.new @gem
|
|
package.security_policy = Gem::Security::HighSecurity
|
|
|
|
e = assert_raise Gem::Security::Exception do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'invalid signature', e.message
|
|
|
|
refute package.instance_variable_get(:@spec), '@spec must not be loaded'
|
|
assert_empty package.instance_variable_get(:@files), '@files must empty'
|
|
end
|
|
|
|
def test_verify_truncate
|
|
File.open 'bad.gem', 'wb' do |io|
|
|
io.write File.read(@gem, 1024) # don't care about newlines
|
|
end
|
|
|
|
package = Gem::Package.new 'bad.gem'
|
|
|
|
e = assert_raise Gem::Package::FormatError do
|
|
package.verify
|
|
end
|
|
|
|
assert_equal 'package content (data.tar.gz) is missing in bad.gem',
|
|
e.message
|
|
end
|
|
|
|
# end #verify tests
|
|
|
|
def test_verify_entry
|
|
entry = Object.new
|
|
def entry.full_name() raise ArgumentError, 'whatever' end
|
|
|
|
package = Gem::Package.new @gem
|
|
|
|
_, err = use_ui @ui do
|
|
e = nil
|
|
|
|
out_err = capture_output do
|
|
e = assert_raise ArgumentError do
|
|
package.verify_entry entry
|
|
end
|
|
end
|
|
|
|
assert_equal "whatever", e.message
|
|
assert_equal "full_name", e.backtrace_locations.first.label
|
|
|
|
out_err
|
|
end
|
|
|
|
assert_equal "Exception while verifying #{@gem}\n", err
|
|
|
|
valid_metadata = ["metadata", "metadata.gz"]
|
|
valid_metadata.each do |vm|
|
|
$spec_loaded = false
|
|
$good_name = vm
|
|
|
|
entry = Object.new
|
|
def entry.full_name() $good_name end
|
|
|
|
package = Gem::Package.new(@gem)
|
|
package.instance_variable_set(:@files, [])
|
|
def package.load_spec(entry) $spec_loaded = true end
|
|
|
|
package.verify_entry(entry)
|
|
|
|
assert $spec_loaded
|
|
end
|
|
|
|
invalid_metadata = ["metadataxgz", "foobar\nmetadata", "metadata\nfoobar"]
|
|
invalid_metadata.each do |vm|
|
|
$spec_loaded = false
|
|
$bad_name = vm
|
|
|
|
entry = Object.new
|
|
def entry.full_name() $bad_name end
|
|
|
|
package = Gem::Package.new(@gem)
|
|
package.instance_variable_set(:@files, [])
|
|
def package.load_spec(entry) $spec_loaded = true end
|
|
|
|
package.verify_entry(entry)
|
|
|
|
refute $spec_loaded
|
|
end
|
|
end
|
|
|
|
def test_spec
|
|
package = Gem::Package.new @gem
|
|
|
|
assert_equal @spec, package.spec
|
|
end
|
|
|
|
def test_gem_attr
|
|
package = Gem::Package.new(@gem)
|
|
assert_equal(@gem, package.gem.path)
|
|
end
|
|
|
|
def test_spec_from_io
|
|
# This functionality is used by rubygems.org to extract spec data from an
|
|
# uploaded gem before it is written to storage.
|
|
io = StringIO.new Gem.read_binary @gem
|
|
package = Gem::Package.new io
|
|
|
|
assert_equal @spec, package.spec
|
|
end
|
|
|
|
def test_spec_from_io_raises_gem_error_for_io_not_at_start
|
|
io = StringIO.new Gem.read_binary @gem
|
|
io.read(1)
|
|
assert_raise(Gem::Package::Error) do
|
|
Gem::Package.new io
|
|
end
|
|
end
|
|
|
|
def test_contents_from_io
|
|
io = StringIO.new Gem.read_binary @gem
|
|
package = Gem::Package.new io
|
|
|
|
assert_equal %w[lib/code.rb], package.contents
|
|
end
|
|
|
|
def util_tar
|
|
tar_io = StringIO.new
|
|
|
|
Gem::Package::TarWriter.new tar_io do |tar|
|
|
yield tar
|
|
end
|
|
|
|
tar_io.rewind
|
|
|
|
tar_io
|
|
end
|
|
|
|
def util_tar_gz(&block)
|
|
tar_io = util_tar(&block)
|
|
|
|
tgz_io = StringIO.new
|
|
|
|
# can't wrap TarWriter because it seeks
|
|
Zlib::GzipWriter.wrap tgz_io do |io|
|
|
io.write tar_io.string
|
|
end
|
|
|
|
StringIO.new tgz_io.string
|
|
end
|
|
end
|