From 82cc2843a92b286cc13afd0860a4e111d4ea2a0b Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Fri, 13 Dec 2019 20:19:08 +0900 Subject: [PATCH] Prepare to release RubyGems 3.1.0 final version. --- lib/rubygems.rb | 18 +---- lib/rubygems/command.rb | 34 +++++++-- .../commands/generate_index_command.rb | 3 + lib/rubygems/commands/setup_command.rb | 2 +- lib/rubygems/commands/sources_command.rb | 14 ++++ lib/rubygems/core_ext/kernel_warn.rb | 16 ++-- lib/rubygems/ext/builder.rb | 2 +- lib/rubygems/remote_fetcher.rb | 51 +++++-------- lib/rubygems/request.rb | 2 + lib/rubygems/source.rb | 8 +- lib/rubygems/specification_policy.rb | 73 ++++++++++-------- lib/rubygems/uri_formatter.rb | 1 - lib/rubygems/uri_parser.rb | 36 +++++++++ lib/rubygems/uri_parsing.rb | 23 ++++++ test/rubygems/test_gem_command.rb | 47 +++++++++--- ...est_gem_commands_generate_index_command.rb | 38 +++++++++- .../test_gem_commands_help_command.rb | 7 +- .../test_gem_commands_sources_command.rb | 74 +++++++++++++++++++ test/rubygems/test_gem_gem_runner.rb | 3 +- test/rubygems/test_gem_indexer.rb | 2 +- test/rubygems/test_gem_source.rb | 14 ++++ test/rubygems/test_remote_fetch_error.rb | 2 +- test/rubygems/test_require.rb | 65 ++++++++-------- 23 files changed, 393 insertions(+), 142 deletions(-) create mode 100644 lib/rubygems/uri_parser.rb create mode 100644 lib/rubygems/uri_parsing.rb diff --git a/lib/rubygems.rb b/lib/rubygems.rb index 474734bd40..6be55a0379 100644 --- a/lib/rubygems.rb +++ b/lib/rubygems.rb @@ -160,24 +160,14 @@ module Gem ].freeze ## - # Exception classes used in a Gem.read_binary +rescue+ statement. Not all of - # these are defined in Ruby 1.8.7, hence the need for this convoluted setup. + # Exception classes used in a Gem.read_binary +rescue+ statement - READ_BINARY_ERRORS = begin - read_binary_errors = [Errno::EACCES, Errno::EROFS, Errno::ENOSYS] - read_binary_errors << Errno::ENOTSUP if Errno.const_defined?(:ENOTSUP) - read_binary_errors - end.freeze + READ_BINARY_ERRORS = [Errno::EACCES, Errno::EROFS, Errno::ENOSYS, Errno::ENOTSUP].freeze ## - # Exception classes used in Gem.write_binary +rescue+ statement. Not all of - # these are defined in Ruby 1.8.7. + # Exception classes used in Gem.write_binary +rescue+ statement - WRITE_BINARY_ERRORS = begin - write_binary_errors = [Errno::ENOSYS] - write_binary_errors << Errno::ENOTSUP if Errno.const_defined?(:ENOTSUP) - write_binary_errors - end.freeze + WRITE_BINARY_ERRORS = [Errno::ENOSYS, Errno::ENOTSUP].freeze @@win_platform = nil diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index ab683f0049..c1e5e13c5a 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -369,22 +369,44 @@ class Gem::Command end end - def deprecate_option(short_name: nil, long_name: nil, version: nil) - @deprecated_options[command].merge!({ short_name => { "rg_version_to_expire" => version } }) if short_name - @deprecated_options[command].merge!({ long_name => { "rg_version_to_expire" => version } }) if long_name + ## + # Mark a command-line option as deprecated, and optionally specify a + # deprecation horizon. + # + # Note that with the current implementation, every version of the option needs + # to be explicitly deprecated, so to deprecate an option defined as + # + # add_option('-t', '--[no-]test', 'Set test mode') do |value, options| + # # ... stuff ... + # end + # + # you would need to explicitly add a call to `deprecate_option` for every + # version of the option you want to deprecate, like + # + # deprecate_option('-t') + # deprecate_option('--test') + # deprecate_option('--no-test') + + def deprecate_option(name, version: nil, extra_msg: nil) + @deprecated_options[command].merge!({ name => { "rg_version_to_expire" => version, "extra_msg" => extra_msg } }) end def check_deprecated_options(options) options.each do |option| if option_is_deprecated?(option) - version_to_expire = @deprecated_options[command][option]["rg_version_to_expire"] + deprecation = @deprecated_options[command][option] + version_to_expire = deprecation["rg_version_to_expire"] deprecate_option_msg = if version_to_expire - "The \"#{option}\" option has been deprecated and will be removed in Rubygems #{version_to_expire}, its use is discouraged." + "The \"#{option}\" option has been deprecated and will be removed in Rubygems #{version_to_expire}." else - "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems, its use is discouraged." + "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems." end + extra_msg = deprecation["extra_msg"] + + deprecate_option_msg += " #{extra_msg}" if extra_msg + alert_warning(deprecate_option_msg) end end diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index 941637ea9c..6dccdcb946 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -25,6 +25,9 @@ class Gem::Commands::GenerateIndexCommand < Gem::Command options[:build_modern] = value end + deprecate_option('--modern', version: '4.0', extra_msg: 'Modern indexes (specs, latest_specs, and prerelease_specs) are always generated, so this option is not needed.') + deprecate_option('--no-modern', version: '4.0', extra_msg: 'The `--no-modern` option is currently ignored. Modern indexes (specs, latest_specs, and prerelease_specs) are always generated.') + add_option '--update', 'Update modern indexes with gems added', 'since the last update' do |value, options| diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 3448fdfb4e..7844e9d199 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -98,7 +98,7 @@ class Gem::Commands::SetupCommand < Gem::Command end def check_ruby_version - required_version = Gem::Requirement.new '>= 1.8.7' + required_version = Gem::Requirement.new '>= 2.3.0' unless required_version.satisfied_by? Gem.ruby_version alert_error "Expected Ruby version #{required_version}, is #{Gem.ruby_version}" diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index af145fd7b4..feab23237d 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -43,6 +43,8 @@ class Gem::Commands::SourcesCommand < Gem::Command source = Gem::Source.new source_uri + check_typo_squatting(source) + begin if Gem.sources.include? source say "source #{source_uri} already present in the cache" @@ -62,6 +64,18 @@ class Gem::Commands::SourcesCommand < Gem::Command end end + def check_typo_squatting(source) + if source.typo_squatting?("rubygems.org") + question = <<-QUESTION.chomp +#{source.uri.to_s} is too similar to https://rubygems.org + +Do you want to add this source? + QUESTION + + terminate_interaction 1 unless ask_yes_no question + end + end + def check_rubygems_https(source_uri) # :nodoc: uri = URI source_uri diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb index 96da272d66..6ea72b10c3 100644 --- a/lib/rubygems/core_ext/kernel_warn.rb +++ b/lib/rubygems/core_ext/kernel_warn.rb @@ -6,13 +6,17 @@ if RUBY_VERSION >= "2.5" module Kernel path = "#{__dir__}/" # Frames to be skipped start with this path. - # Suppress "method redefined" warning - original_warn = instance_method(:warn) - Module.new {define_method(:warn, original_warn)} - original_warn = method(:warn) - module_function define_method(:warn) {|*messages, **kw| + remove_method :warn + + class << self + + remove_method :warn + + end + + define_method(:warn) do |*messages, **kw| unless uplevel = kw[:uplevel] if Gem.java_platform? return original_warn.call(*messages) @@ -46,6 +50,6 @@ if RUBY_VERSION >= "2.5" kw[:uplevel] = uplevel original_warn.call(*messages, **kw) - } + end end end diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index d91ecd25de..2fc1074d92 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -6,7 +6,6 @@ #++ require 'rubygems/user_interaction' -require "open3" class Gem::Ext::Builder @@ -68,6 +67,7 @@ class Gem::Ext::Builder results << "current directory: #{Dir.pwd}" results << (command.respond_to?(:shelljoin) ? command.shelljoin : command) + require "open3" output, status = Open3.capture2e(*command) if verbose puts output diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index c399cc9d95..2ed619f1bc 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -4,6 +4,7 @@ require 'rubygems/request' require 'rubygems/request/connection_pools' require 'rubygems/s3_uri_signer' require 'rubygems/uri_formatter' +require 'rubygems/uri_parsing' require 'rubygems/user_interaction' require 'resolv' require 'rubygems/deprecate' @@ -17,12 +18,16 @@ class Gem::RemoteFetcher include Gem::UserInteraction extend Gem::Deprecate + include Gem::UriParsing + ## # A FetchError exception wraps up the various possible IO and HTTP failures # that could happen while downloading from the internet. class FetchError < Gem::Exception + include Gem::UriParsing + ## # The URI which was being accessed when the exception happened. @@ -30,13 +35,12 @@ class Gem::RemoteFetcher def initialize(message, uri) super message - begin - uri = URI(uri) - uri.password = 'REDACTED' if uri.password - @uri = uri.to_s - rescue URI::InvalidURIError, ArgumentError - @uri = uri - end + + uri = parse_uri(uri) + + uri.password = 'REDACTED' if uri.respond_to?(:password) && uri.password + + @uri = uri.to_s end def to_s # :nodoc: @@ -107,7 +111,7 @@ class Gem::RemoteFetcher spec, source = found.max_by { |(s,_)| s.version } - download spec, source.uri.to_s + download spec, source.uri end ## @@ -130,18 +134,7 @@ class Gem::RemoteFetcher FileUtils.mkdir_p cache_dir rescue nil unless File.exist? cache_dir - # Always escape URI's to deal with potential spaces and such - # It should also be considered that source_uri may already be - # a valid URI with escaped characters. e.g. "{DESede}" is encoded - # as "%7BDESede%7D". If this is escaped again the percentage - # symbols will be escaped. - unless source_uri.is_a?(URI::Generic) - begin - source_uri = URI.parse(source_uri) - rescue - source_uri = URI.parse(URI::DEFAULT_PARSER.escape(source_uri.to_s)) - end - end + source_uri = parse_uri(source_uri) scheme = source_uri.scheme @@ -159,7 +152,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{gem_file_name}" self.cache_update_path remote_gem_path, local_gem_path - rescue Gem::RemoteFetcher::FetchError + rescue FetchError raise if spec.original_platform == spec.platform alternate_name = "#{spec.original_name}.gem" @@ -236,7 +229,7 @@ class Gem::RemoteFetcher unless location = response['Location'] raise FetchError.new("redirecting but no redirect location was given", uri) end - location = URI.parse response['Location'] + location = parse_uri location if https?(uri) && !https?(location) raise FetchError.new("redirecting to non-https resource: #{location}", uri) @@ -254,9 +247,7 @@ class Gem::RemoteFetcher # Downloads +uri+ and returns it as a String. def fetch_path(uri, mtime = nil, head = false) - uri = URI.parse uri unless URI::Generic === uri - - raise ArgumentError, "bad uri: #{uri}" unless uri + uri = parse_uri uri unless uri.scheme raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" @@ -268,21 +259,19 @@ class Gem::RemoteFetcher begin data = Gem::Util.gunzip data rescue Zlib::GzipFile::Error - raise FetchError.new("server did not return a valid file", uri.to_s) + raise FetchError.new("server did not return a valid file", uri) end end data - rescue FetchError - raise rescue Timeout::Error - raise UnknownHostError.new('timed out', uri.to_s) + raise UnknownHostError.new('timed out', uri) rescue IOError, SocketError, SystemCallError, *(OpenSSL::SSL::SSLError if defined?(OpenSSL)) => e if e.message =~ /getaddrinfo/ - raise UnknownHostError.new('no such name', uri.to_s) + raise UnknownHostError.new('no such name', uri) else - raise FetchError.new("#{e.class}: #{e}", uri.to_s) + raise FetchError.new("#{e.class}: #{e}", uri) end end diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index 435c7c53cb..b5a14ec34d 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -19,6 +19,7 @@ class Gem::Request end def self.proxy_uri(proxy) # :nodoc: + require "uri" case proxy when :no_proxy then nil when URI::HTTP then proxy @@ -173,6 +174,7 @@ class Gem::Request :no_proxy : get_proxy_from_env('http') end + require "uri" uri = URI(Gem::UriFormatter.new(env_proxy).normalize) if uri and uri.user.nil? and uri.password.nil? diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index b0cce5bea5..8572cb1806 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true autoload :FileUtils, 'fileutils' -autoload :URI, 'uri' +require "rubygems/text" ## # A Source knows how to list and fetch gems from a RubyGems marshal index. # @@ -11,6 +11,7 @@ autoload :URI, 'uri' class Gem::Source include Comparable + include Gem::Text FILES = { # :nodoc: :released => 'specs', @@ -219,6 +220,11 @@ class Gem::Source end end + def typo_squatting?(host, distance_threshold=4) + return if @uri.host.nil? + levenshtein_distance(@uri.host, host) <= distance_threshold + end + end require 'rubygems/source/git' diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 24c1145907..c3c496db9b 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -1,8 +1,6 @@ -require 'delegate' -require 'uri' require 'rubygems/user_interaction' -class Gem::SpecificationPolicy < SimpleDelegator +class Gem::SpecificationPolicy include Gem::UserInteraction @@ -25,7 +23,7 @@ class Gem::SpecificationPolicy < SimpleDelegator def initialize(specification) @warnings = 0 - super(specification) + @specification = specification end ## @@ -51,7 +49,7 @@ class Gem::SpecificationPolicy < SimpleDelegator validate_require_paths - keep_only_files_and_directories + @specification.keep_only_files_and_directories validate_non_files @@ -92,6 +90,8 @@ class Gem::SpecificationPolicy < SimpleDelegator # Implementation for Specification#validate_metadata def validate_metadata + metadata = @specification.metadata + unless Hash === metadata error 'metadata must be a hash' end @@ -130,7 +130,7 @@ class Gem::SpecificationPolicy < SimpleDelegator error_messages = [] warning_messages = [] - dependencies.each do |dep| + @specification.dependencies.each do |dep| if prev = seen[dep.type][dep.name] error_messages << <<-MESSAGE duplicate dependency on #{dep}, (#{prev.requirement}) use: @@ -145,7 +145,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end warning_messages << "prerelease dependency on #{dep} is not recommended" if - prerelease_dep && !version.prerelease? + prerelease_dep && !@specification.version.prerelease? open_ended = dep.requirement.requirements.all? do |op, version| not version.prerelease? and (op == '>' or op == '>=') @@ -190,14 +190,14 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_permissions return if Gem.win_platform? - files.each do |file| + @specification.files.each do |file| next unless File.file?(file) next if File.stat(file).mode & 0444 == 0444 warning "#{file} is not world-readable" end - executables.each do |name| - exec = File.join bindir, name + @specification.executables.each do |name| + exec = File.join @specification.bindir, name next unless File.file?(exec) next if File.stat(exec).executable? warning "#{exec} is not executable" @@ -208,7 +208,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_nil_attributes nil_attributes = Gem::Specification.non_nil_attributes.select do |attrname| - __getobj__.instance_variable_get("@#{attrname}").nil? + @specification.instance_variable_get("@#{attrname}").nil? end return if nil_attributes.empty? error "#{nil_attributes.join ', '} must not be nil" @@ -216,6 +216,9 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_rubygems_version return unless packaging + + rubygems_version = @specification.rubygems_version + return if rubygems_version == Gem::VERSION error "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}" @@ -223,13 +226,15 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_required_attributes Gem::Specification.required_attributes.each do |symbol| - unless send symbol + unless @specification.send symbol error "missing value for attribute #{symbol}" end end end def validate_name + name = @specification.name + if !name.is_a?(String) error "invalid value for attribute name: \"#{name.inspect}\" must be a string" elsif name !~ /[a-zA-Z]/ @@ -242,14 +247,15 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end def validate_require_paths - return unless raw_require_paths.empty? + return unless @specification.raw_require_paths.empty? error 'specification must have at least one require_path' end def validate_non_files return unless packaging - non_files = files.reject {|x| File.file?(x) || File.symlink?(x)} + + non_files = @specification.files.reject {|x| File.file?(x) || File.symlink?(x)} unless non_files.empty? error "[\"#{non_files.join "\", \""}\"] are not files" @@ -257,18 +263,22 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end def validate_self_inclusion_in_files_list - return unless files.include?(file_name) + file_name = @specification.file_name - error "#{full_name} contains itself (#{file_name}), check your files list" + return unless @specification.files.include?(file_name) + + error "#{@specification.full_name} contains itself (#{file_name}), check your files list" end def validate_specification_version - return if specification_version.is_a?(Integer) + return if @specification.specification_version.is_a?(Integer) error 'specification_version must be an Integer (did you mean version?)' end def validate_platform + platform = @specification.platform + case platform when Gem::Platform, Gem::Platform::RUBY # ok else @@ -283,7 +293,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end def validate_array_attribute(field) - val = self.send(field) + val = @specification.send(field) klass = case field when :dependencies then Gem::Dependency @@ -298,12 +308,14 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end def validate_authors_field - return unless authors.empty? + return unless @specification.authors.empty? error "authors may not be empty" end def validate_licenses + licenses = @specification.licenses + licenses.each do |license| if license.length > 64 error "each license must be 64 characters or less" @@ -331,24 +343,27 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze def validate_lazy_metadata - unless authors.grep(LAZY_PATTERN).empty? + unless @specification.authors.grep(LAZY_PATTERN).empty? error "#{LAZY} is not an author" end - unless Array(email).grep(LAZY_PATTERN).empty? + unless Array(@specification.email).grep(LAZY_PATTERN).empty? error "#{LAZY} is not an email" end - if description =~ LAZY_PATTERN + if @specification.description =~ LAZY_PATTERN error "#{LAZY} is not a description" end - if summary =~ LAZY_PATTERN + if @specification.summary =~ LAZY_PATTERN error "#{LAZY} is not a summary" end + homepage = @specification.homepage + # Make sure a homepage is valid HTTP/HTTPS URI if homepage and not homepage.empty? + require 'uri' begin homepage_uri = URI.parse(homepage) unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class @@ -365,29 +380,29 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li validate_attribute_present(attribute) end - if description == summary + if @specification.description == @specification.summary warning "description and summary are identical" end # TODO: raise at some given date - warning "deprecated autorequire specified" if autorequire + warning "deprecated autorequire specified" if @specification.autorequire - executables.each do |executable| + @specification.executables.each do |executable| validate_shebang_line_in(executable) end - files.select { |f| File.symlink?(f) }.each do |file| + @specification.files.select { |f| File.symlink?(f) }.each do |file| warning "#{file} is a symlink, which is not supported on all platforms" end end def validate_attribute_present(attribute) - value = self.send attribute + value = @specification.send attribute warning("no #{attribute} specified") if value.nil? || value.empty? end def validate_shebang_line_in(executable) - executable_path = File.join(bindir, executable) + executable_path = File.join(@specification.bindir, executable) return if File.read(executable_path, 2) == '#!' warning "#{executable_path} is missing #! line" diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index 0a24dde24d..f3d510470b 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'cgi' -require 'uri' ## # The UriFormatter handles URIs from user-input and escaping. diff --git a/lib/rubygems/uri_parser.rb b/lib/rubygems/uri_parser.rb new file mode 100644 index 0000000000..5c7cabc436 --- /dev/null +++ b/lib/rubygems/uri_parser.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +## +# The UriParser handles parsing URIs. +# + +class Gem::UriParser + + ## + # Parses the #uri, raising if it's invalid + + def parse!(uri) + raise URI::InvalidURIError unless uri + + # Always escape URI's to deal with potential spaces and such + # It should also be considered that source_uri may already be + # a valid URI with escaped characters. e.g. "{DESede}" is encoded + # as "%7BDESede%7D". If this is escaped again the percentage + # symbols will be escaped. + begin + URI.parse(uri) + rescue URI::InvalidURIError + URI.parse(URI::DEFAULT_PARSER.escape(uri)) + end + end + + ## + # Parses the #uri, returning the original uri if it's invalid + + def parse(uri) + parse!(uri) + rescue URI::InvalidURIError + uri + end + +end diff --git a/lib/rubygems/uri_parsing.rb b/lib/rubygems/uri_parsing.rb new file mode 100644 index 0000000000..941d7e023a --- /dev/null +++ b/lib/rubygems/uri_parsing.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "rubygems/uri_parser" + +module Gem::UriParsing + + def parse_uri(source_uri) + return source_uri unless source_uri.is_a?(String) + + uri_parser.parse(source_uri) + end + + private :parse_uri + + def uri_parser + require "uri" + + Gem::UriParser.new + end + + private :uri_parser + +end diff --git a/test/rubygems/test_gem_command.rb b/test/rubygems/test_gem_command.rb index d51371301e..230baa5356 100644 --- a/test/rubygems/test_gem_command.rb +++ b/test/rubygems/test_gem_command.rb @@ -197,9 +197,9 @@ class TestGemCommand < Gem::TestCase assert_equal ['-h', 'command'], args end - def test_deprecate_option_long_name + def test_deprecate_option deprecate_msg = <<-EXPECTED -WARNING: The \"--test\" option has been deprecated and will be removed in Rubygems 3.1, its use is discouraged. +WARNING: The \"--test\" option has been deprecated and will be removed in Rubygems 3.1. EXPECTED testCommand = Class.new(Gem::Command) do @@ -210,7 +210,7 @@ WARNING: The \"--test\" option has been deprecated and will be removed in Rubyg options[:test] = true end - deprecate_option(long_name: '--test', version: '3.1') + deprecate_option('--test', version: '3.1') end def execute @@ -228,7 +228,7 @@ WARNING: The \"--test\" option has been deprecated and will be removed in Rubyg def test_deprecate_option_no_version deprecate_msg = <<-EXPECTED -WARNING: The \"--test\" option has been deprecated and will be removed in future versions of Rubygems, its use is discouraged. +WARNING: The \"--test\" option has been deprecated and will be removed in future versions of Rubygems. EXPECTED testCommand = Class.new(Gem::Command) do @@ -239,7 +239,7 @@ WARNING: The \"--test\" option has been deprecated and will be removed in futur options[:test] = true end - deprecate_option(long_name: '--test') + deprecate_option('--test') end def execute @@ -255,9 +255,9 @@ WARNING: The \"--test\" option has been deprecated and will be removed in futur end end - def test_deprecate_option_short_name + def test_deprecate_option_extra_message deprecate_msg = <<-EXPECTED -WARNING: The \"-t\" option has been deprecated and will be removed in Rubygems 3.5, its use is discouraged. +WARNING: The \"--test\" option has been deprecated and will be removed in Rubygems 3.1. Whether you set `--test` mode or not, this dummy app always runs in test mode. EXPECTED testCommand = Class.new(Gem::Command) do @@ -268,7 +268,7 @@ WARNING: The \"-t\" option has been deprecated and will be removed in Rubygems options[:test] = true end - deprecate_option(short_name: '-t', version: '3.5') + deprecate_option('--test', version: '3.1', extra_msg: 'Whether you set `--test` mode or not, this dummy app always runs in test mode.') end def execute @@ -279,7 +279,36 @@ WARNING: The \"-t\" option has been deprecated and will be removed in Rubygems cmd = testCommand.new use_ui @ui do - cmd.invoke("-t") + cmd.invoke("--test") + assert_equal deprecate_msg, @ui.error + end + end + + def test_deprecate_option_extra_message_and_no_version + deprecate_msg = <<-EXPECTED +WARNING: The \"--test\" option has been deprecated and will be removed in future versions of Rubygems. Whether you set `--test` mode or not, this dummy app always runs in test mode. + EXPECTED + + testCommand = Class.new(Gem::Command) do + def initialize + super('test', 'Gem::Command instance for testing') + + add_option('-t', '--test', 'Test command') do |value, options| + options[:test] = true + end + + deprecate_option('--test', extra_msg: 'Whether you set `--test` mode or not, this dummy app always runs in test mode.') + end + + def execute + true + end + end + + cmd = testCommand.new + + use_ui @ui do + cmd.invoke("--test") assert_equal deprecate_msg, @ui.error end end diff --git a/test/rubygems/test_gem_commands_generate_index_command.rb b/test/rubygems/test_gem_commands_generate_index_command.rb index b4276702f4..d8fda32fc0 100644 --- a/test/rubygems/test_gem_commands_generate_index_command.rb +++ b/test/rubygems/test_gem_commands_generate_index_command.rb @@ -3,6 +3,10 @@ require 'rubygems/test_case' require 'rubygems/indexer' require 'rubygems/commands/generate_index_command' +unless defined?(Builder::XChar) + warn "generate_index tests are being skipped. Install builder gem." +end + class TestGemCommandsGenerateIndexCommand < Gem::TestCase def setup @@ -22,6 +26,18 @@ class TestGemCommandsGenerateIndexCommand < Gem::TestCase assert File.exist?(specs), specs end + def test_execute_no_modern + @cmd.options[:modern] = false + + use_ui @ui do + @cmd.execute + end + + specs = File.join @gemhome, "specs.4.8.gz" + + assert File.exist?(specs), specs + end + def test_handle_options_directory return if win_platform? refute_equal '/nonexistent', @cmd.options[:directory] @@ -47,4 +63,24 @@ class TestGemCommandsGenerateIndexCommand < Gem::TestCase assert @cmd.options[:update] end -end if ''.respond_to? :to_xs + def test_handle_options_modern + use_ui @ui do + @cmd.handle_options %w[--modern] + end + + assert_equal \ + "WARNING: The \"--modern\" option has been deprecated and will be removed in Rubygems 4.0. Modern indexes (specs, latest_specs, and prerelease_specs) are always generated, so this option is not needed.\n", + @ui.error + end + + def test_handle_options_no_modern + use_ui @ui do + @cmd.handle_options %w[--no-modern] + end + + assert_equal \ + "WARNING: The \"--no-modern\" option has been deprecated and will be removed in Rubygems 4.0. The `--no-modern` option is currently ignored. Modern indexes (specs, latest_specs, and prerelease_specs) are always generated.\n", + @ui.error + end + +end if defined?(Builder::XChar) diff --git a/test/rubygems/test_gem_commands_help_command.rb b/test/rubygems/test_gem_commands_help_command.rb index 8fcff6b1e7..f2a519775c 100644 --- a/test/rubygems/test_gem_commands_help_command.rb +++ b/test/rubygems/test_gem_commands_help_command.rb @@ -4,20 +4,15 @@ require "rubygems/test_case" require "rubygems/commands/help_command" require "rubygems/package" require "rubygems/command_manager" -require File.expand_path('../rubygems_plugin', __FILE__) class TestGemCommandsHelpCommand < Gem::TestCase - # previously this was calc'd in setup, but 1.8.7 had - # intermittent failures, but no issues with above require - PLUGIN = File.expand_path('../rubygems_plugin.rb', __FILE__) - def setup super @cmd = Gem::Commands::HelpCommand.new - load PLUGIN unless Gem::Commands.const_defined? :InterruptCommand + load File.expand_path('../rubygems_plugin.rb', __FILE__) unless Gem::Commands.const_defined? :InterruptCommand end def test_gem_help_bad diff --git a/test/rubygems/test_gem_commands_sources_command.rb b/test/rubygems/test_gem_commands_sources_command.rb index 36e6ee9293..b63fbce81f 100644 --- a/test/rubygems/test_gem_commands_sources_command.rb +++ b/test/rubygems/test_gem_commands_sources_command.rb @@ -74,6 +74,80 @@ class TestGemCommandsSourcesCommand < Gem::TestCase assert_equal '', @ui.error end + def test_execute_add_allow_typo_squatting_source + rubygems_org = "https://rubyems.org" + + spec_fetcher do |fetcher| + fetcher.spec("a", 1) + end + + specs = Gem::Specification.map do |spec| + [spec.name, spec.version, spec.original_platform] + end + + specs_dump_gz = StringIO.new + Zlib::GzipWriter.wrap(specs_dump_gz) do |io| + Marshal.dump(specs, io) + end + + @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = specs_dump_gz.string + @cmd.handle_options %W[--add #{rubygems_org}] + ui = Gem::MockGemUi.new("y") + + use_ui ui do + @cmd.execute + end + + expected = "https://rubyems.org is too similar to https://rubygems.org\n\nDo you want to add this source? [yn] https://rubyems.org added to sources\n" + + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + assert Gem.sources.include?(source) + + assert_empty ui.error + end + + def test_execute_add_deny_typo_squatting_source + rubygems_org = "https://rubyems.org" + + spec_fetcher do |fetcher| + fetcher.spec("a", 1) + end + + specs = Gem::Specification.map do |spec| + [spec.name, spec.version, spec.original_platform] + end + + specs_dump_gz = StringIO.new + Zlib::GzipWriter.wrap(specs_dump_gz) do |io| + Marshal.dump(specs, io) + end + + @fetcher.data["#{rubygems_org}/specs.#{@marshal_version}.gz"] = + specs_dump_gz.string + + @cmd.handle_options %W[--add #{rubygems_org}] + + ui = Gem::MockGemUi.new("n") + + use_ui ui do + + assert_raises Gem::MockGemUi::TermError do + @cmd.execute + end + end + + expected = "https://rubyems.org is too similar to https://rubygems.org\n\nDo you want to add this source? [yn] " + + assert_equal expected, ui.output + + source = Gem::Source.new(rubygems_org) + refute Gem.sources.include?(source) + + assert_empty ui.error + end + def test_execute_add_nonexistent_source spec_fetcher diff --git a/test/rubygems/test_gem_gem_runner.rb b/test/rubygems/test_gem_gem_runner.rb index 7c771de9e5..0c801847b0 100644 --- a/test/rubygems/test_gem_gem_runner.rb +++ b/test/rubygems/test_gem_gem_runner.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true require 'rubygems/test_case' -require 'rubygems/gem_runner' class TestGemGemRunner < Gem::TestCase @@ -8,6 +7,8 @@ class TestGemGemRunner < Gem::TestCase super @orig_args = Gem::Command.build_args + + require 'rubygems/gem_runner' @runner = Gem::GemRunner.new end diff --git a/test/rubygems/test_gem_indexer.rb b/test/rubygems/test_gem_indexer.rb index 43a04c3bb3..fdef33e464 100644 --- a/test/rubygems/test_gem_indexer.rb +++ b/test/rubygems/test_gem_indexer.rb @@ -3,7 +3,7 @@ require 'rubygems/test_case' require 'rubygems/indexer' unless defined?(Builder::XChar) - warn "Gem::Indexer tests are being skipped. Install builder gem." if $VERBOSE + warn "Gem::Indexer tests are being skipped. Install builder gem." end class TestGemIndexer < Gem::TestCase diff --git a/test/rubygems/test_gem_source.rb b/test/rubygems/test_gem_source.rb index 2ed9fc78e7..30b45ea267 100644 --- a/test/rubygems/test_gem_source.rb +++ b/test/rubygems/test_gem_source.rb @@ -235,4 +235,18 @@ class TestGemSource < Gem::TestCase refute @source.update_cache? end + def test_typo_squatting + rubygems_source = Gem::Source.new("https://rubgems.org") + assert rubygems_source.typo_squatting?("rubygems.org") + assert rubygems_source.typo_squatting?("rubyagems.org") + assert rubygems_source.typo_squatting?("rubyasgems.org") + refute rubygems_source.typo_squatting?("rubysertgems.org") + end + + def test_typo_squatting_custom_distance_threshold + rubygems_source = Gem::Source.new("https://rubgems.org") + distance_threshold = 5 + assert rubygems_source.typo_squatting?("rubysertgems.org", distance_threshold) + end + end diff --git a/test/rubygems/test_remote_fetch_error.rb b/test/rubygems/test_remote_fetch_error.rb index 780e5e79f7..766086756e 100644 --- a/test/rubygems/test_remote_fetch_error.rb +++ b/test/rubygems/test_remote_fetch_error.rb @@ -5,7 +5,7 @@ class TestRemoteFetchError < Gem::TestCase def test_password_redacted error = Gem::RemoteFetcher::FetchError.new('There was an error fetching', 'https://user:secret@gemsource.org') - refute_match error.to_s, 'secret' + refute_match 'secret', error.to_s end def test_invalid_url diff --git a/test/rubygems/test_require.rb b/test/rubygems/test_require.rb index 69df9093c7..aa2675af5d 100644 --- a/test/rubygems/test_require.rb +++ b/test/rubygems/test_require.rb @@ -365,19 +365,16 @@ class TestGemRequire < Gem::TestCase end def test_realworld_default_gem - begin - gem 'json' - rescue Gem::MissingSpecError - skip "default gems are only available after ruby installation" - end + testing_ruby_repo = !ENV["GEM_COMMAND"].nil? + skip "this test can't work under ruby-core setup" if testing_ruby_repo || java_platform? cmd = <<-RUBY $stderr = $stdout require "json" - puts Gem.loaded_specs["json"].default_gem? + puts Gem.loaded_specs["json"] RUBY output = Gem::Util.popen(Gem.ruby, "-e", cmd).strip - assert_equal "true", output + refute_empty output end def test_default_gem_and_normal_gem @@ -499,36 +496,38 @@ class TestGemRequire < Gem::TestCase end end - # uplevel is 2.5+ only and jruby has some issues with it - if RUBY_VERSION >= "2.5" && !java_platform? - def test_no_kernel_require_in_warn_with_uplevel - lib = File.realpath("../../../lib", __FILE__) - Dir.mktmpdir("warn_test") do |dir| - File.write(dir + "/sub.rb", "warn 'uplevel', 'test', uplevel: 1\n") - File.write(dir + "/main.rb", "require 'sub'\n") - _, err = capture_subprocess_io do - system(@@ruby, "-w", "-rpp", "--disable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") + # uplevel is 2.5+ only + if RUBY_VERSION >= "2.5" + ["", "Kernel."].each do |prefix| + define_method "test_no_kernel_require_in_#{prefix.tr(".", "_")}warn_with_uplevel" do + lib = File.realpath("../../../lib", __FILE__) + Dir.mktmpdir("warn_test") do |dir| + File.write(dir + "/sub.rb", "#{prefix}warn 'uplevel', 'test', uplevel: 1\n") + File.write(dir + "/main.rb", "require 'sub'\n") + _, err = capture_subprocess_io do + system(@@ruby, "-w", "--disable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") + end + assert_match(/main\.rb:1: warning: uplevel\ntest\n$/, err) + _, err = capture_subprocess_io do + system(@@ruby, "-w", "--enable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") + end + assert_match(/main\.rb:1: warning: uplevel\ntest\n$/, err) end - assert_equal "main.rb:1: warning: uplevel\ntest\n", err - _, err = capture_subprocess_io do - system(@@ruby, "-w", "-rpp", "--enable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") - end - assert_equal "main.rb:1: warning: uplevel\ntest\n", err end - end - def test_no_other_behavioral_changes_with_kernel_warn - lib = File.realpath("../../../lib", __FILE__) - Dir.mktmpdir("warn_test") do |dir| - File.write(dir + "/main.rb", "warn({x:1}, {y:2}, [])\n") - _, err = capture_subprocess_io do - system(@@ruby, "-w", "-rpp", "--disable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") + define_method "test_no_other_behavioral_changes_with_#{prefix.tr(".", "_")}warn" do + lib = File.realpath("../../../lib", __FILE__) + Dir.mktmpdir("warn_test") do |dir| + File.write(dir + "/main.rb", "#{prefix}warn({x:1}, {y:2}, [])\n") + _, err = capture_subprocess_io do + system(@@ruby, "-w", "--disable=gems", "-I", lib, "-C", dir, "main.rb") + end + assert_match(/{:x=>1}\n{:y=>2}\n$/, err) + _, err = capture_subprocess_io do + system(@@ruby, "-w", "--enable=gems", "-I", lib, "-C", dir, "main.rb") + end + assert_match(/{:x=>1}\n{:y=>2}\n$/, err) end - assert_equal "{:x=>1}\n{:y=>2}\n", err - _, err = capture_subprocess_io do - system(@@ruby, "-w", "-rpp", "--enable=gems", "-I", lib, "-C", dir, "-I.", "main.rb") - end - assert_equal "{:x=>1}\n{:y=>2}\n", err end end end