Skip to content

Commit 19debfb

Browse files
authored
Merge pull request #9055 from zirni/fix-gem-sources-trailing-slash
fix: Ensure trailing slash is added to source URIs added via gem sources
2 parents fd31044 + da8d622 commit 19debfb

2 files changed

Lines changed: 164 additions & 33 deletions

File tree

lib/rubygems/commands/sources_command.rb

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ def initialize
5050
end
5151

5252
def add_source(source_uri) # :nodoc:
53-
check_rubygems_https source_uri
54-
55-
source = Gem::Source.new source_uri
56-
57-
check_typo_squatting(source)
53+
source = build_new_source(source_uri)
54+
source_uri = source.uri.to_s
5855

5956
begin
6057
if Gem.sources.include? source
@@ -76,11 +73,8 @@ def add_source(source_uri) # :nodoc:
7673
end
7774

7875
def append_source(source_uri) # :nodoc:
79-
check_rubygems_https source_uri
80-
81-
source = Gem::Source.new source_uri
82-
83-
check_typo_squatting(source)
76+
source = build_new_source(source_uri)
77+
source_uri = source.uri.to_s
8478

8579
begin
8680
source.load_specs :released
@@ -103,11 +97,8 @@ def append_source(source_uri) # :nodoc:
10397
end
10498

10599
def prepend_source(source_uri) # :nodoc:
106-
check_rubygems_https source_uri
107-
108-
source = Gem::Source.new source_uri
109-
110-
check_typo_squatting(source)
100+
source = build_new_source(source_uri)
101+
source_uri = source.uri.to_s
111102

112103
begin
113104
source.load_specs :released
@@ -141,6 +132,19 @@ def check_typo_squatting(source)
141132
end
142133
end
143134

135+
def normalize_source_uri(source_uri) # :nodoc:
136+
# Ensure the source URI has a trailing slash for proper RFC 2396 path merging
137+
# Without a trailing slash, the last path segment is treated as a file and removed
138+
# during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem")
139+
# With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem")
140+
uri = Gem::URI.parse(source_uri)
141+
uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty?
142+
uri.to_s
143+
rescue Gem::URI::Error
144+
# If parsing fails, return the original URI and let later validation handle it
145+
source_uri
146+
end
147+
144148
def check_rubygems_https(source_uri) # :nodoc:
145149
uri = Gem::URI source_uri
146150

@@ -273,7 +277,8 @@ def execute
273277
end
274278

275279
def remove_source(source_uri) # :nodoc:
276-
source = Gem::Source.new source_uri
280+
source = build_source(source_uri)
281+
source_uri = source.uri.to_s
277282

278283
if configured_sources&.include? source
279284
Gem.sources.delete source
@@ -328,4 +333,16 @@ def configured_sources
328333
def config_file_name
329334
Gem.configuration.config_file_name
330335
end
336+
337+
def build_source(source_uri)
338+
source_uri = normalize_source_uri(source_uri)
339+
Gem::Source.new(source_uri)
340+
end
341+
342+
def build_new_source(source_uri)
343+
source = build_source(source_uri)
344+
check_rubygems_https(source.uri.to_s)
345+
check_typo_squatting(source)
346+
source
347+
end
331348
end

test/rubygems/test_gem_commands_sources_command.rb

Lines changed: 131 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,82 @@ def test_execute_add
6060
assert_equal "", @ui.error
6161
end
6262

63+
def test_execute_add_without_trailing_slash
64+
setup_fake_source("https://rubygems.pkg.github.com/my-org")
65+
66+
@cmd.handle_options %W[--add https://rubygems.pkg.github.com/my-org]
67+
68+
use_ui @ui do
69+
@cmd.execute
70+
end
71+
72+
assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources
73+
74+
expected = <<-EOF
75+
https://rubygems.pkg.github.com/my-org/ added to sources
76+
EOF
77+
78+
assert_equal expected, @ui.output
79+
assert_equal "", @ui.error
80+
end
81+
82+
def test_execute_add_multiple_trailing_slash
83+
setup_fake_source("https://rubygems.pkg.github.com/my-org/")
84+
85+
@cmd.handle_options %W[--add https://rubygems.pkg.github.com/my-org///]
86+
87+
use_ui @ui do
88+
@cmd.execute
89+
end
90+
91+
assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources
92+
93+
expected = <<-EOF
94+
https://rubygems.pkg.github.com/my-org/ added to sources
95+
EOF
96+
97+
assert_equal expected, @ui.output
98+
assert_equal "", @ui.error
99+
end
100+
101+
def test_execute_append_without_trailing_slash
102+
setup_fake_source("https://rubygems.pkg.github.com/my-org")
103+
104+
@cmd.handle_options %W[--append https://rubygems.pkg.github.com/my-org]
105+
106+
use_ui @ui do
107+
@cmd.execute
108+
end
109+
110+
assert_equal [@gem_repo, "https://rubygems.pkg.github.com/my-org/"], Gem.sources
111+
112+
expected = <<-EOF
113+
https://rubygems.pkg.github.com/my-org/ added to sources
114+
EOF
115+
116+
assert_equal expected, @ui.output
117+
assert_equal "", @ui.error
118+
end
119+
120+
def test_execute_prepend_without_trailing_slash
121+
setup_fake_source("https://rubygems.pkg.github.com/my-org")
122+
123+
@cmd.handle_options %W[--prepend https://rubygems.pkg.github.com/my-org]
124+
125+
use_ui @ui do
126+
@cmd.execute
127+
end
128+
129+
assert_equal ["https://rubygems.pkg.github.com/my-org/", @gem_repo], Gem.sources
130+
131+
expected = <<-EOF
132+
https://rubygems.pkg.github.com/my-org/ added to sources
133+
EOF
134+
135+
assert_equal expected, @ui.output
136+
assert_equal "", @ui.error
137+
end
138+
63139
def test_execute_append
64140
setup_fake_source(@new_repo)
65141

@@ -530,17 +606,14 @@ def test_execute_add_https_rubygems_org
530606

531607
@cmd.handle_options %W[--add #{https_rubygems_org}]
532608

533-
ui = Gem::MockGemUi.new "n"
534-
535-
use_ui ui do
536-
assert_raise Gem::MockGemUi::TermError do
537-
@cmd.execute
538-
end
609+
use_ui @ui do
610+
@cmd.execute
539611
end
540612

541-
assert_equal [@gem_repo], Gem.sources
613+
assert_equal [@gem_repo, https_rubygems_org], Gem.sources
542614

543615
expected = <<-EXPECTED
616+
#{https_rubygems_org} added to sources
544617
EXPECTED
545618

546619
assert_equal expected, @ui.output
@@ -554,17 +627,14 @@ def test_execute_append_https_rubygems_org
554627

555628
@cmd.handle_options %W[--append #{https_rubygems_org}]
556629

557-
ui = Gem::MockGemUi.new "n"
558-
559-
use_ui ui do
560-
assert_raise Gem::MockGemUi::TermError do
561-
@cmd.execute
562-
end
630+
use_ui @ui do
631+
@cmd.execute
563632
end
564633

565-
assert_equal [@gem_repo], Gem.sources
634+
assert_equal [@gem_repo, https_rubygems_org], Gem.sources
566635

567636
expected = <<-EXPECTED
637+
#{https_rubygems_org} added to sources
568638
EXPECTED
569639

570640
assert_equal expected, @ui.output
@@ -583,7 +653,7 @@ def test_execute_add_bad_uri
583653
assert_equal [@gem_repo], Gem.sources
584654

585655
expected = <<-EOF
586-
beta-gems.example.com is not a URI
656+
beta-gems.example.com/ is not a URI
587657
EOF
588658

589659
assert_equal expected, @ui.output
@@ -602,7 +672,26 @@ def test_execute_append_bad_uri
602672
assert_equal [@gem_repo], Gem.sources
603673

604674
expected = <<-EOF
605-
beta-gems.example.com is not a URI
675+
beta-gems.example.com/ is not a URI
676+
EOF
677+
678+
assert_equal expected, @ui.output
679+
assert_equal "", @ui.error
680+
end
681+
682+
def test_execute_prepend_bad_uri
683+
@cmd.handle_options %w[--prepend beta-gems.example.com]
684+
685+
use_ui @ui do
686+
assert_raise Gem::MockGemUi::TermError do
687+
@cmd.execute
688+
end
689+
end
690+
691+
assert_equal [@gem_repo], Gem.sources
692+
693+
expected = <<-EOF
694+
beta-gems.example.com/ is not a URI
606695
EOF
607696

608697
assert_equal expected, @ui.output
@@ -778,6 +867,31 @@ def test_execute_remove_redundant_source_trailing_slash
778867
Gem.configuration.sources = nil
779868
end
780869

870+
def test_execute_remove_without_trailing_slash
871+
source_uri = "https://rubygems.pkg.github.com/my-org/"
872+
873+
Gem.configuration.sources = [source_uri]
874+
875+
setup_fake_source(source_uri)
876+
877+
@cmd.handle_options %W[--remove https://rubygems.pkg.github.com/my-org]
878+
879+
use_ui @ui do
880+
@cmd.execute
881+
end
882+
883+
assert_equal [], Gem.sources
884+
885+
expected = <<-EOF
886+
#{source_uri} removed from sources
887+
EOF
888+
889+
assert_equal expected, @ui.output
890+
assert_equal "", @ui.error
891+
ensure
892+
Gem.configuration.sources = nil
893+
end
894+
781895
def test_execute_update
782896
@cmd.handle_options %w[--update]
783897

@@ -888,6 +1002,6 @@ def setup_fake_source(uri)
8881002
Marshal.dump specs, io
8891003
end
8901004

891-
@fetcher.data["#{uri}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string
1005+
@fetcher.data["#{uri.chomp("/")}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string
8921006
end
8931007
end

0 commit comments

Comments
 (0)