Skip to content

Commit bbb7488

Browse files
committed
Improve handling of paths with invalid Windows characters
Add validation and clear error messages for gems with filenames containing invalid Windows characters (e.g., colons), directing users to report issues to gem authors instead of RubyGems.
1 parent e88e69d commit bbb7488

File tree

2 files changed

+86
-7
lines changed

2 files changed

+86
-7
lines changed

lib/rubygems/package.rb

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,18 @@ class TooLongFileName < Error; end
8989

9090
class TarInvalidError < Error; end
9191

92+
##
93+
# Raised when a filename contains characters that are invalid on Windows
94+
95+
class InvalidFileNameError < Error
96+
def initialize(filename, gem_name = nil)
97+
message = "The gem contains a file '#{filename}' with characters in its name that are not allowed on Windows (e.g., colons)."
98+
message += " This is a problem with the '#{gem_name}' gem, not Rubygems." if gem_name
99+
message += " Please report this issue to the gem author."
100+
super message
101+
end
102+
end
103+
92104
attr_accessor :build_time # :nodoc:
93105

94106
##
@@ -258,6 +270,10 @@ def add_contents(tar) # :nodoc:
258270

259271
def add_files(tar) # :nodoc:
260272
@spec.files.each do |file|
273+
if invalid_windows_filename?(file)
274+
alert_warning "filename '#{file}' contains characters that are invalid on Windows (e.g., colons). This gem may fail to install on Windows."
275+
end
276+
261277
stat = File.lstat file
262278

263279
if stat.symlink?
@@ -427,6 +443,11 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
427443

428444
destination = install_location full_name, destination_dir
429445

446+
if invalid_windows_filename?(full_name)
447+
gem_name = @spec ? @spec.full_name : "unknown"
448+
raise Gem::Package::InvalidFileNameError.new(full_name, gem_name)
449+
end
450+
430451
if entry.symlink?
431452
link_target = entry.header.linkname
432453
real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination))
@@ -450,13 +471,18 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
450471
end
451472

452473
if entry.file?
453-
File.open(destination, "wb") do |out|
454-
copy_stream(tar.io, out, entry.size)
455-
# Flush needs to happen before chmod because there could be data
456-
# in the IO buffer that needs to be written, and that could be
457-
# written after the chmod (on close) which would mess up the perms
458-
out.flush
459-
out.chmod file_mode(entry.header.mode) & ~File.umask
474+
begin
475+
File.open(destination, "wb") do |out|
476+
copy_stream(tar.io, out, entry.size)
477+
# Flush needs to happen before chmod because there could be data
478+
# in the IO buffer that needs to be written, and that could be
479+
# written after the chmod (on close) which would mess up the perms
480+
out.flush
481+
out.chmod file_mode(entry.header.mode) & ~File.umask
482+
end
483+
rescue Errno::EINVAL => e
484+
gem_name = @spec ? @spec.full_name : "unknown"
485+
raise Gem::Package::InvalidFileNameError.new(full_name, gem_name), e.message
460486
end
461487
end
462488

@@ -529,6 +555,19 @@ def normalize_path(pathname) # :nodoc:
529555
end
530556
end
531557

558+
##
559+
# Checks if a filename contains characters that are invalid on Windows.
560+
# Windows doesn't allow: < > : " | ? * \ and control characters (0x00-0x1F).
561+
# Colons are the most common issue since they're allowed on Unix.
562+
# Note: Colons are only valid as drive letter separators (e.g., C:), not in filenames.
563+
564+
def invalid_windows_filename?(filename) # :nodoc:
565+
return false unless Gem.win_platform?
566+
567+
basename = File.basename(filename)
568+
basename.match?(/[:<>"|?*\\\x00-\x1f]/)
569+
end
570+
532571
##
533572
# Loads a Gem::Specification from the TarEntry +entry+
534573

test/rubygems/test_gem_package.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,4 +1303,44 @@ def test_contents_from_io
13031303

13041304
assert_equal %w[lib/code.rb], package.contents
13051305
end
1306+
1307+
def test_invalid_windows_filename
1308+
package = Gem::Package.new @gem
1309+
1310+
if Gem.win_platform?
1311+
assert package.invalid_windows_filename?("spec/internal/:memory")
1312+
assert package.invalid_windows_filename?("file:name.rb")
1313+
assert package.invalid_windows_filename?("file<name.rb")
1314+
assert package.invalid_windows_filename?('file"name.rb')
1315+
end
1316+
end
1317+
1318+
def test_invalid_file_name_error_message
1319+
error = Gem::Package::InvalidFileNameError.new("spec/internal/:memory", "crono-2.0.1")
1320+
assert_match(/The gem contains a file 'spec\/internal\/:memory'/, error.message)
1321+
assert_match(/characters in its name that are not allowed on Windows/, error.message)
1322+
assert_match(/This is a problem with the 'crono-2.0.1' gem, not Rubygems/, error.message)
1323+
assert_match(/Please report this issue to the gem author/, error.message)
1324+
end
1325+
1326+
def test_extract_tar_gz_invalid_filename
1327+
pend "Windows filename validation only applies on Windows" unless Gem.win_platform?
1328+
1329+
package = Gem::Package.new @gem
1330+
package.verify
1331+
1332+
tgz_io = util_tar_gz do |tar|
1333+
tar.add_file "spec/internal/:memory", 0o644 do |io|
1334+
io.write "test content"
1335+
end
1336+
end
1337+
1338+
e = assert_raise Gem::Package::InvalidFileNameError do
1339+
package.extract_tar_gz tgz_io, @destination
1340+
end
1341+
1342+
assert_match(/The gem contains a file 'spec\/internal\/:memory'/, e.message)
1343+
assert_match(/characters in its name that are not allowed on Windows/, e.message)
1344+
assert_match(/This is a problem with the 'a-2' gem, not Rubygems/, e.message)
1345+
end
13061346
end

0 commit comments

Comments
 (0)