Skip to content

Commit 2c07a19

Browse files
tenderlovematzbot
authored andcommitted
[ruby/rubygems] Make install_location private and hoist fn expansion
`install_location` is a hot path during gem installation. I want to make the method private so that we can freely refactor it. This commit makes it private and ports the tests to use integration tests instead of directly calling the method. Before this commit, `install_location` would repeatedly call `File.realpath` on the destination directory when `extract_tar_gz` had already done that work. ruby/rubygems@c15a559007
1 parent 8da752c commit 2c07a19

3 files changed

Lines changed: 74 additions & 50 deletions

File tree

lib/rubygems/package.rb

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -510,24 +510,6 @@ def gzip_to(io) # :yields: gz_io
510510
gz_io.close
511511
end
512512

513-
##
514-
# Returns the full path for installing +filename+.
515-
#
516-
# If +filename+ is not inside +destination_dir+ an exception is raised.
517-
518-
def install_location(filename, destination_dir) # :nodoc:
519-
raise Gem::Package::PathError.new(filename, destination_dir) if
520-
filename.start_with? "/"
521-
522-
destination_dir = File.realpath(destination_dir)
523-
destination = File.expand_path(filename, destination_dir)
524-
525-
raise Gem::Package::PathError.new(destination, destination_dir) unless
526-
normalize_path(destination).start_with? normalize_path(destination_dir + "/")
527-
528-
destination
529-
end
530-
531513
if Gem.win_platform?
532514
def normalize_path(pathname) # :nodoc:
533515
pathname.downcase
@@ -662,6 +644,24 @@ def verify
662644

663645
private
664646

647+
##
648+
# Returns the full path for installing +filename+ into +destination_dir+,
649+
# which must already be resolved with File.realpath by the caller.
650+
#
651+
# If +filename+ is not inside +destination_dir+ an exception is raised.
652+
653+
def install_location(filename, destination_dir) # :nodoc:
654+
raise Gem::Package::PathError.new(filename, destination_dir) if
655+
filename.start_with? "/"
656+
657+
destination = File.expand_path(filename, destination_dir)
658+
659+
raise Gem::Package::PathError.new(destination, destination_dir) unless
660+
normalize_path(destination).start_with? normalize_path(destination_dir + "/")
661+
662+
destination
663+
end
664+
665665
##
666666
# Verifies the +checksums+ against the +digests+. This check is not
667667
# cryptographically secure. Missing checksums are ignored.

lib/rubygems/package/old.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def extract_files(destination_dir)
5959
header = file_list io
6060
raise Gem::Exception, errstr unless header
6161

62+
# install_location expects an already-resolved destination dir
63+
destination_dir = File.realpath(destination_dir)
64+
6265
header.each do |entry|
6366
full_name = entry["path"]
6467

test/rubygems/test_gem_package.rb

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -835,79 +835,100 @@ def test_extract_tar_gz_case_insensitive
835835
end
836836
end
837837

838-
def test_install_location
839-
package = Gem::Package.new @gem
840-
841-
file = "file.rb".dup
842-
843-
destination = package.install_location file, @destination
844-
845-
assert_equal File.join(@destination, "file.rb"), destination
846-
end
838+
# The following tests exercise install_location's path resolution and
839+
# traversal protection through the real extraction path (extract_tar_gz)
840+
# rather than calling the private helper directly. The absolute-path case is
841+
# already covered by test_extract_tar_gz_absolute.
847842

848-
def test_install_location_absolute
843+
def test_extract_tar_gz_basic_file
849844
package = Gem::Package.new @gem
850845

851-
e = assert_raise Gem::Package::PathError do
852-
package.install_location "/absolute.rb", @destination
846+
tgz_io = util_tar_gz do |tar|
847+
tar.add_file "file.rb", 0o644 do |io|
848+
io.write "hi"
849+
end
853850
end
854851

855-
assert_equal("installing into parent path /absolute.rb of " \
856-
"#{@destination} is not allowed", e.message)
852+
package.extract_tar_gz tgz_io, @destination
853+
854+
extracted = File.join @destination, "file.rb"
855+
assert_path_exist extracted
856+
assert_equal "hi", File.read(extracted)
857857
end
858858

859-
def test_install_location_dots
859+
def test_extract_tar_gz_collapses_parent_dots
860860
package = Gem::Package.new @gem
861861

862-
file = "file.rb"
863-
864-
destination = File.join @destination, "foo", "..", "bar"
865-
866-
FileUtils.mkdir_p File.join @destination, "foo"
867-
FileUtils.mkdir_p File.expand_path destination
862+
tgz_io = util_tar_gz do |tar|
863+
tar.add_file "foo/../bar/file.rb", 0o644 do |io|
864+
io.write "hi"
865+
end
866+
end
868867

869-
destination = package.install_location file, destination
868+
package.extract_tar_gz tgz_io, @destination
870869

871-
# this test only fails on ruby missing File.realpath
872-
assert_equal File.join(@destination, "bar", "file.rb"), destination
870+
extracted = File.join @destination, "bar", "file.rb"
871+
assert_path_exist extracted
872+
assert_equal "hi", File.read(extracted)
873+
assert_path_not_exist File.join(@destination, "foo")
873874
end
874875

875-
def test_install_location_extra_slash
876+
def test_extract_tar_gz_collapses_extra_slash
876877
package = Gem::Package.new @gem
877878

878-
file = "foo//file.rb".dup
879+
tgz_io = util_tar_gz do |tar|
880+
tar.add_file "foo//file.rb", 0o644 do |io|
881+
io.write "hi"
882+
end
883+
end
879884

880-
destination = package.install_location file, @destination
885+
package.extract_tar_gz tgz_io, @destination
881886

882-
assert_equal File.join(@destination, "foo", "file.rb"), destination
887+
extracted = File.join @destination, "foo", "file.rb"
888+
assert_path_exist extracted
889+
assert_equal "hi", File.read(extracted)
883890
end
884891

885-
def test_install_location_relative
892+
def test_extract_tar_gz_rejects_relative_escape
886893
package = Gem::Package.new @gem
887894

895+
tgz_io = util_tar_gz do |tar|
896+
tar.add_file "../relative.rb", 0o644 do |io|
897+
io.write "hi"
898+
end
899+
end
900+
888901
e = assert_raise Gem::Package::PathError do
889-
package.install_location "../relative.rb", @destination
902+
package.extract_tar_gz tgz_io, @destination
890903
end
891904

892905
parent = File.expand_path File.join @destination, "../relative.rb"
893906

894907
assert_equal("installing into parent path #{parent} of " \
895908
"#{@destination} is not allowed", e.message)
909+
assert_path_not_exist parent
896910
end
897911

898-
def test_install_location_suffix
912+
def test_extract_tar_gz_rejects_suffix_escape
899913
package = Gem::Package.new @gem
900914

901915
filename = "../#{File.basename(@destination)}suffix.rb"
902916

917+
tgz_io = util_tar_gz do |tar|
918+
tar.add_file filename, 0o644 do |io|
919+
io.write "hi"
920+
end
921+
end
922+
903923
e = assert_raise Gem::Package::PathError do
904-
package.install_location filename, @destination
924+
package.extract_tar_gz tgz_io, @destination
905925
end
906926

907927
parent = File.expand_path File.join @destination, filename
908928

909929
assert_equal("installing into parent path #{parent} of " \
910930
"#{@destination} is not allowed", e.message)
931+
assert_path_not_exist parent
911932
end
912933

913934
def test_load_spec_from_metadata

0 commit comments

Comments
 (0)