From bbb7488d30dbee8539f8901d83b57a09231e9bd7 Mon Sep 17 00:00:00 2001 From: Said Kaldybaev Date: Wed, 28 Jan 2026 23:08:27 -0800 Subject: [PATCH] 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. --- lib/rubygems/package.rb | 53 +++++++++++++++++++++++++++---- test/rubygems/test_gem_package.rb | 40 +++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 6b21ff1b9533..63a213a8e9a0 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -89,6 +89,18 @@ class TooLongFileName < Error; end class TarInvalidError < Error; end + ## + # Raised when a filename contains characters that are invalid on Windows + + class InvalidFileNameError < Error + def initialize(filename, gem_name = nil) + message = "The gem contains a file '#{filename}' with characters in its name that are not allowed on Windows (e.g., colons)." + message += " This is a problem with the '#{gem_name}' gem, not Rubygems." if gem_name + message += " Please report this issue to the gem author." + super message + end + end + attr_accessor :build_time # :nodoc: ## @@ -258,6 +270,10 @@ def add_contents(tar) # :nodoc: def add_files(tar) # :nodoc: @spec.files.each do |file| + if invalid_windows_filename?(file) + alert_warning "filename '#{file}' contains characters that are invalid on Windows (e.g., colons). This gem may fail to install on Windows." + end + stat = File.lstat file if stat.symlink? @@ -427,6 +443,11 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: destination = install_location full_name, destination_dir + if invalid_windows_filename?(full_name) + gem_name = @spec ? @spec.full_name : "unknown" + raise Gem::Package::InvalidFileNameError.new(full_name, gem_name) + end + if entry.symlink? link_target = entry.header.linkname 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: end if entry.file? - File.open(destination, "wb") do |out| - copy_stream(tar.io, out, entry.size) - # Flush needs to happen before chmod because there could be data - # in the IO buffer that needs to be written, and that could be - # written after the chmod (on close) which would mess up the perms - out.flush - out.chmod file_mode(entry.header.mode) & ~File.umask + begin + File.open(destination, "wb") do |out| + copy_stream(tar.io, out, entry.size) + # Flush needs to happen before chmod because there could be data + # in the IO buffer that needs to be written, and that could be + # written after the chmod (on close) which would mess up the perms + out.flush + out.chmod file_mode(entry.header.mode) & ~File.umask + end + rescue Errno::EINVAL => e + gem_name = @spec ? @spec.full_name : "unknown" + raise Gem::Package::InvalidFileNameError.new(full_name, gem_name), e.message end end @@ -529,6 +555,19 @@ def normalize_path(pathname) # :nodoc: end end + ## + # Checks if a filename contains characters that are invalid on Windows. + # Windows doesn't allow: < > : " | ? * \ and control characters (0x00-0x1F). + # Colons are the most common issue since they're allowed on Unix. + # Note: Colons are only valid as drive letter separators (e.g., C:), not in filenames. + + def invalid_windows_filename?(filename) # :nodoc: + return false unless Gem.win_platform? + + basename = File.basename(filename) + basename.match?(/[:<>"|?*\\\x00-\x1f]/) + end + ## # Loads a Gem::Specification from the TarEntry +entry+ diff --git a/test/rubygems/test_gem_package.rb b/test/rubygems/test_gem_package.rb index 2ad63acd03bb..8364f2d14ad9 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -1303,4 +1303,44 @@ def test_contents_from_io assert_equal %w[lib/code.rb], package.contents end + + def test_invalid_windows_filename + package = Gem::Package.new @gem + + if Gem.win_platform? + assert package.invalid_windows_filename?("spec/internal/:memory") + assert package.invalid_windows_filename?("file:name.rb") + assert package.invalid_windows_filename?("file