diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index e6e078dce406..0d05bc962e69 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 InvalidWindowsFileNameError < 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? @@ -425,6 +441,11 @@ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: full_name = entry.full_name next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH + if Gem.win_platform? && invalid_windows_filename?(full_name) + gem_name = @spec ? @spec.full_name : "unknown" + raise Gem::Package::InvalidWindowsFileNameError.new(full_name, gem_name) + end + destination = install_location full_name, destination_dir if entry.symlink? @@ -529,6 +550,16 @@ 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: + filename.to_s.split("/").any? { |part| part.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 0c214a232b76..8027f09a315b 100644 --- a/test/rubygems/test_gem_package.rb +++ b/test/rubygems/test_gem_package.rb @@ -1303,4 +1303,75 @@ 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