diff options
Diffstat (limited to 'lib/rubygems')
270 files changed, 26661 insertions, 7465 deletions
diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb index 58b601f6b0..0af80cc3db 100644 --- a/lib/rubygems/available_set.rb +++ b/lib/rubygems/available_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::AvailableSet include Enumerable @@ -26,7 +27,7 @@ class Gem::AvailableSet s = o.set when Array s = o.map do |sp,so| - if !sp.kind_of?(Gem::Specification) || !so.kind_of?(Gem::Source) + if !sp.is_a?(Gem::Specification) || !so.is_a?(Gem::Source) raise TypeError, "Array must be in [[spec, source], ...] form" end @@ -69,7 +70,7 @@ class Gem::AvailableSet end def all_specs - @set.map {|t| t.spec } + @set.map(&:spec) end def match_platform! @@ -104,14 +105,14 @@ class Gem::AvailableSet def to_request_set(development = :none) request_set = Gem::RequestSet.new - request_set.development = :all == development + request_set.development = development == :all each_spec do |spec| request_set.always_install << spec request_set.gem spec.name, spec.version request_set.import spec.development_dependencies if - :shallow == development + development == :shallow end request_set @@ -146,7 +147,7 @@ class Gem::AvailableSet end def remove_installed!(dep) - @set.reject! do |t| + @set.reject! do |_t| # already locally installed Gem::Specification.any? do |installed_spec| dep.name == installed_spec.name && diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index dcc64e6409..0ed7fc60bb 100644 --- a/lib/rubygems/basic_specification.rb +++ b/lib/rubygems/basic_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # BasicSpecification is an abstract class which implements some common code # used by both Specification and StubSpecification. @@ -33,15 +34,6 @@ class Gem::BasicSpecification internal_init end - def self.default_specifications_dir - Gem.default_specifications_dir - end - - class << self - extend Gem::Deprecate - rubygems_deprecate :default_specifications_dir, "Gem.default_specifications_dir" - end - ## # The path to the gem.build_complete file within the extension install # directory. @@ -70,32 +62,57 @@ class Gem::BasicSpecification # Return true if this spec can require +file+. def contains_requirable_file?(file) - if @ignored - return false - elsif missing_extensions? - @ignored = true - - if Gem::Platform::RUBY == platform || Gem::Platform.local === platform - warn "Ignoring #{full_name} because its extensions are not built. " + + if ignored? + if platform == Gem::Platform::RUBY || Gem::Platform.local === platform + warn "Ignoring #{full_name} because its extensions are not built. " \ "Try: gem pristine #{name} --version #{version}" end return false end - have_file? file, Gem.suffixes + is_soext = file.end_with?(".so", ".o") + + if is_soext + have_file? file.delete_suffix(File.extname(file)), Gem.dynamic_library_suffixes + else + have_file? file, Gem.suffixes + end + end + + ## + # Return true if this spec should be ignored because it's missing extensions. + + def ignored? + return @ignored unless @ignored.nil? + + @ignored = missing_extensions? end def default_gem? - loaded_from && + !loaded_from.nil? && File.dirname(loaded_from) == Gem.default_specifications_dir end ## + # Regular gems take precedence over default gems + + def default_gem_priority + default_gem? ? 1 : -1 + end + + ## + # Gems higher up in +gem_path+ take precedence + + def base_dir_priority(gem_path) + gem_path.index(base_dir) || gem_path.size + end + + ## # Returns full path to the directory where gem's extensions are installed. def extension_dir - @extension_dir ||= File.expand_path(File.join(extensions_dir, full_name)).tap(&Gem::UNTAINT) + @extension_dir ||= File.expand_path(File.join(extensions_dir, full_name)) end ## @@ -108,20 +125,17 @@ class Gem::BasicSpecification end def find_full_gem_path # :nodoc: - # TODO: also, shouldn't it default to full_name if it hasn't been written? - path = File.expand_path File.join(gems_dir, full_name) - path.tap(&Gem::UNTAINT) - path + File.expand_path File.join(gems_dir, full_name) end private :find_full_gem_path ## # The full path to the gem (install path + full name). + # + # TODO: This is duplicated with #gem_dir. Eventually either of them should be deprecated. def full_gem_path - # TODO: This is a heavily used method by gems, so we'll need - # to aleast just alias it to #gem_dir rather than remove it. @full_gem_path ||= find_full_gem_path end @@ -132,9 +146,22 @@ class Gem::BasicSpecification def full_name if platform == Gem::Platform::RUBY || platform.nil? - "#{name}-#{version}".dup.tap(&Gem::UNTAINT) + "#{name}-#{version}" else - "#{name}-#{version}-#{platform}".dup.tap(&Gem::UNTAINT) + "#{name}-#{version}-#{platform}" + end + end + + ## + # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`). + # Information about where the gem is installed is also included if not + # installed in the default GEM_HOME. + + def full_name_with_location + if base_dir != Gem.dir + "#{full_name} in #{base_dir}" + else + full_name end end @@ -144,15 +171,15 @@ class Gem::BasicSpecification def full_require_paths @full_require_paths ||= - begin - full_paths = raw_require_paths.map do |path| - File.join full_gem_path, path.tap(&Gem::UNTAINT) - end + begin + full_paths = raw_require_paths.map do |path| + File.join full_gem_path, path + end - full_paths << extension_dir if have_extensions? + full_paths << extension_dir if have_extensions? - full_paths - end + full_paths + end end ## @@ -160,9 +187,12 @@ class Gem::BasicSpecification def datadir # TODO: drop the extra ", gem_name" which is uselessly redundant - File.expand_path(File.join(gems_dir, full_name, "data", name)).tap(&Gem::UNTAINT) + File.expand_path(File.join(gems_dir, full_name, "data", name)) end + extend Gem::Deprecate + rubygems_deprecate :datadir, :none, "4.1" + ## # Full path of the target library file. # If the file is not in this gem, return nil. @@ -170,27 +200,25 @@ class Gem::BasicSpecification def to_fullpath(path) if activated? @paths_map ||= {} - @paths_map[path] ||= - begin - fullpath = nil - suffixes = Gem.suffixes - suffixes.find do |suf| - full_require_paths.find do |dir| - File.file?(fullpath = "#{dir}/#{path}#{suf}") - end - end ? fullpath : nil + Gem.suffixes.each do |suf| + full_require_paths.each do |dir| + fullpath = "#{dir}/#{path}#{suf}" + next unless File.file?(fullpath) + @paths_map[path] ||= fullpath + end end - else - nil + @paths_map[path] end end ## # Returns the full path to this spec's gem directory. # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0 + # + # TODO: This is duplicated with #full_gem_path. Eventually either of them should be deprecated. def gem_dir - @gem_dir ||= File.expand_path File.join(gems_dir, full_name) + @gem_dir ||= find_full_gem_path end ## @@ -222,6 +250,13 @@ class Gem::BasicSpecification raise NotImplementedError end + def installable_on_platform?(target_platform) # :nodoc: + return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform) + return true if Gem::Platform.new(platform) === target_platform + + false + end + def raw_require_paths # :nodoc: raise NotImplementedError end @@ -271,9 +306,9 @@ class Gem::BasicSpecification # Return all files in this gem that match for +glob+. def matches_for_glob(glob) # TODO: rename? - glob = File.join(self.lib_dirs_glob, glob) + glob = File.join(lib_dirs_glob, glob) - Dir[glob].map {|f| f.tap(&Gem::UNTAINT) } # FIX our tests are broken, run w/ SAFE=1 + Dir[glob] end ## @@ -288,17 +323,17 @@ class Gem::BasicSpecification # for this spec. def lib_dirs_glob - dirs = if self.raw_require_paths - if self.raw_require_paths.size > 1 - "{#{self.raw_require_paths.join(',')}}" + dirs = if raw_require_paths + if raw_require_paths.size > 1 + "{#{raw_require_paths.join(",")}}" else - self.raw_require_paths.first + raw_require_paths.first end else "lib" # default value for require_paths for bundler/inline end - "#{self.full_gem_path}/#{dirs}".dup.tap(&Gem::UNTAINT) + "#{full_gem_path}/#{dirs}" end ## @@ -323,15 +358,19 @@ class Gem::BasicSpecification raise NotImplementedError end - def this; self; end + def this + self + end private - def have_extensions?; !extensions.empty?; end + def have_extensions? + !extensions.empty? + end def have_file?(file, suffixes) return true if raw_require_paths.any? do |path| - base = File.join(gems_dir, full_name, path.tap(&Gem::UNTAINT), file).tap(&Gem::UNTAINT) + base = File.join(gems_dir, full_name, path, file) suffixes.any? {|suf| File.file? base + suf } end diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb index 5b34227d3a..bbe7bf0ab5 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -2,11 +2,17 @@ module Gem::BundlerVersionFinder def self.bundler_version + bcv = bundle_config_version + return if bcv == "system" + v = ENV["BUNDLER_VERSION"] + v = nil if v&.empty? v ||= bundle_update_bundler_version return if v == true + v ||= bcv unless bcv == "lockfile" + v ||= lockfile_version return unless v @@ -21,7 +27,7 @@ module Gem::BundlerVersionFinder end def self.bundle_update_bundler_version - return unless File.basename($0) == "bundle" + return unless ["bundle", "bundler"].include? File.basename($0) return unless "update".start_with?(ARGV.first || " ") bundler_version = nil update_index = nil @@ -46,13 +52,74 @@ module Gem::BundlerVersionFinder private_class_method :lockfile_version def self.lockfile_contents + gemfile = gemfile_path + + return unless gemfile + + lockfile = ENV["BUNDLE_LOCKFILE"] + lockfile = nil if lockfile&.empty? + + lockfile ||= case gemfile + when "gems.rb" then "gems.locked" + else "#{gemfile}.lock" + end + + return unless File.file?(lockfile) + + File.read(lockfile) + end + private_class_method :lockfile_contents + + def self.bundle_config_version + env_version = ENV["BUNDLE_VERSION"] + return env_version if env_version && !env_version.empty? + + version = nil + + [bundler_local_config_file, bundler_global_config_file].each do |config_file| + next unless config_file && File.file?(config_file) + + contents = File.read(config_file) + contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/ + + version = $1 + break if version + end + + version + end + private_class_method :bundle_config_version + + def self.bundler_global_config_file + # see Bundler::Settings#global_config_file + if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty? + ENV["BUNDLE_CONFIG"] + elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty? + ENV["BUNDLE_USER_CONFIG"] + elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty? + ENV["BUNDLE_USER_HOME"] + "config" + elsif Gem.user_home && !Gem.user_home.empty? + Gem.user_home + ".bundle/config" + end + end + private_class_method :bundler_global_config_file + + def self.bundler_local_config_file + gemfile = gemfile_path + return unless gemfile + + File.join(File.dirname(gemfile), ".bundle", "config") + end + private_class_method :bundler_local_config_file + + def self.gemfile_path gemfile = ENV["BUNDLE_GEMFILE"] gemfile = nil if gemfile&.empty? unless gemfile begin Gem::Util.traverse_parents(Dir.pwd) do |directory| - next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f.tap(&Gem::UNTAINT)) } + next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) } gemfile = File.join directory, gemfile break @@ -62,16 +129,7 @@ module Gem::BundlerVersionFinder end end - return unless gemfile - - lockfile = case gemfile - when "gems.rb" then "gems.locked" - else "#{gemfile}.lock" - end.dup.tap(&Gem::UNTAINT) - - return unless File.file?(lockfile) - - File.read(lockfile) + gemfile end - private_class_method :lockfile_contents + private_class_method :gemfile_path end diff --git a/lib/rubygems/ci_detector.rb b/lib/rubygems/ci_detector.rb new file mode 100644 index 0000000000..7a2d4ee29a --- /dev/null +++ b/lib/rubygems/ci_detector.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gem + module CIDetector + # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and + # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates). + # TODO: Drop that duplication once bundler drops support for RubyGems 3.4 + # + # ## Recognized CI providers, their signifiers, and the relevant docs ## + # + # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables + # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables + # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables + # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/ + # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/ + # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables + # dsari - CI, DSARI https://github.com/rfinnie/dsari#running + # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables + # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters + # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments + # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars + # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/ + # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables + # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html + # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + # + # ### Some "standard" ENVs that multiple providers may set ### + # + # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard. + # * CI_NAME - Not as frequently used, but some providers set this to specify their own name + + # Any of these being set is a reasonably reliable indicator that we are + # executing in a CI environment. + ENV_INDICATORS = [ + "CI", + "CI_NAME", + "CONTINUOUS_INTEGRATION", + "BUILD_NUMBER", + "CI_APP_ID", + "CI_BUILD_ID", + "CI_BUILD_NUMBER", + "RUN_ID", + "TASKCLUSTER_ROOT_URL", + ].freeze + + # For each CI, this env suffices to indicate that we're on _that_ CI's + # containers. (A few of them only supply a CI_NAME variable, which is also + # nice). And if they set "CI" but we can't tell which one they are, we also + # want to know that - a bare "ci" without another token tells us as much. + ENV_DESCRIPTORS = { + "TRAVIS" => "travis", + "CIRCLECI" => "circle", + "CIRRUS_CI" => "cirrus", + "DSARI" => "dsari", + "SEMAPHORE" => "semaphore", + "JENKINS_URL" => "jenkins", + "BUILDKITE" => "buildkite", + "GO_SERVER_URL" => "go", + "GITLAB_CI" => "gitlab", + "GITHUB_ACTIONS" => "github", + "TASKCLUSTER_ROOT_URL" => "taskcluster", + "CI" => "ci", + }.freeze + + def self.ci? + ENV_INDICATORS.any? {|var| ENV.include?(var) } + end + + def self.ci_strings + matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values + matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"] + matching_names.reject(&:empty?).sort.uniq + end + end +end diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index 59bfc5a118..d38363f293 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require_relative "optparse" +require_relative "vendored_optparse" require_relative "requirement" require_relative "user_interaction" @@ -19,9 +20,7 @@ require_relative "user_interaction" class Gem::Command include Gem::UserInteraction - Gem::OptionParser.accept Symbol do |value| - value.to_sym - end + Gem::OptionParser.accept Symbol, &:to_sym ## # The name of the command. @@ -93,7 +92,7 @@ class Gem::Command # array or a string to be split on white space. def self.add_specific_extra_args(cmd,args) - args = args.split(/\s+/) if args.kind_of? String + args = args.split(/\s+/) if args.is_a? String specific_extra_args_hash[cmd] = args end @@ -118,7 +117,7 @@ class Gem::Command # Unhandled arguments (gem names, files, etc.) are left in # <tt>options[:args]</tt>. - def initialize(command, summary=nil, defaults={}) + def initialize(command, summary = nil, defaults = {}) @command = command @summary = summary @program_name = "gem #{command}" @@ -191,7 +190,7 @@ class Gem::Command "Please specify at least one gem name (e.g. gem build GEMNAME)" end - args.select {|arg| arg !~ /^-/ } + args.reject {|arg| arg.start_with?("-") } end ## @@ -201,11 +200,15 @@ class Gem::Command # respectively. def get_all_gem_names_and_versions get_all_gem_names.map do |name| - if /\A(.*):(#{Gem::Requirement::PATTERN_RAW})\z/ =~ name - [$1, $2] - else - [name] - end + extract_gem_name_and_version(name) + end + end + + def extract_gem_name_and_version(name) # :nodoc: + if /\A(.*):(#{Gem::Requirement::PATTERN_RAW})\z/ =~ name + [$1, $2] + else + [name] end end @@ -223,7 +226,7 @@ class Gem::Command if args.size > 1 raise Gem::CommandLineError, - "Too many gem names (#{args.join(', ')}); please specify only one" + "Too many gem names (#{args.join(", ")}); please specify only one" end args.first @@ -311,7 +314,7 @@ class Gem::Command options[:build_args] = build_args if options[:silent] - old_ui = self.ui + old_ui = ui self.ui = ui = Gem::SilentUI.new end @@ -393,22 +396,21 @@ class Gem::Command def check_deprecated_options(options) options.each do |option| - if option_is_deprecated?(option) - deprecation = @deprecated_options[command][option] - version_to_expire = deprecation["rg_version_to_expire"] + next unless option_is_deprecated?(option) + 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}." - else - "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems." - end + deprecate_option_msg = if version_to_expire + "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." + end - extra_msg = deprecation["extra_msg"] + extra_msg = deprecation["extra_msg"] - deprecate_option_msg += " #{extra_msg}" if extra_msg + deprecate_option_msg += " #{extra_msg}" if extra_msg - alert_warning(deprecate_option_msg) - end + alert_warning(deprecate_option_msg) end end @@ -425,12 +427,10 @@ class Gem::Command # True if the command handles the given argument list. def handles?(args) - begin - parser.parse!(args.dup) - return true - rescue - return false - end + parser.parse!(args.dup) + true + rescue StandardError + false end ## @@ -457,7 +457,7 @@ class Gem::Command until extra.empty? do ex = [] ex << extra.shift - ex << extra.shift if extra.first.to_s =~ /^[^-]/ # rubocop:disable Performance/StartWith + ex << extra.shift if /^[^-]/.match?(extra.first.to_s) result << ex if handles?(ex) end @@ -473,7 +473,7 @@ class Gem::Command private def option_is_deprecated?(option) - @deprecated_options[command].has_key?(option) + @deprecated_options[command].key?(option) end def add_parser_description # :nodoc: @@ -485,7 +485,7 @@ class Gem::Command @parser.separator nil @parser.separator " Description:" - formatted.split("\n").each do |line| + formatted.each_line do |line| @parser.separator " #{line.rstrip}" end end @@ -512,8 +512,8 @@ class Gem::Command @parser.separator nil @parser.separator " #{title}:" - content.split(/\n/).each do |line| - @parser.separator " #{line}" + content.each_line do |line| + @parser.separator " #{line.rstrip}" end end @@ -522,7 +522,7 @@ class Gem::Command @parser.separator nil @parser.separator " Summary:" - wrap(@summary, 80 - 4).split("\n").each do |line| + wrap(@summary, 80 - 4).each_line do |line| @parser.separator " #{line.strip}" end end @@ -579,12 +579,12 @@ class Gem::Command # Add the options common to all commands. add_common_option("-h", "--help", - "Get help on this command") do |value, options| + "Get help on this command") do |_value, options| options[:help] = true end add_common_option("-V", "--[no-]verbose", - "Set the verbose level of output") do |value, options| + "Set the verbose level of output") do |value, _options| # Set us to "really verbose" so the progress meter works if Gem.configuration.verbose && value Gem.configuration.verbose = 1 @@ -593,12 +593,12 @@ class Gem::Command end end - add_common_option("-q", "--quiet", "Silence command progress meter") do |value, options| + add_common_option("-q", "--quiet", "Silence command progress meter") do |_value, _options| Gem.configuration.verbose = false end add_common_option("--silent", - "Silence RubyGems output") do |value, options| + "Silence RubyGems output") do |_value, options| options[:silent] = true end @@ -650,9 +650,6 @@ RubyGems is a package manager for Ruby. gem help platforms gem platforms guide gem help <COMMAND> show help on COMMAND (e.g. 'gem help install') - gem server present a web page at - http://localhost:8808/ - with info about installed gems Further information: https://guides.rubygems.org HELP diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 1bdbd50530..76b2fba835 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -43,6 +44,7 @@ class Gem::CommandManager :contents, :dependency, :environment, + :exec, :fetch, :generate_index, :help, @@ -56,8 +58,8 @@ class Gem::CommandManager :owner, :pristine, :push, - :query, :rdoc, + :rebuild, :search, :server, :signin, @@ -82,7 +84,7 @@ class Gem::CommandManager # Return the authoritative instance of the command manager. def self.instance - @command_manager ||= new + @instance ||= new end ## @@ -97,14 +99,14 @@ class Gem::CommandManager # Reset the authoritative instance of the command manager. def self.reset - @command_manager = nil + @instance = nil end ## # Register all the subcommands supported by the gem command. def initialize - require "timeout" + require_relative "vendored_timeout" @commands = {} BUILTIN_COMMANDS.each do |name| @@ -115,7 +117,7 @@ class Gem::CommandManager ## # Register the Symbol +command+ as a gem command. - def register_command(command, obj=false) + def register_command(command, obj = false) @commands[command] = obj end @@ -139,15 +141,15 @@ class Gem::CommandManager # Return a sorted list of all command names as strings. def command_names - @commands.keys.collect {|key| key.to_s }.sort + @commands.keys.collect(&:to_s).sort end ## # Run the command specified by +args+. - def run(args, build_args=nil) + def run(args, build_args = nil) process_args(args, build_args) - rescue StandardError, Timeout::Error => ex + rescue StandardError, Gem::Timeout::Error => ex if ex.respond_to?(:detailed_message) msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 } else @@ -162,7 +164,7 @@ class Gem::CommandManager terminate_interaction(1) end - def process_args(args, build_args=nil) + def process_args(args, build_args = nil) if args.empty? say Gem::Command::HELP terminate_interaction 1 @@ -199,7 +201,7 @@ class Gem::CommandManager if possibilities.size > 1 raise Gem::CommandLineError, - "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]" + "Ambiguous command #{cmd_name} matches [#{possibilities.join(", ")}]" elsif possibilities.empty? raise Gem::UnknownCommandError.new(cmd_name) end @@ -227,18 +229,16 @@ class Gem::CommandManager def load_and_instantiate(command_name) command_name = command_name.to_s const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } << "Command" - load_error = nil begin begin require "rubygems/commands/#{command_name}_command" - rescue LoadError => e - load_error = e + rescue LoadError + # it may have been defined from a rubygems_plugin.rb file end - Gem::Commands.const_get(const_name).new - rescue Exception => e - e = load_error if load_error + Gem::Commands.const_get(const_name).new + rescue StandardError => e alert_error clean_text("Loading command: #{command_name} (#{e.class})\n\t#{e}") ui.backtrace e end @@ -247,6 +247,7 @@ class Gem::CommandManager def invoke_command(args, build_args) cmd_name = args.shift.downcase cmd = find_command cmd_name + terminate_interaction 1 unless cmd cmd.deprecation_warning if cmd.deprecated? cmd.invoke_with_build_args args, build_args end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 5d6152d3b9..cfe1f8ec3c 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -1,34 +1,30 @@ # frozen_string_literal: true + require_relative "../command" +require_relative "../gemspec_helpers" require_relative "../package" require_relative "../version_option" class Gem::Commands::BuildCommand < Gem::Command include Gem::VersionOption + include Gem::GemspecHelpers def initialize super "build", "Build a gem from a gemspec" add_platform_option - add_option "--force", "skip validation of the spec" do |value, options| + add_option "--force", "skip validation of the spec" do |_value, options| options[:force] = true end - add_option "--strict", "consider warnings as errors when validating the spec" do |value, options| + add_option "--strict", "consider warnings as errors when validating the spec" do |_value, options| options[:strict] = true end add_option "-o", "--output FILE", "output gem with the given filename" do |value, options| options[:output] = value end - - add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| - options[:build_path] = value - end - deprecate_option "-C", - version: "4.0", - extra_msg: "-C is a global flag now. Use `gem -C PATH build GEMSPEC_FILE [options]` instead" end def arguments # :nodoc: @@ -74,17 +70,6 @@ Gems can be saved to a specified filename with the output option: private - def find_gemspec(glob = "*.gemspec") - gemspecs = Dir.glob(glob).sort - - if gemspecs.size > 1 - alert_error "Multiple gemspecs found: #{gemspecs}, please specify one" - terminate_interaction(1) - end - - gemspecs.first - end - def build_gem gemspec = resolve_gem_name diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb index 17b1d11b19..fe03841ddb 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../security" class Gem::Commands::CertCommand < Gem::Command def initialize super "cert", "Manage RubyGems certificates and signing settings", - :add => [], :remove => [], :list => [], :build => [], :sign => [] + add: [], remove: [], list: [], build: [], sign: [] add_option("-a", "--add CERT", "Add a trusted certificate.") do |cert_file, options| @@ -135,7 +136,7 @@ class Gem::Commands::CertCommand < Gem::Command end def build(email) - if !valid_email?(email) + unless valid_email?(email) raise Gem::CommandLineError, "Invalid email address #{email}" end @@ -157,7 +158,7 @@ class Gem::Commands::CertCommand < Gem::Command cert = Gem::Security.create_cert_email( email, key, - (Gem::Security::ONE_DAY * expiration_length_days) + Gem::Security::ONE_DAY * expiration_length_days ) Gem::Security.write cert, "gem-public_cert.pem" @@ -177,9 +178,9 @@ class Gem::Commands::CertCommand < Gem::Command algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM key = Gem::Security.create_key(algorithm) - key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase + key_path = Gem::Security.write key, "gem-private_key.pem", 0o600, passphrase - return key, key_path + [key, key_path] end def certificates_matching(filter) @@ -262,7 +263,6 @@ For further reading on signing gems see `ri Gem::Security`. key = File.read key_file passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] options[:key] = OpenSSL::PKey.read key, passphrase - rescue Errno::ENOENT alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" @@ -291,7 +291,7 @@ For further reading on signing gems see `ri Gem::Security`. cert = File.read cert_file cert = OpenSSL::X509::Certificate.new cert - permissions = File.stat(cert_file).mode & 0777 + permissions = File.stat(cert_file).mode & 0o777 issuer_cert = options[:issuer_cert] issuer_key = options[:key] diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 4d1f8782b1..fb23dd9cb4 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../validator" @@ -9,7 +10,7 @@ class Gem::Commands::CheckCommand < Gem::Command def initialize super "check", "Check a gem repository for added or missing files", - :alien => true, :doctor => false, :dry_run => false, :gems => true + alien: true, doctor: false, dry_run: false, gems: true add_option("-a", "--[no-]alien", 'Report "unmanaged" or rogue files in the', @@ -40,17 +41,21 @@ class Gem::Commands::CheckCommand < Gem::Command def check_gems say "Checking gems..." say - gems = get_all_gem_names rescue [] + gems = begin + get_all_gem_names + rescue StandardError + [] + end Gem::Validator.new.alien(gems).sort.each do |key, val| - unless val.empty? + if val.empty? + say "#{key} is error-free" if Gem.configuration.verbose + else say "#{key} has #{val.size} problems" val.each do |error_entry| say " #{error_entry.path}:" say " #{error_entry.problem}" end - else - say "#{key} is error-free" if Gem.configuration.verbose end say end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 1ae84924c1..c89a24eee9 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../dependency_list" require_relative "../uninstaller" @@ -7,16 +8,16 @@ class Gem::Commands::CleanupCommand < Gem::Command def initialize super "cleanup", "Clean up old versions of installed gems", - :force => false, :install_dir => Gem.dir, - :check_dev => true + force: false, install_dir: Gem.dir, + check_dev: true add_option("-n", "-d", "--dry-run", - "Do not uninstall gems") do |value, options| + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end add_option(:Deprecated, "--dryrun", - "Do not uninstall gems") do |value, options| + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end deprecate_option("--dryrun", extra_msg: "Use --dry-run instead") @@ -37,8 +38,6 @@ class Gem::Commands::CleanupCommand < Gem::Command @default_gems = [] @full = nil @gems_to_cleanup = nil - @original_home = nil - @original_path = nil @primary_gems = nil end @@ -74,7 +73,7 @@ If no gems are named all gems in GEM_HOME are cleaned. until done do clean_gems - this_set = @gems_to_cleanup.map {|spec| spec.full_name }.sort + this_set = @gems_to_cleanup.map(&:full_name).sort done = this_set.empty? || last_set == this_set @@ -87,16 +86,13 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Clean up complete" verbose do - skipped = @default_gems.map {|spec| spec.full_name } + skipped = @default_gems.map(&:full_name) - "Skipped default gems: #{skipped.join ', '}" + "Skipped default gems: #{skipped.join ", "}" end end def clean_gems - @original_home = Gem.dir - @original_path = Gem.path - get_primary_gems get_candidate_gems get_gems_to_cleanup @@ -111,17 +107,15 @@ If no gems are named all gems in GEM_HOME are cleaned. deps.reverse_each do |spec| uninstall_dep spec end - - Gem::Specification.reset end def get_candidate_gems - @candidate_gems = unless options[:args].empty? - options[:args].map do |gem_name| - Gem::Specification.find_all_by_name gem_name - end.flatten - else + @candidate_gems = if options[:args].empty? Gem::Specification.to_a + else + options[:args].flat_map do |gem_name| + Gem::Specification.find_all_by_name gem_name + end end end @@ -130,11 +124,9 @@ If no gems are named all gems in GEM_HOME are cleaned. @primary_gems[spec.name].version != spec.version end - default_gems, gems_to_cleanup = gems_to_cleanup.partition do |spec| - spec.default_gem? - end + default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?) - uninstall_from = options[:user_install] ? Gem.user_dir : @original_home + uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir gems_to_cleanup = gems_to_cleanup.select do |spec| spec.base_dir == uninstall_from @@ -167,8 +159,8 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Attempting to uninstall #{spec.full_name}" uninstall_options = { - :executables => false, - :version => "= #{spec.version}", + executables: false, + version: "= #{spec.version}", } uninstall_options[:user_install] = Gem.user_dir == spec.base_dir @@ -182,8 +174,5 @@ If no gems are named all gems in GEM_HOME are cleaned. say "Unable to uninstall #{spec.full_name}:" say "\t#{e.class}: #{e.message}" end - ensure - # Restore path Gem::Uninstaller may have changed - Gem.use_paths @original_home, *@original_path end end diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index c5fdfca31e..d4f9871868 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -7,8 +8,8 @@ class Gem::Commands::ContentsCommand < Gem::Command def initialize super "contents", "Display the contents of the installed gems", - :specdirs => [], :lib_only => false, :prefix => true, - :show_install_dir => false + specdirs: [], lib_only: false, prefix: true, + show_install_dir: false add_version_option @@ -91,9 +92,9 @@ prefix or only the files that are requireable. def files_in_gem(spec) gem_path = spec.full_gem_path - extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] + extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only] glob = "#{gem_path}#{extra}/**/*" - prefix_re = /#{Regexp.escape(gem_path)}\// + prefix_re = %r{#{Regexp.escape(gem_path)}/} Dir[glob].map do |file| [gem_path, file.sub(prefix_re, "")] @@ -101,15 +102,22 @@ prefix or only the files that are requireable. end def files_in_default_gem(spec) - spec.files.map do |file| - case file - when /\A#{spec.bindir}\// - # $' is POSTMATCH - [RbConfig::CONFIG["bindir"], $'] - when /\.so\z/ - [RbConfig::CONFIG["archdir"], file] + spec.files.filter_map do |file| + if file.start_with?("#{spec.bindir}/") + [RbConfig::CONFIG["bindir"], file.delete_prefix("#{spec.bindir}/")] else - [RbConfig::CONFIG["rubylibdir"], file] + gem spec.name, spec.version + + require_path = spec.require_paths.find do |path| + file.start_with?("#{path}/") + end + + requirable_part = file.delete_prefix("#{require_path}/") + + resolve = $LOAD_PATH.resolve_feature_path(requirable_part)&.last + next unless resolve + + [resolve.delete_suffix(requirable_part), requirable_part] end end end @@ -177,12 +185,12 @@ prefix or only the files that are requireable. @spec_dirs.sort.each {|dir| say dir } end - return nil + nil end def specification_directories # :nodoc: - options[:specdirs].map do |i| + options[:specdirs].flat_map do |i| [i, File.join(i, "specifications")] - end.flatten + end end end diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index 3f69a95e83..9aaefae999 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -10,15 +11,14 @@ class Gem::Commands::DependencyCommand < Gem::Command def initialize super "dependency", "Show the dependencies of an installed gem", - :version => Gem::Requirement.default, :domain => :local + version: Gem::Requirement.default, domain: :local add_version_option add_platform_option add_prerelease_option add_option("-R", "--[no-]reverse-dependencies", - "Include reverse dependencies in the output") do - |value, options| + "Include reverse dependencies in the output") do |value, options| options[:reverse_dependencies] = value end @@ -90,10 +90,9 @@ use with other commands. def display_pipe(specs) # :nodoc: specs.each do |spec| - unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| - say "#{dep.name} --version '#{dep.requirement}'" - end + next if spec.dependencies.empty? + spec.dependencies.sort_by(&:name).each do |dep| + say "#{dep.name} --version '#{dep.requirement}'" end end end @@ -153,7 +152,7 @@ use with other commands. response = String.new response << " " * level + "Gem #{spec.full_name}\n" unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| + spec.dependencies.sort_by(&:name).each do |dep| response << " " * level + " #{dep}\n" end end diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index d95e1d0dbb..a5eb521a53 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::EnvironmentCommand < Gem::Command @@ -14,9 +15,10 @@ class Gem::Commands::EnvironmentCommand < Gem::Command version display the gem format version remotesources display the remote gem servers platform display the supported gem platforms + credentials display the path where credentials are stored <omitted> display everything EOF - return args.gsub(/^\s+/, "") + args.gsub(/^\s+/, "") end def description # :nodoc: @@ -36,6 +38,7 @@ keys: :verbose: Verbosity of the gem command. false, true, and :really are the levels :update_sources: Enable/disable automatic updating of repository metadata + :concurrent_downloads: The number of gem downloads to perform concurrently :backtrace: Print backtrace when RubyGems encounters an error :gempath: The paths in which to look for gems :disable_default_gem_server: Force specification of gem server host on push @@ -87,6 +90,8 @@ lib/rubygems/defaults/operating_system.rb Gem.sources.to_a.join("\n") when /^platform/ then Gem.platforms.join(File::PATH_SEPARATOR) + when /^credentials/, /^creds/ then + Gem.configuration.credentials_path when nil then show_environment else @@ -107,14 +112,14 @@ lib/rubygems/defaults/operating_system.rb out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n" - out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}" - out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL - out << ") [#{RUBY_PLATFORM}]\n" + out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE} patchlevel #{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]\n" out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n" out << " - USER INSTALLATION DIRECTORY: #{Gem.user_dir}\n" + out << " - CREDENTIALS FILE: #{Gem.configuration.credentials_path}\n" + out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil? out << " - RUBY EXECUTABLE: #{Gem.ruby}\n" @@ -172,6 +177,6 @@ lib/rubygems/defaults/operating_system.rb end end - return nil + nil end end diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb new file mode 100644 index 0000000000..1feafbdd35 --- /dev/null +++ b/lib/rubygems/commands/exec_command.rb @@ -0,0 +1,259 @@ +# frozen_string_literal: true + +require_relative "../command" +require_relative "../dependency_installer" +require_relative "../gem_runner" +require_relative "../package" +require_relative "../version_option" + +class Gem::Commands::ExecCommand < Gem::Command + include Gem::VersionOption + + def initialize + super "exec", "Run a command from a gem", { + version: Gem::Requirement.default, + } + + add_version_option + add_prerelease_option "to be installed" + + add_option "-g", "--gem GEM", "run the executable from the given gem" do |value, options| + options[:gem_name] = value + end + + add_option(:"Install/Update", "--conservative", + "Prefer the most recent installed version, ", + "rather than the latest version overall") do |_value, options| + options[:conservative] = true + end + end + + def arguments # :nodoc: + "COMMAND the executable command to run" + end + + def defaults_str # :nodoc: + "--version '#{Gem::Requirement.default}'" + end + + def description # :nodoc: + <<-EOF +The exec command handles installing (if necessary) and running an executable +from a gem, regardless of whether that gem is currently installed. + +The exec command can be thought of as a shortcut to running `gem install` and +then the executable from the installed gem. + +For example, `gem exec rails new .` will run `rails new .` in the current +directory, without having to manually run `gem install rails`. +Additionally, the exec command ensures the most recent version of the gem +is used (unless run with `--conservative`), and that the gem is not installed +to the same gem path as user-installed gems. + EOF + end + + def usage # :nodoc: + "#{program_name} [options --] COMMAND [args]" + end + + def execute + check_executable + + print_command + if options[:gem_name] == "gem" && options[:executable] == "gem" + set_gem_exec_install_paths + Gem::GemRunner.new.run options[:args] + return + elsif options[:conservative] + install_if_needed + else + install + activate! + end + + load! + end + + private + + def handle_options(args) + args = add_extra_args(args) + check_deprecated_options(args) + @options = Marshal.load Marshal.dump @defaults # deep copy + parser.order!(args) do |v| + # put the non-option back at the front of the list of arguments + args.unshift(v) + + # stop parsing once we hit the first non-option, + # so you can call `gem exec rails --version` and it prints the rails + # version rather than rubygem's + break + end + @options[:args] = args + + options[:executable], gem_version = extract_gem_name_and_version(options[:args].shift) + options[:gem_name] ||= options[:executable] + + if gem_version + if options[:version].none? + options[:version] = Gem::Requirement.new(gem_version) + else + options[:version].concat [gem_version] + end + end + + if options[:prerelease] && !options[:version].prerelease? + if options[:version].none? + options[:version] = Gem::Requirement.default_prerelease + else + options[:version].concat [Gem::Requirement.default_prerelease] + end + end + end + + def check_executable + if options[:executable].nil? + raise Gem::CommandLineError, + "Please specify an executable to run (e.g. #{program_name} COMMAND)" + end + end + + def print_command + verbose "running #{program_name} with:\n" + opts = options.reject {|_, v| v.nil? || Array(v).empty? } + max_length = opts.map {|k, _| k.size }.max + opts.each do |k, v| + next if v.nil? + verbose "\t#{k.to_s.rjust(max_length)}: #{v}" + end + verbose "" + end + + def install_if_needed + activate! + rescue Gem::MissingSpecError + verbose "#{Gem::Dependency.new(options[:gem_name], options[:version])} not available locally, installing from remote" + install + activate! + end + + def set_gem_exec_install_paths + home = Gem.dir + + ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR) + ENV["GEM_HOME"] = home + Gem.clear_paths + end + + def install + set_gem_exec_install_paths + + gem_name = options[:gem_name] + gem_version = options[:version] + + install_options = options.merge( + minimal_deps: false, + wrappers: true + ) + + suppress_always_install do + dep_installer = Gem::DependencyInstaller.new install_options + + request_set = dep_installer.resolve_dependencies gem_name, gem_version + + verbose "Gems to install:" + request_set.sorted_requests.each do |activation_request| + verbose "\t#{activation_request.full_name}" + end + + request_set.install install_options + end + + Gem::Specification.reset + rescue Gem::InstallError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + terminate_interaction 1 + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" + terminate_interaction 2 + rescue Gem::GemNotFoundException => e + show_lookup_failure e.name, e.version, e.errors, false + + terminate_interaction 2 + rescue Gem::UnsatisfiableDependencyError => e + show_lookup_failure e.name, e.version, e.errors, false, + "'#{gem_name}' (#{gem_version})" + + terminate_interaction 2 + end + + def activate! + gem(options[:gem_name], options[:version]) + Gem.finish_resolve + + verbose "activated #{options[:gem_name]} (#{Gem.loaded_specs[options[:gem_name]].version})" + end + + def load! + argv = ARGV.clone + ARGV.replace options[:args] + + executable = options[:executable] + + contains_executable = Gem.loaded_specs.values.select do |spec| + spec.executables.include?(executable) + end + + if contains_executable.any? {|s| s.name == executable } + contains_executable.select! {|s| s.name == executable } + end + + if contains_executable.empty? + spec = Gem.loaded_specs[executable] + + if spec.nil? || spec.executables.empty? + alert_error "Failed to load executable `#{executable}`," \ + " are you sure the gem `#{options[:gem_name]}` contains it?" + terminate_interaction 1 + end + + if spec.executables.size > 1 + alert_error "Ambiguous which executable from gem `#{executable}` should be run: " \ + "the options are #{spec.executables.sort}, specify one via COMMAND, and use `-g` and `-v` to specify gem and version" + terminate_interaction 1 + end + + contains_executable << spec + executable = spec.executable + end + + if contains_executable.size > 1 + alert_error "Ambiguous which gem `#{executable}` should come from: " \ + "the options are #{contains_executable.map(&:name)}, " \ + "specify one via `-g`" + terminate_interaction 1 + end + + old_exe = $0 + $0 = executable + load Gem.activate_bin_path(contains_executable.first.name, executable, ">= 0.a") + ensure + $0 = old_exe if old_exe + ARGV.replace argv + end + + def suppress_always_install + name = :always_install + cls = ::Gem::Resolver::InstallerSet + method = cls.instance_method(name) + cls.remove_method(name) + cls.define_method(name) { [] } + + begin + yield + ensure + cls.remove_method(name) + cls.define_method(name, method) + end + end +end diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb index 5eb45d259c..8e64a18cee 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -9,8 +10,8 @@ class Gem::Commands::FetchCommand < Gem::Command def initialize defaults = { - :suggest_alternate => true, - :version => Gem::Requirement.default, + suggest_alternate: true, + version: Gem::Requirement.default, } super "fetch", "Download a gem and place it in the current directory", defaults @@ -55,13 +56,24 @@ then repackaging it. if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end def execute check_version + + exit_code = fetch_gems + + terminate_interaction exit_code + end + + private + + def fetch_gems + exit_code = 0 + version = options[:version] platform = Gem.platforms.last @@ -85,10 +97,13 @@ then repackaging it. if spec.nil? show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain] + exit_code |= 2 next end source.download spec say "Downloaded #{spec.full_name}" end + + exit_code end end diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index bc71e60ff0..13be92593b 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -1,85 +1,51 @@ # frozen_string_literal: true + require_relative "../command" -require_relative "../indexer" -## -# Generates a index files for use as a gem server. -# -# See `gem help generate_index` +unless defined? Gem::Commands::GenerateIndexCommand + class Gem::Commands::GenerateIndexCommand < Gem::Command + module RubygemsTrampoline + def description # :nodoc: + <<~EOF + The generate_index command has been moved to the rubygems-generate_index gem. + EOF + end -class Gem::Commands::GenerateIndexCommand < Gem::Command - def initialize - super "generate_index", - "Generates the index files for a gem server directory", - :directory => ".", :build_modern => true + def execute + alert_error "Install the rubygems-generate_index gem for the generate_index command" + end - add_option "-d", "--directory=DIRNAME", - "repository base dir containing gems subdir" do |dir, options| - options[:directory] = File.expand_path dir + def invoke_with_build_args(args, build_args) + name = "rubygems-generate_index" + spec = begin + Gem::Specification.find_by_name(name) + rescue Gem::LoadError + require "rubygems/dependency_installer" + Gem.install(name, Gem::Requirement.default, Gem::DependencyInstaller::DEFAULT_OPTIONS).find {|s| s.name == name } + end + + # remove the methods defined in this file so that the methods defined in the gem are used instead, + # and without a method redefinition warning + %w[description execute invoke_with_build_args].each do |method| + RubygemsTrampoline.remove_method(method) + end + self.class.singleton_class.remove_method(:new) + + spec.activate + Gem.load_plugin_files spec.matches_for_glob("rubygems_plugin#{Gem.suffix_pattern}") + + self.class.new.invoke_with_build_args(args, build_args) + end end + private_constant :RubygemsTrampoline - add_option "--[no-]modern", - "Generate indexes for RubyGems", - "(always true)" do |value, options| - options[:build_modern] = value + # remove_method(:initialize) warns, but removing new does not warn + def self.new + command = allocate + command.send(:initialize, "generate_index", "Generates the index files for a gem server directory (requires rubygems-generate_index)") + command 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| - options[:update] = value - end - end - - def defaults_str # :nodoc: - "--directory . --modern" - end - - def description # :nodoc: - <<-EOF -The generate_index command creates a set of indexes for serving gems -statically. The command expects a 'gems' directory under the path given to -the --directory option. The given directory will be the directory you serve -as the gem repository. - -For `gem generate_index --directory /path/to/repo`, expose /path/to/repo via -your HTTP server configuration (not /path/to/repo/gems). - -When done, it will generate a set of files like this: - - gems/*.gem # .gem files you want to - # index - - specs.<version>.gz # specs index - latest_specs.<version>.gz # latest specs index - prerelease_specs.<version>.gz # prerelease specs index - quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file - -The .rz extension files are compressed with the inflate algorithm. -The Marshal version number comes from ruby's Marshal::MAJOR_VERSION and -Marshal::MINOR_VERSION constants. It is used to ensure compatibility. - EOF - end - - def execute - # This is always true because it's the only way now. - options[:build_modern] = true - - if !File.exist?(options[:directory]) || - !File.directory?(options[:directory]) - alert_error "unknown directory name #{options[:directory]}." - terminate_interaction 1 - else - indexer = Gem::Indexer.new options.delete(:directory), options - - if options[:update] - indexer.update_index - else - indexer.generate_index - end - end + prepend(RubygemsTrampoline) end end diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index bf4ffefbb7..664f400561 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::HelpCommand < Gem::Command @@ -58,7 +59,7 @@ multiple environments. The RubyGems implementation is designed to be compatible with Bundler's Gemfile format. You can see additional documentation on the format at: - http://bundler.io + https://bundler.io RubyGems automatically looks for these gem dependencies files: @@ -89,7 +90,9 @@ Use #gem to declare which gems you directly depend upon: To depend on a specific set of versions: - gem 'rake', '~> 10.3', '>= 10.3.2' + gem 'rake', '>= 10.3.2' + # or for multiple version restrictions + gem 'rake', '>= 10.3.2', "< 13" RubyGems will require the gem name when activating the gem using the RUBYGEMS_GEMDEPS environment variable or Gem::use_gemdeps. Use the @@ -171,7 +174,7 @@ and #platforms methods: See the bundler Gemfile manual page for a list of platforms supported in a gem dependencies file.: - http://bundler.io/v1.6/man/gemfile.5.html + https://bundler.io/v2.5/man/gemfile.5.html Ruby Version and Engine Dependency ================================== @@ -268,7 +271,7 @@ Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's platform. EOF - # NOTE when updating also update Gem::Command::HELP + # NOTE: when updating also update Gem::Command::HELP SUBCOMMANDS = [ ["commands", :show_commands], @@ -323,16 +326,16 @@ platform. margin_width = 4 - desc_width = @command_manager.command_names.map {|n| n.size }.max + 4 + desc_width = @command_manager.command_names.map(&:size).max + 4 summary_width = 80 - margin_width - desc_width wrap_indent = " " * (margin_width + desc_width) - format = "#{' ' * margin_width}%-#{desc_width}s%s" + format = "#{" " * margin_width}%-#{desc_width}s%s" @command_manager.command_names.each do |cmd_name| command = @command_manager[cmd_name] - next if command.deprecated? + next if command&.deprecated? summary = if command @@ -342,7 +345,7 @@ platform. end summary = wrap(summary, summary_width).split "\n" - out << sprintf(format, cmd_name, summary.shift) + out << format(format, cmd_name, summary.shift) until summary.empty? do out << "#{wrap_indent}#{summary.shift}" end @@ -366,7 +369,7 @@ platform. command = @command_manager[possibilities.first] command.invoke("--help") elsif possibilities.size > 1 - alert_warning "Ambiguous command #{command_name} (#{possibilities.join(', ')})" + alert_warning "Ambiguous command #{command_name} (#{possibilities.join(", ")})" else alert_warning "Unknown command #{command_name}. Try: gem help commands" end diff --git a/lib/rubygems/commands/info_command.rb b/lib/rubygems/commands/info_command.rb index ced7751ff5..f65c639662 100644 --- a/lib/rubygems/commands/info_command.rb +++ b/lib/rubygems/commands/info_command.rb @@ -8,8 +8,8 @@ class Gem::Commands::InfoCommand < Gem::Command def initialize super "info", "Show information for the given gem", - :name => //, :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + name: //, domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index c04c01f258..6d3beec0b4 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../install_update_options" require_relative "../dependency_installer" @@ -22,11 +23,11 @@ class Gem::Commands::InstallCommand < Gem::Command def initialize defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ - :format_executable => false, - :lock => true, - :suggest_alternate => true, - :version => Gem::Requirement.default, - :without_groups => [], + format_executable: false, + lock: true, + suggest_alternate: true, + version: Gem::Requirement.default, + without_groups: [], }) defaults.merge!(install_update_options) @@ -47,7 +48,7 @@ class Gem::Commands::InstallCommand < Gem::Command end def defaults_str # :nodoc: - "--both --version '#{Gem::Requirement.default}' --no-force\n" + + "--both --version '#{Gem::Requirement.default}' --no-force\n" \ "--install-dir #{Gem.dir} --lock\n" + install_update_defaults_str end @@ -135,18 +136,11 @@ You can use `i` command instead of `install`. "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags" end - def check_install_dir # :nodoc: - if options[:install_dir] && options[:user_install] - alert_error "Use --install-dir or --user-install but not both" - terminate_interaction 1 - end - end - def check_version # :nodoc: if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end @@ -161,7 +155,6 @@ You can use `i` command instead of `install`. ENV.delete "GEM_PATH" if options[:install_dir].nil? - check_install_dir check_version load_hooks @@ -170,7 +163,7 @@ You can use `i` command instead of `install`. show_installed - say update_suggestion if eglible_for_update? + say update_suggestion if eligible_for_update? terminate_interaction exit_code end @@ -231,9 +224,8 @@ You can use `i` command instead of `install`. rescue Gem::InstallError => e alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 1 - rescue Gem::GemNotFoundException => e - show_lookup_failure e.name, e.version, e.errors, suppress_suggestions - + rescue Gem::DependencyResolutionError => e + alert_error "Error installing #{gem_name}:\n\t#{e.message}" exit_code |= 2 rescue Gem::UnsatisfiableDependencyError => e show_lookup_failure e.name, e.version, e.errors, suppress_suggestions, @@ -250,11 +242,7 @@ You can use `i` command instead of `install`. # Loads post-install hooks def load_hooks # :nodoc: - if options[:install_as_default] - require_relative "../install_default_message" - else - require_relative "../install_message" - end + require_relative "../install_message" require_relative "../rdoc" end @@ -262,7 +250,7 @@ You can use `i` command instead of `install`. return unless errors errors.each do |x| - return unless Gem::SourceFetchProblem === x + next unless Gem::SourceFetchProblem === x require_relative "../uri" msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}" diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index 011873b99c..fab4b73814 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -10,8 +11,8 @@ class Gem::Commands::ListCommand < Gem::Command def initialize super "list", "Display local gems whose name matches REGEXP", - :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb index da636492c9..f7fd5ada16 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::LockCommand < Gem::Command def initialize super "lock", "Generate a lockdown list of gems", - :strict => false + strict: false add_option "-s", "--[no-]strict", "fail if unable to satisfy a dependency" do |strict, options| diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index b633cd3d81..b91a8db12d 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::MirrorCommand diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index d5283f72dd..0fe90dc8b8 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -69,9 +70,7 @@ class Gem::Commands::OpenCommand < Gem::Command end def open_editor(path) - Dir.chdir(path) do - system(*@editor.split(/\s+/) + [path]) - end + system(*@editor.split(/\s+/) + [path], { chdir: path }) end def spec_for(name) diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index 1785194389..08a9221a26 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../spec_fetcher" diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 959a6186dc..675e866734 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -38,7 +39,7 @@ permission to. add_proxy_option add_key_option add_otp_option - defaults.merge! :add => [], :remove => [] + defaults.merge! add: [], remove: [] add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options| options[:add] << value @@ -74,11 +75,12 @@ permission to. end with_response response do |resp| - owners = Gem::SafeYAML.load clean_text(resp.body) + owners = Gem::SafeYAML.safe_load clean_text(resp.body) say "Owners for gem: #{name}" owners.each do |owner| - say "- #{owner['email'] || owner['handle'] || owner['id']}" + identifier = owner["email"] || owner["handle"] || owner["id"] + say "- #{identifier} (#{owner["role"]})" end end end @@ -93,14 +95,14 @@ permission to. def manage_owners(method, name, owners) owners.each do |owner| - begin - response = send_owner_request(method, name, owner) - action = method == :delete ? "Removing" : "Adding" - - with_response response, "#{action} #{owner}" - rescue - # ignore - end + response = send_owner_request(method, name, owner) + action = method == :delete ? "Removing" : "Adding" + + with_response response, "#{action} #{owner}" + rescue Gem::WebauthnVerificationError => e + raise e + rescue StandardError + # ignore early exits to allow for completing the iteration of all owners end end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 72db53ef37..10978c2af7 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../package" require_relative "../installer" @@ -10,10 +11,10 @@ class Gem::Commands::PristineCommand < Gem::Command def initialize super "pristine", "Restores installed gems to pristine condition from files located in the gem cache", - :version => Gem::Requirement.default, - :extensions => true, - :extensions_set => false, - :all => false + version: Gem::Requirement.default, + extensions: true, + extensions_set: false, + all: false add_option("--all", "Restore all installed gems to pristine", @@ -34,6 +35,11 @@ class Gem::Commands::PristineCommand < Gem::Command options[:extensions] = value end + add_option("--only-missing-extensions", + "Only restore gems with missing extensions") do |value, options| + options[:only_missing_extensions] = value + end + add_option("--only-executables", "Only restore executables") do |value, options| options[:only_executables] = value @@ -51,7 +57,7 @@ class Gem::Commands::PristineCommand < Gem::Command end add_option("-i", "--install-dir DIR", - "Gem repository to get binstubs and plugins installed") do |value, options| + "Gem repository to get gems restored") do |value, options| options[:install_dir] = File.expand_path(value) end @@ -82,6 +88,10 @@ If you have made modifications to an installed gem, the pristine command will revert them. All extensions are rebuilt and all bin stubs for the gem are regenerated after checking for modifications. +Rebuilding extensions also refreshes C-extension gems against updated system +libraries (for example after OS or package upgrades) to avoid mismatches like +outdated library version warnings. + If the cached gem cannot be found it will be downloaded. If --no-extensions is provided pristine will not attempt to restore a gem @@ -97,55 +107,73 @@ extensions will be restored. end def execute + install_dir = options[:install_dir] + + specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record + specs = if options[:all] - Gem::Specification.map + specification_record.map # `--extensions` must be explicitly given to pristine only gems # with extensions. elsif options[:extensions_set] && options[:extensions] && options[:args].empty? - Gem::Specification.select do |spec| + specification_record.select do |spec| spec.extensions && !spec.extensions.empty? end + elsif options[:only_missing_extensions] + specification_record.select(&:missing_extensions?) else - get_all_gem_names.sort.map do |gem_name| - Gem::Specification.find_all_by_name(gem_name, options[:version]).reverse - end.flatten + get_all_gem_names.sort.flat_map do |gem_name| + specification_record.find_all_by_name(gem_name, options[:version]).reverse + end end - specs = specs.select {|spec| RUBY_ENGINE == spec.platform || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } + specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? + if options[:only_missing_extensions] + say "No gems with missing extensions to restore" + return + end + raise Gem::Exception, "Failed to find gems #{options[:args]} #{options[:version]}" end say "Restoring gems to pristine condition..." - specs.each do |spec| - if spec.default_gem? - say "Skipped #{spec.full_name}, it is a default gem" - next + specs.group_by(&:full_name_with_location).values.each do |grouped_specs| + spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first + + only_executables = options[:only_executables] + only_plugins = options[:only_plugins] + + unless only_executables || only_plugins + # Default gemspecs include changes provided by ruby-core installer that + # can't currently be pristined (inclusion of compiled extension targets in + # the file list). So stick to resetting executables if it's a default gem. + only_executables = true if spec.default_gem? end - if options.has_key? :skip + if options.key? :skip if options[:skip].include? spec.name say "Skipped #{spec.full_name}, it was given through options" next end end - unless spec.extensions.empty? || options[:extensions] || options[:only_executables] || options[:only_plugins] - say "Skipped #{spec.full_name}, it needs to compile an extension" + unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins + say "Skipped #{spec.full_name_with_location}, it needs to compile an extension" next end gem = spec.cache_file - unless File.exist?(gem) || options[:only_executables] || options[:only_plugins] + unless File.exist?(gem) || only_executables || only_plugins require_relative "../remote_fetcher" - say "Cached gem for #{spec.full_name} not found, attempting to fetch..." + say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..." dep = Gem::Dependency.new spec.name, spec.version found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep @@ -168,21 +196,20 @@ extensions will be restored. end bin_dir = options[:bin_dir] if options[:bin_dir] - install_dir = options[:install_dir] if options[:install_dir] installer_options = { - :wrappers => true, - :force => true, - :install_dir => install_dir || spec.base_dir, - :env_shebang => env_shebang, - :build_args => spec.build_args, - :bin_dir => bin_dir, + wrappers: true, + force: true, + install_dir: install_dir || spec.base_dir, + env_shebang: env_shebang, + build_args: spec.build_args, + bin_dir: bin_dir, } - if options[:only_executables] + if only_executables installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_bin - elsif options[:only_plugins] + elsif only_plugins installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_plugins else @@ -190,7 +217,7 @@ extensions will be restored. installer.install end - say "Restored #{spec.full_name}" + say "Restored #{spec.full_name_with_location}" end end end diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 46b65f4e15..02931b3025 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -29,7 +30,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo end def initialize - super "push", "Push a gem up to the gem server", :host => self.host + super "push", "Push a gem up to the gem server", host: host, attestations: [] @user_defined_host = false @@ -44,6 +45,11 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo @user_defined_host = true end + add_option("--attestation FILE", + "Push with sigstore attestations") do |value, options| + options[:attestations] << value + end + @host = nil end @@ -74,7 +80,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo @host ||= push_host # Always include @host, even if it's nil - args += [ @host, push_host ] + args += [@host, push_host] say "Pushing gem to #{@host || Gem.host}..." @@ -86,12 +92,77 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo private def send_push_request(name, args) - rubygems_api_request(*args, scope: get_push_scope) do |request| - request.body = Gem.read_binary name - request.add_field "Content-Length", request.body.size + # Always honor explicit --attestation option + # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby) + if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"]) + send_push_request_with_attestation(name, args) + else + send_push_request_without_attestation(name, args) + end + end + + def send_push_request_without_attestation(name, args) + scope = get_push_scope + rubygems_api_request(*args, scope: scope) do |request| + body = Gem.read_binary name + request.body = body request.add_field "Content-Type", "application/octet-stream" - request.add_field "Authorization", api_key + request.add_field "Content-Length", request.body.size + request.add_field "Authorization", api_key + end + end + + def send_push_request_with_attestation(name, args) + attestations = if options[:attestations].any? + options[:attestations].map do |attestation| + Gem.read_binary(attestation) + end + else + bundle_path = attest!(name) + begin + [Gem.read_binary(bundle_path)] + ensure + File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path) + end + end + bundles = "[" + attestations.join(",") + "]" + + rubygems_api_request(*args, scope: get_push_scope) do |request| + request.set_form([ + ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }], + ["attestations", bundles, { content_type: "application/json" }], + ], "multipart/form-data") + request.add_field "Authorization", api_key end + rescue StandardError => e + message = "Failed to push with attestation, retrying without attestation.\n" + message += if Gem.configuration.really_verbose + e.full_message + else + e.message + end + alert_warning message + send_push_request_without_attestation(name, args) + end + + def attest!(name) + require "open3" + require "tempfile" + + tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"]) + bundle = tempfile.path + tempfile.close(false) + + env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h + out, st = Open3.capture2e( + env, + Gem.ruby, "-S", "gem", "exec", "--conservative", + "sigstore-cli", "sign", name, "--bundle", bundle, + unsetenv_others: true + ) + raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success? + + bundle end def get_hosts_for(name) @@ -106,4 +177,9 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo def get_push_scope :push_rubygem end + + def attestation_supported_host? + host = (@host || Gem.host).to_s.chomp("/") + host == Gem::DEFAULT_HOST + end end diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb deleted file mode 100644 index c6315acf8c..0000000000 --- a/lib/rubygems/commands/query_command.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true -require_relative "../command" -require_relative "../query_utils" -require_relative "../deprecate" - -class Gem::Commands::QueryCommand < Gem::Command - extend Gem::Deprecate - rubygems_deprecate_command - - include Gem::QueryUtils - - alias warning_without_suggested_alternatives deprecation_warning - def deprecation_warning - warning_without_suggested_alternatives - - message = "It is recommended that you use `gem search` or `gem list` instead.\n" - alert_warning message unless Gem::Deprecate.skip - end - - def initialize(name = "query", - summary = "Query gem information in local or remote repositories") - super name, summary, - :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default - - add_option("-n", "--name-matches REGEXP", - "Name of gem(s) to query on matches the", - "provided REGEXP") do |value, options| - options[:name] = /#{value}/i - end - - add_query_options - end - - def description # :nodoc: - <<-EOF -The query command is the basis for the list and search commands. - -You should really use the list and search commands instead. This command -is too hard to use. - EOF - end -end diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index a998a9704c..62c4bf8ce9 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../rdoc" @@ -9,8 +10,8 @@ class Gem::Commands::RdocCommand < Gem::Command def initialize super "rdoc", "Generates RDoc for pre-installed gems", - :version => Gem::Requirement.default, - :include_rdoc => false, :include_ri => true, :overwrite => false + version: Gem::Requirement.default, + include_rdoc: false, include_ri: true, overwrite: false add_option("--all", "Generate RDoc/RI documentation for all", @@ -63,9 +64,9 @@ Use --overwrite to force rebuilding of documentation. specs = if options[:all] Gem::Specification.to_a else - get_all_gem_names.map do |name| + get_all_gem_names.flat_map do |name| Gem::Specification.find_by_name name, options[:version] - end.flatten.uniq + end.uniq end if specs.empty? @@ -83,14 +84,7 @@ Use --overwrite to force rebuilding of documentation. FileUtils.rm_rf File.join(spec.doc_dir, "rdoc") end - begin - doc.generate - rescue Errno::ENOENT => e - match = / - /.match(e.message) - alert_error "Unable to document #{spec.full_name}, " \ - " #{match.post_match} is missing, skipping" - terminate_interaction 1 if specs.length == 1 - end + doc.generate end end end diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb new file mode 100644 index 0000000000..23b9d7b3ba --- /dev/null +++ b/lib/rubygems/commands/rebuild_command.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: true + +require "digest" +require "fileutils" +require "tmpdir" +require_relative "../gemspec_helpers" +require_relative "../package" + +class Gem::Commands::RebuildCommand < Gem::Command + include Gem::GemspecHelpers + + def initialize + super "rebuild", "Attempt to reproduce a build of a gem." + + add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options| + options[:diff] = true + end + + add_option "--force", "Skip validation of the spec." do |_value, options| + options[:force] = true + end + + add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options| + options[:strict] = true + end + + add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options| + options[:source] = value + end + + add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options| + options[:original_gem_file] = value + end + + add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options| + options[:gemspec_file] = value + end + + add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| + options[:build_path] = value + end + end + + def arguments # :nodoc: + "GEM_NAME gem name on gem server\n" \ + "GEM_VERSION gem version you are attempting to rebuild" + end + + def description # :nodoc: + <<-EOF +The rebuild command allows you to (attempt to) reproduce a build of a gem +from a ruby gemspec. + +This command assumes the gemspec can be built with the `gem build` command. +If you use any of `gem build`, `rake build`, or`rake release` in the +build/release process for a gem, it is a potential candidate. + +You will need to match the RubyGems version used, since this is included in +the Gem metadata. + +If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will +require more effort to reproduce a build. For example, it might require +more precisely matched versions of Ruby and/or Bundler to be used. + EOF + end + + def usage # :nodoc: + "#{program_name} GEM_NAME GEM_VERSION" + end + + def execute + gem_name, gem_version = get_gem_name_and_version + + old_dir, new_dir = prep_dirs + + gem_filename = "#{gem_name}-#{gem_version}.gem" + old_file = File.join(old_dir, gem_filename) + new_file = File.join(new_dir, gem_filename) + + if options[:original_gem_file] + FileUtils.copy_file(options[:original_gem_file], old_file) + else + download_gem(gem_name, gem_version, old_file) + end + + rg_version = rubygems_version(old_file) + unless rg_version == Gem::VERSION + alert_error <<-EOF +You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with. + +#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}. +Gem files include the version of RubyGems used to build them. +This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}. + +You're using RubyGems v#{Gem::VERSION}. + +Please install RubyGems v#{rg_version} and try again. + EOF + terminate_interaction 1 + end + + source_date_epoch = get_timestamp(old_file).to_s + + if build_path = options[:build_path] + Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) } + else + build_gem(gem_name, source_date_epoch, new_file) + end + + compare(source_date_epoch, old_file, new_file) + end + + private + + def sha256(file) + Digest::SHA256.hexdigest(Gem.read_binary(file)) + end + + def get_timestamp(file) + mtime = nil + File.open(file, Gem.binary_mode) do |f| + Gem::Package::TarReader.new(f) do |tar| + mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime } + end + end + + mtime + end + + def compare(source_date_epoch, old_file, new_file) + date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z") + + old_hash = sha256(old_file) + new_hash = sha256(new_file) + + say + say "Built at: #{date} (#{source_date_epoch})" + say "Original build saved to: #{old_file}" + say "Reproduced build saved to: #{new_file}" + say "Working directory: #{options[:build_path] || Dir.pwd}" + say + say "Hash comparison:" + say " #{old_hash}\t#{old_file}" + say " #{new_hash}\t#{new_file}" + say + + if old_hash == new_hash + say "SUCCESS - original and rebuild hashes matched" + else + say "FAILURE - original and rebuild hashes did not match" + say + + if options[:diff] + if system("diffoscope", old_file, new_file).nil? + alert_error "error: could not find `diffoscope` executable" + end + else + say "Pass --diff for more details (requires diffoscope to be installed)." + end + + terminate_interaction 1 + end + end + + def prep_dirs + rebuild_dir = Dir.mktmpdir("gem_rebuild") + old_dir = File.join(rebuild_dir, "old") + new_dir = File.join(rebuild_dir, "new") + + FileUtils.mkdir_p(old_dir) + FileUtils.mkdir_p(new_dir) + + [old_dir, new_dir] + end + + def get_gem_name_and_version + args = options[:args] || [] + if args.length == 2 + gem_name, gem_version = args + elsif args.length > 2 + raise Gem::CommandLineError, "Too many arguments" + else + raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)" + end + + [gem_name, gem_version] + end + + def build_gem(gem_name, source_date_epoch, output_file) + gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec") + + if gemspec + build_package(gemspec, source_date_epoch, output_file) + else + alert_error error_message(gem_name) + terminate_interaction(1) + end + end + + def build_package(gemspec, source_date_epoch, output_file) + with_source_date_epoch(source_date_epoch) do + spec = Gem::Specification.load(gemspec) + if spec + Gem::Package.build( + spec, + options[:force], + options[:strict], + output_file + ) + else + alert_error "Error loading gemspec. Aborting." + terminate_interaction 1 + end + end + end + + def with_source_date_epoch(source_date_epoch) + old_sde = ENV["SOURCE_DATE_EPOCH"] + ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s + + yield + ensure + ENV["SOURCE_DATE_EPOCH"] = old_sde + end + + def error_message(gem_name) + if gem_name + "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}" + else + "Couldn't find a gemspec file in #{Dir.pwd}" + end + end + + def download_gem(gem_name, gem_version, old_file) + # This code was based loosely off the `gem fetch` command. + version = "= #{gem_version}" + dep = Gem::Dependency.new gem_name, version + + specs_and_sources, errors = + Gem::SpecFetcher.fetcher.spec_for_dependency dep + + # There should never be more than one item in specs_and_sources, + # since we search for an exact version. + spec, source = specs_and_sources[0] + + if spec.nil? + show_lookup_failure gem_name, version, errors, options[:domain] + terminate_interaction 1 + end + + download_path = source.download spec + + FileUtils.move(download_path, old_file) + + say "Downloaded #{gem_name} version #{gem_version} as #{old_file}." + end + + def rubygems_version(gem_file) + Gem::Package.new(gem_file).spec.rubygems_version + end +end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index 3f8f7e13f2..50e161ac9b 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -7,8 +8,8 @@ class Gem::Commands::SearchCommand < Gem::Command def initialize super "search", "Display remote gems whose name matches REGEXP", - :domain => :remote, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :remote, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb index 56be07c79d..f1dde4aa02 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::ServerCommand diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index c779b7c244..175599967c 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" ## @@ -6,19 +7,19 @@ require_relative "../command" # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = /^#\s*[\d.a-zA-Z]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze - VERSION_MATCHER = /^#\s*([\d.a-zA-Z]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze + HISTORY_HEADER = %r{^##\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} + VERSION_MATCHER = %r{^##\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} ENV_PATHS = %w[/usr/bin/env /bin/env].freeze def initialize super "setup", "Install RubyGems", - :format_executable => false, :document => %w[ri], - :force => true, - :site_or_vendor => "sitelibdir", - :destdir => "", :prefix => "", :previous_version => "", - :regenerate_binstubs => true, - :regenerate_plugins => true + format_executable: false, document: %w[ri], + force: true, + site_or_vendor: "sitelibdir", + destdir: "", prefix: "", previous_version: "", + regenerate_binstubs: true, + regenerate_plugins: true add_option "--previous-version=VERSION", "Previous version of RubyGems", @@ -54,9 +55,9 @@ class Gem::Commands::SetupCommand < Gem::Command "List the documentation types you wish to", "generate. For example: rdoc,ri" do |value, options| options[:document] = case value - when nil then %w[rdoc ri] - when false then [] - else value + when nil then %w[rdoc ri] + when false then [] + else value end end @@ -106,15 +107,6 @@ class Gem::Commands::SetupCommand < Gem::Command @verbose = nil end - def check_ruby_version - required_version = Gem::Requirement.new ">= 2.6.0" - - unless required_version.satisfied_by? Gem.ruby_version - alert_error "Expected Ruby version #{required_version}, is #{Gem.ruby_version}" - terminate_interaction 1 - end - end - def defaults_str # :nodoc: "--format-executable --document ri --regenerate-binstubs" end @@ -133,7 +125,7 @@ prefix and suffix. If ruby was installed as `ruby18`, gem will be installed as `gem18`. By default, this RubyGems will install gem as: - #{Gem.default_exec_format % 'gem'} + #{Gem.default_exec_format % "gem"} EOF end @@ -147,8 +139,6 @@ By default, this RubyGems will install gem as: def execute @verbose = Gem.configuration.really_verbose - check_ruby_version - require "fileutils" if Gem.configuration.really_verbose extend FileUtils::Verbose @@ -242,9 +232,9 @@ By default, this RubyGems will install gem as: end def install_executables(bin_dir) - prog_mode = options[:prog_mode] || 0755 + prog_mode = options[:prog_mode] || 0o755 - executables = { "gem" => "bin" } + executables = { "gem" => "exe" } executables.each do |tool, path| say "Installing #{tool} executable" if @verbose @@ -264,7 +254,7 @@ By default, this RubyGems will install gem as: fp.puts bin.join end - install bin_tmp_file, dest_file, :mode => prog_mode + install bin_tmp_file, dest_file, mode: prog_mode bin_file_names << dest_file ensure rm bin_tmp_file @@ -278,15 +268,11 @@ By default, this RubyGems will install gem as: File.open bin_cmd_file, "w" do |file| file.puts <<-TEXT @ECHO OFF - IF NOT "%~f0" == "~f0" GOTO :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "#{dest_file}" %1 %2 %3 %4 %5 %6 %7 %8 %9 - GOTO :EOF - :WinNT - @"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* + @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* TEXT end - install bin_cmd_file, "#{dest_file}.bat", :mode => prog_mode + install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode ensure rm bin_cmd_file end @@ -339,6 +325,8 @@ By default, this RubyGems will install gem as: require_relative "../rdoc" + return false unless defined?(Gem::RDoc) + fake_spec = Gem::Specification.new "rubygems", Gem::VERSION def fake_spec.full_gem_path File.expand_path "../../..", __dir__ @@ -356,30 +344,39 @@ By default, this RubyGems will install gem as: say "Set the GEM_HOME environment variable if you want RDoc generated" end - return false + false end def install_default_bundler_gem(bin_dir) current_default_spec = Gem::Specification.default_stubs.find {|s| s.name == "bundler" } specs_dir = if current_default_spec && default_dir == Gem.default_dir + all_specs_current_version = Gem::Specification.stubs.select {|s| s.full_name == current_default_spec.full_name } + Gem::Specification.remove_spec current_default_spec loaded_from = current_default_spec.loaded_from File.delete(loaded_from) + + # Remove previous default gem executables if they were not shadowed by a regular gem + FileUtils.rm_rf current_default_spec.full_gem_path if all_specs_current_version.size == 1 + File.dirname(loaded_from) else target_specs_dir = File.join(default_dir, "specifications", "default") - mkdir_p target_specs_dir, :mode => 0755 + mkdir_p target_specs_dir, mode: 0o755 target_specs_dir end - bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } - default_spec_path = File.join(specs_dir, "#{bundler_spec.full_name}.gemspec") - Gem.write_binary(default_spec_path, bundler_spec.to_ruby) + new_bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } + full_name = new_bundler_spec.full_name + gemspec_path = "#{full_name}.gemspec" + + default_spec_path = File.join(specs_dir, gemspec_path) + Gem.write_binary(default_spec_path, new_bundler_spec.to_ruby) bundler_spec = Gem::Specification.load(default_spec_path) # Remove gemspec that was same version of vendored bundler. - normal_gemspec = File.join(default_dir, "specifications", "bundler-#{bundler_spec.version}.gemspec") + normal_gemspec = File.join(default_dir, "specifications", gemspec_path) if File.file? normal_gemspec File.delete normal_gemspec end @@ -387,39 +384,37 @@ By default, this RubyGems will install gem as: # Remove gem files that were same version of vendored bundler. if File.directory? bundler_spec.gems_dir Dir.entries(bundler_spec.gems_dir). - select {|default_gem| File.basename(default_gem) == "bundler-#{bundler_spec.version}" }. + select {|default_gem| File.basename(default_gem) == full_name }. each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) } end - bundler_bin_dir = bundler_spec.bin_dir - mkdir_p bundler_bin_dir, :mode => 0755 - bundler_spec.executables.each do |e| - cp File.join("bundler", bundler_spec.bindir, e), File.join(bundler_bin_dir, e) - end - require_relative "../installer" Dir.chdir("bundler") do - built_gem = Gem::Package.build(bundler_spec) + built_gem = Gem::Package.build(new_bundler_spec) begin - Gem::Installer.at( + installer = Gem::Installer.at( built_gem, env_shebang: options[:env_shebang], format_executable: options[:format_executable], force: options[:force], - install_as_default: true, bin_dir: bin_dir, install_dir: default_dir, wrappers: true - ).install + ) + # We need to install only executable and default spec files. + # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory. + installer.extract_bin + installer.generate_bin + installer.write_default_spec ensure FileUtils.rm_f built_gem end end - bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } + new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } - say "Bundler #{bundler_spec.version} installed" + say "Bundler #{new_bundler_spec.version} installed" end def make_destination_dirs @@ -429,10 +424,10 @@ By default, this RubyGems will install gem as: lib_dir, bin_dir = generate_default_dirs end - mkdir_p lib_dir, :mode => 0755 - mkdir_p bin_dir, :mode => 0755 + mkdir_p lib_dir, mode: 0o755 + mkdir_p bin_dir, mode: 0o755 - return lib_dir, bin_dir + [lib_dir, bin_dir] end def generate_default_man_dir @@ -575,8 +570,8 @@ abort "#{deprecation_message}" def uninstall_old_gemcutter require_relative "../uninstaller" - ui = Gem::Uninstaller.new("gemcutter", :all => true, :ignore => true, - :version => "< 0.4") + ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true, + version: "< 0.4") ui.uninstall rescue Gem::InstallError end @@ -587,6 +582,8 @@ abort "#{deprecation_message}" args = %w[--all --only-executables --silent] args << "--bindir=#{bindir}" + args << "--install-dir=#{default_dir}" + if options[:env_shebang] args << "--env-shebang" end @@ -638,10 +635,10 @@ abort "#{deprecation_message}" dest_file = File.join dest_dir, file dest_dir = File.dirname dest_file unless File.directory? dest_dir - mkdir_p dest_dir, :mode => 0755 + mkdir_p dest_dir, mode: 0o755 end - install file, dest_file, :mode => options[:data_mode] || 0644 + install file, dest_file, mode: options[:data_mode] || 0o644 end def remove_file_list(files, dir) diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb index 2660eee4f3..0f77908c5b 100644 --- a/lib/rubygems/commands/signin_command.rb +++ b/lib/rubygems/commands/signin_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../gemcutter_utilities" diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb index fa688ea3f8..bdd01e4393 100644 --- a/lib/rubygems/commands/signout_command.rb +++ b/lib/rubygems/commands/signout_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::SignoutCommand < Gem::Command diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index 5a8f5af9c3..b399af2bd3 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../remote_fetcher" require_relative "../spec_fetcher" @@ -17,6 +18,14 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:add] = value end + add_option "--append SOURCE_URI", "Append source (can be used multiple times)" do |value, options| + options[:append] = value + end + + add_option "-p", "--prepend SOURCE_URI", "Prepend source (can be used multiple times)" do |value, options| + options[:prepend] = value + end + add_option "-l", "--list", "List sources" do |value, options| options[:list] = value end @@ -25,8 +34,7 @@ class Gem::Commands::SourcesCommand < Gem::Command options[:remove] = value end - add_option "-c", "--clear-all", - "Remove all sources (clear the cache)" do |value, options| + add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options| options[:clear_all] = value end @@ -42,11 +50,8 @@ class Gem::Commands::SourcesCommand < Gem::Command end def add_source(source_uri) # :nodoc: - check_rubygems_https source_uri - - source = Gem::Source.new source_uri - - check_typo_squatting(source) + source = build_new_source(source_uri) + source_uri = source.uri.to_s begin if Gem.sources.include? source @@ -58,7 +63,55 @@ class Gem::Commands::SourcesCommand < Gem::Command say "#{source_uri} added to sources" end - rescue URI::Error, ArgumentError + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + + def append_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.append source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to end of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError + say "#{source_uri} is not a URI" + terminate_interaction 1 + rescue Gem::RemoteFetcher::FetchError => e + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" + terminate_interaction 1 + end + end + + def prepend_source(source_uri) # :nodoc: + source = build_new_source(source_uri) + source_uri = source.uri.to_s + + begin + source.load_specs :released + was_present = Gem.sources.include?(source) + Gem.sources.prepend source + Gem.configuration.write + + if was_present + say "#{source_uri} moved to top of sources" + else + say "#{source_uri} added to sources" + end + rescue Gem::URI::Error, ArgumentError say "#{source_uri} is not a URI" terminate_interaction 1 rescue Gem::RemoteFetcher::FetchError => e @@ -70,7 +123,7 @@ class Gem::Commands::SourcesCommand < Gem::Command 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 +#{source.uri} is too similar to https://rubygems.org Do you want to add this source? QUESTION @@ -79,11 +132,24 @@ Do you want to add this source? end end + def normalize_source_uri(source_uri) # :nodoc: + # Ensure the source URI has a trailing slash for proper RFC 2396 path merging + # Without a trailing slash, the last path segment is treated as a file and removed + # during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem") + # With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem") + uri = Gem::URI.parse(source_uri) + uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty? + uri.to_s + rescue Gem::URI::Error + # If parsing fails, return the original URI and let later validation handle it + source_uri + end + def check_rubygems_https(source_uri) # :nodoc: - uri = URI source_uri + uri = Gem::URI source_uri - if uri.scheme && uri.scheme.downcase == "http" && - uri.host.downcase == "rubygems.org" + if uri.scheme && uri.scheme.casecmp("http").zero? && + uri.host.casecmp("rubygems.org").zero? question = <<-QUESTION.chomp https://rubygems.org is recommended for security over #{uri} @@ -98,16 +164,16 @@ Do you want to add this insecure source? path = Gem.spec_cache_dir FileUtils.rm_rf path - unless File.exist? path - say "*** Removed specs cache ***" - else - unless File.writable? path - say "*** Unable to remove source cache (write protected) ***" - else + if File.exist? path + if File.writable? path say "*** Unable to remove source cache ***" + else + say "*** Unable to remove source cache (write protected) ***" end terminate_interaction 1 + else + say "*** Removed specs cache ***" end end @@ -127,7 +193,7 @@ yourself to use your own gem server. Without any arguments the sources lists your currently configured sources: $ gem sources - *** CURRENT SOURCES *** + *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW *** https://rubygems.org @@ -146,33 +212,49 @@ Since all of these sources point to the same set of gems you only need one of them in your list. https://rubygems.org is recommended as it brings the protections of an SSL connection to gem downloads. -To add a source use the --add argument: +To add a private gem source use the --prepend argument to insert it before +the default source. This is usually the best place for private gem sources: - $ gem sources --add https://rubygems.org - https://rubygems.org added to sources + $ gem sources --prepend https://my.private.source + https://my.private.source added to sources RubyGems will check to see if gems can be installed from the source given before it is added. +To add or move a source after all other sources, use --append: + + $ gem sources --append https://rubygems.org + https://rubygems.org moved to end of sources + To remove a source use the --remove argument: - $ gem sources --remove https://rubygems.org/ - https://rubygems.org/ removed from sources + $ gem sources --remove https://my.private.source/ + https://my.private.source/ removed from sources EOF end def list # :nodoc: - say "*** CURRENT SOURCES ***" + if configured_sources + header = "*** CURRENT SOURCES ***" + list = configured_sources + else + header = "*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***" + list = Gem.sources + end + + say header say - Gem.sources.each do |src| + list.each do |src| say src end end def list? # :nodoc: !(options[:add] || + options[:prepend] || + options[:append] || options[:clear_all] || options[:remove] || options[:update]) @@ -181,11 +263,13 @@ To remove a source use the --remove argument: def execute clear_all if options[:clear_all] - source_uri = options[:add] - add_source source_uri if source_uri + add_source options[:add] if options[:add] + + prepend_source options[:prepend] if options[:prepend] + + append_source options[:append] if options[:append] - source_uri = options[:remove] - remove_source source_uri if source_uri + remove_source options[:remove] if options[:remove] update if options[:update] @@ -193,13 +277,22 @@ To remove a source use the --remove argument: end def remove_source(source_uri) # :nodoc: - unless Gem.sources.include? source_uri - say "source #{source_uri} not present in cache" - else - Gem.sources.delete source_uri + source = build_source(source_uri) + source_uri = source.uri.to_s + + if configured_sources&.include? source + Gem.sources.delete source Gem.configuration.write - say "#{source_uri} removed from sources" + if default_sources.include?(source) && configured_sources.one? + alert_warning "Removing a default source when it is the only source has no effect. Add a different source to #{config_file_name} if you want to stop using it as a source." + else + say "#{source_uri} removed from sources" + end + elsif configured_sources + say "source #{source_uri} cannot be removed because it's not present in #{config_file_name}" + else + say "source #{source_uri} cannot be removed because there are no configured sources in #{config_file_name}" end end @@ -223,4 +316,33 @@ To remove a source use the --remove argument: say "*** Unable to remove #{desc} source cache ***" end end + + private + + def default_sources + Gem::SourceList.from(Gem.default_sources) + end + + def configured_sources + return @configured_sources if defined?(@configured_sources) + + configuration_sources = Gem.configuration.sources + @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources + end + + def config_file_name + Gem.configuration.config_file_name + end + + def build_source(source_uri) + source_uri = normalize_source_uri(source_uri) + Gem::Source.new(source_uri) + end + + def build_new_source(source_uri) + source = build_source(source_uri) + check_rubygems_https(source.uri.to_s) + check_typo_squatting(source) + source + end end diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index 12004a6d56..15e543f1a6 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -12,27 +13,27 @@ class Gem::Commands::SpecificationCommand < Gem::Command Gem.load_yaml super "specification", "Display gem specification (in yaml)", - :domain => :local, :version => Gem::Requirement.default, - :format => :yaml + domain: :local, version: Gem::Requirement.default, + format: :yaml add_version_option("examine") add_platform_option add_prerelease_option add_option("--all", "Output specifications for all versions of", - "the gem") do |value, options| + "the gem") do |_value, options| options[:all] = true end - add_option("--ruby", "Output ruby format") do |value, options| + add_option("--ruby", "Output ruby format") do |_value, options| options[:format] = :ruby end - add_option("--yaml", "Output YAML format") do |value, options| + add_option("--yaml", "Output YAML format") do |_value, options| options[:format] = :yaml end - add_option("--marshal", "Output Marshal format") do |value, options| + add_option("--marshal", "Output Marshal format") do |_value, options| options[:format] = :marshal end @@ -41,7 +42,7 @@ class Gem::Commands::SpecificationCommand < Gem::Command def arguments # :nodoc: <<-ARGS -GEMFILE name of gem to show the gemspec for +GEM_OR_FILE gem name or a .gem file to show the gemspec for FIELD name of gemspec field to show ARGS end @@ -67,7 +68,7 @@ Specific fields in the specification can be extracted in YAML format: end def usage # :nodoc: - "#{program_name} [GEMFILE] [FIELD]" + "#{program_name} [GEM_OR_FILE] [FIELD]" end def execute @@ -76,7 +77,7 @@ Specific fields in the specification can be extracted in YAML format: unless gem raise Gem::CommandLineError, - "Please specify a gem name or file on the command line" + "Please specify a gem name or a .gem file on the command line" end case v = options[:version] @@ -106,7 +107,11 @@ Specific fields in the specification can be extracted in YAML format: if local? if File.exist? gem - specs << Gem::Package.new(gem).spec rescue nil + begin + specs << Gem::Package.new(gem).spec + rescue StandardError + nil + end end if specs.empty? @@ -133,16 +138,16 @@ Specific fields in the specification can be extracted in YAML format: end unless options[:all] - specs = [specs.max_by {|s| s.version }] + specs = [specs.max_by(&:version)] end specs.each do |s| s = s.send field if field say case options[:format] - when :ruby then s.to_ruby - when :marshal then Marshal.dump s - else s.to_yaml + when :ruby then s.to_ruby + when :marshal then Marshal.dump s + else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s) end say "\n" diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 0246f42e3e..0be2b85159 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::StaleCommand < Gem::Command @@ -17,7 +18,7 @@ longer using. end def usage # :nodoc: - "#{program_name}" + program_name.to_s end def execute @@ -33,7 +34,7 @@ longer using. end gem_to_atime.sort_by {|_, atime| atime }.each do |name, atime| - say "#{name} at #{atime.strftime '%c'}" + say "#{name} at #{atime.strftime "%c"}" end end end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 3c520826e5..3c26074f93 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../uninstaller" @@ -14,12 +15,11 @@ class Gem::Commands::UninstallCommand < Gem::Command def initialize super "uninstall", "Uninstall gems from the local repository", - :version => Gem::Requirement.default, :user_install => true, - :check_dev => false, :vendor => false + version: Gem::Requirement.default, user_install: true, + check_dev: false, vendor: false add_option("-a", "--[no-]all", - "Uninstall all matching versions" - ) do |value, options| + "Uninstall all matching versions") do |value, options| options[:all] = value end @@ -79,7 +79,7 @@ class Gem::Commands::UninstallCommand < Gem::Command add_option("--vendor", "Uninstall gem from the vendor directory.", - "Only for use by gem repackagers.") do |value, options| + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end @@ -95,7 +95,7 @@ class Gem::Commands::UninstallCommand < Gem::Command end def defaults_str # :nodoc: - "--version '#{Gem::Requirement.default}' --no-force " + + "--version '#{Gem::Requirement.default}' --no-force " \ "--user-install" end @@ -117,7 +117,7 @@ that is a dependency of an existing gem. You can use the if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \ - " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`" terminate_interaction 1 end end @@ -125,6 +125,9 @@ that is a dependency of an existing gem. You can use the def execute check_version + # Consider only gem specifications installed at `--install-dir` + Gem::Specification.dirs = options[:install_dir] if options[:install_dir] + if options[:all] && !options[:args].empty? uninstall_specific elsif options[:all] @@ -135,7 +138,7 @@ that is a dependency of an existing gem. You can use the end def uninstall_all - specs = Gem::Specification.reject {|spec| spec.default_gem? } + specs = Gem::Specification.reject(&:default_gem?) specs.each do |spec| options[:version] = spec.version @@ -154,9 +157,14 @@ that is a dependency of an existing gem. You can use the gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name]) - say("Gem '#{name}' is not installed") if gem_specs.empty? - gem_specs.each do |spec| - deplist.add spec + if gem_specs.empty? + say("Gem '#{name}' is not installed") + else + gem_specs.reject!(&:default_gem?) if gem_specs.size > 1 + + gem_specs.each do |spec| + deplist.add spec + end end end @@ -165,15 +173,14 @@ that is a dependency of an existing gem. You can use the gems_to_uninstall = {} deps.each do |dep| - unless gems_to_uninstall[dep.name] + if original_gem_version[dep.name] == Gem::Requirement.default + next if gems_to_uninstall[dep.name] gems_to_uninstall[dep.name] = true - - unless original_gem_version[dep.name] == Gem::Requirement.default - options[:version] = dep.version - end - - uninstall_gem(dep.name) + else + options[:version] = dep.version end + + uninstall_gem(dep.name) end end @@ -181,12 +188,12 @@ that is a dependency of an existing gem. You can use the uninstall(gem_name) rescue Gem::GemNotInHomeException => e spec = e.spec - alert("In order to remove #{spec.name}, please execute:\n" + - "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") + alert("In order to remove #{spec.name}, please execute:\n" \ + "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}") rescue Gem::UninstallError => e spec = e.spec - alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " + - "located at '#{spec.full_gem_path}'. This is most likely because" + + alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \ + "located at '#{spec.full_gem_path}'. This is most likely because" \ "the current user does not have the appropriate permissions") terminate_interaction 1 end diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb index b1f939b0bc..c2fc720297 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../security_option" @@ -20,15 +21,15 @@ class Gem::Commands::UnpackCommand < Gem::Command require "fileutils" super "unpack", "Unpack an installed gem to the current directory", - :version => Gem::Requirement.default, - :target => Dir.pwd + version: Gem::Requirement.default, + target: Dir.pwd add_option("--target=DIR", "target directory for unpacking") do |value, options| options[:target] = value end - add_option("--spec", "unpack the gem specification") do |value, options| + add_option("--spec", "unpack the gem specification") do |_value, options| options[:spec] = true end @@ -95,12 +96,10 @@ command help for an example. FileUtils.mkdir_p @options[:target] if @options[:target] - destination = begin - if @options[:target] - File.join @options[:target], spec_file - else - spec_file - end + destination = if @options[:target] + File.join @options[:target], spec_file + else + spec_file end File.open destination, "w" do |io| @@ -131,7 +130,7 @@ command help for an example. return this_path if File.exist? this_path end - return nil + nil end ## @@ -144,24 +143,18 @@ command help for an example. # get_path 'rake', '< 0.1' # nil # get_path 'rak' # nil (exact name required) #-- - # TODO: This should be refactored so that it's a general service. I don't - # think any of our existing classes are the right place though. Just maybe - # 'Cache'? - # - # TODO: It just uses Gem.dir for now. What's an easy way to get the list of - # source directories? def get_path(dependency) - return dependency.name if dependency.name =~ /\.gem$/i + return dependency.name if /\.gem$/i.match?(dependency.name) specs = dependency.matching_specs - selected = specs.max_by {|s| s.version } + selected = specs.max_by(&:version) return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless selected - return unless dependency.name =~ /^#{selected.name}$/i + return unless /^#{selected.name}$/i.match?(dependency.name) # We expect to find (basename).gem in the 'cache' directory. Furthermore, # the name match must be exact (ignoring case). diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index 5c90981645..d9740d814a 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../command_manager" require_relative "../dependency_installer" @@ -20,7 +21,7 @@ class Gem::Commands::UpdateCommand < Gem::Command def initialize options = { - :force => false, + force: false, } options.merge!(install_update_options) @@ -36,10 +37,10 @@ class Gem::Commands::UpdateCommand < Gem::Command end add_option("--system [VERSION]", Gem::Version, - "Update the RubyGems system software") do |value, options| - value = true unless value + "Update the RubyGems system software") do |value, opts| + value ||= true - options[:system] = value + opts[:system] = value end add_local_remote_options @@ -119,7 +120,7 @@ command to remove old versions. updated = update_gems gems_to_update installed_names = highest_installed_gems.keys - updated_names = updated.map {|spec| spec.name } + updated_names = updated.map(&:name) not_updated_names = options[:args].uniq - updated_names not_installed_names = not_updated_names - installed_names up_to_date_names = not_updated_names - not_installed_names @@ -127,10 +128,10 @@ command to remove old versions. if updated.empty? say "Nothing to update" else - say "Gems updated: #{updated_names.join(' ')}" + say "Gems updated: #{updated_names.join(" ")}" end - say "Gems already up-to-date: #{up_to_date_names.join(' ')}" unless up_to_date_names.empty? - say "Gems not currently installed: #{not_installed_names.join(' ')}" unless not_installed_names.empty? + say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty? + say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty? end def fetch_remote_gems(spec) # :nodoc: @@ -185,7 +186,9 @@ command to remove old versions. system Gem.ruby, "--disable-gems", "setup.rb", *args end - say "RubyGems system software updated" if installed unless options[:silent] + unless options[:silent] + say "RubyGems system software updated" if installed + end end end @@ -194,18 +197,17 @@ command to remove old versions. yield else require "tmpdir" - tmpdir = Dir.mktmpdir - FileUtils.mv Gem.plugindir, tmpdir + Dir.mktmpdir("gem_update") do |tmpdir| + FileUtils.mv Gem.plugindir, tmpdir - status = yield + status = yield - if status - FileUtils.rm_rf tmpdir - else - FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir - end + unless status + FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir + end - status + status + end end end @@ -230,7 +232,7 @@ command to remove old versions. highest_remote_tup = highest_remote_name_tuple(rubygems_update) target = highest_remote_tup ? highest_remote_tup.version : version - return target, requirement + [target, requirement] end def update_gem(name, version = Gem::Requirement.default) @@ -241,7 +243,7 @@ command to remove old versions. @installer = Gem::DependencyInstaller.new update_options - say "Updating #{name}" unless options[:system] && options[:silent] + say "Updating #{name}" unless options[:system] begin @installer.install name, Gem::Requirement.new(version) rescue Gem::InstallError, Gem::DependencyError => e @@ -279,7 +281,7 @@ command to remove old versions. check_oldest_rubygems version installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement - installed_gems = update_gem("rubygems-update", version) if installed_gems.empty? || installed_gems.first.version != version + installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version return if installed_gems.empty? install_rubygems installed_gems.first @@ -291,16 +293,14 @@ command to remove old versions. args << "--prefix" << Gem.prefix if Gem.prefix args << "--no-document" unless options[:document].include?("rdoc") || options[:document].include?("ri") args << "--no-format-executable" if options[:no_format_executable] - args << "--previous-version" << Gem::VERSION if - options[:system] == true || - Gem::Version.new(options[:system]) >= Gem::Version.new(2) + args << "--previous-version" << Gem::VERSION args end def which_to_update(highest_installed_gems, gem_names) result = [] - highest_installed_gems.each do |l_name, l_spec| + highest_installed_gems.each do |_l_name, l_spec| next if !gem_names.empty? && gem_names.none? {|name| name == l_spec.name } @@ -317,20 +317,10 @@ command to remove old versions. # # Oldest version we support downgrading to. This is the version that - # originally ships with the first patch version of each ruby, because we never - # test each ruby against older rubygems, so we can't really guarantee it - # works. Version list can be checked here: https://stdgems.org/rubygems + # originally ships with the oldest supported patch version of ruby. # def oldest_supported_version @oldest_supported_version ||= - if Gem.ruby_version > Gem::Version.new("3.1.a") - Gem::Version.new("3.3.3") - elsif Gem.ruby_version > Gem::Version.new("3.0.a") - Gem::Version.new("3.2.3") - elsif Gem.ruby_version > Gem::Version.new("2.7.a") - Gem::Version.new("3.1.2") - else - Gem::Version.new("3.0.1") - end + Gem::Version.new("3.3.3") end end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb index 5b9a79b734..5ed4d9d142 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::WhichCommand < Gem::Command def initialize super "which", "Find the location of a library file you can require", - :search_gems_first => false, :show_all => false + search_gems_first: false, show_all: false add_option "-a", "--[no-]all", "show all matching files" do |show_all, options| options[:show_all] = show_all diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index 1499f72f5d..fbdc262549 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -61,7 +62,7 @@ data you will need to change them immediately and yank your gem. end def yank_gem(version, platform) - say "Yanking gem from #{self.host}..." + say "Yanking gem from #{host}..." args = [:delete, version, platform, "api/v1/gems/yank"] response = yank_api_request(*args) @@ -88,7 +89,7 @@ data you will need to change them immediately and yank your gem. def get_version_from_requirements(requirements) requirements.requirements.first[1].version - rescue + rescue StandardError nil end diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb deleted file mode 100644 index b4c1ef16fa..0000000000 --- a/lib/rubygems/compatibility.rb +++ /dev/null @@ -1,42 +0,0 @@ -# frozen_string_literal: true - -#-- -# This file contains all sorts of little compatibility hacks that we've -# had to introduce over the years. Quarantining them into one file helps -# us know when we can get rid of them. -# -# Ruby 1.9.x has introduced some things that are awkward, and we need to -# support them, so we define some constants to use later. -# -# TODO remove at RubyGems 4 -#++ - -module Gem - # :stopdoc: - - RubyGemsVersion = VERSION - deprecate_constant(:RubyGemsVersion) - - RbConfigPriorities = %w[ - MAJOR - MINOR - TEENY - EXEEXT RUBY_SO_NAME arch bindir datadir libdir ruby_install_name - ruby_version rubylibprefix sitedir sitelibdir vendordir vendorlibdir - rubylibdir - ].freeze - - unless defined?(ConfigMap) - ## - # Configuration settings from ::RbConfig - ConfigMap = Hash.new do |cm, key| - cm[key] = RbConfig::CONFIG[key.to_s] - end - deprecate_constant(:ConfigMap) - else - RbConfigPriorities.each do |key| - ConfigMap[key.to_sym] = RbConfig::CONFIG[key] - end - end - -end diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb index 4aa8b4d33a..d5e9eb4e33 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -25,9 +26,22 @@ require "rbconfig" # RubyGems options use symbol keys. Valid options are: # # +:backtrace+:: See #backtrace -# +:sources+:: Sets Gem::sources +# +:bulk_threshold+:: See #bulk_threshold # +:verbose+:: See #verbose +# +:update_sources+:: See #update_sources # +:concurrent_downloads+:: See #concurrent_downloads +# +:cert_expiration_length_days+:: See #cert_expiration_length_days +# +:install_extension_in_lib+:: See #install_extension_in_lib +# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled +# +:global_gem_cache+:: See #global_gem_cache +# +:use_psych+:: See #use_psych +# +:gemhome+:: See #home +# +:gempath+:: See #path +# +:sources+:: Sets Gem::sources +# +:disable_default_gem_server+:: See #disable_default_gem_server +# +:ssl_verify_mode+:: See #ssl_verify_mode +# +:ssl_ca_cert+:: See #ssl_ca_cert +# +:ssl_client_cert+:: See #ssl_client_cert # # gemrc files may exist in various locations and are read and merged in # the following order: @@ -46,6 +60,9 @@ class Gem::ConfigFile DEFAULT_CONCURRENT_DOWNLOADS = 8 DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365 DEFAULT_IPV4_FALLBACK_ENABLED = false + DEFAULT_INSTALL_EXTENSION_IN_LIB = true + DEFAULT_GLOBAL_GEM_CACHE = false + DEFAULT_USE_PSYCH = false ## # For Ruby packagers to set configuration defaults. Set in @@ -142,12 +159,28 @@ class Gem::ConfigFile attr_accessor :cert_expiration_length_days ## + # Install extensions into lib as well as into the extension directory. + + attr_accessor :install_extension_in_lib + + ## # == Experimental == # Fallback to IPv4 when IPv6 is not reachable or slow (default: false) attr_accessor :ipv4_fallback_enabled ## + # Use a global cache for .gem files shared across all Ruby installations. + # When enabled, gems are cached to ~/.cache/gem/gems (or XDG_CACHE_HOME/gem/gems). + + attr_accessor :global_gem_cache + + ## + # Use Psych (C extension YAML parser) instead of the pure Ruby YAMLSerializer. + + attr_accessor :use_psych + + ## # Path name of directory or file of openssl client certificate, used for remote https connection with client authentication attr_reader :ssl_client_cert @@ -182,15 +215,18 @@ class Gem::ConfigFile @update_sources = DEFAULT_UPDATE_SOURCES @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS + @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED + @global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE + @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS) platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS) system_config = load_file SYSTEM_WIDE_CONFIG_FILE - user_config = load_file config_file_name.dup.tap(&Gem::UNTAINT) + user_config = load_file config_file_name - environment_config = (ENV["GEMRC"] || "") - .split(File::PATH_SEPARATOR).inject({}) do |result, file| + environment_config = (ENV["GEMRC"] || ""). + split(File::PATH_SEPARATOR).inject({}) do |result, file| result.merge load_file file end @@ -201,21 +237,37 @@ class Gem::ConfigFile @hash = @hash.merge environment_config end - # HACK these override command-line args, which is bad + @hash.transform_keys! do |k| + # gemhome and gempath are not working with symbol keys + if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days + concurrent_downloads install_extension_in_lib ipv4_fallback_enabled + global_gem_cache use_psych sources + disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k) + k.to_sym + else + k + end + end + + # HACK: these override command-line args, which is bad @backtrace = @hash[:backtrace] if @hash.key? :backtrace @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold - @home = @hash[:gemhome] if @hash.key? :gemhome - @path = @hash[:gempath] if @hash.key? :gempath - @update_sources = @hash[:update_sources] if @hash.key? :update_sources @verbose = @hash[:verbose] if @hash.key? :verbose - @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server - @sources = @hash[:sources] if @hash.key? :sources + @update_sources = @hash[:update_sources] if @hash.key? :update_sources + @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads @cert_expiration_length_days = @hash[:cert_expiration_length_days] if @hash.key? :cert_expiration_length_days + @install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib @ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled + @global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache + @use_psych = @hash[:use_psych] if @hash.key? :use_psych - @ssl_verify_mode = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode - @ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert - @ssl_client_cert = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert + @home = @hash[:gemhome] if @hash.key? :gemhome + @path = @hash[:gempath] if @hash.key? :gempath + @sources = @hash[:sources] if @hash.key? :sources + @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server + @ssl_verify_mode = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode + @ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert + @ssl_client_cert = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert @api_keys = nil @rubygems_api_key = nil @@ -240,9 +292,9 @@ class Gem::ConfigFile return if Gem.win_platform? # windows doesn't write 0600 as 0600 return unless File.exist? credentials_path - existing_permissions = File.stat(credentials_path).mode & 0777 + existing_permissions = File.stat(credentials_path).mode & 0o777 - return if existing_permissions == 0600 + return if existing_permissions == 0o600 alert_error <<-ERROR Your gem push credentials file located at: @@ -323,11 +375,9 @@ if you believe they were disclosed to a third party. require "fileutils" FileUtils.mkdir_p(dirname) - Gem.load_yaml - - permissions = 0600 & (~File.umask) + permissions = 0o600 & ~File.umask File.open(credentials_path, "w", permissions) do |f| - f.write config.to_yaml + f.write self.class.dump_with_rubygems_yaml(config) end load_api_keys # reload @@ -343,20 +393,20 @@ if you believe they were disclosed to a third party. end def load_file(filename) - Gem.load_yaml - yaml_errors = [ArgumentError] - yaml_errors << Psych::SyntaxError if defined?(Psych::SyntaxError) return {} unless filename && !filename.empty? && File.exist?(filename) begin - content = Gem::SafeYAML.load(File.read(filename)) - unless content.kind_of? Hash + config = self.class.load_with_rubygems_config_hash(File.read(filename)) + has_invalid_keys = config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") } + has_invalid_values = config.values.any? {|v| v.is_a?(String) && v.gsub(%r{https?:\/\/}, "").match?(/\A\S+: /) } + if has_invalid_keys || has_invalid_values warn "Failed to load #{filename} because it doesn't contain valid YAML hash" return {} + else + return config end - return content rescue *yaml_errors => e warn "Failed to load #{filename}, #{e}" rescue Errno::EACCES @@ -467,6 +517,9 @@ if you believe they were disclosed to a third party. yaml_hash[:concurrent_downloads] = @hash.fetch(:concurrent_downloads, DEFAULT_CONCURRENT_DOWNLOADS) + yaml_hash[:install_extension_in_lib] = + @hash.fetch(:install_extension_in_lib, DEFAULT_INSTALL_EXTENSION_IN_LIB) + yaml_hash[:ssl_verify_mode] = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode @@ -476,17 +529,17 @@ if you believe they were disclosed to a third party. yaml_hash[:ssl_client_cert] = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert - keys = yaml_hash.keys.map {|key| key.to_s } + keys = yaml_hash.keys.map(&:to_s) keys << "debug" re = Regexp.union(*keys) @hash.each do |key, value| key = key.to_s - next if key =~ re + next if key&.match?(re) yaml_hash[key.to_s] = value end - yaml_hash.to_yaml + self.class.dump_with_rubygems_yaml(yaml_hash) end # Writes out this config file, replacing its source. @@ -501,12 +554,12 @@ if you believe they were disclosed to a third party. # Return the configuration information for +key+. def [](key) - @hash[key.to_s] + @hash[key] || @hash[key.to_s] end # Set configuration option +key+ to +value+. def []=(key, value) - @hash[key.to_s] = value + @hash[key] = value end def ==(other) # :nodoc: @@ -521,8 +574,66 @@ if you believe they were disclosed to a third party. attr_reader :hash protected :hash + def self.dump_with_rubygems_yaml(content) + content.transform_keys! do |k| + k.is_a?(Symbol) ? ":#{k}" : k + end + + require_relative "yaml_serializer" + Gem::YAMLSerializer.dump(content) + end + + def self.load_with_rubygems_config_hash(yaml) + require_relative "yaml_serializer" + + content = Gem::YAMLSerializer.load(yaml, permitted_classes: []) + return {} unless content.is_a?(Hash) + + deep_transform_config_keys!(content) + end + private + def self.deep_transform_config_keys!(config) + config.transform_keys! do |k| + if k.match?(/\A:(.*)\Z/) + k[1..-1].to_sym + elsif k.include?("__") || k.match?(%r{/\Z}) + if k.is_a?(Symbol) + k.to_s.gsub(/__/,".").gsub(%r{/\Z}, "").to_sym + else + k.dup.gsub(/__/,".").gsub(%r{/\Z}, "") + end + else + k + end + end + + config.transform_values! do |v| + if v.is_a?(String) + if v.match?(/\A:(.*)\Z/) + v[1..-1].to_sym + elsif v.match?(/\A[+-]?\d+\Z/) + v.to_i + elsif v.match?(/\Atrue|false\Z/) + v == "true" + elsif v.empty? + nil + else + v + end + elsif v.respond_to?(:empty?) && v.empty? + nil + elsif v.is_a?(Hash) + deep_transform_config_keys!(v) + else + v + end + end + + config + end + def set_config_file_name(args) @config_file_name = ENV["GEMRC"] need_config_file_name = false @@ -533,7 +644,7 @@ if you believe they were disclosed to a third party. need_config_file_name = false elsif arg =~ /^--config-file=(.*)/ @config_file_name = $1 - elsif arg =~ /^--config-file$/ + elsif /^--config-file$/.match?(arg) need_config_file_name = true end end diff --git a/lib/rubygems/core_ext/kernel_gem.rb b/lib/rubygems/core_ext/kernel_gem.rb index b2f97b9ed9..4e09b95c44 100644 --- a/lib/rubygems/core_ext/kernel_gem.rb +++ b/lib/rubygems/core_ext/kernel_gem.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Kernel - ## # Use Kernel#gem to activate a specific version of +gem_name+. # @@ -37,9 +36,9 @@ module Kernel skip_list = (ENV["GEM_SKIP"] || "").split(/:/) raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name - if gem_name.kind_of? Gem::Dependency + if gem_name.is_a? Gem::Dependency unless Gem::Deprecate.skip - warn "#{Gem.location_of_caller.join ':'}:Warning: Kernel.gem no longer "\ + warn "#{Gem.location_of_caller.join ":"}:Warning: Kernel.gem no longer "\ "accepts a Gem::Dependency object, please pass the name "\ "and requirements directly" end @@ -66,5 +65,4 @@ module Kernel end private :gem - end diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb index b92d6f9965..3a9bdbdc9d 100644 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -8,13 +9,12 @@ require "monitor" module Kernel - RUBYGEMS_ACTIVATION_MONITOR = Monitor.new # :nodoc: # Make sure we have a reference to Ruby's original Kernel#require unless defined?(gem_original_require) # :stopdoc: - alias gem_original_require require + alias_method :gem_original_require, :require private :gem_original_require # :startdoc: end @@ -34,141 +34,119 @@ module Kernel # that file has already been loaded is preserved. def require(path) # :doc: - if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?) - monitor_owned = RUBYGEMS_ACTIVATION_MONITOR.mon_owned? - end - RUBYGEMS_ACTIVATION_MONITOR.enter - - path = path.to_path if path.respond_to? :to_path - - if spec = Gem.find_unresolved_default_spec(path) - # Ensure -I beats a default gem - resolved_path = begin - rp = nil - load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths - Gem.suffixes.each do |s| - $LOAD_PATH[0...load_path_check_index].each do |lp| - safe_lp = lp.dup.tap(&Gem::UNTAINT) - begin - if File.symlink? safe_lp # for backward compatibility + return gem_original_require(path) unless Gem.discover_gems_on_require + + RUBYGEMS_ACTIVATION_MONITOR.synchronize do + path = File.path(path) + + # If +path+ belongs to a default gem, we activate it and then go straight + # to normal require + + if spec = Gem.find_default_spec(path) + name = spec.name + + next if Gem.loaded_specs[name] + + # Ensure -I beats a default gem + resolved_path = begin + rp = nil + load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths + Gem.suffixes.find do |s| + $LOAD_PATH[0...load_path_check_index].find do |lp| + if File.symlink? lp # for backward compatibility next end - rescue SecurityError - RUBYGEMS_ACTIVATION_MONITOR.exit - raise - end - full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}")) - if File.file?(full_path) - rp = full_path - break + full_path = File.expand_path(File.join(lp, "#{path}#{s}")) + rp = full_path if File.file?(full_path) end end - break if rp + rp end - rp - end - begin - Kernel.send(:gem, spec.name, Gem::Requirement.default_prerelease) - rescue Exception - RUBYGEMS_ACTIVATION_MONITOR.exit - raise - end unless resolved_path - end + next if resolved_path - # If there are no unresolved deps, then we can use just try - # normal require handle loading a gem from the rescue below. + Kernel.send(:gem, name, Gem::Requirement.default_prerelease) - if Gem::Specification.unresolved_deps.empty? - RUBYGEMS_ACTIVATION_MONITOR.exit - return gem_original_require(path) - end + Gem.load_bundler_extensions(Gem.loaded_specs[name].version) if name == "bundler" - # If +path+ is for a gem that has already been loaded, don't - # bother trying to find it in an unresolved gem, just go straight - # to normal require. - #-- - # TODO request access to the C implementation of this to speed up RubyGems + next + end - if Gem::Specification.find_active_stub_by_path(path) - RUBYGEMS_ACTIVATION_MONITOR.exit - return gem_original_require(path) - end + # If there are no unresolved deps, then we can use just try + # normal require handle loading a gem from the rescue below. - # Attempt to find +path+ in any unresolved gems... - - found_specs = Gem::Specification.find_in_unresolved path - - # If there are no directly unresolved gems, then try and find +path+ - # in any gems that are available via the currently unresolved gems. - # For example, given: - # - # a => b => c => d - # - # If a and b are currently active with c being unresolved and d.rb is - # requested, then find_in_unresolved_tree will find d.rb in d because - # it's a dependency of c. - # - if found_specs.empty? - found_specs = Gem::Specification.find_in_unresolved_tree path - - found_specs.each do |found_spec| - found_spec.activate + if Gem::Specification.unresolved_deps.empty? + next end - # We found +path+ directly in an unresolved gem. Now we figure out, of - # the possible found specs, which one we should activate. - else + # If +path+ is for a gem that has already been loaded, don't + # bother trying to find it in an unresolved gem, just go straight + # to normal require. + #-- + # TODO request access to the C implementation of this to speed up RubyGems - # Check that all the found specs are just different - # versions of the same gem - names = found_specs.map(&:name).uniq - - if names.size > 1 - RUBYGEMS_ACTIVATION_MONITOR.exit - raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" + if Gem::Specification.find_active_stub_by_path(path) + next end - # Ok, now find a gem that has no conflicts, starting - # at the highest version. - valid = found_specs.find {|s| !s.has_conflicts? } + # Attempt to find +path+ in any unresolved gems... - unless valid - le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" - le.name = names.first - RUBYGEMS_ACTIVATION_MONITOR.exit - raise le - end + found_specs = Gem::Specification.find_in_unresolved path - valid.activate - end + # If there are no directly unresolved gems, then try and find +path+ + # in any gems that are available via the currently unresolved gems. + # For example, given: + # + # a => b => c => d + # + # If a and b are currently active with c being unresolved and d.rb is + # requested, then find_in_unresolved_tree will find d.rb in d because + # it's a dependency of c. + # + if found_specs.empty? + found_specs = Gem::Specification.find_in_unresolved_tree path - RUBYGEMS_ACTIVATION_MONITOR.exit - return gem_original_require(path) - rescue LoadError => load_error - if load_error.path == path - RUBYGEMS_ACTIVATION_MONITOR.enter + found_specs.each(&:activate) - begin - require_again = Gem.try_activate(path) - ensure - RUBYGEMS_ACTIVATION_MONITOR.exit - end + # We found +path+ directly in an unresolved gem. Now we figure out, of + # the possible found specs, which one we should activate. + else + + # Check that all the found specs are just different + # versions of the same gem + names = found_specs.map(&:name).uniq + + if names.size > 1 + raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ", "}" + end - return gem_original_require(path) if require_again + # Ok, now find a gem that has no conflicts, starting + # at the highest version. + valid = found_specs.find {|s| !s.has_conflicts? } + + unless valid + le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" + le.name = names.first + raise le + end + + valid.activate + end end - raise load_error - ensure - if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?) - if monitor_owned != (ow = RUBYGEMS_ACTIVATION_MONITOR.mon_owned?) - STDERR.puts [$$, Thread.current, $!, $!.backtrace].inspect if $! - raise "CRITICAL: RUBYGEMS_ACTIVATION_MONITOR.owned?: before #{monitor_owned} -> after #{ow}" + begin + gem_original_require(path) + rescue LoadError => load_error + if load_error.path == path && + RUBYGEMS_ACTIVATION_MONITOR.synchronize { Gem.try_activate(path) } + + return gem_original_require(path) end + + raise load_error end end private :require - end diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb index 1f4c77f04b..f806b77fab 100644 --- a/lib/rubygems/core_ext/kernel_warn.rb +++ b/lib/rubygems/core_ext/kernel_warn.rb @@ -13,11 +13,7 @@ module Kernel module_function define_method(:warn) {|*messages, **kw| unless uplevel = kw[:uplevel] - if Gem.java_platform? && RUBY_VERSION < "3.1" - return original_warn.bind(self).call(*messages) - else - return original_warn.bind(self).call(*messages, **kw) - end + return original_warn.bind_call(self, *messages, **kw) end # Ensure `uplevel` fits a `long` @@ -35,16 +31,15 @@ module Kernel start += 1 - if path = loc.path - unless path.start_with?(rubygems_path) || path.start_with?("<internal:") - # Non-rubygems frames - uplevel -= 1 - end + next unless path = loc.path + unless path.start_with?(rubygems_path, "<internal:") + # Non-rubygems frames + uplevel -= 1 end end kw[:uplevel] = start end - original_warn.bind(self).call(*messages, **kw) + original_warn.bind_call(self, *messages, **kw) } end diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb index c9e0a92953..018c49dbeb 100644 --- a/lib/rubygems/core_ext/tcpsocket_init.rb +++ b/lib/rubygems/core_ext/tcpsocket_init.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "socket" module CoreExtensions @@ -17,7 +19,7 @@ module CoreExtensions cond_var = Thread::ConditionVariable.new Addrinfo.foreach(host, serv, nil, :STREAM) do |addr| - Thread.report_on_exception = false if defined? Thread.report_on_exception = () + Thread.report_on_exception = false threads << Thread.new(addr) do # give head start to ipv6 addresses diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index 4806ea6469..2247c49c81 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Gem DEFAULT_HOST = "https://rubygems.org" @@ -12,7 +13,7 @@ module Gem # An Array of the default sources that come with RubyGems def self.default_sources - %w[https://rubygems.org/] + @default_sources ||= %w[https://rubygems.org/] end ## @@ -23,7 +24,7 @@ module Gem default_spec_cache_dir = File.join Gem.user_home, ".gem", "specs" unless File.exist?(default_spec_cache_dir) - default_spec_cache_dir = File.join Gem.data_home, "gem", "specs" + default_spec_cache_dir = File.join Gem.cache_home, "gem", "specs" end default_spec_cache_dir @@ -79,7 +80,7 @@ module Gem def self.find_home Dir.home.dup - rescue + rescue StandardError if Gem.win_platform? File.expand_path File.join(ENV["HOMEDRIVE"] || ENV["SystemDrive"], "/") else @@ -93,7 +94,7 @@ module Gem # The home directory for the user. def self.user_home - @user_home ||= find_home.tap(&Gem::UNTAINT) + @user_home ||= find_home end ## @@ -111,7 +112,7 @@ module Gem # The path to standard location of the user's configuration directory. def self.config_home - @config_home ||= (ENV["XDG_CONFIG_HOME"] || File.join(Gem.user_home, ".config")) + @config_home ||= ENV["XDG_CONFIG_HOME"] || File.join(Gem.user_home, ".config") end ## @@ -130,35 +131,44 @@ module Gem # The path to standard location of the user's .gemrc file. def self.config_file - @config_file ||= find_config_file.tap(&Gem::UNTAINT) + @config_file ||= find_config_file end ## # The path to standard location of the user's state file. def self.state_file - @state_file ||= File.join(Gem.state_home, "gem", "last_update_check").tap(&Gem::UNTAINT) + @state_file ||= File.join(Gem.state_home, "gem", "last_update_check") end ## # The path to standard location of the user's cache directory. def self.cache_home - @cache_home ||= (ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache")) + @cache_home ||= ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache") + end + + ## + # The path to the global gem cache directory. + # This is used when global_gem_cache is enabled to share .gem files + # across all Ruby installations. + + def self.global_gem_cache_path + File.join(cache_home, "gem", "gems") end ## # The path to standard location of the user's data directory. def self.data_home - @data_home ||= (ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share")) + @data_home ||= ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share") end ## # The path to standard location of the user's state directory. def self.state_home - @data_home ||= (ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state")) + @state_home ||= ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state") end ## @@ -183,7 +193,11 @@ module Gem # Deduce Ruby's --program-prefix and --program-suffix from its install name def self.default_exec_format - exec_format = RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s") rescue "%s" + exec_format = begin + RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s") + rescue StandardError + "%s" + end unless exec_format.include?("%s") raise Gem::Exception, @@ -231,10 +245,22 @@ module Gem end ## + # Enables automatic installation into user directory + + def self.default_user_install # :nodoc: + if !ENV.key?("GEM_HOME") && File.exist?(Gem.dir) && !File.writable?(Gem.dir) + Gem.ui.say "Defaulting to user installation because default installation directory (#{Gem.dir}) is not writable." + return true + end + + false + end + + ## # Install extensions into lib as well as into the extension directory. def self.install_extension_in_lib # :nodoc: - true + Gem.configuration.install_extension_in_lib end ## diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index cd03e7e299..1e91f493a6 100644 --- a/lib/rubygems/dependency.rb +++ b/lib/rubygems/dependency.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The Dependency class holds a Gem name and a Gem::Requirement. @@ -45,10 +46,10 @@ class Gem::Dependency end type = Symbol === requirements.last ? requirements.pop : :runtime - requirements = requirements.first if 1 == requirements.length # unpack + requirements = requirements.first if requirements.length == 1 # unpack unless TYPES.include? type - raise ArgumentError, "Valid types are #{TYPES.inspect}, " + + raise ArgumentError, "Valid types are #{TYPES.inspect}, " \ "not #{type.inspect}" end @@ -73,11 +74,9 @@ class Gem::Dependency def inspect # :nodoc: if prerelease? - "<%s type=%p name=%p requirements=%p prerelease=ok>" % - [self.class, self.type, self.name, requirement.to_s] + format("<%s type=%p name=%p requirements=%p prerelease=ok>", self.class, type, name, requirement.to_s) else - "<%s type=%p name=%p requirements=%p>" % - [self.class, self.type, self.name, requirement.to_s] + format("<%s type=%p name=%p requirements=%p>", self.class, type, name, requirement.to_s) end end @@ -168,16 +167,16 @@ class Gem::Dependency def ==(other) # :nodoc: Gem::Dependency === other && - self.name == other.name && - self.type == other.type && - self.requirement == other.requirement + name == other.name && + type == other.type && + requirement == other.requirement end ## # Dependencies are ordered by name. def <=>(other) - self.name <=> other.name + name <=> other.name end ## @@ -204,7 +203,7 @@ class Gem::Dependency requirement.satisfied_by? version end - alias === =~ + alias_method :===, :=~ ## # :call-seq: @@ -218,7 +217,7 @@ class Gem::Dependency # NOTE: Unlike #matches_spec? this method does not return true when the # version is a prerelease version unless this is a prerelease dependency. - def match?(obj, version=nil, allow_prerelease=false) + def match?(obj, version = nil, allow_prerelease = false) if !version name = obj.name version = obj.version @@ -262,7 +261,7 @@ class Gem::Dependency end default = Gem::Requirement.default - self_req = self.requirement + self_req = requirement other_req = other.requirement return self.class.new name, self_req if other_req == default @@ -272,15 +271,7 @@ class Gem::Dependency end def matching_specs(platform_only = false) - env_req = Gem.env_requirement(name) - matches = Gem::Specification.stubs_for(name).find_all do |spec| - requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) - end.map(&:to_spec) - - if prioritizes_bundler? - require_relative "bundler_version_finder" - Gem::BundlerVersionFinder.prioritize!(matches) - end + matches = Gem::Specification.find_all_by_name(name, requirement) if platform_only matches.reject! do |spec| @@ -288,7 +279,7 @@ class Gem::Dependency end end - matches + matches.reject(&:ignored?) end ## @@ -298,10 +289,6 @@ class Gem::Dependency @requirement.specific? end - def prioritizes_bundler? - name == "bundler" && !specific? - end - def to_specs matches = matching_specs true @@ -323,15 +310,15 @@ class Gem::Dependency end def to_spec - matches = self.to_specs.compact + matches = to_specs.compact - active = matches.find {|spec| spec.activated? } + active = matches.find(&:activated?) return active if active unless prerelease? - # Move prereleases to the end of the list for >= 0 requirements + # Consider prereleases only as a fallback pre, matches = matches.partition {|spec| spec.version.prerelease? } - matches += pre if requirement == Gem::Requirement.default + matches = pre if matches.empty? end matches.first @@ -350,4 +337,12 @@ class Gem::Dependency :released end end + + def encode_with(coder) # :nodoc: + coder.add "name", @name + coder.add "requirement", @requirement + coder.add "type", @type + coder.add "prerelease", @prerelease + coder.add "version_requirements", @version_requirements + end end diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 1009376b90..c842714d95 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "dependency_list" require_relative "package" @@ -6,28 +7,25 @@ require_relative "installer" require_relative "spec_fetcher" require_relative "user_interaction" require_relative "available_set" -require_relative "deprecate" ## # Installs a gem along with all its dependencies from local and remote gems. class Gem::DependencyInstaller include Gem::UserInteraction - extend Gem::Deprecate DEFAULT_OPTIONS = { # :nodoc: - :env_shebang => false, - :document => %w[ri], - :domain => :both, # HACK dup - :force => false, - :format_executable => false, # HACK dup - :ignore_dependencies => false, - :prerelease => false, - :security_policy => nil, # HACK NoSecurity requires OpenSSL. AlmostNo? Low? - :wrappers => true, - :build_args => nil, - :build_docs_in_background => false, - :install_as_default => false, + env_shebang: false, + document: %w[ri], + domain: :both, # HACK: dup + force: false, + format_executable: false, # HACK: dup + ignore_dependencies: false, + prerelease: false, + security_policy: nil, # HACK: NoSecurity requires OpenSSL. AlmostNo? Low? + wrappers: true, + build_args: nil, + build_docs_in_background: false, }.freeze ## @@ -65,7 +63,7 @@ class Gem::DependencyInstaller # :build_args:: See Gem::Installer::new def initialize(options = {}) - @only_install_dir = !!options[:install_dir] + @only_install_dir = !options[:install_dir].nil? @install_dir = options[:install_dir] || Gem.dir @build_root = options[:build_root] @@ -85,11 +83,13 @@ class Gem::DependencyInstaller @user_install = options[:user_install] @wrappers = options[:wrappers] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] @build_docs_in_background = options[:build_docs_in_background] - @install_as_default = options[:install_as_default] @dir_mode = options[:dir_mode] @data_mode = options[:data_mode] @prog_mode = options[:prog_mode] + @build_extension = options[:build_extension] + @install_plugin = options[:install_plugin] # Indicates that we should not try to update any deps unless # we absolutely must. @@ -120,81 +120,6 @@ class Gem::DependencyInstaller @domain == :both || @domain == :remote end - ## - # Returns a list of pairs of gemspecs and source_uris that match - # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources) - # sources. Gems are sorted with newer gems preferred over older gems, and - # local gems preferred over remote gems. - - def find_gems_with_sources(dep, best_only=false) # :nodoc: - set = Gem::AvailableSet.new - - if consider_local? - sl = Gem::Source::Local.new - - if spec = sl.find_gem(dep.name) - if dep.matches_spec? spec - set.add spec, sl - end - end - end - - if consider_remote? - begin - # This is pulled from #spec_for_dependency to allow - # us to filter tuples before fetching specs. - tuples, errors = Gem::SpecFetcher.fetcher.search_for_dependency dep - - if best_only && !tuples.empty? - tuples.sort! do |a,b| - if b[0].version == a[0].version - if b[0].platform != Gem::Platform::RUBY - 1 - else - -1 - end - else - b[0].version <=> a[0].version - end - end - tuples = [tuples.first] - end - - specs = [] - tuples.each do |tup, source| - begin - spec = source.fetch_spec(tup) - rescue Gem::RemoteFetcher::FetchError => e - errors << Gem::SourceFetchProblem.new(source, e) - else - specs << [spec, source] - end - end - - if @errors - @errors += errors - else - @errors = errors - end - - set << specs - - rescue Gem::RemoteFetcher::FetchError => e - # FIX if there is a problem talking to the network, we either need to always tell - # the user (no really_verbose) or fail hard, not silently tell them that we just - # couldn't find their requested gem. - verbose do - "Error fetching remote data:\t\t#{e.message}\n" \ - "Falling back to local-only install" - end - @domain = :local - end - end - - set - end - rubygems_deprecate :find_gems_with_sources - def in_background(what) # :nodoc: fork_happened = false if @build_docs_in_background && Process.respond_to?(:fork) @@ -230,22 +155,24 @@ class Gem::DependencyInstaller @installed_gems = [] options = { - :bin_dir => @bin_dir, - :build_args => @build_args, - :document => @document, - :env_shebang => @env_shebang, - :force => @force, - :format_executable => @format_executable, - :ignore_dependencies => @ignore_dependencies, - :prerelease => @prerelease, - :security_policy => @security_policy, - :user_install => @user_install, - :wrappers => @wrappers, - :build_root => @build_root, - :install_as_default => @install_as_default, - :dir_mode => @dir_mode, - :data_mode => @data_mode, - :prog_mode => @prog_mode, + bin_dir: @bin_dir, + build_args: @build_args, + build_jobs: @build_jobs, + document: @document, + env_shebang: @env_shebang, + force: @force, + format_executable: @format_executable, + ignore_dependencies: @ignore_dependencies, + prerelease: @prerelease, + security_policy: @security_policy, + user_install: @user_install, + wrappers: @wrappers, + build_root: @build_root, + dir_mode: @dir_mode, + data_mode: @data_mode, + prog_mode: @prog_mode, + build_extension: @build_extension, + install_plugin: @install_plugin, } options[:install_dir] = @install_dir if @only_install_dir @@ -293,13 +220,11 @@ class Gem::DependencyInstaller src = Gem::Source::SpecificFile.new dep_or_name installer_set.add_local dep_or_name, src.spec, src version = src.spec.version if version == Gem::Requirement.default - elsif dep_or_name =~ /\.gem$/ + elsif dep_or_name =~ /\.gem$/ # rubocop:disable Performance/RegexpMatch Dir[dep_or_name].each do |name| - begin - src = Gem::Source::SpecificFile.new name - installer_set.add_local dep_or_name, src.spec, src - rescue Gem::Package::FormatError - end + src = Gem::Source::SpecificFile.new name + installer_set.add_local dep_or_name, src.spec, src + rescue Gem::Package::FormatError end # else This is a dependency. InstallerSet handles this case end diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb index eaf6702177..d50cfe2d54 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require_relative "tsort" -require_relative "deprecate" +require_relative "vendored_tsort" ## # Gem::DependencyList is used for installing and uninstalling gems in the @@ -104,7 +104,7 @@ class Gem::DependencyList end def inspect # :nodoc: - "%s %p>" % [super[0..-2], map {|s| s.full_name }] + format("%s %p>", super[0..-2], map(&:full_name)) end ## @@ -139,7 +139,7 @@ class Gem::DependencyList # If removing the gemspec creates breaks a currently ok dependency, then it # is NOT ok to remove the gemspec. - def ok_to_remove?(full_name, check_dev=true) + def ok_to_remove?(full_name, check_dev = true) gem_to_remove = find_name full_name # If the state is inconsistent, at least don't crash diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 5fe0afb6b0..eb503bb269 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -1,164 +1,171 @@ # frozen_string_literal: true -## -# Provides 3 methods for declaring when something is going away. -# -# +deprecate(name, repl, year, month)+: -# Indicate something may be removed on/after a certain date. -# -# +rubygems_deprecate(name, replacement=:none)+: -# Indicate something will be removed in the next major RubyGems version, -# and (optionally) a replacement for it. -# -# +rubygems_deprecate_command+: -# Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be -# removed in the next RubyGems version. -# -# Also provides +skip_during+ for temporarily turning off deprecation warnings. -# This is intended to be used in the test suite, so deprecation warnings -# don't cause test failures if you need to make sure stderr is otherwise empty. -# -# -# Example usage of +deprecate+ and +rubygems_deprecate+: -# -# class Legacy -# def self.some_class_method -# # ... -# end -# -# def some_instance_method -# # ... -# end -# -# def some_old_method -# # ... -# end -# -# extend Gem::Deprecate -# deprecate :some_instance_method, "X.z", 2011, 4 -# rubygems_deprecate :some_old_method, "Modern#some_new_method" -# -# class << self -# extend Gem::Deprecate -# deprecate :some_class_method, :none, 2011, 4 -# end -# end -# -# -# Example usage of +rubygems_deprecate_command+: -# -# class Gem::Commands::QueryCommand < Gem::Command -# extend Gem::Deprecate -# rubygems_deprecate_command -# -# # ... -# end -# -# -# Example usage of +skip_during+: -# -# class TestSomething < Gem::Testcase -# def test_some_thing_with_deprecations -# Gem::Deprecate.skip_during do -# actual_stdout, actual_stderr = capture_output do -# Gem.something_deprecated -# end -# assert_empty actual_stdout -# assert_equal(expected, actual_stderr) -# end -# end -# end -module Gem::Deprecate +module Gem + ## + # Provides 3 methods for declaring when something is going away. + # + # <tt>deprecate(name, repl, year, month)</tt>: + # Indicate something may be removed on/after a certain date. + # + # <tt>rubygems_deprecate(name, replacement=:none)</tt>: + # Indicate something will be removed in the next major RubyGems version, + # and (optionally) a replacement for it. + # + # +rubygems_deprecate_command+: + # Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be + # removed in the next RubyGems version. + # + # Also provides +skip_during+ for temporarily turning off deprecation warnings. + # This is intended to be used in the test suite, so deprecation warnings + # don't cause test failures if you need to make sure stderr is otherwise empty. + # + # + # Example usage of +deprecate+ and +rubygems_deprecate+: + # + # class Legacy + # def self.some_class_method + # # ... + # end + # + # def some_instance_method + # # ... + # end + # + # def some_old_method + # # ... + # end + # + # extend Gem::Deprecate + # deprecate :some_instance_method, "X.z", 2011, 4 + # rubygems_deprecate :some_old_method, "Modern#some_new_method" + # + # class << self + # extend Gem::Deprecate + # deprecate :some_class_method, :none, 2011, 4 + # end + # end + # + # + # Example usage of +rubygems_deprecate_command+: + # + # class Gem::Commands::QueryCommand < Gem::Command + # extend Gem::Deprecate + # rubygems_deprecate_command + # + # # ... + # end + # + # + # Example usage of +skip_during+: + # + # class TestSomething < Gem::Testcase + # def test_some_thing_with_deprecations + # Gem::Deprecate.skip_during do + # actual_stdout, actual_stderr = capture_output do + # Gem.something_deprecated + # end + # assert_empty actual_stdout + # assert_equal(expected, actual_stderr) + # end + # end + # end - def self.skip # :nodoc: - @skip ||= false - end + module Deprecate + def self.skip # :nodoc: + @skip ||= false + end - def self.skip=(v) # :nodoc: - @skip = v - end + def self.skip=(v) # :nodoc: + @skip = v + end - ## - # Temporarily turn off warnings. Intended for tests only. + ## + # Temporarily turn off warnings. Intended for tests only. - def skip_during - Gem::Deprecate.skip, original = true, Gem::Deprecate.skip - yield - ensure - Gem::Deprecate.skip = original - end + def skip_during + original = Gem::Deprecate.skip + Gem::Deprecate.skip = true + yield + ensure + Gem::Deprecate.skip = original + end - def self.next_rubygems_major_version # :nodoc: - Gem::Version.new(Gem.rubygems_version.segments.first).bump - end + def self.next_rubygems_major_version # :nodoc: + Gem::Version.new(Gem.rubygems_version.segments.first).bump + end - ## - # Simple deprecation method that deprecates +name+ by wrapping it up - # in a dummy method. It warns on each call to the dummy method - # telling the user of +repl+ (unless +repl+ is :none) and the - # year/month that it is planned to go away. + ## + # Simple deprecation method that deprecates +name+ by wrapping it up + # in a dummy method. It warns on each call to the dummy method + # telling the user of +repl+ (unless +repl+ is :none) and the + # year/month that it is planned to go away. - def deprecate(name, repl, year, month) - class_eval do - old = "_deprecated_#{name}" - alias_method old, name - define_method name do |*args, &block| - klass = self.kind_of? Module - target = klass ? "#{self}." : "#{self.class}#" - msg = [ "NOTE: #{target}#{name} is deprecated", - repl == :none ? " with no replacement" : "; use #{repl} instead", - ". It will be removed on or after %4d-%02d." % [year, month], - "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", - ] - warn "#{msg.join}." unless Gem::Deprecate.skip - send old, *args, &block + def deprecate(name, repl, year, month) + class_eval do + old = "_deprecated_#{name}" + alias_method old, name + define_method name do |*args, &block| + klass = is_a? Module + target = klass ? "#{self}." : "#{self.class}#" + msg = [ + "NOTE: #{target}#{name} is deprecated", + repl == :none ? " with no replacement" : "; use #{repl} instead", + format(". It will be removed on or after %4d-%02d.", year, month), + "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", + ] + warn "#{msg.join}." unless Gem::Deprecate.skip + send old, *args, &block + end + ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - end - ## - # Simple deprecation method that deprecates +name+ by wrapping it up - # in a dummy method. It warns on each call to the dummy method - # telling the user of +repl+ (unless +repl+ is :none) and the - # Rubygems version that it is planned to go away. + ## + # Simple deprecation method that deprecates +name+ by wrapping it up + # in a dummy method. It warns on each call to the dummy method + # telling the user of +repl+ (unless +repl+ is :none) and the + # Rubygems version that it is planned to go away. - def rubygems_deprecate(name, replacement=:none) - class_eval do - old = "_deprecated_#{name}" - alias_method old, name - define_method name do |*args, &block| - klass = self.kind_of? Module - target = klass ? "#{self}." : "#{self.class}#" - msg = [ "NOTE: #{target}#{name} is deprecated", - replacement == :none ? " with no replacement" : "; use #{replacement} instead", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}", - "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", - ] - warn "#{msg.join}." unless Gem::Deprecate.skip - send old, *args, &block + def rubygems_deprecate(name, replacement = :none, version = nil) + class_eval do + old = "_deprecated_#{name}" + alias_method old, name + define_method name do |*args, &block| + klass = is_a? Module + target = klass ? "#{self}." : "#{self.class}#" + version ||= Gem::Deprecate.next_rubygems_major_version + msg = [ + "NOTE: #{target}#{name} is deprecated", + replacement == :none ? " with no replacement" : "; use #{replacement} instead", + ". It will be removed in Rubygems #{version}", + "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}", + ] + warn "#{msg.join}." unless Gem::Deprecate.skip + send old, *args, &block + end + ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - ruby2_keywords name if respond_to?(:ruby2_keywords, true) end - end - # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command - class_eval do - define_method "deprecated?" do - true - end + # Deprecation method to deprecate Rubygems commands + def rubygems_deprecate_command(version = nil) + class_eval do + define_method "deprecated?" do + true + end - define_method "deprecation_warning" do - msg = [ "#{self.command} command is deprecated", - ". It will be removed in Rubygems #{Gem::Deprecate.next_rubygems_major_version}.\n", - ] + define_method "deprecation_warning" do + version ||= Gem::Deprecate.next_rubygems_major_version + msg = [ + "#{command} command is deprecated", + ". It will be removed in Rubygems #{version}.\n", + ] - alert_warning "#{msg.join}" unless Gem::Deprecate.skip + alert_warning msg.join.to_s unless Gem::Deprecate.skip + end end end - end - - module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during + module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during + end end diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb index 96829227fc..4f26260d83 100644 --- a/lib/rubygems/doctor.rb +++ b/lib/rubygems/doctor.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "user_interaction" @@ -32,7 +33,7 @@ class Gem::Doctor Gem::REPOSITORY_SUBDIRECTORIES.sort - REPOSITORY_EXTENSION_MAP.map {|(k,_)| k }.sort - raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ', '}" unless + raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ", "}" unless missing.empty? ## @@ -52,7 +53,7 @@ class Gem::Doctor # Specs installed in this gem repository def installed_specs # :nodoc: - @installed_specs ||= Gem::Specification.map {|s| s.full_name } + @installed_specs ||= Gem::Specification.map(&:full_name) end ## @@ -74,7 +75,7 @@ class Gem::Doctor Gem.use_paths @gem_repository.to_s unless gem_repository? - say "This directory does not appear to be a RubyGems repository, " + + say "This directory does not appear to be a RubyGems repository, " \ "skipping" say return @@ -103,16 +104,16 @@ class Gem::Doctor directory = File.join(@gem_repository, sub_directory) Dir.entries(directory).sort.each do |ent| - next if ent == "." || ent == ".." + next if [".", ".."].include?(ent) child = File.join(directory, ent) next unless File.exist?(child) basename = File.basename(child, extension) next if installed_specs.include? basename - next if /^rubygems-\d/ =~ basename - next if "specifications" == sub_directory && "default" == basename - next if "plugins" == sub_directory && Gem.plugin_suffix_regexp =~ (basename) + next if /^rubygems-\d/.match?(basename) + next if sub_directory == "specifications" && basename == "default" + next if sub_directory == "plugins" && Gem.plugin_suffix_regexp =~ basename type = File.directory?(child) ? "directory" : "file" diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index ac82a551a5..4bbc5217e0 100644 --- a/lib/rubygems/errors.rb +++ b/lib/rubygems/errors.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # This file contains all the various exceptions and other errors that are used # inside of RubyGems. @@ -25,10 +26,11 @@ module Gem # system. Instead of rescuing from this class, make sure to rescue from the # superclass Gem::LoadError to catch all types of load errors. class MissingSpecError < Gem::LoadError - def initialize(name, requirement, extra_message=nil) + def initialize(name, requirement, extra_message = nil) @name = name @requirement = requirement @extra_message = extra_message + super(message) end def message # :nodoc: @@ -52,15 +54,15 @@ module Gem attr_reader :specs def initialize(name, requirement, specs) - super(name, requirement) @specs = specs + super(name, requirement) end private def build_message names = specs.map(&:full_name) - "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ','}]\n" + "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ","}]\n" end end @@ -133,11 +135,7 @@ module Gem ## # A wordy description of the error. def wordy - "Found %s (%s), but was for platform%s %s" % - [@name, - @version, - @platforms.size == 1 ? "" : "s", - @platforms.join(" ,")] + format("Found %s (%s), but was for platform%s %s", @name, @version, @platforms.size == 1 ? "" : "s", @platforms.join(" ,")) end end @@ -174,6 +172,6 @@ module Gem ## # The "exception" alias allows you to call raise on a SourceFetchProblem. - alias exception error + alias_method :exception, :error end end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index ca4fbb20de..e00a70c662 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -require_relative "deprecate" require_relative "unknown_command_spell_checker" ## @@ -21,20 +20,11 @@ class Gem::UnknownCommandError < Gem::Exception end def self.attach_correctable - return if defined?(@attached) + return if method_defined?(:corrections) - if defined?(DidYouMean::SPELL_CHECKERS) && defined?(DidYouMean::Correctable) - if DidYouMean.respond_to?(:correct_error) - DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker) - else - DidYouMean::SPELL_CHECKERS["Gem::UnknownCommandError"] = - Gem::UnknownCommandSpellChecker - - prepend DidYouMean::Correctable - end + if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error) + DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker) end - - @attached = true end end @@ -43,22 +33,24 @@ class Gem::DependencyError < Gem::Exception; end class Gem::DependencyRemovalException < Gem::Exception; end ## -# Raised by Gem::Resolver when a Gem::Dependency::Conflict reaches the -# toplevel. Indicates which dependencies were incompatible through #conflict -# and #conflicting_dependencies +# Raised by Gem::Resolver when dependency resolution fails. class Gem::DependencyResolutionError < Gem::DependencyError - attr_reader :conflict - def initialize(conflict) - @conflict = conflict - a, b = conflicting_dependencies + @explanation = conflict.explanation + super @explanation + end + + def explanation + @explanation + end - super "conflicting dependencies #{a} and #{b}\n#{@conflict.explanation}" + def conflict + nil end def conflicting_dependencies - @conflict.conflicting_dependencies + [] end end @@ -104,16 +96,13 @@ end class Gem::GemNotFoundException < Gem::Exception; end -## -# Raised by the DependencyInstaller when a specific gem cannot be found - class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException ## # Creates a new SpecificGemNotFoundException for a gem with the given +name+ # and +version+. Any +errors+ encountered when attempting to find the gem # are also stored. - def initialize(name, version, errors=nil) + def initialize(name, version, errors = nil) super "Could not find a valid gem '#{name}' (#{version}) locally or in a repository" @name = name @@ -137,41 +126,10 @@ class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException attr_reader :errors end -## -# Raised by Gem::Resolver when dependencies conflict and create the -# inability to find a valid possible spec for a request. - -class Gem::ImpossibleDependenciesError < Gem::Exception - attr_reader :conflicts - attr_reader :request - - def initialize(request, conflicts) - @request = request - @conflicts = conflicts - - super build_message - end - - def build_message # :nodoc: - requester = @request.requester - requester = requester ? requester.spec.full_name : "The user" - dependency = @request.dependency - - message = "#{requester} requires #{dependency} but it conflicted:\n".dup - - @conflicts.each do |_, conflict| - message << conflict.explanation - end - - message - end - - def dependency - @request.dependency - end -end +Gem.deprecate_constant :SpecificGemNotFoundException class Gem::InstallError < Gem::Exception; end + class Gem::RuntimeRequirementNotMetError < Gem::InstallError attr_accessor :suggestion def message @@ -214,6 +172,16 @@ class Gem::RubyVersionMismatch < Gem::Exception; end class Gem::VerificationError < Gem::Exception; end ## +# Raised by Gem::WebauthnListener when an error occurs during security +# device verification. + +class Gem::WebauthnVerificationError < Gem::Exception + def initialize(message) + super "Security device verification failed: #{message}" + end +end + +## # Raised to indicate that a system exit should occur with the specified # exit_code @@ -221,7 +189,7 @@ class Gem::SystemExitException < SystemExit ## # The exit code for the process - alias exit_code status + alias_method :exit_code, :status ## # Creates a new SystemExitException with the given +exit_code+ @@ -251,10 +219,10 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError # Creates a new UnsatisfiableDependencyError for the unsatisfiable # Gem::Resolver::DependencyRequest +dep+ - def initialize(dep, platform_mismatch=nil) + def initialize(dep, platform_mismatch = nil) if platform_mismatch && !platform_mismatch.empty? plats = platform_mismatch.map {|x| x.platform.to_s }.sort.uniq - super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(', ')}" + super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(", ")}" else if dep.explicit? super "Unable to resolve dependency: user requested '#{dep}'" @@ -281,9 +249,3 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError @dependency.requirement end end - -## -# Backwards compatible typo'd exception class for early RubyGems 2.0.x - -Gem::UnsatisfiableDepedencyError = Gem::UnsatisfiableDependencyError # :nodoc: -Gem.deprecate_constant :UnsatisfiableDepedencyError diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb index d714985c21..b5ca126a08 100644 --- a/lib/rubygems/ext.rb +++ b/lib/rubygems/ext.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb index 727bc065c2..0329c1eec3 100644 --- a/lib/rubygems/ext/build_error.rb +++ b/lib/rubygems/ext/build_error.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Raised when there is an error while building extensions. diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 98d354183c..e00cf159da 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -10,6 +11,9 @@ require_relative "../user_interaction" class Gem::Ext::Builder include Gem::UserInteraction + class NoMakefileError < Gem::InstallError + end + attr_accessor :build_args # :nodoc: def self.class_name @@ -17,27 +21,37 @@ class Gem::Ext::Builder $1.downcase end - def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"]) + def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"], + target_rbconfig: Gem.target_rbconfig, n_jobs: nil) unless File.exist? File.join(make_dir, "Makefile") - raise Gem::InstallError, "Makefile not found" + # No makefile exists, nothing to do. + raise NoMakefileError, "No Makefile found in #{make_dir}" end # try to find make program from Ruby configure arguments first - RbConfig::CONFIG["configure_args"] =~ /with-make-prog\=(\w+)/ + target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/ make_program_name = ENV["MAKE"] || ENV["make"] || $1 - unless make_program_name - make_program_name = (RUBY_PLATFORM.include?("mswin")) ? "nmake" : "make" - end - make_program = Shellwords.split(make_program_name) + make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make" + make_program = shellsplit(make_program_name) + is_nmake = /\bnmake/i.match?(make_program_name) # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform. - destdir = (/\bnmake/i !~ make_program_name || ENV["DESTDIR"] && ENV["DESTDIR"] != "") ? "DESTDIR=%s" % ENV["DESTDIR"] : "" + destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" + + # nmake doesn't support parallel build + unless is_nmake + have_make_arguments = make_program.size > 1 + + if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs + make_program << "-j#{n_jobs}" + end + end env = [destdir] if sitedir - env << "sitearchdir=%s" % sitedir - env << "sitelibdir=%s" % sitedir + env << format("sitearchdir=%s", sitedir) + env << format("sitelibdir=%s", sitedir) end targets.each do |target| @@ -55,30 +69,54 @@ class Gem::Ext::Builder end end + def self.ruby + # Gem.ruby is quoted if it contains whitespace + cmd = shellsplit(Gem.ruby) + + # This load_path is only needed when running rubygems test without a proper installation. + # Prepending it in a normal installation will cause problem with order of $LOAD_PATH. + # Therefore only add load_path if it is not present in the default $LOAD_PATH. + load_path = File.expand_path("../..", __dir__) + case load_path + when RbConfig::CONFIG["sitelibdir"], RbConfig::CONFIG["vendorlibdir"], RbConfig::CONFIG["rubylibdir"] + cmd + else + cmd << "-I#{load_path}" + end + end + def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {}) verbose = Gem.configuration.really_verbose begin - rubygems_gemdeps, ENV["RUBYGEMS_GEMDEPS"] = ENV["RUBYGEMS_GEMDEPS"], nil + rubygems_gemdeps = ENV["RUBYGEMS_GEMDEPS"] + ENV["RUBYGEMS_GEMDEPS"] = nil if verbose puts("current directory: #{dir}") p(command) end results << "current directory: #{dir}" - require "shellwords" - results << command.shelljoin + results << shelljoin(command) require "open3" # Set $SOURCE_DATE_EPOCH for the subprocess. build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env) output, status = begin - Open3.capture2e(build_env, *command, :chdir => dir) - rescue => error + Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread| + stdin.close + output = String.new + while line = stdouterr.gets + output << line + if verbose + print line + end + end + [output, wait_thread.value] + end + rescue StandardError => error raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}" end - if verbose - puts output - else + unless verbose results << output end ensure @@ -103,17 +141,29 @@ class Gem::Ext::Builder end end + def self.shellsplit(command) + require "shellwords" + + Shellwords.split(command) + end + + def self.shelljoin(command) + require "shellwords" + + Shellwords.join(command) + end + ## # Creates a new extension builder for +spec+. If the +spec+ does not yet # have build arguments, saved, set +build_args+ which is an ARGV-style # array. - def initialize(spec, build_args = spec.build_args) + def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil) @spec = spec @build_args = build_args @gem_dir = spec.full_gem_path - - @ran_rake = false + @target_rbconfig = target_rbconfig + @build_jobs = build_jobs end ## @@ -126,13 +176,11 @@ class Gem::Ext::Builder when /configure/ then Gem::Ext::ConfigureBuilder when /rakefile/i, /mkrf_conf/i then - @ran_rake = true Gem::Ext::RakeBuilder when /CMakeLists.txt/ then - Gem::Ext::CmakeBuilder + Gem::Ext::CmakeBuilder.new when /Cargo.toml/ then - # We use the spec name here to ensure we invoke the correct init function later - Gem::Ext::CargoBuilder.new(@spec) + Gem::Ext::CargoBuilder.new else build_error("No builder for extension '#{extension}'") end @@ -169,12 +217,12 @@ EOF FileUtils.mkdir_p dest_path results = builder.build(extension, dest_path, - results, @build_args, lib_dir, extension_dir) + results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs) verbose { results.join("\n") } write_gem_make_out results.join "\n" - rescue => e + rescue StandardError => e results << e.message build_error(results.join("\n"), $@) end @@ -190,7 +238,7 @@ EOF if @build_args.empty? say "Building native extensions. This could take a while..." else - say "Building native extensions with: '#{@build_args.join ' '}'" + say "Building native extensions with: '#{@build_args.join " "}'" say "This could take a while..." end @@ -200,8 +248,6 @@ EOF FileUtils.rm_f @spec.gem_build_complete_path @spec.extensions.each do |extension| - break if @ran_rake - build_extension extension, dest_path end diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 60ab5544fe..516459dd60 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -6,30 +6,64 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder attr_accessor :spec, :runner, :profile - def initialize(spec) + def initialize require_relative "../command" require_relative "cargo_builder/link_flag_converter" - @spec = spec @runner = self.class.method(:run) @profile = :release end - def build(_extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) + require "tempfile" require "fileutils" - require "shellwords" - build_crate(dest_path, results, args, cargo_dir) - validate_cargo_build!(dest_path) - rename_cdylib_for_ruby_compatibility(dest_path) - finalize_directory(dest_path, lib_dir, cargo_dir) - results - end + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring" + end + + # Where's the Cargo.toml of the crate we're building + cargo_toml = File.join(cargo_dir, "Cargo.toml") + # What's the crate's name + crate_name = cargo_crate_name(cargo_dir, cargo_toml, results) + + begin + # Create a tmp dir to do the build in + tmp_dest = Dir.mktmpdir(".gem.", cargo_dir) + + # Run the build + cmd = cargo_command(cargo_toml, tmp_dest, args, crate_name) + runner.call(cmd, results, "cargo", cargo_dir, build_env) + + # Where do we expect Cargo to write the compiled library + dylib_path = cargo_dylib_path(tmp_dest, crate_name) - def build_crate(dest_path, results, args, cargo_dir) - env = build_env - cmd = cargo_command(cargo_dir, dest_path, args) - runner.call cmd, results, "cargo", cargo_dir, env + # Helpful error if we didn't find the compiled library + raise DylibNotFoundError, tmp_dest unless File.exist?(dylib_path) + + # Cargo and Ruby differ on how the library should be named, rename from + # what Cargo outputs to what Ruby expects + dlext_name = "#{crate_name}.#{makefile_config("DLEXT")}" + dlext_path = File.join(File.dirname(dylib_path), dlext_name) + FileUtils.cp(dylib_path, dlext_path) + + nesting = extension_nesting(extension) + + if Gem.install_extension_in_lib && lib_dir + nested_lib_dir = File.join(lib_dir, nesting) + FileUtils.mkdir_p nested_lib_dir + FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true + end + + # move to final destination + nested_dest_path = File.join(dest_path, nesting) + FileUtils.mkdir_p nested_dest_path + FileUtils.cp_r dlext_path, nested_dest_path, remove_destination: true + ensure + # clean up intermediary build artifacts + FileUtils.rm_rf tmp_dest if tmp_dest + end results end @@ -42,39 +76,57 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder build_env end - def cargo_command(cargo_dir, dest_path, args = []) - manifest = File.join(cargo_dir, "Cargo.toml") - cargo = ENV.fetch("CARGO", "cargo") - + def cargo_command(cargo_toml, dest_path, args = [], crate_name = nil) cmd = [] cmd += [cargo, "rustc"] cmd += ["--crate-type", "cdylib"] cmd += ["--target", ENV["CARGO_BUILD_TARGET"]] if ENV["CARGO_BUILD_TARGET"] cmd += ["--target-dir", dest_path] - cmd += ["--manifest-path", manifest] + cmd += ["--manifest-path", cargo_toml] cmd += ["--lib"] cmd += ["--profile", profile.to_s] cmd += ["--locked"] cmd += Gem::Command.build_args cmd += args cmd += ["--"] - cmd += [*cargo_rustc_args(dest_path)] + cmd += [*cargo_rustc_args(dest_path, crate_name)] cmd end private + def cargo + ENV.fetch("CARGO", "cargo") + end + + # returns the directory nesting of the extension, ignoring the first part, so + # "ext/foo/bar/Cargo.toml" becomes "foo/bar" + def extension_nesting(extension) + parts = extension.to_s.split(Regexp.union([File::SEPARATOR, File::ALT_SEPARATOR].compact)) + + parts = parts.each_with_object([]) do |segment, final| + next if segment == "." + if segment == ".." + raise Gem::InstallError, "extension outside of gem root" if final.empty? + next final.pop + end + final << segment + end + + File.join(parts[1...-1]) + end + def rb_config_env result = {} RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v } result end - def cargo_rustc_args(dest_dir) + def cargo_rustc_args(dest_dir, crate_name) [ *linker_args, *mkmf_libpath, - *rustc_dynamic_linker_flags(dest_dir), + *rustc_dynamic_linker_flags(dest_dir, crate_name), *rustc_lib_flags(dest_dir), *platform_specific_rustc_args(dest_dir), ] @@ -105,7 +157,11 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder # We want to use the same linker that Ruby uses, so that the linker flags from # mkmf work properly. def linker_args - cc_flag = Shellwords.split(makefile_config("CC")) + cc_flag = self.class.shellsplit(makefile_config("CC")) + # Avoid to ccache like tool from Rust build + # see https://github.com/ruby/rubygems/pull/8521#issuecomment-2689854359 + # ex. CC="ccache gcc" or CC="sccache clang --any --args" + cc_flag.shift if cc_flag.size >= 2 && !cc_flag[1].start_with?("-") linker = cc_flag.shift link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] } @@ -124,7 +180,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder def libruby_args(dest_dir) libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED") - raw_libs = Shellwords.split(libs) + raw_libs = self.class.shellsplit(libs) raw_libs.flat_map {|l| ldflag_to_link_modifier(l) } end @@ -134,44 +190,71 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder makefile_config("ENABLE_SHARED") == "no" end - # Ruby expects the dylib to follow a file name convention for loading - def rename_cdylib_for_ruby_compatibility(dest_path) - new_path = final_extension_path(dest_path) - FileUtils.cp(cargo_dylib_path(dest_path), new_path) - new_path + def cargo_dylib_path(dest_path, crate_name) + so_ext = RbConfig::CONFIG["SOEXT"] + prefix = so_ext == "dll" ? "" : "lib" + path_parts = [dest_path] + path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"] + path_parts += ["release", "#{prefix}#{crate_name}.#{so_ext}"] + File.join(*path_parts) end - def validate_cargo_build!(dir) - dylib_path = cargo_dylib_path(dir) + def cargo_crate_name(cargo_dir, manifest_path, results) + require "open3" + Gem.load_yaml - raise DylibNotFoundError, dir unless File.exist?(dylib_path) + output, status = + begin + Open3.capture2e(cargo, "metadata", "--no-deps", "--format-version", "1", chdir: cargo_dir) + rescue StandardError => error + raise Gem::InstallError, "cargo metadata failed #{error.message}" + end - dylib_path - end + unless status.success? + if Gem.configuration.really_verbose + puts output + else + results << output + end - def final_extension_path(dest_path) - dylib_path = cargo_dylib_path(dest_path) - dlext_name = "#{spec.name}.#{makefile_config("DLEXT")}" - dylib_path.gsub(File.basename(dylib_path), dlext_name) - end + exit_reason = + if status.exited? + ", exit code #{status.exitstatus}" + elsif status.signaled? + ", uncaught signal #{status.termsig}" + end - def cargo_dylib_path(dest_path) - prefix = so_ext == "dll" ? "" : "lib" - path_parts = [dest_path] - path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"] - path_parts += ["release", "#{prefix}#{cargo_crate_name}.#{so_ext}"] - File.join(*path_parts) + raise Gem::InstallError, "cargo metadata failed#{exit_reason}" + end + + # cargo metadata output is specified as json + require "json" + metadata = JSON.parse(output) + package = metadata["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path } + unless package + found = metadata["packages"].map {|md| "#{md["name"]} at #{md["manifest_path"]}" } + raise Gem::InstallError, <<-EOF +failed to determine cargo package name + +looking for: #{manifest_path} + +found: +#{found.join("\n")} +EOF + end + package["name"].tr("-", "_") end - def cargo_crate_name - spec.metadata.fetch("cargo_crate_name", spec.name).tr("-", "_") + def normalize_path(path) + return path unless File::ALT_SEPARATOR + + path.tr(File::ALT_SEPARATOR, File::SEPARATOR) end - def rustc_dynamic_linker_flags(dest_dir) - split_flags("DLDFLAGS") - .map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir) } - .compact - .flat_map {|arg| ldflag_to_link_modifier(arg) } + def rustc_dynamic_linker_flags(dest_dir, crate_name) + split_flags("DLDFLAGS"). + filter_map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }. + flat_map {|arg| ldflag_to_link_modifier(arg) } end def rustc_lib_flags(dest_dir) @@ -179,7 +262,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder end def split_flags(var) - Shellwords.split(RbConfig::CONFIG.fetch(var, "")) + self.class.shellsplit(RbConfig::CONFIG.fetch(var, "")) end def ldflag_to_link_modifier(arg) @@ -204,7 +287,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder end # Interpolate substitution vars in the arg (i.e. $(DEFFILE)) - def maybe_resolve_ldflag_variable(input_arg, dest_dir) + def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name) var_matches = input_arg.match(/\$\((\w+)\)/) return input_arg unless var_matches @@ -215,42 +298,26 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder case var_name # On windows, it is assumed that mkmf has setup an exports file for the - # extension, so we have to to create one ourselves. + # extension, so we have to create one ourselves. when "DEFFILE" - write_deffile(dest_dir) + write_deffile(dest_dir, crate_name) else RbConfig::CONFIG[var_name] end end - def write_deffile(dest_dir) - deffile_path = File.join(dest_dir, "#{spec.name}-#{RbConfig::CONFIG["arch"]}.def") + def write_deffile(dest_dir, crate_name) + deffile_path = File.join(dest_dir, "#{crate_name}-#{RbConfig::CONFIG["arch"]}.def") export_prefix = makefile_config("EXPORT_PREFIX") || "" File.open(deffile_path, "w") do |f| f.puts "EXPORTS" - f.puts "#{export_prefix.strip}Init_#{spec.name}" + f.puts "#{export_prefix.strip}Init_#{crate_name}" end deffile_path end - # We have to basically reimplement RbConfig::CONFIG['SOEXT'] here to support - # Ruby < 2.5 - # - # @see https://github.com/ruby/ruby/blob/c87c027f18c005460746a74c07cd80ee355b16e4/configure.ac#L3185 - def so_ext - return RbConfig::CONFIG["SOEXT"] if RbConfig::CONFIG.key?("SOEXT") - - if win_target? - "dll" - elsif darwin_target? - "dylib" - else - "so" - end - end - # Corresponds to $(LIBPATH) in mkmf def mkmf_libpath ["-L", "native=#{makefile_config("libdir")}"] @@ -264,44 +331,6 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder RbConfig.expand(val.dup) end - # Copied from ExtConfBuilder - def finalize_directory(dest_path, lib_dir, extension_dir) - require "fileutils" - require "tempfile" - - ext_path = final_extension_path(dest_path) - - begin - tmp_dest = Dir.mktmpdir(".gem.", extension_dir) - - # Some versions of `mktmpdir` return absolute paths, which will break make - # if the paths contain spaces. - # - # As such, we convert to a relative path. - tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir) - - full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - - # TODO: remove in RubyGems 4 - if Gem.install_extension_in_lib && lib_dir - FileUtils.mkdir_p lib_dir - FileUtils.cp_r ext_path, lib_dir, remove_destination: true - end - - FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| - destent = ent.class.new(dest_path, ent.rel) - destent.exist? || FileUtils.mv(ent.path, destent.path) - end - ensure - FileUtils.rm_rf tmp_dest if tmp_dest - end - end - - def get_relative_path(path, base) - path[0..base.length - 1] = "." if path.start_with?(base) - path - end - # Error raised when no cdylib artifact was created class DylibNotFoundError < StandardError def initialize(dir) diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index b162664784..e660ed558b 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -1,16 +1,110 @@ # frozen_string_literal: true +# This builder creates extensions defined using CMake. Its is invoked if a Gem's spec file +# sets the `extension` property to a string that contains `CMakeLists.txt`. +# +# In general, CMake projects are built in two steps: +# +# * configure +# * build +# +# The builder follow this convention. First it runs a configuration step and then it runs a build step. +# +# CMake projects can be quite configurable - it is likely you will want to specify options when +# installing a gem. To pass options to CMake specify them after `--` in the gem install command. For example: +# +# gem install <gem_name> -- --preset <preset_name> +# +# Note that options are ONLY sent to the configure step - it is not currently possible to specify +# options for the build step. If this becomes and issue then the CMake builder can be updated to +# support build options. +# +# Useful options to know are: +# +# -G to specify a generator (-G Ninja is recommended) +# -D<CMAKE_VARIABLE> to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release) +# --preset <preset_name> to use a preset +# +# If the Gem author provides presets, via CMakePresets.json file, you will likely want to use one of them. +# If not, you may wish to specify a generator. Ninja is recommended because it can build projects in parallel +# and thus much faster than building them serially like Make does. + class Gem::Ext::CmakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, cmake_dir=Dir.pwd) - unless File.exist?(File.join(cmake_dir, "Makefile")) - require_relative "../command" - cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args] + attr_accessor :runner, :profile + def initialize + @runner = self.class.method(:run) + @profile = :release + end - run cmd, results, class_name, cmake_dir + def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring" end - make dest_path, results, cmake_dir + # Figure the build dir + build_dir = File.join(cmake_dir, "build") + + # Check if the gem defined presets + check_presets(cmake_dir, args, results) + + # Configure + configure(cmake_dir, build_dir, dest_path, args, results) + + # Compile + compile(cmake_dir, build_dir, args, results) results end + + def configure(cmake_dir, build_dir, install_dir, args, results) + cmd = ["cmake", + cmake_dir, + "-B", + build_dir, + "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=#{install_dir}", # Windows + "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=#{install_dir}", # Not Windows + *Gem::Command.build_args, + *args] + + runner.call(cmd, results, "cmake_configure", cmake_dir) + end + + def compile(cmake_dir, build_dir, args, results) + cmd = ["cmake", + "--build", + build_dir.to_s, + "--config", + @profile.to_s] + + runner.call(cmd, results, "cmake_compile", cmake_dir) + end + + private + + def check_presets(cmake_dir, args, results) + # Return if the user specified a preset + return unless args.grep(/--preset/i).empty? + + cmd = ["cmake", + "--list-presets"] + + presets = Array.new + begin + runner.call(cmd, presets, "cmake_presets", cmake_dir) + + # Remove the first two lines of the array which is the current_directory and the command + # that was run + presets = presets[2..].join + results << <<~EOS + The gem author provided a list of presets that can be used to build the gem. To use a preset specify it on the command line: + + gem install <gem_name> -- --preset <preset_name> + + #{presets} + EOS + rescue Gem::InstallError + # Do nothing, CMakePresets.json was not included in the Gem + end + end end diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index 51106c6370..230b214b3c 100644 --- a/lib/rubygems/ext/configure_builder.rb +++ b/lib/rubygems/ext/configure_builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -6,14 +7,19 @@ #++ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd) + def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring" + end + unless File.exist?(File.join(configure_dir, "Makefile")) cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args] run cmd, results, class_name, configure_dir end - make dest_path, results, configure_dir + make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs results end diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 27ebd8c62b..822454355d 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -6,7 +7,8 @@ #++ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd) + def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) require "fileutils" require "tempfile" @@ -21,8 +23,8 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder destdir = ENV["DESTDIR"] begin - require "shellwords" - cmd = Gem.ruby.shellsplit << "-I" << File.expand_path("../..", __dir__) << File.basename(extension) + cmd = ruby << File.basename(extension) + cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path cmd.push(*args) run(cmd, results, class_name, extension_dir) do |s, r| @@ -39,16 +41,18 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder ENV["DESTDIR"] = nil - make dest_path, results, extension_dir, tmp_dest_relative + make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - # TODO remove in RubyGems 4 - if Gem.install_extension_in_lib && lib_dir + is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"] + # Do not copy extension libraries by default when cross-compiling + # not to conflict with the one already built for the host platform. + if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling FileUtils.mkdir_p lib_dir entries = Dir.entries(full_tmp_dest) - %w[. ..] entries = entries.map {|entry| File.join full_tmp_dest, entry } - FileUtils.cp_r entries, lib_dir, :remove_destination => true + FileUtils.cp_r entries, lib_dir, remove_destination: true end FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| @@ -56,18 +60,20 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder destent.exist? || FileUtils.mv(ent.path, destent.path) end - make dest_path, results, extension_dir, tmp_dest_relative, ["clean"] + make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig ensure ENV["DESTDIR"] = destdir end results + rescue Gem::Ext::Builder::NoMakefileError => error + results << error.message + results << "Skipping make for #{extension} as no Makefile was found." + # We are good, do not re-raise the error. ensure FileUtils.rm_rf tmp_dest if tmp_dest end - private - def self.get_relative_path(path, base) path[0..base.length - 1] = "." if path.start_with?(base) path diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 9f2e099d40..d702d7f339 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -6,19 +7,23 @@ #++ class Gem::Ext::RakeBuilder < Gem::Ext::Builder - def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd) - if File.basename(extension) =~ /mkrf_conf/i + def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd, + target_rbconfig = Gem.target_rbconfig, n_jobs: nil) + if target_rbconfig.path + warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring" + end + + if /mkrf_conf/i.match?(File.basename(extension)) run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir) end rake = ENV["rake"] if rake - require "shellwords" - rake = rake.shellsplit + rake = shellsplit(rake) else begin - rake = [Gem.ruby, "-I#{File.expand_path("../..", __dir__)}", "-rrubygems", Gem.bin_path("rake", "rake")] + rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake") rescue Gem::Exception rake = [Gem.default_exec_format % "rake"] end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 31890a60d7..e60cebd0cb 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -7,7 +8,6 @@ require_relative "../rubygems" require_relative "command_manager" -require_relative "deprecate" ## # Run an instance of the gem program. @@ -28,11 +28,16 @@ class Gem::GemRunner # Run the gem command with the following arguments. def run(args) + validate_encoding args build_args = extract_build_args args do_configuration args - Gem.load_env_plugins rescue nil + begin + Gem.load_env_plugins + rescue StandardError + nil + end Gem.load_plugins cmd = @command_manager_class.instance @@ -40,10 +45,10 @@ class Gem::GemRunner cmd.command_names.each do |command_name| config_args = Gem.configuration[command_name] config_args = case config_args - when String - config_args.split " " - else - Array(config_args) + when String + config_args.split " " + else + Array(config_args) end Gem::Command.add_specific_extra_args command_name, config_args end @@ -67,6 +72,14 @@ class Gem::GemRunner private + def validate_encoding(args) + invalid_arg = args.find {|arg| !arg.valid_encoding? } + + if invalid_arg + raise Gem::OptionParser::InvalidArgument.new("'#{invalid_arg.scrub}' has invalid encoding") + end + end + def do_configuration(args) Gem.configuration = @config_file_class.new(args) Gem.use_paths Gem.configuration[:gemhome], Gem.configuration[:gempath] diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index d4078aaf5b..9c22c14fad 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true + require_relative "remote_fetcher" require_relative "text" +require_relative "gemcutter_utilities/webauthn_listener" +require_relative "gemcutter_utilities/webauthn_poller" ## # Utility methods for using the RubyGems API. module Gem::GemcutterUtilities - ERROR_CODE = 1 - API_SCOPES = %i[index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks show_dashboard].freeze + API_SCOPES = [:index_rubygems, :push_rubygem, :yank_rubygem, :add_owner, :remove_owner, :access_webhooks].freeze + EXCLUSIVELY_API_SCOPES = [:show_dashboard].freeze include Gem::Text @@ -59,6 +62,10 @@ module Gem::GemcutterUtilities options[:otp] || ENV["GEM_HOST_OTP_CODE"] end + def webauthn_enabled? + options[:webauthn] + end + ## # The host to connect to either from the RUBYGEMS_HOST environment variable # or from the user's configuration @@ -81,8 +88,8 @@ module Gem::GemcutterUtilities # # If +allowed_push_host+ metadata is present, then it will only allow that host. - def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, &block) - require "net/http" + def rubygems_api_request(method, path, host = nil, allowed_push_host = nil, scope: nil, credentials: {}, &block) + require_relative "vendored_net_http" self.host = host if host unless self.host @@ -91,8 +98,8 @@ module Gem::GemcutterUtilities end if allowed_push_host - allowed_host_uri = URI.parse(allowed_push_host) - host_uri = URI.parse(self.host) + allowed_host_uri = Gem::URI.parse(allowed_push_host) + host_uri = Gem::URI.parse(self.host) unless (host_uri.scheme == allowed_host_uri.scheme) && (host_uri.host == allowed_host_uri.host) alert_error "#{self.host.inspect} is not allowed by the gemspec, which only allows #{allowed_push_host.inspect}" @@ -100,11 +107,11 @@ module Gem::GemcutterUtilities end end - uri = URI.parse "#{self.host}/#{path}" + uri = Gem::URI.parse "#{self.host}/#{path}" response = request_with_otp(method, uri, &block) if mfa_unauthorized?(response) - ask_otp + fetch_otp(credentials) response = request_with_otp(method, uri, &block) end @@ -117,27 +124,26 @@ module Gem::GemcutterUtilities end def mfa_unauthorized?(response) - response.kind_of?(Net::HTTPUnauthorized) && response.body.start_with?("You have enabled multifactor authentication") + response.is_a?(Gem::Net::HTTPUnauthorized) && response.body.start_with?("You have enabled multifactor authentication") end def update_scope(scope) - sign_in_host = self.host + sign_in_host = host pretty_host = pretty_host(sign_in_host) update_scope_params = { scope => true } say "The existing key doesn't have access of #{scope} on #{pretty_host}. Please sign in to update access." - email = ask " Email: " - password = ask_for_password "Password: " + identifier = ask "Username/email: " + password = ask_for_password " Password: " response = rubygems_api_request(:put, "api/v1/api_key", sign_in_host, scope: scope) do |request| - request.basic_auth email, password - request["OTP"] = otp if otp - request.body = URI.encode_www_form({ :api_key => api_key }.merge(update_scope_params)) + request.basic_auth identifier, password + request.body = Gem::URI.encode_www_form({ api_key: api_key }.merge(update_scope_params)) end - with_response response do |resp| + with_response response do |_resp| say "Added #{scope} scope to the existing API key" end end @@ -147,33 +153,34 @@ module Gem::GemcutterUtilities # key. def sign_in(sign_in_host = nil, scope: nil) - sign_in_host ||= self.host - return if api_key - + sign_in_host ||= host pretty_host = pretty_host(sign_in_host) - + if api_key + say "You are already signed in on #{pretty_host}." + return + end say "Enter your #{pretty_host} credentials." - say "Don't have an account yet? " + + say "Don't have an account yet? " \ "Create one at #{sign_in_host}/sign_up" - email = ask " Email: " - password = ask_for_password "Password: " + identifier = ask "Username/email: " + password = ask_for_password " Password: " say "\n" key_name = get_key_name(scope) scope_params = get_scope_params(scope) - profile = get_user_profile(email, password) + profile = get_user_profile(identifier, password) mfa_params = get_mfa_params(profile) all_params = scope_params.merge(mfa_params) warning = profile["warning"] + credentials = { identifier: identifier, password: password } say "#{warning}\n" if warning response = rubygems_api_request(:post, "api/v1/api_key", - sign_in_host, scope: scope) do |request| - request.basic_auth email, password - request["OTP"] = otp if otp - request.body = URI.encode_www_form({ name: key_name }.merge(all_params)) + sign_in_host, credentials: credentials, scope: scope) do |request| + request.basic_auth identifier, password + request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params)) end with_response response do |resp| @@ -205,14 +212,14 @@ module Gem::GemcutterUtilities def with_response(response, error_prefix = nil) case response - when Net::HTTPSuccess then + when Gem::Net::HTTPSuccess then if block_given? yield response else say clean_text(response.body) end - when Net::HTTPPermanentRedirect, Net::HTTPRedirection then - message = "The request has redirected permanently to #{response['location']}. Please check your defined push host URL." + when Gem::Net::HTTPPermanentRedirect, Gem::Net::HTTPRedirection then + message = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL." message = "#{error_prefix}: #{message}" if error_prefix say clean_text(message) @@ -241,17 +248,67 @@ module Gem::GemcutterUtilities private def request_with_otp(method, uri, &block) - request_method = Net::HTTP.const_get method.to_s.capitalize + request_method = Gem::Net::HTTP.const_get method.to_s.capitalize Gem::RemoteFetcher.fetcher.request(uri, request_method) do |req| req["OTP"] = otp if otp block.call(req) end + ensure + options[:otp] = nil if webauthn_enabled? + end + + def fetch_otp(credentials) + options[:otp] = if webauthn_url = webauthn_verification_url(credentials) + server = TCPServer.new 0 + port = server.addr[1].to_s + + url_with_port = "#{webauthn_url}?port=#{port}" + say "You have enabled multi-factor authentication. Please visit the following URL to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option." + say "" + say url_with_port + say "" + + threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)] + otp_thread = wait_for_otp_thread(*threads) + + threads.each(&:join) + + if error = otp_thread[:error] + alert_error error.message + terminate_interaction(1) + end + + options[:webauthn] = true + + say "You are verified with a security device. You may close the browser window." + otp_thread[:otp] + else + say "You have enabled multi-factor authentication. Please enter OTP code." + ask "Code: " + end + end + + def wait_for_otp_thread(*threads) + loop do + threads.each do |otp_thread| + return otp_thread unless otp_thread.alive? + end + sleep 0.1 + end + ensure + threads.each(&:exit) end - def ask_otp - say "You have enabled multi-factor authentication. Please enter OTP code." - options[:otp] = ask "Code: " + def webauthn_verification_url(credentials) + response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request| + if credentials.empty? + request.add_field "Authorization", api_key + else + request.basic_auth credentials[:identifier], credentials[:password] + end + end + response.is_a?(Gem::Net::HTTPSuccess) ? response.body : nil end def pretty_host(host) @@ -263,15 +320,31 @@ module Gem::GemcutterUtilities end def get_scope_params(scope) - scope_params = {} + scope_params = { index_rubygems: true, push_rubygem: true } if scope scope_params = { scope => true } else - say "Please select scopes you want to enable for the API key (y/n)" - API_SCOPES.each do |scope| - selected = ask_yes_no("#{scope}", false) - scope_params[scope] = true if selected + say "The default access scope is:" + scope_params.each do |k, _v| + say " #{k}: y" + end + say "\n" + customise = ask_yes_no("Do you want to customise scopes?", false) + if customise + EXCLUSIVELY_API_SCOPES.each do |excl_scope| + selected = ask_yes_no("#{excl_scope} (exclusive scope, answering yes will not prompt for other scopes)", false) + next unless selected + + return { excl_scope => true } + end + + scope_params = {} + + API_SCOPES.each do |s| + selected = ask_yes_no(s.to_s, false) + scope_params[s] = true if selected + end end say "\n" end @@ -280,25 +353,25 @@ module Gem::GemcutterUtilities end def default_host? - self.host == Gem::DEFAULT_HOST + host == Gem::DEFAULT_HOST end - def get_user_profile(email, password) + def get_user_profile(identifier, password) return {} unless default_host? response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request| - request.basic_auth email, password + request.basic_auth identifier, password end with_response response do |resp| - Gem::SafeYAML.load clean_text(resp.body) + Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body)) end end def get_mfa_params(profile) mfa_level = profile["mfa"] params = {} - if mfa_level == "ui_only" || mfa_level == "ui_and_gem_signin" + if ["ui_only", "ui_and_gem_signin"].include?(mfa_level) selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)") params["mfa"] = true if selected end @@ -320,6 +393,6 @@ module Gem::GemcutterUtilities end def api_key_forbidden?(response) - response.kind_of?(Net::HTTPForbidden) && response.body.start_with?("The API key doesn't have access") + response.is_a?(Gem::Net::HTTPForbidden) && response.body.start_with?("The API key doesn't have access") end end diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb new file mode 100644 index 0000000000..3f56a077c9 --- /dev/null +++ b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +require_relative "webauthn_listener/response" + +## +# The WebauthnListener class retrieves an OTP after a user successfully WebAuthns with the Gem host. +# An instance opens a socket using the TCPServer instance given and listens for a request from the Gem host. +# The request should be a GET request to the root path and contains the OTP code in the form +# of a query parameter `code`. The listener will return the code which will be used as the OTP for +# API requests. +# +# Types of responses sent by the listener after receiving a request: +# - 200 OK: OTP code was successfully retrieved +# - 204 No Content: If the request was an OPTIONS request +# - 400 Bad Request: If the request did not contain a query parameter `code` +# - 404 Not Found: The request was not to the root path +# - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request +# +# Example usage: +# +# thread = Gem::WebauthnListener.listener_thread("https://rubygems.example", server) +# thread.join +# otp = thread[:otp] +# error = thread[:error] +# + +module Gem::GemcutterUtilities + class WebauthnListener + attr_reader :host + + def initialize(host) + @host = host + end + + def self.listener_thread(host, server) + Thread.new do + thread = Thread.current + thread.abort_on_exception = true + thread.report_on_exception = false + thread[:otp] = new(host).wait_for_otp_code(server) + rescue Gem::WebauthnVerificationError => e + thread[:error] = e + ensure + server.close + end + end + + def wait_for_otp_code(server) + loop do + socket = server.accept + request_line = socket.gets + + method, req_uri, _protocol = request_line.split(" ") + req_uri = Gem::URI.parse(req_uri) + + responder = SocketResponder.new(socket) + + unless root_path?(req_uri) + responder.send(NotFoundResponse.for(host)) + raise Gem::WebauthnVerificationError, "Page at #{req_uri.path} not found." + end + + case method.upcase + when "OPTIONS" + responder.send(NoContentResponse.for(host)) + next # will be GET + when "GET" + if otp = parse_otp_from_uri(req_uri) + responder.send(OkResponse.for(host)) + return otp + end + responder.send(BadRequestResponse.for(host)) + raise Gem::WebauthnVerificationError, "Did not receive OTP from #{host}." + else + responder.send(MethodNotAllowedResponse.for(host)) + raise Gem::WebauthnVerificationError, "Invalid HTTP method #{method.upcase} received." + end + end + end + + private + + def root_path?(uri) + uri.path == "/" + end + + def parse_otp_from_uri(uri) + query = uri.query + return unless query && !query.empty? + + query.split("&") do |param| + key, value = param.split("=", 2) + if value && Gem::URI.decode_www_form_component(key) == "code" + return Gem::URI.decode_www_form_component(value) + end + end + + nil + end + + class SocketResponder + def initialize(socket) + @socket = socket + end + + def send(response) + @socket.print response.to_s + @socket.close + end + end + end +end diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb new file mode 100644 index 0000000000..17baa64fff --- /dev/null +++ b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb @@ -0,0 +1,163 @@ +# frozen_string_literal: true + +## +# The WebauthnListener Response class is used by the WebauthnListener to create +# responses to be sent to the Gem host. It creates a Gem::Net::HTTPResponse instance +# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`. +# Gem::Net::HTTPResponse instances cannot be directly sent over a socket. +# +# Types of response classes: +# - OkResponse +# - NoContentResponse +# - BadRequestResponse +# - NotFoundResponse +# - MethodNotAllowedResponse +# +# Example usage: +# +# server = TCPServer.new(0) +# socket = server.accept +# +# response = OkResponse.for("https://rubygems.example") +# socket.print response.to_s +# socket.close +# + +module Gem::GemcutterUtilities + class WebauthnListener + class Response + attr_reader :http_response + + def self.for(host) + new(host) + end + + def initialize(host) + @host = host + + build_http_response + end + + def to_s + status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n" + headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n" + body = @http_response.body ? "#{@http_response.body}\n" : "" + + status_line + headers + body + end + + private + + # Must be implemented in subclasses + def code + raise NotImplementedError + end + + def reason_phrase + raise NotImplementedError + end + + def body; end + + def build_http_response + response_class = Gem::Net::HTTPResponse::CODE_TO_OBJ[code.to_s] + @http_response = response_class.new("1.1", code, reason_phrase) + @http_response.instance_variable_set(:@read, true) + + add_connection_header + add_access_control_headers + add_body + end + + def add_connection_header + @http_response["connection"] = "close" + end + + def add_access_control_headers + @http_response["access-control-allow-origin"] = @host + @http_response["access-control-allow-methods"] = "POST" + @http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token] + end + + def add_body + return unless body + @http_response["content-type"] = "text/plain; charset=utf-8" + @http_response["content-length"] = body.bytesize + @http_response.instance_variable_set(:@body, body) + end + end + + class OkResponse < Response + private + + def code + 200 + end + + def reason_phrase + "OK" + end + + def body + "success" + end + end + + class NoContentResponse < Response + private + + def code + 204 + end + + def reason_phrase + "No Content" + end + end + + class BadRequestResponse < Response + private + + def code + 400 + end + + def reason_phrase + "Bad Request" + end + + def body + "missing code parameter" + end + end + + class NotFoundResponse < Response + private + + def code + 404 + end + + def reason_phrase + "Not Found" + end + end + + class MethodNotAllowedResponse < Response + private + + def code + 405 + end + + def reason_phrase + "Method Not Allowed" + end + + def add_access_control_headers + super + @http_response["allow"] = %w[GET OPTIONS] + end + end + end +end diff --git a/lib/rubygems/gemcutter_utilities/webauthn_poller.rb b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb new file mode 100644 index 0000000000..fe3f163a88 --- /dev/null +++ b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +## +# The WebauthnPoller class retrieves an OTP after a user successfully WebAuthns. An instance +# polls the Gem host for the OTP code. The polling request (api/v1/webauthn_verification/<webauthn_token>/status.json) +# is sent to the Gem host every 5 seconds and will timeout after 5 minutes. If the status field in the json response +# is "success", the code field will contain the OTP code. +# +# Example usage: +# +# thread = Gem::WebauthnPoller.poll_thread( +# {}, +# "RubyGems.org", +# "https://rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY", +# { email: "email@example.com", password: "password" } +# ) +# thread.join +# otp = thread[:otp] +# error = thread[:error] +# + +module Gem::GemcutterUtilities + class WebauthnPoller + include Gem::GemcutterUtilities + TIMEOUT_IN_SECONDS = 300 + + attr_reader :options, :host + + def initialize(options, host) + @options = options + @host = host + end + + def self.poll_thread(options, host, webauthn_url, credentials) + Thread.new do + thread = Thread.current + thread.abort_on_exception = true + thread.report_on_exception = false + thread[:otp] = new(options, host).poll_for_otp(webauthn_url, credentials) + rescue Gem::WebauthnVerificationError, Gem::Timeout::Error => e + thread[:error] = e + end + end + + def poll_for_otp(webauthn_url, credentials) + Gem::Timeout.timeout(TIMEOUT_IN_SECONDS) do + loop do + response = webauthn_verification_poll_response(webauthn_url, credentials) + raise Gem::WebauthnVerificationError, response.message unless response.is_a?(Gem::Net::HTTPSuccess) + + require "json" + parsed_response = JSON.parse(response.body) + case parsed_response["status"] + when "pending" + sleep 5 + when "success" + return parsed_response["code"] + else + raise Gem::WebauthnVerificationError, parsed_response.fetch("message", "Invalid response from server") + end + end + end + end + + private + + def webauthn_verification_poll_response(webauthn_url, credentials) + webauthn_token = %r{(?<=\/)[^\/]+(?=$)}.match(webauthn_url)[0] + rubygems_api_request(:get, "api/v1/webauthn_verification/#{webauthn_token}/status.json") do |request| + if credentials.empty? + request.add_field "Authorization", api_key + elsif credentials[:identifier] && credentials[:password] + request.basic_auth credentials[:identifier], credentials[:password] + else + raise Gem::WebauthnVerificationError, "Provided missing credentials" + end + end + end + end +end diff --git a/lib/rubygems/gemspec_helpers.rb b/lib/rubygems/gemspec_helpers.rb new file mode 100644 index 0000000000..2b20fcafa1 --- /dev/null +++ b/lib/rubygems/gemspec_helpers.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require_relative "../rubygems" + +## +# Mixin methods for commands that work with gemspecs. + +module Gem::GemspecHelpers + def find_gemspec(glob = "*.gemspec") + gemspecs = Dir.glob(glob).sort + + if gemspecs.size > 1 + alert_error "Multiple gemspecs found: #{gemspecs}, please specify one" + terminate_interaction(1) + end + + gemspecs.first + end +end diff --git a/lib/rubygems/indexer.rb b/lib/rubygems/indexer.rb deleted file mode 100644 index d0061ff82e..0000000000 --- a/lib/rubygems/indexer.rb +++ /dev/null @@ -1,427 +0,0 @@ -# frozen_string_literal: true -require_relative "../rubygems" -require_relative "package" -require "tmpdir" - -## -# Top level class for building the gem repository index. - -class Gem::Indexer - include Gem::UserInteraction - - ## - # Build indexes for RubyGems 1.2.0 and newer when true - - attr_accessor :build_modern - - ## - # Index install location - - attr_reader :dest_directory - - ## - # Specs index install location - - attr_reader :dest_specs_index - - ## - # Latest specs index install location - - attr_reader :dest_latest_specs_index - - ## - # Prerelease specs index install location - - attr_reader :dest_prerelease_specs_index - - ## - # Index build directory - - attr_reader :directory - - ## - # Create an indexer that will index the gems in +directory+. - - def initialize(directory, options = {}) - require "fileutils" - require "tmpdir" - require "zlib" - - options = { :build_modern => true }.merge options - - @build_modern = options[:build_modern] - - @dest_directory = directory - @directory = Dir.mktmpdir "gem_generate_index" - - marshal_name = "Marshal.#{Gem.marshal_version}" - - @master_index = File.join @directory, "yaml" - @marshal_index = File.join @directory, marshal_name - - @quick_dir = File.join @directory, "quick" - @quick_marshal_dir = File.join @quick_dir, marshal_name - @quick_marshal_dir_base = File.join "quick", marshal_name # FIX: UGH - - @quick_index = File.join @quick_dir, "index" - @latest_index = File.join @quick_dir, "latest_index" - - @specs_index = File.join @directory, "specs.#{Gem.marshal_version}" - @latest_specs_index = - File.join(@directory, "latest_specs.#{Gem.marshal_version}") - @prerelease_specs_index = - File.join(@directory, "prerelease_specs.#{Gem.marshal_version}") - @dest_specs_index = - File.join(@dest_directory, "specs.#{Gem.marshal_version}") - @dest_latest_specs_index = - File.join(@dest_directory, "latest_specs.#{Gem.marshal_version}") - @dest_prerelease_specs_index = - File.join(@dest_directory, "prerelease_specs.#{Gem.marshal_version}") - - @files = [] - end - - ## - # Build various indices - - def build_indices - specs = map_gems_to_specs gem_file_list - Gem::Specification._resort! specs - build_marshal_gemspecs specs - build_modern_indices specs if @build_modern - - compress_indices - end - - ## - # Builds Marshal quick index gemspecs. - - def build_marshal_gemspecs(specs) - count = specs.count - progress = ui.progress_reporter count, - "Generating Marshal quick index gemspecs for #{count} gems", - "Complete" - - files = [] - - Gem.time "Generated Marshal quick index gemspecs" do - specs.each do |spec| - next if spec.default_gem? - spec_file_name = "#{spec.original_name}.gemspec.rz" - marshal_name = File.join @quick_marshal_dir, spec_file_name - - marshal_zipped = Gem.deflate Marshal.dump(spec) - - File.open marshal_name, "wb" do |io| - io.write marshal_zipped - end - - files << marshal_name - - progress.updated spec.original_name - end - - progress.done - end - - @files << @quick_marshal_dir - - files - end - - ## - # Build a single index for RubyGems 1.2 and newer - - def build_modern_index(index, file, name) - say "Generating #{name} index" - - Gem.time "Generated #{name} index" do - File.open(file, "wb") do |io| - specs = index.map do |*spec| - # We have to splat here because latest_specs is an array, while the - # others are hashes. - spec = spec.flatten.last - platform = spec.original_platform - - # win32-api-1.0.4-x86-mswin32-60 - unless String === platform - alert_warning "Skipping invalid platform in gem: #{spec.full_name}" - next - end - - platform = Gem::Platform::RUBY if platform.nil? || platform.empty? - [spec.name, spec.version, platform] - end - - specs = compact_specs(specs) - Marshal.dump(specs, io) - end - end - end - - ## - # Builds indices for RubyGems 1.2 and newer. Handles full, latest, prerelease - - def build_modern_indices(specs) - prerelease, released = specs.partition do |s| - s.version.prerelease? - end - latest_specs = - Gem::Specification._latest_specs specs - - build_modern_index(released.sort, @specs_index, "specs") - build_modern_index(latest_specs.sort, @latest_specs_index, "latest specs") - build_modern_index(prerelease.sort, @prerelease_specs_index, - "prerelease specs") - - @files += [@specs_index, - "#{@specs_index}.gz", - @latest_specs_index, - "#{@latest_specs_index}.gz", - @prerelease_specs_index, - "#{@prerelease_specs_index}.gz"] - end - - def map_gems_to_specs(gems) - gems.map do |gemfile| - if File.size(gemfile) == 0 - alert_warning "Skipping zero-length gem: #{gemfile}" - next - end - - begin - spec = Gem::Package.new(gemfile).spec - spec.loaded_from = gemfile - - spec.abbreviate - spec.sanitize - - spec - rescue SignalException - alert_error "Received signal, exiting" - raise - rescue Exception => e - msg = ["Unable to process #{gemfile}", - "#{e.message} (#{e.class})", - "\t#{e.backtrace.join "\n\t"}"].join("\n") - alert_error msg - end - end.compact - end - - ## - # Compresses indices on disk - #-- - # All future files should be compressed using gzip, not deflate - - def compress_indices - say "Compressing indices" - - Gem.time "Compressed indices" do - if @build_modern - gzip @specs_index - gzip @latest_specs_index - gzip @prerelease_specs_index - end - end - end - - ## - # Compacts Marshal output for the specs index data source by using identical - # objects as much as possible. - - def compact_specs(specs) - names = {} - versions = {} - platforms = {} - - specs.map do |(name, version, platform)| - names[name] = name unless names.include? name - versions[version] = version unless versions.include? version - platforms[platform] = platform unless platforms.include? platform - - [names[name], versions[version], platforms[platform]] - end - end - - ## - # Compress +filename+ with +extension+. - - def compress(filename, extension) - data = Gem.read_binary filename - - zipped = Gem.deflate data - - File.open "#{filename}.#{extension}", "wb" do |io| - io.write zipped - end - end - - ## - # List of gem file names to index. - - def gem_file_list - Gem::Util.glob_files_in_dir("*.gem", File.join(@dest_directory, "gems")) - end - - ## - # Builds and installs indices. - - def generate_index - make_temp_directories - build_indices - install_indices - rescue SignalException - ensure - FileUtils.rm_rf @directory - end - - ## - # Zlib::GzipWriter wrapper that gzips +filename+ on disk. - - def gzip(filename) - Zlib::GzipWriter.open "#{filename}.gz" do |io| - io.write Gem.read_binary(filename) - end - end - - ## - # Install generated indices into the destination directory. - - def install_indices - verbose = Gem.configuration.really_verbose - - say "Moving index into production dir #{@dest_directory}" if verbose - - files = @files - files.delete @quick_marshal_dir if files.include? @quick_dir - - if files.include?(@quick_marshal_dir) && !files.include?(@quick_dir) - files.delete @quick_marshal_dir - - dst_name = File.join(@dest_directory, @quick_marshal_dir_base) - - FileUtils.mkdir_p File.dirname(dst_name), :verbose => verbose - FileUtils.rm_rf dst_name, :verbose => verbose - FileUtils.mv(@quick_marshal_dir, dst_name, - :verbose => verbose, :force => true) - end - - files = files.map do |path| - path.sub(/^#{Regexp.escape @directory}\/?/, "") # HACK? - end - - files.each do |file| - src_name = File.join @directory, file - dst_name = File.join @dest_directory, file - - FileUtils.rm_rf dst_name, :verbose => verbose - FileUtils.mv(src_name, @dest_directory, - :verbose => verbose, :force => true) - end - end - - ## - # Make directories for index generation - - def make_temp_directories - FileUtils.rm_rf @directory - FileUtils.mkdir_p @directory, :mode => 0700 - FileUtils.mkdir_p @quick_marshal_dir - end - - ## - # Ensure +path+ and path with +extension+ are identical. - - def paranoid(path, extension) - data = Gem.read_binary path - compressed_data = Gem.read_binary "#{path}.#{extension}" - - unless data == Gem::Util.inflate(compressed_data) - raise "Compressed file #{compressed_path} does not match uncompressed file #{path}" - end - end - - ## - # Perform an in-place update of the repository from newly added gems. - - def update_index - make_temp_directories - - specs_mtime = File.stat(@dest_specs_index).mtime - newest_mtime = Time.at 0 - - updated_gems = gem_file_list.select do |gem| - gem_mtime = File.stat(gem).mtime - newest_mtime = gem_mtime if gem_mtime > newest_mtime - gem_mtime >= specs_mtime - end - - if updated_gems.empty? - say "No new gems" - terminate_interaction 0 - end - - specs = map_gems_to_specs updated_gems - prerelease, released = specs.partition {|s| s.version.prerelease? } - - files = build_marshal_gemspecs specs - - Gem.time "Updated indexes" do - update_specs_index released, @dest_specs_index, @specs_index - update_specs_index released, @dest_latest_specs_index, @latest_specs_index - update_specs_index(prerelease, - @dest_prerelease_specs_index, - @prerelease_specs_index) - end - - compress_indices - - verbose = Gem.configuration.really_verbose - - say "Updating production dir #{@dest_directory}" if verbose - - files << @specs_index - files << "#{@specs_index}.gz" - files << @latest_specs_index - files << "#{@latest_specs_index}.gz" - files << @prerelease_specs_index - files << "#{@prerelease_specs_index}.gz" - - files = files.map do |path| - path.sub(/^#{Regexp.escape @directory}\/?/, "") # HACK? - end - - files.each do |file| - src_name = File.join @directory, file - dst_name = File.join @dest_directory, file # REFACTOR: duped above - - FileUtils.mv src_name, dst_name, :verbose => verbose, - :force => true - - File.utime newest_mtime, newest_mtime, dst_name - end - ensure - FileUtils.rm_rf @directory - end - - ## - # Combines specs in +index+ and +source+ then writes out a new copy to - # +dest+. For a latest index, does not ensure the new file is minimal. - - def update_specs_index(index, source, dest) - specs_index = Marshal.load Gem.read_binary(source) - - index.each do |spec| - platform = spec.original_platform - platform = Gem::Platform::RUBY if platform.nil? || platform.empty? - specs_index << [spec.name, spec.version, platform] - end - - specs_index = compact_specs specs_index.uniq.sort - - File.open dest, "wb" do |io| - Marshal.dump specs_index, io - end - end -end diff --git a/lib/rubygems/install_default_message.rb b/lib/rubygems/install_default_message.rb deleted file mode 100644 index 0d112a15df..0000000000 --- a/lib/rubygems/install_default_message.rb +++ /dev/null @@ -1,12 +0,0 @@ -# frozen_string_literal: true -require_relative "../rubygems" -require_relative "user_interaction" - -## -# A post-install hook that displays "Successfully installed -# some_gem-1.0 as a default gem" - -Gem.post_install do |installer| - ui = Gem::DefaultUserInteraction.ui - ui.say "Successfully installed #{installer.spec.full_name} as a default gem" -end diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb index 2565f36261..a24e26b918 100644 --- a/lib/rubygems/install_message.rb +++ b/lib/rubygems/install_message.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "user_interaction" diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 79effcf21f..e8859cadaf 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -30,14 +31,23 @@ module Gem::InstallUpdateOptions options[:bin_dir] = File.expand_path(value) end + add_option(:"Install/Update", "-j", "--build-jobs VALUE", Integer, + "Specify the number of jobs to pass to `make` when installing", + "gems with native extensions.", + "Defaults to the number of processors.", + "This option is ignored on the mswin platform or", + "if the MAKEFLAGS environment variable is set.") do |value, options| + options[:build_jobs] = value + end + add_option(:"Install/Update", "--document [TYPES]", Array, "Generate documentation for installed gems", "List the documentation types you wish to", "generate. For example: rdoc,ri") do |value, options| options[:document] = case value - when nil then %w[ri] - when false then [] - else value + when nil then %w[ri] + when false then [] + else value end end @@ -49,7 +59,7 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--vendor", "Install gem into the vendor directory.", - "Only for use by gem repackagers.") do |value, options| + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end @@ -59,7 +69,7 @@ module Gem::InstallUpdateOptions end add_option(:"Install/Update", "-N", "--no-document", - "Disable documentation generation") do |value, options| + "Disable documentation generation") do |_value, options| options[:document] = [] end @@ -103,21 +113,21 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--development", "Install additional development", - "dependencies") do |value, options| + "dependencies") do |_value, options| options[:development] = true options[:dev_shallow] = true end add_option(:"Install/Update", "--development-all", "Install development dependencies for all", - "gems (including dev deps themselves)") do |value, options| + "gems (including dev deps themselves)") do |_value, options| options[:development] = true options[:dev_shallow] = false end add_option(:"Install/Update", "--conservative", "Don't attempt to upgrade gems already", - "meeting version requirement") do |value, options| + "meeting version requirement") do |_value, options| options[:conservative] = true options[:minimal_deps] = true end @@ -135,13 +145,13 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "-g", "--file [FILE]", "Read from a gem dependencies API file and", - "install the listed gems") do |v,o| - v = Gem::GEM_DEP_FILES.find do |file| + "install the listed gems") do |v,_o| + v ||= Gem::GEM_DEP_FILES.find do |file| File.exist? file - end unless v + end unless v - message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ', '})" + message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ", "})" raise Gem::OptionParser::InvalidArgument, "cannot find gem dependencies file #{message}" @@ -153,31 +163,47 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--without GROUPS", Array, "Omit the named groups (comma separated)", "when installing from a gem dependencies", - "file") do |v,o| - options[:without_groups].concat v.map {|without| without.intern } + "file") do |v,_o| + options[:without_groups].concat v.map(&:intern) end - add_option(:"Install/Update", "--default", + add_option(:Deprecated, "--default", "Add the gem's full specification to", - "specifications/default and extract only its bin") do |v,o| - options[:install_as_default] = v + "specifications/default and extract only its bin") do |v,_o| end add_option(:"Install/Update", "--explain", "Rather than install the gems, indicate which would", - "be installed") do |v,o| + "be installed") do |v,_o| options[:explain] = v end add_option(:"Install/Update", "--[no-]lock", - "Create a lock file (when used with -g/--file)") do |v,o| + "Create a lock file (when used with -g/--file)") do |v,_o| options[:lock] = v end add_option(:"Install/Update", "--[no-]suggestions", - "Suggest alternates when gems are not found") do |v,o| + "Suggest alternates when gems are not found") do |v,_o| options[:suggest_alternate] = v end + + add_option(:"Install/Update", "--target-rbconfig [FILE]", + "rbconfig.rb for the deployment target platform") do |v, _o| + Gem.set_target_rbconfig(v) + end + + add_option(:"Install/Update", "--[no-]build-extension", + "Build native extensions during installation.", + "Defaults to true") do |v, _o| + options[:build_extension] = v + end + + add_option(:"Install/Update", "--[no-]install-plugin", + "Install plugins during installation.", + "Defaults to true") do |v, _o| + options[:install_plugin] = v + end end ## @@ -185,7 +211,7 @@ module Gem::InstallUpdateOptions def install_update_options { - :document => %w[ri], + document: %w[ri], } end @@ -195,5 +221,4 @@ module Gem::InstallUpdateOptions def install_update_defaults_str "--document=ri" end - end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 9fbb2824c7..a6e1dc4730 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -7,7 +8,6 @@ require_relative "installer_uninstaller_utils" require_relative "exceptions" -require_relative "deprecate" require_relative "package" require_relative "ext" require_relative "user_interaction" @@ -26,8 +26,6 @@ require_relative "user_interaction" # file. See Gem.pre_install and Gem.post_install for details. class Gem::Installer - extend Gem::Deprecate - ## # Paths where env(1) might live. Some systems are broken and have it in # /bin @@ -65,31 +63,7 @@ class Gem::Installer attr_reader :package - @path_warning = false - class << self - # - # Changes in rubygems to lazily loading `rubygems/command` (in order to - # lazily load `optparse` as a side effect) affect bundler's custom installer - # which uses `Gem::Command` without requiring it (up until bundler 2.2.29). - # This hook is to compensate for that missing require. - # - # TODO: Remove when rubygems no longer supports running on bundler older - # than 2.2.29. - - def inherited(klass) - if klass.name == "Bundler::RubyGemsGemInstaller" - require "rubygems/command" - end - - super(klass) - end - - ## - # True if we've warned about PATH not including Gem.bindir - - attr_accessor :path_warning - ## # Overrides the executable format. # @@ -176,7 +150,7 @@ class Gem::Installer # process. If not set, then Gem::Command.build_args is used # :post_install_message:: Print gem post install message if true - def initialize(package, options={}) + def initialize(package, options = {}) require "fileutils" @options = options @@ -187,13 +161,6 @@ class Gem::Installer @package.dir_mode = options[:dir_mode] @package.prog_mode = options[:prog_mode] @package.data_mode = options[:data_mode] - - if options[:user_install] - @gem_home = Gem.user_dir - @bin_dir = Gem.bindir gem_home unless options[:bin_dir] - @plugins_dir = Gem.plugindir(gem_home) - check_that_user_bin_dir_is_in_path - end end ## @@ -221,31 +188,31 @@ class Gem::Installer File.open generated_bin, "rb" do |io| line = io.gets - shebang = /^#!.*ruby/ + shebang = /^#!.*ruby/o - if load_relative_enabled? - until line.nil? || line =~ shebang do + # TruffleRuby uses a bash prelude in default launchers + if load_relative_enabled? || RUBY_ENGINE == "truffleruby" + until line.nil? || shebang.match?(line) do line = io.gets end end - next unless line =~ shebang + next unless line&.match?(shebang) io.gets # blankline - # TODO detect a specially formatted comment instead of trying + # TODO: detect a specially formatted comment instead of trying # to find a string inside Ruby code. - next unless io.gets.to_s.include?("This file was generated by RubyGems") + next unless io.gets&.include?("This file was generated by RubyGems") ruby_executable = true - existing = io.read.slice(%r{ + existing = io.read.slice(/ ^\s*( - gem \s | - load \s Gem\.bin_path\( | + Gem\.activate_and_load_bin_path\( | load \s Gem\.activate_bin_path\( ) (['"])(.*?)(\2), - }x, 3) + /x, 3) end return if spec.name == existing @@ -304,75 +271,67 @@ class Gem::Installer run_pre_install_hooks # Set loaded_from to ensure extension_dir is correct - if @options[:install_as_default] - spec.loaded_from = default_spec_file - else - spec.loaded_from = spec_file - end + spec.loaded_from = spec_file # Completely remove any previous gem files FileUtils.rm_rf gem_dir FileUtils.rm_rf spec.extension_dir dir_mode = options[:dir_mode] - FileUtils.mkdir_p gem_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755 - if @options[:install_as_default] - extract_bin - write_default_spec - else - extract_files + extract_files - build_extensions - write_build_info_file - run_post_build_hooks - end + build_extensions + write_build_info_file + run_post_build_hooks generate_bin - generate_plugins - - unless @options[:install_as_default] - write_spec - write_cache_file + if options[:install_plugin] == false + remove_stale_plugins + warn_skipped_plugins + else + generate_plugins end + write_spec + write_cache_file + File.chmod(dir_mode, gem_dir) if dir_mode - say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? + say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil? + + Gem::Specification.add_spec(spec) unless @install_dir - Gem::Specification.add_spec(spec) + load_plugin unless options[:install_plugin] == false run_post_install_hooks spec - - # TODO This rescue is in the wrong place. What is raising this exception? - # move this rescue to around the code that actually might raise it. - rescue Zlib::GzipFile::Error - raise Gem::InstallError, "gzip error installing #{gem}" + rescue Errno::EACCES => e + # Permission denied - /path/to/foo + raise Gem::FilePermissionError, e.message.split(" - ").last end def run_pre_install_hooks # :nodoc: Gem.pre_install_hooks.each do |hook| - if hook.call(self) == false - location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ + next unless hook.call(self) == false + location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ - message = "pre-install hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message - end + message = "pre-install hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message end end def run_post_build_hooks # :nodoc: Gem.post_build_hooks.each do |hook| - if hook.call(self) == false - FileUtils.rm_rf gem_dir + next unless hook.call(self) == false + FileUtils.rm_rf gem_dir - location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ + location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/ - message = "post-build hook#{location} failed for #{spec.full_name}" - raise Gem::InstallError, message - end + message = "post-build hook#{location} failed for #{spec.full_name}" + raise Gem::InstallError, message end end @@ -388,11 +347,11 @@ class Gem::Installer # we'll be installing into. def installed_specs - @specs ||= begin + @installed_specs ||= begin specs = [] Gem::Util.glob_files_in_dir("*.gemspec", File.join(gem_home, "specifications")).each do |path| - spec = Gem::Specification.load path.tap(&Gem::UNTAINT) + spec = Gem::Specification.load path specs << spec if spec end @@ -425,15 +384,6 @@ class Gem::Installer end ## - # Unpacks the gem into the given directory. - - def unpack(directory) - @gem_dir = directory - extract_files - end - rubygems_deprecate :unpack - - ## # The location of the spec file that is installed. # @@ -441,12 +391,18 @@ class Gem::Installer File.join gem_home, "specifications", "#{spec.full_name}.gemspec" end + def default_spec_dir + dir = File.join(gem_home, "specifications", "default") + FileUtils.mkdir_p dir + dir + end + ## # The location of the default spec file for default gems. # def default_spec_file - File.join gem_home, "specifications", "default", "#{spec.full_name}.gemspec" + File.join default_spec_dir, "#{spec.full_name}.gemspec" end ## @@ -462,6 +418,9 @@ class Gem::Installer ## # Writes the full .gemspec specification (in Ruby) to the gem home's # specifications/default directory. + # + # In contrast to #write_spec, this keeps file lists, so the `gem contents` + # command works. def write_default_spec Gem.write_binary(default_spec_file, spec.to_ruby) @@ -483,21 +442,29 @@ class Gem::Installer end def generate_bin # :nodoc: - return if spec.executables.nil? || spec.executables.empty? + executables = spec.executables + return if executables.nil? || executables.empty? + + if @gem_home == Gem.user_dir + # If we get here, then one of the following likely happened: + # - `--user-install` was specified + # - `Gem::PathSupport#home` fell back to `Gem.user_dir` + # - GEM_HOME was manually set to `Gem.user_dir` + + check_that_user_bin_dir_is_in_path(executables) + end ensure_writable_dir @bin_dir - spec.executables.each do |filename| - filename.tap(&Gem::UNTAINT) + executables.each do |filename| bin_path = File.join gem_dir, spec.bindir, filename next unless File.exist? bin_path mode = File.stat(bin_path).mode - dir_mode = options[:prog_mode] || (mode | 0111) + dir_mode = options[:prog_mode] || (mode | 0o111) unless dir_mode == mode - require "fileutils" - FileUtils.chmod dir_mode, bin_path + File.chmod dir_mode, bin_path end check_executable_overwrite filename @@ -521,6 +488,8 @@ class Gem::Installer else regenerate_plugins_for(spec, @plugins_dir) end + rescue ArgumentError => e + raise e, "#{latest.name} #{latest.version} #{spec.name} #{spec.version}: #{e.message}" end ## @@ -528,17 +497,19 @@ class Gem::Installer #-- # The Windows script is generated in addition to the regular one due to a # bug or misfeature in the Windows shell's pipe. See - # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379 + # https://blade.ruby-lang.org/ruby-talk/193379 def generate_bin_script(filename, bindir) bin_script_path = File.join bindir, formatted_program_filename(filename) - require "fileutils" - FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers + Gem.open_file_with_lock(bin_script_path) do + require "fileutils" + FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers - File.open bin_script_path, "wb", 0755 do |file| - file.print app_script_text(filename) - file.chmod(options[:prog_mode] || 0755) + File.open(bin_script_path, "wb", 0o755) do |file| + file.write app_script_text(filename) + file.chmod(options[:prog_mode] || 0o755) + end end verbose bin_script_path @@ -563,7 +534,7 @@ class Gem::Installer File.unlink dst end - FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose + FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose rescue NotImplementedError, SystemCallError alert_warning "Unable to use symlinks, installing wrapper" generate_bin_script filename, bindir @@ -586,7 +557,7 @@ class Gem::Installer def shebang(bin_file_name) path = File.join gem_dir, spec.bindir, bin_file_name - first_line = File.open(path, "rb") {|file| file.gets } || "" + first_line = File.open(path, "rb", &:gets) || "" if first_line.start_with?("#!") # Preserve extra words on shebang line, like "-w". Thanks RPA. @@ -628,7 +599,6 @@ class Gem::Installer def ensure_loadable_spec ruby = spec.to_ruby_for_cache - ruby.tap(&Gem::UNTAINT) begin eval ruby @@ -649,31 +619,37 @@ class Gem::Installer def process_options # :nodoc: @options = { - :bin_dir => nil, - :env_shebang => false, - :force => false, - :only_install_dir => false, - :post_install_message => true, + bin_dir: nil, + env_shebang: false, + force: false, + only_install_dir: false, + post_install_message: true, }.merge options @env_shebang = options[:env_shebang] @force = options[:force] @install_dir = options[:install_dir] - @gem_home = options[:install_dir] || Gem.dir - @plugins_dir = Gem.plugindir(@gem_home) + @user_install = options[:user_install] @ignore_dependencies = options[:ignore_dependencies] @format_executable = options[:format_executable] @wrappers = options[:wrappers] @only_install_dir = options[:only_install_dir] - # If the user has asked for the gem to be installed in a directory that is - # the system gem directory, then use the system bin directory, else create - # (or use) a new bin dir under the gem_home. - @bin_dir = options[:bin_dir] || Gem.bindir(gem_home) + @bin_dir = options[:bin_dir] @development = options[:development] @build_root = options[:build_root] @build_args = options[:build_args] + @build_jobs = options[:build_jobs] + + @gem_home = @install_dir || user_install_dir || Gem.dir + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + @bin_dir ||= Gem.bindir(@gem_home) + + @plugins_dir = Gem.plugindir(@gem_home) unless @build_root.nil? @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, "")) @@ -683,9 +659,7 @@ class Gem::Installer end end - def check_that_user_bin_dir_is_in_path # :nodoc: - return if self.class.path_warning - + def check_that_user_bin_dir_is_in_path(executables) # :nodoc: user_bin_dir = @bin_dir || Gem.bindir(gem_home) user_bin_dir = user_bin_dir.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR @@ -701,19 +675,17 @@ class Gem::Installer unless path.include? user_bin_dir unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV["HOME"], "~")) - alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run." - self.class.path_warning = true + alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables (#{executables.join(", ")}) will not run." end end end def verify_gem_home # :nodoc: - FileUtils.mkdir_p gem_home, :mode => options[:dir_mode] && 0755 - raise Gem::FilePermissionError, gem_home unless File.writable?(gem_home) + FileUtils.mkdir_p gem_home, mode: options[:dir_mode] && 0o755 end def verify_spec - unless spec.name =~ Gem::Specification::VALID_NAME_PATTERN + unless Gem::Specification::VALID_NAME_PATTERN.match?(spec.name) raise Gem::InstallError, "#{spec} has an invalid name" end @@ -725,11 +697,11 @@ class Gem::Installer raise Gem::InstallError, "#{spec} has an invalid extensions" end - if spec.platform.to_s =~ /\R/ + if /\R/.match?(spec.platform.to_s) raise Gem::InstallError, "#{spec.platform} is an invalid platform" end - unless spec.specification_version.to_s =~ /\A\d+\z/ + unless /\A\d+\z/.match?(spec.specification_version.to_s) raise Gem::InstallError, "#{spec} has an invalid specification_version" end @@ -740,62 +712,74 @@ class Gem::Installer if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ } raise Gem::InstallError, "#{spec} has an invalid dependencies" end + + if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) } + raise Gem::InstallError, "#{spec} has an invalid executable" + end + + raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String) + + expanded_gem_dir = File.expand_path(gem_dir) + expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir)) + unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/") + raise Gem::InstallError, "#{spec} has an invalid bindir" + end end ## # Return the text for an application file. def app_script_text(bin_file_name) - # note that the `load` lines cannot be indented, as old RG versions match + # NOTE: that the `load` lines cannot be indented, as old RG versions match # against the beginning of the line - return <<-TEXT -#{shebang bin_file_name} -# -# This file was generated by RubyGems. -# -# The application '#{spec.name}' is installed as part of a gem, and -# this file is here to facilitate running it. -# - -require 'rubygems' -#{gemdeps_load(spec.name)} -version = "#{Gem::Requirement.default_prerelease}" - -str = ARGV.first -if str - str = str.b[/\\A_(.*)_\\z/, 1] - if str and Gem::Version.correct?(str) - #{explicit_version_requirement(spec.name)} - ARGV.shift - end -end + escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" } + <<~TEXT + #{shebang bin_file_name} + # + # This file was generated by RubyGems. + # + # The application '#{spec.name}' is installed as part of a gem, and + # this file is here to facilitate running it. + # + + require 'rubygems' + #{gemdeps_load(spec.name)} + version = "#{Gem::Requirement.default_prerelease}" + + str = ARGV.first + if str + str = str.b[/\\A_(.*)_\\z/, 1] + if str and Gem::Version.correct?(str) + #{explicit_version_requirement(spec.name)} + ARGV.shift + end + end -if Gem.respond_to?(:activate_bin_path) -load Gem.activate_bin_path('#{spec.name}', '#{bin_file_name}', version) -else -gem #{spec.name.dump}, version -load Gem.bin_path(#{spec.name.dump}, #{bin_file_name.dump}, version) -end -TEXT + if Gem.respond_to?(:activate_and_load_bin_path) + Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version) + else + load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version) + end + TEXT end def gemdeps_load(name) return "" if name == "bundler" - <<-TEXT + <<~TEXT -Gem.use_gemdeps -TEXT + Gem.use_gemdeps + TEXT end def explicit_version_requirement(name) code = "version = str" return code unless name == "bundler" - code += <<-TEXT + code += <<~TEXT - ENV['BUNDLER_VERSION'] = str -TEXT + ENV['BUNDLER_VERSION'] = str + TEXT end ## @@ -805,14 +789,14 @@ TEXT rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"]) # get ruby executable file name from RbConfig - ruby_exe = "#{rb_config['RUBY_INSTALL_NAME']}#{rb_config['EXEEXT']}" + ruby_exe = "#{rb_config["RUBY_INSTALL_NAME"]}#{rb_config["EXEEXT"]}" ruby_exe = "ruby.exe" if ruby_exe.empty? - if File.exist?(File.join bindir, ruby_exe) + if File.exist?(File.join(bindir, ruby_exe)) # stub & ruby.exe within same folder. Portable - <<-TEXT -@ECHO OFF -@"%~dp0#{ruby_exe}" "%~dpn0" %* + <<~TEXT + @ECHO OFF + @"%~dp0#{ruby_exe}" "%~dpn0" %* TEXT elsif bindir.downcase.start_with? rb_topdir.downcase # stub within ruby folder, but not standard bin. Portable @@ -820,16 +804,16 @@ TEXT from = Pathname.new bindir to = Pathname.new "#{rb_topdir}/bin" rel = to.relative_path_from from - <<-TEXT -@ECHO OFF -@"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %* + <<~TEXT + @ECHO OFF + @"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %* TEXT else # outside ruby folder, maybe -user-install or bundler. Portable, but ruby # is dependent on PATH - <<-TEXT -@ECHO OFF -@#{ruby_exe} "%~dpn0" %* + <<~TEXT + @ECHO OFF + @#{ruby_exe} "%~dpn0" %* TEXT end end @@ -838,11 +822,37 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, build_args + if options[:build_extension] == false + warn_skipped_extensions + return + end + + builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs builder.build_extensions end + def warn_skipped_extensions # :nodoc: + return if spec.extensions.empty? + + alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \ + "To build extensions, run: gem pristine #{spec.name} --extensions" + end + + def warn_skipped_plugins # :nodoc: + return if spec.plugins.empty? + + alert_warning "#{spec.full_name} contains plugins that were not installed.\n" \ + "To install plugins, run: gem pristine #{spec.name} --only-plugins" + end + + def remove_stale_plugins # :nodoc: + return unless spec.plugins.empty? + + ensure_writable_dir @plugins_dir + remove_plugins_for(spec, @plugins_dir) + end + ## # Reads the file index and extracts each file into the gem directory. # @@ -907,11 +917,7 @@ TEXT ensure_loadable_spec - if options[:install_as_default] - Gem.ensure_default_gem_subdirectories gem_home - else - Gem.ensure_gem_subdirectories gem_home - end + Gem.ensure_gem_subdirectories gem_home return true if @force @@ -930,7 +936,7 @@ TEXT build_info_dir = File.join gem_home, "build_info" dir_mode = options[:dir_mode] - FileUtils.mkdir_p build_info_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p build_info_dir, mode: dir_mode && 0o755 build_info_file = File.join build_info_dir, "#{spec.full_name}.info" @@ -952,17 +958,27 @@ TEXT end def ensure_writable_dir(dir) # :nodoc: - begin - Dir.mkdir dir, *[options[:dir_mode] && 0755].compact - rescue SystemCallError - raise unless File.directory? dir - end + require "fileutils" + FileUtils.mkdir_p dir, mode: options[:dir_mode] && 0o755 raise Gem::FilePermissionError.new(dir) unless File.writable? dir end private + def user_install_dir + # never install to user home in --build-root mode + return unless @build_root.nil? + + # Please note that @user_install might have three states: + # * `true`: `--user-install` + # * `false`: `--no-user-install` and + # * `nil`: option was not specified + if @user_install || (@user_install.nil? && Gem.default_user_install) + Gem.user_dir + end + end + def build_args @build_args ||= begin require_relative "command" @@ -970,8 +986,17 @@ TEXT end end + def build_jobs + @build_jobs ||= begin + require "etc" + Etc.nprocessors + 1 + rescue LoadError + 1 + end + end + def rb_config - RbConfig::CONFIG + Gem.target_rbconfig end def ruby_install_name @@ -984,22 +1009,35 @@ TEXT def bash_prolog_script if load_relative_enabled? - script = +<<~EOS - bindir="${0%/*}" - EOS - - script << %Q(exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n) - <<~EOS #!/bin/sh # -*- ruby -*- _=_\\ =begin - #{script.chomp} + bindir="${0%/*}" + ruby="$bindir/#{ruby_install_name}" + if [ ! -f "$ruby" ]; then + ruby="#{ruby_install_name}" + fi + exec "$ruby" "-x" "$0" "$@" =end EOS else "" end end + + def load_plugin + specs = Gem::Specification.find_all_by_name(spec.name) + # If old version already exists, this plugin isn't loaded + # immediately. It's for avoiding a case that multiple versions + # are loaded at the same time. + return unless specs.size == 1 + + plugin_files = spec.plugins.filter_map do |plugin| + path = File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + path if File.exist?(path) + end + Gem.load_plugin_files(plugin_files) unless plugin_files.empty? + end end diff --git a/lib/rubygems/installer_uninstaller_utils.rb b/lib/rubygems/installer_uninstaller_utils.rb index d97b4e29b1..c5c2a52bab 100644 --- a/lib/rubygems/installer_uninstaller_utils.rb +++ b/lib/rubygems/installer_uninstaller_utils.rb @@ -4,7 +4,6 @@ # Helper methods for both Gem::Installer and Gem::Uninstaller module Gem::InstallerUninstallerUtils - def regenerate_plugins_for(spec, plugins_dir) plugins = spec.plugins return if plugins.empty? @@ -25,5 +24,4 @@ module Gem::InstallerUninstallerUtils def remove_plugins_for(spec, plugins_dir) FileUtils.rm_f Gem::Util.glob_files_in_dir("#{spec.name}#{Gem.plugin_suffix_pattern}", plugins_dir) end - end diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb index b2c2dea905..3b88c43149 100644 --- a/lib/rubygems/local_remote_options.rb +++ b/lib/rubygems/local_remote_options.rb @@ -1,26 +1,26 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require "uri" +require_relative "vendor/uri/lib/uri" require_relative "../rubygems" ## # Mixin methods for local and remote Gem::Command options. module Gem::LocalRemoteOptions - ## # Allows Gem::OptionParser to handle HTTP URIs. def accept_uri_http - Gem::OptionParser.accept URI::HTTP do |value| + Gem::OptionParser.accept Gem::URI::HTTP do |value| begin - uri = URI.parse value - rescue URI::InvalidURIError + uri = Gem::URI.parse value + rescue Gem::URI::InvalidURIError raise Gem::OptionParser::InvalidArgument, value end @@ -39,17 +39,17 @@ module Gem::LocalRemoteOptions def add_local_remote_options add_option(:"Local/Remote", "-l", "--local", - "Restrict operations to the LOCAL domain") do |value, options| + "Restrict operations to the LOCAL domain") do |_value, options| options[:domain] = :local end add_option(:"Local/Remote", "-r", "--remote", - "Restrict operations to the REMOTE domain") do |value, options| + "Restrict operations to the REMOTE domain") do |_value, options| options[:domain] = :remote end add_option(:"Local/Remote", "-b", "--both", - "Allow LOCAL and REMOTE operations") do |value, options| + "Allow LOCAL and REMOTE operations") do |_value, options| options[:domain] = :both end @@ -66,8 +66,7 @@ module Gem::LocalRemoteOptions def add_bulk_threshold_option add_option(:"Local/Remote", "-B", "--bulk-threshold COUNT", "Threshold for switching to bulk", - "synchronization (default #{Gem.configuration.bulk_threshold})") do - |value, options| + "synchronization (default #{Gem.configuration.bulk_threshold})") do |value, _options| Gem.configuration.bulk_threshold = value.to_i end end @@ -77,7 +76,7 @@ module Gem::LocalRemoteOptions def add_clear_sources_option add_option(:"Local/Remote", "--clear-sources", - "Clear the gem sources") do |value, options| + "Clear the gem sources") do |_value, options| Gem.sources = nil options[:sources_cleared] = true end @@ -89,9 +88,9 @@ module Gem::LocalRemoteOptions def add_proxy_option accept_uri_http - add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", URI::HTTP, + add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", Gem::URI::HTTP, "Use HTTP proxy for remote operations") do |value, options| - options[:http_proxy] = (value == false) ? :no_proxy : value + options[:http_proxy] = value == false ? :no_proxy : value Gem.configuration[:http_proxy] = options[:http_proxy] end end @@ -102,9 +101,9 @@ module Gem::LocalRemoteOptions def add_source_option accept_uri_http - add_option(:"Local/Remote", "-s", "--source URL", URI::HTTP, + add_option(:"Local/Remote", "-s", "--source URL", Gem::URI::HTTP, "Append URL to list of remote gem sources") do |source, options| - source << "/" if source !~ /\/\z/ + source << "/" unless source.end_with?("/") if options.delete :sources_cleared Gem.sources = [source] @@ -119,7 +118,7 @@ module Gem::LocalRemoteOptions def add_update_sources_option add_option(:Deprecated, "-u", "--[no-]update-sources", - "Update local source cache") do |value, options| + "Update local source cache") do |value, _options| Gem.configuration.update_sources = value end end @@ -135,14 +134,13 @@ module Gem::LocalRemoteOptions # Is local fetching enabled? def local? - options[:domain] == :local || options[:domain] == :both + [:local, :both].include?(options[:domain]) end ## # Is remote fetching enabled? def remote? - options[:domain] == :remote || options[:domain] == :both + [:remote, :both].include?(options[:domain]) end - end diff --git a/lib/rubygems/mock_gem_ui.rb b/lib/rubygems/mock_gem_ui.rb deleted file mode 100644 index 5cc67ad099..0000000000 --- a/lib/rubygems/mock_gem_ui.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true -require_relative "user_interaction" - -## -# This Gem::StreamUI subclass records input and output to StringIO for -# retrieval during tests. - -class Gem::MockGemUi < Gem::StreamUI - ## - # Raised when you haven't provided enough input to your MockGemUi - - class InputEOFError < RuntimeError - def initialize(question) - super "Out of input for MockGemUi on #{question.inspect}" - end - end - - class TermError < RuntimeError - attr_reader :exit_code - - def initialize(exit_code) - super - @exit_code = exit_code - end - end - class SystemExitException < RuntimeError; end - - module TTY - - attr_accessor :tty - - def tty?() - @tty = true unless defined?(@tty) - @tty - end - - def noecho - yield self - end - end - - def initialize(input = "") - require "stringio" - ins = StringIO.new input - outs = StringIO.new - errs = StringIO.new - - ins.extend TTY - outs.extend TTY - errs.extend TTY - - super ins, outs, errs, true - - @terminated = false - end - - def ask(question) - raise InputEOFError, question if @ins.eof? - - super - end - - def input - @ins.string - end - - def output - @outs.string - end - - def error - @errs.string - end - - def terminated? - @terminated - end - - def terminate_interaction(status=0) - @terminated = true - - raise TermError, status if status != 0 - raise SystemExitException - end -end diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb index 767dc1fb45..cbdf4d7ac5 100644 --- a/lib/rubygems/name_tuple.rb +++ b/lib/rubygems/name_tuple.rb @@ -1,18 +1,17 @@ # frozen_string_literal: true + ## # # Represents a gem of name +name+ at +version+ of +platform+. These # wrap the data returned from the indexes. class Gem::NameTuple - def initialize(name, version, platform="ruby") + def initialize(name, version, platform = Gem::Platform::RUBY) @name = name @version = version - unless platform.kind_of? Gem::Platform - platform = "ruby" if !platform || platform.empty? - end - + platform &&= platform.to_s + platform = Gem::Platform::RUBY if !platform || platform.empty? @platform = platform end @@ -31,7 +30,7 @@ class Gem::NameTuple # [name, version, platform] tuples. def self.to_basic(list) - list.map {|t| t.to_a } + list.map(&:to_a) end ## @@ -48,11 +47,11 @@ class Gem::NameTuple def full_name case @platform - when nil, "ruby", "" + when nil, "", Gem::Platform::RUBY "#{@name}-#{@version}" else "#{@name}-#{@version}-#{@platform}" - end.dup.tap(&Gem::UNTAINT) + end end ## @@ -82,11 +81,17 @@ class Gem::NameTuple [@name, @version, @platform] end + alias_method :deconstruct, :to_a + + def deconstruct_keys(keys) + { name: @name, version: @version, platform: @platform } + end + def inspect # :nodoc: "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>" end - alias to_s inspect # :nodoc: + alias_method :to_s, :inspect # :nodoc: def <=>(other) [@name, @version, Gem::Platform.sort_priority(@platform)] <=> diff --git a/lib/rubygems/optparse.rb b/lib/rubygems/optparse.rb deleted file mode 100644 index 6ed718423c..0000000000 --- a/lib/rubygems/optparse.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "optparse/lib/optparse" diff --git a/lib/rubygems/optparse/lib/optparse/uri.rb b/lib/rubygems/optparse/lib/optparse/uri.rb deleted file mode 100644 index 664d7f2af4..0000000000 --- a/lib/rubygems/optparse/lib/optparse/uri.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: false -# -*- ruby -*- - -require_relative '../optparse' -require 'uri' - -Gem::OptionParser.accept(URI) {|s,| URI.parse(s) if s} diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 050ffbfe77..7e41b18f66 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -1,10 +1,13 @@ # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ -require_relative "../rubygems" +# rubocop:enable Style/AsciiComments + +require_relative "win_platform" require_relative "security" require_relative "user_interaction" @@ -56,9 +59,9 @@ class Gem::Package def initialize(message, source = nil) if source - @path = source.path + @path = source.is_a?(String) ? source : source.path - message = message + " in #{path}" if path + message += " in #{path}" if path end super message @@ -67,15 +70,13 @@ class Gem::Package class PathError < Error def initialize(destination, destination_dir) - super "installing into parent path %s of %s is not allowed" % - [destination, destination_dir] + super format("installing into parent path %s of %s is not allowed", destination, destination_dir) end end class SymlinkError < Error def initialize(name, destination, destination_dir) - super "installing symlink '%s' pointing to parent path %s of %s is not allowed" % - [name, destination, destination_dir] + super format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir) end end @@ -154,7 +155,7 @@ class Gem::Package Gem::Package::FileSource.new gem end - return super unless Gem::Package == self + return super unless self == Gem::Package return super unless gem.present? return super unless gem.start @@ -186,7 +187,7 @@ class Gem::Package end end - return spec, metadata + [spec, metadata] end ## @@ -229,9 +230,13 @@ class Gem::Package end end - tar.add_file_signed "checksums.yaml.gz", 0444, @signer do |io| + tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io| gzip_to io do |gz_io| - Psych.dump checksums_by_algorithm, gz_io + if Gem.use_psych? + Psych.dump checksums_by_algorithm, gz_io + else + gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm) + end end end end @@ -241,7 +246,7 @@ class Gem::Package # and adds this file to the +tar+. def add_contents(tar) # :nodoc: - digests = tar.add_file_signed "data.tar.gz", 0444, @signer do |io| + digests = tar.add_file_signed "data.tar.gz", 0o444, @signer do |io| gzip_to io do |gz_io| Gem::Package::TarWriter.new gz_io do |data_tar| add_files data_tar @@ -267,7 +272,7 @@ class Gem::Package tar.add_file_simple file, stat.mode, stat.size do |dst_io| File.open file, "rb" do |src_io| - dst_io.write src_io.read 16384 until src_io.eof? + copy_stream(src_io, dst_io, stat.size) end end end @@ -277,7 +282,7 @@ class Gem::Package # Adds the package's Gem::Specification to the +tar+ file def add_metadata(tar) # :nodoc: - digests = tar.add_file_signed "metadata.gz", 0444, @signer do |io| + digests = tar.add_file_signed "metadata.gz", 0o444, @signer do |io| gzip_to io do |gz_io| gz_io.write @spec.to_yaml end @@ -294,7 +299,6 @@ class Gem::Package Gem.load_yaml - @spec.mark_version @spec.validate true, strict_validation unless skip_validation setup_signer( @@ -346,6 +350,8 @@ EOM return @contents end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -354,18 +360,21 @@ EOM def digest(entry) # :nodoc: algorithms = if @checksums - @checksums.keys - else - [Gem::Security::DIGEST_NAME].compact + @checksums.to_h {|algorithm, _| [algorithm, Gem::Security.create_digest(algorithm)] } + elsif Gem::Security::DIGEST_NAME + { Gem::Security::DIGEST_NAME => Gem::Security.create_digest(Gem::Security::DIGEST_NAME) } end - algorithms.each do |algorithm| - digester = Gem::Security.create_digest(algorithm) - - digester << entry.read(16384) until entry.eof? + return @digests if algorithms.nil? || algorithms.empty? - entry.rewind + buf = String.new(capacity: 16_384, encoding: Encoding::BINARY) + until entry.eof? + entry.readpartial(16_384, buf) + algorithms.each_value {|digester| digester << buf } + end + entry.rewind + algorithms.each do |algorithm, digester| @digests[algorithm][entry.full_name] = digester end @@ -381,7 +390,7 @@ EOM def extract_files(destination_dir, pattern = "*") verify unless @spec - FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755 @gem.with_read_io do |io| reader = Gem::Package::TarReader.new io @@ -391,9 +400,11 @@ EOM extract_tar_gz entry, destination_dir, pattern - return # ignore further entries + break # ignore further entries end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -408,6 +419,8 @@ EOM # extracted. def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: + destination_dir = File.realpath(destination_dir) + directories = [] symlinks = [] @@ -428,10 +441,6 @@ EOM symlinks << [full_name, link_target, destination, real_destination] end - FileUtils.rm_rf destination - - mkdir_options = {} - mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?) mkdir = if entry.directory? destination @@ -440,13 +449,24 @@ EOM end unless directories.include?(mkdir) - FileUtils.mkdir_p mkdir, **mkdir_options + FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?) directories << mkdir end + real_mkdir = File.realpath(mkdir) + unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/")) + raise Gem::Package::PathError.new(real_mkdir, destination_dir) + end + if entry.file? - File.open(destination, "wb") {|out| out.write entry.read } - FileUtils.chmod file_mode(entry.header.mode), destination + 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 end verbose destination @@ -455,7 +475,7 @@ EOM symlinks.each do |name, target, destination, real_destination| if File.exist?(real_destination) - File.symlink(target, destination) + create_symlink(target, destination) else alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring" end @@ -467,7 +487,7 @@ EOM end def file_mode(mode) # :nodoc: - ((mode & 0111).zero? ? data_mode : prog_mode) || + ((mode & 0o111).zero? ? data_mode : prog_mode) || # If we're not using one of the default modes, then we're going to fall # back to the mode from the tarball. In this case we need to mask it down # to fit into 2^16 bits (the maximum value for a mode in CRuby since it @@ -505,14 +525,15 @@ EOM raise Gem::Package::PathError.new(destination, destination_dir) unless normalize_path(destination).start_with? normalize_path(destination_dir + "/") - destination.tap(&Gem::UNTAINT) destination end - def normalize_path(pathname) - if Gem.win_platform? + if Gem.win_platform? + def normalize_path(pathname) # :nodoc: pathname.downcase - else + end + else + def normalize_path(pathname) # :nodoc: pathname end end @@ -520,13 +541,14 @@ EOM ## # Loads a Gem::Specification from the TarEntry +entry+ - def load_spec(entry) # :nodoc: + def load_spec_from_metadata(entry) # :nodoc: + limit = 10 * 1024 * 1024 case entry.full_name when "metadata" then - @spec = Gem::Specification.from_yaml entry.read + @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit) when "metadata.gz" then Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| - @spec = Gem::Specification.from_yaml gzio.read + @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit) end end end @@ -539,6 +561,15 @@ EOM tar = Gem::Package::TarReader.new gzio yield tar + ensure + # Consume remaining gzip data to prevent the + # "attempt to close unfinished zstream; reset forced" warning + # when the GzipReader is closed with unconsumed compressed data. + begin + IO.copy_stream(gzio, IO::NULL) + rescue Zlib::GzipFile::Error, IOError + nil + end end end @@ -550,7 +581,7 @@ EOM @checksums = gem.seek "checksums.yaml.gz" do |entry| Zlib::GzipReader.wrap entry do |gz_io| - Gem::SafeYAML.safe_load gz_io.read + Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024) end end end @@ -571,10 +602,10 @@ EOM ) @spec.signing_key = nil - @spec.cert_chain = @signer.cert_chain.map {|cert| cert.to_s } + @spec.cert_chain = @signer.cert_chain.map(&:to_s) else @signer = Gem::Security::Signer.new nil, nil, passphrase - @spec.cert_chain = @signer.cert_chain.map {|cert| cert.to_pem } if + @spec.cert_chain = @signer.cert_chain.map(&:to_pem) if @signer.cert_chain end end @@ -625,10 +656,12 @@ EOM raise rescue Errno::ENOENT => e raise Gem::Package::FormatError.new e.message - rescue Gem::Package::TarInvalidError => e + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e raise Gem::Package::FormatError.new e.message, @gem end + private + ## # Verifies the +checksums+ against the +digests+. This check is not # cryptographically secure. Missing checksums are ignored. @@ -657,19 +690,14 @@ EOM case file_name when /\.sig$/ then - @signatures[$`] = entry.read if @security_policy + @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy return else digest entry end - case file_name - when "metadata", "metadata.gz" then - load_spec entry - when "data.tar.gz" then - verify_gz entry - end - rescue + load_spec_from_metadata entry + rescue StandardError warn "Exception while verifying #{@gem.path}" raise end @@ -688,23 +716,45 @@ EOM unless @files.include? "data.tar.gz" raise Gem::Package::FormatError.new \ - "package content (data.tar.gz) is missing", @gem + "package content (data.tar.gz) is missing", @gem end - if (duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first)) && duplicates.any? - raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})" + if (duplicates = @files.group_by {|f| f }.select {|_k,v| v.size > 1 }.map(&:first)) && duplicates.any? + raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(", ")})" end end - ## - # Verifies that +entry+ is a valid gzipped file. + if RUBY_ENGINE == "truffleruby" + def copy_stream(src, dst, size) # :nodoc: + dst.write src.read(size) + end + else + def copy_stream(src, dst, size) # :nodoc: + IO.copy_stream(src, dst, size) + end + end + + def limit_read(io, name, limit) + bytes = io.read(limit + 1) + raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit + bytes + end - def verify_gz(entry) # :nodoc: - Zlib::GzipReader.wrap entry do |gzio| - gzio.read 16384 until gzio.eof? # gzip checksum verification + if Gem.win_platform? + # Create a symlink and fallback to copy the file or directory on Windows, + # where symlink creation needs special privileges in form of the Developer Mode. + # JRuby on Windows raises TypeError from the wincode path-conversion helper + # when it cannot create the symlink, so fall back to copy in that case too. + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) + rescue Errno::EACCES, TypeError + from = File.expand_path(old_name, File.dirname(new_name)) + FileUtils.cp_r(from, new_name) + end + else + def create_symlink(old_name, new_name) + File.symlink(old_name, new_name) end - rescue Zlib::GzipFile::Error => e - raise Gem::Package::FormatError.new(e.message, entry.full_name) end end diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb index 4736f76d93..f04ab97462 100644 --- a/lib/rubygems/package/digest_io.rb +++ b/lib/rubygems/package/digest_io.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # IO wrapper that creates digests of contents written to the IO it wraps. @@ -35,7 +36,7 @@ class Gem::Package::DigestIO yield digest_io - return digests + digests end ## diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb index 14c7a9f6d2..d9717e0f2a 100644 --- a/lib/rubygems/package/file_source.rb +++ b/lib/rubygems/package/file_source.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The primary source of gems is a file on disk, including all usages # internal to rubygems. diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb index 03d7714524..227835dfce 100644 --- a/lib/rubygems/package/io_source.rb +++ b/lib/rubygems/package/io_source.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Supports reading and writing gems from/to a generic IO object. This is # useful for other applications built on top of rubygems, such as diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb index 09a02d3ecd..1a13ac3e29 100644 --- a/lib/rubygems/package/old.rb +++ b/lib/rubygems/package/old.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -69,7 +70,7 @@ class Gem::Package::Old < Gem::Package file_data << line end - file_data = file_data.strip.unpack("m")[0] + file_data = file_data.strip.unpack1("m") file_data = Zlib::Inflate.inflate file_data raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if @@ -77,7 +78,7 @@ class Gem::Package::Old < Gem::Package FileUtils.rm_rf destination - FileUtils.mkdir_p File.dirname(destination), :mode => dir_mode && 0755 + FileUtils.mkdir_p File.dirname(destination), mode: dir_mode && 0o755 File.open destination, "wb", file_mode(entry["mode"]) do |out| out.write file_data diff --git a/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb index 69701e55e9..8c44f8c305 100644 --- a/lib/rubygems/package/source.rb +++ b/lib/rubygems/package/source.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true + class Gem::Package::Source # :nodoc: end diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 590a2f0315..dd20d65080 100644 --- a/lib/rubygems/package/tar_header.rb +++ b/lib/rubygems/package/tar_header.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ + +# rubocop:enable Style/AsciiComments ## #-- @@ -53,7 +56,7 @@ class Gem::Package::TarHeader ## # Pack format for a tar header - PACK_FORMAT = "a100" + # name + PACK_FORMAT = ("a100" + # name "a8" + # mode "a8" + # uid "a8" + # gid @@ -68,12 +71,12 @@ class Gem::Package::TarHeader "a32" + # gname "a8" + # devmajor "a8" + # devminor - "a155" # prefix + "a155").freeze # prefix ## # Unpack format for a tar header - UNPACK_FORMAT = "A100" + # name + UNPACK_FORMAT = ("A100" + # name "A8" + # mode "A8" + # uid "A8" + # gid @@ -88,43 +91,44 @@ class Gem::Package::TarHeader "A32" + # gname "A8" + # devmajor "A8" + # devminor - "A155" # prefix + "A155").freeze # prefix attr_reader(*FIELDS) - EMPTY_HEADER = ("\0" * 512).freeze # :nodoc: + EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc: ## # Creates a tar header from IO +stream+ def self.from(stream) header = stream.read 512 - empty = (EMPTY_HEADER == header) + return EMPTY if header == EMPTY_HEADER fields = header.unpack UNPACK_FORMAT - new :name => fields.shift, - :mode => strict_oct(fields.shift), - :uid => oct_or_256based(fields.shift), - :gid => oct_or_256based(fields.shift), - :size => strict_oct(fields.shift), - :mtime => strict_oct(fields.shift), - :checksum => strict_oct(fields.shift), - :typeflag => fields.shift, - :linkname => fields.shift, - :magic => fields.shift, - :version => strict_oct(fields.shift), - :uname => fields.shift, - :gname => fields.shift, - :devmajor => strict_oct(fields.shift), - :devminor => strict_oct(fields.shift), - :prefix => fields.shift, - - :empty => empty + new name: fields.shift, + mode: strict_oct(fields.shift), + uid: oct_or_256based(fields.shift), + gid: oct_or_256based(fields.shift), + size: strict_oct(fields.shift), + mtime: strict_oct(fields.shift), + checksum: strict_oct(fields.shift), + typeflag: fields.shift, + linkname: fields.shift, + magic: fields.shift, + version: strict_oct(fields.shift), + uname: fields.shift, + gname: fields.shift, + devmajor: strict_oct(fields.shift), + devminor: strict_oct(fields.shift), + prefix: fields.shift, + + empty: false end def self.strict_oct(str) - return str.strip.oct if str.strip =~ /\A[0-7]*\z/ + str.strip! + return str.oct if /\A[0-7]*\z/.match?(str) raise ArgumentError, "#{str.inspect} is not an octal string" end @@ -134,7 +138,8 @@ class Gem::Package::TarHeader # \ff flags a negative 256-based number # In case we have a match, parse it as a signed binary value # in big-endian order, except that the high-order bit is ignored. - return str.unpack("N2").last if str =~ /\A[\x80\xff]/n + + return str.unpack1("@4N") if /\A[\x80\xff]/n.match?(str) strict_oct(str) end @@ -146,25 +151,43 @@ class Gem::Package::TarHeader raise ArgumentError, ":name, :size, :prefix and :mode required" end - vals[:uid] ||= 0 - vals[:gid] ||= 0 - vals[:mtime] ||= 0 - vals[:checksum] ||= "" - vals[:typeflag] = "0" if vals[:typeflag].nil? || vals[:typeflag].empty? - vals[:magic] ||= "ustar" - vals[:version] ||= "00" - vals[:uname] ||= "wheel" - vals[:gname] ||= "wheel" - vals[:devmajor] ||= 0 - vals[:devminor] ||= 0 - - FIELDS.each do |name| - instance_variable_set "@#{name}", vals[name] - end + @checksum = vals[:checksum] || "" + @devmajor = vals[:devmajor] || 0 + @devminor = vals[:devminor] || 0 + @gid = vals[:gid] || 0 + @gname = vals[:gname] || "wheel" + @linkname = vals[:linkname] + @magic = vals[:magic] || "ustar" + @mode = vals[:mode] + @mtime = vals[:mtime] || 0 + @name = vals[:name] + @prefix = vals[:prefix] + @size = vals[:size] + @typeflag = vals[:typeflag] + @typeflag = "0" if @typeflag.nil? || @typeflag.empty? + @uid = vals[:uid] || 0 + @uname = vals[:uname] || "wheel" + @version = vals[:version] || "00" @empty = vals[:empty] end + EMPTY = new({ # :nodoc: + checksum: 0, + gname: "", + linkname: "", + magic: "", + mode: 0, + name: "", + prefix: "", + size: 0, + uname: "", + version: 0, + + empty: true, + }).freeze + private_constant :EMPTY + ## # Is the tar entry empty? @@ -205,10 +228,21 @@ class Gem::Package::TarHeader @checksum = oct calculate_checksum(header), 6 end + ## + # Header's full name, including prefix + + def full_name + if prefix != "" + File.join prefix, name + else + name + end + end + private def calculate_checksum(header) - header.unpack("C*").inject {|a, b| a + b } + header.sum(0) end def header(checksum = @checksum) @@ -234,10 +268,10 @@ class Gem::Package::TarHeader header = header.pack PACK_FORMAT - header << ("\0" * ((512 - header.size) % 512)) + header.ljust 512, "\0" end def oct(num, len) - "%0#{len}o" % num + format("%0#{len}o", num) end end diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb index cdc3fdc015..b66a8a62bc 100644 --- a/lib/rubygems/package/tar_reader.rb +++ b/lib/rubygems/package/tar_reader.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ + +# rubocop:enable Style/AsciiComments ## # TarReader reads tar files and allows iteration over their items @@ -11,11 +14,6 @@ class Gem::Package::TarReader include Enumerable ## - # Raised if the tar IO is not seekable - - class UnexpectedEOF < StandardError; end - - ## # Creates a new TarReader on +io+ and yields it to the block, if given. def self.new(io) @@ -32,6 +30,8 @@ class Gem::Package::TarReader nil end + attr_reader :io # :nodoc: + ## # Creates a new tar file reader on +io+ which needs to respond to #pos, # #eof?, #read, #getc and #pos= @@ -53,44 +53,23 @@ class Gem::Package::TarReader def each return enum_for __method__ unless block_given? - use_seek = @io.respond_to?(:seek) - until @io.eof? do - header = Gem::Package::TarHeader.from @io - return if header.empty? + begin + header = Gem::Package::TarHeader.from @io + rescue ArgumentError => e + # Specialize only exceptions from Gem::Package::TarHeader.strict_oct + raise e unless e.message.match?(/ is not an octal string$/) + raise Gem::Package::TarInvalidError, e.message + end + return if header.empty? entry = Gem::Package::TarReader::Entry.new header, @io - size = entry.header.size - yield entry - - skip = (512 - (size % 512)) % 512 - pending = size - entry.bytes_read - - if use_seek - begin - # avoid reading if the @io supports seeking - @io.seek pending, IO::SEEK_CUR - pending = 0 - rescue Errno::EINVAL - end - end - - # if seeking isn't supported or failed - while pending > 0 do - bytes_read = @io.read([pending, 4096].min).size - raise UnexpectedEOF if @io.eof? - pending -= bytes_read - end - - @io.read skip # discard trailing zeros - - # make sure nobody can use #read, #getc or #rewind anymore entry.close end end - alias each_entry each + alias_method :each_entry, :each ## # NOTE: Do not call #rewind during #each @@ -115,7 +94,7 @@ class Gem::Package::TarReader return unless found - return yield found + yield found ensure rewind end diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb index 8634381c18..f837e86fd6 100644 --- a/lib/rubygems/package/tar_reader/entry.rb +++ b/lib/rubygems/package/tar_reader/entry.rb @@ -1,14 +1,31 @@ # frozen_string_literal: true -#++ + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#-- + +# rubocop:enable Style/AsciiComments ## # Class for reading entries out of a tar file class Gem::Package::TarReader::Entry ## + # Creates a new tar entry for +header+ that will be read from +io+ + # If a block is given, the entry is yielded and then closed. + + def self.open(header, io, &block) + entry = new header, io + return entry unless block_given? + begin + yield entry + ensure + entry.close + end + end + + ## # Header for this tar entry attr_reader :header @@ -21,6 +38,7 @@ class Gem::Package::TarReader::Entry @header = header @io = io @orig_pos = @io.pos + @end_pos = @orig_pos + @header.size @read = 0 end @@ -39,7 +57,14 @@ class Gem::Package::TarReader::Entry # Closes the tar entry def close + return if closed? + # Seek to the end of the entry if it wasn't fully read + seek(0, IO::SEEK_END) + # discard trailing zeros + skip = (512 - (@header.size % 512)) % 512 + @io.read(skip) @closed = true + nil end ## @@ -62,11 +87,7 @@ class Gem::Package::TarReader::Entry # Full name of the tar entry def full_name - if @header.prefix != "" - File.join @header.prefix, @header.name - else - @header.name - end + @header.full_name.force_encoding(Encoding::UTF_8) rescue ArgumentError => e raise unless e.message == "string contains null byte" raise Gem::Package::TarInvalidError, @@ -77,9 +98,7 @@ class Gem::Package::TarReader::Entry # Read one byte from the tar entry def getc - check_closed - - return nil if @read >= @header.size + return nil if eof? ret = @io.getc @read += 1 if ret @@ -117,36 +136,43 @@ class Gem::Package::TarReader::Entry bytes_read end + ## + # Seek to the position in the tar entry + + def pos=(new_pos) + seek(new_pos, IO::SEEK_SET) + end + def size @header.size end - alias length size + alias_method :length, :size ## - # Reads +len+ bytes from the tar file entry, or the rest of the entry if - # nil - - def read(len = nil) - check_closed + # Reads +maxlen+ bytes from the tar file entry, or the rest of the entry if nil - return nil if @read >= @header.size + def read(maxlen = nil) + if eof? + return maxlen.to_i.zero? ? "" : nil + end - len ||= @header.size - @read - max_read = [len, @header.size - @read].min + max_read = [maxlen, @header.size - @read].compact.min ret = @io.read max_read + if ret.nil? + return maxlen ? nil : "" # IO.read returns nil on EOF with len argument + end @read += ret.size ret end - def readpartial(maxlen = nil, outbuf = "".b) - check_closed - - raise EOFError if @read >= @header.size + def readpartial(maxlen, outbuf = "".b) + if eof? && maxlen > 0 + raise EOFError, "end of file reached" + end - maxlen ||= @header.size - @read max_read = [maxlen, @header.size - @read].min @io.readpartial(max_read, outbuf) @@ -156,12 +182,63 @@ class Gem::Package::TarReader::Entry end ## + # Seeks to +offset+ bytes into the tar file entry + # +whence+ can be IO::SEEK_SET, IO::SEEK_CUR, or IO::SEEK_END + + def seek(offset, whence = IO::SEEK_SET) + check_closed + + new_pos = + case whence + when IO::SEEK_SET then @orig_pos + offset + when IO::SEEK_CUR then @io.pos + offset + when IO::SEEK_END then @end_pos + offset + else + raise ArgumentError, "invalid whence" + end + + if new_pos < @orig_pos + new_pos = @orig_pos + elsif new_pos > @end_pos + new_pos = @end_pos + end + + pending = new_pos - @io.pos + + return 0 if pending == 0 + + if @io.respond_to?(:seek) + begin + # avoid reading if the @io supports seeking + @io.seek new_pos, IO::SEEK_SET + pending = 0 + rescue Errno::EINVAL + end + end + + # if seeking isn't supported or failed + # negative seek requires that we rewind and read + if pending < 0 + @io.rewind + pending = new_pos + end + + while pending > 0 do + size_read = @io.read([pending, 4096].min)&.size + raise(EOFError, "end of file reached") if size_read.nil? + pending -= size_read + end + + @read = @io.pos - @orig_pos + + 0 + end + + ## # Rewinds to the beginning of the tar file entry def rewind check_closed - - @io.pos = @orig_pos - @read = 0 + seek(0, IO::SEEK_SET) end end diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index db5242c5e4..39fed9e2af 100644 --- a/lib/rubygems/package/tar_writer.rb +++ b/lib/rubygems/package/tar_writer.rb @@ -1,8 +1,11 @@ # frozen_string_literal: true -#-- + +# rubocop:disable Style/AsciiComments + # Copyright (C) 2004 Mauricio Julio Fernández Pradier # See LICENSE.txt for additional licensing information. -#++ + +# rubocop:enable Style/AsciiComments ## # Allows writing of tar files @@ -92,10 +95,11 @@ class Gem::Package::TarWriter end ## - # Adds file +name+ with permissions +mode+, and yields an IO for writing the - # file to + # Adds file +name+ with permissions +mode+ and mtime +mtime+ (sets + # Gem.source_date_epoch if not specified), and yields an IO for + # writing the file to - def add_file(name, mode) # :yields: io + def add_file(name, mode, mtime = nil) # :yields: io check_closed name, prefix = split_name name @@ -113,9 +117,9 @@ class Gem::Package::TarWriter final_pos = @io.pos @io.pos = init_pos - header = Gem::Package::TarHeader.new :name => name, :mode => mode, - :size => size, :prefix => prefix, - :mtime => Gem.source_date_epoch + header = Gem::Package::TarHeader.new name: name, mode: mode, + size: size, prefix: prefix, + mtime: mtime || Gem.source_date_epoch @io.write header @io.pos = final_pos @@ -189,7 +193,7 @@ class Gem::Package::TarWriter if signer.key signature = signer.sign signature_digest.digest - add_file_simple "#{name}.sig", 0444, signature.length do |io| + add_file_simple "#{name}.sig", 0o444, signature.length do |io| io.write signature end end @@ -206,9 +210,9 @@ class Gem::Package::TarWriter name, prefix = split_name name - header = Gem::Package::TarHeader.new(:name => name, :mode => mode, - :size => size, :prefix => prefix, - :mtime => Gem.source_date_epoch).to_s + header = Gem::Package::TarHeader.new(name: name, mode: mode, + size: size, prefix: prefix, + mtime: Gem.source_date_epoch).to_s @io.write header os = BoundedStream.new @io, size @@ -232,11 +236,11 @@ class Gem::Package::TarWriter name, prefix = split_name name - header = Gem::Package::TarHeader.new(:name => name, :mode => mode, - :size => 0, :typeflag => "2", - :linkname => target, - :prefix => prefix, - :mtime => Gem.source_date_epoch).to_s + header = Gem::Package::TarHeader.new(name: name, mode: mode, + size: 0, typeflag: "2", + linkname: target, + prefix: prefix, + mtime: Gem.source_date_epoch).to_s @io.write header @@ -286,10 +290,10 @@ class Gem::Package::TarWriter name, prefix = split_name(name) - header = Gem::Package::TarHeader.new :name => name, :mode => mode, - :typeflag => "5", :size => 0, - :prefix => prefix, - :mtime => Gem.source_date_epoch + header = Gem::Package::TarHeader.new name: name, mode: mode, + typeflag: "5", size: 0, + prefix: prefix, + mtime: Gem.source_date_epoch @io.write header @@ -323,6 +327,6 @@ class Gem::Package::TarWriter end end - return name, prefix + [name, prefix] end end diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb index 8432bc5806..d26411684d 100644 --- a/lib/rubygems/package_task.rb +++ b/lib/rubygems/package_task.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Copyright (c) 2003, 2004 Jim Weirich, 2009 Eric Hodel # # Permission is hereby granted, free of charge, to any person obtaining @@ -96,13 +97,13 @@ class Gem::PackageTask < Rake::PackageTask gem_path = File.join package_dir, gem_file gem_dir = File.join package_dir, gem_spec.full_name - task :package => [:gem] + task package: [:gem] directory package_dir directory gem_dir desc "Build the gem file #{gem_file}" - task :gem => [gem_path] + task gem: [gem_path] trace = Rake.application.options.trace Gem.configuration.verbose = trace diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index d601e653c9..13091e29ba 100644 --- a/lib/rubygems/path_support.rb +++ b/lib/rubygems/path_support.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # # Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings @@ -23,23 +24,22 @@ class Gem::PathSupport # hashtable, or defaults to ENV, the system environment. # def initialize(env) - @home = env["GEM_HOME"] || Gem.default_dir - - if File::ALT_SEPARATOR - @home = @home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) - end - - @home = expand(@home) - + @home = normalize_home_dir(env["GEM_HOME"] || Gem.default_dir) @path = split_gem_path env["GEM_PATH"], @home @spec_cache_dir = env["GEM_SPEC_CACHE"] || Gem.default_spec_cache_dir - - @spec_cache_dir = @spec_cache_dir.dup.tap(&Gem::UNTAINT) end private + def normalize_home_dir(home) + if File::ALT_SEPARATOR + home = home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) + end + + expand(home) + end + ## # Split the Gem search path (as reported by Gem.path). @@ -52,7 +52,7 @@ class Gem::PathSupport gem_path = gpaths.split(Gem.path_separator) # Handle the path_separator being set to a regexp, which will cause # end_with? to error - if gpaths =~ /#{Gem.path_separator}\z/ + if /#{Gem.path_separator}\z/.match?(gpaths) gem_path += default_path end diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index f4983c1153..367b00e7e1 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -require_relative "deprecate" ## # Available list of platforms for targeting Gem installations. @@ -11,14 +10,13 @@ class Gem::Platform attr_accessor :cpu, :os, :version - def self.local - arch = RbConfig::CONFIG["arch"] - arch = "#{arch}_60" if arch =~ /mswin(?:32|64)$/ - @local ||= new(arch) - end - - def self.match(platform) - match_platforms?(platform, Gem.platforms) + def self.local(refresh: false) + return @local if @local && !refresh + @local = begin + arch = Gem.target_rbconfig["arch"] + arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch) + new(arch) + end end def self.match_platforms?(platform, platforms) @@ -35,10 +33,20 @@ class Gem::Platform match_gem?(spec.platform, spec.name) end - def self.match_gem?(platform, gem_name) - # Note: this method might be redefined by Ruby implementations to - # customize behavior per RUBY_ENGINE, gem_name or other criteria. - match_platforms?(platform, Gem.platforms) + if RUBY_ENGINE == "truffleruby" + def self.match_gem?(platform, gem_name) + raise "Not a string: #{gem_name.inspect}" unless String === gem_name + + if REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(gem_name) + match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform.local]) + else + match_platforms?(platform, Gem.platforms) + end + end + else + def self.match_gem?(platform, gem_name) + match_platforms?(platform, Gem.platforms) + end end def self.sort_priority(platform) @@ -69,52 +77,46 @@ class Gem::Platform when Array then @cpu, @os, @version = arch when String then - arch = arch.split "-" + cpu, os = arch.sub(/-+$/, "").split("-", 2) - if arch.length > 2 && arch.last !~ /\d+(\.\d+)?$/ # reassemble x86-linux-{libc} - extra = arch.pop - arch.last << "-#{extra}" + @cpu = if cpu&.match?(/i\d86/) + "x86" + else + cpu end - cpu = arch.shift - - @cpu = case cpu - when /i\d86/ then "x86" - else cpu - end - - if arch.length == 2 && arch.last =~ /^\d+(\.\d+)?$/ # for command-line - @os, @version = arch - return - end - - os, = arch - @cpu, os = nil, cpu if os.nil? # legacy jruby + if os.nil? + @cpu = nil + os = cpu + end # legacy jruby @os, @version = case os - when /aix(\d+)?/ then [ "aix", $1 ] - when /cygwin/ then [ "cygwin", nil ] - when /darwin(\d+)?/ then [ "darwin", $1 ] - when /^macruby$/ then [ "macruby", nil ] - when /freebsd(\d+)?/ then [ "freebsd", $1 ] - when /^java$/, /^jruby$/ then [ "java", nil ] - when /^java([\d.]*)/ then [ "java", $1 ] - when /^dalvik(\d+)?$/ then [ "dalvik", $1 ] - when /^dotnet$/ then [ "dotnet", nil ] - when /^dotnet([\d.]*)/ then [ "dotnet", $1 ] - when /linux-?(\w+)?/ then [ "linux", $1 ] - when /mingw32/ then [ "mingw32", nil ] - when /mingw-?(\w+)?/ then [ "mingw", $1 ] - when /(mswin\d+)(\_(\d+))?/ then - os, version = $1, $3 - @cpu = "x86" if @cpu.nil? && os =~ /32$/ - [os, version] - when /netbsdelf/ then [ "netbsdelf", nil ] - when /openbsd(\d+\.\d+)?/ then [ "openbsd", $1 ] - when /solaris(\d+\.\d+)?/ then [ "solaris", $1 ] - # test - when /^(\w+_platform)(\d+)?/ then [ $1, $2 ] - else [ "unknown", nil ] + when /aix-?(\d+)?/ then ["aix", $1] + when /cygwin/ then ["cygwin", nil] + when /darwin-?(\d+)?/ then ["darwin", $1] + when "macruby" then ["macruby", nil] + when /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1] + when /freebsd-?(\d+)?/ then ["freebsd", $1] + when "java", "jruby" then ["java", nil] + when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1] + when /^dalvik-?(\d+)?$/ then ["dalvik", $1] + when /^dotnet$/ then ["dotnet", nil] + when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1] + when /linux-?(\w+)?/ then ["linux", $1] + when /mingw32/ then ["mingw32", nil] + when /mingw-?(\w+)?/ then ["mingw", $1] + when /(mswin\d+)(?:[_-](\d+))?/ then + os = $1 + version = $2 + @cpu = "x86" if @cpu.nil? && os.end_with?("32") + [os, version] + when /netbsdelf/ then ["netbsdelf", nil] + when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1] + when /solaris-?(\d+\.\d+)?/ then ["solaris", $1] + when /wasi/ then ["wasi", nil] + # test + when /^(\w+_platform)-?(\d+)?/ then [$1, $2] + else ["unknown", nil] end when Gem::Platform then @cpu = arch.cpu @@ -130,7 +132,38 @@ class Gem::Platform end def to_s - to_a.compact.join "-" + to_a.compact.join(@cpu.nil? ? "" : "-") + end + + ## + # Deconstructs the platform into an array for pattern matching. + # Returns [cpu, os, version]. + # + # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil] + # + # This enables array pattern matching: + # + # case Gem::Platform.new("arm64-darwin-21") + # in ["arm64", "darwin", version] + # # version => "21" + # end + alias_method :deconstruct, :to_a + + ## + # Deconstructs the platform into a hash for pattern matching. + # Returns a hash with keys +:cpu+, +:os+, and +:version+. + # + # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil) + # #=> { cpu: "x86_64", os: "darwin", version: "20" } + # + # This enables hash pattern matching: + # + # case Gem::Platform.new("x86_64-linux") + # in cpu: "x86_64", os: "linux" + # # Matches Linux on x86_64 + # end + def deconstruct_keys(keys) + { cpu: @cpu, os: @os, version: @version } end ## @@ -141,7 +174,7 @@ class Gem::Platform self.class === other && to_a == other.to_a end - alias :eql? :== + alias_method :eql?, :== def hash # :nodoc: to_a.hash @@ -153,7 +186,7 @@ class Gem::Platform # they have the same version, or either one has no version # # Additionally, the platform will match if the local CPU is 'arm' and the - # other CPU starts with "arm" (for generic ARM family support). + # other CPU starts with "armv" (for generic 32-bit ARM family support). # # Of note, this method is not commutative. Indeed the OS 'linux' has a # special case: the version is the libc name, yet while "no version" stands @@ -174,7 +207,7 @@ class Gem::Platform # cpu ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu || - (@cpu == "arm" && other.cpu.start_with?("arm"))) && + (@cpu == "arm" && other.cpu.start_with?("armv"))) && # os @os == other.os && @@ -209,18 +242,18 @@ class Gem::Platform when String then # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007 other = case other - when /^i686-darwin(\d)/ then ["x86", "darwin", $1 ] - when /^i\d86-linux/ then ["x86", "linux", nil ] - when "java", "jruby" then [nil, "java", nil ] - when /^dalvik(\d+)?$/ then [nil, "dalvik", $1 ] - when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2 ] - when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2 ] - when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2 ] - when "powerpc-darwin" then ["powerpc", "darwin", nil ] - when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1 ] - when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8" ] - when /universal-darwin(\d)/ then ["universal", "darwin", $1 ] - else other + when /^i686-darwin(\d)/ then ["x86", "darwin", $1] + when /^i\d86-linux/ then ["x86", "linux", nil] + when "java", "jruby" then [nil, "java", nil] + when /^dalvik(\d+)?$/ then [nil, "dalvik", $1] + when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2] + when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2] + when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2] + when "powerpc-darwin" then ["powerpc", "darwin", nil] + when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1] + when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"] + when /universal-darwin(\d)/ then ["universal", "darwin", $1] + else other end other = Gem::Platform.new other @@ -242,4 +275,118 @@ class Gem::Platform # This will be replaced with Gem::Platform::local. CURRENT = "current" + + JAVA = Gem::Platform.new("java") # :nodoc: + MSWIN = Gem::Platform.new("mswin32") # :nodoc: + MSWIN64 = Gem::Platform.new("mswin64") # :nodoc: + MINGW = Gem::Platform.new("x86-mingw32") # :nodoc: + X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc: + X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc: + UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc: + WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc: + X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc: + X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc: + + GENERICS = [JAVA, *WINDOWS].freeze # :nodoc: + private_constant :GENERICS + + GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc: + private_constant :GENERIC_CACHE + + class << self + ## + # Returns the generic platform for the given platform. + + def generic(platform) + return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY + + GENERIC_CACHE[platform] ||= begin + found = GENERICS.find do |match| + platform === match + end + found || Gem::Platform::RUBY + end + end + + ## + # Returns the platform specificity match for the given spec platform and user platform. + + def platform_specificity_match(spec_platform, user_platform) + return -1 if spec_platform == user_platform + return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY + + os_match(spec_platform, user_platform) + + cpu_match(spec_platform, user_platform) * 10 + + version_match(spec_platform, user_platform) * 100 + end + + ## + # Sorts and filters the best platform match for the given matching specs and platform. + + def sort_and_filter_best_platform_match(matching, platform) + return matching if matching.one? + + exact = matching.select {|spec| spec.platform == platform } + return exact if exact.any? + + sorted_matching = sort_best_platform_match(matching, platform) + exemplary_spec = sorted_matching.first + + sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) } + end + + ## + # Sorts the best platform match for the given matching specs and platform. + + def sort_best_platform_match(matching, platform) + matching.sort_by.with_index do |spec, i| + [ + platform_specificity_match(spec.platform, platform), + i, # for stable sort + ] + end + end + + private + + def same_specificity?(platform, spec, exemplary_spec) + platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform) + end + + def same_deps?(spec, exemplary_spec) + spec.required_ruby_version == exemplary_spec.required_ruby_version && + spec.required_rubygems_version == exemplary_spec.required_rubygems_version && + spec.dependencies.sort == exemplary_spec.dependencies.sort + end + + def os_match(spec_platform, user_platform) + if spec_platform.os == user_platform.os + 0 + else + 1 + end + end + + def cpu_match(spec_platform, user_platform) + if spec_platform.cpu == user_platform.cpu + 0 + elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm") + 0 + elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal" + 1 + else + 2 + end + end + + def version_match(spec_platform, user_platform) + if spec_platform.version == user_platform.version + 0 + elsif spec_platform.version.nil? + 1 + else + 2 + end + end + end end diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb index b90f9f7d1d..8b4c425a33 100644 --- a/lib/rubygems/psych_tree.rb +++ b/lib/rubygems/psych_tree.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Gem if defined? ::Psych::Visitors class NoAliasYAMLTree < Psych::Visitors::YAMLTree @@ -13,11 +14,15 @@ module Gem @emitter.scalar str, nil, nil, false, true, quote end + def visit_Hash(o) + super(o.compact) + end + # Noop this out so there are no anchors def register(target, obj) end - # This is ported over from the yaml_tree in 1.9.3 + # This is ported over from the YAMLTree implementation in Ruby 1.9.3 def format_time(time) if time.utc? time.strftime("%Y-%m-%d %H:%M:%S.%9N Z") diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb index c72955f83b..9849370b1a 100644 --- a/lib/rubygems/query_utils.rb +++ b/lib/rubygems/query_utils.rb @@ -6,7 +6,6 @@ require_relative "version_option" require_relative "text" module Gem::QueryUtils - include Gem::Text include Gem::LocalRemoteOptions include Gem::VersionOption @@ -17,7 +16,7 @@ module Gem::QueryUtils options[:installed] = value end - add_option("-I", "Equivalent to --no-installed") do |value, options| + add_option("-I", "Equivalent to --no-installed") do |_value, options| options[:installed] = false end @@ -85,7 +84,7 @@ module Gem::QueryUtils installed = !installed unless options[:installed] say(installed) - exit_code = 1 if !installed + exit_code = 1 unless installed end exit_code @@ -119,7 +118,7 @@ module Gem::QueryUtils end end - #Guts of original execute + # Guts of original execute def show_gems(name) show_local_gems(name) if local? show_remote_gems(name) if remote? @@ -133,7 +132,7 @@ module Gem::QueryUtils version_matches = show_prereleases? || !s.version.prerelease? name_matches && version_matches - end + end.uniq(&:full_name) spec_tuples = specs.map do |spec| [spec.name_tuple, spec] @@ -197,7 +196,7 @@ module Gem::QueryUtils end def output_versions(output, versions) - versions.each do |gem_name, matching_tuples| + versions.each do |_gem_name, matching_tuples| matching_tuples = matching_tuples.sort_by {|n,_| n.version }.reverse platforms = Hash.new {|h,version| h[version] = [] } @@ -243,7 +242,7 @@ module Gem::QueryUtils list = if platforms.empty? || options[:details] - name_tuples.map {|n| n.version }.uniq + name_tuples.map(&:version).uniq else platforms.sort.reverse.map do |version, pls| out = version.to_s @@ -264,7 +263,7 @@ module Gem::QueryUtils end end - entry << " (#{list.join ', '})" + entry << " (#{list.join ", "})" end def make_entry(entry_tuples, platforms) @@ -283,7 +282,7 @@ module Gem::QueryUtils end def spec_authors(entry, spec) - authors = "Author#{spec.authors.length > 1 ? 's' : ''}: ".dup + authors = "Author#{spec.authors.length > 1 ? "s" : ""}: ".dup authors << spec.authors.join(", ") entry << format_text(authors, 68, 4) end @@ -297,7 +296,7 @@ module Gem::QueryUtils def spec_license(entry, spec) return if spec.license.nil? || spec.license.empty? - licenses = "License#{spec.licenses.length > 1 ? 's' : ''}: ".dup + licenses = "License#{spec.licenses.length > 1 ? "s" : ""}: ".dup licenses << spec.licenses.join(", ") entry << "\n" << format_text(licenses, 68, 4) end @@ -312,8 +311,8 @@ module Gem::QueryUtils label = "Installed at" specs.each do |s| version = s.version.to_s - version << ", default" if s.default_gem? - entry << "\n" << " #{label} (#{version}): #{s.base_dir}" + default = s.default_gem? ? ", default" : "" + entry << "\n" << " #{label} (#{version}#{default}): #{s.base_dir}" label = " " * label.length end end @@ -328,11 +327,11 @@ module Gem::QueryUtils if platforms.length == 1 title = platforms.values.length == 1 ? "Platform" : "Platforms" - entry << " #{title}: #{platforms.values.sort.join(', ')}\n" + entry << " #{title}: #{platforms.values.sort.join(", ")}\n" else entry << " Platforms:\n" - sorted_platforms = platforms.sort_by {|version,| version } + sorted_platforms = platforms.sort sorted_platforms.each do |version, pls| label = " #{version}: " @@ -347,5 +346,4 @@ module Gem::QueryUtils summary = truncate_text(spec.summary, "the summary for #{spec.full_name}") entry << "\n\n" << format_text(summary, 68, 4) end - end diff --git a/lib/rubygems/rdoc.rb b/lib/rubygems/rdoc.rb index 769ec61d1e..3524b161b2 100644 --- a/lib/rubygems/rdoc.rb +++ b/lib/rubygems/rdoc.rb @@ -1,12 +1,26 @@ # frozen_string_literal: true + require_relative "../rubygems" begin require "rdoc/rubygems_hook" module Gem - RDoc = ::RDoc::RubygemsHook - end + ## + # Returns whether RDoc defines its own install hooks through a RubyGems + # plugin. This and whatever is guarded by it can be removed once no + # supported Ruby ships with RDoc older than 6.9.0. + + def self.rdoc_hooks_defined_via_plugin? + Gem::Version.new(::RDoc::VERSION) >= Gem::Version.new("6.9.0") + end - Gem.done_installing(&Gem::RDoc.method(:generation_hook)) + if rdoc_hooks_defined_via_plugin? + RDoc = ::RDoc::RubyGemsHook + else + RDoc = ::RDoc::RubygemsHook + + Gem.done_installing(&Gem::RDoc.method(:generation_hook)) + end + end rescue LoadError end diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 0ac6eaa130..5b83dc6f6f 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "request" require_relative "request/connection_pools" @@ -52,7 +53,7 @@ class Gem::RemoteFetcher # Cached RemoteFetcher instance. def self.fetcher - @fetcher ||= self.new Gem.configuration[:http_proxy] + @fetcher ||= new Gem.configuration[:http_proxy] end attr_accessor :headers @@ -71,17 +72,17 @@ class Gem::RemoteFetcher # +headers+: A set of additional HTTP headers to be sent to the server when # fetching the gem. - def initialize(proxy=nil, dns=nil, headers={}) + def initialize(proxy = nil, dns = nil, headers = {}) require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled - require "net/http" - require "stringio" - require "uri" + require_relative "vendored_net_http" + require_relative "vendor/uri/lib/uri" Socket.do_not_reverse_lookup = true @proxy = proxy @pools = {} @pool_lock = Thread::Mutex.new + @pool_size = 1 @cert_files = Gem::Request.get_cert_files @headers = headers @@ -110,28 +111,35 @@ class Gem::RemoteFetcher # always replaced. def download(spec, source_uri, install_dir = Gem.dir) + gem_file_name = File.basename spec.cache_file + install_cache_dir = File.join install_dir, "cache" cache_dir = - if Dir.pwd == install_dir # see fetch_command + if Gem.configuration.global_gem_cache + Gem.global_gem_cache_path + elsif Dir.pwd == install_dir # see fetch_command install_dir - elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && (!File.exist?(install_cache_dir))) + elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && !File.exist?(install_cache_dir)) install_cache_dir else File.join Gem.user_dir, "cache" end - gem_file_name = File.basename spec.cache_file local_gem_path = File.join cache_dir, gem_file_name require "fileutils" - FileUtils.mkdir_p cache_dir rescue nil unless File.exist? cache_dir + begin + FileUtils.mkdir_p cache_dir + rescue StandardError + nil + end unless File.exist? cache_dir source_uri = Gem::Uri.new(source_uri) scheme = source_uri.scheme - # URI.parse gets confused by MS Windows paths with forward slashes. - scheme = nil if scheme =~ /^[a-z]$/i + # Gem::URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if /^[a-z]$/i.match?(scheme) # REFACTOR: split this up and dispatch on scheme (eg download_http) # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner @@ -143,7 +151,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{gem_file_name}" - self.cache_update_path remote_gem_path, local_gem_path + cache_update_path remote_gem_path, local_gem_path rescue FetchError raise if spec.original_platform == spec.platform @@ -153,7 +161,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{alternate_name}" - self.cache_update_path remote_gem_path, local_gem_path + cache_update_path remote_gem_path, local_gem_path end end when "file" then @@ -169,7 +177,7 @@ class Gem::RemoteFetcher end verbose "Using local gem #{local_gem_path}" - when nil then # TODO test for local overriding cache + when nil then source_path = if Gem.win_platform? && source_uri.scheme && !source_uri.path.include?(":") "#{source_uri.scheme}:#{source_uri.path}" @@ -205,17 +213,17 @@ class Gem::RemoteFetcher # HTTP Fetcher. Dispatched by +fetch_path+. Use it instead. def fetch_http(uri, last_modified = nil, head = false, depth = 0) - fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get + fetch_type = head ? Gem::Net::HTTP::Head : Gem::Net::HTTP::Get response = request uri, fetch_type, last_modified do |req| headers.each {|k,v| req.add_field(k,v) } end case response - when Net::HTTPOK, Net::HTTPNotModified then + when Gem::Net::HTTPOK, Gem::Net::HTTPNotModified then response.uri = uri head ? response : response.body - when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, - Net::HTTPTemporaryRedirect then + when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther, + Gem::Net::HTTPTemporaryRedirect then raise FetchError.new("too many redirects", uri) if depth > 10 unless location = response["Location"] @@ -229,11 +237,13 @@ class Gem::RemoteFetcher fetch_http(location, last_modified, head, depth + 1) else - raise FetchError.new("bad response #{response.message} #{response.code}", uri) + custom_error = response["X-Error-Message"] + error_detail = custom_error || response.message + raise FetchError.new("Bad response #{error_detail} #{response.code}", uri) end end - alias :fetch_https :fetch_http + alias_method :fetch_https, :fetch_http ## # Downloads +uri+ and returns it as a String. @@ -241,11 +251,14 @@ class Gem::RemoteFetcher def fetch_path(uri, mtime = nil, head = false) uri = Gem::Uri.new uri - unless uri.scheme - raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" - end + method = { + "http" => "fetch_http", + "https" => "fetch_http", + "s3" => "fetch_s3", + "file" => "fetch_file", + }.fetch(uri.scheme) { raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" } - data = send "fetch_#{uri.scheme}", uri, mtime, head + data = send method, uri, mtime, head if data && !head && uri.to_s.end_with?(".gz") begin @@ -256,14 +269,14 @@ class Gem::RemoteFetcher end data - rescue Timeout::Error, IOError, SocketError, SystemCallError, + rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError, *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e raise FetchError.new("#{e.class}: #{e}", uri) end def fetch_s3(uri, mtime = nil, head = false) begin - public_uri = s3_uri_signer(uri).sign + public_uri = s3_uri_signer(uri, head ? "HEAD" : "GET").sign rescue Gem::S3URISigner::ConfigurationError, Gem::S3URISigner::InstanceProfileError => e raise FetchError.new(e.message, "s3://#{uri.host}") end @@ -271,8 +284,8 @@ class Gem::RemoteFetcher end # we have our own signing code here to avoid a dependency on the aws-sdk gem - def s3_uri_signer(uri) - Gem::S3URISigner.new(uri) + def s3_uri_signer(uri, method) + Gem::S3URISigner.new(uri, method) end ## @@ -280,7 +293,11 @@ class Gem::RemoteFetcher # passes the data. def cache_update_path(uri, path = nil, update = true) - mtime = path && File.stat(path).mtime rescue nil + mtime = begin + path && File.stat(path).mtime + rescue StandardError + nil + end data = fetch_path(uri, mtime) @@ -296,8 +313,8 @@ class Gem::RemoteFetcher end ## - # Performs a Net::HTTP request of type +request_class+ on +uri+ returning - # a Net::HTTP response object. request maintains a table of persistent + # Performs a Gem::Net::HTTP request of type +request_class+ on +uri+ returning + # a Gem::Net::HTTP response object. request maintains a table of persistent # connections to reduce connect overhead. def request(uri, request_class, last_modified = nil) @@ -312,11 +329,11 @@ class Gem::RemoteFetcher end def https?(uri) - uri.scheme.downcase == "https" + uri.scheme.casecmp("https").zero? end def close_all - @pools.each_value {|pool| pool.close_all } + @pools.each_value(&:close_all) end private @@ -327,7 +344,7 @@ class Gem::RemoteFetcher def pools_for(proxy) @pool_lock.synchronize do - @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files + @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files, @pool_size end end end diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index c3ea46e0eb..e817ee5704 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -require "net/http" + +require_relative "vendored_net_http" require_relative "user_interaction" +require_relative "uri_formatter" class Gem::Request extend Gem::UserInteraction @@ -17,11 +19,11 @@ class Gem::Request end def self.proxy_uri(proxy) # :nodoc: - require "uri" + require_relative "vendor/uri/lib/uri" case proxy when :no_proxy then nil - when URI::HTTP then proxy - else URI.parse(proxy) + when Gem::URI::HTTP then proxy + else Gem::URI.parse(proxy) end end @@ -29,14 +31,19 @@ class Gem::Request @uri = uri @request_class = request_class @last_modified = last_modified - @requests = Hash.new 0 + @requests = Hash.new(0).compare_by_identity @user_agent = user_agent @connection_pool = pool end - def proxy_uri; @connection_pool.proxy_uri; end - def cert_files; @connection_pool.cert_files; end + def proxy_uri + @connection_pool.proxy_uri + end + + def cert_files + @connection_pool.cert_files + end def self.get_cert_files pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__) @@ -159,23 +166,22 @@ class Gem::Request # environment variables. def self.get_proxy_from_env(scheme = "http") - _scheme = scheme.downcase - _SCHEME = scheme.upcase - env_proxy = ENV["#{_scheme}_proxy"] || ENV["#{_SCHEME}_PROXY"] + downcase_scheme = scheme.downcase + upcase_scheme = scheme.upcase + env_proxy = ENV["#{downcase_scheme}_proxy"] || ENV["#{upcase_scheme}_PROXY"] no_env_proxy = env_proxy.nil? || env_proxy.empty? if no_env_proxy - return (_scheme == "https" || _scheme == "http") ? - :no_proxy : get_proxy_from_env("http") + return ["https", "http"].include?(downcase_scheme) ? :no_proxy : get_proxy_from_env("http") end require "uri" - uri = URI(Gem::UriFormatter.new(env_proxy).normalize) + uri = Gem::URI(Gem::UriFormatter.new(env_proxy).normalize) if uri && uri.user.nil? && uri.password.nil? - user = ENV["#{_scheme}_proxy_user"] || ENV["#{_SCHEME}_PROXY_USER"] - password = ENV["#{_scheme}_proxy_pass"] || ENV["#{_SCHEME}_PROXY_PASS"] + user = ENV["#{downcase_scheme}_proxy_user"] || ENV["#{upcase_scheme}_PROXY_USER"] + password = ENV["#{downcase_scheme}_proxy_pass"] || ENV["#{upcase_scheme}_PROXY_PASS"] uri.user = Gem::UriFormatter.new(user).escape uri.password = Gem::UriFormatter.new(password).escape @@ -191,7 +197,7 @@ class Gem::Request bad_response = false begin - @requests[connection.object_id] += 1 + @requests[connection] += 1 verbose "#{request.method} #{Gem::Uri.redact(@uri)}" @@ -200,7 +206,7 @@ class Gem::Request if request.response_body_permitted? && file_name =~ /\.gem$/ reporter = ui.download_reporter response = connection.request(request) do |incomplete_response| - if Net::HTTPOK === incomplete_response + if Gem::Net::HTTPOK === incomplete_response reporter.fetch(file_name, incomplete_response.content_length) downloaded = 0 data = String.new @@ -223,8 +229,7 @@ class Gem::Request end verbose "#{response.code} #{response.message}" - - rescue Net::HTTPBadResponse + rescue Gem::Net::HTTPBadResponse verbose "bad response" reset connection @@ -233,17 +238,17 @@ class Gem::Request bad_response = true retry - rescue Net::HTTPFatalError + rescue Gem::Net::HTTPFatalError verbose "fatal error" raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri) - # HACK work around EOFError bug in Net::HTTP + # HACK: work around EOFError bug in Gem::Net::HTTP # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible # to install gems. - rescue EOFError, Timeout::Error, + rescue EOFError, Gem::Timeout::Error, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE - requests = @requests[connection.object_id] + requests = @requests[connection] verbose "connection reset after #{requests} requests, retrying" raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried @@ -263,7 +268,7 @@ class Gem::Request # Resets HTTP connection +connection+. def reset(connection) - @requests.delete connection.object_id + @requests.delete connection connection.finish connection.start @@ -278,7 +283,7 @@ class Gem::Request ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" if RUBY_PATCHLEVEL >= 0 ua << " patchlevel #{RUBY_PATCHLEVEL}" - elsif defined?(RUBY_REVISION) + else ua << " revision #{RUBY_REVISION}" end ua << ")" diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index 44280489fb..01e7e0629a 100644 --- a/lib/rubygems/request/connection_pools.rb +++ b/lib/rubygems/request/connection_pools.rb @@ -1,17 +1,18 @@ # frozen_string_literal: true class Gem::Request::ConnectionPools # :nodoc: - @client = Net::HTTP + @client = Gem::Net::HTTP class << self attr_accessor :client end - def initialize(proxy_uri, cert_files) + def initialize(proxy_uri, cert_files, pool_size = 1) @proxy_uri = proxy_uri @cert_files = cert_files @pools = {} @pool_mutex = Thread::Mutex.new + @pool_size = pool_size end def pool_for(uri) @@ -20,15 +21,15 @@ class Gem::Request::ConnectionPools # :nodoc: @pool_mutex.synchronize do @pools[key] ||= if https? uri - Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri) + Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri, @pool_size) else - Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri) + Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri, @pool_size) end end end def close_all - @pools.each_value {|pool| pool.close_all } + @pools.each_value(&:close_all) end private @@ -45,7 +46,7 @@ class Gem::Request::ConnectionPools # :nodoc: end def https?(uri) - uri.scheme.downcase == "https" + uri.scheme.casecmp("https").zero? end def no_proxy?(host, env_no_proxy) diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index 7b309eedd3..468502ca6b 100644 --- a/lib/rubygems/request/http_pool.rb +++ b/lib/rubygems/request/http_pool.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A connection "pool" that only manages one connection for now. Provides # thread safe `checkout` and `checkin` methods. The pool consists of one @@ -8,12 +9,14 @@ class Gem::Request::HTTPPool # :nodoc: attr_reader :cert_files, :proxy_uri - def initialize(http_args, cert_files, proxy_uri) + def initialize(http_args, cert_files, proxy_uri, pool_size) @http_args = http_args @cert_files = cert_files @proxy_uri = proxy_uri - @queue = Thread::SizedQueue.new 1 - @queue << nil + @pool_size = pool_size + + @queue = Thread::SizedQueue.new @pool_size + setup_queue end def checkout @@ -30,7 +33,8 @@ class Gem::Request::HTTPPool # :nodoc: connection.finish end end - @queue.push(nil) + + setup_queue end private @@ -43,4 +47,8 @@ class Gem::Request::HTTPPool # :nodoc: connection.start connection end + + def setup_queue + @pool_size.times { @queue.push(nil) } + end end diff --git a/lib/rubygems/request/https_pool.rb b/lib/rubygems/request/https_pool.rb index 50f42d9e0d..cb1d4b59b6 100644 --- a/lib/rubygems/request/https_pool.rb +++ b/lib/rubygems/request/https_pool.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::Request::HTTPSPool < Gem::Request::HTTPPool # :nodoc: private diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 64701a8214..eb8b4658f3 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require_relative "tsort" + +require_relative "vendored_tsort" ## # A RequestSet groups a request to activate a set of dependencies. @@ -107,7 +108,7 @@ class Gem::RequestSet @requests = [] @sets = [] @soft_missing = false - @sorted = nil + @sorted_requests = nil @specs = nil @vendor_set = nil @source_set = nil @@ -159,7 +160,7 @@ class Gem::RequestSet end # Create N threads in a pool, have them download all the gems - threads = Gem.configuration.concurrent_downloads.times.map do + threads = Array.new(Gem.configuration.concurrent_downloads) do # When a thread pops this item, it knows to stop running. The symbol # is queued here so that there will be one symbol per thread. download_queue << :stop @@ -180,13 +181,10 @@ class Gem::RequestSet # Install requested gems after they have been downloaded sorted_requests.each do |req| - if req.installed? - req.spec.spec.build_extensions - - if @always_install.none? {|spec| spec == req.spec.spec } - yield req, nil if block_given? - next - end + if req.installed? && @always_install.none? {|spec| spec == req.spec.spec } + req.spec.spec.build_extensions unless options[:build_extension] == false + yield req, nil if block_given? + next end spec = @@ -236,10 +234,6 @@ class Gem::RequestSet sorted_requests.each do |spec| puts " #{spec.full_name}" end - - if Gem.configuration.really_verbose - @resolver.stats.display - end else installed = install options, &block @@ -254,7 +248,8 @@ class Gem::RequestSet end def install_into(dir, force = true, options = {}) - gem_home, ENV["GEM_HOME"] = ENV["GEM_HOME"], dir + gem_home = ENV["GEM_HOME"] + ENV["GEM_HOME"] = dir existing = force ? [] : specs_in(dir) existing.delete_if {|s| @always_install.include? s } @@ -322,12 +317,9 @@ class Gem::RequestSet @git_set.root_dir = @install_dir - lock_file = "#{File.expand_path(path)}.lock".dup.tap(&Gem::UNTAINT) - begin - tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file lock_file - parser = tokenizer.make_parser self, [] - parser.parse - rescue Errno::ENOENT + lock_file = "#{File.expand_path(path)}.lock" + if File.exist?(lock_file) + load_lockfile lock_file end gf = Gem::RequestSet::GemDependencyAPI.new self, path @@ -336,6 +328,63 @@ class Gem::RequestSet gf.load end + def load_lockfile(lock_file) # :nodoc: + require "bundler" + require "bundler/lockfile_parser" + + # Bundler::Source::Path resolves relative `remote:` paths against + # Bundler.root, which raises when there is no Gemfile in the working + # directory. Anchor it to the lockfile's directory so PATH sections in a + # `gem install -g` lockfile can be parsed without a Bundler environment. + previous_root = Bundler.instance_variable_get(:@root) + Bundler.instance_variable_set(:@root, Pathname.new(File.expand_path(File.dirname(lock_file)))) + + parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file) + + parser.specs.group_by(&:source).each do |source, specs| + case source + when Bundler::Source::Rubygems + remotes = source.remotes.map {|remote| Gem::Source.new(remote.to_s) } + remotes << Gem::Source.new(Gem::DEFAULT_HOST) if remotes.empty? + lock_set = Gem::Resolver::LockSet.new(remotes) + specs.each do |spec| + added = lock_set.add(spec.name, spec.version.to_s, spec.platform) + spec.dependencies.each do |dep| + added.each {|s| s.add_dependency dep } + end + end + @sets << lock_set + when Bundler::Source::Git + git_set = Gem::Resolver::GitSet.new + git_set.root_dir = @install_dir + specs.each do |spec| + git_spec = git_set.add_git_spec( + spec.name, + spec.version.to_s, + source.uri.to_s, + source.revision, + source.submodules || false + ) + spec.dependencies.each {|dep| git_spec.add_dependency dep } + end + @sets << git_set + when Bundler::Source::Path + vendor_set = Gem::Resolver::VendorSet.new + specs.each do |spec| + loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s) + spec.dependencies.each {|dep| loaded.dependencies << dep } + end + @sets << vendor_set + end + end + + parser.dependencies.each_value do |dep| + gem dep.name, *dep.requirement.as_list + end + ensure + Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root) + end + def pretty_print(q) # :nodoc: q.group 2, "[RequestSet:", "]" do q.breakable @@ -374,7 +423,7 @@ class Gem::RequestSet q.text "sets:" q.breakable - q.pp @sets.map {|set| set.class } + q.pp @sets.map(&:class) end end @@ -424,11 +473,11 @@ class Gem::RequestSet end def sorted_requests - @sorted ||= strongly_connected_components.flatten + @sorted_requests ||= strongly_connected_components.flatten end def specs - @specs ||= @requests.map {|r| r.full_spec } + @specs ||= @requests.map(&:full_spec) end def specs_in(dir) @@ -446,7 +495,7 @@ class Gem::RequestSet next if dep.type == :development && !@development match = @requests.find do |r| - dep.match? r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease + dep.match?(r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease) end unless match @@ -463,4 +512,3 @@ end require_relative "request_set/gem_dependency_api" require_relative "request_set/lockfile" -require_relative "request_set/lockfile/tokenizer" diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb index ad6e45005b..99d96f928b 100644 --- a/lib/rubygems/request_set/gem_dependency_api.rb +++ b/lib/rubygems/request_set/gem_dependency_api.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A semi-compatible DSL for the Bundler Gemfile and Isolate gem dependencies # files. @@ -32,22 +33,22 @@ class Gem::RequestSet::GemDependencyAPI ENGINE_MAP = { # :nodoc: - :jruby => %w[jruby], - :jruby_18 => %w[jruby], - :jruby_19 => %w[jruby], - :maglev => %w[maglev], - :mri => %w[ruby], - :mri_18 => %w[ruby], - :mri_19 => %w[ruby], - :mri_20 => %w[ruby], - :mri_21 => %w[ruby], - :rbx => %w[rbx], - :truffleruby => %w[truffleruby], - :ruby => %w[ruby rbx maglev truffleruby], - :ruby_18 => %w[ruby rbx maglev truffleruby], - :ruby_19 => %w[ruby rbx maglev truffleruby], - :ruby_20 => %w[ruby rbx maglev truffleruby], - :ruby_21 => %w[ruby rbx maglev truffleruby], + jruby: %w[jruby], + jruby_18: %w[jruby], + jruby_19: %w[jruby], + maglev: %w[maglev], + mri: %w[ruby], + mri_18: %w[ruby], + mri_19: %w[ruby], + mri_20: %w[ruby], + mri_21: %w[ruby], + rbx: %w[rbx], + truffleruby: %w[truffleruby], + ruby: %w[ruby rbx maglev truffleruby], + ruby_18: %w[ruby rbx maglev truffleruby], + ruby_19: %w[ruby rbx maglev truffleruby], + ruby_20: %w[ruby rbx maglev truffleruby], + ruby_21: %w[ruby rbx maglev truffleruby], }.freeze mswin = Gem::Platform.new "x86-mswin32" @@ -56,39 +57,39 @@ class Gem::RequestSet::GemDependencyAPI x64_mingw = Gem::Platform.new "x64-mingw32" PLATFORM_MAP = { # :nodoc: - :jruby => Gem::Platform::RUBY, - :jruby_18 => Gem::Platform::RUBY, - :jruby_19 => Gem::Platform::RUBY, - :maglev => Gem::Platform::RUBY, - :mingw => x86_mingw, - :mingw_18 => x86_mingw, - :mingw_19 => x86_mingw, - :mingw_20 => x86_mingw, - :mingw_21 => x86_mingw, - :mri => Gem::Platform::RUBY, - :mri_18 => Gem::Platform::RUBY, - :mri_19 => Gem::Platform::RUBY, - :mri_20 => Gem::Platform::RUBY, - :mri_21 => Gem::Platform::RUBY, - :mswin => mswin, - :mswin_18 => mswin, - :mswin_19 => mswin, - :mswin_20 => mswin, - :mswin_21 => mswin, - :mswin64 => mswin64, - :mswin64_19 => mswin64, - :mswin64_20 => mswin64, - :mswin64_21 => mswin64, - :rbx => Gem::Platform::RUBY, - :ruby => Gem::Platform::RUBY, - :ruby_18 => Gem::Platform::RUBY, - :ruby_19 => Gem::Platform::RUBY, - :ruby_20 => Gem::Platform::RUBY, - :ruby_21 => Gem::Platform::RUBY, - :truffleruby => Gem::Platform::RUBY, - :x64_mingw => x64_mingw, - :x64_mingw_20 => x64_mingw, - :x64_mingw_21 => x64_mingw, + jruby: Gem::Platform::RUBY, + jruby_18: Gem::Platform::RUBY, + jruby_19: Gem::Platform::RUBY, + maglev: Gem::Platform::RUBY, + mingw: x86_mingw, + mingw_18: x86_mingw, + mingw_19: x86_mingw, + mingw_20: x86_mingw, + mingw_21: x86_mingw, + mri: Gem::Platform::RUBY, + mri_18: Gem::Platform::RUBY, + mri_19: Gem::Platform::RUBY, + mri_20: Gem::Platform::RUBY, + mri_21: Gem::Platform::RUBY, + mswin: mswin, + mswin_18: mswin, + mswin_19: mswin, + mswin_20: mswin, + mswin_21: mswin, + mswin64: mswin64, + mswin64_19: mswin64, + mswin64_20: mswin64, + mswin64_21: mswin64, + rbx: Gem::Platform::RUBY, + ruby: Gem::Platform::RUBY, + ruby_18: Gem::Platform::RUBY, + ruby_19: Gem::Platform::RUBY, + ruby_20: Gem::Platform::RUBY, + ruby_21: Gem::Platform::RUBY, + truffleruby: Gem::Platform::RUBY, + x64_mingw: x64_mingw, + x64_mingw_20: x64_mingw, + x64_mingw_21: x64_mingw, }.freeze gt_eq_0 = Gem::Requirement.new ">= 0" @@ -98,70 +99,70 @@ class Gem::RequestSet::GemDependencyAPI tilde_gt_2_1_0 = Gem::Requirement.new "~> 2.1.0" VERSION_MAP = { # :nodoc: - :jruby => gt_eq_0, - :jruby_18 => tilde_gt_1_8_0, - :jruby_19 => tilde_gt_1_9_0, - :maglev => gt_eq_0, - :mingw => gt_eq_0, - :mingw_18 => tilde_gt_1_8_0, - :mingw_19 => tilde_gt_1_9_0, - :mingw_20 => tilde_gt_2_0_0, - :mingw_21 => tilde_gt_2_1_0, - :mri => gt_eq_0, - :mri_18 => tilde_gt_1_8_0, - :mri_19 => tilde_gt_1_9_0, - :mri_20 => tilde_gt_2_0_0, - :mri_21 => tilde_gt_2_1_0, - :mswin => gt_eq_0, - :mswin_18 => tilde_gt_1_8_0, - :mswin_19 => tilde_gt_1_9_0, - :mswin_20 => tilde_gt_2_0_0, - :mswin_21 => tilde_gt_2_1_0, - :mswin64 => gt_eq_0, - :mswin64_19 => tilde_gt_1_9_0, - :mswin64_20 => tilde_gt_2_0_0, - :mswin64_21 => tilde_gt_2_1_0, - :rbx => gt_eq_0, - :ruby => gt_eq_0, - :ruby_18 => tilde_gt_1_8_0, - :ruby_19 => tilde_gt_1_9_0, - :ruby_20 => tilde_gt_2_0_0, - :ruby_21 => tilde_gt_2_1_0, - :truffleruby => gt_eq_0, - :x64_mingw => gt_eq_0, - :x64_mingw_20 => tilde_gt_2_0_0, - :x64_mingw_21 => tilde_gt_2_1_0, + jruby: gt_eq_0, + jruby_18: tilde_gt_1_8_0, + jruby_19: tilde_gt_1_9_0, + maglev: gt_eq_0, + mingw: gt_eq_0, + mingw_18: tilde_gt_1_8_0, + mingw_19: tilde_gt_1_9_0, + mingw_20: tilde_gt_2_0_0, + mingw_21: tilde_gt_2_1_0, + mri: gt_eq_0, + mri_18: tilde_gt_1_8_0, + mri_19: tilde_gt_1_9_0, + mri_20: tilde_gt_2_0_0, + mri_21: tilde_gt_2_1_0, + mswin: gt_eq_0, + mswin_18: tilde_gt_1_8_0, + mswin_19: tilde_gt_1_9_0, + mswin_20: tilde_gt_2_0_0, + mswin_21: tilde_gt_2_1_0, + mswin64: gt_eq_0, + mswin64_19: tilde_gt_1_9_0, + mswin64_20: tilde_gt_2_0_0, + mswin64_21: tilde_gt_2_1_0, + rbx: gt_eq_0, + ruby: gt_eq_0, + ruby_18: tilde_gt_1_8_0, + ruby_19: tilde_gt_1_9_0, + ruby_20: tilde_gt_2_0_0, + ruby_21: tilde_gt_2_1_0, + truffleruby: gt_eq_0, + x64_mingw: gt_eq_0, + x64_mingw_20: tilde_gt_2_0_0, + x64_mingw_21: tilde_gt_2_1_0, }.freeze WINDOWS = { # :nodoc: - :mingw => :only, - :mingw_18 => :only, - :mingw_19 => :only, - :mingw_20 => :only, - :mingw_21 => :only, - :mri => :never, - :mri_18 => :never, - :mri_19 => :never, - :mri_20 => :never, - :mri_21 => :never, - :mswin => :only, - :mswin_18 => :only, - :mswin_19 => :only, - :mswin_20 => :only, - :mswin_21 => :only, - :mswin64 => :only, - :mswin64_19 => :only, - :mswin64_20 => :only, - :mswin64_21 => :only, - :rbx => :never, - :ruby => :never, - :ruby_18 => :never, - :ruby_19 => :never, - :ruby_20 => :never, - :ruby_21 => :never, - :x64_mingw => :only, - :x64_mingw_20 => :only, - :x64_mingw_21 => :only, + mingw: :only, + mingw_18: :only, + mingw_19: :only, + mingw_20: :only, + mingw_21: :only, + mri: :never, + mri_18: :never, + mri_19: :never, + mri_20: :never, + mri_21: :never, + mswin: :only, + mswin_18: :only, + mswin_19: :only, + mswin_20: :only, + mswin_21: :only, + mswin64: :only, + mswin64_19: :only, + mswin64_20: :only, + mswin64_21: :only, + rbx: :never, + ruby: :never, + ruby_18: :never, + ruby_19: :never, + ruby_20: :never, + ruby_21: :never, + x64_mingw: :only, + x64_mingw_20: :only, + x64_mingw_21: :only, }.freeze ## @@ -261,7 +262,7 @@ class Gem::RequestSet::GemDependencyAPI raise ArgumentError, "no gemspecs found at #{Dir.pwd}" else raise ArgumentError, - "found multiple gemspecs at #{Dir.pwd}, " + + "found multiple gemspecs at #{Dir.pwd}, " \ "use the name: option to specify the one you want" end end @@ -279,7 +280,7 @@ class Gem::RequestSet::GemDependencyAPI # Loads the gem dependency file and returns self. def load - instance_eval File.read(@path).tap(&Gem::UNTAINT), @path, 1 + instance_eval File.read(@path), @path, 1 self end @@ -329,7 +330,7 @@ class Gem::RequestSet::GemDependencyAPI # git: :: # Install this dependency from a git repository: # - # gem 'private_gem', git: git@my.company.example:private_gem.git' + # gem 'private_gem', git: 'git@my.company.example:private_gem.git' # # gist: :: # Install this dependency from the gist ID: @@ -356,7 +357,7 @@ class Gem::RequestSet::GemDependencyAPI # Use the given tag for git:, gist: and github: dependencies. def gem(name, *requirements) - options = requirements.pop if requirements.last.kind_of?(Hash) + options = requirements.pop if requirements.last.is_a?(Hash) options ||= {} options[:git] = @current_repository if @current_repository @@ -435,7 +436,6 @@ Gem dependencies file #{@path} requires #{name} more than once. reference ||= ref reference ||= branch reference ||= tag - reference ||= "master" if ref && branch warn <<-WARNING @@ -533,8 +533,8 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta # platform matches the current platform. def gem_platforms(name, options) # :nodoc: - platform_names = Array(options.delete :platform) - platform_names.concat Array(options.delete :platforms) + platform_names = Array(options.delete(:platform)) + platform_names.concat Array(options.delete(:platforms)) platform_names.concat @current_platforms if @current_platforms return true if platform_names.empty? @@ -593,7 +593,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_repository = repository yield - ensure @current_repository = nil end @@ -685,7 +684,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_groups = groups yield - ensure @current_groups = nil end @@ -760,7 +758,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_platforms = platforms yield - ensure @current_platforms = nil end @@ -771,7 +768,7 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta # Block form for restricting gems to a particular set of platforms. See # #platform. - alias :platforms :platform + alias_method :platforms, :platform ## # :category: Gem Dependencies DSL @@ -793,15 +790,15 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta return true if @installing - unless RUBY_VERSION == version - message = "Your Ruby version is #{RUBY_VERSION}, " + + unless version == RUBY_VERSION + message = "Your Ruby version is #{RUBY_VERSION}, " \ "but your #{gem_deps_file} requires #{version}" raise Gem::RubyVersionMismatch, message end if engine && engine != Gem.ruby_engine - message = "Your Ruby engine is #{Gem.ruby_engine}, " + + message = "Your Ruby engine is #{Gem.ruby_engine}, " \ "but your #{gem_deps_file} requires #{engine}" raise Gem::RubyVersionMismatch, message @@ -810,14 +807,14 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta if engine_version if engine_version != RUBY_ENGINE_VERSION message = - "Your Ruby engine version is #{Gem.ruby_engine} #{RUBY_ENGINE_VERSION}, " + + "Your Ruby engine version is #{Gem.ruby_engine} #{RUBY_ENGINE_VERSION}, " \ "but your #{gem_deps_file} requires #{engine} #{engine_version}" raise Gem::RubyVersionMismatch, message end end - return true + true end ## diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb index 3ba202f661..8b9c9690d6 100644 --- a/lib/rubygems/request_set/lockfile.rb +++ b/lib/rubygems/request_set/lockfile.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Parses a gem.deps.rb.lock file and constructs a LockSet containing the # dependencies found inside. If the lock file is missing no LockSet is @@ -37,7 +38,7 @@ class Gem::RequestSet::Lockfile end ## - # Creates a new Lockfile for the given +request_set+ and +gem_deps_file+ + # Creates a new Lockfile for the given Gem::RequestSet and +gem_deps_file+ # location. def self.build(request_set, gem_deps_file, dependencies = nil) @@ -75,18 +76,13 @@ class Gem::RequestSet::Lockfile @dependencies = dependencies @gem_deps_file = File.expand_path(gem_deps_file) @gem_deps_dir = File.dirname(@gem_deps_file) - - if RUBY_VERSION < "2.7" - @gem_deps_file.untaint unless gem_deps_file.tainted? - end - @platforms = [] end def add_DEPENDENCIES(out) # :nodoc: out << "DEPENDENCIES" - out.concat @dependencies.sort_by {|name,| name }.map {|name, requirement| + out.concat @dependencies.sort.map {|name, requirement| " #{name}#{requirement.for_lockfile}" } @@ -105,10 +101,10 @@ class Gem::RequestSet::Lockfile out << " remote: #{group}" out << " specs:" - requests.sort_by {|request| request.name }.each do |request| + requests.sort_by(&:name).each do |request| next if request.spec.name == "bundler" platform = "-#{request.spec.platform}" unless - Gem::Platform::RUBY == request.spec.platform + request.spec.platform == Gem::Platform::RUBY out << " #{request.name} (#{request.version}#{platform})" @@ -137,10 +133,10 @@ class Gem::RequestSet::Lockfile out << " revision: #{revision}" out << " specs:" - requests.sort_by {|request| request.name }.each do |request| + requests.sort_by(&:name).each do |request| out << " #{request.name} (#{request.version})" - dependencies = request.spec.dependencies.sort_by {|dep| dep.name } + dependencies = request.spec.dependencies.sort_by(&:name) dependencies.each do |dep| out << " #{dep.name}#{dep.requirement.for_lockfile}" end @@ -184,7 +180,7 @@ class Gem::RequestSet::Lockfile platforms = requests.map {|request| request.spec.platform }.uniq - platforms = platforms.sort_by {|platform| platform.to_s } + platforms = platforms.sort_by(&:to_s) platforms.each do |platform| out << " #{platform}" @@ -235,5 +231,3 @@ class Gem::RequestSet::Lockfile @set.sorted_requests end end - -require_relative "lockfile/tokenizer" diff --git a/lib/rubygems/request_set/lockfile/parser.rb b/lib/rubygems/request_set/lockfile/parser.rb deleted file mode 100644 index 2d98c9520b..0000000000 --- a/lib/rubygems/request_set/lockfile/parser.rb +++ /dev/null @@ -1,343 +0,0 @@ -# frozen_string_literal: true -class Gem::RequestSet::Lockfile::Parser - ### - # Parses lockfiles - - def initialize(tokenizer, set, platforms, filename = nil) - @tokens = tokenizer - @filename = filename - @set = set - @platforms = platforms - end - - def parse - until @tokens.empty? do - token = get - - case token.type - when :section then - @tokens.skip :newline - - case token.value - when "DEPENDENCIES" then - parse_DEPENDENCIES - when "GIT" then - parse_GIT - when "GEM" then - parse_GEM - when "PATH" then - parse_PATH - when "PLATFORMS" then - parse_PLATFORMS - else - token = get until @tokens.empty? || peek.first == :section - end - else - raise "BUG: unhandled token #{token.type} (#{token.value.inspect}) at line #{token.line} column #{token.column}" - end - end - end - - ## - # Gets the next token for a Lockfile - - def get(expected_types = nil, expected_value = nil) # :nodoc: - token = @tokens.shift - - if expected_types && !Array(expected_types).include?(token.type) - unget token - - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + - "expected #{expected_types.inspect}" - - raise Gem::RequestSet::Lockfile::ParseError.new message, token.column, token.line, @filename - end - - if expected_value && expected_value != token.value - unget token - - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + - "expected [#{expected_types.inspect}, " + - "#{expected_value.inspect}]" - - raise Gem::RequestSet::Lockfile::ParseError.new message, token.column, token.line, @filename - end - - token - end - - def parse_DEPENDENCIES # :nodoc: - while !@tokens.empty? && :text == peek.type do - token = get :text - - requirements = [] - - case peek[0] - when :bang then - get :bang - - requirements << pinned_requirement(token.value) - when :l_paren then - get :l_paren - - loop do - op = get(:requirement).value - version = get(:text).value - - requirements << "#{op} #{version}" - - break unless peek.type == :comma - - get :comma - end - - get :r_paren - - if peek[0] == :bang - requirements.clear - requirements << pinned_requirement(token.value) - - get :bang - end - end - - @set.gem token.value, *requirements - - skip :newline - end - end - - def parse_GEM # :nodoc: - sources = [] - - while [:entry, "remote"] == peek.first(2) do - get :entry, "remote" - data = get(:text).value - skip :newline - - sources << Gem::Source.new(data) - end - - sources << Gem::Source.new(Gem::DEFAULT_HOST) if sources.empty? - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::LockSet.new sources - last_specs = nil - - while !@tokens.empty? && :text == peek.type do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_specs.each do |spec| - spec.add_dependency Gem::Dependency.new name if column == 6 - end - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - version, platform = data.split "-", 2 - - platform = - platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY - - last_specs = set.add name, version, platform - else - dependency = parse_dependency name, data - - last_specs.each do |spec| - spec.add_dependency dependency - end - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_GIT # :nodoc: - get :entry, "remote" - repository = get(:text).value - - skip :newline - - get :entry, "revision" - revision = get(:text).value - - skip :newline - - type = peek.type - value = peek.value - if type == :entry && %w[branch ref tag].include?(value) - get - get :text - - skip :newline - end - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::GitSet.new - set.root_dir = @set.install_dir - - last_spec = nil - - while !@tokens.empty? && :text == peek.type do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_spec.add_dependency Gem::Dependency.new name if column == 6 - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - last_spec = set.add_git_spec name, data, repository, revision, true - else - dependency = parse_dependency name, data - - last_spec.add_dependency dependency - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_PATH # :nodoc: - get :entry, "remote" - directory = get(:text).value - - skip :newline - - get :entry, "specs" - - skip :newline - - set = Gem::Resolver::VendorSet.new - last_spec = nil - - while !@tokens.empty? && :text == peek.first do - token = get :text - name = token.value - column = token.column - - case peek[0] - when :newline then - last_spec.add_dependency Gem::Dependency.new name if column == 6 - when :l_paren then - get :l_paren - - token = get [:text, :requirement] - type = token.type - data = token.value - - if type == :text && column == 4 - last_spec = set.add_vendor_gem name, directory - else - dependency = parse_dependency name, data - - last_spec.dependencies << dependency - end - - get :r_paren - else - raise "BUG: unknown token #{peek}" - end - - skip :newline - end - - @set.sets << set - end - - def parse_PLATFORMS # :nodoc: - while !@tokens.empty? && :text == peek.first do - name = get(:text).value - - @platforms << name - - skip :newline - end - end - - ## - # Parses the requirements following the dependency +name+ and the +op+ for - # the first token of the requirements and returns a Gem::Dependency object. - - def parse_dependency(name, op) # :nodoc: - return Gem::Dependency.new name, op unless peek[0] == :text - - version = get(:text).value - - requirements = ["#{op} #{version}"] - - while peek.type == :comma do - get :comma - op = get(:requirement).value - version = get(:text).value - - requirements << "#{op} #{version}" - end - - Gem::Dependency.new name, requirements - end - - private - - def skip(type) # :nodoc: - @tokens.skip type - end - - ## - # Peeks at the next token for Lockfile - - def peek # :nodoc: - @tokens.peek - end - - def pinned_requirement(name) # :nodoc: - requirement = Gem::Dependency.new name - specification = @set.sets.flat_map do |set| - set.find_all(requirement) - end.compact.first - - specification&.version - end - - ## - # Ungets the last token retrieved by #get - - def unget(token) # :nodoc: - @tokens.unshift token - end -end diff --git a/lib/rubygems/request_set/lockfile/tokenizer.rb b/lib/rubygems/request_set/lockfile/tokenizer.rb deleted file mode 100644 index 4476a041c4..0000000000 --- a/lib/rubygems/request_set/lockfile/tokenizer.rb +++ /dev/null @@ -1,112 +0,0 @@ -#) frozen_string_literal: true -require_relative "parser" - -class Gem::RequestSet::Lockfile::Tokenizer - Token = Struct.new :type, :value, :column, :line - EOF = Token.new :EOF - - def self.from_file(file) - new File.read(file), file - end - - def initialize(input, filename = nil, line = 0, pos = 0) - @line = line - @line_pos = pos - @tokens = [] - @filename = filename - tokenize input - end - - def make_parser(set, platforms) - Gem::RequestSet::Lockfile::Parser.new self, set, platforms, @filename - end - - def to_a - @tokens.map {|token| [token.type, token.value, token.column, token.line] } - end - - def skip(type) - @tokens.shift while !@tokens.empty? && peek.type == type - end - - ## - # Calculates the column (by byte) and the line of the current token based on - # +byte_offset+. - - def token_pos(byte_offset) # :nodoc: - [byte_offset - @line_pos, @line] - end - - def empty? - @tokens.empty? - end - - def unshift(token) - @tokens.unshift token - end - - def next_token - @tokens.shift - end - alias :shift :next_token - - def peek - @tokens.first || EOF - end - - private - - def tokenize(input) - require "strscan" - s = StringScanner.new input - - until s.eos? do - pos = s.pos - - pos = s.pos if leading_whitespace = s.scan(/ +/) - - if s.scan(/[<|=>]{7}/) - message = "your #{@filename} contains merge conflict markers" - column, line = token_pos pos - - raise Gem::RequestSet::Lockfile::ParseError.new message, column, line, @filename - end - - @tokens << - case - when s.scan(/\r?\n/) then - token = Token.new(:newline, nil, *token_pos(pos)) - @line_pos = s.pos - @line += 1 - token - when s.scan(/[A-Z]+/) then - if leading_whitespace - text = s.matched - text += s.scan(/[^\s)]*/).to_s # in case of no match - Token.new(:text, text, *token_pos(pos)) - else - Token.new(:section, s.matched, *token_pos(pos)) - end - when s.scan(/([a-z]+):\s/) then - s.pos -= 1 # rewind for possible newline - Token.new(:entry, s[1], *token_pos(pos)) - when s.scan(/\(/) then - Token.new(:l_paren, nil, *token_pos(pos)) - when s.scan(/\)/) then - Token.new(:r_paren, nil, *token_pos(pos)) - when s.scan(/<=|>=|=|~>|<|>|!=/) then - Token.new(:requirement, s.matched, *token_pos(pos)) - when s.scan(/,/) then - Token.new(:comma, nil, *token_pos(pos)) - when s.scan(/!/) then - Token.new(:bang, nil, *token_pos(pos)) - when s.scan(/[^\s),!]*/) then - Token.new(:text, s.matched, *token_pos(pos)) - else - raise "BUG: can't create token for: #{s.string[s.pos..-1].inspect}" - end - end - - @tokens - end -end diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index bc2fd9af55..0d3f98eb0f 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "version" ## @@ -9,11 +10,11 @@ require_relative "version" # together in RubyGems. class Gem::Requirement - OPS = { #:nodoc: + OPS = { # :nodoc: "=" => lambda {|v, r| v == r }, "!=" => lambda {|v, r| v != r }, - ">" => lambda {|v, r| v > r }, - "<" => lambda {|v, r| v < r }, + ">" => lambda {|v, r| v > r }, + "<" => lambda {|v, r| v < r }, ">=" => lambda {|v, r| v >= r }, "<=" => lambda {|v, r| v <= r }, "~>" => lambda {|v, r| v >= r && v.release < r.bump }, @@ -21,13 +22,13 @@ class Gem::Requirement SOURCE_SET_REQUIREMENT = Struct.new(:for_lockfile).new "!" # :nodoc: - quoted = OPS.keys.map {|k| Regexp.quote k }.join "|" - PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*" # :nodoc: + quoted = Regexp.union(OPS.keys) + PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*".freeze # :nodoc: ## # A regular expression that matches a requirement - PATTERN = /\A#{PATTERN_RAW}\z/.freeze + PATTERN = /\A#{PATTERN_RAW}\z/ ## # The default requirement matches any non-prerelease version @@ -105,13 +106,15 @@ class Gem::Requirement unless PATTERN =~ obj.to_s raise BadRequirementError, "Illformed requirement [#{obj.inspect}]" end + op = -($1 || "=") + version = -$2 - if $1 == ">=" && $2 == "0" + if op == ">=" && version == "0" DefaultRequirement - elsif $1 == ">=" && $2 == "0.a" + elsif op == ">=" && version == "0.a" DefaultPrereleaseRequirement else - [-($1 || "="), Gem::Version.new($2)] + [op, Gem::Version.new(version)] end end @@ -119,7 +122,7 @@ class Gem::Requirement # An array of requirement pairs. The first element of the pair is # the op, and the second is the Gem::Version. - attr_reader :requirements #:nodoc: + attr_reader :requirements # :nodoc: ## # Constructs a requirement from +requirements+. Requirements can be @@ -155,7 +158,7 @@ class Gem::Requirement # Formats this requirement for use in a Gem::RequestSet::Lockfile. def for_lockfile # :nodoc: - return if [DefaultRequirement] == @requirements + return if @requirements == [DefaultRequirement] list = requirements.sort_by do |_, version| version @@ -163,7 +166,7 @@ class Gem::Requirement "#{op} #{version}" end.uniq - " (#{list.join ', '})" + " (#{list.join ", "})" end ## @@ -200,7 +203,8 @@ class Gem::Requirement def marshal_load(array) # :nodoc: @requirements = array[0] - raise TypeError, "wrong @requirements" unless Array === @requirements + raise TypeError, "wrong @requirements" unless Array === @requirements && + @requirements.all? {|r| r.size == 2 && (r.first.is_a?(String) || r[0] = "=") && r.last.is_a?(Gem::Version) } end def yaml_initialize(tag, vals) # :nodoc: @@ -213,10 +217,6 @@ class Gem::Requirement yaml_initialize coder.tag, coder.map end - def to_yaml_properties # :nodoc: - ["@requirements"] - end - def encode_with(coder) # :nodoc: coder.add "requirements", @requirements end @@ -241,11 +241,11 @@ class Gem::Requirement def satisfied_by?(version) raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless Gem::Version === version - requirements.all? {|op, rv| OPS[op].call version, rv } + requirements.all? {|op, rv| OPS.fetch(op).call version, rv } end - alias :=== :satisfied_by? - alias :=~ :satisfied_by? + alias_method :===, :satisfied_by? + alias_method :=~, :satisfied_by? ## # True if the requirement will not always match the latest version. @@ -283,6 +283,11 @@ class Gem::Requirement def _tilde_requirements @_tilde_requirements ||= _sorted_requirements.select {|r| r.first == "~>" } end + + def initialize_copy(other) # :nodoc: + @requirements = other.requirements.dup + super + end end class Gem::Version diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 76d1e9d0cc..788206c056 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true + require_relative "dependency" require_relative "exceptions" -require_relative "util/list" ## # Given a set of Gem::Dependency objects as +needed+ and a way to query the @@ -10,7 +10,7 @@ require_relative "util/list" # all the requirements. class Gem::Resolver - require_relative "resolver/molinillo" + require_relative "vendored_pub_grub" ## # If the DEBUG_RESOLVER environment variable is set then debugging mode is @@ -35,21 +35,13 @@ class Gem::Resolver attr_accessor :ignore_dependencies ## - # List of dependencies that could not be found in the configured sources. - - attr_reader :missing - - attr_reader :stats - - ## # Hash of gems to skip resolution. Keyed by gem name, with arrays of # gem specifications as values. attr_accessor :skip_gems ## - # When a missing dependency, don't stop. Just go on and record what was - # missing. + # attr_accessor :soft_missing @@ -61,7 +53,7 @@ class Gem::Resolver def self.compose_sets(*sets) sets.compact! - sets = sets.map do |set| + sets = sets.flat_map do |set| case set when Gem::Resolver::BestSet then set @@ -70,7 +62,7 @@ class Gem::Resolver else set end - end.flatten + end case sets.length when 0 then @@ -105,225 +97,451 @@ class Gem::Resolver @development = false @development_shallow = false @ignore_dependencies = false - @missing = [] @skip_gems = {} @soft_missing = false - @stats = Gem::Resolver::Stats.new - end - def explain(stage, *data) # :nodoc: - return unless DEBUG_RESOLVER + @root_package = RootPackage.new + @root_version = Gem::PubGrub::Package.root_version + + @packages = {} - d = data.map {|x| x.pretty_inspect }.join(", ") - $stderr.printf "%10s %s\n", stage.to_s.upcase, d + @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) } + @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) } + @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort } + @sorted_versions = Hash.new do |h, pkg| + h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg] + end + @cached_dependencies = Hash.new do |h, pkg| + h[pkg] = if Gem::PubGrub::Package.root?(pkg) + { @root_version => root_dependencies } + else + Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) } + end + end + @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h } + @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} } + @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) } end - def explain_list(stage) # :nodoc: - return unless DEBUG_RESOLVER + ## + # Proceed with resolution! Returns an array of ActivationRequest objects. + + def resolve + # Pre-check: raise UnsatisfiableDependencyError for root deps with no + # platform match. We filter by platform ONLY here (not required_ruby_version + # / required_rubygems_version): a foreign-platform gem is genuinely "not + # found", but a gem that exists yet is incompatible with the running Ruby + # should flow through the solver to a DependencyResolutionError that names + # the Ruby requirement. That matches Bundler (which models Ruby as a + # synthetic dependency, so this surfaces as a solve failure) and gives a + # clearer message than the platform-oriented UnsatisfiableDependencyError. + @needed.each do |dep| + next if @soft_missing + dep_request = DependencyRequest.new(dep, nil) + all = @set.find_all(dep_request) + matching = select_local_platforms(all) + + next unless matching.empty? + + exc = Gem::UnsatisfiableDependencyError.new(dep_request, all) + exc.errors = @set.errors + raise exc + end - data = yield - $stderr.printf "%10s (%d entries)\n", stage.to_s.upcase, data.size - unless data.empty? - require "pp" - PP.pp data, $stderr + solver = Gem::PubGrub::VersionSolver.new( + source: self, + root: @root_package, + strategy: Gem::Resolver::Strategy.new(self), + logger: make_logger + ) + result = solver.solve + + # Convert to Array<ActivationRequest> + needed_by_name = @needed.group_by(&:name) + result.filter_map do |package, version| + next if Gem::PubGrub::Package.root?(package) + spec = spec_for(package.to_s, version) + dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s) + dep_request = DependencyRequest.new(dep, nil) + ActivationRequest.new(spec, dep_request) + end + rescue Gem::PubGrub::SolveFailure => e + extended = extract_extended_explanation(e.incompatibility) + if extended + message = "#{e.explanation}\n\n#{extended}" + raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message) + else + raise Gem::DependencyResolutionError, e end end - ## - # Creates an ActivationRequest for the given +dep+ and the last +possible+ - # specification. - # - # Returns the Specification and the ActivationRequest + # PubGrub source interface methods + + def all_versions_for(package) + versions = @sorted_versions[package].reverse # highest first + name = package.to_s + + if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty? + # Conservative mode: float the already-installed (skip) versions to the + # front so the solver prefers them. This sets *preference* only (it feeds + # the strategy's version-index map); it does not restrict availability, so + # every version stays selectable via versions_for. When an installed + # version is made impossible by a downstream conflict, the solver + # backtracks to a newer version instead of failing. Molinillo instead + # hard-restricted the candidate set to skip versions and raised. + # + # This reaches the same outcome as Bundler (upgrade-over-raise) for the + # common single-blocked-gem case, though the mechanism differs: Bundler + # hard-pins locked gems and selectively unlocks + re-solves on conflict, + # whereas we float as a preference and let PubGrub backtrack in one solve. + # The float can therefore over-upgrade when several installed gems are + # jointly involved in a conflict; that outcome-level divergence is + # accepted (see test_conservative_upgrades_when_installed_blocked). + skip_versions = skip_dep_gems.map(&:version) + preferred, rest = versions.partition {|v| skip_versions.include?(v) } + preferred + rest + else + # Prefer already-installed versions to avoid unnecessary upgrades + installed_versions = @all_specs[name]. + select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }. + map(&:version) + if installed_versions.any? + preferred, rest = versions.partition {|v| installed_versions.include?(v) } + preferred + rest + else + versions + end + end + end - def activation_request(dep, possible) # :nodoc: - spec = possible.pop + def versions_for(package, range = Gem::PubGrub::VersionRange.any) + @versions_for_cache[package][range] ||= begin + candidates = range.select_versions(@sorted_versions[package]) - explain :activate, [spec.full_name, possible.size] - explain :possible, possible + if Gem::PubGrub::Package.root?(package) || + (@set.respond_to?(:prerelease) && @set.prerelease) || + range_admits_prerelease?(range) + candidates + elsif @all_versions[package].any? {|v| !v.prerelease? } + candidates.reject(&:prerelease?) + else + # Only prereleases exist for this gem; fall back to them so + # dependencies like `>= 1.0` can still be satisfied. + candidates + end + end + end + + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term) + + name = unsatisfied_term.package.to_s + constraint = unsatisfied_term.constraint + extended_explanation = build_extended_explanation(name, constraint) - activation_request = - Gem::Resolver::ActivationRequest.new spec, dep, possible + custom_explanation = if extended_explanation + "#{constraint} could not be found in any repository" + end - return spec, activation_request + Gem::Resolver::Incompatibility.new( + [unsatisfied_term], + cause: cause, + custom_explanation: custom_explanation, + extended_explanation: extended_explanation + ) end - def requests(s, act, reqs=[]) # :nodoc: - return reqs if @ignore_dependencies + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].filter_map do |dep_package_name, dep_constraint| + dep_package = dep_constraint.package - s.fetch_development_dependencies if @development + low = high = @version_to_index[package][version] - s.dependencies.reverse_each do |d| - next if d.type == :development && !@development - next if d.type == :development && @development_shallow && - act.development? - next if d.type == :development && @development_shallow && - act.parent + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package_name] == dep_constraint + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?) + self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range) + + # No specs anywhere means an unknown package. Check @unfiltered_specs, not + # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls + # through to NoVersions for proper hints instead. The band-scoped + # self_constraint lets clean sibling versions still resolve via backtracking. + if @unfiltered_specs[dep_package_name].empty? + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint) + self_term = Gem::PubGrub::Term.new(self_constraint, true) + # PubGrub's default InvalidDependency rendering drops the version + # requirement ("depends on unknown package bar"). Supply a custom + # explanation so the missing dependency's constraint is preserved + # ("depends on bar = 0.5 which could not be found in any repository"), + # matching Molinillo's diagnostics. + return [Gem::PubGrub::Incompatibility.new( + [self_term], + cause: cause, + custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository" + )] + end + + # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`). + if dep_constraint.range.empty? + return [Gem::Resolver::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true)], + cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint), + custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}" + )] + end - reqs << Gem::Resolver::DependencyRequest.new(d, act) - @stats.requirement! + Gem::PubGrub::Incompatibility.new( + [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)], + cause: :dependency + ) end + end + + ## + # Returns the gems in +specs+ that match the local platform. - @set.prefetch reqs + def select_local_platforms(specs) # :nodoc: + specs.select do |spec| + Gem::Platform.installable? spec + end + end - @stats.record_requirements reqs + private - reqs + def package_for(name) + @packages[name] ||= Gem::PubGrub::Package.new(name) end - include Molinillo::UI + def root_dependencies + deps = {} + @needed.each do |dep| + constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement) + deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint + end + deps + end - def output - @output ||= debug? ? $stdout : File.open(IO::NULL, "w") + # Only the min bound is inspected: `~>` synthesises a max like `X.A` + # whose suffix looks prerelease to Gem::Version but is not the user's + # intent, so checking max would mis-admit prereleases for every `~>`. + def range_admits_prerelease?(range) + range.ranges.any? do |r| + next false if r.empty? + r.min&.prerelease? + end end - def debug? - DEBUG_RESOLVER + def find_unfiltered_specs_for(name) + dep = Gem::Dependency.new(name, ">= 0.a") + dep_request = DependencyRequest.new(dep, nil) + @set.find_all(dep_request) end - include Molinillo::SpecificationProvider + def filter_specs(specs) + filtered = select_local_platforms(specs) - ## - # Proceed with resolution! Returns an array of ActivationRequest objects. + unless @soft_missing + filtered = filtered.select do |s| + s.required_ruby_version.satisfied_by?(Gem.ruby_version) && + s.required_rubygems_version.satisfied_by?(Gem.rubygems_version) + rescue StandardError + true + end + end - def resolve - locking_dg = Molinillo::DependencyGraph.new - Molinillo::Resolver.new(self, self).resolve(@needed.map {|d| DependencyRequest.new d, nil }, locking_dg).tsort.map(&:payload).compact - rescue Molinillo::VersionConflict => e - conflict = e.conflicts.values.first - raise Gem::DependencyResolutionError, Conflict.new(conflict.requirement_trees.first.first, conflict.existing, conflict.requirement) - ensure - @output.close if defined?(@output) && !debug? + filtered end - ## - # Extracts the specifications that may be able to fulfill +dependency+ and - # returns those that match the local platform and all those that match. + def spec_for(name, version) + @spec_for_cache[name][version] + end - def find_possible(dependency) # :nodoc: - all = @set.find_all dependency + def build_spec_for_cache(name) + # Rank sources by the order they were first supplied so that, when multiple + # sources offer the same version and platform, the earlier source wins. + source_rank = {} + @all_specs[name].each do |s| + source_rank[s.source] ||= source_rank.size + end - if (skip_dep_gems = skip_gems[dependency.name]) && !skip_dep_gems.empty? - matching = all.select do |api_spec| - skip_dep_gems.any? {|s| api_spec.version == s.version } - end + @all_specs[name].group_by(&:version).transform_values do |candidates| + next candidates.first if candidates.length == 1 - all = matching unless matching.empty? + # Prefer already-installed specs to avoid unnecessary downloads + installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) } + next installed.first if installed.length == 1 + candidates = installed if installed.any? + + # Among remaining candidates, prefer the most specific platform, then the + # earlier-supplied source. + candidates.min_by do |s| + [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local), + source_rank[s.source]] + end end + end - matching_platform = select_local_platforms all + def compute_dependencies(package, version) + spec = spec_for(package.to_s, version) + return {} unless spec + return {} if @ignore_dependencies - return matching_platform, all - end + spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies) - ## - # Returns the gems in +specs+ that match the local platform. + deps = {} + root_names = @needed.map(&:name) - def select_local_platforms(specs) # :nodoc: - specs.select do |spec| - Gem::Platform.installable? spec + spec.dependencies.each do |d| + next if d.name == package.to_s + next if d.type == :development && !@development + next if d.type == :development && @development_shallow && !root_names.include?(package.to_s) + + dep_package = package_for(d.name) + + # In force mode, skip deps that can't be satisfied - either no + # specs at all, or no specs matching the version requirement. + if @soft_missing + dep_specs = @all_specs[d.name] + matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) } + next if matching.empty? + end + + deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement) end + + deps end - def search_for(dependency) - possibles, all = find_possible(dependency) - if !@soft_missing && possibles.empty? - @missing << dependency - exc = Gem::UnsatisfiableDependencyError.new dependency, all - exc.errors = @set.errors - raise exc - end + def build_extended_explanation(name, constraint) + unfiltered = @unfiltered_specs[name] + return if unfiltered.empty? + + filtered = @all_specs[name] + pkg = package_for(name) - groups = Hash.new {|hash, key| hash[key] = [] } + # A prerelease hint applies when the source would strip prereleases for + # this constraint (global prerelease flag off and the constraint's range + # doesn't itself reach into prerelease territory) AND a prerelease of + # the gem exists somewhere. + prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) && + !range_admits_prerelease?(constraint.range) + has_prerelease_candidate = prerelease_gated && + @all_versions[pkg].any?(&:prerelease?) - # create groups & sources in the same loop - sources = possibles.map do |spec| - source = spec.source - groups[source] << spec - source - end.uniq.reverse + return if filtered.length == unfiltered.length && !has_prerelease_candidate - activation_requests = [] + hints = [] - sources.each do |source| - groups[source]. - sort_by {|spec| [spec.version, spec.platform =~ Gem::Platform.local ? 1 : 0] }. - map {|spec| ActivationRequest.new spec, dependency }. - each {|activation_request| activation_requests << activation_request } + # Check for specs that exist for other platforms + platform_specs = unfiltered.select do |s| + !Gem::Platform.installable?(s) && constraint.range.include?(s.version) + end + if platform_specs.any? + label = "#{name} (#{constraint.constraint_string})" + hints << "The source contains the following gems matching '#{label}':" + platform_specs.each do |s| + actual = s.respond_to?(:spec) ? s.spec : s + hints << " * #{actual.full_name}" + end end - activation_requests - end + # Check for specs filtered by Ruby version + installable = select_local_platforms(unfiltered) + ruby_specs = installable.select do |s| + actual = s.respond_to?(:spec) ? s.spec : s + constraint.range.include?(s.version) && + !actual.required_ruby_version.satisfied_by?(Gem.ruby_version) + rescue StandardError + false + end + if ruby_specs.any? + versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3) + sample = ruby_specs.find {|s| s.version == versions.first } + actual = sample.respond_to?(:spec) ? sample.spec : sample + ruby_req = actual.required_ruby_version + hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})" + end - def dependencies_for(specification) - return [] if @ignore_dependencies - spec = specification.spec - requests(spec, specification) + # Check for specs filtered by prerelease status + if prerelease_gated + prerelease_versions = @all_versions[pkg].select(&:prerelease?) + if prerelease_versions.any? + versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output + hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems." + end + end + + hints.empty? ? nil : hints.join("\n") end - def requirement_satisfied_by?(requirement, activated, spec) - matches_spec = requirement.matches_spec? spec - return matches_spec if @soft_missing + def extract_extended_explanation(incompatibility) + while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause) + cause = incompatibility.cause - matches_spec && - spec.spec.required_ruby_version.satisfied_by?(Gem.ruby_version) && - spec.spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version) - end + [cause.conflict, cause.other].each do |incompat| + if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) && + incompat.respond_to?(:extended_explanation) && + incompat.extended_explanation + return incompat.extended_explanation + end + end + + incompatibility = cause.conflict + end - def name_for(dependency) - dependency.name + nil end - def allow_missing?(dependency) - @missing << dependency - @soft_missing + def make_logger + DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new end - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by.with_index do |dependency, i| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - amount_constrained(dependency), - conflicts[name] ? 0 : 1, - activated.vertex_named(name).payload ? 0 : search_for(dependency).count, - i, # for stable sort - ] + # Custom root package so error messages say "your request depends on..." + # instead of PubGrub's default "root depends on...". + class RootPackage < Gem::PubGrub::Package + def initialize + super(:root) end - end - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY = 1_000_000 - private_constant :SINGLE_POSSIBILITY_CONSTRAINT_PENALTY if defined?(private_constant) + def root? + true + end - # returns an integer \in (-\infty, 0] - # a number closer to 0 means the dependency is less constraining - # - # dependencies w/ 0 or 1 possibilities (ignoring version requirements) - # are given very negative values, so they _always_ sort first, - # before dependencies that are unconstrained - def amount_constrained(dependency) - @amount_constrained ||= {} - @amount_constrained[dependency.name] ||= begin - name_dependency = Gem::Dependency.new(dependency.name) - dependency_request_for_name = Gem::Resolver::DependencyRequest.new(name_dependency, dependency.requester) - all = @set.find_all(dependency_request_for_name).size - - if all <= 1 - all - SINGLE_POSSIBILITY_CONSTRAINT_PENALTY - else - search = search_for(dependency).size - search - all - end + def to_s + "your request" end end - private :amount_constrained end require_relative "resolver/activation_request" -require_relative "resolver/conflict" require_relative "resolver/dependency_request" +require_relative "resolver/incompatibility" +require_relative "resolver/strategy" require_relative "resolver/requirement_list" -require_relative "resolver/stats" - require_relative "resolver/set" require_relative "resolver/api_set" require_relative "resolver/composed_set" diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb index 27877e0f4b..5c722001b1 100644 --- a/lib/rubygems/resolver/activation_request.rb +++ b/lib/rubygems/resolver/activation_request.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Specifies a Specification object that should be activated. Also contains a # dependency that was used to introduce this activation. @@ -58,10 +59,8 @@ class Gem::Resolver::ActivationRequest if @spec.respond_to? :sources exception = nil path = @spec.sources.find do |source| - begin - source.download full_spec, path - rescue exception - end + source.download full_spec, path + rescue exception end return path if path raise exception if exception @@ -93,9 +92,7 @@ class Gem::Resolver::ActivationRequest end def inspect # :nodoc: - "#<%s for %p from %s>" % [ - self.class, @spec, @request - ] + format("#<%s for %p from %s>", self.class, @spec, @request) end ## @@ -109,7 +106,7 @@ class Gem::Resolver::ActivationRequest this_spec = full_spec Gem::Specification.any? do |s| - s == this_spec + s == this_spec && s.base_dir == this_spec.base_dir end end end diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index f2bef54a9c..3f443519d8 100644 --- a/lib/rubygems/resolver/api_set.rb +++ b/lib/rubygems/resolver/api_set.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true + ## -# The global rubygems pool, available via the rubygems.org API. +# The global rubygems pool, available via the Compact Index API. # Returns instances of APISpecification. class Gem::Resolver::APISet < Gem::Resolver::Set autoload :GemParser, File.expand_path("api_set/gem_parser", __dir__) ## - # The URI for the dependency API this APISet uses. + # The URI for the Compact Index API this APISet uses. attr_reader :dep_uri # :nodoc: @@ -22,14 +23,14 @@ class Gem::Resolver::APISet < Gem::Resolver::Set attr_reader :uri ## - # Creates a new APISet that will retrieve gems from +uri+ using the RubyGems - # API URL +dep_uri+ which is described at - # https://guides.rubygems.org/rubygems-org-api + # Creates a new APISet that will retrieve gems from +uri+ using the Compact + # Index API URL +dep_uri+ which is described at + # https://guides.rubygems.org/rubygems-org-compact-index-api def initialize(dep_uri = "https://index.rubygems.org/info/") super() - dep_uri = URI dep_uri unless URI === dep_uri + dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri @dep_uri = dep_uri @uri = dep_uri + ".." @@ -75,7 +76,8 @@ class Gem::Resolver::APISet < Gem::Resolver::Set end def prefetch_now # :nodoc: - needed, @to_fetch = @to_fetch, [] + needed = @to_fetch + @to_fetch = [] needed.sort.each do |name| versions(name) @@ -102,16 +104,21 @@ class Gem::Resolver::APISet < Gem::Resolver::Set end uri = @dep_uri + name - str = Gem::RemoteFetcher.fetcher.fetch_path uri - lines(str).each do |ver| - number, platform, dependencies, requirements = parse_gem(ver) + begin + str = Gem::RemoteFetcher.fetcher.fetch_path uri + rescue Gem::RemoteFetcher::FetchError + @data[name] = [] + else + lines(str).each do |ver| + number, platform, dependencies, requirements = parse_gem(ver) - platform ||= "ruby" - dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] } - requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h + platform ||= "ruby" + dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] } + requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h - @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements } + @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements } + end end @data[name] diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 685c39558d..4d827f4980 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -4,17 +4,18 @@ class Gem::Resolver::APISet::GemParser def parse(line) version_and_platform, rest = line.split(" ", 2) version, platform = version_and_platform.split("-", 2) - dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest - dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : [] - requirements = requirements ? requirements.map {|d| parse_dependency(d) } : [] + dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest + dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : [] + requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : [] [version, platform, dependencies, requirements] end private def parse_dependency(string) - dependency = string.split(":") + dependency = string.split(":", 2) dependency[-1] = dependency[-1].split("&") if dependency.size > 1 + dependency[0] = -dependency[0] dependency end end diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb index 1e65d5e5a9..ccfd6fe084 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true + ## -# Represents a specification retrieved via the rubygems.org API. +# Represents a specification retrieved via the Compact Index API. # # This is used to avoid loading the full Specification object when all we need # is the name, version, and dependencies. @@ -18,10 +19,10 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification end ## - # Creates an APISpecification for the given +set+ from the rubygems.org + # Creates an APISpecification for the given +set+ from the Compact Index API # +api_data+. # - # See https://guides.rubygems.org/rubygems-org-api/#misc_methods for the + # See https://guides.rubygems.org/rubygems-org-compact-index-api for the # format of the +api_data+. def initialize(set, api_data) diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index 075ee1ef5c..e647a2c11b 100644 --- a/lib/rubygems/resolver/best_set.rb +++ b/lib/rubygems/resolver/best_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The BestSet chooses the best available method to query a remote index. # @@ -20,7 +21,7 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet def pick_sets # :nodoc: @sources.each_source do |source| - @sets << source.dependency_resolver_set + @sets << source.dependency_resolver_set(@prerelease) end end @@ -28,10 +29,6 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet pick_sets if @remote && @sets.empty? super - rescue Gem::RemoteFetcher::FetchError => e - replace_failed_api_set e - - retry end def prefetch(reqs) # :nodoc: @@ -49,28 +46,4 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet q.pp @sets end end - - ## - # Replaces a failed APISet for the URI in +error+ with an IndexSet. - # - # If no matching APISet can be found the original +error+ is raised. - # - # The calling method must retry the exception to repeat the lookup. - - def replace_failed_api_set(error) # :nodoc: - uri = error.original_uri - uri = URI uri unless URI === uri - uri = uri + "." - - raise error unless api_set = @sets.find do |set| - Gem::Resolver::APISet === set && set.dep_uri == uri - end - - index_set = Gem::Resolver::IndexSet.new api_set.source - - @sets.map! do |set| - next set unless set == api_set - index_set - end - end end diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb index 226da1e1e0..e67dd41754 100644 --- a/lib/rubygems/resolver/composed_set.rb +++ b/lib/rubygems/resolver/composed_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A ComposedSet allows multiple sets to be queried like a single set. # @@ -43,16 +44,16 @@ class Gem::Resolver::ComposedSet < Gem::Resolver::Set end def errors - @errors + @sets.map {|set| set.errors }.flatten + @errors + @sets.flat_map(&:errors) end ## # Finds all specs matching +req+ in all sets. def find_all(req) - @sets.map do |s| + @sets.flat_map do |s| s.find_all req - end.flatten + end end ## diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb deleted file mode 100644 index aba6d73ea7..0000000000 --- a/lib/rubygems/resolver/conflict.rb +++ /dev/null @@ -1,153 +0,0 @@ -# frozen_string_literal: true -## -# Used internally to indicate that a dependency conflicted -# with a spec that would be activated. - -class Gem::Resolver::Conflict - ## - # The specification that was activated prior to the conflict - - attr_reader :activated - - ## - # The dependency that is in conflict with the activated gem. - - attr_reader :dependency - - attr_reader :failed_dep # :nodoc: - - ## - # Creates a new resolver conflict when +dependency+ is in conflict with an - # already +activated+ specification. - - def initialize(dependency, activated, failed_dep=dependency) - @dependency = dependency - @activated = activated - @failed_dep = failed_dep - end - - def ==(other) # :nodoc: - self.class === other && - @dependency == other.dependency && - @activated == other.activated && - @failed_dep == other.failed_dep - end - - ## - # A string explanation of the conflict. - - def explain - "<Conflict wanted: #{@failed_dep}, had: #{activated.spec.full_name}>" - end - - ## - # Return the 2 dependency objects that conflicted - - def conflicting_dependencies - [@failed_dep.dependency, @activated.request.dependency] - end - - ## - # Explanation of the conflict used by exceptions to print useful messages - - def explanation - activated = @activated.spec.full_name - dependency = @failed_dep.dependency - requirement = dependency.requirement - alternates = dependency.matching_specs.map {|spec| spec.full_name } - - unless alternates.empty? - matching = <<-MATCHING.chomp - - Gems matching %s: - %s - MATCHING - - matching = matching % [ - dependency, - alternates.join(", "), - ] - end - - explanation = <<-EXPLANATION - Activated %s - which does not match conflicting dependency (%s) - - Conflicting dependency chains: - %s - - versus: - %s -%s - EXPLANATION - - explanation % [ - activated, requirement, - request_path(@activated).reverse.join(", depends on\n "), - request_path(@failed_dep).reverse.join(", depends on\n "), - matching - ] - end - - ## - # Returns true if the conflicting dependency's name matches +spec+. - - def for_spec?(spec) - @dependency.name == spec.name - end - - def pretty_print(q) # :nodoc: - q.group 2, "[Dependency conflict: ", "]" do - q.breakable - - q.text "activated " - q.pp @activated - - q.breakable - q.text " dependency " - q.pp @dependency - - q.breakable - if @dependency == @failed_dep - q.text " failed" - else - q.text " failed dependency " - q.pp @failed_dep - end - end - end - - ## - # Path of activations from the +current+ list. - - def request_path(current) - path = [] - - while current do - case current - when Gem::Resolver::ActivationRequest then - path << - "#{current.request.dependency}, #{current.spec.version} activated" - - current = current.parent - when Gem::Resolver::DependencyRequest then - path << "#{current.dependency}" - - current = current.requester - else - raise Gem::Exception, "[BUG] unknown request class #{current.class}" - end - end - - path = ["user request (gem command or Gemfile)"] if path.empty? - - path - end - - ## - # Return the Specification that listed the dependency - - def requester - @failed_dep.requester - end -end diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb index c3aa3a2c37..370e445089 100644 --- a/lib/rubygems/resolver/current_set.rb +++ b/lib/rubygems/resolver/current_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A set which represents the installed gems. Respects # all the normal settings that control where to look diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb index 70a61cbc25..60b338277f 100644 --- a/lib/rubygems/resolver/dependency_request.rb +++ b/lib/rubygems/resolver/dependency_request.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Used Internally. Wraps a Dependency object to also track which spec # contained the Dependency. diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb index f010273a8f..2912378fe7 100644 --- a/lib/rubygems/resolver/git_set.rb +++ b/lib/rubygems/resolver/git_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A GitSet represents gems that are sourced from git repositories. # @@ -35,7 +36,6 @@ class Gem::Resolver::GitSet < Gem::Resolver::Set def initialize # :nodoc: super() - @git = ENV["git"] || "git" @need_submodules = {} @repositories = {} @root_dir = Gem.dir diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb index 6a178ea82e..e587c17d2a 100644 --- a/lib/rubygems/resolver/git_specification.rb +++ b/lib/rubygems/resolver/git_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A GitSpecification represents a gem that is sourced from a git repository # and is being loaded through a gem dependencies file through the +git:+ diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb new file mode 100644 index 0000000000..57a60affb4 --- /dev/null +++ b/lib/rubygems/resolver/incompatibility.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility + attr_reader :extended_explanation + + def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil) + @extended_explanation = extended_explanation + super(terms, cause: cause, custom_explanation: custom_explanation) + end +end diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb index 2344178314..cddaf8773f 100644 --- a/lib/rubygems/resolver/index_set.rb +++ b/lib/rubygems/resolver/index_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The global rubygems pool represented via the traditional # source index. @@ -43,10 +44,10 @@ class Gem::Resolver::IndexSet < Gem::Resolver::Set name = req.dependency.name @all[name].each do |uri, n| - if req.match? n, @prerelease - res << Gem::Resolver::IndexSpecification.new( - self, n.name, n.version, uri, n.platform) - end + next unless req.match? n, @prerelease + res << Gem::Resolver::IndexSpecification.new( + self, n.name, n.version, uri, n.platform + ) end res @@ -64,11 +65,11 @@ class Gem::Resolver::IndexSet < Gem::Resolver::Set q.breakable - names = @all.values.map do |tuples| + names = @all.values.flat_map do |tuples| tuples.map do |_, tuple| tuple.full_name end - end.flatten + end q.seplist names do |name| q.text name diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb index 0fc758dfd3..7b95608071 100644 --- a/lib/rubygems/resolver/index_specification.rb +++ b/lib/rubygems/resolver/index_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Represents a possible Specification object returned from IndexSet. Used to # delay needed to download full Specification objects when only the +name+ @@ -67,7 +68,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification end def inspect # :nodoc: - "#<%s %s source %s>" % [self.class, full_name, @source] + format("#<%s %s source %s>", self.class, full_name, @source) end def pretty_print(q) # :nodoc: @@ -75,7 +76,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification q.breakable q.text full_name - unless Gem::Platform::RUBY == @platform + unless @platform == Gem::Platform::RUBY q.breakable q.text @platform.to_s end diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb index 8932e068be..8280ae4672 100644 --- a/lib/rubygems/resolver/installed_specification.rb +++ b/lib/rubygems/resolver/installed_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # An InstalledSpecification represents a gem that is already installed # locally. @@ -24,7 +25,7 @@ class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification def installable_platform? # BACKCOMPAT If the file is coming out of a specified file, then we # ignore the platform. This code can be removed in RG 3.0. - return true if @source.kind_of? Gem::Source::SpecificFile + return true if @source.is_a? Gem::Source::SpecificFile super end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index 5e18be50ef..42ce0890e2 100644 --- a/lib/rubygems/resolver/installer_set.rb +++ b/lib/rubygems/resolver/installer_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A set of gems for installation sourced from remote sources and local .gem # files @@ -147,6 +148,8 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set res << Gem::Resolver::InstalledSpecification.new(self, gemspec) end unless @ignore_installed + matching_local = [] + if consider_local? matching_local = @local.values.select do |spec, _| req.match? spec @@ -157,17 +160,18 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set res.concat matching_local begin - if local_spec = @local_source.find_gem(name, dep.requirement) + @local_source.find_all_gems(name, dep.requirement).each do |local_spec| res << Gem::Resolver::IndexSpecification.new( self, local_spec.name, local_spec.version, - @local_source, local_spec.platform) + @local_source, local_spec.platform + ) end rescue Gem::Package::FormatError # ignore end end - res.concat @remote_set.find_all req if consider_remote? + res.concat @remote_set.find_all req if consider_remote? && matching_local.empty? res end @@ -183,11 +187,9 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set end def inspect # :nodoc: - always_install = @always_install.map {|s| s.full_name } + always_install = @always_install.map(&:full_name) - "#<%s domain: %s specs: %p always install: %p>" % [ - self.class, @domain, @specs.keys, always_install - ] + format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install) end ## @@ -261,7 +263,7 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set unless rrgv.satisfied_by? Gem.rubygems_version rg_version = Gem::VERSION raise Gem::RuntimeRequirementNotMetError, - "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " + + "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " \ "Try 'gem update --system' to update RubyGems itself." end end diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb index c27bab0f5a..b57d40e795 100644 --- a/lib/rubygems/resolver/local_specification.rb +++ b/lib/rubygems/resolver/local_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A LocalSpecification comes from a .gem file on the local filesystem. @@ -7,7 +8,7 @@ class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification # Returns +true+ if this gem is installable for the current platform. def installable_platform? - return true if @source.kind_of? Gem::Source::SpecificFile + return true if @source.is_a? Gem::Source::SpecificFile super end diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb index b1a5433cb5..e5ee32a9a6 100644 --- a/lib/rubygems/resolver/lock_set.rb +++ b/lib/rubygems/resolver/lock_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A set of gems from a gem dependencies lockfile. @@ -74,7 +75,7 @@ class Gem::Resolver::LockSet < Gem::Resolver::Set q.text "specs:" q.breakable - q.pp @specs.map {|spec| spec.full_name } + q.pp @specs.map(&:full_name) end end end diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb index 7de2a14658..06f912dd85 100644 --- a/lib/rubygems/resolver/lock_specification.rb +++ b/lib/rubygems/resolver/lock_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile). # diff --git a/lib/rubygems/resolver/molinillo.rb b/lib/rubygems/resolver/molinillo.rb deleted file mode 100644 index e154342571..0000000000 --- a/lib/rubygems/resolver/molinillo.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -require_relative "molinillo/lib/molinillo" diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo.rb b/lib/rubygems/resolver/molinillo/lib/molinillo.rb deleted file mode 100644 index f67badbde7..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -require_relative 'molinillo/gem_metadata' -require_relative 'molinillo/errors' -require_relative 'molinillo/resolver' -require_relative 'molinillo/modules/ui' -require_relative 'molinillo/modules/specification_provider' - -# Gem::Resolver::Molinillo is a generic dependency resolution algorithm. -module Gem::Resolver::Molinillo -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb deleted file mode 100644 index d540d3baff..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/resolution_state.rb +++ /dev/null @@ -1,57 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # @!visibility private - module Delegates - # Delegates all {Gem::Resolver::Molinillo::ResolutionState} methods to a `#state` property. - module ResolutionState - # (see Gem::Resolver::Molinillo::ResolutionState#name) - def name - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.name - end - - # (see Gem::Resolver::Molinillo::ResolutionState#requirements) - def requirements - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.requirements - end - - # (see Gem::Resolver::Molinillo::ResolutionState#activated) - def activated - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.activated - end - - # (see Gem::Resolver::Molinillo::ResolutionState#requirement) - def requirement - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.requirement - end - - # (see Gem::Resolver::Molinillo::ResolutionState#possibilities) - def possibilities - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.possibilities - end - - # (see Gem::Resolver::Molinillo::ResolutionState#depth) - def depth - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.depth - end - - # (see Gem::Resolver::Molinillo::ResolutionState#conflicts) - def conflicts - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.conflicts - end - - # (see Gem::Resolver::Molinillo::ResolutionState#unused_unwind_options) - def unused_unwind_options - current_state = state || Gem::Resolver::Molinillo::ResolutionState.empty - current_state.unused_unwind_options - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb deleted file mode 100644 index b765226fb0..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - module Delegates - # Delegates all {Gem::Resolver::Molinillo::SpecificationProvider} methods to a - # `#specification_provider` property. - module SpecificationProvider - # (see Gem::Resolver::Molinillo::SpecificationProvider#search_for) - def search_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.search_for(dependency) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#dependencies_for) - def dependencies_for(specification) - with_no_such_dependency_error_handling do - specification_provider.dependencies_for(specification) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#requirement_satisfied_by?) - def requirement_satisfied_by?(requirement, activated, spec) - with_no_such_dependency_error_handling do - specification_provider.requirement_satisfied_by?(requirement, activated, spec) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#dependencies_equal?) - def dependencies_equal?(dependencies, other_dependencies) - with_no_such_dependency_error_handling do - specification_provider.dependencies_equal?(dependencies, other_dependencies) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#name_for) - def name_for(dependency) - with_no_such_dependency_error_handling do - specification_provider.name_for(dependency) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#name_for_explicit_dependency_source) - def name_for_explicit_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_explicit_dependency_source - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#name_for_locking_dependency_source) - def name_for_locking_dependency_source - with_no_such_dependency_error_handling do - specification_provider.name_for_locking_dependency_source - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#sort_dependencies) - def sort_dependencies(dependencies, activated, conflicts) - with_no_such_dependency_error_handling do - specification_provider.sort_dependencies(dependencies, activated, conflicts) - end - end - - # (see Gem::Resolver::Molinillo::SpecificationProvider#allow_missing?) - def allow_missing?(dependency) - with_no_such_dependency_error_handling do - specification_provider.allow_missing?(dependency) - end - end - - private - - # Ensures any raised {NoSuchDependencyError} has its - # {NoSuchDependencyError#required_by} set. - # @yield - def with_no_such_dependency_error_handling - yield - rescue NoSuchDependencyError => error - if state - vertex = activated.vertex_named(name_for(error.dependency)) - error.required_by += vertex.incoming_edges.map { |e| e.origin.name } - error.required_by << name_for_explicit_dependency_source unless vertex.explicit_requirements.empty? - end - raise - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph.rb deleted file mode 100644 index 731a9e3e90..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph.rb +++ /dev/null @@ -1,255 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../../tsort' - -require_relative 'dependency_graph/log' -require_relative 'dependency_graph/vertex' - -module Gem::Resolver::Molinillo - # A directed acyclic graph that is tuned to hold named dependencies - class DependencyGraph - include Enumerable - - # Enumerates through the vertices of the graph. - # @return [Array<Vertex>] The graph's vertices. - def each - return vertices.values.each unless block_given? - vertices.values.each { |v| yield v } - end - - include Gem::TSort - - # @!visibility private - alias tsort_each_node each - - # @!visibility private - def tsort_each_child(vertex, &block) - vertex.successors.each(&block) - end - - # Topologically sorts the given vertices. - # @param [Enumerable<Vertex>] vertices the vertices to be sorted, which must - # all belong to the same graph. - # @return [Array<Vertex>] The sorted vertices. - def self.tsort(vertices) - Gem::TSort.tsort( - lambda { |b| vertices.each(&b) }, - lambda { |v, &b| (v.successors & vertices).each(&b) } - ) - end - - # A directed edge of a {DependencyGraph} - # @attr [Vertex] origin The origin of the directed edge - # @attr [Vertex] destination The destination of the directed edge - # @attr [Object] requirement The requirement the directed edge represents - Edge = Struct.new(:origin, :destination, :requirement) - - # @return [{String => Vertex}] the vertices of the dependency graph, keyed - # by {Vertex#name} - attr_reader :vertices - - # @return [Log] the op log for this graph - attr_reader :log - - # Initializes an empty dependency graph - def initialize - @vertices = {} - @log = Log.new - end - - # Tags the current state of the dependency as the given tag - # @param [Object] tag an opaque tag for the current state of the graph - # @return [Void] - def tag(tag) - log.tag(self, tag) - end - - # Rewinds the graph to the state tagged as `tag` - # @param [Object] tag the tag to rewind to - # @return [Void] - def rewind_to(tag) - log.rewind_to(self, tag) - end - - # Initializes a copy of a {DependencyGraph}, ensuring that all {#vertices} - # are properly copied. - # @param [DependencyGraph] other the graph to copy. - def initialize_copy(other) - super - @vertices = {} - @log = other.log.dup - traverse = lambda do |new_v, old_v| - return if new_v.outgoing_edges.size == old_v.outgoing_edges.size - old_v.outgoing_edges.each do |edge| - destination = add_vertex(edge.destination.name, edge.destination.payload) - add_edge_no_circular(new_v, destination, edge.requirement) - traverse.call(destination, edge.destination) - end - end - other.vertices.each do |name, vertex| - new_vertex = add_vertex(name, vertex.payload, vertex.root?) - new_vertex.explicit_requirements.replace(vertex.explicit_requirements) - traverse.call(new_vertex, vertex) - end - end - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{vertices.values.inspect}" - end - - # @param [Hash] options options for dot output. - # @return [String] Returns a dot format representation of the graph - def to_dot(options = {}) - edge_label = options.delete(:edge_label) - raise ArgumentError, "Unknown options: #{options.keys}" unless options.empty? - - dot_vertices = [] - dot_edges = [] - vertices.each do |n, v| - dot_vertices << " #{n} [label=\"{#{n}|#{v.payload}}\"]" - v.outgoing_edges.each do |e| - label = edge_label ? edge_label.call(e) : e.requirement - dot_edges << " #{e.origin.name} -> #{e.destination.name} [label=#{label.to_s.dump}]" - end - end - - dot_vertices.uniq! - dot_vertices.sort! - dot_edges.uniq! - dot_edges.sort! - - dot = dot_vertices.unshift('digraph G {').push('') + dot_edges.push('}') - dot.join("\n") - end - - # @param [DependencyGraph] other - # @return [Boolean] whether the two dependency graphs are equal, determined - # by a recursive traversal of each {#root_vertices} and its - # {Vertex#successors} - def ==(other) - return false unless other - return true if equal?(other) - vertices.each do |name, vertex| - other_vertex = other.vertex_named(name) - return false unless other_vertex - return false unless vertex.payload == other_vertex.payload - return false unless other_vertex.successors.to_set == vertex.successors.to_set - end - end - - # @param [String] name - # @param [Object] payload - # @param [Array<String>] parent_names - # @param [Object] requirement the requirement that is requiring the child - # @return [void] - def add_child_vertex(name, payload, parent_names, requirement) - root = !parent_names.delete(nil) { true } - vertex = add_vertex(name, payload, root) - vertex.explicit_requirements << requirement if root - parent_names.each do |parent_name| - parent_vertex = vertex_named(parent_name) - add_edge(parent_vertex, vertex, requirement) - end - vertex - end - - # Adds a vertex with the given name, or updates the existing one. - # @param [String] name - # @param [Object] payload - # @return [Vertex] the vertex that was added to `self` - def add_vertex(name, payload, root = false) - log.add_vertex(self, name, payload, root) - end - - # Detaches the {#vertex_named} `name` {Vertex} from the graph, recursively - # removing any non-root vertices that were orphaned in the process - # @param [String] name - # @return [Array<Vertex>] the vertices which have been detached - def detach_vertex_named(name) - log.detach_vertex_named(self, name) - end - - # @param [String] name - # @return [Vertex,nil] the vertex with the given name - def vertex_named(name) - vertices[name] - end - - # @param [String] name - # @return [Vertex,nil] the root vertex with the given name - def root_vertex_named(name) - vertex = vertex_named(name) - vertex if vertex && vertex.root? - end - - # Adds a new {Edge} to the dependency graph - # @param [Vertex] origin - # @param [Vertex] destination - # @param [Object] requirement the requirement that this edge represents - # @return [Edge] the added edge - def add_edge(origin, destination, requirement) - if destination.path_to?(origin) - raise CircularDependencyError.new(path(destination, origin)) - end - add_edge_no_circular(origin, destination, requirement) - end - - # Deletes an {Edge} from the dependency graph - # @param [Edge] edge - # @return [Void] - def delete_edge(edge) - log.delete_edge(self, edge.origin.name, edge.destination.name, edge.requirement) - end - - # Sets the payload of the vertex with the given name - # @param [String] name the name of the vertex - # @param [Object] payload the payload - # @return [Void] - def set_payload(name, payload) - log.set_payload(self, name, payload) - end - - private - - # Adds a new {Edge} to the dependency graph without checking for - # circularity. - # @param (see #add_edge) - # @return (see #add_edge) - def add_edge_no_circular(origin, destination, requirement) - log.add_edge_no_circular(self, origin.name, destination.name, requirement) - end - - # Returns the path between two vertices - # @raise [ArgumentError] if there is no path between the vertices - # @param [Vertex] from - # @param [Vertex] to - # @return [Array<Vertex>] the shortest path from `from` to `to` - def path(from, to) - distances = Hash.new(vertices.size + 1) - distances[from.name] = 0 - predecessors = {} - each do |vertex| - vertex.successors.each do |successor| - if distances[successor.name] > distances[vertex.name] + 1 - distances[successor.name] = distances[vertex.name] + 1 - predecessors[successor] = vertex - end - end - end - - path = [to] - while before = predecessors[to] - path << before - to = before - break if to == from - end - - unless path.last.equal?(from) - raise ArgumentError, "There is no path from #{from.name} to #{to.name}" - end - - path.reverse - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/action.rb deleted file mode 100644 index cc140031b3..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/action.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - class DependencyGraph - # An action that modifies a {DependencyGraph} that is reversible. - # @abstract - class Action - # rubocop:disable Lint/UnusedMethodArgument - - # @return [Symbol] The name of the action. - def self.action_name - raise 'Abstract' - end - - # Performs the action on the given graph. - # @param [DependencyGraph] graph the graph to perform the action on. - # @return [Void] - def up(graph) - raise 'Abstract' - end - - # Reverses the action on the given graph. - # @param [DependencyGraph] graph the graph to reverse the action on. - # @return [Void] - def down(graph) - raise 'Abstract' - end - - # @return [Action,Nil] The previous action - attr_accessor :previous - - # @return [Action,Nil] The next action - attr_accessor :next - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb deleted file mode 100644 index 5570483253..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_edge_no_circular) - class AddEdgeNoCircular < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - delete_first(edge.origin.outgoing_edges, edge) - delete_first(edge.destination.incoming_edges, edge) - end - - # @!group AddEdgeNoCircular - - # @return [String] the name of the origin of the edge - attr_reader :origin - - # @return [String] the name of the destination of the edge - attr_reader :destination - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new(graph.vertex_named(origin), graph.vertex_named(destination), requirement) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin the name of the origin of the edge - # @param [String] destination the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin, destination, requirement) - @origin = origin - @destination = destination - @requirement = requirement - end - - private - - def delete_first(array, item) - return unless index = array.index(item) - array.delete_at(index) - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_vertex.rb deleted file mode 100644 index f1411d5efa..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ /dev/null @@ -1,62 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#add_vertex) - class AddVertex < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - if existing = graph.vertices[name] - @existing_payload = existing.payload - @existing_root = existing.root - end - vertex = existing || Vertex.new(name, payload) - graph.vertices[vertex.name] = vertex - vertex.payload ||= payload - vertex.root ||= root - vertex - end - - # (see Action#down) - def down(graph) - if defined?(@existing_payload) - vertex = graph.vertices[name] - vertex.payload = @existing_payload - vertex.root = @existing_root - else - graph.vertices.delete(name) - end - end - - # @!group AddVertex - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # @return [Boolean] whether the vertex is root or not - attr_reader :root - - # Initialize an action to add a vertex to a dependency graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - # @param [Boolean] root whether the vertex is root or not - def initialize(name, payload, root) - @name = name - @payload = payload - @root = root - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/delete_edge.rb deleted file mode 100644 index 3b48d77a50..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # (see DependencyGraph#delete_edge) - class DeleteEdge < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :delete_edge - end - - # (see Action#up) - def up(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges.delete(edge) - edge.destination.incoming_edges.delete(edge) - end - - # (see Action#down) - def down(graph) - edge = make_edge(graph) - edge.origin.outgoing_edges << edge - edge.destination.incoming_edges << edge - edge - end - - # @!group DeleteEdge - - # @return [String] the name of the origin of the edge - attr_reader :origin_name - - # @return [String] the name of the destination of the edge - attr_reader :destination_name - - # @return [Object] the requirement that the edge represents - attr_reader :requirement - - # @param [DependencyGraph] graph the graph to find vertices from - # @return [Edge] The edge this action adds - def make_edge(graph) - Edge.new( - graph.vertex_named(origin_name), - graph.vertex_named(destination_name), - requirement - ) - end - - # Initialize an action to add an edge to a dependency graph - # @param [String] origin_name the name of the origin of the edge - # @param [String] destination_name the name of the destination of the edge - # @param [Object] requirement the requirement that the edge represents - def initialize(origin_name, destination_name, requirement) - @origin_name = origin_name - @destination_name = destination_name - @requirement = requirement - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb deleted file mode 100644 index 92f60d5be8..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#detach_vertex_named - class DetachVertexNamed < Action - # @!group Action - - # (see Action#name) - def self.action_name - :add_vertex - end - - # (see Action#up) - def up(graph) - return [] unless @vertex = graph.vertices.delete(name) - - removed_vertices = [@vertex] - @vertex.outgoing_edges.each do |e| - v = e.destination - v.incoming_edges.delete(e) - if !v.root? && v.incoming_edges.empty? - removed_vertices.concat graph.detach_vertex_named(v.name) - end - end - - @vertex.incoming_edges.each do |e| - v = e.origin - v.outgoing_edges.delete(e) - end - - removed_vertices - end - - # (see Action#down) - def down(graph) - return unless @vertex - graph.vertices[@vertex.name] = @vertex - @vertex.outgoing_edges.each do |e| - e.destination.incoming_edges << e - end - @vertex.incoming_edges.each do |e| - e.origin.outgoing_edges << e - end - end - - # @!group DetachVertexNamed - - # @return [String] the name of the vertex to detach - attr_reader :name - - # Initialize an action to detach a vertex from a dependency graph - # @param [String] name the name of the vertex to detach - def initialize(name) - @name = name - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/log.rb deleted file mode 100644 index 7aeb8847ec..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/log.rb +++ /dev/null @@ -1,126 +0,0 @@ -# frozen_string_literal: true - -require_relative 'add_edge_no_circular' -require_relative 'add_vertex' -require_relative 'delete_edge' -require_relative 'detach_vertex_named' -require_relative 'set_payload' -require_relative 'tag' - -module Gem::Resolver::Molinillo - class DependencyGraph - # A log for dependency graph actions - class Log - # Initializes an empty log - def initialize - @current_action = @first_action = nil - end - - # @!macro [new] action - # {include:DependencyGraph#$0} - # @param [Graph] graph the graph to perform the action on - # @param (see DependencyGraph#$0) - # @return (see DependencyGraph#$0) - - # @macro action - def tag(graph, tag) - push_action(graph, Tag.new(tag)) - end - - # @macro action - def add_vertex(graph, name, payload, root) - push_action(graph, AddVertex.new(name, payload, root)) - end - - # @macro action - def detach_vertex_named(graph, name) - push_action(graph, DetachVertexNamed.new(name)) - end - - # @macro action - def add_edge_no_circular(graph, origin, destination, requirement) - push_action(graph, AddEdgeNoCircular.new(origin, destination, requirement)) - end - - # {include:DependencyGraph#delete_edge} - # @param [Graph] graph the graph to perform the action on - # @param [String] origin_name - # @param [String] destination_name - # @param [Object] requirement - # @return (see DependencyGraph#delete_edge) - def delete_edge(graph, origin_name, destination_name, requirement) - push_action(graph, DeleteEdge.new(origin_name, destination_name, requirement)) - end - - # @macro action - def set_payload(graph, name, payload) - push_action(graph, SetPayload.new(name, payload)) - end - - # Pops the most recent action from the log and undoes the action - # @param [DependencyGraph] graph - # @return [Action] the action that was popped off the log - def pop!(graph) - return unless action = @current_action - unless @current_action = action.previous - @first_action = nil - end - action.down(graph) - action - end - - extend Enumerable - - # @!visibility private - # Enumerates each action in the log - # @yield [Action] - def each - return enum_for unless block_given? - action = @first_action - loop do - break unless action - yield action - action = action.next - end - self - end - - # @!visibility private - # Enumerates each action in the log in reverse order - # @yield [Action] - def reverse_each - return enum_for(:reverse_each) unless block_given? - action = @current_action - loop do - break unless action - yield action - action = action.previous - end - self - end - - # @macro action - def rewind_to(graph, tag) - loop do - action = pop!(graph) - raise "No tag #{tag.inspect} found" unless action - break if action.class.action_name == :tag && action.tag == tag - end - end - - private - - # Adds the given action to the log, running the action - # @param [DependencyGraph] graph - # @param [Action] action - # @return The value returned by `action.up` - def push_action(graph, action) - action.previous = @current_action - @current_action.next = action if @current_action - @current_action = action - @first_action ||= action - action.up(graph) - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/set_payload.rb deleted file mode 100644 index 726292a2c3..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#set_payload - class SetPayload < Action # :nodoc: - # @!group Action - - # (see Action.action_name) - def self.action_name - :set_payload - end - - # (see Action#up) - def up(graph) - vertex = graph.vertex_named(name) - @old_payload = vertex.payload - vertex.payload = payload - end - - # (see Action#down) - def down(graph) - graph.vertex_named(name).payload = @old_payload - end - - # @!group SetPayload - - # @return [String] the name of the vertex - attr_reader :name - - # @return [Object] the payload for the vertex - attr_reader :payload - - # Initialize an action to add set the payload for a vertex in a dependency - # graph - # @param [String] name the name of the vertex - # @param [Object] payload the payload for the vertex - def initialize(name, payload) - @name = name - @payload = payload - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/tag.rb deleted file mode 100644 index bfe6fd31f8..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/tag.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require_relative 'action' -module Gem::Resolver::Molinillo - class DependencyGraph - # @!visibility private - # @see DependencyGraph#tag - class Tag < Action - # @!group Action - - # (see Action.action_name) - def self.action_name - :tag - end - - # (see Action#up) - def up(graph) - end - - # (see Action#down) - def down(graph) - end - - # @!group Tag - - # @return [Object] An opaque tag - attr_reader :tag - - # Initialize an action to tag a state of a dependency graph - # @param [Object] tag an opaque tag - def initialize(tag) - @tag = tag - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/vertex.rb deleted file mode 100644 index 77114951b2..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ /dev/null @@ -1,164 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - class DependencyGraph - # A vertex in a {DependencyGraph} that encapsulates a {#name} and a - # {#payload} - class Vertex - # @return [String] the name of the vertex - attr_accessor :name - - # @return [Object] the payload the vertex holds - attr_accessor :payload - - # @return [Array<Object>] the explicit requirements that required - # this vertex - attr_reader :explicit_requirements - - # @return [Boolean] whether the vertex is considered a root vertex - attr_accessor :root - alias root? root - - # Initializes a vertex with the given name and payload. - # @param [String] name see {#name} - # @param [Object] payload see {#payload} - def initialize(name, payload) - @name = name.frozen? ? name : name.dup.freeze - @payload = payload - @explicit_requirements = [] - @outgoing_edges = [] - @incoming_edges = [] - end - - # @return [Array<Object>] all of the requirements that required - # this vertex - def requirements - (incoming_edges.map(&:requirement) + explicit_requirements).uniq - end - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#origin} - attr_accessor :outgoing_edges - - # @return [Array<Edge>] the edges of {#graph} that have `self` as their - # {Edge#destination} - attr_accessor :incoming_edges - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#destination} - def predecessors - incoming_edges.map(&:origin) - end - - # @return [Set<Vertex>] the vertices of {#graph} where `self` is a - # {#descendent?} - def recursive_predecessors - _recursive_predecessors - end - - # @param [Set<Vertex>] vertices the set to add the predecessors to - # @return [Set<Vertex>] the vertices of {#graph} where `self` is a - # {#descendent?} - def _recursive_predecessors(vertices = new_vertex_set) - incoming_edges.each do |edge| - vertex = edge.origin - next unless vertices.add?(vertex) - vertex._recursive_predecessors(vertices) - end - - vertices - end - protected :_recursive_predecessors - - # @return [Array<Vertex>] the vertices of {#graph} that have an edge with - # `self` as their {Edge#origin} - def successors - outgoing_edges.map(&:destination) - end - - # @return [Set<Vertex>] the vertices of {#graph} where `self` is an - # {#ancestor?} - def recursive_successors - _recursive_successors - end - - # @param [Set<Vertex>] vertices the set to add the successors to - # @return [Set<Vertex>] the vertices of {#graph} where `self` is an - # {#ancestor?} - def _recursive_successors(vertices = new_vertex_set) - outgoing_edges.each do |edge| - vertex = edge.destination - next unless vertices.add?(vertex) - vertex._recursive_successors(vertices) - end - - vertices - end - protected :_recursive_successors - - # @return [String] a string suitable for debugging - def inspect - "#{self.class}:#{name}(#{payload.inspect})" - end - - # @return [Boolean] whether the two vertices are equal, determined - # by a recursive traversal of each {Vertex#successors} - def ==(other) - return true if equal?(other) - shallow_eql?(other) && - successors.to_set == other.successors.to_set - end - - # @param [Vertex] other the other vertex to compare to - # @return [Boolean] whether the two vertices are equal, determined - # solely by {#name} and {#payload} equality - def shallow_eql?(other) - return true if equal?(other) - other && - name == other.name && - payload == other.payload - end - - alias eql? == - - # @return [Fixnum] a hash for the vertex based upon its {#name} - def hash - name.hash - end - - # Is there a path from `self` to `other` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def path_to?(other) - _path_to?(other) - end - - alias descendent? path_to? - - # @param [Vertex] other the vertex to check if there's a path to - # @param [Set<Vertex>] visited the vertices of {#graph} that have been visited - # @return [Boolean] whether there is a path to `other` from `self` - def _path_to?(other, visited = new_vertex_set) - return false unless visited.add?(self) - return true if equal?(other) - successors.any? { |v| v._path_to?(other, visited) } - end - protected :_path_to? - - # Is there a path from `other` to `self` following edges in the - # dependency graph? - # @return whether there is a path following edges within this {#graph} - def ancestor?(other) - other.path_to?(self) - end - - alias is_reachable_from? ancestor? - - def new_vertex_set - require 'set' - Set.new - end - private :new_vertex_set - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/errors.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/errors.rb deleted file mode 100644 index 4289902828..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/errors.rb +++ /dev/null @@ -1,149 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # An error that occurred during the resolution process - class ResolverError < StandardError; end - - # An error caused by searching for a dependency that is completely unknown, - # i.e. has no versions available whatsoever. - class NoSuchDependencyError < ResolverError - # @return [Object] the dependency that could not be found - attr_accessor :dependency - - # @return [Array<Object>] the specifications that depended upon {#dependency} - attr_accessor :required_by - - # Initializes a new error with the given missing dependency. - # @param [Object] dependency @see {#dependency} - # @param [Array<Object>] required_by @see {#required_by} - def initialize(dependency, required_by = []) - @dependency = dependency - @required_by = required_by.uniq - super() - end - - # The error message for the missing dependency, including the specifications - # that had this dependency. - def message - sources = required_by.map { |r| "`#{r}`" }.join(' and ') - message = "Unable to find a specification for `#{dependency}`" - message += " depended upon by #{sources}" unless sources.empty? - message - end - end - - # An error caused by attempting to fulfil a dependency that was circular - # - # @note This exception will be thrown if and only if a {Vertex} is added to a - # {DependencyGraph} that has a {DependencyGraph::Vertex#path_to?} an - # existing {DependencyGraph::Vertex} - class CircularDependencyError < ResolverError - # [Set<Object>] the dependencies responsible for causing the error - attr_reader :dependencies - - # Initializes a new error with the given circular vertices. - # @param [Array<DependencyGraph::Vertex>] vertices the vertices in the dependency - # that caused the error - def initialize(vertices) - super "There is a circular dependency between #{vertices.map(&:name).join(' and ')}" - @dependencies = vertices.map { |vertex| vertex.payload.possibilities.last }.to_set - end - end - - # An error caused by conflicts in version - class VersionConflict < ResolverError - # @return [{String => Resolution::Conflict}] the conflicts that caused - # resolution to fail - attr_reader :conflicts - - # @return [SpecificationProvider] the specification provider used during - # resolution - attr_reader :specification_provider - - # Initializes a new error with the given version conflicts. - # @param [{String => Resolution::Conflict}] conflicts see {#conflicts} - # @param [SpecificationProvider] specification_provider see {#specification_provider} - def initialize(conflicts, specification_provider) - pairs = [] - conflicts.values.flat_map(&:requirements).each do |conflicting| - conflicting.each do |source, conflict_requirements| - conflict_requirements.each do |c| - pairs << [c, source] - end - end - end - - super "Unable to satisfy the following requirements:\n\n" \ - "#{pairs.map { |r, d| "- `#{r}` required by `#{d}`" }.join("\n")}" - - @conflicts = conflicts - @specification_provider = specification_provider - end - - require_relative 'delegates/specification_provider' - include Delegates::SpecificationProvider - - # @return [String] An error message that includes requirement trees, - # which is much more detailed & customizable than the default message - # @param [Hash] opts the options to create a message with. - # @option opts [String] :solver_name The user-facing name of the solver - # @option opts [String] :possibility_type The generic name of a possibility - # @option opts [Proc] :reduce_trees A proc that reduced the list of requirement trees - # @option opts [Proc] :printable_requirement A proc that pretty-prints requirements - # @option opts [Proc] :additional_message_for_conflict A proc that appends additional - # messages for each conflict - # @option opts [Proc] :version_for_spec A proc that returns the version number for a - # possibility - def message_with_trees(opts = {}) - solver_name = opts.delete(:solver_name) { self.class.name.split('::').first } - possibility_type = opts.delete(:possibility_type) { 'possibility named' } - reduce_trees = opts.delete(:reduce_trees) { proc { |trees| trees.uniq.sort_by(&:to_s) } } - printable_requirement = opts.delete(:printable_requirement) { proc { |req| req.to_s } } - additional_message_for_conflict = opts.delete(:additional_message_for_conflict) { proc {} } - version_for_spec = opts.delete(:version_for_spec) { proc(&:to_s) } - incompatible_version_message_for_conflict = opts.delete(:incompatible_version_message_for_conflict) do - proc do |name, _conflict| - %(#{solver_name} could not find compatible versions for #{possibility_type} "#{name}":) - end - end - - full_message_for_conflict = opts.delete(:full_message_for_conflict) do - proc do |name, conflict| - o = "\n".dup << incompatible_version_message_for_conflict.call(name, conflict) << "\n" - if conflict.locked_requirement - o << %( In snapshot (#{name_for_locking_dependency_source}):\n) - o << %( #{printable_requirement.call(conflict.locked_requirement)}\n) - o << %(\n) - end - o << %( In #{name_for_explicit_dependency_source}:\n) - trees = reduce_trees.call(conflict.requirement_trees) - - o << trees.map do |tree| - t = ''.dup - depth = 2 - tree.each do |req| - t << ' ' * depth << printable_requirement.call(req) - unless tree.last == req - if spec = conflict.activated_by_name[name_for(req)] - t << %( was resolved to #{version_for_spec.call(spec)}, which) - end - t << %( depends on) - end - t << %(\n) - depth += 1 - end - t - end.join("\n") - - additional_message_for_conflict.call(o, name, conflict) - - o - end - end - - conflicts.sort.reduce(''.dup) do |o, (name, conflict)| - o << full_message_for_conflict.call(name, conflict) - end.strip - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/gem_metadata.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/gem_metadata.rb deleted file mode 100644 index 86c249c404..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/gem_metadata.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # The version of Gem::Resolver::Molinillo. - VERSION = '0.8.0'.freeze -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider.rb deleted file mode 100644 index 1067bf7439..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # Provides information about specifications and dependencies to the resolver, - # allowing the {Resolver} class to remain generic while still providing power - # and flexibility. - # - # This module contains the methods that users of Gem::Resolver::Molinillo must to implement, - # using knowledge of their own model classes. - module SpecificationProvider - # Search for the specifications that match the given dependency. - # The specifications in the returned array will be considered in reverse - # order, so the latest version ought to be last. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [Array<Object>] the specifications that satisfy the given - # `dependency`. - def search_for(dependency) - [] - end - - # Returns the dependencies of `specification`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `specification` parameter. - # - # @param [Object] specification - # @return [Array<Object>] the dependencies that are required by the given - # `specification`. - def dependencies_for(specification) - [] - end - - # Determines whether the given `requirement` is satisfied by the given - # `spec`, in the context of the current `activated` dependency graph. - # - # @param [Object] requirement - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [Object] spec - # @return [Boolean] whether `requirement` is satisfied by `spec` in the - # context of the current `activated` dependency graph. - def requirement_satisfied_by?(requirement, activated, spec) - true - end - - # Determines whether two arrays of dependencies are equal, and thus can be - # grouped. - # - # @param [Array<Object>] dependencies - # @param [Array<Object>] other_dependencies - # @return [Boolean] whether `dependencies` and `other_dependencies` should - # be considered equal. - def dependencies_equal?(dependencies, other_dependencies) - dependencies == other_dependencies - end - - # Returns the name for the given `dependency`. - # @note This method should be 'pure', i.e. the return value should depend - # only on the `dependency` parameter. - # - # @param [Object] dependency - # @return [String] the name for the given `dependency`. - def name_for(dependency) - dependency.to_s - end - - # @return [String] the name of the source of explicit dependencies, i.e. - # those passed to {Resolver#resolve} directly. - def name_for_explicit_dependency_source - 'user-specified dependency' - end - - # @return [String] the name of the source of 'locked' dependencies, i.e. - # those passed to {Resolver#resolve} directly as the `base` - def name_for_locking_dependency_source - 'Lockfile' - end - - # Sort dependencies so that the ones that are easiest to resolve are first. - # Easiest to resolve is (usually) defined by: - # 1) Is this dependency already activated? - # 2) How relaxed are the requirements? - # 3) Are there any conflicts for this dependency? - # 4) How many possibilities are there to satisfy this dependency? - # - # @param [Array<Object>] dependencies - # @param [DependencyGraph] activated the current dependency graph in the - # resolution process. - # @param [{String => Array<Conflict>}] conflicts - # @return [Array<Object>] a sorted copy of `dependencies`. - def sort_dependencies(dependencies, activated, conflicts) - dependencies.sort_by do |dependency| - name = name_for(dependency) - [ - activated.vertex_named(name).payload ? 0 : 1, - conflicts[name] ? 0 : 1, - ] - end - end - - # Returns whether this dependency, which has no possible matching - # specifications, can safely be ignored. - # - # @param [Object] dependency - # @return [Boolean] whether this dependency can safely be skipped. - def allow_missing?(dependency) - false - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/ui.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/modules/ui.rb deleted file mode 100644 index a810fd519c..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/ui.rb +++ /dev/null @@ -1,67 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # Conveys information about the resolution process to a user. - module UI - # The {IO} object that should be used to print output. `STDOUT`, by default. - # - # @return [IO] - def output - STDOUT - end - - # Called roughly every {#progress_rate}, this method should convey progress - # to the user. - # - # @return [void] - def indicate_progress - output.print '.' unless debug? - end - - # How often progress should be conveyed to the user via - # {#indicate_progress}, in seconds. A third of a second, by default. - # - # @return [Float] - def progress_rate - 0.33 - end - - # Called before resolution begins. - # - # @return [void] - def before_resolution - output.print 'Resolving dependencies...' - end - - # Called after resolution ends (either successfully or with an error). - # By default, prints a newline. - # - # @return [void] - def after_resolution - output.puts - end - - # Conveys debug information to the user. - # - # @param [Integer] depth the current depth of the resolution process. - # @return [void] - def debug(depth = 0) - if debug? - debug_info = yield - debug_info = debug_info.inspect unless debug_info.is_a?(String) - debug_info = debug_info.split("\n").map { |s| ":#{depth.to_s.rjust 4}: #{s}" } - output.puts debug_info - end - end - - # Whether or not debug messages should be printed. - # By default, whether or not the `MOLINILLO_DEBUG` environment variable is - # set. - # - # @return [Boolean] - def debug? - return @debug_mode if defined?(@debug_mode) - @debug_mode = ENV['MOLINILLO_DEBUG'] - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/resolution.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/resolution.rb deleted file mode 100644 index 8b40e59e42..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/resolution.rb +++ /dev/null @@ -1,839 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - class Resolver - # A specific resolution from a given {Resolver} - class Resolution - # A conflict that the resolution process encountered - # @attr [Object] requirement the requirement that immediately led to the conflict - # @attr [{String,Nil=>[Object]}] requirements the requirements that caused the conflict - # @attr [Object, nil] existing the existing spec that was in conflict with - # the {#possibility} - # @attr [Object] possibility_set the set of specs that was unable to be - # activated due to a conflict. - # @attr [Object] locked_requirement the relevant locking requirement. - # @attr [Array<Array<Object>>] requirement_trees the different requirement - # trees that led to every requirement for the conflicting name. - # @attr [{String=>Object}] activated_by_name the already-activated specs. - # @attr [Object] underlying_error an error that has occurred during resolution, and - # will be raised at the end of it if no resolution is found. - Conflict = Struct.new( - :requirement, - :requirements, - :existing, - :possibility_set, - :locked_requirement, - :requirement_trees, - :activated_by_name, - :underlying_error - ) - - class Conflict - # @return [Object] a spec that was unable to be activated due to a conflict - def possibility - possibility_set && possibility_set.latest_version - end - end - - # A collection of possibility states that share the same dependencies - # @attr [Array] dependencies the dependencies for this set of possibilities - # @attr [Array] possibilities the possibilities - PossibilitySet = Struct.new(:dependencies, :possibilities) - - class PossibilitySet - # String representation of the possibility set, for debugging - def to_s - "[#{possibilities.join(', ')}]" - end - - # @return [Object] most up-to-date dependency in the possibility set - def latest_version - possibilities.last - end - end - - # Details of the state to unwind to when a conflict occurs, and the cause of the unwind - # @attr [Integer] state_index the index of the state to unwind to - # @attr [Object] state_requirement the requirement of the state we're unwinding to - # @attr [Array] requirement_tree for the requirement we're relaxing - # @attr [Array] conflicting_requirements the requirements that combined to cause the conflict - # @attr [Array] requirement_trees for the conflict - # @attr [Array] requirements_unwound_to_instead array of unwind requirements that were chosen over this unwind - UnwindDetails = Struct.new( - :state_index, - :state_requirement, - :requirement_tree, - :conflicting_requirements, - :requirement_trees, - :requirements_unwound_to_instead - ) - - class UnwindDetails - include Comparable - - # We compare UnwindDetails when choosing which state to unwind to. If - # two options have the same state_index we prefer the one most - # removed from a requirement that caused the conflict. Both options - # would unwind to the same state, but a `grandparent` option will - # filter out fewer of its possibilities after doing so - where a state - # is both a `parent` and a `grandparent` to requirements that have - # caused a conflict this is the correct behaviour. - # @param [UnwindDetail] other UnwindDetail to be compared - # @return [Integer] integer specifying ordering - def <=>(other) - if state_index > other.state_index - 1 - elsif state_index == other.state_index - reversed_requirement_tree_index <=> other.reversed_requirement_tree_index - else - -1 - end - end - - # @return [Integer] index of state requirement in reversed requirement tree - # (the conflicting requirement itself will be at position 0) - def reversed_requirement_tree_index - @reversed_requirement_tree_index ||= - if state_requirement - requirement_tree.reverse.index(state_requirement) - else - 999_999 - end - end - - # @return [Boolean] where the requirement of the state we're unwinding - # to directly caused the conflict. Note: in this case, it is - # impossible for the state we're unwinding to to be a parent of - # any of the other conflicting requirements (or we would have - # circularity) - def unwinding_to_primary_requirement? - requirement_tree.last == state_requirement - end - - # @return [Array] array of sub-dependencies to avoid when choosing a - # new possibility for the state we've unwound to. Only relevant for - # non-primary unwinds - def sub_dependencies_to_avoid - @requirements_to_avoid ||= - requirement_trees.map do |tree| - index = tree.index(state_requirement) - tree[index + 1] if index - end.compact - end - - # @return [Array] array of all the requirements that led to the need for - # this unwind - def all_requirements - @all_requirements ||= requirement_trees.flatten(1) - end - end - - # @return [SpecificationProvider] the provider that knows about - # dependencies, requirements, specifications, versions, etc. - attr_reader :specification_provider - - # @return [UI] the UI that knows how to communicate feedback about the - # resolution process back to the user - attr_reader :resolver_ui - - # @return [DependencyGraph] the base dependency graph to which - # dependencies should be 'locked' - attr_reader :base - - # @return [Array] the dependencies that were explicitly required - attr_reader :original_requested - - # Initializes a new resolution. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui see {#resolver_ui} - # @param [Array] requested see {#original_requested} - # @param [DependencyGraph] base see {#base} - def initialize(specification_provider, resolver_ui, requested, base) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - @original_requested = requested - @base = base - @states = [] - @iteration_counter = 0 - @parents_of = Hash.new { |h, k| h[k] = [] } - end - - # Resolves the {#original_requested} dependencies into a full dependency - # graph - # @raise [ResolverError] if successful resolution is impossible - # @return [DependencyGraph] the dependency graph of successfully resolved - # dependencies - def resolve - start_resolution - - while state - break if !state.requirement && state.requirements.empty? - indicate_progress - if state.respond_to?(:pop_possibility_state) # DependencyState - debug(depth) { "Creating possibility state for #{requirement} (#{possibilities.count} remaining)" } - state.pop_possibility_state.tap do |s| - if s - states.push(s) - activated.tag(s) - end - end - end - process_topmost_state - end - - resolve_activated_specs - ensure - end_resolution - end - - # @return [Integer] the number of resolver iterations in between calls to - # {#resolver_ui}'s {UI#indicate_progress} method - attr_accessor :iteration_rate - private :iteration_rate - - # @return [Time] the time at which resolution began - attr_accessor :started_at - private :started_at - - # @return [Array<ResolutionState>] the stack of states for the resolution - attr_accessor :states - private :states - - private - - # Sets up the resolution process - # @return [void] - def start_resolution - @started_at = Time.now - - push_initial_state - - debug { "Starting resolution (#{@started_at})\nUser-requested dependencies: #{original_requested}" } - resolver_ui.before_resolution - end - - def resolve_activated_specs - activated.vertices.each do |_, vertex| - next unless vertex.payload - - latest_version = vertex.payload.possibilities.reverse_each.find do |possibility| - vertex.requirements.all? { |req| requirement_satisfied_by?(req, activated, possibility) } - end - - activated.set_payload(vertex.name, latest_version) - end - activated.freeze - end - - # Ends the resolution process - # @return [void] - def end_resolution - resolver_ui.after_resolution - debug do - "Finished resolution (#{@iteration_counter} steps) " \ - "(Took #{(ended_at = Time.now) - @started_at} seconds) (#{ended_at})" - end - debug { 'Unactivated: ' + Hash[activated.vertices.reject { |_n, v| v.payload }].keys.join(', ') } if state - debug { 'Activated: ' + Hash[activated.vertices.select { |_n, v| v.payload }].keys.join(', ') } if state - end - - require_relative 'state' - require_relative 'modules/specification_provider' - - require_relative 'delegates/resolution_state' - require_relative 'delegates/specification_provider' - - include Gem::Resolver::Molinillo::Delegates::ResolutionState - include Gem::Resolver::Molinillo::Delegates::SpecificationProvider - - # Processes the topmost available {RequirementState} on the stack - # @return [void] - def process_topmost_state - if possibility - attempt_to_activate - else - create_conflict - unwind_for_conflict - end - rescue CircularDependencyError => underlying_error - create_conflict(underlying_error) - unwind_for_conflict - end - - # @return [Object] the current possibility that the resolution is trying - # to activate - def possibility - possibilities.last - end - - # @return [RequirementState] the current state the resolution is - # operating upon - def state - states.last - end - - # Creates and pushes the initial state for the resolution, based upon the - # {#requested} dependencies - # @return [void] - def push_initial_state - graph = DependencyGraph.new.tap do |dg| - original_requested.each do |requested| - vertex = dg.add_vertex(name_for(requested), nil, true) - vertex.explicit_requirements << requested - end - dg.tag(:initial_state) - end - - push_state_for_requirements(original_requested, true, graph) - end - - # Unwinds the states stack because a conflict has been encountered - # @return [void] - def unwind_for_conflict - details_for_unwind = build_details_for_unwind - unwind_options = unused_unwind_options - debug(depth) { "Unwinding for conflict: #{requirement} to #{details_for_unwind.state_index / 2}" } - conflicts.tap do |c| - sliced_states = states.slice!((details_for_unwind.state_index + 1)..-1) - raise_error_unless_state(c) - activated.rewind_to(sliced_states.first || :initial_state) if sliced_states - state.conflicts = c - state.unused_unwind_options = unwind_options - filter_possibilities_after_unwind(details_for_unwind) - index = states.size - 1 - @parents_of.each { |_, a| a.reject! { |i| i >= index } } - state.unused_unwind_options.reject! { |uw| uw.state_index >= index } - end - end - - # Raises a VersionConflict error, or any underlying error, if there is no - # current state - # @return [void] - def raise_error_unless_state(conflicts) - return if state - - error = conflicts.values.map(&:underlying_error).compact.first - raise error || VersionConflict.new(conflicts, specification_provider) - end - - # @return [UnwindDetails] Details of the nearest index to which we could unwind - def build_details_for_unwind - # Get the possible unwinds for the current conflict - current_conflict = conflicts[name] - binding_requirements = binding_requirements_for_conflict(current_conflict) - unwind_details = unwind_options_for_requirements(binding_requirements) - - last_detail_for_current_unwind = unwind_details.sort.last - current_detail = last_detail_for_current_unwind - - # Look for past conflicts that could be unwound to affect the - # requirement tree for the current conflict - all_reqs = last_detail_for_current_unwind.all_requirements - all_reqs_size = all_reqs.size - relevant_unused_unwinds = unused_unwind_options.select do |alternative| - diff_reqs = all_reqs - alternative.requirements_unwound_to_instead - next if diff_reqs.size == all_reqs_size - # Find the highest index unwind whilst looping through - current_detail = alternative if alternative > current_detail - alternative - end - - # Add the current unwind options to the `unused_unwind_options` array. - # The "used" option will be filtered out during `unwind_for_conflict`. - state.unused_unwind_options += unwind_details.reject { |detail| detail.state_index == -1 } - - # Update the requirements_unwound_to_instead on any relevant unused unwinds - relevant_unused_unwinds.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - unwind_details.each do |d| - (d.requirements_unwound_to_instead << current_detail.state_requirement).uniq! - end - - current_detail - end - - # @param [Array<Object>] binding_requirements array of requirements that combine to create a conflict - # @return [Array<UnwindDetails>] array of UnwindDetails that have a chance - # of resolving the passed requirements - def unwind_options_for_requirements(binding_requirements) - unwind_details = [] - - trees = [] - binding_requirements.reverse_each do |r| - partial_tree = [r] - trees << partial_tree - unwind_details << UnwindDetails.new(-1, nil, partial_tree, binding_requirements, trees, []) - - # If this requirement has alternative possibilities, check if any would - # satisfy the other requirements that created this conflict - requirement_state = find_state_for(r) - if conflict_fixing_possibilities?(requirement_state, binding_requirements) - unwind_details << UnwindDetails.new( - states.index(requirement_state), - r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Next, look at the parent of this requirement, and check if the requirement - # could have been avoided if an alternative PossibilitySet had been chosen - parent_r = parent_of(r) - next if parent_r.nil? - partial_tree.unshift(parent_r) - requirement_state = find_state_for(parent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - parent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - - # Finally, look at the grandparent and up of this requirement, looking - # for any possibilities that wouldn't create their parent requirement - grandparent_r = parent_of(parent_r) - until grandparent_r.nil? - partial_tree.unshift(grandparent_r) - requirement_state = find_state_for(grandparent_r) - if requirement_state.possibilities.any? { |set| !set.dependencies.include?(parent_r) } - unwind_details << UnwindDetails.new( - states.index(requirement_state), - grandparent_r, - partial_tree, - binding_requirements, - trees, - [] - ) - end - parent_r = grandparent_r - grandparent_r = parent_of(parent_r) - end - end - - unwind_details - end - - # @param [DependencyState] state - # @param [Array] binding_requirements array of requirements - # @return [Boolean] whether or not the given state has any possibilities - # that could satisfy the given requirements - def conflict_fixing_possibilities?(state, binding_requirements) - return false unless state - - state.possibilities.any? do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, binding_requirements) - end - end - end - - # Filter's a state's possibilities to remove any that would not fix the - # conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just - # unwound from - # @return [void] - def filter_possibilities_after_unwind(unwind_details) - return unless state && !state.possibilities.empty? - - if unwind_details.unwinding_to_primary_requirement? - filter_possibilities_for_primary_unwind(unwind_details) - else - filter_possibilities_for_parent_unwind(unwind_details) - end - end - - # Filter's a state's possibilities to remove any that would not satisfy - # the requirements in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_primary_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - unwind_requirement_sets = unwinds_to_state.map(&:conflicting_requirements) - - state.possibilities.reject! do |possibility_set| - possibility_set.possibilities.none? do |poss| - unwind_requirement_sets.any? do |requirements| - possibility_satisfies_requirements?(poss, requirements) - end - end - end - end - - # @param [Object] possibility a single possibility - # @param [Array] requirements an array of requirements - # @return [Boolean] whether the possibility satisfies all of the - # given requirements - def possibility_satisfies_requirements?(possibility, requirements) - name = name_for(possibility) - - activated.tag(:swap) - activated.set_payload(name, possibility) if activated.vertex_named(name) - satisfied = requirements.all? { |r| requirement_satisfied_by?(r, activated, possibility) } - activated.rewind_to(:swap) - - satisfied - end - - # Filter's a state's possibilities to remove any that would (eventually) - # create a requirement in the conflict we've just rewound from - # @param [UnwindDetails] unwind_details details of the conflict just unwound from - # @return [void] - def filter_possibilities_for_parent_unwind(unwind_details) - unwinds_to_state = unused_unwind_options.select { |uw| uw.state_index == unwind_details.state_index } - unwinds_to_state << unwind_details - - primary_unwinds = unwinds_to_state.select(&:unwinding_to_primary_requirement?).uniq - parent_unwinds = unwinds_to_state.uniq - primary_unwinds - - allowed_possibility_sets = primary_unwinds.flat_map do |unwind| - states[unwind.state_index].possibilities.select do |possibility_set| - possibility_set.possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, unwind.conflicting_requirements) - end - end - end - - requirements_to_avoid = parent_unwinds.flat_map(&:sub_dependencies_to_avoid) - - state.possibilities.reject! do |possibility_set| - !allowed_possibility_sets.include?(possibility_set) && - (requirements_to_avoid - possibility_set.dependencies).empty? - end - end - - # @param [Conflict] conflict - # @return [Array] minimal array of requirements that would cause the passed - # conflict to occur. - def binding_requirements_for_conflict(conflict) - return [conflict.requirement] if conflict.possibility.nil? - - possible_binding_requirements = conflict.requirements.values.flatten(1).uniq - - # When there's a `CircularDependency` error the conflicting requirement - # (the one causing the circular) won't be `conflict.requirement` - # (which won't be for the right state, because we won't have created it, - # because it's circular). - # We need to make sure we have that requirement in the conflict's list, - # otherwise we won't be able to unwind properly, so we just return all - # the requirements for the conflict. - return possible_binding_requirements if conflict.underlying_error - - possibilities = search_for(conflict.requirement) - - # If all the requirements together don't filter out all possibilities, - # then the only two requirements we need to consider are the initial one - # (where the dependency's version was first chosen) and the last - if binding_requirement_in_set?(nil, possible_binding_requirements, possibilities) - return [conflict.requirement, requirement_for_existing_name(name_for(conflict.requirement))].compact - end - - # Loop through the possible binding requirements, removing each one - # that doesn't bind. Use a `reverse_each` as we want the earliest set of - # binding requirements, and don't use `reject!` as we wish to refine the - # array *on each iteration*. - binding_requirements = possible_binding_requirements.dup - possible_binding_requirements.reverse_each do |req| - next if req == conflict.requirement - unless binding_requirement_in_set?(req, binding_requirements, possibilities) - binding_requirements -= [req] - end - end - - binding_requirements - end - - # @param [Object] requirement we wish to check - # @param [Array] possible_binding_requirements array of requirements - # @param [Array] possibilities array of possibilities the requirements will be used to filter - # @return [Boolean] whether or not the given requirement is required to filter - # out all elements of the array of possibilities. - def binding_requirement_in_set?(requirement, possible_binding_requirements, possibilities) - possibilities.any? do |poss| - possibility_satisfies_requirements?(poss, possible_binding_requirements - [requirement]) - end - end - - # @param [Object] requirement - # @return [Object] the requirement that led to `requirement` being added - # to the list of requirements. - def parent_of(requirement) - return unless requirement - return unless index = @parents_of[requirement].last - return unless parent_state = @states[index] - parent_state.requirement - end - - # @param [String] name - # @return [Object] the requirement that led to a version of a possibility - # with the given name being activated. - def requirement_for_existing_name(name) - return nil unless vertex = activated.vertex_named(name) - return nil unless vertex.payload - states.find { |s| s.name == name }.requirement - end - - # @param [Object] requirement - # @return [ResolutionState] the state whose `requirement` is the given - # `requirement`. - def find_state_for(requirement) - return nil unless requirement - states.find { |i| requirement == i.requirement } - end - - # @param [Object] underlying_error - # @return [Conflict] a {Conflict} that reflects the failure to activate - # the {#possibility} in conjunction with the current {#state} - def create_conflict(underlying_error = nil) - vertex = activated.vertex_named(name) - locked_requirement = locked_requirement_named(name) - - requirements = {} - unless vertex.explicit_requirements.empty? - requirements[name_for_explicit_dependency_source] = vertex.explicit_requirements - end - requirements[name_for_locking_dependency_source] = [locked_requirement] if locked_requirement - vertex.incoming_edges.each do |edge| - (requirements[edge.origin.payload.latest_version] ||= []).unshift(edge.requirement) - end - - activated_by_name = {} - activated.each { |v| activated_by_name[v.name] = v.payload.latest_version if v.payload } - conflicts[name] = Conflict.new( - requirement, - requirements, - vertex.payload && vertex.payload.latest_version, - possibility, - locked_requirement, - requirement_trees, - activated_by_name, - underlying_error - ) - end - - # @return [Array<Array<Object>>] The different requirement - # trees that led to every requirement for the current spec. - def requirement_trees - vertex = activated.vertex_named(name) - vertex.requirements.map { |r| requirement_tree_for(r) } - end - - # @param [Object] requirement - # @return [Array<Object>] the list of requirements that led to - # `requirement` being required. - def requirement_tree_for(requirement) - tree = [] - while requirement - tree.unshift(requirement) - requirement = parent_of(requirement) - end - tree - end - - # Indicates progress roughly once every second - # @return [void] - def indicate_progress - @iteration_counter += 1 - @progress_rate ||= resolver_ui.progress_rate - if iteration_rate.nil? - if Time.now - started_at >= @progress_rate - self.iteration_rate = @iteration_counter - end - end - - if iteration_rate && (@iteration_counter % iteration_rate) == 0 - resolver_ui.indicate_progress - end - end - - # Calls the {#resolver_ui}'s {UI#debug} method - # @param [Integer] depth the depth of the {#states} stack - # @param [Proc] block a block that yields a {#to_s} - # @return [void] - def debug(depth = 0, &block) - resolver_ui.debug(depth, &block) - end - - # Attempts to activate the current {#possibility} - # @return [void] - def attempt_to_activate - debug(depth) { 'Attempting to activate ' + possibility.to_s } - existing_vertex = activated.vertex_named(name) - if existing_vertex.payload - debug(depth) { "Found existing spec (#{existing_vertex.payload})" } - attempt_to_filter_existing_spec(existing_vertex) - else - latest = possibility.latest_version - possibility.possibilities.select! do |possibility| - requirement_satisfied_by?(requirement, activated, possibility) - end - if possibility.latest_version.nil? - # ensure there's a possibility for better error messages - possibility.possibilities << latest if latest - create_conflict - unwind_for_conflict - else - activate_new_spec - end - end - end - - # Attempts to update the existing vertex's `PossibilitySet` with a filtered version - # @return [void] - def attempt_to_filter_existing_spec(vertex) - filtered_set = filtered_possibility_set(vertex) - if !filtered_set.possibilities.empty? - activated.set_payload(name, filtered_set) - new_requirements = requirements.dup - push_state_for_requirements(new_requirements, false) - else - create_conflict - debug(depth) { "Unsatisfied by existing spec (#{vertex.payload})" } - unwind_for_conflict - end - end - - # Generates a filtered version of the existing vertex's `PossibilitySet` using the - # current state's `requirement` - # @param [Object] vertex existing vertex - # @return [PossibilitySet] filtered possibility set - def filtered_possibility_set(vertex) - PossibilitySet.new(vertex.payload.dependencies, vertex.payload.possibilities & possibility.possibilities) - end - - # @param [String] requirement_name the spec name to search for - # @return [Object] the locked spec named `requirement_name`, if one - # is found on {#base} - def locked_requirement_named(requirement_name) - vertex = base.vertex_named(requirement_name) - vertex && vertex.payload - end - - # Add the current {#possibility} to the dependency graph of the current - # {#state} - # @return [void] - def activate_new_spec - conflicts.delete(name) - debug(depth) { "Activated #{name} at #{possibility}" } - activated.set_payload(name, possibility) - require_nested_dependencies_for(possibility) - end - - # Requires the dependencies that the recently activated spec has - # @param [Object] possibility_set the PossibilitySet that has just been - # activated - # @return [void] - def require_nested_dependencies_for(possibility_set) - nested_dependencies = dependencies_for(possibility_set.latest_version) - debug(depth) { "Requiring nested dependencies (#{nested_dependencies.join(', ')})" } - nested_dependencies.each do |d| - activated.add_child_vertex(name_for(d), nil, [name_for(possibility_set.latest_version)], d) - parent_index = states.size - 1 - parents = @parents_of[d] - parents << parent_index if parents.empty? - end - - push_state_for_requirements(requirements + nested_dependencies, !nested_dependencies.empty?) - end - - # Pushes a new {DependencyState} that encapsulates both existing and new - # requirements - # @param [Array] new_requirements - # @param [Boolean] requires_sort - # @param [Object] new_activated - # @return [void] - def push_state_for_requirements(new_requirements, requires_sort = true, new_activated = activated) - new_requirements = sort_dependencies(new_requirements.uniq, new_activated, conflicts) if requires_sort - new_requirement = nil - loop do - new_requirement = new_requirements.shift - break if new_requirement.nil? || states.none? { |s| s.requirement == new_requirement } - end - new_name = new_requirement ? name_for(new_requirement) : ''.freeze - possibilities = possibilities_for_requirement(new_requirement) - handle_missing_or_push_dependency_state DependencyState.new( - new_name, new_requirements, new_activated, - new_requirement, possibilities, depth, conflicts.dup, unused_unwind_options.dup - ) - end - - # Checks a proposed requirement with any existing locked requirement - # before generating an array of possibilities for it. - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibilities - def possibilities_for_requirement(requirement, activated = self.activated) - return [] unless requirement - if locked_requirement_named(name_for(requirement)) - return locked_requirement_possibility_set(requirement, activated) - end - - group_possibilities(search_for(requirement)) - end - - # @param [Object] requirement the proposed requirement - # @param [Object] activated - # @return [Array] possibility set containing only the locked requirement, if any - def locked_requirement_possibility_set(requirement, activated = self.activated) - all_possibilities = search_for(requirement) - locked_requirement = locked_requirement_named(name_for(requirement)) - - # Longwinded way to build a possibilities array with either the locked - # requirement or nothing in it. Required, since the API for - # locked_requirement isn't guaranteed. - locked_possibilities = all_possibilities.select do |possibility| - requirement_satisfied_by?(locked_requirement, activated, possibility) - end - - group_possibilities(locked_possibilities) - end - - # Build an array of PossibilitySets, with each element representing a group of - # dependency versions that all have the same sub-dependency version constraints - # and are contiguous. - # @param [Array] possibilities an array of possibilities - # @return [Array<PossibilitySet>] an array of possibility sets - def group_possibilities(possibilities) - possibility_sets = [] - current_possibility_set = nil - - possibilities.reverse_each do |possibility| - dependencies = dependencies_for(possibility) - if current_possibility_set && dependencies_equal?(current_possibility_set.dependencies, dependencies) - current_possibility_set.possibilities.unshift(possibility) - else - possibility_sets.unshift(PossibilitySet.new(dependencies, [possibility])) - current_possibility_set = possibility_sets.first - end - end - - possibility_sets - end - - # Pushes a new {DependencyState}. - # If the {#specification_provider} says to - # {SpecificationProvider#allow_missing?} that particular requirement, and - # there are no possibilities for that requirement, then `state` is not - # pushed, and the vertex in {#activated} is removed, and we continue - # resolving the remaining requirements. - # @param [DependencyState] state - # @return [void] - def handle_missing_or_push_dependency_state(state) - if state.requirement && state.possibilities.empty? && allow_missing?(state.requirement) - state.activated.detach_vertex_named(state.name) - push_state_for_requirements(state.requirements.dup, false, state.activated) - else - states.push(state).tap { activated.tag(state) } - end - end - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/resolver.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/resolver.rb deleted file mode 100644 index d43121f8ca..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/resolver.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require_relative 'dependency_graph' - -module Gem::Resolver::Molinillo - # This class encapsulates a dependency resolver. - # The resolver is responsible for determining which set of dependencies to - # activate, with feedback from the {#specification_provider} - # - # - class Resolver - require_relative 'resolution' - - # @return [SpecificationProvider] the specification provider used - # in the resolution process - attr_reader :specification_provider - - # @return [UI] the UI module used to communicate back to the user - # during the resolution process - attr_reader :resolver_ui - - # Initializes a new resolver. - # @param [SpecificationProvider] specification_provider - # see {#specification_provider} - # @param [UI] resolver_ui - # see {#resolver_ui} - def initialize(specification_provider, resolver_ui) - @specification_provider = specification_provider - @resolver_ui = resolver_ui - end - - # Resolves the requested dependencies into a {DependencyGraph}, - # locking to the base dependency graph (if specified) - # @param [Array] requested an array of 'requested' dependencies that the - # {#specification_provider} can understand - # @param [DependencyGraph,nil] base the base dependency graph to which - # dependencies should be 'locked' - def resolve(requested, base = DependencyGraph.new) - Resolution.new(specification_provider, - resolver_ui, - requested, - base). - resolve - end - end -end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/state.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/state.rb deleted file mode 100644 index 6e7c715fce..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/state.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # A state that a {Resolution} can be in - # @attr [String] name the name of the current requirement - # @attr [Array<Object>] requirements currently unsatisfied requirements - # @attr [DependencyGraph] activated the graph of activated dependencies - # @attr [Object] requirement the current requirement - # @attr [Object] possibilities the possibilities to satisfy the current requirement - # @attr [Integer] depth the depth of the resolution - # @attr [Hash] conflicts unresolved conflicts, indexed by dependency name - # @attr [Array<UnwindDetails>] unused_unwind_options unwinds for previous conflicts that weren't explored - ResolutionState = Struct.new( - :name, - :requirements, - :activated, - :requirement, - :possibilities, - :depth, - :conflicts, - :unused_unwind_options - ) - - class ResolutionState - # Returns an empty resolution state - # @return [ResolutionState] an empty state - def self.empty - new(nil, [], DependencyGraph.new, nil, nil, 0, {}, []) - end - end - - # A state that encapsulates a set of {#requirements} with an {Array} of - # possibilities - class DependencyState < ResolutionState - # Removes a possibility from `self` - # @return [PossibilityState] a state with a single possibility, - # the possibility that was removed from `self` - def pop_possibility_state - PossibilityState.new( - name, - requirements.dup, - activated, - requirement, - [possibilities.pop], - depth + 1, - conflicts.dup, - unused_unwind_options.dup - ).tap do |state| - state.activated.tag(state) - end - end - end - - # A state that encapsulates a single possibility to fulfill the given - # {#requirement} - class PossibilityState < ResolutionState - end -end diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb index 5b51493c9a..6f86f0f412 100644 --- a/lib/rubygems/resolver/requirement_list.rb +++ b/lib/rubygems/resolver/requirement_list.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The RequirementList is used to hold the requirements being considered # while resolving a set of gems. diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb index 5d8dd51eaa..243fee5fd5 100644 --- a/lib/rubygems/resolver/set.rb +++ b/lib/rubygems/resolver/set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Resolver sets are used to look up specifications (and their # dependencies) used in resolution. This set is abstract. diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index bf8c23184e..074b473edc 100644 --- a/lib/rubygems/resolver/source_set.rb +++ b/lib/rubygems/resolver/source_set.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # The SourceSet chooses the best available method to query a remote index. # @@ -40,6 +42,6 @@ class Gem::Resolver::SourceSet < Gem::Resolver::Set def get_set(name) link = @links[name] - @sets[link] ||= Gem::Source.new(link).dependency_resolver_set if link + @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link end end diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb index 7b665fe876..00ef9fdba0 100644 --- a/lib/rubygems/resolver/spec_specification.rb +++ b/lib/rubygems/resolver/spec_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The Resolver::SpecSpecification contains common functionality for # Resolver specifications that are backed by a Gem::Specification. @@ -65,4 +66,11 @@ class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification def version spec.version end + + ## + # The hash value for this specification. + + def hash + spec.hash + end end diff --git a/lib/rubygems/resolver/specification.rb b/lib/rubygems/resolver/specification.rb index 3da803cab5..d2098ef0e2 100644 --- a/lib/rubygems/resolver/specification.rb +++ b/lib/rubygems/resolver/specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A Resolver::Specification contains a subset of the information # contained in a Gem::Specification. Only the information necessary for diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb deleted file mode 100644 index 3b95efebf7..0000000000 --- a/lib/rubygems/resolver/stats.rb +++ /dev/null @@ -1,45 +0,0 @@ -# frozen_string_literal: true -class Gem::Resolver::Stats - def initialize - @max_depth = 0 - @max_requirements = 0 - @requirements = 0 - @backtracking = 0 - @iterations = 0 - end - - def record_depth(stack) - if stack.size > @max_depth - @max_depth = stack.size - end - end - - def record_requirements(reqs) - if reqs.size > @max_requirements - @max_requirements = reqs.size - end - end - - def requirement! - @requirements += 1 - end - - def backtracking! - @backtracking += 1 - end - - def iteration! - @iterations += 1 - end - - PATTERN = "%20s: %d\n" - - def display - $stdout.puts "=== Resolver Statistics ===" - $stdout.printf PATTERN, "Max Depth", @max_depth - $stdout.printf PATTERN, "Total Requirements", @requirements - $stdout.printf PATTERN, "Max Requirements", @max_requirements - $stdout.printf PATTERN, "Backtracking #", @backtracking - $stdout.printf PATTERN, "Iteration #", @iterations - end -end diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb new file mode 100644 index 0000000000..bf0dbb6adc --- /dev/null +++ b/lib/rubygems/resolver/strategy.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +# Custom PubGrub strategy with caching for version selection. +# Modeled after Bundler's strategy to avoid redundant versions_for +# calls during the solver's package selection loop. + +class Gem::Resolver::Strategy + def initialize(source) + @source = source + @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} } + + @version_indexes = Hash.new do |h, k| + if Gem::PubGrub::Package.root?(k) + h[k] = { Gem::PubGrub::Package.root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + indexes = @version_indexes[package] + versions.min_by {|version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + @package_priority_cache[package][range] ||= begin + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb index 6c0ef2a1a1..293a1e3331 100644 --- a/lib/rubygems/resolver/vendor_set.rb +++ b/lib/rubygems/resolver/vendor_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A VendorSet represents gems that have been unpacked into a specific # directory that contains a gemspec. diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb index 600a98a2bf..ac78f54558 100644 --- a/lib/rubygems/resolver/vendor_specification.rb +++ b/lib/rubygems/resolver/vendor_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A VendorSpecification represents a gem that has been unpacked into a project # and is being loaded through a gem dependencies file through the +path:+ diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index 5522753af5..148cba38c4 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,16 +1,21 @@ +# frozen_string_literal: true + require_relative "openssl" +require_relative "user_interaction" ## # S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems # More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html class Gem::S3URISigner + include Gem::UserInteraction + class ConfigurationError < Gem::Exception def initialize(message) super message end def to_s # :nodoc: - "#{super}" + super.to_s end end @@ -20,23 +25,25 @@ class Gem::S3URISigner end def to_s # :nodoc: - "#{super}" + super.to_s end end attr_accessor :uri + attr_accessor :method - def initialize(uri) + def initialize(uri, method) @uri = uri + @method = method end ## # Signs S3 URI using query-params according to the reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html - def sign(expiration = 86400) + def sign(expiration = 86_400) s3_config = fetch_s3_config current_time = Time.now.utc - date_time = current_time.strftime("%Y%m%dT%H%m%SZ") + date_time = current_time.strftime("%Y%m%dT%H%M%SZ") date = date_time[0,8] credential_info = "#{date}/#{s3_config.region}/s3/aws4_request" @@ -47,7 +54,7 @@ class Gem::S3URISigner string_to_sign = generate_string_to_sign(date_time, credential_info, canonical_request) signature = generate_signature(s3_config, date, string_to_sign) - URI.parse("https://#{canonical_host}#{uri.path}?#{query_params}&X-Amz-Signature=#{signature}") + Gem::URI.parse("https://#{canonical_host}#{uri.path}?#{query_params}&X-Amz-Signature=#{signature}") end private @@ -71,7 +78,7 @@ class Gem::S3URISigner def generate_canonical_request(canonical_host, query_params) [ - "GET", + method.upcase, uri.path, query_params, "host:#{canonical_host}", @@ -134,35 +141,78 @@ class Gem::S3URISigner end def base64_uri_escape(str) - str.gsub(/[\+\/=\n]/, BASE64_URI_TRANSLATE) + str.gsub(%r{[\+/=\n]}, BASE64_URI_TRANSLATE) end def ec2_metadata_credentials_json - require "net/http" + require_relative "vendored_net_http" require_relative "request" require_relative "request/connection_pools" require "json" - iam_info = ec2_metadata_request(EC2_IAM_INFO) + # First try V2 fallback to V1 + res = nil + begin + res = ec2_metadata_credentials_imds_v2 + rescue InstanceProfileError + alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1" + res = ec2_metadata_credentials_imds_v1 + end + res + end + + def ec2_metadata_credentials_imds_v2 + token = ec2_metadata_token + iam_info = ec2_metadata_request(EC2_IAM_INFO, token:) # Expected format: arn:aws:iam::<id>:instance-profile/<role_name> role_name = iam_info["InstanceProfileArn"].split("/").last - ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name) + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:) end - def ec2_metadata_request(url) - uri = URI(url) - @request_pool ||= create_request_pool(uri) - request = Gem::Request.new(uri, Net::HTTP::Get, nil, @request_pool) - response = request.fetch + def ec2_metadata_credentials_imds_v1 + iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil) + # Expected format: arn:aws:iam::<id>:instance-profile/<role_name> + role_name = iam_info["InstanceProfileArn"].split("/").last + ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil) + end + + def ec2_metadata_request(url, token:) + request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get) + + response = request.fetch do |req| + if token + req.add_field "X-aws-ec2-metadata-token", token + end + end case response - when Net::HTTPOK then + when Gem::Net::HTTPOK then JSON.parse(response.body) else raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}") end end + def ec2_metadata_token + request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put) + + response = request.fetch do |req| + req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60 + end + + case response + when Gem::Net::HTTPOK then + response.body + else + raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}") + end + end + + def ec2_iam_request(uri, verb) + @request_pool ||= create_request_pool(uri) + Gem::Request.new(uri, verb, nil, @request_pool) + end + def create_request_pool(uri) proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme)) certs = Gem::Request.get_cert_files @@ -170,6 +220,7 @@ class Gem::S3URISigner end BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze - EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info".freeze - EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/".freeze + EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token" + EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info" + EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" end diff --git a/lib/rubygems/safe_marshal.rb b/lib/rubygems/safe_marshal.rb new file mode 100644 index 0000000000..871f24727d --- /dev/null +++ b/lib/rubygems/safe_marshal.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "stringio" + +require_relative "safe_marshal/reader" +require_relative "safe_marshal/visitors/to_ruby" + +module Gem + ### + # This module is used for safely loading Marshal specs from a gem. The + # `safe_load` method defined on this module is specifically designed for + # loading Gem specifications. + + module SafeMarshal + PERMITTED_CLASSES = %w[ + Date + Time + Rational + + Gem::Dependency + Gem::NameTuple + Gem::Platform + Gem::Requirement + Gem::Specification + Gem::Version + Gem::Version::Requirement + + YAML::Syck::DefaultKey + YAML::PrivateType + ].freeze + private_constant :PERMITTED_CLASSES + + PERMITTED_SYMBOLS = %w[ + development + runtime + + name + number + platform + dependencies + ].freeze + private_constant :PERMITTED_SYMBOLS + + PERMITTED_IVARS = { + "String" => %w[E encoding @taguri @debug_created_info], + "Time" => %w[ + offset zone nano_num nano_den submicro + @_zone @marshal_with_utc_coercion + ], + "Gem::Dependency" => %w[ + @name @requirement @prerelease @version_requirement @version_requirements @type + @force_ruby_platform + ], + "Gem::NameTuple" => %w[@name @version @platform], + "Gem::Platform" => %w[@os @cpu @version], + "Psych::PrivateType" => %w[@value @type_id], + "YAML::PrivateType" => %w[@value @type_id], + }.freeze + private_constant :PERMITTED_IVARS + + def self.safe_load(input) + load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, permitted_ivars: PERMITTED_IVARS) + end + + def self.load(input, permitted_classes: [::Symbol], permitted_symbols: [], permitted_ivars: {}) + root = Reader.new(StringIO.new(input, "r").binmode).read! + + Visitors::ToRuby.new( + permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + permitted_ivars: permitted_ivars, + ).visit(root) + end + end +end diff --git a/lib/rubygems/safe_marshal/elements.rb b/lib/rubygems/safe_marshal/elements.rb new file mode 100644 index 0000000000..f8874b1b2f --- /dev/null +++ b/lib/rubygems/safe_marshal/elements.rb @@ -0,0 +1,146 @@ +# frozen_string_literal: true + +module Gem + module SafeMarshal + module Elements + class Element + end + + class Symbol < Element + def initialize(name) + @name = name + end + attr_reader :name + end + + class UserDefined < Element + def initialize(name, binary_string) + @name = name + @binary_string = binary_string + end + + attr_reader :name, :binary_string + end + + class UserMarshal < Element + def initialize(name, data) + @name = name + @data = data + end + + attr_reader :name, :data + end + + class String < Element + def initialize(str) + @str = str + end + + attr_reader :str + end + + class Hash < Element + def initialize(pairs) + @pairs = pairs + end + + attr_reader :pairs + end + + class HashWithDefaultValue < Hash + def initialize(pairs, default) + super(pairs) + @default = default + end + + attr_reader :default + end + + class Array < Element + def initialize(elements) + @elements = elements + end + + attr_reader :elements + end + + class Integer < Element + def initialize(int) + @int = int + end + + attr_reader :int + end + + class True < Element + def initialize + end + TRUE = new.freeze + end + + class False < Element + def initialize + end + + FALSE = new.freeze + end + + class WithIvars < Element + def initialize(object, ivars) + @object = object + @ivars = ivars + end + + attr_reader :object, :ivars + end + + class Object < Element + def initialize(name) + @name = name + end + attr_reader :name + end + + class Nil < Element + NIL = new.freeze + end + + class ObjectLink < Element + def initialize(offset) + @offset = offset + end + attr_reader :offset + end + + class SymbolLink < Element + def initialize(offset) + @offset = offset + end + attr_reader :offset + end + + class Float < Element + def initialize(string) + @string = string + end + attr_reader :string + end + + class Bignum < Element # rubocop:disable Lint/UnifiedInteger + def initialize(sign, data) + @sign = sign + @data = data + end + attr_reader :sign, :data + end + + class UserClass < Element + def initialize(name, wrapped_object) + @name = name + @wrapped_object = wrapped_object + end + attr_reader :name, :wrapped_object + end + end + end +end diff --git a/lib/rubygems/safe_marshal/reader.rb b/lib/rubygems/safe_marshal/reader.rb new file mode 100644 index 0000000000..4362d65fd6 --- /dev/null +++ b/lib/rubygems/safe_marshal/reader.rb @@ -0,0 +1,325 @@ +# frozen_string_literal: true + +require_relative "elements" + +module Gem + module SafeMarshal + class Reader + class Error < StandardError + end + + class UnsupportedVersionError < Error + end + + class UnconsumedBytesError < Error + end + + class NotImplementedError < Error + end + + class EOFError < Error + end + + class DataTooShortError < Error + end + + class NegativeLengthError < Error + end + + def initialize(io) + @io = io + end + + def read! + read_header + root = read_element + raise UnconsumedBytesError, "expected EOF, got #{@io.read(10).inspect}... after top-level element #{root.class}" unless @io.eof? + root + end + + private + + MARSHAL_VERSION = [Marshal::MAJOR_VERSION, Marshal::MINOR_VERSION].map(&:chr).join.freeze + private_constant :MARSHAL_VERSION + + def read_header + v = @io.read(2) + raise UnsupportedVersionError, "Unsupported marshal version #{v.bytes.map(&:ord).join(".")}, expected #{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" unless v == MARSHAL_VERSION + end + + def read_bytes(n) + raise NegativeLengthError if n < 0 + str = @io.read(n) + raise EOFError, "expected #{n} bytes, got EOF" if str.nil? + raise DataTooShortError, "expected #{n} bytes, got #{str.inspect}" unless str.bytesize == n + str + end + + def read_byte + @io.getbyte || raise(EOFError, "Unexpected EOF") + end + + def read_integer + b = read_byte + + case b + when 0x00 + 0 + when 0x01 + read_byte + when 0x02 + read_byte | (read_byte << 8) + when 0x03 + read_byte | (read_byte << 8) | (read_byte << 16) + when 0x04 + read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24) + when 0xFC + read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24) | -0x100000000 + when 0xFD + read_byte | (read_byte << 8) | (read_byte << 16) | -0x1000000 + when 0xFE + read_byte | (read_byte << 8) | -0x10000 + when 0xFF + read_byte | -0x100 + else + signed = (b ^ 128) - 128 + if b >= 128 + signed + 5 + else + signed - 5 + end + end + end + + def read_element + type = read_byte + case type + when 34 then read_string # ?" + when 48 then read_nil # ?0 + when 58 then read_symbol # ?: + when 59 then read_symbol_link # ?; + when 64 then read_object_link # ?@ + when 70 then read_false # ?F + when 73 then read_object_with_ivars # ?I + when 84 then read_true # ?T + when 85 then read_user_marshal # ?U + when 91 then read_array # ?[ + when 102 then read_float # ?f + when 105 then Elements::Integer.new(read_integer) # ?i + when 108 then read_bignum # ?l + when 111 then read_object # ?o + when 117 then read_user_defined # ?u + when 123 then read_hash # ?{ + when 125 then read_hash_with_default_value # ?} + when 101 then read_extended_object # ?e + when 99 then read_class # ?c + when 109 then read_module # ?m + when 77 then read_class_or_module # ?M + when 100 then read_data # ?d + when 47 then read_regexp # ?/ + when 83 then read_struct # ?S + when 67 then read_user_class # ?C + else + raise Error, "Unknown marshal type discriminator #{type.chr.inspect} (#{type})" + end + end + + STRING_E_SYMBOL = Elements::Symbol.new("E").freeze + private_constant :STRING_E_SYMBOL + + def read_symbol + len = read_integer + if len == 1 + byte = read_byte + if byte == 69 # ?E + STRING_E_SYMBOL + else + Elements::Symbol.new(byte.chr) + end + else + name = read_bytes(len) + Elements::Symbol.new(name) + end + end + + EMPTY_STRING = Elements::String.new("".b.freeze).freeze + private_constant :EMPTY_STRING + + def read_string + length = read_integer + return EMPTY_STRING if length == 0 + str = read_bytes(length) + Elements::String.new(str) + end + + def read_true + Elements::True::TRUE + end + + def read_false + Elements::False::FALSE + end + + def read_user_defined + name = read_element + binary_string = read_bytes(read_integer) + Elements::UserDefined.new(name, binary_string) + end + + EMPTY_ARRAY = Elements::Array.new([].freeze).freeze + private_constant :EMPTY_ARRAY + + def read_array + length = read_integer + return EMPTY_ARRAY if length == 0 + raise NegativeLengthError if length < 0 + elements = Array.new(length) do + read_element + end + Elements::Array.new(elements) + end + + def read_object_with_ivars + object = read_element + length = read_integer + raise NegativeLengthError if length < 0 + ivars = Array.new(length) do + [read_element, read_element] + end + Elements::WithIvars.new(object, ivars) + end + + def read_symbol_link + offset = read_integer + Elements::SymbolLink.new(offset) + end + + def read_user_marshal + name = read_element + data = read_element + Elements::UserMarshal.new(name, data) + end + + # profiling bundle install --full-index shows that + # offset 6 is by far the most common object link, + # so we special case it to avoid allocating a new + # object a third of the time. + # the following are all the object links that + # appear more than 10000 times in my profiling + + OBJECT_LINKS = { + 6 => Elements::ObjectLink.new(6).freeze, + 30 => Elements::ObjectLink.new(30).freeze, + 81 => Elements::ObjectLink.new(81).freeze, + 34 => Elements::ObjectLink.new(34).freeze, + 38 => Elements::ObjectLink.new(38).freeze, + 50 => Elements::ObjectLink.new(50).freeze, + 91 => Elements::ObjectLink.new(91).freeze, + 42 => Elements::ObjectLink.new(42).freeze, + 46 => Elements::ObjectLink.new(46).freeze, + 150 => Elements::ObjectLink.new(150).freeze, + 100 => Elements::ObjectLink.new(100).freeze, + 104 => Elements::ObjectLink.new(104).freeze, + 108 => Elements::ObjectLink.new(108).freeze, + 242 => Elements::ObjectLink.new(242).freeze, + 246 => Elements::ObjectLink.new(246).freeze, + 139 => Elements::ObjectLink.new(139).freeze, + 143 => Elements::ObjectLink.new(143).freeze, + 114 => Elements::ObjectLink.new(114).freeze, + 308 => Elements::ObjectLink.new(308).freeze, + 200 => Elements::ObjectLink.new(200).freeze, + 54 => Elements::ObjectLink.new(54).freeze, + 62 => Elements::ObjectLink.new(62).freeze, + 1_286_245 => Elements::ObjectLink.new(1_286_245).freeze, + }.freeze + private_constant :OBJECT_LINKS + + def read_object_link + offset = read_integer + OBJECT_LINKS[offset] || Elements::ObjectLink.new(offset) + end + + EMPTY_HASH = Elements::Hash.new([].freeze).freeze + private_constant :EMPTY_HASH + + def read_hash + length = read_integer + return EMPTY_HASH if length == 0 + pairs = Array.new(length) do + [read_element, read_element] + end + Elements::Hash.new(pairs) + end + + def read_hash_with_default_value + length = read_integer + raise NegativeLengthError if length < 0 + pairs = Array.new(length) do + [read_element, read_element] + end + default = read_element + Elements::HashWithDefaultValue.new(pairs, default) + end + + def read_object + name = read_element + object = Elements::Object.new(name) + length = read_integer + raise NegativeLengthError if length < 0 + ivars = Array.new(length) do + [read_element, read_element] + end + Elements::WithIvars.new(object, ivars) + end + + def read_nil + Elements::Nil::NIL + end + + def read_float + string = read_bytes(read_integer) + Elements::Float.new(string) + end + + def read_bignum + sign = read_byte + data = read_bytes(read_integer * 2) + Elements::Bignum.new(sign, data) + end + + def read_extended_object + raise NotImplementedError, "Reading Marshal objects of type extended_object is not implemented" + end + + def read_class + raise NotImplementedError, "Reading Marshal objects of type class is not implemented" + end + + def read_module + raise NotImplementedError, "Reading Marshal objects of type module is not implemented" + end + + def read_class_or_module + raise NotImplementedError, "Reading Marshal objects of type class_or_module is not implemented" + end + + def read_data + raise NotImplementedError, "Reading Marshal objects of type data is not implemented" + end + + def read_regexp + raise NotImplementedError, "Reading Marshal objects of type regexp is not implemented" + end + + def read_struct + raise NotImplementedError, "Reading Marshal objects of type struct is not implemented" + end + + def read_user_class + name = read_element + wrapped_object = read_element + Elements::UserClass.new(name, wrapped_object) + end + end + end +end diff --git a/lib/rubygems/safe_marshal/visitors/stream_printer.rb b/lib/rubygems/safe_marshal/visitors/stream_printer.rb new file mode 100644 index 0000000000..162b36ad05 --- /dev/null +++ b/lib/rubygems/safe_marshal/visitors/stream_printer.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require_relative "visitor" + +module Gem::SafeMarshal + module Visitors + class StreamPrinter < Visitor + def initialize(io, indent: "") + @io = io + @indent = indent + @level = 0 + end + + def visit(target) + @io.write("#{@indent * @level}#{target.class}") + target.instance_variables.each do |ivar| + value = target.instance_variable_get(ivar) + next if Elements::Element === value || Array === value + @io.write(" #{ivar}=#{value.inspect}") + end + @io.write("\n") + begin + @level += 1 + super + ensure + @level -= 1 + end + end + end + end +end diff --git a/lib/rubygems/safe_marshal/visitors/to_ruby.rb b/lib/rubygems/safe_marshal/visitors/to_ruby.rb new file mode 100644 index 0000000000..a1f9481776 --- /dev/null +++ b/lib/rubygems/safe_marshal/visitors/to_ruby.rb @@ -0,0 +1,428 @@ +# frozen_string_literal: true + +require_relative "visitor" + +module Gem::SafeMarshal + module Visitors + class ToRuby < Visitor + def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:) + @permitted_classes = permitted_classes + @permitted_symbols = ["E"].concat(permitted_symbols).concat(permitted_classes) + @permitted_ivars = permitted_ivars + + @objects = [] + @symbols = [] + @class_cache = {} + + @stack = ["root"] + @stack_idx = 1 + end + + def inspect # :nodoc: + format("#<%s permitted_classes: %p permitted_symbols: %p permitted_ivars: %p>", + self.class, @permitted_classes, @permitted_symbols, @permitted_ivars) + end + + def visit(target) + stack_idx = @stack_idx + super + ensure + @stack_idx = stack_idx - 1 + end + + private + + def push_stack(element) + @stack[@stack_idx] = element + @stack_idx += 1 + end + + def visit_Gem_SafeMarshal_Elements_Array(a) + array = register_object([]) + + elements = a.elements + size = elements.size + idx = 0 + # not idiomatic, but there's a huge number of IMEMOs allocated here, so we avoid the block + # because this is such a hot path when doing a bundle install with the full index + while idx < size + push_stack idx + array << visit(elements[idx]) + idx += 1 + end + + array + end + + def visit_Gem_SafeMarshal_Elements_Symbol(s) + name = s.name + raise UnpermittedSymbolError.new(symbol: name, stack: formatted_stack) unless @permitted_symbols.include?(name) + visit_symbol_type(s) + end + + def map_ivars(klass, ivars) + stack_idx = @stack_idx + ivars.map.with_index do |(k, v), i| + @stack_idx = stack_idx + + push_stack "ivar_" + push_stack i + k = resolve_ivar(klass, k) + + @stack_idx = stack_idx + push_stack k + + next k, visit(v) + end + end + + def visit_Gem_SafeMarshal_Elements_WithIvars(e) + object_offset = @objects.size + push_stack "object" + object = visit(e.object) + ivars = map_ivars(object.class, e.ivars) + + case e.object + when Elements::UserDefined + if object.class == ::Time + internal = [] + + ivars.reject! do |k, v| + case k + when :offset, :zone, :nano_num, :nano_den, :submicro + internal << [k, v] + true + else + false + end + end + + s = e.object.binary_string + # 122 is the largest integer that can be represented in marshal in a single byte + raise TimeTooLargeError.new("binary string too large", stack: formatted_stack) if s.bytesize > 122 + + marshal_string = "\x04\bIu:\tTime".b + marshal_string.concat(s.bytesize + 5) + marshal_string << s + # internal is limited to 5, so no overflow is possible + marshal_string.concat(internal.size + 5) + + internal.each do |k, v| + k = k.name + # ivar name can't be too large because only known ivars are in the internal ivars list + marshal_string.concat(":") + marshal_string.concat(k.bytesize + 5) + marshal_string.concat(k) + dumped = Marshal.dump(v) + dumped[0, 2] = "" + marshal_string.concat(dumped) + end + + object = @objects[object_offset] = Marshal.load(marshal_string) + end + when Elements::String + enc = nil + + ivars.reject! do |k, v| + case k + when :E + case v + when TrueClass + enc = "UTF-8" + when FalseClass + enc = "US-ASCII" + else + raise FormatError, "Unexpected value for String :E #{v.inspect}" + end + when :encoding + enc = v + else + next false + end + true + end + + object.force_encoding(enc) if enc + end + + ivars.each do |k, v| + object.instance_variable_set k, v + end + object + end + + def visit_Gem_SafeMarshal_Elements_Hash(o) + hash = register_object({}) + + o.pairs.each_with_index do |(k, v), i| + push_stack i + k = visit(k) + push_stack k + hash[k] = visit(v) + end + + hash + end + + def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o) + hash = visit_Gem_SafeMarshal_Elements_Hash(o) + push_stack :default + hash.default = visit(o.default) + hash + end + + def visit_Gem_SafeMarshal_Elements_Object(o) + register_object(resolve_class(o.name).allocate) + end + + def visit_Gem_SafeMarshal_Elements_ObjectLink(o) + @objects.fetch(o.offset) + end + + def visit_Gem_SafeMarshal_Elements_SymbolLink(o) + @symbols.fetch(o.offset) + end + + def visit_Gem_SafeMarshal_Elements_UserDefined(o) + register_object(call_method(resolve_class(o.name), :_load, o.binary_string)) + end + + def visit_Gem_SafeMarshal_Elements_UserMarshal(o) + klass = resolve_class(o.name) + compat = COMPAT_CLASSES.fetch(klass, nil) + idx = @objects.size + object = register_object(call_method(compat || klass, :allocate)) + + push_stack :data + ret = call_method(object, :marshal_load, visit(o.data)) + + if compat + object = @objects[idx] = ret + end + + object + end + + def visit_Gem_SafeMarshal_Elements_Integer(i) + i.int + end + + def visit_Gem_SafeMarshal_Elements_Nil(_) + nil + end + + def visit_Gem_SafeMarshal_Elements_True(_) + true + end + + def visit_Gem_SafeMarshal_Elements_False(_) + false + end + + def visit_Gem_SafeMarshal_Elements_String(s) + register_object(+s.str) + end + + def visit_Gem_SafeMarshal_Elements_Float(f) + register_object( + case f.string + when "inf" + ::Float::INFINITY + when "-inf" + -::Float::INFINITY + when "nan" + ::Float::NAN + else + f.string.to_f + end + ) + end + + def visit_Gem_SafeMarshal_Elements_Bignum(b) + result = 0 + b.data.each_byte.with_index do |byte, exp| + result += (byte * 2**(exp * 8)) + end + + case b.sign + when 43 # ?+ + result + when 45 # ?- + -result + else + raise FormatError, "Unexpected sign for Bignum #{b.sign.chr.inspect} (#{b.sign})" + end + end + + def visit_Gem_SafeMarshal_Elements_UserClass(r) + if resolve_class(r.name) == ::Hash && r.wrapped_object.is_a?(Elements::Hash) + + hash = register_object({}.compare_by_identity) + + o = r.wrapped_object + o.pairs.each_with_index do |(k, v), i| + push_stack i + k = visit(k) + push_stack k + hash[k] = visit(v) + end + + if o.is_a?(Elements::HashWithDefaultValue) + push_stack :default + hash.default = visit(o.default) + end + + hash + else + raise UnsupportedError.new("Unsupported user class #{resolve_class(r.name)} in marshal stream", stack: formatted_stack) + end + end + + def resolve_class(n) + @class_cache[n] ||= begin + to_s = resolve_symbol_name(n) + raise UnpermittedClassError.new(name: to_s, stack: formatted_stack) unless @permitted_classes.include?(to_s) + visit_symbol_type(n) + begin + ::Object.const_get(to_s) + rescue NameError + raise ArgumentError, "Undefined class #{to_s.inspect}" + end + end + end + + class RationalCompat + def marshal_load(s) + num, den = s + raise ArgumentError, "Expected 2 ints" unless s.size == 2 && num.is_a?(Integer) && den.is_a?(Integer) + Rational(num, den) + end + end + private_constant :RationalCompat + + COMPAT_CLASSES = {}.tap do |h| + h[Rational] = RationalCompat + end.compare_by_identity.freeze + private_constant :COMPAT_CLASSES + + def resolve_ivar(klass, name) + to_s = resolve_symbol_name(name) + + raise UnpermittedIvarError.new(symbol: to_s, klass: klass, stack: formatted_stack) unless @permitted_ivars.fetch(klass.name, [].freeze).include?(to_s) + + visit_symbol_type(name) + end + + def visit_symbol_type(element) + case element + when Elements::Symbol + sym = element.name.to_sym + @symbols << sym + sym + when Elements::SymbolLink + visit_Gem_SafeMarshal_Elements_SymbolLink(element) + end + end + + # This is a hot method, so avoid respond_to? checks on every invocation + if :read.respond_to?(:name) + def resolve_symbol_name(element) + case element + when Elements::Symbol + element.name + when Elements::SymbolLink + visit_Gem_SafeMarshal_Elements_SymbolLink(element).name + else + raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}" + end + end + else + def resolve_symbol_name(element) + case element + when Elements::Symbol + element.name + when Elements::SymbolLink + visit_Gem_SafeMarshal_Elements_SymbolLink(element).to_s + else + raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}" + end + end + end + + def register_object(o) + @objects << o + o + end + + def call_method(receiver, method, *args) + receiver.__send__(method, *args) + rescue NoMethodError => e + raise unless e.receiver == receiver + + raise MethodCallError, "Unable to call #{method.inspect} on #{receiver.inspect}, perhaps it is a class using marshal compat, which is not visible in ruby? #{e}" + end + + def formatted_stack + formatted = [] + @stack[0, @stack_idx].each do |e| + if e.is_a?(Integer) + if formatted.last == "ivar_" + formatted[-1] = "ivar_#{e}" + else + formatted << "[#{e}]" + end + else + formatted << e + end + end + formatted + end + + class Error < StandardError + end + + class TimeTooLargeError < Error + def initialize(message, stack:) + super "#{message} @ #{stack.join "."}" + end + end + + class UnpermittedSymbolError < Error + def initialize(symbol:, stack:) + @symbol = symbol + @stack = stack + super "Attempting to load unpermitted symbol #{symbol.inspect} @ #{stack.join "."}" + end + end + + class UnpermittedIvarError < Error + def initialize(symbol:, klass:, stack:) + @symbol = symbol + @klass = klass + @stack = stack + super "Attempting to set unpermitted ivar #{symbol.inspect} on object of class #{klass} @ #{stack.join "."}" + end + end + + class UnpermittedClassError < Error + def initialize(name:, stack:) + @name = name + @stack = stack + super "Attempting to load unpermitted class #{name.inspect} @ #{stack.join "."}" + end + end + + class UnsupportedError < Error + def initialize(message, stack:) + super "#{message} @ #{stack.join "."}" + end + end + + class FormatError < Error + end + + class MethodCallError < Error + end + end + end +end diff --git a/lib/rubygems/safe_marshal/visitors/visitor.rb b/lib/rubygems/safe_marshal/visitors/visitor.rb new file mode 100644 index 0000000000..c9a079dc0e --- /dev/null +++ b/lib/rubygems/safe_marshal/visitors/visitor.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module Gem::SafeMarshal::Visitors + class Visitor + def visit(target) + send DISPATCH.fetch(target.class), target + end + + private + + DISPATCH = Gem::SafeMarshal::Elements.constants.each_with_object({}) do |c, h| + next if c == :Element + + klass = Gem::SafeMarshal::Elements.const_get(c) + h[klass] = :"visit_#{klass.name.gsub("::", "_")}" + h.default = :visit_unknown_element + end.compare_by_identity.freeze + private_constant :DISPATCH + + def visit_unknown_element(e) + raise ArgumentError, "Attempting to visit unknown element #{e.inspect}" + end + + def visit_Gem_SafeMarshal_Elements_Array(target) + target.elements.each {|e| visit(e) } + end + + def visit_Gem_SafeMarshal_Elements_Bignum(target); end + def visit_Gem_SafeMarshal_Elements_False(target); end + def visit_Gem_SafeMarshal_Elements_Float(target); end + + def visit_Gem_SafeMarshal_Elements_Hash(target) + target.pairs.each do |k, v| + visit(k) + visit(v) + end + end + + def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(target) + visit_Gem_SafeMarshal_Elements_Hash(target) + visit(target.default) + end + + def visit_Gem_SafeMarshal_Elements_Integer(target); end + def visit_Gem_SafeMarshal_Elements_Nil(target); end + + def visit_Gem_SafeMarshal_Elements_Object(target) + visit(target.name) + end + + def visit_Gem_SafeMarshal_Elements_ObjectLink(target); end + def visit_Gem_SafeMarshal_Elements_String(target); end + def visit_Gem_SafeMarshal_Elements_Symbol(target); end + def visit_Gem_SafeMarshal_Elements_SymbolLink(target); end + def visit_Gem_SafeMarshal_Elements_True(target); end + + def visit_Gem_SafeMarshal_Elements_UserDefined(target) + visit(target.name) + end + + def visit_Gem_SafeMarshal_Elements_UserMarshal(target) + visit(target.name) + visit(target.data) + end + + def visit_Gem_SafeMarshal_Elements_WithIvars(target) + visit(target.object) + target.ivars.each do |k, v| + visit(k) + visit(v) + end + end + end +end diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb index 5a98505598..f4bba00136 100644 --- a/lib/rubygems/safe_yaml.rb +++ b/lib/rubygems/safe_yaml.rb @@ -1,5 +1,6 @@ -module Gem +# frozen_string_literal: true +module Gem ### # This module is used for safely loading YAML specs from a gem. The # `safe_load` method defined on this module is specifically designed for @@ -24,34 +25,31 @@ module Gem runtime ].freeze - if ::Psych.respond_to? :safe_load - def self.safe_load(input) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1.0.pre1") - ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: true) - else - ::Psych.safe_load(input, PERMITTED_CLASSES, PERMITTED_SYMBOLS, true) - end - end + @aliases_enabled = true + def self.aliases_enabled=(value) # :nodoc: + @aliases_enabled = !!value + end - def self.load(input) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1.0.pre1") - ::Psych.safe_load(input, permitted_classes: [::Symbol]) - else - ::Psych.safe_load(input, [::Symbol]) - end - end - else - unless Gem::Deprecate.skip - warn "Psych safe loading is not available. Please upgrade psych to a version that supports safe loading (>= 2.0)." - end + def self.aliases_enabled? # :nodoc: + @aliases_enabled + end - def self.safe_load(input, *args) - ::Psych.load input + def self.safe_load(input) + if Gem.use_psych? + ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, + permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled) + else + Gem::YAMLSerializer.load( + input, + permitted_classes: PERMITTED_CLASSES, + permitted_symbols: PERMITTED_SYMBOLS, + aliases: aliases_enabled? + ) end + end - def self.load(input) - ::Psych.load input - end + class << self + alias_method :load, :safe_load end end end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index 3ba8c6957c..69ba87b07f 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -322,10 +323,9 @@ require_relative "openssl" # == Original author # # Paul Duncan <pabs@pablotron.org> -# http://pablotron.org/ +# https://pablotron.org/ module Gem::Security - ## # Gem::Security default exception type @@ -360,7 +360,7 @@ module Gem::Security ## # One day in seconds - ONE_DAY = 86400 + ONE_DAY = 86_400 ## # One year in seconds @@ -398,8 +398,7 @@ module Gem::Security # # The +extensions+ restrict the key to the indicated uses. - def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, - serial = 1) + def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) cert = OpenSSL::X509::Certificate.new cert.public_key = get_public_key(key) @@ -450,8 +449,7 @@ module Gem::Security # Creates a self-signed certificate with an issuer and subject of +subject+ # and the given +extensions+ for the +key+. - def self.create_cert_self_signed(subject, key, age = ONE_YEAR, - extensions = EXTENSIONS, serial = 1) + def self.create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) certificate = create_cert subject, key, age, extensions sign certificate, key, certificate, age, extensions, serial @@ -461,16 +459,8 @@ module Gem::Security # Creates a new digest instance using the specified +algorithm+. The default # is SHA256. - if defined?(OpenSSL::Digest) - def self.create_digest(algorithm = DIGEST_NAME) - OpenSSL::Digest.new(algorithm) - end - else - require "digest" - - def self.create_digest(algorithm = DIGEST_NAME) - Digest.const_get(algorithm).new - end + def self.create_digest(algorithm = DIGEST_NAME) + OpenSSL::Digest.new(algorithm) end ## @@ -515,11 +505,10 @@ module Gem::Security #-- # TODO increment serial - def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, - extensions = EXTENSIONS) + def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, extensions = EXTENSIONS) raise Gem::Security::Exception, "incorrect signing key for re-signing " + - "#{expired_certificate.subject}" unless + expired_certificate.subject.to_s unless expired_certificate.check_private_key(private_key) unless expired_certificate.subject.to_s == @@ -528,7 +517,7 @@ module Gem::Security issuer = alt_name_or_x509_entry expired_certificate, :issuer raise Gem::Security::Exception, - "#{subject} is not self-signed, contact #{issuer} " + + "#{subject} is not self-signed, contact #{issuer} " \ "to obtain a valid certificate" end @@ -552,8 +541,7 @@ module Gem::Security # # Returns the newly signed certificate. - def self.sign(certificate, signing_key, signing_cert, - age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) + def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) signee_subject = certificate.subject signee_key = certificate.public_key @@ -601,7 +589,7 @@ module Gem::Security # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be # passed to +to_pem+. - def self.write(pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER) + def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER) path = File.expand_path path File.open path, "wb", permissions do |io| @@ -616,7 +604,6 @@ module Gem::Security end reset - end if Gem::HAVE_OPENSSL diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb index d28005223e..41f66043ad 100644 --- a/lib/rubygems/security/policies.rb +++ b/lib/rubygems/security/policies.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -module Gem::Security +module Gem::Security ## # No security policy: all package signature checks are disabled. NoSecurity = Policy.new( "No Security", - :verify_data => false, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: false, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -24,12 +24,12 @@ module Gem::Security AlmostNoSecurity = Policy.new( "Almost No Security", - :verify_data => true, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: true, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -41,12 +41,12 @@ module Gem::Security LowSecurity = Policy.new( "Low Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: true, + verify_signer: true, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -60,12 +60,12 @@ module Gem::Security MediumSecurity = Policy.new( "Medium Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => false + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: false ) ## @@ -79,12 +79,12 @@ module Gem::Security HighSecurity = Policy.new( "High Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => true + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: true ) ## @@ -92,12 +92,12 @@ module Gem::Security SigningPolicy = Policy.new( "Signing Policy", - :verify_data => false, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => false, - :only_signed => false + verify_data: false, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: false, + only_signed: false ) ## @@ -111,5 +111,4 @@ module Gem::Security "HighSecurity" => HighSecurity, # SigningPolicy is not intended for use by `gem -P` so do not list it }.freeze - end diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 959880ddc1..128958ab80 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../user_interaction" ## @@ -134,7 +135,7 @@ class Gem::Security::Policy raise Gem::Security::Exception, "missing root certificate" unless root raise Gem::Security::Exception, - "root certificate #{root.subject} is not self-signed " + + "root certificate #{root.subject} is not self-signed " \ "(issuer #{root.issuer})" if root.issuer != root.subject @@ -142,7 +143,7 @@ class Gem::Security::Policy end ## - # Ensures the root of +chain+ has a trusted certificate in +trust_dir+ and + # Ensures the root of +chain+ has a trusted certificate in Gem::Security.trust_dir and # the digests of the two certificates match according to +digester+ def check_trust(chain, digester, trust_dir) @@ -170,7 +171,7 @@ class Gem::Security::Policy cert_dgst = digester.digest pkey_str raise Gem::Security::Exception, - "trusted root certificate #{root.subject} checksum " + + "trusted root certificate #{root.subject} checksum " \ "does not match signing root certificate checksum" unless save_dgst == cert_dgst @@ -191,11 +192,8 @@ class Gem::Security::Policy end def inspect # :nodoc: - ("[Policy: %s - data: %p signer: %p chain: %p root: %p " + - "signed-only: %p trusted-only: %p]") % [ - @name, @verify_chain, @verify_data, @verify_root, @verify_signer, - @only_signed, @only_trusted - ] + format("[Policy: %s - data: %p signer: %p chain: %p root: %p " \ + "signed-only: %p trusted-only: %p]", @name, @verify_chain, @verify_data, @verify_root, @verify_signer, @only_signed, @only_trusted) end ## @@ -205,8 +203,7 @@ class Gem::Security::Policy # # If +key+ is given it is used to validate the signing certificate. - def verify(chain, key = nil, digests = {}, signatures = {}, - full_name = "(unknown)") + def verify(chain, key = nil, digests = {}, signatures = {}, full_name = "(unknown)") if signatures.empty? if @only_signed raise Gem::Security::Exception, @@ -225,7 +222,7 @@ class Gem::Security::Policy trust_dir = opt[:trust_dir] time = Time.now - _, signer_digests = digests.find do |algorithm, file_digests| + _, signer_digests = digests.find do |_algorithm, file_digests| file_digests.values.first.name == Gem::Security::DIGEST_NAME end @@ -287,5 +284,5 @@ class Gem::Security::Policy true end - alias to_s name # :nodoc: + alias_method :to_s, :name # :nodoc: end diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index cca82f1cf8..eeeeb52906 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Basic OpenSSL-based package signing class. @@ -51,7 +52,7 @@ class Gem::Security::Signer re_signed_cert = Gem::Security.re_sign( expired_cert, private_key, - (Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days) + Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days ) Gem::Security.write(re_signed_cert, expired_cert_path) @@ -105,7 +106,7 @@ class Gem::Security::Signer # this value is preferred, otherwise the subject is used. def extract_name(cert) # :nodoc: - subject_alt_name = cert.extensions.find {|e| "subjectAltName" == e.oid } + subject_alt_name = cert.extensions.find {|e| e.oid == "subjectAltName" } if subject_alt_name /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith @@ -174,10 +175,18 @@ class Gem::Security::Signer old_cert = @cert_chain.last disk_cert_path = File.join(Gem.default_cert_path) - disk_cert = File.read(disk_cert_path) rescue nil + disk_cert = begin + File.read(disk_cert_path) + rescue StandardError + nil + end disk_key_path = File.join(Gem.default_key_path) - disk_key = OpenSSL::PKey.read(File.read(disk_key_path), @passphrase) rescue nil + disk_key = begin + OpenSSL::PKey.read(File.read(disk_key_path), @passphrase) + rescue StandardError + nil + end return unless disk_key diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb index df59680d84..d23d161cfe 100644 --- a/lib/rubygems/security/trust_dir.rb +++ b/lib/rubygems/security/trust_dir.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The TrustDir manages the trusted certificates for gem signature # verification. @@ -8,8 +9,8 @@ class Gem::Security::TrustDir # Default permissions for the trust directory and its contents DEFAULT_PERMISSIONS = { - :trust_dir => 0700, - :trusted_cert => 0600, + trust_dir: 0o700, + trusted_cert: 0o600, }.freeze ## @@ -44,13 +45,11 @@ class Gem::Security::TrustDir glob = File.join @dir, "*.pem" Dir[glob].each do |certificate_file| - begin - certificate = load_certificate certificate_file + certificate = load_certificate certificate_file - yield certificate, certificate_file - rescue OpenSSL::X509::CertificateError - next # HACK warn - end + yield certificate, certificate_file + rescue OpenSSL::X509::CertificateError + next # HACK: warn end end @@ -92,7 +91,7 @@ class Gem::Security::TrustDir destination = cert_path certificate - File.open destination, "wb", 0600 do |io| + File.open destination, "wb", 0o600 do |io| io.write certificate.to_pem io.chmod(@permissions[:trusted_cert]) end @@ -110,9 +109,9 @@ class Gem::Security::TrustDir "trust directory #{@dir} is not a directory" unless File.directory? @dir - FileUtils.chmod 0700, @dir + FileUtils.chmod 0o700, @dir else - FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir] + FileUtils.mkdir_p @dir, mode: @permissions[:trust_dir] end end end diff --git a/lib/rubygems/security_option.rb b/lib/rubygems/security_option.rb index ab3898bf11..3a101fe9db 100644 --- a/lib/rubygems/security_option.rb +++ b/lib/rubygems/security_option.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -28,7 +29,7 @@ module Gem::SecurityOption policy = Gem::Security::Policies[value] unless policy valid = Gem::Security::Policies.keys.sort - raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ', '} are valid)" + raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ", "} are valid)" end policy end diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index aa0cbc1641..86717e3e71 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -5,16 +5,16 @@ require_relative "text" # A Source knows how to list and fetch gems from a RubyGems marshal index. # # There are other Source subclasses for installed gems, local gems, the -# bundler dependency API and so-forth. +# Compact Index API and so-forth. class Gem::Source include Comparable include Gem::Text FILES = { # :nodoc: - :released => "specs", - :latest => "latest_specs", - :prerelease => "prerelease_specs", + released: "specs", + latest: "latest_specs", + prerelease: "prerelease_specs", }.freeze ## @@ -44,20 +44,18 @@ class Gem::Source Gem::Source::Vendor then -1 when Gem::Source then - if !@uri + unless @uri return 0 unless other.uri return 1 end - return -1 if !other.uri + return -1 unless other.uri # Returning 1 here ensures that when sorting a list of sources, the # original ordering of sources supplied by the user is preserved. return 1 unless @uri.to_s == other.uri.to_s 0 - else - nil end end @@ -69,28 +67,11 @@ class Gem::Source ## # Returns a Set that can fetch specifications from this source. - - def dependency_resolver_set # :nodoc: - return Gem::Resolver::IndexSet.new self if "file" == uri.scheme - - fetch_uri = if uri.host == "rubygems.org" - index_uri = uri.dup - index_uri.host = "index.rubygems.org" - index_uri - else - uri - end - - bundler_api_uri = enforce_trailing_slash(fetch_uri) - - begin - fetcher = Gem::RemoteFetcher.fetcher - response = fetcher.fetch_path bundler_api_uri, nil, true - rescue Gem::RemoteFetcher::FetchError - Gem::Resolver::IndexSet.new self - else - Gem::Resolver::APISet.new response.uri + "./info/" - end + # + # The set will optionally fetch prereleases if requested. + # + def dependency_resolver_set(prerelease = false) + new_dependency_resolver_set.tap {|set| set.prerelease = prerelease } end def hash # :nodoc: @@ -102,8 +83,7 @@ class Gem::Source def cache_dir(uri) # Correct for windows paths - escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/') - escaped_path.tap(&Gem::UNTAINT) + escaped_path = uri.path.sub(%r{^/([a-z]):/}i, '/\\1-/') File.join Gem.spec_cache_dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path) end @@ -122,7 +102,7 @@ class Gem::Source end ## - # Fetches a specification for the given +name_tuple+. + # Fetches a specification for the given Gem::NameTuple. def fetch_spec(name_tuple) fetcher = Gem::RemoteFetcher.fetcher @@ -137,7 +117,12 @@ class Gem::Source if File.exist? local_spec spec = Gem.read_binary local_spec - spec = Marshal.load(spec) rescue nil + Gem.load_safe_marshal + spec = begin + Gem::SafeMarshal.safe_load(spec) + rescue StandardError + nil + end return spec if spec end @@ -155,8 +140,9 @@ class Gem::Source end end + Gem.load_safe_marshal # TODO: Investigate setting Gem::Specification#loaded_from to a URI - Marshal.load spec + Gem::SafeMarshal.safe_load spec end ## @@ -186,8 +172,9 @@ class Gem::Source spec_dump = fetcher.cache_update_path spec_path, local_file, update_cache? + Gem.load_safe_marshal begin - Gem::NameTuple.from_list Marshal.load(spec_dump) + Gem::NameTuple.from_list Gem::SafeMarshal.safe_load(spec_dump) rescue ArgumentError if update_cache? && !retried FileUtils.rm local_file @@ -203,33 +190,58 @@ class Gem::Source # Downloads +spec+ and writes it to +dir+. See also # Gem::RemoteFetcher#download. - def download(spec, dir=Dir.pwd) + def download(spec, dir = Dir.pwd) fetcher = Gem::RemoteFetcher.fetcher fetcher.download spec, uri.to_s, dir end def pretty_print(q) # :nodoc: - q.group 2, "[Remote:", "]" do - q.breakable - q.text @uri.to_s - - if api = uri + q.object_group(self) do + q.group 2, "[Remote:", "]" do q.breakable - q.text "API URI: " - q.text api.to_s + q.text @uri.to_s + + if api = uri + q.breakable + q.text "API URI: " + q.text api.to_s + end end end end - def typo_squatting?(host, distance_threshold=4) + def typo_squatting?(host, distance_threshold = 4) return if @uri.host.nil? levenshtein_distance(@uri.host, host).between? 1, distance_threshold end private + def new_dependency_resolver_set + return Gem::Resolver::IndexSet.new self if uri.scheme == "file" + + fetch_uri = if uri.host == "rubygems.org" + index_uri = uri.dup + index_uri.host = "index.rubygems.org" + index_uri + else + uri + end + + bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions" + + begin + fetcher = Gem::RemoteFetcher.fetcher + response = fetcher.fetch_path bundler_api_uri, nil, true + rescue Gem::RemoteFetcher::FetchError + Gem::Resolver::IndexSet.new self + else + Gem::Resolver::APISet.new response.uri + "./info/" + end + end + def enforce_trailing_slash(uri) - uri.merge(uri.path.gsub(/\/+$/, "") + "/") + uri.merge(uri.path.gsub(%r{/+$}, "") + "/") end end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index 2609a309e8..baf2f9dd4c 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -53,12 +53,11 @@ class Gem::Source::Git < Gem::Source @uri = Gem::Uri.parse(repository) @name = name @repository = repository - @reference = reference + @reference = reference || "HEAD" @need_submodules = submodules @remote = true @root_dir = Gem.dir - @git = ENV["git"] || "git" end def <=>(other) @@ -70,8 +69,6 @@ class Gem::Source::Git < Gem::Source -1 when Gem::Source then 1 - else - nil end end @@ -83,6 +80,10 @@ class Gem::Source::Git < Gem::Source @need_submodules == other.need_submodules end + def git_command + ENV.fetch("git", "git") + end + ## # Checks out the files for the repository into the install_dir. @@ -92,18 +93,18 @@ class Gem::Source::Git < Gem::Source return false unless File.exist? repo_cache_dir unless File.exist? install_dir - system @git, "clone", "--quiet", "--no-checkout", + system git_command, "clone", "--quiet", "--no-checkout", repo_cache_dir, install_dir end Dir.chdir install_dir do - system @git, "fetch", "--quiet", "--force", "--tags", install_dir + system git_command, "fetch", "--quiet", "--force", "--tags", install_dir - success = system @git, "reset", "--quiet", "--hard", rev_parse + success = system git_command, "reset", "--quiet", "--hard", rev_parse if @need_submodules require "open3" - _, status = Open3.capture2e(@git, "submodule", "update", "--quiet", "--init", "--recursive") + _, status = Open3.capture2e(git_command, "submodule", "update", "--quiet", "--init", "--recursive") success &&= status.success? end @@ -120,11 +121,11 @@ class Gem::Source::Git < Gem::Source if File.exist? repo_cache_dir Dir.chdir repo_cache_dir do - system @git, "fetch", "--quiet", "--force", "--tags", + system git_command, "fetch", "--quiet", "--force", "--tags", @repository, "refs/heads/*:refs/heads/*" end else - system @git, "clone", "--quiet", "--bare", "--no-hardlinks", + system git_command, "clone", "--quiet", "--bare", "--no-hardlinks", @repository, repo_cache_dir end end @@ -159,12 +160,14 @@ class Gem::Source::Git < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[Git: ", "]" do - q.breakable - q.text @repository + q.object_group(self) do + q.group 2, "[Git: ", "]" do + q.breakable + q.text @repository - q.breakable - q.text @reference + q.breakable + q.text @reference + end end end @@ -182,7 +185,7 @@ class Gem::Source::Git < Gem::Source hash = nil Dir.chdir repo_cache_dir do - hash = Gem::Util.popen(@git, "rev-parse", @reference).strip + hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip end raise Gem::Exception, @@ -201,7 +204,7 @@ class Gem::Source::Git < Gem::Source return [] unless install_dir Dir.chdir install_dir do - Dir["{,*,*/*}.gemspec"].map do |spec_file| + Dir["{,*,*/*}.gemspec"].filter_map do |spec_file| directory = File.dirname spec_file file = File.basename spec_file @@ -218,19 +221,19 @@ class Gem::Source::Git < Gem::Source end spec end - end.compact + end end end ## - # A hash for the git gem based on the git repository URI. + # A hash for the git gem based on the git repository Gem::URI. def uri_hash # :nodoc: require_relative "../openssl" normalized = - if @repository =~ %r{^\w+://(\w+@)?} - uri = URI(@repository).normalize.to_s.sub %r{/$},"" + if @repository.match?(%r{^\w+://(\w+@)?}) + uri = Gem::URI(@repository).normalize.to_s.sub %r{/$},"" uri.sub(/\A(\w+)/) { $1.downcase } else @repository diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb index 786faab3e3..f5c96fee51 100644 --- a/lib/rubygems/source/installed.rb +++ b/lib/rubygems/source/installed.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Represents an installed gem. This is used for dependency resolution. @@ -20,8 +21,6 @@ class Gem::Source::Installed < Gem::Source 0 when Gem::Source then 1 - else - nil end end @@ -33,6 +32,8 @@ class Gem::Source::Installed < Gem::Source end def pretty_print(q) # :nodoc: - q.text "[Installed]" + q.object_group(self) do + q.text "[Installed]" + end end end diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index ec1a594238..4bef31a265 100644 --- a/lib/rubygems/source/local.rb +++ b/lib/rubygems/source/local.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The local source finds gems in the current directory for fulfilling # dependencies. @@ -23,14 +24,12 @@ class Gem::Source::Local < Gem::Source 0 when Gem::Source then 1 - else - nil end end def inspect # :nodoc: keys = @specs ? @specs.keys.sort : "NOT LOADED" - "#<%s specs: %p>" % [self.class, keys] + format("#<%s specs: %p>", self.class, keys) end def load_specs(type) # :nodoc: @@ -40,36 +39,35 @@ class Gem::Source::Local < Gem::Source @specs = {} Dir["*.gem"].each do |file| - begin - pkg = Gem::Package.new(file) - rescue SystemCallError, Gem::Package::FormatError - # ignore - else - tup = pkg.spec.name_tuple - @specs[tup] = [File.expand_path(file), pkg] - - case type - when :released - unless pkg.spec.version.prerelease? - names << pkg.spec.name_tuple - end - when :prerelease - if pkg.spec.version.prerelease? - names << pkg.spec.name_tuple - end - when :latest - tup = pkg.spec.name_tuple - - cur = names.find {|x| x.name == tup.name } - if !cur - names << tup - elsif cur.version < tup.version - names.delete cur - names << tup - end - else + pkg = Gem::Package.new(file) + spec = pkg.spec + rescue SystemCallError, Gem::Package::FormatError + # ignore + else + tup = spec.name_tuple + @specs[tup] = [File.expand_path(file), pkg] + + case type + when :released + unless pkg.spec.version.prerelease? names << pkg.spec.name_tuple end + when :prerelease + if pkg.spec.version.prerelease? + names << pkg.spec.name_tuple + end + when :latest + tup = pkg.spec.name_tuple + + cur = names.find {|x| x.name == tup.name } + if !cur + names << tup + elsif cur.version < tup.version + names.delete cur + names << tup + end + else + names << pkg.spec.name_tuple end end @@ -77,27 +75,29 @@ class Gem::Source::Local < Gem::Source end end - def find_gem(gem_name, version = Gem::Requirement.default, # :nodoc: - prerelease = false) + def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: + find_all_gems(gem_name, version, prerelease).max_by(&:version) + end + + def find_all_gems(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc: load_specs :complete found = [] @specs.each do |n, data| - if n.name == gem_name - s = data[1].spec - - if version.satisfied_by?(s.version) - if prerelease - found << s - elsif !s.version.prerelease? || version.prerelease? - found << s - end + next unless n.name == gem_name + s = data[1].spec + + if version.satisfied_by?(s.version) + if prerelease + found << s + elsif !s.version.prerelease? || version.prerelease? + found << s end end end - found.max_by {|s| s.version } + found end def fetch_spec(name) # :nodoc: @@ -113,7 +113,7 @@ class Gem::Source::Local < Gem::Source def download(spec, cache_dir = nil) # :nodoc: load_specs :complete - @specs.each do |name, data| + @specs.each do |_name, data| return data[0] if data[1].spec == spec end @@ -121,10 +121,14 @@ class Gem::Source::Local < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[Local gems:", "]" do - q.breakable - q.seplist @specs.keys do |v| - q.text v.full_name + q.object_group(self) do + q.group 2, "[Local gems:", "]" do + q.breakable + if @specs + q.seplist @specs.keys do |v| + q.text v.full_name + end + end end end end diff --git a/lib/rubygems/source/lock.rb b/lib/rubygems/source/lock.rb index 49f097467b..70849210bd 100644 --- a/lib/rubygems/source/lock.rb +++ b/lib/rubygems/source/lock.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A Lock source wraps an installed gem's source and sorts before other sources # during dependency resolution. This allows RubyGems to prefer gems from @@ -24,13 +25,11 @@ class Gem::Source::Lock < Gem::Source @wrapped <=> other.wrapped when Gem::Source then 1 - else - nil end end def ==(other) # :nodoc: - 0 == (self <=> other) + (self <=> other) == 0 end def hash # :nodoc: diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb index 552aeba50f..dde1d48a21 100644 --- a/lib/rubygems/source/specific_file.rb +++ b/lib/rubygems/source/specific_file.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A source representing a single .gem file. This is used for installation of # local gems. @@ -33,7 +34,6 @@ class Gem::Source::SpecificFile < Gem::Source def fetch_spec(name) # :nodoc: return @spec if name == @name raise Gem::Exception, "Unable to find '#{name}'" - @spec end def download(spec, dir = nil) # :nodoc: @@ -42,9 +42,11 @@ class Gem::Source::SpecificFile < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, "[SpecificFile:", "]" do - q.breakable - q.text @path + q.object_group(self) do + q.group 2, "[SpecificFile:", "]" do + q.breakable + q.text @path + end end end diff --git a/lib/rubygems/source/vendor.rb b/lib/rubygems/source/vendor.rb index 543acf1388..44ef614441 100644 --- a/lib/rubygems/source/vendor.rb +++ b/lib/rubygems/source/vendor.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # This represents a vendored source that is similar to an installed gem. @@ -18,8 +19,6 @@ class Gem::Source::Vendor < Gem::Source::Installed 0 when Gem::Source then 1 - else - nil end end end diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb index 7abe796409..19bf4595c4 100644 --- a/lib/rubygems/source_list.rb +++ b/lib/rubygems/source_list.rb @@ -36,7 +36,7 @@ class Gem::SourceList list.replace ary - return list + list end def initialize_copy(other) # :nodoc: @@ -44,15 +44,15 @@ class Gem::SourceList end ## - # Appends +obj+ to the source list which may be a Gem::Source, URI or URI + # Appends +obj+ to the source list which may be a Gem::Source, Gem::URI or URI # String. def <<(obj) src = case obj - when Gem::Source - obj - else - Gem::Source.new(obj) + when Gem::Source + obj + else + Gem::Source.new(obj) end @sources << src unless @sources.include?(src) @@ -60,6 +60,42 @@ class Gem::SourceList end ## + # Prepends +obj+ to the beginning of the source list which may be a Gem::Source, Gem::URI or URI + # Moves +obj+ to the beginning of the list if already present. + # String. + + def prepend(obj) + src = case obj + when Gem::Source + obj + else + Gem::Source.new(obj) + end + + @sources.delete(src) if @sources.include?(src) + @sources.unshift(src) + src + end + + ## + # Appends +obj+ to the end of the source list, moving it if already present. + # +obj+ may be a Gem::Source, Gem::URI or URI String. + # Moves +obj+ to the end of the list if already present. + + def append(obj) + src = case obj + when Gem::Source + obj + else + Gem::Source.new(obj) + end + + @sources.delete(src) if @sources.include?(src) + @sources << src + src + end + + ## # Replaces this SourceList with the sources in +other+ See #<< for # acceptable items in +other+. @@ -126,7 +162,7 @@ class Gem::SourceList # Gem::Source or a source URI. def include?(other) - if other.kind_of? Gem::Source + if other.is_a? Gem::Source @sources.include? other else @sources.find {|x| x.uri.to_s == other.to_s } @@ -137,7 +173,7 @@ class Gem::SourceList # Deletes +source+ from the source list which may be a Gem::Source or a URI. def delete(source) - if source.kind_of? Gem::Source + if source.is_a? Gem::Source @sources.delete source else @sources.delete_if {|x| x.uri.to_s == source.to_s } diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb index 0d06d1f144..835dedf948 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "remote_fetcher" require_relative "user_interaction" require_relative "errors" @@ -68,9 +69,9 @@ class Gem::SpecFetcher @prerelease_specs = {} @caches = { - :latest => @latest_specs, - :prerelease => @prerelease_specs, - :released => @specs, + latest: @latest_specs, + prerelease: @prerelease_specs, + released: @specs, } @fetcher = Gem::RemoteFetcher.fetcher @@ -82,7 +83,7 @@ class Gem::SpecFetcher # # If +matching_platform+ is false, gems for all platforms are returned. - def search_for_dependency(dependency, matching_platform=true) + def search_for_dependency(dependency, matching_platform = true) found = {} rejected_specs = {} @@ -91,9 +92,9 @@ class Gem::SpecFetcher list.each do |source, specs| if dependency.name.is_a?(String) && specs.respond_to?(:bsearch) - start_index = (0 ... specs.length).bsearch {|i| specs[i].name >= dependency.name } - end_index = (0 ... specs.length).bsearch {|i| specs[i].name > dependency.name } - specs = specs[start_index ... end_index] if start_index && end_index + start_index = (0...specs.length).bsearch {|i| specs[i].name >= dependency.name } + end_index = (0...specs.length).bsearch {|i| specs[i].name > dependency.name } + specs = specs[start_index...end_index] if start_index && end_index end found[source] = specs.select do |tup| @@ -123,13 +124,13 @@ class Gem::SpecFetcher tuples = tuples.sort_by {|x| x[0].version } - return [tuples, errors] + [tuples, errors] end ## # Return all gem name tuples who's names match +obj+ - def detect(type=:complete) + def detect(type = :complete) tuples = [] list, _ = available_specs(type) @@ -149,21 +150,19 @@ class Gem::SpecFetcher # # If +matching_platform+ is false, gems for all platforms are returned. - def spec_for_dependency(dependency, matching_platform=true) + def spec_for_dependency(dependency, matching_platform = true) tuples, errors = search_for_dependency(dependency, matching_platform) specs = [] tuples.each do |tup, source| - begin - spec = source.fetch_spec(tup) - rescue Gem::RemoteFetcher::FetchError => e - errors << Gem::SourceFetchProblem.new(source, e) - else - specs << [spec, source] - end + spec = source.fetch_spec(tup) + rescue Gem::RemoteFetcher::FetchError => e + errors << Gem::SourceFetchProblem.new(source, e) + else + specs << [spec, source] end - return [specs, errors] + [specs, errors] end ## @@ -171,32 +170,64 @@ class Gem::SpecFetcher # alternative gem names. def suggest_gems_from_name(gem_name, type = :latest, num_results = 5) - gem_name = gem_name.downcase.tr("_-", "") - max = gem_name.size / 2 - names = available_specs(type).first.values.flatten(1) + gem_name = gem_name.downcase.tr("_-", "") + + # All results for 3-character-or-shorter (minus hyphens/underscores) gem + # names get rejected, so we just return an empty array immediately instead. + return [] if gem_name.length <= 3 - matches = names.map do |n| + max = gem_name.size / 2 + names = available_specs(type).first.values.flatten(1) + + min_length = gem_name.length - max + max_length = gem_name.length + max + + gem_name_with_postfix = "#{gem_name}ruby" + gem_name_with_prefix = "ruby#{gem_name}" + + matches = names.filter_map do |n| + len = n.name.length + # If the gem doesn't support the current platform, bail early. next unless n.match_platform? - [n.name, 0] if n.name.downcase.tr("_-", "").include?(gem_name) - end.compact - - if matches.length < num_results - matches += names.map do |n| - next unless n.match_platform? - distance = levenshtein_distance gem_name, n.name.downcase.tr("_-", "") - next if distance >= max - return [n.name] if distance == 0 - [n.name, distance] - end.compact + + # If the length is min_length or shorter, we've done `max` deletions. + # This would be rejected later, so we skip it for performance. + next if len <= min_length + + # The candidate name, normalized the same as gem_name. + normalized_name = n.name.downcase + normalized_name.tr!("_-", "") + + # If the gem is "{NAME}-ruby" and "ruby-{NAME}", we want to return it. + # But we already removed hyphens, so we check "{NAME}ruby" and "ruby{NAME}". + next [n.name, 0] if normalized_name == gem_name_with_postfix + next [n.name, 0] if normalized_name == gem_name_with_prefix + + # If the length is max_length or longer, we've done `max` insertions. + # This would be rejected later, so we skip it for performance. + next if len >= max_length + + # If we found an exact match (after stripping underscores and hyphens), + # that's our most likely candidate. + # Return it immediately, and skip the rest of the loop. + return [n.name] if normalized_name == gem_name + + distance = levenshtein_distance gem_name, normalized_name + + # Skip current candidate, if the edit distance is greater than allowed. + next if distance >= max + + # If all else fails, return the name and the calculated distance. + [n.name, distance] end matches = if matches.empty? && type != :prerelease suggest_gems_from_name gem_name, :prerelease else - matches.uniq.sort_by {|name, dist| dist } + matches.uniq.sort_by {|_name, dist| dist } end - matches.map {|name, dist| name }.uniq.first(num_results) + matches.map {|name, _dist| name }.uniq.first(num_results) end ## @@ -214,34 +245,32 @@ class Gem::SpecFetcher list = {} @sources.each_source do |source| - begin - names = case type - when :latest - tuples_for source, :latest - when :released - tuples_for source, :released - when :complete - names = - tuples_for(source, :prerelease, true) + - tuples_for(source, :released) - - names.sort - when :abs_latest - names = - tuples_for(source, :prerelease, true) + - tuples_for(source, :latest) - - names.sort - when :prerelease - tuples_for(source, :prerelease) - else - raise Gem::Exception, "Unknown type - :#{type}" - end - rescue Gem::RemoteFetcher::FetchError => e - errors << Gem::SourceFetchProblem.new(source, e) - else - list[source] = names + names = case type + when :latest + tuples_for source, :latest + when :released + tuples_for source, :released + when :complete + names = + tuples_for(source, :prerelease, true) + + tuples_for(source, :released) + + names.sort + when :abs_latest + names = + tuples_for(source, :prerelease, true) + + tuples_for(source, :latest) + + names.sort + when :prerelease + tuples_for(source, :prerelease) + else + raise Gem::Exception, "Unknown type - :#{type}" end + rescue Gem::RemoteFetcher::FetchError => e + errors << Gem::SourceFetchProblem.new(source, e) + else + list[source] = names end [list, errors] @@ -251,9 +280,9 @@ class Gem::SpecFetcher # Retrieves NameTuples from +source+ of the given +type+ (:prerelease, # etc.). If +gracefully_ignore+ is true, errors are ignored. - def tuples_for(source, type, gracefully_ignore=false) # :nodoc: + def tuples_for(source, type, gracefully_ignore = false) # :nodoc: @caches[type][source.uri] ||= - source.load_specs(type).sort_by {|tup| tup.name } + source.load_specs(type).sort_by(&:name) rescue Gem::RemoteFetcher::FetchError raise unless gracefully_ignore [] diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb index 31b8ef9546..51729d755b 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. @@ -6,11 +7,12 @@ # See LICENSE.txt for permissions. #++ -require_relative "deprecate" require_relative "basic_specification" require_relative "stub_specification" require_relative "platform" -require_relative "util/list" +require_relative "specification_record" + +require "rbconfig" ## # The Specification class contains the information for a gem. Typically @@ -32,10 +34,17 @@ require_relative "util/list" # Starting in RubyGems 2.0, a Specification can hold arbitrary # metadata. See #metadata for restrictions on the format and size of metadata # items you may add to a specification. +# +# Specifications must be deterministic, as in the example above. For instance, +# you cannot define attributes conditionally: +# +# # INVALID: do not do this. +# unless RUBY_ENGINE == "jruby" +# s.extensions << "ext/example/extconf.rb" +# end +# class Gem::Specification < Gem::BasicSpecification - extend Gem::Deprecate - # REFACTOR: Consider breaking out this version stuff into a separate # module. There's enough special stuff around it that it may justify # a separate class. @@ -105,7 +114,7 @@ class Gem::Specification < Gem::BasicSpecification @load_cache = {} # :nodoc: @load_cache_mutex = Thread::Mutex.new - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: # :startdoc: @@ -124,35 +133,35 @@ class Gem::Specification < Gem::BasicSpecification # Map of attribute names to default values. @@default_value = { - :authors => [], - :autorequire => nil, - :bindir => "bin", - :cert_chain => [], - :date => nil, - :dependencies => [], - :description => nil, - :email => nil, - :executables => [], - :extensions => [], - :extra_rdoc_files => [], - :files => [], - :homepage => nil, - :licenses => [], - :metadata => {}, - :name => nil, - :platform => Gem::Platform::RUBY, - :post_install_message => nil, - :rdoc_options => [], - :require_paths => ["lib"], - :required_ruby_version => Gem::Requirement.default, - :required_rubygems_version => Gem::Requirement.default, - :requirements => [], - :rubygems_version => Gem::VERSION, - :signing_key => nil, - :specification_version => CURRENT_SPECIFICATION_VERSION, - :summary => nil, - :test_files => [], - :version => nil, + authors: [], + autorequire: nil, + bindir: "bin", + cert_chain: [], + date: nil, + dependencies: [], + description: nil, + email: nil, + executables: [], + extensions: [], + extra_rdoc_files: [], + files: [], + homepage: nil, + licenses: [], + metadata: {}, + name: nil, + platform: Gem::Platform::RUBY, + post_install_message: nil, + rdoc_options: [], + require_paths: ["lib"], + required_ruby_version: Gem::Requirement.default, + required_rubygems_version: Gem::Requirement.default, + requirements: [], + rubygems_version: Gem::VERSION, + signing_key: nil, + specification_version: CURRENT_SPECIFICATION_VERSION, + summary: nil, + test_files: [], + version: nil, }.freeze # rubocop:disable Style/MutableConstant @@ -161,39 +170,27 @@ class Gem::Specification < Gem::BasicSpecification @@default_value.each do |k,v| INITIALIZE_CODE_FOR_DEFAULTS[k] = case v - when [], {}, true, false, nil, Numeric, Symbol - v.inspect - when String - v.dump - when Numeric - "default_value(:#{k})" - else - "default_value(:#{k}).dup" + when [], {}, true, false, nil, Numeric, Symbol + v.inspect + when String + v.dump + else + "default_value(:#{k}).dup" end end - @@attributes = @@default_value.keys.sort_by {|s| s.to_s } - @@array_attributes = @@default_value.reject {|k,v| v != [] }.keys + @@attributes = @@default_value.keys.sort_by(&:to_s) + @@array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k| @@default_value[k].nil? end - def self.clear_specs # :nodoc: - @@all = nil - @@stubs = nil - @@stubs_by_name = {} - @@spec_with_requirable_file = {} - @@active_stub_with_requirable_file = {} - end - private_class_method :clear_specs - - clear_specs - # Sentinel object to represent "not found" stubs NOT_FOUND = Struct.new(:to_spec, :this).new # :nodoc: + deprecate_constant :NOT_FOUND # Tracking removed method calls to warn users during build time. - REMOVED_METHODS = [:rubyforge_project=].freeze # :nodoc: + REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc: def removed_method_calls @removed_method_calls ||= [] end @@ -262,8 +259,7 @@ class Gem::Specification < Gem::BasicSpecification @test_files, add_bindir(@executables), @extra_rdoc_files, - @extensions, - ].flatten.compact.uniq.sort + @extensions].flatten.compact.uniq.sort end ## @@ -301,7 +297,7 @@ class Gem::Specification < Gem::BasicSpecification # # Usage: # - # spec.description = <<-EOF + # spec.description = <<~EOF # Rake is a Make-like program implemented in Ruby. Tasks and # dependencies are specified in standard Ruby syntax. # EOF @@ -338,10 +334,10 @@ class Gem::Specification < Gem::BasicSpecification # The simplest way is to specify the standard SPDX ID # https://spdx.org/licenses/ for the license. # Ideally, you should pick one that is OSI (Open Source Initiative) - # http://opensource.org/licenses/alphabetical approved. + # https://opensource.org/licenses/ approved. # # The most commonly used OSI-approved licenses are MIT and Apache-2.0. - # GitHub also provides a license picker at http://choosealicense.com/. + # GitHub also provides a license picker at https://choosealicense.com/. # # You can also use a custom license file along with your gemspec and specify # a LicenseRef-<idstring>, where idstring is the name of the file containing @@ -400,7 +396,7 @@ class Gem::Specification < Gem::BasicSpecification # "homepage_uri" => "https://bestgemever.example.io", # "mailing_list_uri" => "https://groups.example.com/bestgemever", # "source_code_uri" => "https://example.com/user/bestgemever", - # "wiki_uri" => "https://example.com/user/bestgemever/wiki" + # "wiki_uri" => "https://example.com/user/bestgemever/wiki", # "funding_uri" => "https://example.com/donate" # } # @@ -426,11 +422,11 @@ class Gem::Specification < Gem::BasicSpecification end ## - # The path in the gem for executable scripts. Usually 'bin' + # The path in the gem for executable scripts. Usually 'exe' # # Usage: # - # spec.bindir = 'bin' + # spec.bindir = 'exe' attr_accessor :bindir @@ -473,10 +469,7 @@ class Gem::Specification < Gem::BasicSpecification # spec.platform = Gem::Platform.local def platform=(platform) - if @original_platform.nil? || - @original_platform == Gem::Platform::RUBY - @original_platform = platform - end + @original_platform = platform case platform when Gem::Platform::CURRENT then @@ -500,10 +493,6 @@ class Gem::Specification < Gem::BasicSpecification end @platform = @new_platform.to_s - - invalidate_memoized_attributes - - @new_platform end ## @@ -533,13 +522,6 @@ class Gem::Specification < Gem::BasicSpecification attr_reader :required_rubygems_version ## - # The version of RubyGems used to create this gem. - # - # Do not set this, it is set automatically when the gem is packaged. - - attr_accessor :rubygems_version - - ## # The key used to sign this gem. See Gem::Security for details. attr_accessor :signing_key @@ -564,9 +546,9 @@ class Gem::Specification < Gem::BasicSpecification # # Usage: # - # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4' + # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4' - def add_runtime_dependency(gem, *requirements) + def add_dependency(gem, *requirements) if requirements.uniq.size != requirements.size warn "WARNING: duplicated #{gem} dependency #{requirements}" end @@ -577,7 +559,7 @@ class Gem::Specification < Gem::BasicSpecification ## # Executables included in the gem. # - # For example, the rake gem has rake as an executable. You don’t specify the + # For example, the rake gem has rake as an executable. You don't specify the # full path (as in bin/rake); all application-style files are expected to be # found in bindir. These files must be executable Ruby files. Files that # use bash or other interpreters will not work. @@ -598,7 +580,7 @@ class Gem::Specification < Gem::BasicSpecification # extconf.rb-style files used to compile extensions. # # These files will be run when the gem is installed, causing the C (or - # whatever) code to be compiled on the user’s machine. + # whatever) code to be compiled on the user's machine. # # Usage: # @@ -727,6 +709,21 @@ class Gem::Specification < Gem::BasicSpecification end ###################################################################### + # :section: Read-only attributes + + ## + # The version of RubyGems used to create this gem. + + attr_accessor :rubygems_version + + ## + # The path where this gem installs its extensions. + + def extensions_dir + @extensions_dir ||= super + end + + ###################################################################### # :section: Specification internals ## @@ -734,7 +731,7 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :activated - alias :activated? :activated + alias_method :activated?, :activated ## # Autorequire was used by old RubyGems to automatically require a file. @@ -744,14 +741,6 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :autorequire # :nodoc: ## - # Sets the default executable for this gem. - # - # Deprecated: You must now specify the executable name to Gem.bin_path. - - attr_writer :default_executable - rubygems_deprecate :default_executable= - - ## # Allows deinstallation of gems with legacy platforms. attr_writer :original_platform # :nodoc: @@ -764,7 +753,7 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :specification_version def self._all # :nodoc: - @@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec) + specification_record.all end def self.clear_load_cache # :nodoc: @@ -774,34 +763,22 @@ class Gem::Specification < Gem::BasicSpecification end private_class_method :clear_load_cache + def self.gem_path # :nodoc: + Gem.path + end + private_class_method :gem_path + def self.each_gemspec(dirs) # :nodoc: dirs.each do |dir| Gem::Util.glob_files_in_dir("*.gemspec", dir).each do |path| - yield path.tap(&Gem::UNTAINT) + yield path end end end - def self.gemspec_stubs_in(dir, pattern) + def self.gemspec_stubs_in(dir, pattern) # :nodoc: Gem::Util.glob_files_in_dir(pattern, dir).map {|path| yield path }.select(&:valid?) end - private_class_method :gemspec_stubs_in - - def self.installed_stubs(dirs, pattern) - map_stubs(dirs, pattern) do |path, base_dir, gems_dir| - Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) - end - end - private_class_method :installed_stubs - - def self.map_stubs(dirs, pattern) # :nodoc: - dirs.flat_map do |dir| - base_dir = File.dirname dir - gems_dir = File.join base_dir, "gems" - gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir } - end - end - private_class_method :map_stubs def self.each_spec(dirs) # :nodoc: each_gemspec(dirs) do |path| @@ -814,13 +791,7 @@ class Gem::Specification < Gem::BasicSpecification # Returns a Gem::StubSpecification for every installed gem def self.stubs - @@stubs ||= begin - pattern = "*.gemspec" - stubs = stubs_for_pattern(pattern, false) - - @@stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name) - stubs - end + specification_record.stubs end ## @@ -839,13 +810,7 @@ class Gem::Specification < Gem::BasicSpecification # only returns stubs that match Gem.platforms def self.stubs_for(name) - if @@stubs - @@stubs_by_name[name] || [] - else - @@stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s| - s.name == name - end - end + specification_record.stubs_for(name) end ## @@ -853,12 +818,7 @@ class Gem::Specification < Gem::BasicSpecification # optionally filtering out specs not matching the current platform # def self.stubs_for_pattern(pattern, match_platform = true) # :nodoc: - installed_stubs = installed_stubs(Gem::Specification.dirs, pattern) - installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform - stubs = installed_stubs + default_stubs(pattern) - stubs = stubs.uniq {|stub| stub.full_name } - _resort!(stubs) - stubs + specification_record.stubs_for_pattern(pattern, match_platform) end def self._resort!(specs) # :nodoc: @@ -867,7 +827,11 @@ class Gem::Specification < Gem::BasicSpecification next names if names.nonzero? versions = b.version <=> a.version next versions if versions.nonzero? - Gem::Platform.sort_priority(b.platform) + platforms = Gem::Platform.sort_priority(b.platform) <=> Gem::Platform.sort_priority(a.platform) + next platforms if platforms.nonzero? + default_gem = a.default_gem_priority <=> b.default_gem_priority + next default_gem if default_gem.nonzero? + a.base_dir_priority(gem_path) <=> b.base_dir_priority(gem_path) end end @@ -887,23 +851,14 @@ class Gem::Specification < Gem::BasicSpecification # properly sorted. def self.add_spec(spec) - return if _all.include? spec - - _all << spec - stubs << spec - (@@stubs_by_name[spec.name] ||= []) << spec - - _resort!(@@stubs_by_name[spec.name]) - _resort!(stubs) + specification_record.add_spec(spec) end ## # Removes +spec+ from the known specs. def self.remove_spec(spec) - _all.delete spec.to_spec - stubs.delete spec - (@@stubs_by_name[spec.name] || []).delete spec + specification_record.remove_spec(spec) end ## @@ -911,33 +866,23 @@ class Gem::Specification < Gem::BasicSpecification # You probably want to use one of the Enumerable methods instead. def self.all - warn "NOTE: Specification.all called from #{caller.first}" unless + warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless Gem::Deprecate.skip _all end ## - # Sets the known specs to +specs+. Not guaranteed to work for you in - # the future. Use at your own risk. Caveat emptor. Doomy doom doom. - # Etc etc. - # - #-- - # Makes +specs+ the known specs - # Listen, time is a river - # Winter comes, code breaks - # - # -- wilsonb + # Sets the known specs to +specs+. def self.all=(specs) - @@stubs_by_name = specs.group_by(&:name) - @@all = @@stubs = specs + specification_record.all = specs end ## # Return full names of all specs in sorted order. def self.all_names - self._all.map(&:full_name) + specification_record.all_names end ## @@ -962,9 +907,7 @@ class Gem::Specification < Gem::BasicSpecification # Return the directories that Specification uses to find specs. def self.dirs - @@dirs ||= Gem.path.collect do |dir| - File.join dir.dup.tap(&Gem::UNTAINT), "specifications" - end + @@dirs ||= Gem::SpecificationRecord.dirs_from(gem_path) end ## @@ -972,9 +915,9 @@ class Gem::Specification < Gem::BasicSpecification # this resets the list of known specs. def self.dirs=(dirs) - self.reset + reset - @@dirs = Array(dirs).map {|dir| File.join dir, "specifications" } + @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs)) end extend Enumerable @@ -983,23 +926,15 @@ class Gem::Specification < Gem::BasicSpecification # Enumerate every known spec. See ::dirs= and ::add_spec to set the list of # specs. - def self.each - return enum_for(:each) unless block_given? - - self._all.each do |x| - yield x - end + def self.each(&block) + specification_record.each(&block) end ## # Returns every spec that matches +name+ and optional +requirements+. def self.find_all_by_name(name, *requirements) - requirements = Gem::Requirement.default if requirements.empty? - - # TODO: maybe try: find_all { |s| spec === dep } - - Gem::Dependency.new(name, *requirements).matching_specs + specification_record.find_all_by_name(name, *requirements) end ## @@ -1016,20 +951,29 @@ class Gem::Specification < Gem::BasicSpecification def self.find_by_name(name, *requirements) requirements = Gem::Requirement.default if requirements.empty? - # TODO: maybe try: find { |s| spec === dep } - Gem::Dependency.new(name, *requirements).to_spec end ## + # Find the best specification matching a +full_name+. + def self.find_by_full_name(full_name) + stubs.find {|s| s.full_name == full_name }&.to_spec + end + + ## # Return the best specification that contains the file matching +path+. def self.find_by_path(path) - path = path.dup.freeze - spec = @@spec_with_requirable_file[path] ||= (stubs.find do |s| - s.contains_requirable_file? path - end || NOT_FOUND) - spec.to_spec + specification_record.find_by_path(path) + end + + ## + # Return the best specification that contains the file matching +path+ + # amongst the specs that are not loaded. This method is different than + # +find_inactive_by_path+ as it will filter out loaded specs by their name. + + def self.find_unloaded_by_path(path) + specification_record.find_unloaded_by_path(path) end ## @@ -1037,18 +981,15 @@ class Gem::Specification < Gem::BasicSpecification # amongst the specs that are not activated. def self.find_inactive_by_path(path) - stub = stubs.find do |s| - next if s.activated? - s.contains_requirable_file? path - end - stub&.to_spec + specification_record.find_inactive_by_path(path) end + ## + # Return the best specification that contains the file matching +path+, among + # those already activated. + def self.find_active_stub_by_path(path) - stub = @@active_stub_with_requirable_file[path] ||= (stubs.find do |s| - s.activated? && s.contains_requirable_file?(path) - end || NOT_FOUND) - stub.this + specification_record.find_active_stub_by_path(path) end ## @@ -1064,8 +1005,8 @@ class Gem::Specification < Gem::BasicSpecification def self.find_in_unresolved_tree(path) unresolved_specs.each do |spec| - spec.traverse do |from_spec, dep, to_spec, trail| - if to_spec.has_conflicts? || to_spec.conficts_when_loaded_with?(trail) + spec.traverse do |_from_spec, _dep, to_spec, trail| + if to_spec.has_conflicts? || to_spec.conflicts_when_loaded_with?(trail) :next else return trail.reverse if to_spec.contains_requirable_file? path @@ -1077,7 +1018,7 @@ class Gem::Specification < Gem::BasicSpecification end def self.unresolved_specs - unresolved_deps.values.map {|dep| dep.to_specs }.flatten + unresolved_deps.values.flat_map(&:to_specs) end private_class_method :unresolved_specs @@ -1115,26 +1056,28 @@ class Gem::Specification < Gem::BasicSpecification # +prerelease+ is true. def self.latest_specs(prerelease = false) - _latest_specs Gem::Specification.stubs, prerelease + specification_record.latest_specs(prerelease) end ## # Return the latest installed spec for gem +name+. def self.latest_spec_for(name) - latest_specs(true).find {|installed_spec| installed_spec.name == name } + specification_record.latest_spec_for(name) end def self._latest_specs(specs, prerelease = false) # :nodoc: result = {} specs.reverse_each do |spec| - next if spec.version.prerelease? unless prerelease + unless prerelease + next if spec.version.prerelease? + end result[spec.name] = spec end - result.map(&:last).flatten.sort_by {|tup| tup.name } + result.flat_map(&:last).sort_by(&:name) end ## @@ -1143,36 +1086,33 @@ class Gem::Specification < Gem::BasicSpecification def self.load(file) return unless file - _spec = @load_cache_mutex.synchronize { @load_cache[file] } - return _spec if _spec + spec = @load_cache_mutex.synchronize { @load_cache[file] } + return spec if spec - file = file.dup.tap(&Gem::UNTAINT) return unless File.file?(file) code = Gem.open_file(file, "r:UTF-8:-", &:read) - code.tap(&Gem::UNTAINT) - begin - _spec = eval code, binding, file + spec = eval code, binding, file - if Gem::Specification === _spec - _spec.loaded_from = File.expand_path file.to_s + if Gem::Specification === spec + spec.loaded_from = File.expand_path file.to_s @load_cache_mutex.synchronize do prev = @load_cache[file] if prev - _spec = prev + spec = prev else - @load_cache[file] = _spec + @load_cache[file] = spec end end - return _spec + return spec end - warn "[#{file}] isn't a Gem::Specification (#{_spec.class} instead)." + warn "[#{file}] isn't a Gem::Specification (#{spec.class} instead)." rescue SignalException, SystemExit raise - rescue SyntaxError, Exception => e + rescue SyntaxError, StandardError => e warn "Invalid gemspec in [#{file}]: #{e}" end @@ -1260,26 +1200,42 @@ class Gem::Specification < Gem::BasicSpecification def self.reset @@dirs = nil - Gem.pre_reset_hooks.each {|hook| hook.call } - clear_specs + Gem.pre_reset_hooks.each(&:call) + @specification_record = nil clear_load_cache - unresolved = unresolved_deps - unless unresolved.empty? - warn "WARN: Unresolved or ambiguous specs during Gem::Specification.reset:" - unresolved.values.each do |dep| - warn " #{dep}" - - versions = find_all_by_name(dep.name) - unless versions.empty? - warn " Available/installed versions of this gem:" - versions.each {|s| warn " - #{s.version}" } + + unless unresolved_deps.empty? + unresolved = unresolved_deps.filter_map do |name, dep| + matching_versions = find_all_by_name(name) + next if dep.latest_version? && matching_versions.any?(&:default_gem?) + + [dep, matching_versions.uniq(&:full_name)] + end.to_h + + unless unresolved.empty? + warn "WARN: Unresolved or ambiguous specs during Gem::Specification.reset:" + unresolved.each do |dep, versions| + warn " #{dep}" + + unless versions.empty? + warn " Available/installed versions of this gem:" + versions.each {|s| warn " - #{s.version}" } + end end + warn "WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'" + warn "Please report a bug if this causes problems." end - warn "WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'" - warn "Please report a bug if this causes problems." - unresolved.clear + + unresolved_deps.clear end - Gem.post_reset_hooks.each {|hook| hook.call } + Gem.post_reset_hooks.each(&:call) + end + + ## + # Keeps track of all currently known specifications + + def self.specification_record + @specification_record ||= Gem::SpecificationRecord.new(dirs) end # DOC: This method needs documented or nodoc'd @@ -1292,10 +1248,23 @@ class Gem::Specification < Gem::BasicSpecification def self._load(str) Gem.load_yaml + Gem.load_safe_marshal + + yaml_set = false + retry_count = 0 array = begin - Marshal.load str + Gem::SafeMarshal.safe_load str rescue ArgumentError => e + # Avoid an infinite retry loop when the argument error has nothing to do + # with the classes not being defined. + # 1 retry each allowed in case all 3 of + # - YAML + # - YAML::Syck::DefaultKey + # - YAML::PrivateType + # need to be defined + raise if retry_count >= 3 + # # Some very old marshaled specs included references to `YAML::PrivateType` # and `YAML::Syck::DefaultKey` constants due to bugs in the old emitter @@ -1305,17 +1274,23 @@ class Gem::Specification < Gem::BasicSpecification message = e.message raise unless message.include?("YAML::") - Object.const_set "YAML", Psych unless Object.const_defined?(:YAML) + unless Object.const_defined?(:YAML) + Object.const_set "YAML", Module.new + yaml_set = true + end if message.include?("YAML::Syck::") YAML.const_set "Syck", YAML unless YAML.const_defined?(:Syck) - YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") - elsif message.include?("YAML::PrivateType") - YAML.const_set "PrivateType", Class.new + YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") && !YAML::Syck.const_defined?(:DefaultKey) + elsif message.include?("YAML::PrivateType") && !YAML.const_defined?(:PrivateType) + YAML.const_set "PrivateType", Class.new { attr_accessor :type_id, :value } end + retry_count += 1 retry + ensure + Object.__send__(:remove_const, "YAML") if yaml_set end spec = Gem::Specification.new @@ -1343,17 +1318,15 @@ class Gem::Specification < Gem::BasicSpecification spec.instance_variable_set :@summary, array[5] spec.instance_variable_set :@required_ruby_version, array[6] spec.instance_variable_set :@required_rubygems_version, array[7] - spec.instance_variable_set :@original_platform, array[8] + spec.platform = array[8] spec.instance_variable_set :@dependencies, array[9] # offset due to rubyforge_project removal spec.instance_variable_set :@email, array[11] spec.instance_variable_set :@authors, array[12] spec.instance_variable_set :@description, array[13] spec.instance_variable_set :@homepage, array[14] - spec.instance_variable_set :@has_rdoc, array[15] - spec.instance_variable_set :@new_platform, array[16] - spec.instance_variable_set :@platform, array[16].to_s - spec.instance_variable_set :@license, array[17] + # offset due to has_rdoc removal + spec.instance_variable_set :@licenses, array[17] spec.instance_variable_set :@metadata, array[18] spec.instance_variable_set :@loaded, false spec.instance_variable_set :@activated, false @@ -1409,7 +1382,7 @@ class Gem::Specification < Gem::BasicSpecification # there are conflicts upon activation. def activate - other = Gem.loaded_specs[self.name] + other = Gem.loaded_specs[name] if other check_version_conflict other return false @@ -1420,11 +1393,11 @@ class Gem::Specification < Gem::BasicSpecification activate_dependencies add_self_to_load_path - Gem.loaded_specs[self.name] = self + Gem.loaded_specs[name] = self @activated = true @loaded = true - return true + true end ## @@ -1435,7 +1408,7 @@ class Gem::Specification < Gem::BasicSpecification def activate_dependencies unresolved = Gem::Specification.unresolved_deps - self.runtime_dependencies.each do |spec_dep| + runtime_dependencies.each do |spec_dep| if loaded = Gem.loaded_specs[spec_dep.name] next if spec_dep.matches_spec? loaded @@ -1446,13 +1419,11 @@ class Gem::Specification < Gem::BasicSpecification raise e end - begin - specs = spec_dep.to_specs - rescue Gem::MissingSpecError => e - raise Gem::MissingSpecError.new(e.name, e.requirement, "at: #{self.spec_file}") - end + specs = spec_dep.matching_specs(true).uniq(&:full_name) - if specs.size == 1 + if specs.size == 0 + raise Gem::MissingSpecError.new(spec_dep.name, spec_dep.requirement, "at: #{spec_file}") + elsif specs.size == 1 specs.first.activate else name = spec_dep.name @@ -1495,7 +1466,7 @@ class Gem::Specification < Gem::BasicSpecification def sanitize_string(string) return string unless string - # HACK the #to_s is in here because RSpec has an Array of Arrays of + # HACK: the #to_s is in here because RSpec has an Array of Arrays of # Strings for authors. Need a way to disallow bad values on gemspec # generation. (Probably won't happen.) string.to_s @@ -1513,8 +1484,8 @@ class Gem::Specification < Gem::BasicSpecification else executables end - rescue - return nil + rescue StandardError + nil end ## @@ -1539,7 +1510,7 @@ class Gem::Specification < Gem::BasicSpecification private :add_dependency_with_type - alias add_dependency add_runtime_dependency + alias_method :add_runtime_dependency, :add_dependency ## # Adds this spec's require paths to LOAD_PATH, in the proper location. @@ -1591,7 +1562,7 @@ class Gem::Specification < Gem::BasicSpecification def build_args if File.exist? build_info_file build_info = File.readlines build_info_file - build_info = build_info.map {|x| x.strip } + build_info = build_info.map(&:strip) build_info.delete "" build_info else @@ -1606,9 +1577,11 @@ class Gem::Specification < Gem::BasicSpecification def build_extensions # :nodoc: return if extensions.empty? return if default_gem? + # we need to fresh build when same name and version of default gems + return if self.class.find_by_full_name(full_name)&.default_gem? return if File.exist? gem_build_complete_path - return if !File.writable?(base_dir) - return if !File.exist?(File.join(base_dir, "extensions")) + return unless File.writable?(base_dir) + return unless File.exist?(File.join(base_dir, "extensions")) begin # We need to require things in $LOAD_PATH without looking for the @@ -1651,14 +1624,14 @@ class Gem::Specification < Gem::BasicSpecification # spec's cached gem. def cache_dir - @cache_dir ||= File.join base_dir, "cache" + File.join base_dir, "cache" end ## # Returns the full path to the cached gem for this spec. def cache_file - @cache_file ||= File.join cache_dir, "#{full_name}.gem" + File.join cache_dir, "#{full_name}.gem" end ## @@ -1666,7 +1639,7 @@ class Gem::Specification < Gem::BasicSpecification def conflicts conflicts = {} - self.runtime_dependencies.each do |dep| + runtime_dependencies.each do |dep| spec = Gem.loaded_specs[dep.name] if spec && !spec.satisfies_requirement?(dep) (conflicts[spec] ||= []) << dep @@ -1680,9 +1653,9 @@ class Gem::Specification < Gem::BasicSpecification ## # return true if there will be conflict when spec if loaded together with the list of specs. - def conficts_when_loaded_with?(list_of_specs) # :nodoc: + def conflicts_when_loaded_with?(list_of_specs) # :nodoc: result = list_of_specs.any? do |spec| - spec.dependencies.any? {|dep| dep.runtime? && (dep.name == name) && !satisfies_requirement?(dep) } + spec.runtime_dependencies.any? {|dep| (dep.name == name) && !satisfies_requirement?(dep) } end result end @@ -1692,14 +1665,12 @@ class Gem::Specification < Gem::BasicSpecification def has_conflicts? return true unless Gem.env_requirement(name).satisfied_by?(version) - self.dependencies.any? do |dep| - if dep.runtime? - spec = Gem.loaded_specs[dep.name] - spec && !spec.satisfies_requirement?(dep) - else - false - end + runtime_dependencies.any? do |dep| + spec = Gem.loaded_specs[dep.name] + spec && !spec.satisfies_requirement?(dep) end + rescue ArgumentError => e + raise e, "#{name} #{version}: #{e.message}" end # The date this gem was created. @@ -1723,7 +1694,7 @@ class Gem::Specification < Gem::BasicSpecification /\A (\d{4})-(\d{2})-(\d{2}) (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )? - \Z/x.freeze + \Z/x ## # The date this gem was created @@ -1735,39 +1706,21 @@ class Gem::Specification < Gem::BasicSpecification # This is the cleanest, most-readable, faster-than-using-Date # way to do it. @date = case date - when String then - if DateTimeFormat =~ date - Time.utc($1.to_i, $2.to_i, $3.to_i) - else - raise(Gem::InvalidSpecificationException, - "invalid date format in specification: #{date.inspect}") - end - when Time, DateLike then - Time.utc(date.year, date.month, date.day) - else - TODAY + when String then + if DateTimeFormat =~ date + Time.utc($1.to_i, $2.to_i, $3.to_i) + else + raise(Gem::InvalidSpecificationException, + "invalid date format in specification: #{date.inspect}") + end + when Time, DateLike then + Time.utc(date.year, date.month, date.day) + else + TODAY end end ## - # The default executable for this gem. - # - # Deprecated: The name of the gem is assumed to be the name of the - # executable now. See Gem.bin_path. - - def default_executable # :nodoc: - if defined?(@default_executable) && @default_executable - result = @default_executable - elsif @executables && @executables.size == 1 - result = Array(@executables).first - else - result = nil - end - result - end - rubygems_deprecate :default_executable - - ## # The default value for specification attribute +name+ def default_value(name) @@ -1790,18 +1743,17 @@ class Gem::Specification < Gem::BasicSpecification # # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]] - def dependent_gems(check_dev=true) + def dependent_gems(check_dev = true) out = [] Gem::Specification.each do |spec| deps = check_dev ? spec.dependencies : spec.runtime_dependencies deps.each do |dep| - if self.satisfies_requirement?(dep) - sats = [] - find_all_satisfiers(dep) do |sat| - sats << sat - end - out << [spec, dep, sats] + next unless satisfies_requirement?(dep) + sats = [] + find_all_satisfiers(dep) do |sat| + sats << sat end + out << [spec, dep, sats] end end out @@ -1811,7 +1763,7 @@ class Gem::Specification < Gem::BasicSpecification # Returns all specs that matches this spec's runtime dependencies. def dependent_specs - runtime_dependencies.map {|dep| dep.to_specs }.flatten + runtime_dependencies.flat_map(&:to_specs) end ## @@ -1847,23 +1799,15 @@ class Gem::Specification < Gem::BasicSpecification end def encode_with(coder) # :nodoc: - mark_version - coder.add "name", @name coder.add "version", @version - platform = case @original_platform - when nil, "" then - "ruby" - when String then - @original_platform - else - @original_platform.to_s - end - coder.add "platform", platform + coder.add "platform", platform.to_s + coder.add "original_platform", original_platform.to_s if platform.to_s != original_platform.to_s attributes = @@attributes.map(&:to_s) - %w[name version platform] attributes.each do |name| - coder.add name, instance_variable_get("@#{name}") + value = instance_variable_get("@#{name}") + coder.add name, value unless value.nil? end end @@ -1945,12 +1889,9 @@ class Gem::Specification < Gem::BasicSpecification spec end - def full_name - @full_name ||= super - end - ## - # Work around bundler removing my methods + # Work around old bundler versions removing my methods + # Can be removed once RubyGems can no longer install Bundler 2.5 def gem_dir # :nodoc: super @@ -1961,29 +1902,6 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Deprecated and ignored, defaults to true. - # - # Formerly used to indicate this gem was RDoc-capable. - - def has_rdoc # :nodoc: - true - end - rubygems_deprecate :has_rdoc - - ## - # Deprecated and ignored. - # - # Formerly used to indicate this gem was RDoc-capable. - - def has_rdoc=(ignored) # :nodoc: - @has_rdoc = true - end - rubygems_deprecate :has_rdoc= - - alias :has_rdoc? :has_rdoc # :nodoc: - rubygems_deprecate :has_rdoc? - - ## # True if this gem has files in test_files def has_unit_tests? # :nodoc: @@ -1991,7 +1909,7 @@ class Gem::Specification < Gem::BasicSpecification end # :stopdoc: - alias has_test_suite? has_unit_tests? + alias_method :has_test_suite?, :has_unit_tests? # :startdoc: def hash # :nodoc: @@ -2048,7 +1966,8 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Duplicates array_attributes from +other_spec+ so state isn't shared. + # Duplicates Array and Gem::Requirement attributes from +other_spec+ so state isn't shared. + # def initialize_copy(other_spec) self.class.array_attributes.each do |name| @@ -2070,6 +1989,9 @@ class Gem::Specification < Gem::BasicSpecification raise e end end + + @required_ruby_version = other_spec.required_ruby_version.dup + @required_rubygems_version = other_spec.required_rubygems_version.dup end def base_dir @@ -2081,17 +2003,6 @@ class Gem::Specification < Gem::BasicSpecification end end - ## - # Expire memoized instance variables that can incorrectly generate, replace - # or miss files due changes in certain attributes used to compute them. - - def invalidate_memoized_attributes - @full_name = nil - @cache_file = nil - end - - private :invalidate_memoized_attributes - def inspect # :nodoc: if $DEBUG super @@ -2130,8 +2041,6 @@ class Gem::Specification < Gem::BasicSpecification def internal_init # :nodoc: super @bin_dir = nil - @cache_dir = nil - @cache_file = nil @doc_dir = nil @ri_dir = nil @spec_dir = nil @@ -2139,13 +2048,6 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Sets the rubygems_version to the current RubyGems version. - - def mark_version - @rubygems_version = Gem::VERSION - end - - ## # Track removed method calls to warn about during build time. # Warn about unknown attributes while loading a spec. @@ -2168,6 +2070,7 @@ class Gem::Specification < Gem::BasicSpecification # probably want to build_extensions def missing_extensions? + return false if RUBY_ENGINE == "jruby" return false if extensions.empty? return false if default_gem? return false if File.exist? gem_build_complete_path @@ -2188,11 +2091,11 @@ class Gem::Specification < Gem::BasicSpecification @files.concat(@extra_rdoc_files) end - @files = @files.uniq if @files - @extensions = @extensions.uniq if @extensions - @test_files = @test_files.uniq if @test_files - @executables = @executables.uniq if @executables - @extra_rdoc_files = @extra_rdoc_files.uniq if @extra_rdoc_files + @files = @files.uniq.sort if @files + @extensions = @extensions.uniq.sort if @extensions + @test_files = @test_files.uniq.sort if @test_files + @executables = @executables.uniq.sort if @executables + @extra_rdoc_files = @extra_rdoc_files.uniq.sort if @extra_rdoc_files end ## @@ -2225,7 +2128,7 @@ class Gem::Specification < Gem::BasicSpecification # The platform this gem runs on. See Gem::Platform for details. def platform - @new_platform ||= Gem::Platform::RUBY + @new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName end def pretty_print(q) # :nodoc: @@ -2238,23 +2141,22 @@ class Gem::Specification < Gem::BasicSpecification attributes.unshift :name attributes.each do |attr_name| - current_value = self.send attr_name - current_value = current_value.sort if %i[files test_files].include? attr_name - if current_value != default_value(attr_name) || - self.class.required_attribute?(attr_name) - - q.text "s.#{attr_name} = " + current_value = send attr_name + current_value = current_value.sort if [:files, :test_files].include? attr_name + next unless current_value != default_value(attr_name) || + self.class.required_attribute?(attr_name) - if attr_name == :date - current_value = current_value.utc + q.text "s.#{attr_name} = " - q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})" - else - q.pp current_value - end + if attr_name == :date + current_value = current_value.utc - q.breakable + q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})" + else + q.pp current_value end + + q.breakable end end end @@ -2264,7 +2166,7 @@ class Gem::Specification < Gem::BasicSpecification # that is already loaded (+other+) def check_version_conflict(other) # :nodoc: - return if self.version == other.version + return if version == other.version # This gem is already loaded. If the currently loaded gem is not in the # list of candidate gems, then we have a version conflict. @@ -2272,7 +2174,7 @@ class Gem::Specification < Gem::BasicSpecification msg = "can't activate #{full_name}, already activated #{other.full_name}" e = Gem::LoadError.new msg - e.name = self.name + e.name = name raise e end @@ -2289,10 +2191,13 @@ class Gem::Specification < Gem::BasicSpecification end ## - # Sets rdoc_options to +value+, ensuring it is an array. + # Sets rdoc_options to +value+, ensuring it is a flat array of strings. + # Handles malformed gemspecs where rdoc_options may be a Hash or contain Hashes. def rdoc_options=(options) - @rdoc_options = Array options + @rdoc_options = Array(options).flat_map do |opt| + opt.is_a?(Hash) ? opt.to_a.flatten.map(&:to_s) : opt + end end ## @@ -2337,13 +2242,13 @@ class Gem::Specification < Gem::BasicSpecification when Array then "[" + obj.map {|x| ruby_code x }.join(", ") + "]" when Hash then seg = obj.keys.sort.map {|k| "#{k.to_s.dump} => #{obj[k].to_s.dump}" } - "{ #{seg.join(', ')} }" - when Gem::Version then obj.to_s.dump + "{ #{seg.join(", ")} }" + when Gem::Version then ruby_code(obj.to_s) when DateLike then obj.strftime("%Y-%m-%d").dump when Time then obj.strftime("%Y-%m-%d").dump when Numeric then obj.inspect when true, false, nil then obj.inspect - when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})" + when Gem::Platform then "Gem::Platform.new(#{ruby_code obj.to_a})" when Gem::Requirement then list = obj.as_list "Gem::Requirement.new(#{ruby_code(list.size == 1 ? obj.to_s : list)})" @@ -2364,7 +2269,7 @@ class Gem::Specification < Gem::BasicSpecification # True if this gem has the same attributes as +other+. def same_attributes?(spec) - @@attributes.all? {|name, default| self.send(name) == spec.send(name) } + @@attributes.all? {|name, _default| send(name) == spec.send(name) } end private :same_attributes? @@ -2373,8 +2278,8 @@ class Gem::Specification < Gem::BasicSpecification # Checks if this specification meets the requirement of +dependency+. def satisfies_requirement?(dependency) - return @name == dependency.name && - dependency.requirement.satisfied_by?(@version) + @name == dependency.name && + dependency.requirement.satisfied_by?(@version) end ## @@ -2463,7 +2368,6 @@ class Gem::Specification < Gem::BasicSpecification # still have their default values are omitted. def to_ruby - mark_version result = [] result << "# -*- encoding: utf-8 -*-" result << "#{Gem::StubSpecification::PREFIX}#{name} #{version} #{platform} #{raw_require_paths.join("\0")}" @@ -2493,27 +2397,25 @@ class Gem::Specification < Gem::BasicSpecification :required_rubygems_version, :specification_version, :version, - :has_rdoc, - :default_executable, :metadata, :signing_key, ] @@attributes.each do |attr_name| next if handled.include? attr_name - current_value = self.send(attr_name) + current_value = send(attr_name) if current_value != default_value(attr_name) || self.class.required_attribute?(attr_name) result << " s.#{attr_name} = #{ruby_code current_value}" end end if String === signing_key - result << " s.signing_key = #{signing_key.dump}.freeze" + result << " s.signing_key = #{ruby_code signing_key}" end if @installed_by_version result << nil - result << " s.installed_by_version = \"#{Gem::VERSION}\" if s.respond_to? :installed_by_version" + result << " s.installed_by_version = #{ruby_code Gem::VERSION}" end unless dependencies.empty? @@ -2522,9 +2424,8 @@ class Gem::Specification < Gem::BasicSpecification result << nil dependencies.each do |dep| - req = dep.requirements_list.inspect dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK - result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{req})" + result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})" end end @@ -2558,24 +2459,28 @@ class Gem::Specification < Gem::BasicSpecification def to_yaml(opts = {}) # :nodoc: Gem.load_yaml - # Because the user can switch the YAML engine behind our - # back, we have to check again here to make sure that our - # psych code was properly loaded, and load it if not. - unless Gem.const_defined?(:NoAliasYAMLTree) - require_relative "psych_tree" - end + if Gem.use_psych? + # Because the user can switch the YAML engine behind our + # back, we have to check again here to make sure that our + # psych code was properly loaded, and load it if not. + unless Gem.const_defined?(:NoAliasYAMLTree) + require_relative "psych_tree" + end - builder = Gem::NoAliasYAMLTree.create - builder << self - ast = builder.tree + builder = Gem::NoAliasYAMLTree.create + builder << self + ast = builder.tree - require "stringio" - io = StringIO.new - io.set_encoding Encoding::UTF_8 + require "stringio" + io = StringIO.new + io.set_encoding Encoding::UTF_8 - Psych::Visitors::Emitter.new(io).accept(ast) + Psych::Visitors::Emitter.new(io).accept(ast) - io.string.gsub(/ !!null \n/, " \n") + io.string.gsub(/ !!null \n/, " \n") + else + Gem::YAMLSerializer.dump(self) + end end ## @@ -2585,10 +2490,9 @@ class Gem::Specification < Gem::BasicSpecification def traverse(trail = [], visited = {}, &block) trail.push(self) begin - dependencies.each do |dep| - next unless dep.runtime? + runtime_dependencies.each do |dep| dep.matching_specs(true).each do |dep_spec| - next if visited.has_key?(dep_spec) + next if visited.key?(dep_spec) visited[dep_spec] = true trail.push(dep_spec) begin @@ -2596,11 +2500,10 @@ class Gem::Specification < Gem::BasicSpecification ensure trail.pop end - unless result == :next - spec_name = dep_spec.name - dep_spec.traverse(trail, visited, &block) unless - trail.any? {|s| s.name == spec_name } - end + next if result == :next + spec_name = dep_spec.name + dep_spec.traverse(trail, visited, &block) unless + trail.any? {|s| s.name == spec_name } end end ensure @@ -2631,38 +2534,15 @@ class Gem::Specification < Gem::BasicSpecification @test_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) } end - def validate_metadata - Gem::SpecificationPolicy.new(self).validate_metadata + def validate_for_resolution + Gem::SpecificationPolicy.new(self).validate_for_resolution end - rubygems_deprecate :validate_metadata - - def validate_dependencies - Gem::SpecificationPolicy.new(self).validate_dependencies - end - rubygems_deprecate :validate_dependencies - - def validate_permissions - Gem::SpecificationPolicy.new(self).validate_permissions - end - rubygems_deprecate :validate_permissions ## - # Set the version to +version+, potentially also setting - # required_rubygems_version if +version+ indicates it is a - # prerelease. + # Set the version to +version+. def version=(version) - @version = Gem::Version.create(version) - return if @version.nil? - - # skip to set required_ruby_version when pre-released rubygems. - # It caused to raise CircularDependencyError - if @version.prerelease? && (@name.nil? || @name.strip != "rubygems") - self.required_rubygems_version = "> 1.3.1" - end - invalidate_memoized_attributes - - return @version + @version = version.nil? ? version : Gem::Version.create(version) end def stubbed? @@ -2674,14 +2554,17 @@ class Gem::Specification < Gem::BasicSpecification case ivar when "date" # Force Date to go through the extra coerce logic in date= - self.date = val.tap(&Gem::UNTAINT) + self.date = val + when "platform" + self.platform = val + when "rdoc_options" + self.rdoc_options = val + when "requirements" + self.requirements = val else - instance_variable_set "@#{ivar}", val.tap(&Gem::UNTAINT) + instance_variable_set "@#{ivar}", val end end - - @original_platform = @platform # for backwards compatibility - self.platform = Gem::Platform.new @platform end ## @@ -2693,17 +2576,19 @@ class Gem::Specification < Gem::BasicSpecification end nil_attributes.each do |attribute| - default = self.default_value attribute + default = default_value attribute value = case default - when Time, Numeric, Symbol, true, false, nil then default - else default.dup + when Time, Numeric, Symbol, true, false, nil then default + else default.dup end instance_variable_set "@#{attribute}", value end @installed_by_version ||= nil + + nil end def flatten_require_paths # :nodoc: diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index f01a6cd743..478e294e09 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -1,22 +1,25 @@ +# frozen_string_literal: true + require_relative "user_interaction" class Gem::SpecificationPolicy include Gem::UserInteraction - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: - SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/.freeze # :nodoc: + SPECIAL_CHARACTERS = /\A[#{Regexp.escape(".-_")}]+/ # :nodoc: - VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}.freeze # :nodoc: + VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc: METADATA_LINK_KEYS = %w[ - bug_tracker_uri - changelog_uri - documentation_uri homepage_uri - mailing_list_uri + changelog_uri source_code_uri + documentation_uri wiki_uri + mailing_list_uri + bug_tracker_uri + download_uri funding_uri ].freeze # :nodoc: @@ -42,6 +45,7 @@ class Gem::SpecificationPolicy def validate(strict = false) validate_required! + validate_required_metadata! validate_optional(strict) if packaging || strict @@ -82,15 +86,17 @@ class Gem::SpecificationPolicy validate_authors_field - validate_metadata - validate_licenses_length - validate_lazy_metadata - validate_duplicate_dependencies end + def validate_required_metadata! + validate_metadata + + validate_lazy_metadata + end + def validate_optional(strict) validate_licenses @@ -100,10 +106,14 @@ class Gem::SpecificationPolicy validate_dependencies + validate_required_ruby_version + validate_extensions validate_removed_attributes + validate_unique_links + if @warnings > 0 if strict error "specification has warnings" @@ -114,6 +124,13 @@ class Gem::SpecificationPolicy end ## + # Implementation for Specification#validate_for_resolution + + def validate_for_resolution + validate_required! + end + + ## # Implementation for Specification#validate_metadata def validate_metadata @@ -125,7 +142,7 @@ class Gem::SpecificationPolicy metadata.each do |key, value| entry = "metadata['#{key}']" - if !key.kind_of?(String) + unless key.is_a?(String) error "metadata keys must be a String" end @@ -133,7 +150,7 @@ class Gem::SpecificationPolicy error "metadata key is too large (#{key.size} > 128)" end - if !value.kind_of?(String) + unless value.is_a?(String) error "#{entry} value must be a String" end @@ -141,10 +158,9 @@ class Gem::SpecificationPolicy error "#{entry} value is too large (#{value.size} > 1024)" end - if METADATA_LINK_KEYS.include? key - if value !~ VALID_URI_PATTERN - error "#{entry} has invalid link: #{value.inspect}" - end + next unless METADATA_LINK_KEYS.include? key + unless VALID_URI_PATTERN.match?(value) + error "#{entry} has invalid link: #{value.inspect}" end end end @@ -161,7 +177,7 @@ class Gem::SpecificationPolicy if prev = seen[dep.type][dep.name] error_messages << <<-MESSAGE duplicate dependency on #{dep}, (#{prev.requirement}) use: - add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}' + add_#{dep.type}_dependency \"#{dep.name}\", \"#{dep.requirement}\", \"#{prev.requirement}\" MESSAGE end @@ -173,49 +189,22 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end ## - # Checks that dependencies use requirements as we recommend. Warnings are - # issued when dependencies are open-ended or overly strict for semantic - # versioning. + # Checks that the gem does not depend on itself. def validate_dependencies # :nodoc: - warning_messages = [] + error_messages = [] @specification.dependencies.each do |dep| - prerelease_dep = dep.requirements_list.any? do |req| - Gem::Requirement.new(req).prerelease? + if dep.name == @specification.name # error on self reference + error_messages << "Dependencies of this gem include a self-reference." end + end - warning_messages << "prerelease dependency on #{dep} is not recommended" if - prerelease_dep && !@specification.version.prerelease? - - open_ended = dep.requirement.requirements.all? do |op, version| - !version.prerelease? && (op == ">" || op == ">=") - end - - if open_ended - op, dep_version = dep.requirement.requirements.first - - segments = dep_version.segments - - base = segments.first 2 - - recommendation = if (op == ">" || op == ">=") && segments == [0] - " use a bounded requirement, such as '~> x.y'" - else - bugfix = if op == ">" - ", '> #{dep_version}'" - elsif op == ">=" && base != segments - ", '>= #{dep_version}'" - end - - " if #{dep.name} is semantically versioned, use:\n" \ - " add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}" - end + error error_messages.join if error_messages.any? + end - warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" - end - end - if warning_messages.any? - warning_messages.each {|warning_message| warning warning_message } + def validate_required_ruby_version + if @specification.required_ruby_version.requirements == [Gem::Requirement::DefaultRequirement] + warning "make sure you specify the oldest ruby version constraint (like \">= 3.0\") that you want your gem to support by setting the `required_ruby_version` gemspec attribute" end end @@ -229,7 +218,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: @specification.files.each do |file| next unless File.file?(file) - next if File.stat(file).mode & 0444 == 0444 + next if File.stat(file).mode & 0o444 == 0o444 warning "#{file} is not world-readable" end @@ -248,7 +237,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: @specification.instance_variable_get("@#{attrname}").nil? end return if nil_attributes.empty? - error "#{nil_attributes.join ', '} must not be nil" + error "#{nil_attributes.join ", "} must not be nil" end def validate_rubygems_version @@ -258,7 +247,9 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: return if rubygems_version == Gem::VERSION - error "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}" + warning "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}" + + @specification.rubygems_version = Gem::VERSION end def validate_required_attributes @@ -274,12 +265,12 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: if !name.is_a?(String) error "invalid value for attribute name: \"#{name.inspect}\" must be a string" - elsif name !~ /[a-zA-Z]/ + elsif !/[a-zA-Z]/.match?(name) error "invalid value for attribute name: #{name.dump} must include at least one letter" - elsif name !~ VALID_NAME_PATTERN + elsif !VALID_NAME_PATTERN.match?(name) error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores" - elsif name =~ SPECIAL_CHARACTERS - error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore" + elsif SPECIAL_CHARACTERS.match?(name) + error "invalid value for attribute name: #{name.dump} cannot begin with a period, dash, or underscore" end end @@ -332,13 +323,13 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_array_attribute(field) val = @specification.send(field) klass = case field - when :dependencies then - Gem::Dependency - else - String + when :dependencies then + Gem::Dependency + else + String end - unless Array === val && val.all? {|x| x.kind_of?(klass) } + unless Array === val && val.all? {|x| x.is_a?(klass) || (field == :licenses && x.nil?) } error "#{field} must be an Array of #{klass}" end end @@ -353,6 +344,8 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: licenses = @specification.licenses licenses.each do |license| + next if license.nil? + if license.length > 64 error "each license must be 64 characters or less" end @@ -363,26 +356,38 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: licenses = @specification.licenses licenses.each do |license| - if !Gem::Licenses.match?(license) - suggestions = Gem::Licenses.suggestions(license) - message = <<-WARNING -license value '#{license}' is invalid. Use a license identifier from -http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license. - WARNING - message += "Did you mean #{suggestions.map {|s| "'#{s}'" }.join(', ')}?\n" unless suggestions.nil? - warning(message) + next if Gem::Licenses.match?(license) || license.nil? + license_id_deprecated = Gem::Licenses.deprecated_license_id?(license) + exception_id_deprecated = Gem::Licenses.deprecated_exception_id?(license) + suggestions = Gem::Licenses.suggestions(license) + + if license_id_deprecated + main_message = "License identifier '#{license}' is deprecated" + elsif exception_id_deprecated + main_message = "Exception identifier at '#{license}' is deprecated" + else + main_message = "License identifier '#{license}' is invalid" end + + message = <<-WARNING +#{main_message}. Use an identifier from +https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license, +or set it to nil if you don't want to specify a license. + WARNING + message += "Did you mean #{suggestions.map {|s| "'#{s}'" }.join(", ")}?\n" unless suggestions.nil? + warning(message) end warning <<-WARNING if licenses.empty? -licenses is empty, but is recommended. Use a license identifier from -http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license. +licenses is empty, but is recommended. Use an license identifier from +https://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard license, +or set it to nil if you don't want to specify a license. WARNING end LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, "") - LAZY_PATTERN = /\AFI XME|\ATO DO/x.freeze - HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze + LAZY_PATTERN = /\AFI XME|\ATO DO/x + HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i def validate_lazy_metadata unless @specification.authors.grep(LAZY_PATTERN).empty? @@ -393,11 +398,11 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li error "#{LAZY} is not an email" end - if @specification.description =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.description) error "#{LAZY} is not a description" end - if @specification.summary =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.summary) error "#{LAZY} is not a summary" end @@ -405,13 +410,13 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li # Make sure a homepage is valid HTTP/HTTPS URI if homepage && !homepage.empty? - require "uri" + require_relative "vendor/uri/lib/uri" begin - homepage_uri = URI.parse(homepage) - unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class + homepage_uri = Gem::URI.parse(homepage) + unless [Gem::URI::HTTP, Gem::URI::HTTPS].member? homepage_uri.class error "\"#{homepage}\" is not a valid HTTP URI" end - rescue URI::InvalidURIError + rescue Gem::URI::InvalidURIError error "\"#{homepage}\" is not a valid HTTP URI" end end @@ -430,6 +435,7 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li warning "deprecated autorequire specified" if @specification.autorequire @specification.executables.each do |executable| + validate_executable(executable) validate_shebang_line_in(executable) end @@ -443,6 +449,13 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li warning("no #{attribute} specified") if value.nil? || value.empty? end + def validate_executable(executable) + separators = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR].compact.map {|sep| Regexp.escape(sep) }.join + return unless executable.match?(/[\s#{separators}]/) + + error "executable \"#{executable}\" contains invalid characters" + end + def validate_shebang_line_in(executable) executable_path = File.join(@specification.bindir, executable) return if File.read(executable_path, 2) == "#!" @@ -462,11 +475,12 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li validate_rake_extensions(builder) validate_rust_extensions(builder) + validate_extension_require_relative end def validate_rust_extensions(builder) # :nodoc: rust_extension = @specification.extensions.any? {|s| builder.builder_for(s).is_a? Gem::Ext::CargoBuilder } - missing_cargo_lock = !@specification.files.include?("Cargo.lock") + missing_cargo_lock = !@specification.files.any? {|f| f.end_with?("Cargo.lock") } error <<-ERROR if rust_extension && missing_cargo_lock You have specified rust based extension, but Cargo.lock is not part of the gem files. Please run `cargo generate-lockfile` or any other command to generate Cargo.lock and ensure it is added to your gem files section in gemspec. @@ -475,13 +489,56 @@ You have specified rust based extension, but Cargo.lock is not part of the gem f def validate_rake_extensions(builder) # :nodoc: rake_extension = @specification.extensions.any? {|s| builder.builder_for(s) == Gem::Ext::RakeBuilder } - rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" } + rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" && d.type == :runtime } warning <<-WARNING if rake_extension && !rake_dependency -You have specified rake based extension, but rake is not added as dependency. It is recommended to add rake as a dependency in gemspec since there's no guarantee rake will be already installed. +You have specified rake based extension, but rake is not added as runtime dependency. It is recommended to add rake as a runtime dependency in gemspec since there's no guarantee rake will be already installed. WARNING end + def validate_extension_require_relative # :nodoc: + return unless @specification.extensions.any? + + require_paths = @specification.require_paths + + @specification.files.each do |rb_file| + next unless rb_file.end_with?(".rb") + next unless require_paths.any? {|rp| rb_file.start_with?("#{rp}/") } + next unless File.file?(rb_file) + + File.foreach(rb_file).with_index(1) do |line, lineno| + next unless line =~ /^\s*require_relative\s+["']([^"']+)["']/ + + required_path = Regexp.last_match(1) + resolved = File.join(File.dirname(rb_file), required_path) + + next if @specification.files.any? {|f| f == "#{resolved}.rb" || f == resolved } + + warning <<~WARNING + #{rb_file}:#{lineno} uses `require_relative "#{required_path}"` to load a compiled extension. + This will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory. + Use `require` instead of `require_relative` to load compiled extensions. + WARNING + end + end + end + + def validate_unique_links + links = @specification.metadata.slice(*METADATA_LINK_KEYS) + grouped = links.group_by {|_key, uri| uri } + grouped.each do |uri, copies| + next unless copies.length > 1 + keys = copies.map(&:first).join("\n ") + warning <<~WARNING + You have specified the uri: + #{uri} + for all of the following keys: + #{keys} + Only the first one will be shown on rubygems.org + WARNING + end + end + def warning(statement) # :nodoc: @warnings += 1 diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb new file mode 100644 index 0000000000..c7e5cbedb5 --- /dev/null +++ b/lib/rubygems/specification_record.rb @@ -0,0 +1,225 @@ +# frozen_string_literal: true + +module Gem + class SpecificationRecord + def self.dirs_from(paths) + paths.map do |path| + File.join(path, "specifications") + end + end + + def self.from_path(path) + new(dirs_from([path])) + end + + def initialize(dirs) + @all = nil + @stubs = nil + @stubs_by_name = {} + @spec_with_requirable_file = {} + @active_stub_with_requirable_file = {} + + @dirs = dirs + end + + # Sentinel object to represent "not found" stubs + NOT_FOUND = Struct.new(:to_spec, :this).new + private_constant :NOT_FOUND + + ## + # Returns the list of all specifications in the record + + def all + @all ||= stubs.map(&:to_spec) + end + + ## + # Returns a Gem::StubSpecification for every specification in the record + + def stubs + @stubs ||= begin + pattern = "*.gemspec" + stubs = stubs_for_pattern(pattern, false) + + @stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name) + stubs + end + end + + ## + # Returns a Gem::StubSpecification for every specification in the record + # named +name+ only returns stubs that match Gem.platforms + + def stubs_for(name) + if @stubs + @stubs_by_name[name] || [] + else + @stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s| + s.name == name + end + end + end + + ## + # Finds stub specifications matching a pattern in the record, optionally + # filtering out specs not matching the current platform + + def stubs_for_pattern(pattern, match_platform = true) + installed_stubs = installed_stubs(pattern) + installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform + stubs = installed_stubs + Gem::Specification.default_stubs(pattern) + Gem::Specification._resort!(stubs) + stubs + end + + ## + # Adds +spec+ to the record, keeping the collection properly sorted. + + def add_spec(spec) + return if all.include? spec + + all << spec + stubs << spec + (@stubs_by_name[spec.name] ||= []) << spec + + Gem::Specification._resort!(@stubs_by_name[spec.name]) + Gem::Specification._resort!(stubs) + end + + ## + # Removes +spec+ from the record. + + def remove_spec(spec) + all.delete spec.to_spec + stubs.delete spec + (@stubs_by_name[spec.name] || []).delete spec + end + + ## + # Sets the specs known by the record to +specs+. + + def all=(specs) + @stubs_by_name = specs.group_by(&:name) + @all = @stubs = specs + end + + ## + # Return full names of all specs in the record in sorted order. + + def all_names + all.map(&:full_name) + end + + include Enumerable + + ## + # Enumerate every known spec. + + def each + return enum_for(:each) unless block_given? + + all.each do |x| + yield x + end + end + + ## + # Returns every spec in the record that matches +name+ and optional +requirements+. + + def find_all_by_name(name, *requirements) + req = Gem::Requirement.create(*requirements) + env_req = Gem.env_requirement(name) + + matches = stubs_for(name).find_all do |spec| + req.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) + end.map(&:to_spec) + + if name == "bundler" && !req.specific? + require_relative "bundler_version_finder" + Gem::BundlerVersionFinder.prioritize!(matches) + end + + matches + end + + ## + # Return the best specification in the record that contains the file matching +path+. + + def find_by_path(path) + path = path.dup.freeze + spec = @spec_with_requirable_file[path] ||= stubs.find do |s| + s.contains_requirable_file? path + end || NOT_FOUND + + spec.to_spec + end + + ## + # Return the best specification that contains the file matching +path+ + # amongst the specs that are not loaded. This method is different than + # +find_inactive_by_path+ as it will filter out loaded specs by their name. + + def find_unloaded_by_path(path) + stub = stubs.find do |s| + next if Gem.loaded_specs[s.name] + s.contains_requirable_file? path + end + stub&.to_spec + end + + ## + # Return the best specification in the record that contains the file + # matching +path+ amongst the specs that are not activated. + + def find_inactive_by_path(path) + stub = stubs.find do |s| + next if s.activated? + s.contains_requirable_file? path + end + stub&.to_spec + end + + ## + # Return the best specification in the record that contains the file + # matching +path+, among those already activated. + + def find_active_stub_by_path(path) + stub = @active_stub_with_requirable_file[path] ||= stubs.find do |s| + s.activated? && s.contains_requirable_file?(path) + end || NOT_FOUND + + stub.this + end + + ## + # Return the latest specs in the record, optionally including prerelease + # specs if +prerelease+ is true. + + def latest_specs(prerelease) + Gem::Specification._latest_specs stubs, prerelease + end + + ## + # Return the latest installed spec in the record for gem +name+. + + def latest_spec_for(name) + latest_specs(true).find {|installed_spec| installed_spec.name == name } + end + + private + + def installed_stubs(pattern) + map_stubs(pattern) do |path, base_dir, gems_dir| + Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir) + end + end + + def map_stubs(pattern) + @dirs.flat_map do |dir| + base_dir = File.dirname dir + gems_dir = File.join base_dir, "gems" + Gem::Specification.gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir } + end + end + end +end diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA_R3.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem index 8afb219058..8afb219058 100644 --- a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA_R3.pem +++ b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem deleted file mode 100644 index f4ce4ca43d..0000000000 --- a/lib/rubygems/ssl_certs/rubygems.org/GlobalSignRootCA.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG -A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv -b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw -MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i -YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT -aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ -jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp -xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp -1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG -snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ -U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 -9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E -BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B -AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz -yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE -38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP -AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad -DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME -HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== ------END CERTIFICATE----- diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb index d87abdd993..53b337ed85 100644 --- a/lib/rubygems/stub_specification.rb +++ b/lib/rubygems/stub_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Gem::StubSpecification reads the stub: line from the gemspec. This prevents # us having to eval the entire gemspec in order to find out certain @@ -34,7 +35,7 @@ class Gem::StubSpecification < Gem::BasicSpecification def initialize(data, extensions) parts = data[PREFIX.length..-1].split(" ", 4) - @name = parts[0].freeze + @name = -parts[0] @version = if Gem::Version.correct?(parts[1]) Gem::Version.new(parts[1]) else @@ -68,7 +69,6 @@ class Gem::StubSpecification < Gem::BasicSpecification def initialize(filename, base_dir, gems_dir, default_gem) super() - filename.tap(&Gem::UNTAINT) self.loaded_from = filename @data = nil @@ -83,11 +83,7 @@ class Gem::StubSpecification < Gem::BasicSpecification # True when this gem has been activated def activated? - @activated ||= - begin - loaded = Gem.loaded_specs[name] - loaded && loaded.version == version - end + @activated ||= !loaded_spec.nil? end def default_gem? @@ -111,20 +107,23 @@ class Gem::StubSpecification < Gem::BasicSpecification saved_lineno = $. Gem.open_file loaded_from, OPEN_MODE do |file| - begin - file.readline # discard encoding line - stubline = file.readline.chomp - if stubline.start_with?(PREFIX) - extensions = if /\A#{PREFIX}/ =~ file.readline.chomp - $'.split "\0" + file.readline # discard encoding line + stubline = file.readline + if stubline.start_with?(PREFIX) + extline = file.readline + + extensions = + if extline.delete_prefix!(PREFIX) + extline.chomp! + extline.split "\0" else StubLine::NO_EXTENSIONS end - @data = StubLine.new stubline, extensions - end - rescue EOFError + stubline.chomp! # readline(chomp: true) allocates 3x as much as .readline.chomp! + @data = StubLine.new stubline, extensions end + rescue EOFError end ensure $. = saved_lineno @@ -141,6 +140,7 @@ class Gem::StubSpecification < Gem::BasicSpecification end def missing_extensions? + return false if RUBY_ENGINE == "jruby" return false if default_gem? return false if extensions.empty? return false if File.exist? gem_build_complete_path @@ -183,14 +183,11 @@ class Gem::StubSpecification < Gem::BasicSpecification ## # The full Gem::Specification for this gem, loaded from evalling its gemspec - def to_spec - @spec ||= if @data - loaded = Gem.loaded_specs[name] - loaded if loaded && loaded.version == version - end - + def spec + @spec ||= loaded_spec if @data @spec ||= Gem::Specification.load(loaded_from) end + alias_method :to_spec, :spec ## # Is this StubSpecification valid? i.e. have we found a stub line, OR does @@ -206,4 +203,34 @@ class Gem::StubSpecification < Gem::BasicSpecification def stubbed? data.is_a? StubLine end + + def ==(other) # :nodoc: + self.class === other && + name == other.name && + version == other.version && + platform == other.platform + end + + alias_method :eql?, :== # :nodoc: + + def hash # :nodoc: + name.hash ^ version.hash ^ platform.hash + end + + def <=>(other) # :nodoc: + sort_obj <=> other.sort_obj + end + + def sort_obj # :nodoc: + [name, version, Gem::Platform.sort_priority(platform)] + end + + private + + def loaded_spec + spec = Gem.loaded_specs[name] + return unless spec && spec.version == version && spec.default_gem? == default_gem? + + spec + end end diff --git a/lib/rubygems/target_rbconfig.rb b/lib/rubygems/target_rbconfig.rb new file mode 100644 index 0000000000..21d90ee9db --- /dev/null +++ b/lib/rubygems/target_rbconfig.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require "rbconfig" + +## +# A TargetConfig is a wrapper around an RbConfig object that provides a +# consistent interface for querying configuration for *deployment target +# platform*, where the gem being installed is intended to run on. +# +# The TargetConfig is typically created from the RbConfig of the running Ruby +# process, but can also be created from an RbConfig file on disk for cross- +# compiling gems. + +class Gem::TargetRbConfig + attr_reader :path + + def initialize(rbconfig, path) + @rbconfig = rbconfig + @path = path + end + + ## + # Creates a TargetRbConfig for the platform that RubyGems is running on. + + def self.for_running_ruby + new(::RbConfig, nil) + end + + ## + # Creates a TargetRbConfig from the RbConfig file at the given path. + # Typically used for cross-compiling gems. + + def self.from_path(rbconfig_path) + namespace = Module.new do |m| + # Load the rbconfig.rb file within a new anonymous module to avoid + # conflicts with the rbconfig for the running platform. + Kernel.load rbconfig_path, m + end + rbconfig = namespace.const_get(:RbConfig) + + new(rbconfig, rbconfig_path) + end + + ## + # Queries the configuration for the given key. + + def [](key) + @rbconfig::CONFIG[key] + end +end diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index be811525f2..0550dc473d 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -4,12 +4,20 @@ # A collection of text-wrangling methods module Gem::Text - ## # Remove any non-printable characters and make the text suitable for # printing. def clean_text(text) - text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") + text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") + + # Match C1 control characters (U+0080-U+009F) as codepoints. This requires + # a valid UTF-8 string so the regexp does not split a multibyte sequence; + # strings in other encodings are left unchanged. + if text.encoding == Encoding::UTF_8 && text.valid_encoding? + text = text.gsub(/[\u0080-\u009f]/, ".") + end + + text end def truncate_text(text, description, max_length = 100_000) @@ -22,7 +30,7 @@ module Gem::Text # Wraps +text+ to +wrap+ characters and optionally indents by +indent+ # characters - def format_text(text, wrap, indent=0) + def format_text(text, wrap, indent = 0) result = [] work = clean_text(text) @@ -67,7 +75,7 @@ module Gem::Text str1.each_codepoint.with_index(1) do |char1, i| j = 0 while j < m - cost = (char1 == str2_codepoints[j]) ? 0 : 1 + cost = char1 == str2_codepoints[j] ? 0 : 1 x = min3( d[j + 1] + 1, # insertion i + 1, # deletion diff --git a/lib/rubygems/tsort.rb b/lib/rubygems/tsort.rb deleted file mode 100644 index 60ebe22e81..0000000000 --- a/lib/rubygems/tsort.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "tsort/lib/tsort" diff --git a/lib/rubygems/tsort/.document b/lib/rubygems/tsort/.document deleted file mode 100644 index 0c43bbd6b3..0000000000 --- a/lib/rubygems/tsort/.document +++ /dev/null @@ -1 +0,0 @@ -# Vendored files do not need to be documented diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index 5883ed1c41..fe4c3a80cf 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -9,7 +10,6 @@ require "fileutils" require_relative "../rubygems" require_relative "installer_uninstaller_utils" require_relative "dependency_list" -require_relative "rdoc" require_relative "user_interaction" ## @@ -31,7 +31,7 @@ class Gem::Uninstaller attr_reader :bin_dir ## - # The gem repository the gem will be installed into + # The gem repository the gem will be uninstalled from attr_reader :gem_home @@ -42,24 +42,36 @@ class Gem::Uninstaller attr_reader :spec ## - # Constructs an uninstaller that will uninstall +gem+ + # Constructs an uninstaller that will uninstall gem named +gem+. + # +options+ is a Hash with the following keys: + # + # :version:: Version requirement for the gem to uninstall. If not specified, + # uses Gem::Requirement.default. + # :install_dir:: The directory where the gem is installed. If not specified, + # uses Gem.dir. + # :executables:: Whether executables should be removed without confirmation or not. If nil, asks the user explicitly. + # :all:: If more than one version matches the requirement, whether to forcefully remove all matching versions or ask the user to select specific matching versions that should be removed. + # :ignore:: Ignore broken dependency checks when uninstalling. + # :bin_dir:: Directory containing executables to remove. If not specified, + # uses Gem.bindir. + # :format_executable:: In order to find executables to be removed, format executable names using Gem::Installer.exec_format. + # :abort_on_dependent:: Directly abort uninstallation if dependencies would be broken, rather than asking the user for confirmation. + # :check_dev:: When checking if uninstalling gem would leave broken dependencies around, also consider development dependencies. + # :force:: Set both :all and :ignore to true for forced uninstallation. + # :user_install:: Uninstall from user gem directory instead of system directory. def initialize(gem, options = {}) - # TODO document the valid options @gem = gem @version = options[:version] || Gem::Requirement.default - @gem_home = File.realpath(options[:install_dir] || Gem.dir) - @plugins_dir = Gem.plugindir(@gem_home) + @install_dir = options[:install_dir] + @gem_home = File.realpath(@install_dir || Gem.dir) + @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir @force_executables = options[:executables] @force_all = options[:all] @force_ignore = options[:ignore] @bin_dir = options[:bin_dir] @format_executable = options[:format_executable] @abort_on_dependent = options[:abort_on_dependent] - - # Indicate if development dependencies should be checked when - # uninstalling. (default: false) - # @check_dev = options[:check_dev] if options[:force] @@ -69,7 +81,7 @@ class Gem::Uninstaller # only add user directory if install_dir is not set @user_install = false - @user_install = options[:user_install] unless options[:install_dir] + @user_install = options[:user_install] unless @install_dir # Optimization: populated during #uninstall @default_specs_matching_uninstall_params = [] @@ -84,11 +96,7 @@ class Gem::Uninstaller list = [] - dirs = - Gem::Specification.dirs + - [Gem.default_specifications_dir] - - Gem::Specification.each_spec dirs do |spec| + specification_record.stubs.each do |spec| next unless dependency.matches_spec? spec list << spec @@ -98,15 +106,13 @@ class Gem::Uninstaller raise Gem::InstallError, "gem #{@gem.inspect} is not installed" end - default_specs, list = list.partition do |spec| - spec.default_gem? - end + default_specs, list = list.partition(&:default_gem?) warn_cannot_uninstall_default_gems(default_specs - list) - @default_specs_matching_uninstall_params = default_specs + @default_specs_matching_uninstall_params = default_specs.map(&:to_spec) list, other_repo_specs = list.partition do |spec| @gem_home == spec.base_dir || - (@user_install && spec.base_dir == Gem.user_dir) + (@user_install && spec.base_dir == @user_dir) end list.sort! @@ -114,7 +120,7 @@ class Gem::Uninstaller if list.empty? return unless other_repo_specs.any? - other_repos = other_repo_specs.map {|spec| spec.base_dir }.uniq + other_repos = other_repo_specs.map(&:base_dir).uniq message = ["#{@gem} is not installed in GEM_HOME, try:"] message.concat other_repos.map {|repo| @@ -126,7 +132,7 @@ class Gem::Uninstaller remove_all list elsif list.size > 1 - gem_names = list.map {|gem| gem.full_name } + gem_names = list.map(&:full_name_with_location) gem_names << "All versions" say @@ -134,7 +140,7 @@ class Gem::Uninstaller if index == list.size remove_all list - elsif index >= 0 && index < list.size + elsif index && index >= 0 && index < list.size uninstall_gem list[index] else say "Error: must enter a number [1-#{list.size + 1}]" @@ -147,7 +153,9 @@ class Gem::Uninstaller ## # Uninstalls gem +spec+ - def uninstall_gem(spec) + def uninstall_gem(stub) + spec = stub.to_spec + @spec = spec unless dependencies_ok? spec @@ -165,6 +173,8 @@ class Gem::Uninstaller remove_plugins @spec remove @spec + specification_record.remove_spec(stub) + regenerate_plugins Gem.post_uninstall_hooks.each do |hook| @@ -178,7 +188,7 @@ class Gem::Uninstaller # Removes installed executables and batch files (windows only) for +spec+. def remove_executables(spec) - return if spec.executables.empty? + return if spec.executables.empty? || default_spec_matches?(spec) executables = spec.executables.clone @@ -200,8 +210,8 @@ class Gem::Uninstaller executables = executables.map {|exec| formatted_program_filename exec } remove = if @force_executables.nil? - ask_yes_no("Remove executables:\n" + - "\t#{executables.join ', '}\n\n" + + ask_yes_no("Remove executables:\n" \ + "\t#{executables.join ", "}\n\n" \ "in addition to the gem?", true) else @@ -240,9 +250,9 @@ class Gem::Uninstaller def remove(spec) unless path_ok?(@gem_home, spec) || - (@user_install && path_ok?(Gem.user_dir, spec)) + (@user_install && path_ok?(@user_dir, spec)) e = Gem::GemNotInHomeException.new \ - "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}" + "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}" e.spec = spec raise e @@ -251,7 +261,15 @@ class Gem::Uninstaller raise Gem::FilePermissionError, spec.base_dir unless File.writable?(spec.base_dir) - safe_delete { FileUtils.rm_r spec.full_gem_path } + full_gem_path = spec.full_gem_path + exclusions = [] + + if default_spec_matches?(spec) && spec.executables.any? + exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) } + exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path + end + + safe_delete { rm_r full_gem_path, exclusions: exclusions } safe_delete { FileUtils.rm_r spec.extension_dir } old_platform_name = spec.original_name @@ -275,8 +293,6 @@ class Gem::Uninstaller safe_delete { FileUtils.rm_r gemspec } announce_deletion_of(spec) - - Gem::Specification.reset end ## @@ -285,17 +301,17 @@ class Gem::Uninstaller def remove_plugins(spec) # :nodoc: return if spec.plugins.empty? - remove_plugins_for(spec, @plugins_dir) + remove_plugins_for(spec, plugin_dir_for(spec)) end ## # Regenerates plugin wrappers after removal. def regenerate_plugins - latest = Gem::Specification.latest_spec_for(@spec.name) + latest = specification_record.latest_spec_for(@spec.name) return if latest.nil? - regenerate_plugins_for(latest, @plugins_dir) + regenerate_plugins_for(latest, plugin_dir_for(@spec)) end ## @@ -341,7 +357,7 @@ class Gem::Uninstaller s.name == spec.name && s.full_name != spec.full_name end - spec.dependent_gems(@check_dev).each do |dep_spec, dep, satlist| + spec.dependent_gems(@check_dev).each do |dep_spec, dep, _satlist| unless siblings.any? {|s| s.satisfies_requirement? dep } msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}" end @@ -349,7 +365,7 @@ class Gem::Uninstaller msg << "If you remove this gem, these dependencies will not be met." msg << "Continue with Uninstall?" - return ask_yes_no(msg.join("\n"), false) + ask_yes_no(msg.join("\n"), false) end ## @@ -380,6 +396,16 @@ class Gem::Uninstaller private + def rm_r(path, exclusions:) + FileUtils::Entry_.new(path).postorder_traverse do |ent| + ent.remove unless exclusions.include?(ent.path) + end + end + + def specification_record + @specification_record ||= @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record + end + def announce_deletion_of(spec) name = spec.full_name say "Successfully uninstalled #{name}" @@ -407,4 +433,8 @@ class Gem::Uninstaller say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem" end end + + def plugin_dir_for(spec) + Gem.plugindir(spec.base_dir) + end end diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb index c2e81b2374..6f3ec5f493 100644 --- a/lib/rubygems/update_suggestion.rb +++ b/lib/rubygems/update_suggestion.rb @@ -4,15 +4,6 @@ # Mixin methods for Gem::Command to promote available RubyGems update module Gem::UpdateSuggestion - # list taken from https://github.com/watson/ci-info/blob/7a3c30d/index.js#L56-L66 - CI_ENV_VARS = [ - "CI", # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari - "CONTINUOUS_INTEGRATION", # Travis CI, Cirrus CI - "BUILD_NUMBER", # Jenkins, TeamCity - "CI_APP_ID", "CI_BUILD_ID", "CI_BUILD_NUMBER", # Applfow - "RUN_ID" # TaskCluster, dsari - ].freeze - ONE_WEEK = 7 * 24 * 60 * 60 ## @@ -28,9 +19,9 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa end ## - # Determines if current environment is eglible for update suggestion. + # Determines if current environment is eligible for update suggestion. - def eglible_for_update? + def eligible_for_update? # explicit opt-out return false if Gem.configuration[:prevent_update_suggestion] return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] @@ -39,7 +30,7 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa return false unless Gem.ui.tty? return false if Gem.rubygems_version.prerelease? return false if Gem.disable_system_update_message - return false if ci? + return false if Gem::CIDetector.ci? # check makes sense only when we can store timestamp of last try # otherwise we will not be able to prevent "annoying" update message @@ -53,17 +44,13 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa # compare current and latest version, this is the part where # latest rubygems spec is fetched from remote - (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eglible| + (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eligible| # store the time of last successful check into state file Gem.configuration.last_update_check = check_time - return eglible + return eligible end - rescue # don't block install command on any problem + rescue StandardError # don't block install command on any problem false end - - def ci? - CI_ENV_VARS.any? {|var| ENV.include?(var) } - end end diff --git a/lib/rubygems/uri.rb b/lib/rubygems/uri.rb index 4b5d035aa0..d729c67d26 100644 --- a/lib/rubygems/uri.rb +++ b/lib/rubygems/uri.rb @@ -16,9 +16,9 @@ class Gem::Uri # Parses uri, raising if it's invalid def self.parse!(uri) - require "uri" + require_relative "vendor/uri/lib/uri" - raise URI::InvalidURIError unless uri + raise Gem::URI::InvalidURIError unless uri return uri unless uri.is_a?(String) @@ -28,9 +28,9 @@ class Gem::Uri # 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)) + Gem::URI.parse(uri) + rescue Gem::URI::InvalidURIError + Gem::URI.parse(Gem::URI::RFC2396_PARSER.escape(uri)) end end @@ -39,7 +39,7 @@ class Gem::Uri def self.parse(uri) parse!(uri) - rescue URI::InvalidURIError + rescue Gem::URI::InvalidURIError uri end diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index 3f1d02d774..8856fdadd2 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -17,7 +17,8 @@ class Gem::UriFormatter # Creates a new URI formatter for +uri+. def initialize(uri) - require "cgi" + require "cgi/escape" + require "cgi/util" unless defined?(CGI::EscapeExt) @uri = uri end @@ -34,7 +35,7 @@ class Gem::UriFormatter # Normalize the URI by adding "http://" if it is missing. def normalize - (@uri =~ /^(https?|ftp|file):/i) ? @uri : "http://#{@uri}" + /^(https?|ftp|file):/i.match?(@uri) ? @uri : "http://#{@uri}" end ## diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 69de05fa24..9fe3e755c4 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require_relative "deprecate" require_relative "text" ## @@ -13,7 +13,6 @@ require_relative "text" # module will have access to the +ui+ method that returns the default UI. module Gem::DefaultUserInteraction - include Gem::Text ## @@ -68,7 +67,6 @@ module Gem::DefaultUserInteraction def use_ui(new_ui, &block) Gem::DefaultUserInteraction.use_ui(new_ui, &block) end - end ## @@ -91,7 +89,6 @@ end # end module Gem::UserInteraction - include Gem::DefaultUserInteraction ## @@ -172,8 +169,6 @@ end # Gem::StreamUI implements a simple stream based user interface. class Gem::StreamUI - extend Gem::Deprecate - ## # The input stream @@ -195,7 +190,7 @@ class Gem::StreamUI # then special operations (like asking for passwords) will use the TTY # commands to disable character echo. - def initialize(in_stream, out_stream, err_stream=STDERR, usetty=true) + def initialize(in_stream, out_stream, err_stream = $stderr, usetty = true) @ins = in_stream @outs = out_stream @errs = err_stream @@ -239,7 +234,8 @@ class Gem::StreamUI return nil, nil unless result result = result.strip.to_i - 1 - return list[result], result + return nil, nil unless (0...list.size) === result + [list[result], result] end ## @@ -247,7 +243,7 @@ class Gem::StreamUI # to a tty, raises an exception if default is nil, otherwise returns # default. - def ask_yes_no(question, default=nil) + def ask_yes_no(question, default = nil) unless tty? if default.nil? raise Gem::OperationNotSupportedError, @@ -258,33 +254,32 @@ class Gem::StreamUI end default_answer = case default - when nil - "yn" - when true - "Yn" - else - "yN" + when nil + "yn" + when true + "Yn" + else + "yN" end result = nil while result.nil? do result = case ask "#{question} [#{default_answer}]" - when /^y/i then true - when /^n/i then false - when /^$/ then default - else nil + when /^y/i then true + when /^n/i then false + when /^$/ then default end end - return result + result end ## # Ask a question. Returns an answer if connected to a tty, nil otherwise. def ask(question) - return nil if !tty? + return nil unless tty? @outs.print(question + " ") @outs.flush @@ -298,7 +293,7 @@ class Gem::StreamUI # Ask for a password. Does not echo response to terminal. def ask_for_password(question) - return nil if !tty? + return nil unless tty? @outs.print(question, " ") @outs.flush @@ -327,14 +322,14 @@ class Gem::StreamUI ## # Display a statement. - def say(statement="") + def say(statement = "") @outs.puts statement end ## # Display an informational alert. Will ask +question+ if it is not nil. - def alert(statement, question=nil) + def alert(statement, question = nil) @outs.puts "INFO: #{statement}" ask(question) if question end @@ -342,7 +337,7 @@ class Gem::StreamUI ## # Display a warning on stderr. Will ask +question+ if it is not nil. - def alert_warning(statement, question=nil) + def alert_warning(statement, question = nil) @errs.puts "WARNING: #{statement}" ask(question) if question end @@ -351,7 +346,7 @@ class Gem::StreamUI # Display an error message in a location expected to get error messages. # Will ask +question+ if it is not nil. - def alert_error(statement, question=nil) + def alert_error(statement, question = nil) @errs.puts "ERROR: #{statement}" ask(question) if question end @@ -428,8 +423,7 @@ class Gem::StreamUI # +size+ items. Shows the given +initial_message+ when progress starts # and the +terminal_message+ when it is complete. - def initialize(out_stream, size, initial_message, - terminal_message = "complete") + def initialize(out_stream, size, initial_message, terminal_message = "complete") @out = out_stream @total = size @count = 0 @@ -471,8 +465,7 @@ class Gem::StreamUI # +size+ items. Shows the given +initial_message+ when progress starts # and the +terminal_message+ when it is complete. - def initialize(out_stream, size, initial_message, - terminal_message = "complete") + def initialize(out_stream, size, initial_message, terminal_message = "complete") @out = out_stream @total = size @count = 0 @@ -595,8 +588,8 @@ class Gem::StreamUI end ## -# Subclass of StreamUI that instantiates the user interaction using STDIN, -# STDOUT, and STDERR. +# Subclass of StreamUI that instantiates the user interaction using $stdin, +# $stdout, and $stderr. class Gem::ConsoleUI < Gem::StreamUI ## @@ -604,7 +597,7 @@ class Gem::ConsoleUI < Gem::StreamUI # stdin, output to stdout and warnings or errors to stderr. def initialize - super STDIN, STDOUT, STDERR, true + super $stdin, $stdout, $stderr, true end end diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 05e5788932..ee4106c6ce 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -require_relative "deprecate" ## # This module contains various utility methods as module methods. module Gem::Util - ## # Zlib::GzipReader wrapper that unzips +data+. @@ -57,26 +55,6 @@ module Gem::Util end ## - # Invokes system, but silences all output. - - def self.silent_system(*command) - opt = { :out => IO::NULL, :err => [:child, :out] } - if Hash === command.last - opt.update(command.last) - cmds = command[0...-1] - else - cmds = command.dup - end - system(*(cmds << opt)) - end - - class << self - extend Gem::Deprecate - - rubygems_deprecate :silent_system - end - - ## # Enumerates the parents of +directory+. def self.traverse_parents(directory, &block) @@ -84,7 +62,11 @@ module Gem::Util here = File.expand_path directory loop do - Dir.chdir here, &block rescue Errno::EACCES + begin + Dir.chdir here, &block + rescue StandardError + Errno::EACCES + end new_here = File.expand_path("..", here) return if new_here == here # toplevel @@ -101,15 +83,14 @@ module Gem::Util end ## - # Corrects +path+ (usually returned by `URI.parse().path` on Windows), that + # Corrects +path+ (usually returned by `Gem::URI.parse().path` on Windows), that # comes with a leading slash. def self.correct_for_windows_path(path) - if path[0].chr == "/" && path[1].chr =~ /[a-z]/i && path[2].chr == ":" + if path[0].chr == "/" && path[1].chr.match?(/[a-z]/i) && path[2].chr == ":" path[1..-1] else path end end - end diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb new file mode 100644 index 0000000000..32767c6a79 --- /dev/null +++ b/lib/rubygems/util/atomic_file_writer.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +# Based on ActiveSupport's AtomicFile implementation +# Copyright (c) David Heinemeier Hansson +# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb +# Licensed under the MIT License + +module Gem + class AtomicFileWriter + ## + # Write to a file atomically. Useful for situations where you don't + # want other processes or threads to see half-written files. + + def self.open(file_name) + require "securerandom" unless defined?(SecureRandom) + + old_stat = begin + File.stat(file_name) + rescue SystemCallError + nil + end + + # Names can't be longer than 255B + tmp_suffix = ".tmp.#{SecureRandom.hex}" + dirname = File.dirname(file_name) + basename = File.basename(file_name) + tmp_path = File.join(dirname, ".#{basename.byteslice(0, 254 - tmp_suffix.bytesize)}#{tmp_suffix}") + + flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY + flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE) + + File.open(tmp_path, flags) do |temp_file| + temp_file.binmode + if old_stat + # Set correct permissions on new file + begin + File.chown(old_stat.uid, old_stat.gid, temp_file.path) + # This operation will affect filesystem ACL's + File.chmod(old_stat.mode, temp_file.path) + rescue Errno::EPERM, Errno::EACCES + # Changing file ownership failed, moving on. + end + end + + return_val = yield temp_file + rescue StandardError => error + begin + temp_file.close + rescue StandardError + nil + end + + begin + File.unlink(temp_file.path) + rescue StandardError + nil + end + + raise error + else + begin + File.rename(temp_file.path, file_name) + rescue StandardError + begin + File.unlink(temp_file.path) + rescue StandardError + end + + raise + end + + return_val + end + end + end +end diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb index a823521d10..caf53d0b7e 100644 --- a/lib/rubygems/util/licenses.rb +++ b/lib/rubygems/util/licenses.rb @@ -1,4 +1,8 @@ # frozen_string_literal: true + +# This is generated by generate_spdx_license_list.rb, any edits to this +# file will be discarded. + require_relative "../text" class Gem::Licenses @@ -11,6 +15,7 @@ class Gem::Licenses # license identifiers LICENSE_IDENTIFIERS = %w[ 0BSD + 3D-Slicer-1.0 AAL ADSL AFL-1.1 @@ -18,14 +23,15 @@ class Gem::Licenses AFL-2.0 AFL-2.1 AFL-3.0 - AGPL-1.0 AGPL-1.0-only AGPL-1.0-or-later - AGPL-3.0 AGPL-3.0-only AGPL-3.0-or-later + ALGLIB-Documentation + AMD-newlib AMDPLPA AML + AML-glslang AMPAS ANTLR-PD ANTLR-PD-fallback @@ -35,27 +41,40 @@ class Gem::Licenses APSL-1.1 APSL-1.2 APSL-2.0 + ASWF-Digital-Assets-1.0 + ASWF-Digital-Assets-1.1 Abstyles + AdaCore-doc Adobe-2006 + Adobe-Display-PostScript Adobe-Glyph + Adobe-Utopia + Advanced-Cryptics-Dictionary Afmparse Aladdin Apache-1.0 Apache-1.1 Apache-2.0 + App-s2p + Arphic-1999 Artistic-1.0 Artistic-1.0-Perl Artistic-1.0-cl8 Artistic-2.0 + Artistic-dist + Aspell-RU + BOLA-1.1 BSD-1-Clause BSD-2-Clause - BSD-2-Clause-FreeBSD - BSD-2-Clause-NetBSD + BSD-2-Clause-Darwin BSD-2-Clause-Patent BSD-2-Clause-Views + BSD-2-Clause-first-lines + BSD-2-Clause-pkgconf-disclaimer BSD-3-Clause BSD-3-Clause-Attribution BSD-3-Clause-Clear + BSD-3-Clause-HP BSD-3-Clause-LBNL BSD-3-Clause-Modification BSD-3-Clause-No-Military-License @@ -63,51 +82,86 @@ class Gem::Licenses BSD-3-Clause-No-Nuclear-License-2014 BSD-3-Clause-No-Nuclear-Warranty BSD-3-Clause-Open-MPI + BSD-3-Clause-Sun + BSD-3-Clause-Tso + BSD-3-Clause-acpica + BSD-3-Clause-flex BSD-4-Clause BSD-4-Clause-Shortened BSD-4-Clause-UC + BSD-4.3RENO + BSD-4.3TAHOE + BSD-Advertising-Acknowledgement + BSD-Attribution-HPND-disclaimer + BSD-Inferno-Nettverk + BSD-Mark-Modifications BSD-Protection BSD-Source-Code + BSD-Source-beginning-file + BSD-Systemics + BSD-Systemics-W3Works BSL-1.0 BUSL-1.1 + Baekmuk Bahyph Barr Beerware BitTorrent-1.0 BitTorrent-1.1 + Bitstream-Charter + Bitstream-Vera BlueOak-1.0.0 + Boehm-GC + Boehm-GC-without-fee Borceux + Brian-Gladman-2-Clause + Brian-Gladman-3-Clause + Buddy C-UDA-1.0 CAL-1.0 CAL-1.0-Combined-Work-Exception + CAPEC-tou CATOSL-1.1 CC-BY-1.0 CC-BY-2.0 CC-BY-2.5 + CC-BY-2.5-AU CC-BY-3.0 CC-BY-3.0-AT + CC-BY-3.0-AU + CC-BY-3.0-DE + CC-BY-3.0-IGO + CC-BY-3.0-NL CC-BY-3.0-US CC-BY-4.0 CC-BY-NC-1.0 CC-BY-NC-2.0 CC-BY-NC-2.5 CC-BY-NC-3.0 + CC-BY-NC-3.0-DE CC-BY-NC-4.0 CC-BY-NC-ND-1.0 CC-BY-NC-ND-2.0 CC-BY-NC-ND-2.5 CC-BY-NC-ND-3.0 + CC-BY-NC-ND-3.0-DE CC-BY-NC-ND-3.0-IGO CC-BY-NC-ND-4.0 CC-BY-NC-SA-1.0 CC-BY-NC-SA-2.0 + CC-BY-NC-SA-2.0-DE + CC-BY-NC-SA-2.0-FR + CC-BY-NC-SA-2.0-UK CC-BY-NC-SA-2.5 CC-BY-NC-SA-3.0 + CC-BY-NC-SA-3.0-DE + CC-BY-NC-SA-3.0-IGO CC-BY-NC-SA-4.0 CC-BY-ND-1.0 CC-BY-ND-2.0 CC-BY-ND-2.5 CC-BY-ND-3.0 + CC-BY-ND-3.0-DE CC-BY-ND-4.0 CC-BY-SA-1.0 CC-BY-SA-2.0 @@ -116,13 +170,18 @@ class Gem::Licenses CC-BY-SA-2.5 CC-BY-SA-3.0 CC-BY-SA-3.0-AT + CC-BY-SA-3.0-DE + CC-BY-SA-3.0-IGO CC-BY-SA-4.0 CC-PDDC + CC-PDM-1.0 + CC-SA-1.0 CC0-1.0 CDDL-1.0 CDDL-1.1 CDL-1.0 CDLA-Permissive-1.0 + CDLA-Permissive-2.0 CDLA-Sharing-1.0 CECILL-1.0 CECILL-1.1 @@ -135,23 +194,42 @@ class Gem::Licenses CERN-OHL-P-2.0 CERN-OHL-S-2.0 CERN-OHL-W-2.0 + CFITSIO + CMU-Mach + CMU-Mach-nodoc CNRI-Jython CNRI-Python CNRI-Python-GPL-Compatible + COIL-1.0 CPAL-1.0 CPL-1.0 CPOL-1.02 CUA-OPL-1.0 Caldera + Caldera-no-preamble + Catharon ClArtistic + Clips + Community-Spec-1.0 Condor-1.1 + Cornell-Lossless-JPEG + Cronyx Crossword + CryptoSwift CrystalStacker Cube D-FSL-1.0 + DEC-3-Clause + DL-DE-BY-2.0 + DL-DE-ZERO-2.0 DOC DRL-1.0 + DRL-1.1 DSDP + DocBook-DTD + DocBook-Schema + DocBook-Stylesheet + DocBook-XML Dotseqn ECL-1.0 ECL-2.0 @@ -160,37 +238,48 @@ class Gem::Licenses EPICS EPL-1.0 EPL-2.0 + ESA-PL-permissive-2.4 + ESA-PL-strong-copyleft-2.4 + ESA-PL-weak-copyleft-2.4 EUDatagrid EUPL-1.0 EUPL-1.1 EUPL-1.2 + Elastic-2.0 Entessa ErlPL-1.1 Eurosym + FBM + FDK-AAC FSFAP + FSFAP-no-warranty-disclaimer FSFUL FSFULLR + FSFULLRSD + FSFULLRWD + FSL-1.1-ALv2 + FSL-1.1-MIT FTL Fair + Ferguson-Twofish Frameworx-1.0 FreeBSD-DOC FreeImage + Furuseth + GCR-docs GD - GFDL-1.1 GFDL-1.1-invariants-only GFDL-1.1-invariants-or-later GFDL-1.1-no-invariants-only GFDL-1.1-no-invariants-or-later GFDL-1.1-only GFDL-1.1-or-later - GFDL-1.2 GFDL-1.2-invariants-only GFDL-1.2-invariants-or-later GFDL-1.2-no-invariants-only GFDL-1.2-no-invariants-or-later GFDL-1.2-only GFDL-1.2-or-later - GFDL-1.3 GFDL-1.3-invariants-only GFDL-1.3-invariants-or-later GFDL-1.3-no-invariants-only @@ -199,63 +288,88 @@ class Gem::Licenses GFDL-1.3-or-later GL2PS GLWTPL - GPL-1.0 - GPL-1.0+ GPL-1.0-only GPL-1.0-or-later - GPL-2.0 - GPL-2.0+ GPL-2.0-only GPL-2.0-or-later - GPL-2.0-with-GCC-exception - GPL-2.0-with-autoconf-exception - GPL-2.0-with-bison-exception - GPL-2.0-with-classpath-exception - GPL-2.0-with-font-exception - GPL-3.0 - GPL-3.0+ GPL-3.0-only GPL-3.0-or-later - GPL-3.0-with-GCC-exception - GPL-3.0-with-autoconf-exception + Game-Programming-Gems Giftware Glide Glulxe + Graphics-Gems + Gutmann + HDF5 + HIDAPI + HP-1986 + HP-1989 HPND + HPND-DEC + HPND-Fenneberg-Livingston + HPND-INRIA-IMAG + HPND-Intel + HPND-Kevlin-Henney + HPND-MIT-disclaimer + HPND-Markus-Kuhn + HPND-Netrek + HPND-Pbmplus + HPND-SMC + HPND-UC + HPND-UC-export-US + HPND-doc + HPND-doc-sell + HPND-export-US + HPND-export-US-acknowledgement + HPND-export-US-modify + HPND-export2-US + HPND-merchantability-variant + HPND-sell-MIT-disclaimer-xserver + HPND-sell-regexpr HPND-sell-variant + HPND-sell-variant-MIT-disclaimer + HPND-sell-variant-MIT-disclaimer-rev + HPND-sell-variant-critical-systems HTMLTIDY HaskellReport Hippocratic-2.1 IBM-pibs ICU + IEC-Code-Components-EULA IJG + IJG-short IPA IPL-1.0 ISC + ISC-Veillard + ISO-permission ImageMagick Imlib2 Info-ZIP + Inner-Net-2.0 + InnoSetup Intel Intel-ACPI Interbase-1.0 + JPL-image JPNIC JSON + Jam JasPer-2.0 + Kastrup + Kazlib + Knuth-CTAN LAL-1.2 LAL-1.3 - LGPL-2.0 - LGPL-2.0+ LGPL-2.0-only LGPL-2.0-or-later - LGPL-2.1 - LGPL-2.1+ LGPL-2.1-only LGPL-2.1-or-later - LGPL-3.0 - LGPL-3.0+ LGPL-3.0-only LGPL-3.0-or-later LGPLLR + LOOP + LPD-document LPL-1.0 LPL-1.02 LPPL-1.0 @@ -263,30 +377,54 @@ class Gem::Licenses LPPL-1.2 LPPL-1.3a LPPL-1.3c + LZMA-SDK-9.11-to-9.20 + LZMA-SDK-9.22 Latex2e + Latex2e-translated-notice Leptonica LiLiQ-P-1.1 LiLiQ-R-1.1 LiLiQ-Rplus-1.1 Libpng Linux-OpenIB + Linux-man-pages-1-para + Linux-man-pages-copyleft + Linux-man-pages-copyleft-2-para + Linux-man-pages-copyleft-var + Lucida-Bitmap-Fonts + MIPS MIT MIT-0 MIT-CMU + MIT-Click + MIT-Festival + MIT-Khronos-old MIT-Modern-Variant + MIT-STK + MIT-Wu MIT-advertising MIT-enna MIT-feh MIT-open-group + MIT-testregex MITNFA + MMIXware + MMPL-1.0.1 + MPEG-SSG MPL-1.0 MPL-1.1 MPL-2.0 MPL-2.0-no-copyleft-exception + MS-LPL MS-PL MS-RL MTLL + Mackerras-3-Clause + Mackerras-3-Clause-acknowledgment MakeIndex + Martin-Birgmeier + McPhee-slideshow + Minpack MirOS Motosoto MulanPSL-1.0 @@ -296,32 +434,39 @@ class Gem::Licenses NAIST-2003 NASA-1.3 NBPL-1.0 + NCBI-PD NCGL-UK-2.0 + NCL NCSA NGPL + NICTA-1.0 NIST-PD + NIST-PD-TNT NIST-PD-fallback + NIST-Software NLOD-1.0 + NLOD-2.0 NLPL NOSL NPL-1.0 NPL-1.1 NPOSL-3.0 NRL + NTIA-PD NTP NTP-0 Naumen - Net-SNMP NetCDF Newsletr Nokia Noweb - Nunit O-UDA-1.0 + OAR OCCT-PL OCLC-2.0 ODC-By-1.0 ODbL-1.0 + OFFIS OFL-1.0 OFL-1.0-RFN OFL-1.0-no-RFN @@ -351,27 +496,42 @@ class Gem::Licenses OLDAP-2.6 OLDAP-2.7 OLDAP-2.8 + OLFL-1.3 OML OPL-1.0 + OPL-UK-3.0 + OPUBL-1.0 + OSC-1.0 OSET-PL-2.1 OSL-1.0 OSL-1.1 OSL-2.0 OSL-2.1 OSL-3.0 + OSSP + OpenMDW-1.0 + OpenPBS-2.3 OpenSSL + OpenSSL-standalone + OpenVision + PADL PDDL-1.0 PHP-3.0 PHP-3.01 + PPL PSF-2.0 + ParaType-Free-Font-1.3 Parity-6.0.0 Parity-7.0.0 + Pixar Plexus PolyForm-Noncommercial-1.0.0 PolyForm-Small-Business-1.0.0 PostgreSQL Python-2.0 + Python-2.0.1 QPL-1.0 + QPL-1.0-INRIA-2004 Qhull RHeCos-1.1 RPL-1.1 @@ -381,60 +541,103 @@ class Gem::Licenses RSCPL Rdisc Ruby + Ruby-pty SAX-PD + SAX-PD-2.0 SCEA SGI-B-1.0 SGI-B-1.1 SGI-B-2.0 + SGI-OpenGL + SGMLUG-PM + SGP4 SHL-0.5 SHL-0.51 SISSL SISSL-1.2 + SL + SMAIL-GPL SMLNJ SMPPL SNIA + SOFA SPL-1.0 SSH-OpenSSH SSH-short + SSLeay-standalone SSPL-1.0 + SUL-1.0 SWL Saxpath + SchemeReport Sendmail Sendmail-8.23 + Sendmail-Open-Source-1.1 SimPL-2.0 Sleepycat + Soundex Spencer-86 Spencer-94 Spencer-99 - StandardML-NJ SugarCRM-1.1.3 + Sun-PPP + Sun-PPP-2000 + SunPro + Symlinks TAPR-OHL-1.0 TCL TCP-wrappers + TGPPL-1.0 TMate TORQUE-1.1 TOSL + TPDL + TPL-1.0 + TTWL + TTYP0 TU-Berlin-1.0 TU-Berlin-2.0 + TekHVC + TermReadKey + ThirdEye + TrustedQSL + UCAR UCL-1.0 + UMich-Merit UPL-1.0 + URT-RLE + Ubuntu-font-1.0 + UnRAR + Unicode-3.0 Unicode-DFS-2015 Unicode-DFS-2016 Unicode-TOU + UnixCrypt Unlicense + Unlicense-libtelnet + Unlicense-libwhirlpool VOSTROM VSL-1.0 Vim + Vixie-Cron W3C W3C-19980720 W3C-20150513 + WTFNMFPL WTFPL Watcom-1.0 + Widget-Workshop + WordNet Wsuipa X11 + X11-distribute-modifications-variant + X11-no-permit-persons + X11-swapped XFree86-1.1 XSkat + Xdebug-1.03 Xerox + Xfig Xnet YPL-1.0 YPL-1.1 @@ -442,82 +645,199 @@ class Gem::Licenses ZPL-2.0 ZPL-2.1 Zed + Zeeff Zend-2.0 Zimbra-1.3 Zimbra-1.4 Zlib + any-OSI + any-OSI-perl-modules + bcrypt-Solar-Designer blessing - bzip2-1.0.5 bzip2-1.0.6 + check-cvs + checkmk copyleft-next-0.3.0 copyleft-next-0.3.1 curl + cve-tou diffmark + dtoa dvipdfm - eCos-2.0 eGenix etalab-2.0 + fwlw gSOAP-1.3b + generic-xts gnuplot + gtkbook + hdparm + hyphen-bulgarian iMatix + jove + libpng-1.6.35 libpng-2.0 libselinux-1.0 libtiff + libutil-David-Nugent + lsof + magaz + mailprio + man2html + metamail + mpi-permissive mpich2 + mplus + ngrep + pkgconf + pnmstitch psfrag psutils - wxWindows + python-ldap + radvd + snprintf + softSurfer + ssh-keyscan + swrule + threeparttable + ulem + w3m + wwl xinetd + xkeyboard-config-Zinoviev + xlock xpp + xzoom zlib-acknowledgement ].freeze + DEPRECATED_LICENSE_IDENTIFIERS = %w[ + AGPL-1.0 + AGPL-3.0 + BSD-2-Clause-FreeBSD + BSD-2-Clause-NetBSD + GFDL-1.1 + GFDL-1.2 + GFDL-1.3 + GPL-1.0 + GPL-1.0+ + GPL-2.0 + GPL-2.0+ + GPL-2.0-with-GCC-exception + GPL-2.0-with-autoconf-exception + GPL-2.0-with-bison-exception + GPL-2.0-with-classpath-exception + GPL-2.0-with-font-exception + GPL-3.0 + GPL-3.0+ + GPL-3.0-with-GCC-exception + GPL-3.0-with-autoconf-exception + LGPL-2.0 + LGPL-2.0+ + LGPL-2.1 + LGPL-2.1+ + LGPL-3.0 + LGPL-3.0+ + Net-SNMP + Nunit + StandardML-NJ + bzip2-1.0.5 + eCos-2.0 + wxWindows + ].freeze + # exception identifiers EXCEPTION_IDENTIFIERS = %w[ 389-exception + Asterisk-exception + Asterisk-linking-protocols-exception Autoconf-exception-2.0 Autoconf-exception-3.0 + Autoconf-exception-generic + Autoconf-exception-generic-3.0 + Autoconf-exception-macro + Bison-exception-1.24 Bison-exception-2.2 Bootloader-exception + CGAL-linking-exception CLISP-exception-2.0 Classpath-exception-2.0 + Classpath-exception-2.0-short DigiRule-FOSS-exception + Digia-Qt-LGPL-exception-1.1 FLTK-exception Fawkes-Runtime-exception Font-exception-2.0 GCC-exception-2.0 + GCC-exception-2.0-note GCC-exception-3.1 + GNAT-exception + GNOME-examples-exception + GNU-compiler-exception + GPL-3.0-389-ds-base-exception + GPL-3.0-interface-exception GPL-3.0-linking-exception GPL-3.0-linking-source-exception GPL-CC-1.0 + GStreamer-exception-2005 + GStreamer-exception-2008 + Gmsh-exception + Independent-modules-exception + KiCad-libraries-exception LGPL-3.0-linking-exception + LLGPL LLVM-exception LZMA-exception Libtool-exception Linux-syscall-note - Nokia-Qt-exception-1.1 OCCT-exception-1.0 OCaml-LGPL-linking-exception OpenJDK-assembly-exception-1.0 + PCRE2-exception PS-or-PDF-font-exception-20170817 + QPL-1.0-INRIA-2004-exception Qt-GPL-exception-1.0 Qt-LGPL-exception-1.1 Qwt-exception-1.0 + RRDtool-FLOSS-exception-2.0 + SANE-exception SHL-2.0 SHL-2.1 + SWI-exception + Simple-Library-Usage-exception Swift-exception + Texinfo-exception + UBDL-exception Universal-FOSS-exception-1.0 WxWindows-exception-3.1 + cryptsetup-OpenSSL-exception eCos-exception-2.0 + erlang-otp-linking-exception + fmt-exception freertos-exception-2.0 gnu-javamail-exception + harbour-exception i2p-gpl-java-exception + kvirc-openssl-exception + libpri-OpenH323-exception mif-exception + mxml-exception openvpn-openssl-exception + polyparse-exception + romic-exception + rsync-linking-exception + sqlitestudio-OpenSSL-exception + stunnel-exception u-boot-exception-2.0 + vsftpd-openssl-exception + x11vnc-openssl-exception + ].freeze + + DEPRECATED_EXCEPTION_IDENTIFIERS = %w[ + Nokia-Qt-exception-1.1 ].freeze - REGEXP = %r{ + VALID_REGEXP = / \A (?: #{Regexp.union(LICENSE_IDENTIFIERS)} @@ -527,10 +847,34 @@ class Gem::Licenses | #{LICENSE_REF} ) \Z - }ox.freeze + /ox + + DEPRECATED_LICENSE_REGEXP = / + \A + #{Regexp.union(DEPRECATED_LICENSE_IDENTIFIERS)} + \+? + (?:\s WITH \s .+?)? + \Z + /ox + + DEPRECATED_EXCEPTION_REGEXP = / + \A + .+? + \+? + (?:\s WITH \s #{Regexp.union(DEPRECATED_EXCEPTION_IDENTIFIERS)}) + \Z + /ox def self.match?(license) - !REGEXP.match(license).nil? + VALID_REGEXP.match?(license) + end + + def self.deprecated_license_id?(license) + DEPRECATED_LICENSE_REGEXP.match?(license) + end + + def self.deprecated_exception_id?(license) + DEPRECATED_EXCEPTION_REGEXP.match?(license) end def self.suggestions(license) diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb deleted file mode 100644 index 33c40af4bb..0000000000 --- a/lib/rubygems/util/list.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true -module Gem - class List - include Enumerable - attr_accessor :value, :tail - - def initialize(value = nil, tail = nil) - @value = value - @tail = tail - end - - def each - n = self - while n - yield n.value - n = n.tail - end - end - - def to_a - super.reverse - end - - def prepend(value) - List.new value, self - end - - def pretty_print(q) # :nodoc: - q.pp to_a - end - - def self.prepend(list, value) - return List.new(value) unless list - List.new value, list - end - end -end diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index 1609924607..eb5b513570 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -24,7 +25,7 @@ class Gem::Validator installed_files = [] Find.find gem_directory do |file_name| - fn = file_name[gem_directory.size..file_name.size - 1].sub(/^\//, "") + fn = file_name[gem_directory.size..file_name.size - 1].sub(%r{^/}, "") installed_files << fn unless fn.empty? || fn.include?("CVS") || File.directory?(file_name) end @@ -58,11 +59,13 @@ class Gem::Validator #-- # TODO needs further cleanup - def alien(gems=[]) + def alien(gems = []) errors = Hash.new {|h,k| h[k] = {} } Gem::Specification.each do |spec| - next unless gems.include? spec.name unless gems.empty? + unless gems.empty? + next unless gems.include? spec.name + end next if spec.default_gem? gem_name = spec.file_name @@ -87,7 +90,7 @@ class Gem::Validator good, gone, unreadable = nil, nil, nil, nil - File.open gem_path, Gem.binary_mode do |file| + File.open gem_path, Gem.binary_mode do |_file| package = Gem::Package.new gem_path good, gone = package.contents.partition do |file_name| @@ -107,15 +110,13 @@ class Gem::Validator end good.each do |entry, data| - begin - next unless data # HACK `gem check -a mkrf` + next unless data # HACK: `gem check -a mkrf` - source = File.join gem_directory, entry["path"] + source = File.join gem_directory, entry["path"] - File.open source, Gem.binary_mode do |f| - unless f.read == data - errors[gem_name][entry["path"]] = "Modified from original" - end + File.open source, Gem.binary_mode do |f| + unless f.read == data + errors[gem_name][entry["path"]] = "Modified from original" end end end diff --git a/lib/rubygems/optparse/.document b/lib/rubygems/vendor/.document index 0c43bbd6b3..0c43bbd6b3 100644 --- a/lib/rubygems/optparse/.document +++ b/lib/rubygems/vendor/.document diff --git a/lib/rubygems/vendor/net-http/lib/net/http.rb b/lib/rubygems/vendor/net-http/lib/net/http.rb new file mode 100644 index 0000000000..4800cd25f1 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http.rb @@ -0,0 +1,2608 @@ +# frozen_string_literal: true +# +# = net/http.rb +# +# Copyright (c) 1999-2007 Yukihiro Matsumoto +# Copyright (c) 1999-2007 Minero Aoki +# Copyright (c) 2001 GOTOU Yuuzou +# +# Written and maintained by Minero Aoki <aamine@loveruby.net>. +# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>. +# +# This file is derived from "http-access.rb". +# +# Documented by Minero Aoki; converted to RDoc by William Webber. +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms of ruby itself --- +# Ruby Distribution License or GNU General Public License. +# +# See Gem::Net::HTTP for an overview and examples. +# + +require_relative '../../../net-protocol/lib/net/protocol' +require_relative '../../../uri/lib/uri' +require_relative '../../../resolv/lib/resolv' +autoload :OpenSSL, 'openssl' + +module Gem::Net #:nodoc: + + # :stopdoc: + class HTTPBadResponse < StandardError; end + class HTTPHeaderSyntaxError < StandardError; end + # :startdoc: + + # \Class \Gem::Net::HTTP provides a rich library that implements the client + # in a client-server model that uses the \HTTP request-response protocol. + # For information about \HTTP, see: + # + # - {Hypertext Transfer Protocol}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol]. + # - {Technical overview}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Technical_overview]. + # + # == About the Examples + # + # :include: doc/net-http/examples.rdoc + # + # == Strategies + # + # - If you will make only a few GET requests, + # consider using {OpenURI}[https://docs.ruby-lang.org/en/master/OpenURI.html]. + # - If you will make only a few requests of all kinds, + # consider using the various singleton convenience methods in this class. + # Each of the following methods automatically starts and finishes + # a {session}[rdoc-ref:Gem::Net::HTTP@Sessions] that sends a single request: + # + # # Return string response body. + # Gem::Net::HTTP.get(hostname, path) + # Gem::Net::HTTP.get(uri) + # + # # Write string response body to $stdout. + # Gem::Net::HTTP.get_print(hostname, path) + # Gem::Net::HTTP.get_print(uri) + # + # # Return response as Gem::Net::HTTPResponse object. + # Gem::Net::HTTP.get_response(hostname, path) + # Gem::Net::HTTP.get_response(uri) + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # Gem::Net::HTTP.post(uri, data) + # params = {title: 'foo', body: 'bar', userId: 1} + # Gem::Net::HTTP.post_form(uri, params) + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # Gem::Net::HTTP.put(uri, data) + # + # - If performance is important, consider using sessions, which lower request overhead. + # This {session}[rdoc-ref:Gem::Net::HTTP@Sessions] has multiple requests for + # {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods] + # and {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: + # + # Gem::Net::HTTP.start(hostname) do |http| + # # Session started automatically before block execution. + # http.get(path) + # http.head(path) + # body = 'Some text' + # http.post(path, body) # Can also have a block. + # http.put(path, body) + # http.delete(path) + # http.options(path) + # http.trace(path) + # http.patch(path, body) # Can also have a block. + # http.copy(path) + # http.lock(path, body) + # http.mkcol(path, body) + # http.move(path) + # http.propfind(path, body) + # http.proppatch(path, body) + # http.unlock(path, body) + # # Session finished automatically at block exit. + # end + # + # The methods cited above are convenience methods that, via their few arguments, + # allow minimal control over the requests. + # For greater control, consider using {request objects}[rdoc-ref:Gem::Net::HTTPRequest]. + # + # == URIs + # + # On the internet, a Gem::URI + # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier]) + # is a string that identifies a particular resource. + # It consists of some or all of: scheme, hostname, path, query, and fragment; + # see {Gem::URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax]. + # + # A Ruby {Gem::URI::Generic}[https://docs.ruby-lang.org/en/master/Gem::URI/Generic.html] object + # represents an internet Gem::URI. + # It provides, among others, methods + # +scheme+, +hostname+, +path+, +query+, and +fragment+. + # + # === Schemes + # + # An internet \Gem::URI has + # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes]. + # + # The two schemes supported in \Gem::Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>: + # + # uri.scheme # => "https" + # Gem::URI('http://example.com').scheme # => "http" + # + # === Hostnames + # + # A hostname identifies a server (host) to which requests may be sent: + # + # hostname = uri.hostname # => "jsonplaceholder.typicode.com" + # Gem::Net::HTTP.start(hostname) do |http| + # # Some HTTP stuff. + # end + # + # === Paths + # + # A host-specific path identifies a resource on the host: + # + # _uri = uri.dup + # _uri.path = '/todos/1' + # hostname = _uri.hostname + # path = _uri.path + # Gem::Net::HTTP.get(hostname, path) + # + # === Queries + # + # A host-specific query adds name/value pairs to the Gem::URI: + # + # _uri = uri.dup + # params = {userId: 1, completed: false} + # _uri.query = Gem::URI.encode_www_form(params) + # _uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com?userId=1&completed=false> + # Gem::Net::HTTP.get(_uri) + # + # === Fragments + # + # A {Gem::URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect + # in \Gem::Net::HTTP; + # the same data is returned, regardless of whether a fragment is included. + # + # == Request Headers + # + # Request headers may be used to pass additional information to the host, + # similar to arguments passed in a method call; + # each header is a name/value pair. + # + # Each of the \Gem::Net::HTTP methods that sends a request to the host + # has optional argument +headers+, + # where the headers are expressed as a hash of field-name/value pairs: + # + # headers = {Accept: 'application/json', Connection: 'Keep-Alive'} + # Gem::Net::HTTP.get(uri, headers) + # + # See lists of both standard request fields and common request fields at + # {Request Fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields]. + # A host may also accept other custom fields. + # + # == \HTTP Sessions + # + # A _session_ is a connection between a server (host) and a client that: + # + # - Is begun by instance method Gem::Net::HTTP#start. + # - May contain any number of requests. + # - Is ended by instance method Gem::Net::HTTP#finish. + # + # See example sessions at {Strategies}[rdoc-ref:Gem::Net::HTTP@Strategies]. + # + # === Session Using \Gem::Net::HTTP.start + # + # If you have many requests to make to a single host (and port), + # consider using singleton method Gem::Net::HTTP.start with a block; + # the method handles the session automatically by: + # + # - Calling #start before block execution. + # - Executing the block. + # - Calling #finish after block execution. + # + # In the block, you can use these instance methods, + # each of which that sends a single request: + # + # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]: + # + # - #get, #request_get: GET. + # - #head, #request_head: HEAD. + # - #post, #request_post: POST. + # - #delete: DELETE. + # - #options: OPTIONS. + # - #trace: TRACE. + # - #patch: PATCH. + # + # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]: + # + # - #copy: COPY. + # - #lock: LOCK. + # - #mkcol: MKCOL. + # - #move: MOVE. + # - #propfind: PROPFIND. + # - #proppatch: PROPPATCH. + # - #unlock: UNLOCK. + # + # === Session Using \Gem::Net::HTTP.start and \Gem::Net::HTTP.finish + # + # You can manage a session manually using methods #start and #finish: + # + # http = Gem::Net::HTTP.new(hostname) + # http.start + # http.get('/todos/1') + # http.get('/todos/2') + # http.delete('/posts/1') + # http.finish # Needed to free resources. + # + # === Single-Request Session + # + # Certain convenience methods automatically handle a session by: + # + # - Creating an \HTTP object + # - Starting a session. + # - Sending a single request. + # - Finishing the session. + # - Destroying the object. + # + # Such methods that send GET requests: + # + # - ::get: Returns the string response body. + # - ::get_print: Writes the string response body to $stdout. + # - ::get_response: Returns a Gem::Net::HTTPResponse object. + # + # Such methods that send POST requests: + # + # - ::post: Posts data to the host. + # - ::post_form: Posts form data to the host. + # + # == \HTTP Requests and Responses + # + # Many of the methods above are convenience methods, + # each of which sends a request and returns a string + # without directly using \Gem::Net::HTTPRequest and \Gem::Net::HTTPResponse objects. + # + # You can, however, directly create a request object, send the request, + # and retrieve the response object; see: + # + # - Gem::Net::HTTPRequest. + # - Gem::Net::HTTPResponse. + # + # == Following Redirection + # + # Each returned response is an instance of a subclass of Gem::Net::HTTPResponse. + # See the {response class hierarchy}[rdoc-ref:Gem::Net::HTTPResponse@Response+Subclasses]. + # + # In particular, class Gem::Net::HTTPRedirection is the parent + # of all redirection classes. + # This allows you to craft a case statement to handle redirections properly: + # + # def fetch(uri, limit = 10) + # # You should choose a better exception. + # raise ArgumentError, 'Too many HTTP redirects' if limit == 0 + # + # res = Gem::Net::HTTP.get_response(Gem::URI(uri)) + # case res + # when Gem::Net::HTTPSuccess # Any success class. + # res + # when Gem::Net::HTTPRedirection # Any redirection class. + # location = res['Location'] + # warn "Redirected to #{location}" + # fetch(location, limit - 1) + # else # Any other class. + # res.value + # end + # end + # + # fetch(uri) + # + # == Basic Authentication + # + # Basic authentication is performed according to + # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.basic_auth('user', 'pass') + # res = Gem::Net::HTTP.start(hostname) do |http| + # http.request(req) + # end + # + # == Streaming Response Bodies + # + # By default \Gem::Net::HTTP reads an entire response into memory. If you are + # handling large files or wish to implement a progress bar you can instead + # stream the body directly to an IO. + # + # Gem::Net::HTTP.start(hostname) do |http| + # req = Gem::Net::HTTP::Get.new(uri) + # http.request(req) do |res| + # open('t.tmp', 'w') do |f| + # res.read_body do |chunk| + # f.write chunk + # end + # end + # end + # end + # + # == HTTPS + # + # HTTPS is enabled for an \HTTP connection by Gem::Net::HTTP#use_ssl=: + # + # Gem::Net::HTTP.start(hostname, :use_ssl => true) do |http| + # req = Gem::Net::HTTP::Get.new(uri) + # res = http.request(req) + # end + # + # Or if you simply want to make a GET request, you may pass in a Gem::URI + # object that has an \HTTPS URL. \Gem::Net::HTTP automatically turns on TLS + # verification if the Gem::URI object has a 'https' Gem::URI scheme: + # + # uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/> + # Gem::Net::HTTP.get(uri) + # + # == Proxy Server + # + # An \HTTP object can have + # a {proxy server}[https://en.wikipedia.org/wiki/Proxy_server]. + # + # You can create an \HTTP object with a proxy server + # using method Gem::Net::HTTP.new or method Gem::Net::HTTP.start. + # + # The proxy may be defined either by argument +p_addr+ + # or by environment variable <tt>'http_proxy'</tt>. + # + # === Proxy Using Argument +p_addr+ as a \String + # + # When argument +p_addr+ is a string hostname, + # the returned +http+ has the given host as its proxy: + # + # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example') + # http.proxy? # => true + # http.proxy_from_env? # => false + # http.proxy_address # => "proxy.example" + # # These use default values. + # http.proxy_port # => 80 + # http.proxy_user # => nil + # http.proxy_pass # => nil + # + # The port, username, and password for the proxy may also be given: + # + # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example', 8000, 'pname', 'ppass') + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.proxy? # => true + # http.proxy_from_env? # => false + # http.proxy_address # => "proxy.example" + # http.proxy_port # => 8000 + # http.proxy_user # => "pname" + # http.proxy_pass # => "ppass" + # + # === Proxy Using '<tt>ENV['http_proxy']</tt>' + # + # When environment variable <tt>'http_proxy'</tt> + # is set to a \Gem::URI string, + # the returned +http+ will have the server at that Gem::URI as its proxy; + # note that the \Gem::URI string must have a protocol + # such as <tt>'http'</tt> or <tt>'https'</tt>: + # + # ENV['http_proxy'] = 'http://example.com' + # http = Gem::Net::HTTP.new(hostname) + # http.proxy? # => true + # http.proxy_from_env? # => true + # http.proxy_address # => "example.com" + # # These use default values. + # http.proxy_port # => 80 + # http.proxy_user # => nil + # http.proxy_pass # => nil + # + # The \Gem::URI string may include proxy username, password, and port number: + # + # ENV['http_proxy'] = 'http://pname:ppass@example.com:8000' + # http = Gem::Net::HTTP.new(hostname) + # http.proxy? # => true + # http.proxy_from_env? # => true + # http.proxy_address # => "example.com" + # http.proxy_port # => 8000 + # http.proxy_user # => "pname" + # http.proxy_pass # => "ppass" + # + # === Filtering Proxies + # + # With method Gem::Net::HTTP.new (but not Gem::Net::HTTP.start), + # you can use argument +p_no_proxy+ to filter proxies: + # + # - Reject a certain address: + # + # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example') + # http.proxy_address # => nil + # + # - Reject certain domains or subdomains: + # + # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy.example', 8000, 'pname', 'ppass', 'proxy.example') + # http.proxy_address # => nil + # + # - Reject certain addresses and port combinations: + # + # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:1234') + # http.proxy_address # => "proxy.example" + # + # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:8000') + # http.proxy_address # => nil + # + # - Reject a list of the types above delimited using a comma: + # + # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000') + # http.proxy_address # => nil + # + # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000') + # http.proxy_address # => nil + # + # == Compression and Decompression + # + # \Gem::Net::HTTP does not compress the body of a request before sending. + # + # By default, \Gem::Net::HTTP adds header <tt>'Accept-Encoding'</tt> + # to a new {request object}[rdoc-ref:Gem::Net::HTTPRequest]: + # + # Gem::Net::HTTP::Get.new(uri)['Accept-Encoding'] + # # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + # + # This requests the server to zip-encode the response body if there is one; + # the server is not required to do so. + # + # \Gem::Net::HTTP does not automatically decompress a response body + # if the response has header <tt>'Content-Range'</tt>. + # + # Otherwise decompression (or not) depends on the value of header + # {Content-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-encoding-response-header]: + # + # - <tt>'deflate'</tt>, <tt>'gzip'</tt>, or <tt>'x-gzip'</tt>: + # decompresses the body and deletes the header. + # - <tt>'none'</tt> or <tt>'identity'</tt>: + # does not decompress the body, but deletes the header. + # - Any other value: + # leaves the body and header unchanged. + # + # == What's Here + # + # First, what's elsewhere. Class Gem::Net::HTTP: + # + # - Inherits from {class Object}[https://docs.ruby-lang.org/en/master/Object.html#class-Object-label-What-27s+Here]. + # + # This is a categorized summary of methods and attributes. + # + # === \Gem::Net::HTTP Objects + # + # - {::new}[rdoc-ref:Gem::Net::HTTP.new]: + # Creates a new instance. + # - {#inspect}[rdoc-ref:Gem::Net::HTTP#inspect]: + # Returns a string representation of +self+. + # + # === Sessions + # + # - {::start}[rdoc-ref:Gem::Net::HTTP.start]: + # Begins a new session in a new \Gem::Net::HTTP object. + # - {#started?}[rdoc-ref:Gem::Net::HTTP#started?]: + # Returns whether in a session. + # - {#finish}[rdoc-ref:Gem::Net::HTTP#finish]: + # Ends an active session. + # - {#start}[rdoc-ref:Gem::Net::HTTP#start]: + # Begins a new session in an existing \Gem::Net::HTTP object (+self+). + # + # === Connections + # + # - {:continue_timeout}[rdoc-ref:Gem::Net::HTTP#continue_timeout]: + # Returns the continue timeout. + # - {#continue_timeout=}[rdoc-ref:Gem::Net::HTTP#continue_timeout=]: + # Sets the continue timeout seconds. + # - {:keep_alive_timeout}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout]: + # Returns the keep-alive timeout. + # - {:keep_alive_timeout=}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout=]: + # Sets the keep-alive timeout. + # - {:max_retries}[rdoc-ref:Gem::Net::HTTP#max_retries]: + # Returns the maximum retries. + # - {#max_retries=}[rdoc-ref:Gem::Net::HTTP#max_retries=]: + # Sets the maximum retries. + # - {:open_timeout}[rdoc-ref:Gem::Net::HTTP#open_timeout]: + # Returns the open timeout. + # - {:open_timeout=}[rdoc-ref:Gem::Net::HTTP#open_timeout=]: + # Sets the open timeout. + # - {:read_timeout}[rdoc-ref:Gem::Net::HTTP#read_timeout]: + # Returns the open timeout. + # - {:read_timeout=}[rdoc-ref:Gem::Net::HTTP#read_timeout=]: + # Sets the read timeout. + # - {:ssl_timeout}[rdoc-ref:Gem::Net::HTTP#ssl_timeout]: + # Returns the ssl timeout. + # - {:ssl_timeout=}[rdoc-ref:Gem::Net::HTTP#ssl_timeout=]: + # Sets the ssl timeout. + # - {:write_timeout}[rdoc-ref:Gem::Net::HTTP#write_timeout]: + # Returns the write timeout. + # - {write_timeout=}[rdoc-ref:Gem::Net::HTTP#write_timeout=]: + # Sets the write timeout. + # + # === Requests + # + # - {::get}[rdoc-ref:Gem::Net::HTTP.get]: + # Sends a GET request and returns the string response body. + # - {::get_print}[rdoc-ref:Gem::Net::HTTP.get_print]: + # Sends a GET request and write the string response body to $stdout. + # - {::get_response}[rdoc-ref:Gem::Net::HTTP.get_response]: + # Sends a GET request and returns a response object. + # - {::post_form}[rdoc-ref:Gem::Net::HTTP.post_form]: + # Sends a POST request with form data and returns a response object. + # - {::post}[rdoc-ref:Gem::Net::HTTP.post]: + # Sends a POST request with data and returns a response object. + # - {::put}[rdoc-ref:Gem::Net::HTTP.put]: + # Sends a PUT request with data and returns a response object. + # - {#copy}[rdoc-ref:Gem::Net::HTTP#copy]: + # Sends a COPY request and returns a response object. + # - {#delete}[rdoc-ref:Gem::Net::HTTP#delete]: + # Sends a DELETE request and returns a response object. + # - {#get}[rdoc-ref:Gem::Net::HTTP#get]: + # Sends a GET request and returns a response object. + # - {#head}[rdoc-ref:Gem::Net::HTTP#head]: + # Sends a HEAD request and returns a response object. + # - {#lock}[rdoc-ref:Gem::Net::HTTP#lock]: + # Sends a LOCK request and returns a response object. + # - {#mkcol}[rdoc-ref:Gem::Net::HTTP#mkcol]: + # Sends a MKCOL request and returns a response object. + # - {#move}[rdoc-ref:Gem::Net::HTTP#move]: + # Sends a MOVE request and returns a response object. + # - {#options}[rdoc-ref:Gem::Net::HTTP#options]: + # Sends a OPTIONS request and returns a response object. + # - {#patch}[rdoc-ref:Gem::Net::HTTP#patch]: + # Sends a PATCH request and returns a response object. + # - {#post}[rdoc-ref:Gem::Net::HTTP#post]: + # Sends a POST request and returns a response object. + # - {#propfind}[rdoc-ref:Gem::Net::HTTP#propfind]: + # Sends a PROPFIND request and returns a response object. + # - {#proppatch}[rdoc-ref:Gem::Net::HTTP#proppatch]: + # Sends a PROPPATCH request and returns a response object. + # - {#put}[rdoc-ref:Gem::Net::HTTP#put]: + # Sends a PUT request and returns a response object. + # - {#request}[rdoc-ref:Gem::Net::HTTP#request]: + # Sends a request and returns a response object. + # - {#request_get}[rdoc-ref:Gem::Net::HTTP#request_get]: + # Sends a GET request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#request_head}[rdoc-ref:Gem::Net::HTTP#request_head]: + # Sends a HEAD request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#request_post}[rdoc-ref:Gem::Net::HTTP#request_post]: + # Sends a POST request and forms a response object; + # if a block given, calls the block with the object, + # otherwise returns the object. + # - {#send_request}[rdoc-ref:Gem::Net::HTTP#send_request]: + # Sends a request and returns a response object. + # - {#trace}[rdoc-ref:Gem::Net::HTTP#trace]: + # Sends a TRACE request and returns a response object. + # - {#unlock}[rdoc-ref:Gem::Net::HTTP#unlock]: + # Sends an UNLOCK request and returns a response object. + # + # === Responses + # + # - {:close_on_empty_response}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response]: + # Returns whether to close connection on empty response. + # - {:close_on_empty_response=}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response=]: + # Sets whether to close connection on empty response. + # - {:ignore_eof}[rdoc-ref:Gem::Net::HTTP#ignore_eof]: + # Returns whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers. + # - {:ignore_eof=}[rdoc-ref:Gem::Net::HTTP#ignore_eof=]: + # Sets whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers. + # - {:response_body_encoding}[rdoc-ref:Gem::Net::HTTP#response_body_encoding]: + # Returns the encoding to use for the response body. + # - {#response_body_encoding=}[rdoc-ref:Gem::Net::HTTP#response_body_encoding=]: + # Sets the response body encoding. + # + # === Proxies + # + # - {:proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]: + # Returns the proxy address. + # - {:proxy_address=}[rdoc-ref:Gem::Net::HTTP#proxy_address=]: + # Sets the proxy address. + # - {::proxy_class?}[rdoc-ref:Gem::Net::HTTP.proxy_class?]: + # Returns whether +self+ is a proxy class. + # - {#proxy?}[rdoc-ref:Gem::Net::HTTP#proxy?]: + # Returns whether +self+ has a proxy. + # - {#proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]: + # Returns the proxy address. + # - {#proxy_from_env?}[rdoc-ref:Gem::Net::HTTP#proxy_from_env?]: + # Returns whether the proxy is taken from an environment variable. + # - {:proxy_from_env=}[rdoc-ref:Gem::Net::HTTP#proxy_from_env=]: + # Sets whether the proxy is to be taken from an environment variable. + # - {:proxy_pass}[rdoc-ref:Gem::Net::HTTP#proxy_pass]: + # Returns the proxy password. + # - {:proxy_pass=}[rdoc-ref:Gem::Net::HTTP#proxy_pass=]: + # Sets the proxy password. + # - {:proxy_port}[rdoc-ref:Gem::Net::HTTP#proxy_port]: + # Returns the proxy port. + # - {:proxy_port=}[rdoc-ref:Gem::Net::HTTP#proxy_port=]: + # Sets the proxy port. + # - {#proxy_user}[rdoc-ref:Gem::Net::HTTP#proxy_user]: + # Returns the proxy user name. + # - {:proxy_user=}[rdoc-ref:Gem::Net::HTTP#proxy_user=]: + # Sets the proxy user. + # + # === Security + # + # - {:ca_file}[rdoc-ref:Gem::Net::HTTP#ca_file]: + # Returns the path to a CA certification file. + # - {:ca_file=}[rdoc-ref:Gem::Net::HTTP#ca_file=]: + # Sets the path to a CA certification file. + # - {:ca_path}[rdoc-ref:Gem::Net::HTTP#ca_path]: + # Returns the path of to CA directory containing certification files. + # - {:ca_path=}[rdoc-ref:Gem::Net::HTTP#ca_path=]: + # Sets the path of to CA directory containing certification files. + # - {:cert}[rdoc-ref:Gem::Net::HTTP#cert]: + # Returns the OpenSSL::X509::Certificate object to be used for client certification. + # - {:cert=}[rdoc-ref:Gem::Net::HTTP#cert=]: + # Sets the OpenSSL::X509::Certificate object to be used for client certification. + # - {:cert_store}[rdoc-ref:Gem::Net::HTTP#cert_store]: + # Returns the X509::Store to be used for verifying peer certificate. + # - {:cert_store=}[rdoc-ref:Gem::Net::HTTP#cert_store=]: + # Sets the X509::Store to be used for verifying peer certificate. + # - {:ciphers}[rdoc-ref:Gem::Net::HTTP#ciphers]: + # Returns the available SSL ciphers. + # - {:ciphers=}[rdoc-ref:Gem::Net::HTTP#ciphers=]: + # Sets the available SSL ciphers. + # - {:extra_chain_cert}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert]: + # Returns the extra X509 certificates to be added to the certificate chain. + # - {:extra_chain_cert=}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert=]: + # Sets the extra X509 certificates to be added to the certificate chain. + # - {:key}[rdoc-ref:Gem::Net::HTTP#key]: + # Returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # - {:key=}[rdoc-ref:Gem::Net::HTTP#key=]: + # Sets the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + # - {:max_version}[rdoc-ref:Gem::Net::HTTP#max_version]: + # Returns the maximum SSL version. + # - {:max_version=}[rdoc-ref:Gem::Net::HTTP#max_version=]: + # Sets the maximum SSL version. + # - {:min_version}[rdoc-ref:Gem::Net::HTTP#min_version]: + # Returns the minimum SSL version. + # - {:min_version=}[rdoc-ref:Gem::Net::HTTP#min_version=]: + # Sets the minimum SSL version. + # - {#peer_cert}[rdoc-ref:Gem::Net::HTTP#peer_cert]: + # Returns the X509 certificate chain for the session's socket peer. + # - {:ssl_version}[rdoc-ref:Gem::Net::HTTP#ssl_version]: + # Returns the SSL version. + # - {:ssl_version=}[rdoc-ref:Gem::Net::HTTP#ssl_version=]: + # Sets the SSL version. + # - {#use_ssl=}[rdoc-ref:Gem::Net::HTTP#use_ssl=]: + # Sets whether a new session is to use Transport Layer Security. + # - {#use_ssl?}[rdoc-ref:Gem::Net::HTTP#use_ssl?]: + # Returns whether +self+ uses SSL. + # - {:verify_callback}[rdoc-ref:Gem::Net::HTTP#verify_callback]: + # Returns the callback for the server certification verification. + # - {:verify_callback=}[rdoc-ref:Gem::Net::HTTP#verify_callback=]: + # Sets the callback for the server certification verification. + # - {:verify_depth}[rdoc-ref:Gem::Net::HTTP#verify_depth]: + # Returns the maximum depth for the certificate chain verification. + # - {:verify_depth=}[rdoc-ref:Gem::Net::HTTP#verify_depth=]: + # Sets the maximum depth for the certificate chain verification. + # - {:verify_hostname}[rdoc-ref:Gem::Net::HTTP#verify_hostname]: + # Returns the flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_hostname=}[rdoc-ref:Gem::Net::HTTP#verify_hostname=]: + # Sets he flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_mode}[rdoc-ref:Gem::Net::HTTP#verify_mode]: + # Returns the flags for server the certification verification at the beginning of the SSL/TLS session. + # - {:verify_mode=}[rdoc-ref:Gem::Net::HTTP#verify_mode=]: + # Sets the flags for server the certification verification at the beginning of the SSL/TLS session. + # + # === Addresses and Ports + # + # - {:address}[rdoc-ref:Gem::Net::HTTP#address]: + # Returns the string host name or host IP. + # - {::default_port}[rdoc-ref:Gem::Net::HTTP.default_port]: + # Returns integer 80, the default port to use for HTTP requests. + # - {::http_default_port}[rdoc-ref:Gem::Net::HTTP.http_default_port]: + # Returns integer 80, the default port to use for HTTP requests. + # - {::https_default_port}[rdoc-ref:Gem::Net::HTTP.https_default_port]: + # Returns integer 443, the default port to use for HTTPS requests. + # - {#ipaddr}[rdoc-ref:Gem::Net::HTTP#ipaddr]: + # Returns the IP address for the connection. + # - {#ipaddr=}[rdoc-ref:Gem::Net::HTTP#ipaddr=]: + # Sets the IP address for the connection. + # - {:local_host}[rdoc-ref:Gem::Net::HTTP#local_host]: + # Returns the string local host used to establish the connection. + # - {:local_host=}[rdoc-ref:Gem::Net::HTTP#local_host=]: + # Sets the string local host used to establish the connection. + # - {:local_port}[rdoc-ref:Gem::Net::HTTP#local_port]: + # Returns the integer local port used to establish the connection. + # - {:local_port=}[rdoc-ref:Gem::Net::HTTP#local_port=]: + # Sets the integer local port used to establish the connection. + # - {:port}[rdoc-ref:Gem::Net::HTTP#port]: + # Returns the integer port number. + # + # === \HTTP Version + # + # - {::version_1_2?}[rdoc-ref:Gem::Net::HTTP.version_1_2?] + # (aliased as {::version_1_2}[rdoc-ref:Gem::Net::HTTP.version_1_2]): + # Returns true; retained for compatibility. + # + # === Debugging + # + # - {#set_debug_output}[rdoc-ref:Gem::Net::HTTP#set_debug_output]: + # Sets the output stream for debugging. + # + class HTTP < Protocol + + # :stopdoc: + VERSION = "0.9.1" + HTTPVersion = '1.1' + begin + require 'zlib' + HAVE_ZLIB=true + rescue LoadError + HAVE_ZLIB=false + end + # :startdoc: + + # Returns +true+; retained for compatibility. + def HTTP.version_1_2 + true + end + + # Returns +true+; retained for compatibility. + def HTTP.version_1_2? + true + end + + # Returns +false+; retained for compatibility. + def HTTP.version_1_1? #:nodoc: + false + end + + class << HTTP + alias is_version_1_1? version_1_1? #:nodoc: + alias is_version_1_2? version_1_2? #:nodoc: + end + + # :call-seq: + # Gem::Net::HTTP.get_print(hostname, path, port = 80) -> nil + # Gem::Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil + # + # Like Gem::Net::HTTP.get, but writes the returned body to $stdout; + # returns +nil+. + def HTTP.get_print(uri_or_host, path_or_headers = nil, port = nil) + get_response(uri_or_host, path_or_headers, port) {|res| + res.read_body do |chunk| + $stdout.print chunk + end + } + nil + end + + # :call-seq: + # Gem::Net::HTTP.get(hostname, path, port = 80) -> body + # Gem::Net::HTTP:get(uri, headers = {}, port = uri.port) -> body + # + # Sends a GET request and returns the \HTTP response body as a string. + # + # With string arguments +hostname+ and +path+: + # + # hostname = 'jsonplaceholder.typicode.com' + # path = '/todos/1' + # puts Gem::Net::HTTP.get(hostname, path) + # + # Output: + # + # { + # "userId": 1, + # "id": 1, + # "title": "delectus aut autem", + # "completed": false + # } + # + # With Gem::URI object +uri+ and optional hash argument +headers+: + # + # uri = Gem::URI('https://jsonplaceholder.typicode.com/todos/1') + # headers = {'Content-type' => 'application/json; charset=UTF-8'} + # Gem::Net::HTTP.get(uri, headers) + # + # Related: + # + # - Gem::Net::HTTP::Get: request class for \HTTP method +GET+. + # - Gem::Net::HTTP#get: convenience method for \HTTP method +GET+. + # + def HTTP.get(uri_or_host, path_or_headers = nil, port = nil) + get_response(uri_or_host, path_or_headers, port).body + end + + # :call-seq: + # Gem::Net::HTTP.get_response(hostname, path, port = 80) -> http_response + # Gem::Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response + # + # Like Gem::Net::HTTP.get, but returns a Gem::Net::HTTPResponse object + # instead of the body string. + def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block) + if path_or_headers && !path_or_headers.is_a?(Hash) + host = uri_or_host + path = path_or_headers + new(host, port || HTTP.default_port).start {|http| + return http.request_get(path, &block) + } + else + uri = uri_or_host + headers = path_or_headers + start(uri.hostname, uri.port, + :use_ssl => uri.scheme == 'https') {|http| + return http.request_get(uri, headers, &block) + } + end + end + + # Posts data to a host; returns a Gem::Net::HTTPResponse object. + # + # Argument +url+ must be a URL; + # argument +data+ must be a string: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # headers = {'content-type': 'application/json'} + # res = Gem::Net::HTTP.post(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": 1, + # "id": 101 + # } + # + # Related: + # + # - Gem::Net::HTTP::Post: request class for \HTTP method +POST+. + # - Gem::Net::HTTP#post: convenience method for \HTTP method +POST+. + # + def HTTP.post(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.post(url, data, header) + } + end + + # Posts data to a host; returns a Gem::Net::HTTPResponse object. + # + # Argument +url+ must be a Gem::URI; + # argument +data+ must be a hash: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = {title: 'foo', body: 'bar', userId: 1} + # res = Gem::Net::HTTP.post_form(_uri, data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": "1", + # "id": 101 + # } + # + def HTTP.post_form(url, params) + req = Post.new(url) + req.form_data = params + req.basic_auth url.user, url.password if url.user + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.request(req) + } + end + + # Sends a PUT request to the server; returns a Gem::Net::HTTPResponse object. + # + # Argument +url+ must be a URL; + # argument +data+ must be a string: + # + # _uri = uri.dup + # _uri.path = '/posts' + # data = '{"title": "foo", "body": "bar", "userId": 1}' + # headers = {'content-type': 'application/json'} + # res = Gem::Net::HTTP.put(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # puts res.body + # + # Output: + # + # { + # "title": "foo", + # "body": "bar", + # "userId": 1, + # "id": 101 + # } + # + # Related: + # + # - Gem::Net::HTTP::Put: request class for \HTTP method +PUT+. + # - Gem::Net::HTTP#put: convenience method for \HTTP method +PUT+. + # + def HTTP.put(url, data, header = nil) + start(url.hostname, url.port, + :use_ssl => url.scheme == 'https' ) {|http| + http.put(url, data, header) + } + end + + # + # \HTTP session management + # + + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Gem::Net::HTTP.default_port # => 80 + # + def HTTP.default_port + http_default_port() + end + + # Returns integer +80+, the default port to use for \HTTP requests: + # + # Gem::Net::HTTP.http_default_port # => 80 + # + def HTTP.http_default_port + 80 + end + + # Returns integer +443+, the default port to use for HTTPS requests: + # + # Gem::Net::HTTP.https_default_port # => 443 + # + def HTTP.https_default_port + 443 + end + + def HTTP.socket_type #:nodoc: obsolete + BufferedIO + end + + # :call-seq: + # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) -> http + # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) {|http| ... } -> object + # + # Creates a new \Gem::Net::HTTP object, +http+, via \Gem::Net::HTTP.new: + # + # - For arguments +address+ and +port+, see Gem::Net::HTTP.new. + # - For proxy-defining arguments +p_addr+ through +p_pass+, + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + # - For argument +opts+, see below. + # + # With no block given: + # + # - Calls <tt>http.start</tt> with no block (see #start), + # which opens a TCP connection and \HTTP session. + # - Returns +http+. + # - The caller should call #finish to close the session: + # + # http = Gem::Net::HTTP.start(hostname) + # http.started? # => true + # http.finish + # http.started? # => false + # + # With a block given: + # + # - Calls <tt>http.start</tt> with the block (see #start), which: + # + # - Opens a TCP connection and \HTTP session. + # - Calls the block, + # which may make any number of requests to the host. + # - Closes the \HTTP session and TCP connection on block exit. + # - Returns the block's value +object+. + # + # - Returns +object+. + # + # Example: + # + # hostname = 'jsonplaceholder.typicode.com' + # Gem::Net::HTTP.start(hostname) do |http| + # puts http.get('/todos/1').body + # puts http.get('/todos/2').body + # end + # + # Output: + # + # { + # "userId": 1, + # "id": 1, + # "title": "delectus aut autem", + # "completed": false + # } + # { + # "userId": 1, + # "id": 2, + # "title": "quis ut nam facilis et officia qui", + # "completed": false + # } + # + # If the last argument given is a hash, it is the +opts+ hash, + # where each key is a method or accessor to be called, + # and its value is the value to be set. + # + # The keys may include: + # + # - #ca_file + # - #ca_path + # - #cert + # - #cert_store + # - #ciphers + # - #close_on_empty_response + # - +ipaddr+ (calls #ipaddr=) + # - #keep_alive_timeout + # - #key + # - #open_timeout + # - #read_timeout + # - #ssl_timeout + # - #ssl_version + # - +use_ssl+ (calls #use_ssl=) + # - #verify_callback + # - #verify_depth + # - #verify_mode + # - #write_timeout + # + # Note: If +port+ is +nil+ and <tt>opts[:use_ssl]</tt> is a truthy value, + # the value passed to +new+ is Gem::Net::HTTP.https_default_port, not +port+. + # + def HTTP.start(address, *arg, &block) # :yield: +http+ + arg.pop if opt = Hash.try_convert(arg[-1]) + port, p_addr, p_port, p_user, p_pass = *arg + p_addr = :ENV if arg.size < 2 + port = https_default_port if !port && opt && opt[:use_ssl] + http = new(address, port, p_addr, p_port, p_user, p_pass) + http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr] + + if opt + if opt[:use_ssl] + opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) + end + http.methods.grep(/\A(\w+)=\z/) do |meth| + key = $1.to_sym + opt.key?(key) or next + http.__send__(meth, opt[key]) + end + end + + http.start(&block) + end + + class << HTTP + alias newobj new # :nodoc: + end + + # Returns a new \Gem::Net::HTTP object +http+ + # (but does not open a TCP connection or \HTTP session). + # + # With only string argument +address+ given + # (and <tt>ENV['http_proxy']</tt> undefined or +nil+), + # the returned +http+: + # + # - Has the given address. + # - Has the default port number, Gem::Net::HTTP.default_port (80). + # - Has no proxy. + # + # Example: + # + # http = Gem::Net::HTTP.new(hostname) + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.address # => "jsonplaceholder.typicode.com" + # http.port # => 80 + # http.proxy? # => false + # + # With integer argument +port+ also given, + # the returned +http+ has the given port: + # + # http = Gem::Net::HTTP.new(hostname, 8000) + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:8000 open=false> + # http.port # => 8000 + # + # For proxy-defining arguments +p_addr+ through +p_no_proxy+, + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + # + def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil) + http = super address, port + + if proxy_class? then # from Gem::Net::HTTP::Proxy() + http.proxy_from_env = @proxy_from_env + http.proxy_address = @proxy_address + http.proxy_port = @proxy_port + http.proxy_user = @proxy_user + http.proxy_pass = @proxy_pass + http.proxy_use_ssl = @proxy_use_ssl + elsif p_addr == :ENV then + http.proxy_from_env = true + else + if p_addr && p_no_proxy && !Gem::URI::Generic.use_proxy?(address, address, port, p_no_proxy) + p_addr = nil + p_port = nil + end + http.proxy_address = p_addr + http.proxy_port = p_port || default_port + http.proxy_user = p_user + http.proxy_pass = p_pass + http.proxy_use_ssl = p_use_ssl + end + + http + end + + class << HTTP + # Allows to set the default configuration that will be used + # when creating a new connection. + # + # Example: + # + # Gem::Net::HTTP.default_configuration = { + # read_timeout: 1, + # write_timeout: 1 + # } + # http = Gem::Net::HTTP.new(hostname) + # http.open_timeout # => 60 + # http.read_timeout # => 1 + # http.write_timeout # => 1 + # + attr_accessor :default_configuration + end + + # Creates a new \Gem::Net::HTTP object for the specified server address, + # without opening the TCP connection or initializing the \HTTP session. + # The +address+ should be a DNS hostname or IP address. + def initialize(address, port = nil) # :nodoc: + defaults = { + keep_alive_timeout: 2, + close_on_empty_response: false, + open_timeout: 60, + read_timeout: 60, + write_timeout: 60, + continue_timeout: nil, + max_retries: 1, + debug_output: nil, + response_body_encoding: false, + ignore_eof: true + } + options = defaults.merge(self.class.default_configuration || {}) + + @address = address + @port = (port || HTTP.default_port) + @ipaddr = nil + @local_host = nil + @local_port = nil + @curr_http_version = HTTPVersion + @keep_alive_timeout = options[:keep_alive_timeout] + @last_communicated = nil + @close_on_empty_response = options[:close_on_empty_response] + @socket = nil + @started = false + @open_timeout = options[:open_timeout] + @read_timeout = options[:read_timeout] + @write_timeout = options[:write_timeout] + @continue_timeout = options[:continue_timeout] + @max_retries = options[:max_retries] + @debug_output = options[:debug_output] + @response_body_encoding = options[:response_body_encoding] + @ignore_eof = options[:ignore_eof] + @tcpsocket_supports_open_timeout = nil + + @proxy_from_env = false + @proxy_uri = nil + @proxy_address = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + @proxy_use_ssl = nil + + @use_ssl = false + @ssl_context = nil + @ssl_session = nil + @sspi_enabled = false + SSL_IVNAMES.each do |ivname| + instance_variable_set ivname, nil + end + end + + # Returns a string representation of +self+: + # + # Gem::Net::HTTP.new(hostname).inspect + # # => "#<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>" + # + def inspect + "#<#{self.class} #{@address}:#{@port} open=#{started?}>" + end + + # *WARNING* This method opens a serious security hole. + # Never use this method in production code. + # + # Sets the output stream for debugging: + # + # http = Gem::Net::HTTP.new(hostname) + # File.open('t.tmp', 'w') do |file| + # http.set_debug_output(file) + # http.start + # http.get('/nosuch/1') + # http.finish + # end + # puts File.read('t.tmp') + # + # Output: + # + # opening connection to jsonplaceholder.typicode.com:80... + # opened + # <- "GET /nosuch/1 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: jsonplaceholder.typicode.com\r\n\r\n" + # -> "HTTP/1.1 404 Not Found\r\n" + # -> "Date: Mon, 12 Dec 2022 21:14:11 GMT\r\n" + # -> "Content-Type: application/json; charset=utf-8\r\n" + # -> "Content-Length: 2\r\n" + # -> "Connection: keep-alive\r\n" + # -> "X-Powered-By: Express\r\n" + # -> "X-Ratelimit-Limit: 1000\r\n" + # -> "X-Ratelimit-Remaining: 999\r\n" + # -> "X-Ratelimit-Reset: 1670879660\r\n" + # -> "Vary: Origin, Accept-Encoding\r\n" + # -> "Access-Control-Allow-Credentials: true\r\n" + # -> "Cache-Control: max-age=43200\r\n" + # -> "Pragma: no-cache\r\n" + # -> "Expires: -1\r\n" + # -> "X-Content-Type-Options: nosniff\r\n" + # -> "Etag: W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"\r\n" + # -> "Via: 1.1 vegur\r\n" + # -> "CF-Cache-Status: MISS\r\n" + # -> "Server-Timing: cf-q-config;dur=1.3000000762986e-05\r\n" + # -> "Report-To: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yOr40jo%2BwS1KHzhTlVpl54beJ5Wx2FcG4gGV0XVrh3X9OlR5q4drUn2dkt5DGO4GDcE%2BVXT7CNgJvGs%2BZleIyMu8CLieFiDIvOviOY3EhHg94m0ZNZgrEdpKD0S85S507l1vsEwEHkoTm%2Ff19SiO\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\n" + # -> "NEL: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\n" + # -> "Server: cloudflare\r\n" + # -> "CF-RAY: 778977dc484ce591-DFW\r\n" + # -> "alt-svc: h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400\r\n" + # -> "\r\n" + # reading 2 bytes... + # -> "{}" + # read 2 bytes + # Conn keep-alive + # + def set_debug_output(output) + warn 'Gem::Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started? + @debug_output = output + end + + # Returns the string host name or host IP given as argument +address+ in ::new. + attr_reader :address + + # Returns the integer port number given as argument +port+ in ::new. + attr_reader :port + + # Sets or returns the string local host used to establish the connection; + # initially +nil+. + attr_accessor :local_host + + # Sets or returns the integer local port used to establish the connection; + # initially +nil+. + attr_accessor :local_port + + # Returns the encoding to use for the response body; + # see #response_body_encoding=. + attr_reader :response_body_encoding + + # Sets the encoding to be used for the response body; + # returns the encoding. + # + # The given +value+ may be: + # + # - An Encoding object. + # - The name of an encoding. + # - An alias for an encoding name. + # + # See {Encoding}[https://docs.ruby-lang.org/en/master/Encoding.html]. + # + # Examples: + # + # http = Gem::Net::HTTP.new(hostname) + # http.response_body_encoding = Encoding::US_ASCII # => #<Encoding:US-ASCII> + # http.response_body_encoding = 'US-ASCII' # => "US-ASCII" + # http.response_body_encoding = 'ASCII' # => "ASCII" + # + def response_body_encoding=(value) + value = Encoding.find(value) if value.is_a?(String) + @response_body_encoding = value + end + + # Sets whether to determine the proxy from environment variable + # '<tt>ENV['http_proxy']</tt>'; + # see {Proxy Using ENV['http_proxy']}[rdoc-ref:Gem::Net::HTTP@Proxy+Using+-27ENV-5B-27http_proxy-27-5D-27]. + attr_writer :proxy_from_env + + # Sets the proxy address; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + attr_writer :proxy_address + + # Sets the proxy port; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + attr_writer :proxy_port + + # Sets the proxy user; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + attr_writer :proxy_user + + # Sets the proxy password; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + attr_writer :proxy_pass + + # Sets whether the proxy uses SSL; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + attr_writer :proxy_use_ssl + + # Returns the IP address for the connection. + # + # If the session has not been started, + # returns the value set by #ipaddr=, + # or +nil+ if it has not been set: + # + # http = Gem::Net::HTTP.new(hostname) + # http.ipaddr # => nil + # http.ipaddr = '172.67.155.76' + # http.ipaddr # => "172.67.155.76" + # + # If the session has been started, + # returns the IP address from the socket: + # + # http = Gem::Net::HTTP.new(hostname) + # http.start + # http.ipaddr # => "172.67.155.76" + # http.finish + # + def ipaddr + started? ? @socket.io.peeraddr[3] : @ipaddr + end + + # Sets the IP address for the connection: + # + # http = Gem::Net::HTTP.new(hostname) + # http.ipaddr # => nil + # http.ipaddr = '172.67.155.76' + # http.ipaddr # => "172.67.155.76" + # + # The IP address may not be set if the session has been started. + def ipaddr=(addr) + raise IOError, "ipaddr value changed, but session already started" if started? + @ipaddr = addr + end + + # Sets or returns the numeric (\Integer or \Float) number of seconds + # to wait for a connection to open; + # initially 60. + # If the connection is not made in the given interval, + # an exception is raised. + attr_accessor :open_timeout + + # Returns the numeric (\Integer or \Float) number of seconds + # to wait for one block to be read (via one read(2) call); + # see #read_timeout=. + attr_reader :read_timeout + + # Returns the numeric (\Integer or \Float) number of seconds + # to wait for one block to be written (via one write(2) call); + # see #write_timeout=. + attr_reader :write_timeout + + # Sets the maximum number of times to retry an idempotent request in case of + # \Gem::Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, + # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError, + # Gem::Timeout::Error. + # The initial value is 1. + # + # Argument +retries+ must be a non-negative numeric value: + # + # http = Gem::Net::HTTP.new(hostname) + # http.max_retries = 2 # => 2 + # http.max_retries # => 2 + # + def max_retries=(retries) + retries = retries.to_int + if retries < 0 + raise ArgumentError, 'max_retries should be non-negative integer number' + end + @max_retries = retries + end + + # Returns the maximum number of times to retry an idempotent request; + # see #max_retries=. + attr_reader :max_retries + + # Sets the read timeout, in seconds, for +self+ to integer +sec+; + # the initial value is 60. + # + # Argument +sec+ must be a non-negative numeric value: + # + # http = Gem::Net::HTTP.new(hostname) + # http.read_timeout # => 60 + # http.get('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # http.read_timeout = 0 + # http.get('/todos/1') # Raises Gem::Net::ReadTimeout. + # + def read_timeout=(sec) + @socket.read_timeout = sec if @socket + @read_timeout = sec + end + + # Sets the write timeout, in seconds, for +self+ to integer +sec+; + # the initial value is 60. + # + # Argument +sec+ must be a non-negative numeric value: + # + # _uri = uri.dup + # _uri.path = '/posts' + # body = 'bar' * 200000 + # data = <<EOF + # {"title": "foo", "body": "#{body}", "userId": "1"} + # EOF + # headers = {'content-type': 'application/json'} + # http = Gem::Net::HTTP.new(hostname) + # http.write_timeout # => 60 + # http.post(_uri.path, data, headers) + # # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # http.write_timeout = 0 + # http.post(_uri.path, data, headers) # Raises Gem::Net::WriteTimeout. + # + def write_timeout=(sec) + @socket.write_timeout = sec if @socket + @write_timeout = sec + end + + # Returns the continue timeout value; + # see continue_timeout=. + attr_reader :continue_timeout + + # Sets the continue timeout value, + # which is the number of seconds to wait for an expected 100 Continue response. + # If the \HTTP object does not receive a response in this many seconds + # it sends the request body. + def continue_timeout=(sec) + @socket.continue_timeout = sec if @socket + @continue_timeout = sec + end + + # Sets or returns the numeric (\Integer or \Float) number of seconds + # to keep the connection open after a request is sent; + # initially 2. + # If a new request is made during the given interval, + # the still-open connection is used; + # otherwise the connection will have been closed + # and a new connection is opened. + attr_accessor :keep_alive_timeout + + # Sets or returns whether to ignore end-of-file when reading a response body + # with <tt>Content-Length</tt> headers; + # initially +true+. + attr_accessor :ignore_eof + + # Returns +true+ if the \HTTP session has been started: + # + # http = Gem::Net::HTTP.new(hostname) + # http.started? # => false + # http.start + # http.started? # => true + # http.finish # => nil + # http.started? # => false + # + # Gem::Net::HTTP.start(hostname) do |http| + # http.started? + # end # => true + # http.started? # => false + # + def started? + @started + end + + alias active? started? #:nodoc: obsolete + + # Sets or returns whether to close the connection when the response is empty; + # initially +false+. + attr_accessor :close_on_empty_response + + # Returns +true+ if +self+ uses SSL, +false+ otherwise. + # See Gem::Net::HTTP#use_ssl=. + def use_ssl? + @use_ssl + end + + # Sets whether a new session is to use + # {Transport Layer Security}[https://en.wikipedia.org/wiki/Transport_Layer_Security]: + # + # Raises IOError if attempting to change during a session. + # + # Raises OpenSSL::SSL::SSLError if the port is not an HTTPS port. + def use_ssl=(flag) + flag = flag ? true : false + if started? and @use_ssl != flag + raise IOError, "use_ssl value changed, but session already started" + end + @use_ssl = flag + end + + SSL_ATTRIBUTES = [ + :ca_file, + :ca_path, + :cert, + :cert_store, + :ciphers, + :extra_chain_cert, + :key, + :ssl_timeout, + :ssl_version, + :min_version, + :max_version, + :verify_callback, + :verify_depth, + :verify_mode, + :verify_hostname, + ].freeze # :nodoc: + + SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc: + + # Sets or returns the path to a CA certification file in PEM format. + attr_accessor :ca_file + + # Sets or returns the path of to CA directory + # containing certification files in PEM format. + attr_accessor :ca_path + + # Sets or returns the OpenSSL::X509::Certificate object + # to be used for client certification. + attr_accessor :cert + + # Sets or returns the X509::Store to be used for verifying peer certificate. + attr_accessor :cert_store + + # Sets or returns the available SSL ciphers. + # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=]. + attr_accessor :ciphers + + # Sets or returns the extra X509 certificates to be added to the certificate chain. + # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate]. + attr_accessor :extra_chain_cert + + # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object. + attr_accessor :key + + # Sets or returns the SSL timeout seconds. + attr_accessor :ssl_timeout + + # Sets or returns the SSL version. + # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=]. + attr_accessor :ssl_version + + # Sets or returns the minimum SSL version. + # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=]. + attr_accessor :min_version + + # Sets or returns the maximum SSL version. + # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=]. + attr_accessor :max_version + + # Sets or returns the callback for the server certification verification. + attr_accessor :verify_callback + + # Sets or returns the maximum depth for the certificate chain verification. + attr_accessor :verify_depth + + # Sets or returns the flags for server the certification verification + # at the beginning of the SSL/TLS session. + # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable. + attr_accessor :verify_mode + + # Sets or returns whether to verify that the server certificate is valid + # for the hostname. + # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=]. + attr_accessor :verify_hostname + + # Returns the X509 certificate chain (an array of strings) + # for the session's socket peer, + # or +nil+ if none. + def peer_cert + if not use_ssl? or not @socket + return nil + end + @socket.io.peer_cert + end + + # Starts an \HTTP session. + # + # Without a block, returns +self+: + # + # http = Gem::Net::HTTP.new(hostname) + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.start + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=true> + # http.started? # => true + # http.finish + # + # With a block, calls the block with +self+, + # finishes the session when the block exits, + # and returns the block's value: + # + # http.start do |http| + # http + # end + # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false> + # http.started? # => false + # + def start # :yield: http + raise IOError, 'HTTP session already opened' if @started + if block_given? + begin + do_start + return yield(self) + ensure + do_finish + end + end + do_start + self + end + + # Finishes the \HTTP session: + # + # http = Gem::Net::HTTP.new(hostname) + # http.start + # http.started? # => true + # http.finish # => nil + # http.started? # => false + # + # Raises IOError if not in a session. + def finish + raise IOError, 'HTTP session not yet started' unless started? + do_finish + end + + # :stopdoc: + def do_start + connect + @started = true + end + private :do_start + + def connect + if use_ssl? + # reference early to load OpenSSL before connecting, + # as OpenSSL may take time to load. + @ssl_context = OpenSSL::SSL::SSLContext.new + end + + if proxy? then + conn_addr = proxy_address + conn_port = proxy_port + else + conn_addr = conn_address + conn_port = port + end + + debug "opening connection to #{conn_addr}:#{conn_port}..." + begin + s = timeouted_connect(conn_addr, conn_port) + rescue => e + if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions + e = Gem::Net::OpenTimeout.new(e) + end + raise e, "Failed to open TCP connection to " + + "#{conn_addr}:#{conn_port} (#{e.message})" + end + s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) + debug "opened" + if use_ssl? + if proxy? + if @proxy_use_ssl + proxy_sock = OpenSSL::SSL::SSLSocket.new(s) + ssl_socket_connect(proxy_sock, @open_timeout) + else + proxy_sock = s + end + proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout, + write_timeout: @write_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \ + "Host: #{@address}:#{@port}\r\n" + if proxy_user + credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0') + buf << "Proxy-Authorization: Basic #{credential}\r\n" + end + buf << "\r\n" + proxy_sock.write(buf) + HTTPResponse.read_new(proxy_sock).value + # assuming nothing left in buffers after successful CONNECT response + end + + ssl_parameters = Hash.new + iv_list = instance_variables + SSL_IVNAMES.each_with_index do |ivname, i| + if iv_list.include?(ivname) + value = instance_variable_get(ivname) + unless value.nil? + ssl_parameters[SSL_ATTRIBUTES[i]] = value + end + end + end + @ssl_context.set_params(ssl_parameters) + unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby + @ssl_context.session_cache_mode = + OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | + OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE + end + if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby + @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } + end + + # Still do the post_connection_check below even if connecting + # to IP address + verify_hostname = @ssl_context.verify_hostname + + # Server Name Indication (SNI) RFC 3546/6066 + case @address + when Gem::Resolv::IPv4::Regex, Gem::Resolv::IPv6::Regex + # don't set SNI, as IP addresses in SNI is not valid + # per RFC 6066, section 3. + + # Avoid openssl warning + @ssl_context.verify_hostname = false + else + ssl_host_address = @address + end + + debug "starting SSL for #{conn_addr}:#{conn_port}..." + s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) + s.sync_close = true + s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address + + if @ssl_session and + Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout + s.session = @ssl_session + end + ssl_socket_connect(s, @open_timeout) + if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname + s.post_connection_check(@address) + end + debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}" + end + @socket = BufferedIO.new(s, read_timeout: @read_timeout, + write_timeout: @write_timeout, + continue_timeout: @continue_timeout, + debug_output: @debug_output) + @last_communicated = nil + on_connect + rescue => exception + if s + debug "Conn close because of connect error #{exception}" + s.close + end + raise + end + private :connect + + tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters + TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]] + tcp_socket_parameters.include?([:key, :open_timeout]) + else + # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize + # See discussion in https://github.com/ruby/net-http/pull/224 + Socket.method(:tcp).parameters.include?([:key, :open_timeout]) + end + private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT + + def timeouted_connect(conn_addr, conn_port) + if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout) + else + Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) { + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + } + end + end + private :timeouted_connect + + def on_connect + end + private :on_connect + + def do_finish + @started = false + @socket.close if @socket + @socket = nil + end + private :do_finish + + # + # proxy + # + + public + + # no proxy + @is_proxy_class = false + @proxy_from_env = false + @proxy_addr = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = nil + @proxy_use_ssl = nil + + # Creates an \HTTP proxy class which behaves like \Gem::Net::HTTP, but + # performs all access via the specified proxy. + # + # This class is obsolete. You may pass these same parameters directly to + # \Gem::Net::HTTP.new. See Gem::Net::HTTP.new for details of the arguments. + def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc: + return self unless p_addr + + Class.new(self) { + @is_proxy_class = true + + if p_addr == :ENV then + @proxy_from_env = true + @proxy_address = nil + @proxy_port = nil + else + @proxy_from_env = false + @proxy_address = p_addr + @proxy_port = p_port || default_port + end + + @proxy_user = p_user + @proxy_pass = p_pass + @proxy_use_ssl = p_use_ssl + } + end + + # :startdoc: + + class << HTTP + # Returns true if self is a class which was created by HTTP::Proxy. + def proxy_class? + defined?(@is_proxy_class) ? @is_proxy_class : false + end + + # Returns the address of the proxy host, or +nil+ if none; + # see Gem::Net::HTTP@Proxy+Server. + attr_reader :proxy_address + + # Returns the port number of the proxy host, or +nil+ if none; + # see Gem::Net::HTTP@Proxy+Server. + attr_reader :proxy_port + + # Returns the user name for accessing the proxy, or +nil+ if none; + # see Gem::Net::HTTP@Proxy+Server. + attr_reader :proxy_user + + # Returns the password for accessing the proxy, or +nil+ if none; + # see Gem::Net::HTTP@Proxy+Server. + attr_reader :proxy_pass + + # Use SSL when talking to the proxy. If Gem::Net::HTTP does not use a proxy, nil. + attr_reader :proxy_use_ssl + end + + # Returns +true+ if a proxy server is defined, +false+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy? + !!(@proxy_from_env ? proxy_uri : @proxy_address) + end + + # Returns +true+ if the proxy server is defined in the environment, + # +false+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy_from_env? + @proxy_from_env + end + + # The proxy Gem::URI determined from the environment for this connection. + def proxy_uri # :nodoc: + return if @proxy_uri == false + @proxy_uri ||= Gem::URI::HTTP.new( + "http", nil, address, port, nil, nil, nil, nil, nil + ).find_proxy || false + @proxy_uri || nil + end + + # Returns the address of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy_address + if @proxy_from_env then + proxy_uri&.hostname + else + @proxy_address + end + end + + # Returns the port number of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy_port + if @proxy_from_env then + proxy_uri&.port + else + @proxy_port + end + end + + # Returns the user name of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy_user + if @proxy_from_env + user = proxy_uri&.user + unescape(user) if user + else + @proxy_user + end + end + + # Returns the password of the proxy server, if defined, +nil+ otherwise; + # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server]. + def proxy_pass + if @proxy_from_env + pass = proxy_uri&.password + unescape(pass) if pass + else + @proxy_pass + end + end + + alias proxyaddr proxy_address #:nodoc: obsolete + alias proxyport proxy_port #:nodoc: obsolete + + private + # :stopdoc: + + def unescape(value) + require 'cgi/escape' + require 'cgi/util' unless defined?(CGI::EscapeExt) + CGI.unescape(value) + end + + # without proxy, obsolete + + def conn_address # :nodoc: + @ipaddr || address() + end + + def conn_port # :nodoc: + port() + end + + def edit_path(path) + if proxy? + if path.start_with?("ftp://") || use_ssl? + path + else + "http://#{addr_port}#{path}" + end + else + path + end + end + # :startdoc: + + # + # HTTP operations + # + + public + + # :call-seq: + # get(path, initheader = nil) {|res| ... } + # + # Sends a GET request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Get object + # created from string +path+ and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # http = Gem::Net::HTTP.new(hostname) + # http.get('/todos/1') do |res| + # p res + # end # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" + # + # With no block given, simply returns the response object: + # + # http.get('/') # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Related: + # + # - Gem::Net::HTTP::Get: request class for \HTTP method GET. + # - Gem::Net::HTTP.get: sends GET request, returns response body. + # + def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + res = nil + + request(Get.new(path, initheader)) {|r| + r.read_body dest, &block + res = r + } + res + end + + # Sends a HEAD request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Head object + # created from string +path+ and initial headers hash +initheader+: + # + # res = http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # res.body # => nil + # res.to_hash.take(3) + # # => + # [["date", ["Wed, 15 Feb 2023 15:25:42 GMT"]], + # ["content-type", ["application/json; charset=utf-8"]], + # ["connection", ["close"]]] + # + def head(path, initheader = nil) + request(Head.new(path, initheader)) + end + + # :call-seq: + # post(path, data, initheader = nil) {|res| ... } + # + # Sends a POST request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Post object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.post('/todos', data) do |res| + # p res + # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + # Output: + # + # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}" + # + # With no block given, simply returns the response object: + # + # http.post('/todos', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + # Related: + # + # - Gem::Net::HTTP::Post: request class for \HTTP method POST. + # - Gem::Net::HTTP.post: sends POST request, returns response body. + # + def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Post, &block) + end + + # :call-seq: + # patch(path, data, initheader = nil) {|res| ... } + # + # Sends a PATCH request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Patch object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With a block given, calls the block with the response body: + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.patch('/todos/1', data) do |res| + # p res + # end # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false,\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\"\n}" + # + # With no block given, simply returns the response object: + # + # http.patch('/todos/1', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+ + send_entity(path, data, initheader, dest, Patch, &block) + end + + # Sends a PUT request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Put object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.put('/todos/1', data) # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Related: + # + # - Gem::Net::HTTP::Put: request class for \HTTP method PUT. + # - Gem::Net::HTTP.put: sends PUT request, returns response body. + # + def put(path, data, initheader = nil) + request(Put.new(path, initheader), data) + end + + # Sends a PROPPATCH request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Proppatch object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.proppatch('/todos/1', data) + # + def proppatch(path, body, initheader = nil) + request(Proppatch.new(path, initheader), body) + end + + # Sends a LOCK request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Lock object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.lock('/todos/1', data) + # + def lock(path, body, initheader = nil) + request(Lock.new(path, initheader), body) + end + + # Sends an UNLOCK request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Unlock object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.unlock('/todos/1', data) + # + def unlock(path, body, initheader = nil) + request(Unlock.new(path, initheader), body) + end + + # Sends an Options request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Options object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.options('/') + # + def options(path, initheader = nil) + request(Options.new(path, initheader)) + end + + # Sends a PROPFIND request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Propfind object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http = Gem::Net::HTTP.new(hostname) + # http.propfind('/todos/1', data) + # + def propfind(path, body = nil, initheader = {'Depth' => '0'}) + request(Propfind.new(path, initheader), body) + end + + # Sends a DELETE request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Delete object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.delete('/todos/1') + # + def delete(path, initheader = {'Depth' => 'Infinity'}) + request(Delete.new(path, initheader)) + end + + # Sends a MOVE request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Move object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.move('/todos/1') + # + def move(path, initheader = nil) + request(Move.new(path, initheader)) + end + + # Sends a COPY request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Copy object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.copy('/todos/1') + # + def copy(path, initheader = nil) + request(Copy.new(path, initheader)) + end + + # Sends a MKCOL request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Mkcol object + # created from string +path+, string +body+, and initial headers hash +initheader+. + # + # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}' + # http.mkcol('/todos/1', data) + # http = Gem::Net::HTTP.new(hostname) + # + def mkcol(path, body = nil, initheader = nil) + request(Mkcol.new(path, initheader), body) + end + + # Sends a TRACE request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Trace object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.trace('/todos/1') + # + def trace(path, initheader = nil) + request(Trace.new(path, initheader)) + end + + # Sends a GET request to the server; + # forms the response into a Gem::Net::HTTPResponse object. + # + # The request is based on the Gem::Net::HTTP::Get object + # created from string +path+ and initial headers hash +initheader+. + # + # With no block given, returns the response object: + # + # http = Gem::Net::HTTP.new(hostname) + # http.request_get('/todos') # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # With a block given, calls the block with the response object + # and returns the response object: + # + # http.request_get('/todos') do |res| + # p res + # end # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # #<Gem::Net::HTTPOK 200 OK readbody=false> + # + def request_get(path, initheader = nil, &block) # :yield: +response+ + request(Get.new(path, initheader), &block) + end + + # Sends a HEAD request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Head object + # created from string +path+ and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + def request_head(path, initheader = nil, &block) + request(Head.new(path, initheader), &block) + end + + # Sends a POST request to the server; + # forms the response into a Gem::Net::HTTPResponse object. + # + # The request is based on the Gem::Net::HTTP::Post object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # With no block given, returns the response object: + # + # http = Gem::Net::HTTP.new(hostname) + # http.post('/todos', 'xyzzy') + # # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + # With a block given, calls the block with the response body + # and returns the response object: + # + # http.post('/todos', 'xyzzy') do |res| + # p res + # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + # Output: + # + # "{\n \"xyzzy\": \"\",\n \"id\": 201\n}" + # + def request_post(path, data, initheader = nil, &block) # :yield: +response+ + request Post.new(path, initheader), data, &block + end + + # Sends a PUT request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTP::Put object + # created from string +path+, string +data+, and initial headers hash +initheader+. + # + # http = Gem::Net::HTTP.new(hostname) + # http.put('/todos/1', 'xyzzy') + # # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + def request_put(path, data, initheader = nil, &block) #:nodoc: + request Put.new(path, initheader), data, &block + end + + alias get2 request_get #:nodoc: obsolete + alias head2 request_head #:nodoc: obsolete + alias post2 request_post #:nodoc: obsolete + alias put2 request_put #:nodoc: obsolete + + # Sends an \HTTP request to the server; + # returns an instance of a subclass of Gem::Net::HTTPResponse. + # + # The request is based on the Gem::Net::HTTPRequest object + # created from string +path+, string +data+, and initial headers hash +header+. + # That object is an instance of the + # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses], + # that corresponds to the given uppercase string +name+, + # which must be + # an {HTTP request method}[https://en.wikipedia.org/wiki/HTTP#Request_methods] + # or a {WebDAV request method}[https://en.wikipedia.org/wiki/WebDAV#Implementation]. + # + # Examples: + # + # http = Gem::Net::HTTP.new(hostname) + # http.send_request('GET', '/todos/1') + # # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # http.send_request('POST', '/todos', 'xyzzy') + # # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + def send_request(name, path, data = nil, header = nil) + has_response_body = name != 'HEAD' + r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header) + request r, data + end + + # Sends the given request +req+ to the server; + # forms the response into a Gem::Net::HTTPResponse object. + # + # The given +req+ must be an instance of a + # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses]. + # Argument +body+ should be given only if needed for the request. + # + # With no block given, returns the response object: + # + # http = Gem::Net::HTTP.new(hostname) + # + # req = Gem::Net::HTTP::Get.new('/todos/1') + # http.request(req) + # # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # req = Gem::Net::HTTP::Post.new('/todos') + # http.request(req, 'xyzzy') + # # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + # With a block given, calls the block with the response and returns the response: + # + # req = Gem::Net::HTTP::Get.new('/todos/1') + # http.request(req) do |res| + # p res + # end # => #<Gem::Net::HTTPOK 200 OK readbody=true> + # + # Output: + # + # #<Gem::Net::HTTPOK 200 OK readbody=false> + # + def request(req, body = nil, &block) # :yield: +response+ + unless started? + start { + req['connection'] ||= 'close' + return request(req, body, &block) + } + end + if proxy_user() + req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? + end + req.set_body_internal body + res = transport_request(req, &block) + if sspi_auth?(res) + sspi_auth(req) + res = transport_request(req, &block) + end + res + end + + private + + # Executes a request which uses a representation + # and returns its body. + def send_entity(path, data, initheader, dest, type, &block) + res = nil + request(type.new(path, initheader), data) {|r| + r.read_body dest, &block + res = r + } + res + end + + # :stopdoc: + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc: + + def transport_request(req) + count = 0 + begin + begin_transport req + res = catch(:response) { + begin + req.exec @socket, @curr_http_version, edit_path(req.path) + rescue Errno::EPIPE + # Failure when writing full request, but we can probably + # still read the received response. + end + + begin + res = HTTPResponse.read_new(@socket) + res.decode_content = req.decode_content + res.body_encoding = @response_body_encoding + res.ignore_eof = @ignore_eof + end while res.kind_of?(HTTPInformation) + + res.uri = req.uri + + res + } + res.reading_body(@socket, req.response_body_permitted?) { + if block_given? + count = max_retries # Don't restart in the middle of a download + yield res + end + } + rescue Gem::Net::OpenTimeout + raise + rescue Gem::Net::ReadTimeout, IOError, EOFError, + Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT, + # avoid a dependency on OpenSSL + defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, + Gem::Timeout::Error => exception + if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method) + count += 1 + @socket.close if @socket + debug "Conn close because of error #{exception}, and retry" + retry + end + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise + end + + end_transport req, res + res + rescue => exception + debug "Conn close because of error #{exception}" + @socket.close if @socket + raise exception + end + + def begin_transport(req) + if @socket.closed? + connect + elsif @last_communicated + if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC) + debug 'Conn close because of keep_alive_timeout' + @socket.close + connect + elsif @socket.io.to_io.wait_readable(0) && @socket.eof? + debug "Conn close because of EOF" + @socket.close + connect + end + end + + if not req.response_body_permitted? and @close_on_empty_response + req['connection'] ||= 'close' + end + + req.update_uri address, port, use_ssl? + req['host'] ||= addr_port() + end + + def end_transport(req, res) + @curr_http_version = res.http_version + @last_communicated = nil + if @socket.closed? + debug 'Conn socket closed' + elsif not res.body and @close_on_empty_response + debug 'Conn close' + @socket.close + elsif keep_alive?(req, res) + debug 'Conn keep-alive' + @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC) + else + debug 'Conn close' + @socket.close + end + end + + def keep_alive?(req, res) + return false if req.connection_close? + if @curr_http_version <= '1.0' + res.connection_keep_alive? + else # HTTP/1.1 or later + not res.connection_close? + end + end + + def sspi_auth?(res) + return false unless @sspi_enabled + if res.kind_of?(HTTPProxyAuthenticationRequired) and + proxy? and res["Proxy-Authenticate"].include?("Negotiate") + begin + require 'win32/sspi' + true + rescue LoadError + false + end + else + false + end + end + + def sspi_auth(req) + n = Win32::SSPI::NegotiateAuth.new + req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}" + # Some versions of ISA will close the connection if this isn't present. + req["Connection"] = "Keep-Alive" + req["Proxy-Connection"] = "Keep-Alive" + res = transport_request(req) + authphrase = res["Proxy-Authenticate"] or return res + req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}" + rescue => err + raise HTTPAuthenticationError.new('HTTP authentication failed', err) + end + + # + # utils + # + + private + + def addr_port + addr = address + addr = "[#{addr}]" if addr.include?(":") + default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port + default_port == port ? addr : "#{addr}:#{port}" + end + + # Adds a message to debugging output + def debug(msg) + return unless @debug_output + @debug_output << msg + @debug_output << "\n" + end + + alias_method :D, :debug + end + + # for backward compatibility until Ruby 4.0 + # https://bugs.ruby-lang.org/issues/20900 + # https://github.com/bblimke/webmock/pull/1081 + HTTPSession = HTTP + deprecate_constant :HTTPSession +end + +require_relative 'http/exceptions' + +require_relative 'http/header' + +require_relative 'http/generic_request' +require_relative 'http/request' +require_relative 'http/requests' + +require_relative 'http/response' +require_relative 'http/responses' + +require_relative 'http/proxy_delta' diff --git a/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb new file mode 100644 index 0000000000..218df9a8bd --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true +module Gem::Net + # Gem::Net::HTTP exception class. + # You cannot use Gem::Net::HTTPExceptions directly; instead, you must use + # its subclasses. + module HTTPExceptions # :nodoc: + def initialize(msg, res) #:nodoc: + super msg + @response = res + end + attr_reader :response + alias data response #:nodoc: obsolete + end + + # :stopdoc: + class HTTPError < ProtocolError + include HTTPExceptions + end + + class HTTPRetriableError < ProtoRetriableError + include HTTPExceptions + end + + class HTTPClientException < ProtoServerError + include HTTPExceptions + end + + class HTTPFatalError < ProtoFatalError + include HTTPExceptions + end + + # We cannot use the name "HTTPServerError", it is the name of the response. + HTTPServerException = HTTPClientException # :nodoc: + deprecate_constant(:HTTPServerException) +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb new file mode 100644 index 0000000000..d6496d4ac1 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb @@ -0,0 +1,429 @@ +# frozen_string_literal: true +# +# \HTTPGenericRequest is the parent of the Gem::Net::HTTPRequest class. +# +# Do not use this directly; instead, use a subclass of Gem::Net::HTTPRequest. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +class Gem::Net::HTTPGenericRequest + + include Gem::Net::HTTPHeader + + def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc: + @method = m + @request_has_body = reqbody + @response_has_body = resbody + + if Gem::URI === uri_or_path then + raise ArgumentError, "not an HTTP Gem::URI" unless Gem::URI::HTTP === uri_or_path + hostname = uri_or_path.host + raise ArgumentError, "no host component for Gem::URI" unless (hostname && hostname.length > 0) + @uri = uri_or_path.dup + @path = uri_or_path.request_uri + raise ArgumentError, "no HTTP request path given" unless @path + else + @uri = nil + raise ArgumentError, "no HTTP request path given" unless uri_or_path + raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? + @path = uri_or_path.dup + end + + @decode_content = false + + if Gem::Net::HTTP::HAVE_ZLIB then + if !initheader || + !initheader.keys.any? { |k| + %w[accept-encoding range].include? k.downcase + } then + @decode_content = true if @response_has_body + initheader = initheader ? initheader.dup : {} + initheader["accept-encoding"] = + "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + end + end + + initialize_http_header initheader + self['Accept'] ||= '*/*' + self['User-Agent'] ||= 'Ruby' + self['Host'] ||= @uri.authority if @uri + @body = nil + @body_stream = nil + @body_data = nil + end + + # Returns the string method name for the request: + # + # Gem::Net::HTTP::Get.new(uri).method # => "GET" + # Gem::Net::HTTP::Post.new(uri).method # => "POST" + # + attr_reader :method + + # Returns the string path for the request: + # + # Gem::Net::HTTP::Get.new(uri).path # => "/" + # Gem::Net::HTTP::Post.new('example.com').path # => "example.com" + # + attr_reader :path + + # Returns the Gem::URI object for the request, or +nil+ if none: + # + # Gem::Net::HTTP::Get.new(uri).uri + # # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/> + # Gem::Net::HTTP::Get.new('example.com').uri # => nil + # + attr_reader :uri + + # Returns +false+ if the request's header <tt>'Accept-Encoding'</tt> + # has been set manually or deleted + # (indicating that the user intends to handle encoding in the response), + # +true+ otherwise: + # + # req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET> + # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" + # req.decode_content # => true + # req['Accept-Encoding'] = 'foo' + # req.decode_content # => false + # req.delete('Accept-Encoding') + # req.decode_content # => false + # + attr_reader :decode_content + + # Returns a string representation of the request: + # + # Gem::Net::HTTP::Post.new(uri).inspect # => "#<Gem::Net::HTTP::Post POST>" + # + def inspect + "\#<#{self.class} #{@method}>" + end + + # Returns a string representation of the request with the details for pp: + # + # require 'pp' + # post = Gem::Net::HTTP::Post.new(uri) + # post.inspect # => "#<Gem::Net::HTTP::Post POST>" + # post.pretty_inspect + # # => #<Gem::Net::HTTP::Post + # POST + # path="/" + # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], + # "accept" => ["*/*"], + # "user-agent" => ["Ruby"], + # "host" => ["www.ruby-lang.org"]}> + # + def pretty_print(q) + q.object_group(self) { + q.breakable + q.text @method + q.breakable + q.text "path="; q.pp @path + q.breakable + q.text "headers="; q.pp to_hash + } + end + + ## + # Don't automatically decode response content-encoding if the user indicates + # they want to handle it. + + def []=(key, val) # :nodoc: + @decode_content = false if key.downcase == 'accept-encoding' + + super key, val + end + + # Returns whether the request may have a body: + # + # Gem::Net::HTTP::Post.new(uri).request_body_permitted? # => true + # Gem::Net::HTTP::Get.new(uri).request_body_permitted? # => false + # + def request_body_permitted? + @request_has_body + end + + # Returns whether the response may have a body: + # + # Gem::Net::HTTP::Post.new(uri).response_body_permitted? # => true + # Gem::Net::HTTP::Head.new(uri).response_body_permitted? # => false + # + def response_body_permitted? + @response_has_body + end + + def body_exist? # :nodoc: + warn "Gem::Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE + response_body_permitted? + end + + # Returns the string body for the request, or +nil+ if there is none: + # + # req = Gem::Net::HTTP::Post.new(uri) + # req.body # => nil + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" + # + attr_reader :body + + # Sets the body for the request: + # + # req = Gem::Net::HTTP::Post.new(uri) + # req.body # => nil + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}" + # + def body=(str) + @body = str + @body_stream = nil + @body_data = nil + str + end + + # Returns the body stream object for the request, or +nil+ if there is none: + # + # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST> + # req.body_stream # => nil + # require 'stringio' + # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8> + # req.body_stream # => #<StringIO:0x0000027d1e5affa8> + # + attr_reader :body_stream + + # Sets the body stream for the request: + # + # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST> + # req.body_stream # => nil + # require 'stringio' + # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8> + # req.body_stream # => #<StringIO:0x0000027d1e5affa8> + # + def body_stream=(input) + @body = nil + @body_stream = input + @body_data = nil + input + end + + def set_body_internal(str) #:nodoc: internal use only + raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream) + self.body = str if str + if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted? + self.body = '' + end + end + + # + # write + # + + def exec(sock, ver, path) #:nodoc: internal use only + if @body + send_request_with_body sock, ver, path, @body + elsif @body_stream + send_request_with_body_stream sock, ver, path, @body_stream + elsif @body_data + send_request_with_body_data sock, ver, path, @body_data + else + write_header sock, ver, path + end + end + + def update_uri(addr, port, ssl) # :nodoc: internal use only + # reflect the connection and @path to @uri + return unless @uri + + if ssl + scheme = 'https' + klass = Gem::URI::HTTPS + else + scheme = 'http' + klass = Gem::URI::HTTP + end + + if host = self['host'] + host = Gem::URI.parse("//#{host}").host # Remove a port component from the existing Host header + elsif host = @uri.host + else + host = addr + end + # convert the class of the Gem::URI + if @uri.is_a?(klass) + @uri.host = host + @uri.port = port + else + @uri = klass.new( + scheme, @uri.userinfo, + host, port, nil, + @uri.path, nil, @uri.query, nil) + end + end + + private + + # :stopdoc: + + class Chunker #:nodoc: + def initialize(sock) + @sock = sock + @prev = nil + end + + def write(buf) + # avoid memcpy() of buf, buf can huge and eat memory bandwidth + rv = buf.bytesize + @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n") + rv + end + + def finish + @sock.write("0\r\n\r\n") + end + end + + def send_request_with_body(sock, ver, path, body) + self.content_length = body.bytesize + delete 'Transfer-Encoding' + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + sock.write body + end + + def send_request_with_body_stream(sock, ver, path, f) + unless content_length() or chunked? + raise ArgumentError, + "Content-Length not given and Transfer-Encoding is not `chunked'" + end + write_header sock, ver, path + wait_for_continue sock, ver if sock.continue_timeout + if chunked? + chunker = Chunker.new(sock) + IO.copy_stream(f, chunker) + chunker.finish + else + IO.copy_stream(f, sock) + end + end + + def send_request_with_body_data(sock, ver, path, params) + if /\Amultipart\/form-data\z/i !~ self.content_type + self.content_type = 'application/x-www-form-urlencoded' + return send_request_with_body(sock, ver, path, Gem::URI.encode_www_form(params)) + end + + opt = @form_option.dup + require 'securerandom' unless defined?(SecureRandom) + opt[:boundary] ||= SecureRandom.urlsafe_base64(40) + self.set_content_type(self.content_type, boundary: opt[:boundary]) + if chunked? + write_header sock, ver, path + encode_multipart_form_data(sock, params, opt) + else + require 'tempfile' + file = Tempfile.new('multipart') + file.binmode + encode_multipart_form_data(file, params, opt) + file.rewind + self.content_length = file.size + write_header sock, ver, path + IO.copy_stream(file, sock) + file.close(true) + end + end + + def encode_multipart_form_data(out, params, opt) + charset = opt[:charset] + boundary = opt[:boundary] + require 'securerandom' unless defined?(SecureRandom) + boundary ||= SecureRandom.urlsafe_base64(40) + chunked_p = chunked? + + buf = +'' + params.each do |key, value, h={}| + key = quote_string(key, charset) + filename = + h.key?(:filename) ? h[:filename] : + value.respond_to?(:to_path) ? File.basename(value.to_path) : + nil + + buf << "--#{boundary}\r\n" + if filename + filename = quote_string(filename, charset) + type = h[:content_type] || 'application/octet-stream' + buf << "Content-Disposition: form-data; " \ + "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \ + "Content-Type: #{type}\r\n\r\n" + if !out.respond_to?(:write) || !value.respond_to?(:read) + # if +out+ is not an IO or +value+ is not an IO + buf << (value.respond_to?(:read) ? value.read : value) + elsif value.respond_to?(:size) && chunked_p + # if +out+ is an IO and +value+ is a File, use IO.copy_stream + flush_buffer(out, buf, chunked_p) + out << "%x\r\n" % value.size if chunked_p + IO.copy_stream(value, out) + out << "\r\n" if chunked_p + else + # +out+ is an IO, and +value+ is not a File but an IO + flush_buffer(out, buf, chunked_p) + 1 while flush_buffer(out, value.read(4096), chunked_p) + end + else + # non-file field: + # HTML5 says, "The parts of the generated multipart/form-data + # resource that correspond to non-file fields must not have a + # Content-Type header specified." + buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n" + buf << (value.respond_to?(:read) ? value.read : value) + end + buf << "\r\n" + end + buf << "--#{boundary}--\r\n" + flush_buffer(out, buf, chunked_p) + out << "0\r\n\r\n" if chunked_p + end + + def quote_string(str, charset) + str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset + str.gsub(/[\\"]/, '\\\\\&') + end + + def flush_buffer(out, buf, chunked_p) + return unless buf + out << "%x\r\n"%buf.bytesize if chunked_p + out << buf + out << "\r\n" if chunked_p + buf.clear + end + + ## + # Waits up to the continue timeout for a response from the server provided + # we're speaking HTTP 1.1 and are expecting a 100-continue response. + + def wait_for_continue(sock, ver) + if ver >= '1.1' and @header['expect'] and + @header['expect'].include?('100-continue') + if sock.io.to_io.wait_readable(sock.continue_timeout) + res = Gem::Net::HTTPResponse.read_new(sock) + unless res.kind_of?(Gem::Net::HTTPContinue) + res.decode_content = @decode_content + throw :response, res + end + end + end + end + + def write_header(sock, ver, path) + reqline = "#{@method} #{path} HTTP/#{ver}" + if /[\r\n]/ =~ reqline + raise ArgumentError, "A Request-Line must not contain CR or LF" + end + buf = +'' + buf << reqline << "\r\n" + each_capitalized do |k,v| + buf << "#{k}: #{v}\r\n" + end + buf << "\r\n" + sock.write buf + end + +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/header.rb b/lib/rubygems/vendor/net-http/lib/net/http/header.rb new file mode 100644 index 0000000000..bc68cd2eef --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/header.rb @@ -0,0 +1,985 @@ +# frozen_string_literal: true +# +# The \HTTPHeader module provides access to \HTTP headers. +# +# The module is included in: +# +# - Gem::Net::HTTPGenericRequest (and therefore Gem::Net::HTTPRequest). +# - Gem::Net::HTTPResponse. +# +# The headers are a hash-like collection of key/value pairs called _fields_. +# +# == Request and Response Fields +# +# Headers may be included in: +# +# - A Gem::Net::HTTPRequest object: +# the object's headers will be sent with the request. +# Any fields may be defined in the request; +# see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters]. +# - A Gem::Net::HTTPResponse object: +# the objects headers are usually those returned from the host. +# Fields may be retrieved from the object; +# see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters] +# and {Iterators}[rdoc-ref:Gem::Net::HTTPHeader@Iterators]. +# +# Exactly which fields should be sent or expected depends on the host; +# see: +# +# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields]. +# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields]. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +# == Fields +# +# A header field is a key/value pair. +# +# === Field Keys +# +# A field key may be: +# +# - A string: Key <tt>'Accept'</tt> is treated as if it were +# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>. +# - A symbol: Key <tt>:Accept</tt> is treated as if it were +# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>. +# +# Examples: +# +# req = Gem::Net::HTTP::Get.new(uri) +# req[:accept] # => "*/*" +# req['Accept'] # => "*/*" +# req['ACCEPT'] # => "*/*" +# +# req['accept'] = 'text/html' +# req[:accept] = 'text/html' +# req['ACCEPT'] = 'text/html' +# +# === Field Values +# +# A field value may be returned as an array of strings or as a string: +# +# - These methods return field values as arrays: +# +# - #get_fields: Returns the array value for the given key, +# or +nil+ if it does not exist. +# - #to_hash: Returns a hash of all header fields: +# each key is a field name; its value is the array value for the field. +# +# - These methods return field values as string; +# the string value for a field is equivalent to +# <tt>self[key.downcase.to_s].join(', '))</tt>: +# +# - #[]: Returns the string value for the given key, +# or +nil+ if it does not exist. +# - #fetch: Like #[], but accepts a default value +# to be returned if the key does not exist. +# +# The field value may be set: +# +# - #[]=: Sets the value for the given key; +# the given value may be a string, a symbol, an array, or a hash. +# - #add_field: Adds a given value to a value for the given key +# (not overwriting the existing value). +# - #delete: Deletes the field for the given key. +# +# Example field values: +# +# - \String: +# +# req['Accept'] = 'text/html' # => "text/html" +# req['Accept'] # => "text/html" +# req.get_fields('Accept') # => ["text/html"] +# +# - \Symbol: +# +# req['Accept'] = :text # => :text +# req['Accept'] # => "text" +# req.get_fields('Accept') # => ["text"] +# +# - Simple array: +# +# req[:foo] = %w[bar baz bat] +# req[:foo] # => "bar, baz, bat" +# req.get_fields(:foo) # => ["bar", "baz", "bat"] +# +# - Simple hash: +# +# req[:foo] = {bar: 0, baz: 1, bat: 2} +# req[:foo] # => "bar, 0, baz, 1, bat, 2" +# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"] +# +# - Nested: +# +# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}] +# req[:foo] # => "bar, baz, bat, 0, bam, 1" +# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"] +# +# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}} +# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1" +# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"] +# +# == Convenience Methods +# +# Various convenience methods retrieve values, set values, query values, +# set form values, or iterate over fields. +# +# === Setters +# +# \Method #[]= can set any field, but does little to validate the new value; +# some of the other setter methods provide some validation: +# +# - #[]=: Sets the string or array value for the given key. +# - #add_field: Creates or adds to the array value for the given key. +# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>. +# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>. +# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>. +# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>. +# - #set_range: Sets the value for field <tt>'Range'</tt>. +# +# === Form Setters +# +# - #set_form: Sets an HTML form data set. +# - #set_form_data: Sets header fields and a body from HTML form data. +# +# === Getters +# +# \Method #[] can retrieve the value of any field that exists, +# but always as a string; +# some of the other getter methods return something different +# from the simple string value: +# +# - #[]: Returns the string field value for the given key. +# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>. +# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>. +# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>. +# - #fetch: Returns the string field value for the given key. +# - #get_fields: Returns the array field value for the given +key+. +# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>. +# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>. +# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+. +# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>. +# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>. +# +# === Queries +# +# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>. +# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>. +# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>. +# - #key?: Returns whether a given key exists. +# +# === Iterators +# +# - #each_capitalized: Passes each field capitalized-name/value pair to the block. +# - #each_capitalized_name: Passes each capitalized field name to the block. +# - #each_header: Passes each field name/value pair to the block. +# - #each_name: Passes each field name to the block. +# - #each_value: Passes each string field value to the block. +# +module Gem::Net::HTTPHeader + # The maximum length of HTTP header keys. + MAX_KEY_LENGTH = 1024 + # The maximum length of HTTP header values. + MAX_FIELD_LENGTH = 65536 + + def initialize_http_header(initheader) #:nodoc: + @header = {} + return unless initheader + initheader.each do |key, value| + warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE + if value.nil? + warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE + else + value = value.strip # raise error for invalid byte sequences + if key.to_s.bytesize > MAX_KEY_LENGTH + raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..." + end + if value.to_s.bytesize > MAX_FIELD_LENGTH + raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}" + end + if value.count("\r\n") > 0 + raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF" + end + @header[key.downcase.to_s] = [value] + end + end + end + + def size #:nodoc: obsolete + @header.size + end + + alias length size #:nodoc: obsolete + + # Returns the string field value for the case-insensitive field +key+, + # or +nil+ if there is no such key; + # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['Connection'] # => "keep-alive" + # res['Nosuch'] # => nil + # + # Note that some field values may be retrieved via convenience methods; + # see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters]. + def [](key) + a = @header[key.downcase.to_s] or return nil + a.join(', ') + end + + # Sets the value for the case-insensitive +key+ to +val+, + # overwriting the previous value if the field exists; + # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req['Accept'] # => "*/*" + # req['Accept'] = 'text/html' + # req['Accept'] # => "text/html" + # + # Note that some field values may be set via convenience methods; + # see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters]. + def []=(key, val) + unless val + @header.delete key.downcase.to_s + return val + end + set_field(key, val) + end + + # Adds value +val+ to the value array for field +key+ if the field exists; + # creates the field with the given +key+ and +val+ if it does not exist. + # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.add_field('Foo', 'bar') + # req['Foo'] # => "bar" + # req.add_field('Foo', 'baz') + # req['Foo'] # => "bar, baz" + # req.add_field('Foo', %w[baz bam]) + # req['Foo'] # => "bar, baz, baz, bam" + # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"] + # + def add_field(key, val) + stringified_downcased_key = key.downcase.to_s + if @header.key?(stringified_downcased_key) + append_field_value(@header[stringified_downcased_key], val) + else + set_field(key, val) + end + end + + # :stopdoc: + private def set_field(key, val) + case val + when Enumerable + ary = [] + append_field_value(ary, val) + @header[key.downcase.to_s] = ary + else + val = val.to_s # for compatibility use to_s instead of to_str + if val.b.count("\r\n") > 0 + raise ArgumentError, 'header field value cannot include CR/LF' + end + @header[key.downcase.to_s] = [val] + end + end + + private def append_field_value(ary, val) + case val + when Enumerable + val.each{|x| append_field_value(ary, x)} + else + val = val.to_s + if /[\r\n]/n.match?(val.b) + raise ArgumentError, 'header field value cannot include CR/LF' + end + ary.push val + end + end + # :startdoc: + + # Returns the array field value for the given +key+, + # or +nil+ if there is no such field; + # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.get_fields('Connection') # => ["keep-alive"] + # res.get_fields('Nosuch') # => nil + # + def get_fields(key) + stringified_downcased_key = key.downcase.to_s + return nil unless @header[stringified_downcased_key] + @header[stringified_downcased_key].dup + end + + # call-seq: + # fetch(key, default_val = nil) {|key| ... } -> object + # fetch(key, default_val = nil) -> value or default_val + # + # With a block, returns the string value for +key+ if it exists; + # otherwise returns the value of the block; + # ignores the +default_val+; + # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # + # # Field exists; block not called. + # res.fetch('Connection') do |value| + # fail 'Cannot happen' + # end # => "keep-alive" + # + # # Field does not exist; block called. + # res.fetch('Nosuch') do |value| + # value.downcase + # end # => "nosuch" + # + # With no block, returns the string value for +key+ if it exists; + # otherwise, returns +default_val+ if it was given; + # otherwise raises an exception: + # + # res.fetch('Connection', 'Foo') # => "keep-alive" + # res.fetch('Nosuch', 'Foo') # => "Foo" + # res.fetch('Nosuch') # Raises KeyError. + # + def fetch(key, *args, &block) #:yield: +key+ + a = @header.fetch(key.downcase.to_s, *args, &block) + a.kind_of?(Array) ? a.join(', ') : a + end + + # Calls the block with each key/value pair: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.each_header do |key, value| + # p [key, value] if key.start_with?('c') + # end + # + # Output: + # + # ["content-type", "application/json; charset=utf-8"] + # ["connection", "keep-alive"] + # ["cache-control", "max-age=43200"] + # ["cf-cache-status", "HIT"] + # ["cf-ray", "771d17e9bc542cf5-ORD"] + # + # Returns an enumerator if no block is given. + # + # Gem::Net::HTTPHeader#each is an alias for Gem::Net::HTTPHeader#each_header. + def each_header #:yield: +key+, +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,va| + yield k, va.join(', ') + end + end + + alias each each_header + + # Calls the block with each field key: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.each_key do |key| + # p key if key.start_with?('c') + # end + # + # Output: + # + # "content-type" + # "connection" + # "cache-control" + # "cf-cache-status" + # "cf-ray" + # + # Returns an enumerator if no block is given. + # + # Gem::Net::HTTPHeader#each_name is an alias for Gem::Net::HTTPHeader#each_key. + def each_name(&block) #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key(&block) + end + + alias each_key each_name + + # Calls the block with each capitalized field name: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.each_capitalized_name do |key| + # p key if key.start_with?('C') + # end + # + # Output: + # + # "Content-Type" + # "Connection" + # "Cache-Control" + # "Cf-Cache-Status" + # "Cf-Ray" + # + # The capitalization is system-dependent; + # see {Case Mapping}[https://docs.ruby-lang.org/en/master/case_mapping_rdoc.html]. + # + # Returns an enumerator if no block is given. + def each_capitalized_name #:yield: +key+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_key do |k| + yield capitalize(k) + end + end + + # Calls the block with each string field value: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.each_value do |value| + # p value if value.start_with?('c') + # end + # + # Output: + # + # "chunked" + # "cf-q-config;dur=6.0000002122251e-06" + # "cloudflare" + # + # Returns an enumerator if no block is given. + def each_value #:yield: +value+ + block_given? or return enum_for(__method__) { @header.size } + @header.each_value do |va| + yield va.join(', ') + end + end + + # Removes the header for the given case-insensitive +key+ + # (see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]); + # returns the deleted value, or +nil+ if no such field exists: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.delete('Accept') # => ["*/*"] + # req.delete('Nosuch') # => nil + # + def delete(key) + @header.delete(key.downcase.to_s) + end + + # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.key?('Accept') # => true + # req.key?('Nosuch') # => false + # + def key?(key) + @header.key?(key.downcase.to_s) + end + + # Returns a hash of the key/value pairs: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.to_hash + # # => + # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], + # "accept"=>["*/*"], + # "user-agent"=>["Ruby"], + # "host"=>["jsonplaceholder.typicode.com"]} + # + def to_hash + @header.dup + end + + # Like #each_header, but the keys are returned in capitalized form. + # + # Gem::Net::HTTPHeader#canonical_each is an alias for Gem::Net::HTTPHeader#each_capitalized. + def each_capitalized + block_given? or return enum_for(__method__) { @header.size } + @header.each do |k,v| + yield capitalize(k), v.join(', ') + end + end + + alias canonical_each each_capitalized + + def capitalize(name) # :nodoc: + name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze) + end + private :capitalize + + # Returns an array of Range objects that represent + # the value of field <tt>'Range'</tt>, + # or +nil+ if there is no such field; + # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req['Range'] = 'bytes=0-99,200-299,400-499' + # req.range # => [0..99, 200..299, 400..499] + # req.delete('Range') + # req.range # # => nil + # + def range + return nil unless @header['range'] + + value = self['Range'] + # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec ) + # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] ) + # corrected collected ABNF + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1 + # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C + # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5 + unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value + raise Gem::Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'" + end + + byte_range_set = $1 + result = byte_range_set.split(/,/).map {|spec| + m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or + raise Gem::Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'" + d1 = m[1].to_i + d2 = m[2].to_i + if m[1] and m[2] + if d1 > d2 + raise Gem::Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'" + end + d1..d2 + elsif m[1] + d1..-1 + elsif m[2] + -d2..-1 + else + raise Gem::Net::HTTPHeaderSyntaxError, 'range is not specified' + end + } + # if result.empty? + # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec + # but above regexp already denies it. + if result.size == 1 && result[0].begin == 0 && result[0].end == -1 + raise Gem::Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length' + end + result + end + + # call-seq: + # set_range(length) -> length + # set_range(offset, length) -> range + # set_range(begin..length) -> range + # + # Sets the value for field <tt>'Range'</tt>; + # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]: + # + # With argument +length+: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.set_range(100) # => 100 + # req['Range'] # => "bytes=0-99" + # + # With arguments +offset+ and +length+: + # + # req.set_range(100, 100) # => 100...200 + # req['Range'] # => "bytes=100-199" + # + # With argument +range+: + # + # req.set_range(100..199) # => 100..199 + # req['Range'] # => "bytes=100-199" + # + # Gem::Net::HTTPHeader#range= is an alias for Gem::Net::HTTPHeader#set_range. + def set_range(r, e = nil) + unless r + @header.delete 'range' + return r + end + r = (r...r+e) if e + case r + when Numeric + n = r.to_i + rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}") + when Range + first = r.first + last = r.end + last -= 1 if r.exclude_end? + if last == -1 + rangestr = (first > 0 ? "#{first}-" : "-#{-first}") + else + raise Gem::Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0 + raise Gem::Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0 + raise Gem::Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last + rangestr = "#{first}-#{last}" + end + else + raise TypeError, 'Range/Integer is required' + end + @header['range'] = ["bytes=#{rangestr}"] + r + end + + alias range= set_range + + # Returns the value of field <tt>'Content-Length'</tt> as an integer, + # or +nil+ if there is no such field; + # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/nosuch/1') + # res.content_length # => 2 + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res.content_length # => nil + # + def content_length + return nil unless key?('Content-Length') + len = self['Content-Length'].slice(/\d+/) or + raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Length format' + len.to_i + end + + # Sets the value of field <tt>'Content-Length'</tt> to the given numeric; + # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]: + # + # _uri = uri.dup + # hostname = _uri.hostname # => "jsonplaceholder.typicode.com" + # _uri.path = '/posts' # => "/posts" + # req = Gem::Net::HTTP::Post.new(_uri) # => #<Gem::Net::HTTP::Post POST> + # req.body = '{"title": "foo","body": "bar","userId": 1}' + # req.content_length = req.body.size # => 42 + # req.content_type = 'application/json' + # res = Gem::Net::HTTP.start(hostname) do |http| + # http.request(req) + # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true> + # + def content_length=(len) + unless len + @header.delete 'content-length' + return nil + end + @header['content-length'] = [len.to_i.to_s] + end + + # Returns +true+ if field <tt>'Transfer-Encoding'</tt> + # exists and has value <tt>'chunked'</tt>, + # +false+ otherwise; + # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['Transfer-Encoding'] # => "chunked" + # res.chunked? # => true + # + def chunked? + return false unless @header['transfer-encoding'] + field = self['Transfer-Encoding'] + (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false + end + + # Returns a Range object representing the value of field + # <tt>'Content-Range'</tt>, or +nil+ if no such field exists; + # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['Content-Range'] # => nil + # res['Content-Range'] = 'bytes 0-499/1000' + # res['Content-Range'] # => "bytes 0-499/1000" + # res.content_range # => 0..499 + # + def content_range + return nil unless @header['content-range'] + m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or + raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Range format' + return unless m[1] == 'bytes' + m[2].to_i .. m[3].to_i + end + + # Returns the integer representing length of the value of field + # <tt>'Content-Range'</tt>, or +nil+ if no such field exists; + # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['Content-Range'] # => nil + # res['Content-Range'] = 'bytes 0-499/1000' + # res.range_length # => 500 + # + def range_length + r = content_range() or return nil + r.end - r.begin + 1 + end + + # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.content_type # => "application/json" + # + def content_type + main = main_type() + return nil unless main + + sub = sub_type() + if sub + "#{main}/#{sub}" + else + main + end + end + + # Returns the leading ('type') part of the + # {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.main_type # => "application" + # + def main_type + return nil unless @header['content-type'] + self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip + end + + # Returns the trailing ('subtype') part of the + # {media type}[https://en.wikipedia.org/wiki/Media_type] + # from the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.sub_type # => "json" + # + def sub_type + return nil unless @header['content-type'] + _, sub = *self['Content-Type'].split(';').first.to_s.split('/') + return nil unless sub + sub.strip + end + + # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>, + # or +nil+ if no such field exists; + # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]: + # + # res = Gem::Net::HTTP.get_response(hostname, '/todos/1') + # res['content-type'] # => "application/json; charset=utf-8" + # res.type_params # => {"charset"=>"utf-8"} + # + def type_params + result = {} + list = self['Content-Type'].to_s.split(';') + list.shift + list.each do |param| + k, v = *param.split('=', 2) + result[k.strip] = v.strip + end + result + end + + # Sets the value of field <tt>'Content-Type'</tt>; + # returns the new value; + # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]: + # + # req = Gem::Net::HTTP::Get.new(uri) + # req.set_content_type('application/json') # => ["application/json"] + # + # Gem::Net::HTTPHeader#content_type= is an alias for Gem::Net::HTTPHeader#set_content_type. + def set_content_type(type, params = {}) + @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')] + end + + alias content_type= set_content_type + + # Sets the request body to a URL-encoded string derived from argument +params+, + # and sets request header field <tt>'Content-Type'</tt> + # to <tt>'application/x-www-form-urlencoded'</tt>. + # + # The resulting request is suitable for HTTP request +POST+ or +PUT+. + # + # Argument +params+ must be suitable for use as argument +enum+ to + # {Gem::URI.encode_www_form}[https://docs.ruby-lang.org/en/master/Gem::URI.html#method-c-encode_www_form]. + # + # With only argument +params+ given, + # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>: + # + # req = Gem::Net::HTTP::Post.new('example.com') + # + # req.set_form_data(q: 'ruby', lang: 'en') + # req.body # => "q=ruby&lang=en" + # req['Content-Type'] # => "application/x-www-form-urlencoded" + # + # req.set_form_data([['q', 'ruby'], ['lang', 'en']]) + # req.body # => "q=ruby&lang=en" + # + # req.set_form_data(q: ['ruby', 'perl'], lang: 'en') + # req.body # => "q=ruby&q=perl&lang=en" + # + # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']]) + # req.body # => "q=ruby&q=perl&lang=en" + # + # With string argument +sep+ also given, + # uses that string as the separator: + # + # req.set_form_data({q: 'ruby', lang: 'en'}, '|') + # req.body # => "q=ruby|lang=en" + # + # Gem::Net::HTTPHeader#form_data= is an alias for Gem::Net::HTTPHeader#set_form_data. + def set_form_data(params, sep = '&') + query = Gem::URI.encode_www_form(params) + query.gsub!(/&/, sep) if sep != '&' + self.body = query + self.content_type = 'application/x-www-form-urlencoded' + end + + alias form_data= set_form_data + + # Stores form data to be used in a +POST+ or +PUT+ request. + # + # The form data given in +params+ consists of zero or more fields; + # each field is: + # + # - A scalar value. + # - A name/value pair. + # - An IO stream opened for reading. + # + # Argument +params+ should be an + # {Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-Enumerable+in+Ruby+Classes] + # (method <tt>params.map</tt> will be called), + # and is often an array or hash. + # + # First, we set up a request: + # + # _uri = uri.dup + # _uri.path ='/posts' + # req = Gem::Net::HTTP::Post.new(_uri) + # + # <b>Argument +params+ As an Array</b> + # + # When +params+ is an array, + # each of its elements is a subarray that defines a field; + # the subarray may contain: + # + # - One string: + # + # req.set_form([['foo'], ['bar'], ['baz']]) + # + # - Two strings: + # + # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]]) + # + # - When argument +enctype+ (see below) is given as + # <tt>'multipart/form-data'</tt>: + # + # - A string name and an IO stream opened for reading: + # + # require 'stringio' + # req.set_form([['file', StringIO.new('Ruby is cool.')]]) + # + # - A string name, an IO stream opened for reading, + # and an options hash, which may contain these entries: + # + # - +:filename+: The name of the file to use. + # - +:content_type+: The content type of the uploaded file. + # + # Example: + # + # req.set_form([['file', file, {filename: "other-filename.foo"}]] + # + # The various forms may be mixed: + # + # req.set_form(['foo', %w[bar 1], ['file', file]]) + # + # <b>Argument +params+ As a Hash</b> + # + # When +params+ is a hash, + # each of its entries is a name/value pair that defines a field: + # + # - The name is a string. + # - The value may be: + # + # - +nil+. + # - Another string. + # - An IO stream opened for reading + # (only when argument +enctype+ -- see below -- is given as + # <tt>'multipart/form-data'</tt>). + # + # Examples: + # + # # Nil-valued fields. + # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil}) + # + # # String-valued fields. + # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2}) + # + # # IO-valued field. + # require 'stringio' + # req.set_form({'file' => StringIO.new('Ruby is cool.')}) + # + # # Mixture of fields. + # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file}) + # + # Optional argument +enctype+ specifies the value to be given + # to field <tt>'Content-Type'</tt>, and must be one of: + # + # - <tt>'application/x-www-form-urlencoded'</tt> (the default). + # - <tt>'multipart/form-data'</tt>; + # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578]. + # + # Optional argument +formopt+ is a hash of options + # (applicable only when argument +enctype+ + # is <tt>'multipart/form-data'</tt>) + # that may include the following entries: + # + # - +:boundary+: The value is the boundary string for the multipart message. + # If not given, the boundary is a random string. + # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1]. + # - +:charset+: Value is the character set for the form submission. + # Field names and values of non-file fields should be encoded with this charset. + # + def set_form(params, enctype='application/x-www-form-urlencoded', formopt={}) + @body_data = params + @body = nil + @body_stream = nil + @form_option = formopt + case enctype + when /\Aapplication\/x-www-form-urlencoded\z/i, + /\Amultipart\/form-data\z/i + self.content_type = enctype + else + raise ArgumentError, "invalid enctype: #{enctype}" + end + end + + # Sets header <tt>'Authorization'</tt> using the given + # +account+ and +password+ strings: + # + # req.basic_auth('my_account', 'my_password') + # req['Authorization'] + # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" + # + def basic_auth(account, password) + @header['authorization'] = [basic_encode(account, password)] + end + + # Sets header <tt>'Proxy-Authorization'</tt> using the given + # +account+ and +password+ strings: + # + # req.proxy_basic_auth('my_account', 'my_password') + # req['Proxy-Authorization'] + # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA==" + # + def proxy_basic_auth(account, password) + @header['proxy-authorization'] = [basic_encode(account, password)] + end + + def basic_encode(account, password) # :nodoc: + 'Basic ' + ["#{account}:#{password}"].pack('m0') + end + private :basic_encode + + # Returns whether the HTTP session is to be closed. + def connection_close? + token = /(?:\A|,)\s*close\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + + # Returns whether the HTTP session is to be kept alive. + def connection_keep_alive? + token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i + @header['connection']&.grep(token) {return true} + @header['proxy-connection']&.grep(token) {return true} + false + end + +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb new file mode 100644 index 0000000000..137295a883 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +module Gem::Net::HTTP::ProxyDelta #:nodoc: internal use only + private + + def conn_address + proxy_address() + end + + def conn_port + proxy_port() + end + + def edit_path(path) + use_ssl? ? path : "http://#{addr_port()}#{path}" + end +end + diff --git a/lib/rubygems/vendor/net-http/lib/net/http/request.rb b/lib/rubygems/vendor/net-http/lib/net/http/request.rb new file mode 100644 index 0000000000..495ec9be54 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/request.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +# This class is the base class for \Gem::Net::HTTP request classes. +# The class should not be used directly; +# instead you should use its subclasses, listed below. +# +# == Creating a Request +# +# An request object may be created with either a Gem::URI or a string hostname: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('https://jsonplaceholder.typicode.com/') +# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET> +# req = Gem::Net::HTTP::Get.new(uri.hostname) # => #<Gem::Net::HTTP::Get GET> +# +# And with any of the subclasses: +# +# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD> +# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST> +# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT> +# # ... +# +# The new instance is suitable for use as the argument to Gem::Net::HTTP#request. +# +# == Request Headers +# +# A new request object has these header fields by default: +# +# req.to_hash +# # => +# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"], +# "accept"=>["*/*"], +# "user-agent"=>["Ruby"], +# "host"=>["jsonplaceholder.typicode.com"]} +# +# See: +# +# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding] +# and {Compression and Decompression}[rdoc-ref:Gem::Net::HTTP@Compression+and+Decompression]. +# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header]. +# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header]. +# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header]. +# +# You can add headers or override default headers: +# +# # res = Gem::Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'}) +# +# This class (and therefore its subclasses) also includes (indirectly) +# module Gem::Net::HTTPHeader, which gives access to its +# {methods for setting headers}[rdoc-ref:Gem::Net::HTTPHeader@Setters]. +# +# == Request Subclasses +# +# Subclasses for HTTP requests: +# +# - Gem::Net::HTTP::Get +# - Gem::Net::HTTP::Head +# - Gem::Net::HTTP::Post +# - Gem::Net::HTTP::Put +# - Gem::Net::HTTP::Delete +# - Gem::Net::HTTP::Options +# - Gem::Net::HTTP::Trace +# - Gem::Net::HTTP::Patch +# +# Subclasses for WebDAV requests: +# +# - Gem::Net::HTTP::Propfind +# - Gem::Net::HTTP::Proppatch +# - Gem::Net::HTTP::Mkcol +# - Gem::Net::HTTP::Copy +# - Gem::Net::HTTP::Move +# - Gem::Net::HTTP::Lock +# - Gem::Net::HTTP::Unlock +# +class Gem::Net::HTTPRequest < Gem::Net::HTTPGenericRequest + # Creates an HTTP request object for +path+. + # + # +initheader+ are the default headers to use. Gem::Net::HTTP adds + # Accept-Encoding to enable compression of the response body unless + # Accept-Encoding or Range are supplied in +initheader+. + + def initialize(path, initheader = nil) + super self.class::METHOD, + self.class::REQUEST_HAS_BODY, + self.class::RESPONSE_HAS_BODY, + path, initheader + end +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/requests.rb b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb new file mode 100644 index 0000000000..f990761042 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb @@ -0,0 +1,444 @@ +# frozen_string_literal: true + +# HTTP/1.1 methods --- RFC2616 + +# \Class for representing +# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Gem::Net::HTTP.get: sends +GET+ request, returns response body. +# - Gem::Net::HTTP#get: sends +GET+ request, returns response object. +# +class Gem::Net::HTTP::Get < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'GET' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: no. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Gem::Net::HTTP#head: sends +HEAD+ request, returns response object. +# +class Gem::Net::HTTP::Head < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'HEAD' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = false +end + +# \Class for representing +# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes. +# +# Related: +# +# - Gem::Net::HTTP.post: sends +POST+ request, returns response object. +# - Gem::Net::HTTP#post: sends +POST+ request, returns response object. +# +class Gem::Net::HTTP::Post < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'POST' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Gem::Net::HTTP.put: sends +PUT+ request, returns response object. +# - Gem::Net::HTTP#put: sends +PUT+ request, returns response object. +# +class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'PUT' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts/1' +# req = Gem::Net::HTTP::Delete.new(uri) # => #<Gem::Net::HTTP::Delete DELETE> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Gem::Net::HTTP#delete: sends +DELETE+ request, returns response object. +# +class Gem::Net::HTTP::Delete < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'DELETE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Options.new(uri) # => #<Gem::Net::HTTP::Options OPTIONS> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: optional. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Gem::Net::HTTP#options: sends +OPTIONS+ request, returns response object. +# +class Gem::Net::HTTP::Options < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'OPTIONS' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Trace.new(uri) # => #<Gem::Net::HTTP::Trace TRACE> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: no. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Gem::Net::HTTP#trace: sends +TRACE+ request, returns response object. +# +class Gem::Net::HTTP::Trace < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'TRACE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# uri.path = '/posts' +# req = Gem::Net::HTTP::Patch.new(uri) # => #<Gem::Net::HTTP::Patch PATCH> +# req.body = '{"title": "foo","body": "bar","userId": 1}' +# req.content_type = 'application/json' +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Properties: +# +# - Request body: yes. +# - Response body: yes. +# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no. +# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no. +# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no. +# +# Related: +# +# - Gem::Net::HTTP#patch: sends +PATCH+ request, returns response object. +# +class Gem::Net::HTTP::Patch < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'PATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# +# WebDAV methods --- RFC2518 +# + +# \Class for representing +# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Propfind.new(uri) # => #<Gem::Net::HTTP::Propfind PROPFIND> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#propfind: sends +PROPFIND+ request, returns response object. +# +class Gem::Net::HTTP::Propfind < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'PROPFIND' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Proppatch.new(uri) # => #<Gem::Net::HTTP::Proppatch PROPPATCH> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object. +# +class Gem::Net::HTTP::Proppatch < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'PROPPATCH' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Mkcol.new(uri) # => #<Gem::Net::HTTP::Mkcol MKCOL> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#mkcol: sends +MKCOL+ request, returns response object. +# +class Gem::Net::HTTP::Mkcol < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'MKCOL' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Copy.new(uri) # => #<Gem::Net::HTTP::Copy COPY> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#copy: sends +COPY+ request, returns response object. +# +class Gem::Net::HTTP::Copy < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'COPY' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Move.new(uri) # => #<Gem::Net::HTTP::Move MOVE> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#move: sends +MOVE+ request, returns response object. +# +class Gem::Net::HTTP::Move < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'MOVE' + REQUEST_HAS_BODY = false + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Lock.new(uri) # => #<Gem::Net::HTTP::Lock LOCK> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#lock: sends +LOCK+ request, returns response object. +# +class Gem::Net::HTTP::Lock < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'LOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end + +# \Class for representing +# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]: +# +# require 'rubygems/vendor/net-http/lib/net/http' +# uri = Gem::URI('http://example.com') +# hostname = uri.hostname # => "example.com" +# req = Gem::Net::HTTP::Unlock.new(uri) # => #<Gem::Net::HTTP::Unlock UNLOCK> +# res = Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end +# +# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers]. +# +# Related: +# +# - Gem::Net::HTTP#unlock: sends +UNLOCK+ request, returns response object. +# +class Gem::Net::HTTP::Unlock < Gem::Net::HTTPRequest + # :stopdoc: + METHOD = 'UNLOCK' + REQUEST_HAS_BODY = true + RESPONSE_HAS_BODY = true +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/response.rb b/lib/rubygems/vendor/net-http/lib/net/http/response.rb new file mode 100644 index 0000000000..dc164f1504 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/response.rb @@ -0,0 +1,739 @@ +# frozen_string_literal: true + +# This class is the base class for \Gem::Net::HTTP response classes. +# +# == About the Examples +# +# :include: doc/net-http/examples.rdoc +# +# == Returned Responses +# +# \Method Gem::Net::HTTP.get_response returns +# an instance of one of the subclasses of \Gem::Net::HTTPResponse: +# +# Gem::Net::HTTP.get_response(uri) +# # => #<Gem::Net::HTTPOK 200 OK readbody=true> +# Gem::Net::HTTP.get_response(hostname, '/nosuch') +# # => #<Gem::Net::HTTPNotFound 404 Not Found readbody=true> +# +# As does method Gem::Net::HTTP#request: +# +# req = Gem::Net::HTTP::Get.new(uri) +# Gem::Net::HTTP.start(hostname) do |http| +# http.request(req) +# end # => #<Gem::Net::HTTPOK 200 OK readbody=true> +# +# \Class \Gem::Net::HTTPResponse includes module Gem::Net::HTTPHeader, +# which provides access to response header values via (among others): +# +# - \Hash-like method <tt>[]</tt>. +# - Specific reader methods, such as +content_type+. +# +# Examples: +# +# res = Gem::Net::HTTP.get_response(uri) # => #<Gem::Net::HTTPOK 200 OK readbody=true> +# res['Content-Type'] # => "text/html; charset=UTF-8" +# res.content_type # => "text/html" +# +# == Response Subclasses +# +# \Class \Gem::Net::HTTPResponse has a subclass for each +# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes]. +# You can look up the response class for a given code: +# +# Gem::Net::HTTPResponse::CODE_TO_OBJ['200'] # => Gem::Net::HTTPOK +# Gem::Net::HTTPResponse::CODE_TO_OBJ['400'] # => Gem::Net::HTTPBadRequest +# Gem::Net::HTTPResponse::CODE_TO_OBJ['404'] # => Gem::Net::HTTPNotFound +# +# And you can retrieve the status code for a response object: +# +# Gem::Net::HTTP.get_response(uri).code # => "200" +# Gem::Net::HTTP.get_response(hostname, '/nosuch').code # => "404" +# +# The response subclasses (indentation shows class hierarchy): +# +# - Gem::Net::HTTPUnknownResponse (for unhandled \HTTP extensions). +# +# - Gem::Net::HTTPInformation: +# +# - Gem::Net::HTTPContinue (100) +# - Gem::Net::HTTPSwitchProtocol (101) +# - Gem::Net::HTTPProcessing (102) +# - Gem::Net::HTTPEarlyHints (103) +# +# - Gem::Net::HTTPSuccess: +# +# - Gem::Net::HTTPOK (200) +# - Gem::Net::HTTPCreated (201) +# - Gem::Net::HTTPAccepted (202) +# - Gem::Net::HTTPNonAuthoritativeInformation (203) +# - Gem::Net::HTTPNoContent (204) +# - Gem::Net::HTTPResetContent (205) +# - Gem::Net::HTTPPartialContent (206) +# - Gem::Net::HTTPMultiStatus (207) +# - Gem::Net::HTTPAlreadyReported (208) +# - Gem::Net::HTTPIMUsed (226) +# +# - Gem::Net::HTTPRedirection: +# +# - Gem::Net::HTTPMultipleChoices (300) +# - Gem::Net::HTTPMovedPermanently (301) +# - Gem::Net::HTTPFound (302) +# - Gem::Net::HTTPSeeOther (303) +# - Gem::Net::HTTPNotModified (304) +# - Gem::Net::HTTPUseProxy (305) +# - Gem::Net::HTTPTemporaryRedirect (307) +# - Gem::Net::HTTPPermanentRedirect (308) +# +# - Gem::Net::HTTPClientError: +# +# - Gem::Net::HTTPBadRequest (400) +# - Gem::Net::HTTPUnauthorized (401) +# - Gem::Net::HTTPPaymentRequired (402) +# - Gem::Net::HTTPForbidden (403) +# - Gem::Net::HTTPNotFound (404) +# - Gem::Net::HTTPMethodNotAllowed (405) +# - Gem::Net::HTTPNotAcceptable (406) +# - Gem::Net::HTTPProxyAuthenticationRequired (407) +# - Gem::Net::HTTPRequestTimeOut (408) +# - Gem::Net::HTTPConflict (409) +# - Gem::Net::HTTPGone (410) +# - Gem::Net::HTTPLengthRequired (411) +# - Gem::Net::HTTPPreconditionFailed (412) +# - Gem::Net::HTTPRequestEntityTooLarge (413) +# - Gem::Net::HTTPRequestURITooLong (414) +# - Gem::Net::HTTPUnsupportedMediaType (415) +# - Gem::Net::HTTPRequestedRangeNotSatisfiable (416) +# - Gem::Net::HTTPExpectationFailed (417) +# - Gem::Net::HTTPMisdirectedRequest (421) +# - Gem::Net::HTTPUnprocessableEntity (422) +# - Gem::Net::HTTPLocked (423) +# - Gem::Net::HTTPFailedDependency (424) +# - Gem::Net::HTTPUpgradeRequired (426) +# - Gem::Net::HTTPPreconditionRequired (428) +# - Gem::Net::HTTPTooManyRequests (429) +# - Gem::Net::HTTPRequestHeaderFieldsTooLarge (431) +# - Gem::Net::HTTPUnavailableForLegalReasons (451) +# +# - Gem::Net::HTTPServerError: +# +# - Gem::Net::HTTPInternalServerError (500) +# - Gem::Net::HTTPNotImplemented (501) +# - Gem::Net::HTTPBadGateway (502) +# - Gem::Net::HTTPServiceUnavailable (503) +# - Gem::Net::HTTPGatewayTimeOut (504) +# - Gem::Net::HTTPVersionNotSupported (505) +# - Gem::Net::HTTPVariantAlsoNegotiates (506) +# - Gem::Net::HTTPInsufficientStorage (507) +# - Gem::Net::HTTPLoopDetected (508) +# - Gem::Net::HTTPNotExtended (510) +# - Gem::Net::HTTPNetworkAuthenticationRequired (511) +# +# There is also the Gem::Net::HTTPBadResponse exception which is raised when +# there is a protocol error. +# +class Gem::Net::HTTPResponse + class << self + # true if the response has a body. + def body_permitted? + self::HAS_BODY + end + + def exception_type # :nodoc: internal use only + self::EXCEPTION_TYPE + end + + def read_new(sock) #:nodoc: internal use only + httpv, code, msg = read_status_line(sock) + res = response_class(code).new(httpv, code, msg) + each_response_header(sock) do |k,v| + res.add_field k, v + end + res + end + + private + # :stopdoc: + + def read_status_line(sock) + str = sock.readline + m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or + raise Gem::Net::HTTPBadResponse, "wrong status line: #{str.dump}" + m.captures + end + + def response_class(code) + CODE_TO_OBJ[code] or + CODE_CLASS_TO_OBJ[code[0,1]] or + Gem::Net::HTTPUnknownResponse + end + + def each_response_header(sock) + key = value = nil + while true + line = sock.readuntil("\n", true).sub(/\s+\z/, '') + break if line.empty? + if line[0] == ?\s or line[0] == ?\t and value + value << ' ' unless value.empty? + value << line.strip + else + yield key, value if key + key, value = line.strip.split(/\s*:\s*/, 2) + raise Gem::Net::HTTPBadResponse, 'wrong header line format' if value.nil? + end + end + yield key, value if key + end + end + + # next is to fix bug in RDoc, where the private inside class << self + # spills out. + public + + include Gem::Net::HTTPHeader + + def initialize(httpv, code, msg) #:nodoc: internal use only + @http_version = httpv + @code = code + @message = msg + initialize_http_header nil + @body = nil + @read = false + @uri = nil + @decode_content = false + @body_encoding = false + @ignore_eof = true + end + + # The HTTP version supported by the server. + attr_reader :http_version + + # The HTTP result code string. For example, '302'. You can also + # determine the response type by examining which response subclass + # the response object is an instance of. + attr_reader :code + + # The HTTP result message sent by the server. For example, 'Not Found'. + attr_reader :message + alias msg message # :nodoc: obsolete + + # The Gem::URI used to fetch this response. The response Gem::URI is only available + # if a Gem::URI was used to create the request. + attr_reader :uri + + # Set to true automatically when the request did not contain an + # Accept-Encoding header from the user. + attr_accessor :decode_content + + # Returns the value set by body_encoding=, or +false+ if none; + # see #body_encoding=. + attr_reader :body_encoding + + # Sets the encoding that should be used when reading the body: + # + # - If the given value is an Encoding object, that encoding will be used. + # - Otherwise if the value is a string, the value of + # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find] + # will be used. + # - Otherwise an encoding will be deduced from the body itself. + # + # Examples: + # + # http = Gem::Net::HTTP.new(hostname) + # req = Gem::Net::HTTP::Get.new('/') + # + # http.request(req) do |res| + # p res.body.encoding # => #<Encoding:ASCII-8BIT> + # end + # + # http.request(req) do |res| + # res.body_encoding = "UTF-8" + # p res.body.encoding # => #<Encoding:UTF-8> + # end + # + def body_encoding=(value) + value = Encoding.find(value) if value.is_a?(String) + @body_encoding = value + end + + # Whether to ignore EOF when reading bodies with a specified Content-Length + # header. + attr_accessor :ignore_eof + + def inspect # :nodoc: + "#<#{self.class} #{@code} #{@message} readbody=#{@read}>" + end + + # + # response <-> exception relationship + # + + def code_type #:nodoc: + self.class + end + + def error! #:nodoc: + message = @code + message = "#{message} #{@message.dump}" if @message + raise error_type().new(message, self) + end + + def error_type #:nodoc: + self.class::EXCEPTION_TYPE + end + + # Raises an HTTP error if the response is not 2xx (success). + def value + error! unless self.kind_of?(Gem::Net::HTTPSuccess) + end + + def uri= uri # :nodoc: + @uri = uri.dup if uri + end + + # + # header (for backward compatibility only; DO NOT USE) + # + + def response #:nodoc: + warn "Gem::Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE + self + end + + def header #:nodoc: + warn "Gem::Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE + self + end + + def read_header #:nodoc: + warn "Gem::Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE + self + end + + # + # body + # + + def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only + @socket = sock + @body_exist = reqmethodallowbody && self.class.body_permitted? + begin + yield + self.body # ensure to read body + ensure + @socket = nil + end + end + + # Gets the entity body returned by the remote HTTP server. + # + # If a block is given, the body is passed to the block, and + # the body is provided in fragments, as it is read in from the socket. + # + # If +dest+ argument is given, response is read into that variable, + # with <code>dest#<<</code> method (it could be String or IO, or any + # other object responding to <code><<</code>). + # + # Calling this method a second or subsequent time for the same + # HTTPResponse object will return the value already read. + # + # http.request_get('/index.html') {|res| + # puts res.read_body + # } + # + # http.request_get('/index.html') {|res| + # p res.read_body.object_id # 538149362 + # p res.read_body.object_id # 538149362 + # } + # + # # using iterator + # http.request_get('/index.html') {|res| + # res.read_body do |segment| + # print segment + # end + # } + # + def read_body(dest = nil, &block) + if @read + raise IOError, "#{self.class}\#read_body called twice" if dest or block + return @body + end + to = procdest(dest, block) + stream_check + if @body_exist + read_body_0 to + @body = to + else + @body = nil + end + @read = true + return if @body.nil? + + case enc = @body_encoding + when Encoding, false, nil + # Encoding: force given encoding + # false/nil: do not force encoding + else + # other value: detect encoding from body + enc = detect_encoding(@body) + end + + @body.force_encoding(enc) if enc + + @body + end + + # Returns the string response body; + # note that repeated calls for the unmodified body return a cached string: + # + # path = '/todos/1' + # Gem::Net::HTTP.start(hostname) do |http| + # res = http.get(path) + # p res.body + # p http.head(path).body # No body. + # end + # + # Output: + # + # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}" + # nil + # + def body + read_body() + end + + # Sets the body of the response to the given value. + def body=(value) + @body = value + end + + alias entity body #:nodoc: obsolete + + private + + # :nodoc: + def detect_encoding(str, encoding=nil) + if encoding + elsif encoding = type_params['charset'] + elsif encoding = check_bom(str) + else + encoding = case content_type&.downcase + when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml} + /\A<xml[ \t\r\n]+ + version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+ + encoding[ \t\r\n]*=[ \t\r\n]* + (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str + encoding = $1 || $2 || Encoding::UTF_8 + when %r{text/html.*} + sniff_encoding(str) + end + end + return encoding + end + + # :nodoc: + def sniff_encoding(str, encoding=nil) + # the encoding sniffing algorithm + # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding + if enc = scanning_meta(str) + enc + # 6. last visited page or something + # 7. frequency + elsif str.ascii_only? + Encoding::US_ASCII + elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding? + Encoding::UTF_8 + end + # 8. implementation-defined or user-specified + end + + # :nodoc: + def check_bom(str) + case str.byteslice(0, 2) + when "\xFE\xFF" + return Encoding::UTF_16BE + when "\xFF\xFE" + return Encoding::UTF_16LE + end + if "\xEF\xBB\xBF" == str.byteslice(0, 3) + return Encoding::UTF_8 + end + nil + end + + # :nodoc: + def scanning_meta(str) + require 'strscan' + ss = StringScanner.new(str) + if ss.scan_until(/<meta[\t\n\f\r ]*/) + attrs = {} # attribute_list + got_pragma = false + need_pragma = nil + charset = nil + + # step: Attributes + while attr = get_attribute(ss) + name, value = *attr + next if attrs[name] + attrs[name] = true + case name + when 'http-equiv' + got_pragma = true if value == 'content-type' + when 'content' + encoding = extracting_encodings_from_meta_elements(value) + unless charset + charset = encoding + end + need_pragma = true + when 'charset' + need_pragma = false + charset = value + end + end + + # step: Processing + return if need_pragma.nil? + return if need_pragma && !got_pragma + + charset = Encoding.find(charset) rescue nil + return unless charset + charset = Encoding::UTF_8 if charset == Encoding::UTF_16 + return charset # tentative + end + nil + end + + def get_attribute(ss) + ss.scan(/[\t\n\f\r \/]*/) + if ss.peek(1) == '>' + ss.getch + return nil + end + name = ss.scan(/[^=\t\n\f\r \/>]*/) + name.downcase! + raise if name.empty? + ss.skip(/[\t\n\f\r ]*/) + if ss.getch != '=' + value = '' + return [name, value] + end + ss.skip(/[\t\n\f\r ]*/) + case ss.peek(1) + when '"' + ss.getch + value = ss.scan(/[^"]+/) + value.downcase! + ss.getch + when "'" + ss.getch + value = ss.scan(/[^']+/) + value.downcase! + ss.getch + when '>' + value = '' + else + value = ss.scan(/[^\t\n\f\r >]+/) + value.downcase! + end + [name, value] + end + + def extracting_encodings_from_meta_elements(value) + # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element + if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value + return $1 || $2 || $3 + end + return nil + end + + ## + # Checks for a supported Content-Encoding header and yields an Inflate + # wrapper for this response's socket when zlib is present. If the + # Content-Encoding is not supported or zlib is missing, the plain socket is + # yielded. + # + # If a Content-Range header is present, a plain socket is yielded as the + # bytes in the range may not be a complete deflate block. + + def inflater # :nodoc: + return yield @socket unless Gem::Net::HTTP::HAVE_ZLIB + return yield @socket unless @decode_content + return yield @socket if self['content-range'] + + v = self['content-encoding'] + case v&.downcase + when 'deflate', 'gzip', 'x-gzip' then + self.delete 'content-encoding' + + inflate_body_io = Inflater.new(@socket) + + begin + yield inflate_body_io + success = true + ensure + begin + inflate_body_io.finish + if self['content-length'] + self['content-length'] = inflate_body_io.bytes_inflated.to_s + end + rescue => err + # Ignore #finish's error if there is an exception from yield + raise err if success + end + end + when 'none', 'identity' then + self.delete 'content-encoding' + + yield @socket + else + yield @socket + end + end + + def read_body_0(dest) + inflater do |inflate_body_io| + if chunked? + read_chunked dest, inflate_body_io + return + end + + @socket = inflate_body_io + + clen = content_length() + if clen + @socket.read clen, dest, @ignore_eof + return + end + clen = range_length() + if clen + @socket.read clen, dest + return + end + @socket.read_all dest + end + end + + ## + # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF, + # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip + # encoded. + # + # See RFC 2616 section 3.6.1 for definitions + + def read_chunked(dest, chunk_data_io) # :nodoc: + total = 0 + while true + line = @socket.readline + hexlen = line.slice(/[0-9a-fA-F]+/) or + raise Gem::Net::HTTPBadResponse, "wrong chunk size line: #{line}" + len = hexlen.hex + break if len == 0 + begin + chunk_data_io.read len, dest + ensure + total += len + @socket.read 2 # \r\n + end + end + until @socket.readline.empty? + # none + end + end + + def stream_check + raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed? + end + + def procdest(dest, block) + raise ArgumentError, 'both arg and block given for HTTP method' if + dest and block + if block + Gem::Net::ReadAdapter.new(block) + else + dest || +'' + end + end + + ## + # Inflater is a wrapper around Gem::Net::BufferedIO that transparently inflates + # zlib and gzip streams. + + class Inflater # :nodoc: + + ## + # Creates a new Inflater wrapping +socket+ + + def initialize socket + @socket = socket + # zlib with automatic gzip detection + @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS) + end + + ## + # Finishes the inflate stream. + + def finish + return if @inflate.total_in == 0 + @inflate.finish + end + + ## + # The number of bytes inflated, used to update the Content-Length of + # the response. + + def bytes_inflated + @inflate.total_out + end + + ## + # Returns a Gem::Net::ReadAdapter that inflates each read chunk into +dest+. + # + # This allows a large response body to be inflated without storing the + # entire body in memory. + + def inflate_adapter(dest) + if dest.respond_to?(:set_encoding) + dest.set_encoding(Encoding::ASCII_8BIT) + elsif dest.respond_to?(:force_encoding) + dest.force_encoding(Encoding::ASCII_8BIT) + end + block = proc do |compressed_chunk| + @inflate.inflate(compressed_chunk) do |chunk| + compressed_chunk.clear + dest << chunk + end + end + + Gem::Net::ReadAdapter.new(block) + end + + ## + # Reads +clen+ bytes from the socket, inflates them, then writes them to + # +dest+. +ignore_eof+ is passed down to Gem::Net::BufferedIO#read + # + # Unlike Gem::Net::BufferedIO#read, this method returns more than +clen+ bytes. + # At this time there is no way for a user of Gem::Net::HTTPResponse to read a + # specific number of bytes from the HTTP response body, so this internal + # API does not return the same number of bytes as were requested. + # + # See https://bugs.ruby-lang.org/issues/6492 for further discussion. + + def read clen, dest, ignore_eof = false + temp_dest = inflate_adapter(dest) + + @socket.read clen, temp_dest, ignore_eof + end + + ## + # Reads the rest of the socket, inflates it, then writes it to +dest+. + + def read_all dest + temp_dest = inflate_adapter(dest) + + @socket.read_all temp_dest + end + + end + +end + diff --git a/lib/rubygems/vendor/net-http/lib/net/http/responses.rb b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb new file mode 100644 index 0000000000..62ce1cba1b --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb @@ -0,0 +1,1242 @@ +# frozen_string_literal: true +#-- +# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +module Gem::Net + + # Unknown HTTP response + class HTTPUnknownResponse < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for informational (1xx) HTTP response classes. + # + # An informational response indicates that the request was received and understood. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.1xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response]. + # + class HTTPInformation < HTTPResponse + # :stopdoc: + HAS_BODY = false + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for success (2xx) HTTP response classes. + # + # A success response indicates the action requested by the client + # was received, understood, and accepted. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.2xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success]. + # + class HTTPSuccess < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPError # + end + + # Parent class for redirection (3xx) HTTP response classes. + # + # A redirection response indicates the client must take additional action + # to complete the request. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.3xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection]. + # + class HTTPRedirection < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPRetriableError # + end + + # Parent class for client error (4xx) HTTP response classes. + # + # A client error response indicates that the client may have caused an error. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.4xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors]. + # + class HTTPClientError < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPClientException # + end + + # Parent class for server error (5xx) HTTP response classes. + # + # A server error response indicates that the server failed to fulfill a request. + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.5xx]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors]. + # + class HTTPServerError < HTTPResponse + # :stopdoc: + HAS_BODY = true + EXCEPTION_TYPE = HTTPFatalError # + end + + # Response class for +Continue+ responses (status code 100). + # + # A +Continue+ response indicates that the server has received the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-100-continue]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100]. + # + class HTTPContinue < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Switching Protocol</tt> responses (status code 101). + # + # The <tt>Switching Protocol<tt> response indicates that the server has received + # a request to switch protocols, and has agreed to do so. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101]. + # + class HTTPSwitchProtocol < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for +Processing+ responses (status code 102). + # + # The +Processing+ response indicates that the server has received + # and is processing the request, but no response is available yet. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 2518}[https://www.rfc-editor.org/rfc/rfc2518#section-10.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102]. + # + class HTTPProcessing < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Early Hints</tt> responses (status code 103). + # + # The <tt>Early Hints</tt> indicates that the server has received + # and is processing the request, and contains certain headers; + # the final response is not available yet. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103]. + # - {RFC 8297}[https://www.rfc-editor.org/rfc/rfc8297.html#section-2]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103]. + # + class HTTPEarlyHints < HTTPInformation + # :stopdoc: + HAS_BODY = false + end + + # Response class for +OK+ responses (status code 200). + # + # The +OK+ response indicates that the server has received + # a request and has responded successfully. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200]. + # + class HTTPOK < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for +Created+ responses (status code 201). + # + # The +Created+ response indicates that the server has received + # and has fulfilled a request to create a new resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-201-created]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201]. + # + class HTTPCreated < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for +Accepted+ responses (status code 202). + # + # The +Accepted+ response indicates that the server has received + # and is processing a request, but the processing has not yet been completed. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202]. + # + class HTTPAccepted < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Non-Authoritative Information</tt> responses (status code 203). + # + # The <tt>Non-Authoritative Information</tt> response indicates that the server + # is a transforming proxy (such as a Web accelerator) + # that received a 200 OK response from its origin, + # and is returning a modified version of the origin's response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-203-non-authoritative-infor]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203]. + # + class HTTPNonAuthoritativeInformation < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>No Content</tt> responses (status code 204). + # + # The <tt>No Content</tt> response indicates that the server + # successfully processed the request, and is not returning any content. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204]. + # + class HTTPNoContent < HTTPSuccess + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Reset Content</tt> responses (status code 205). + # + # The <tt>Reset Content</tt> response indicates that the server + # successfully processed the request, + # asks that the client reset its document view, and is not returning any content. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-205-reset-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205]. + # + class HTTPResetContent < HTTPSuccess + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Partial Content</tt> responses (status code 206). + # + # The <tt>Partial Content</tt> response indicates that the server is delivering + # only part of the resource (byte serving) + # due to a Range header in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206]. + # + class HTTPPartialContent < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Multi-Status (WebDAV)</tt> responses (status code 207). + # + # The <tt>Multi-Status (WebDAV)</tt> response indicates that the server + # has received the request, + # and that the message body can contain a number of separate response codes. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4818}[https://www.rfc-editor.org/rfc/rfc4918#section-11.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207]. + # + class HTTPMultiStatus < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Already Reported (WebDAV)</tt> responses (status code 208). + # + # The <tt>Already Reported (WebDAV)</tt> response indicates that the server + # has received the request, + # and that the members of a DAV binding have already been enumerated + # in a preceding part of the (multi-status) response, + # and are not being included again. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 5842}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208]. + # + class HTTPAlreadyReported < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>IM Used</tt> responses (status code 226). + # + # The <tt>IM Used</tt> response indicates that the server has fulfilled a request + # for the resource, and the response is a representation of the result + # of one or more instance-manipulations applied to the current instance. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 3229}[https://www.rfc-editor.org/rfc/rfc3229.html#section-10.4.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226]. + # + class HTTPIMUsed < HTTPSuccess + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Multiple Choices</tt> responses (status code 300). + # + # The <tt>Multiple Choices</tt> response indicates that the server + # offers multiple options for the resource from which the client may choose. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-300-multiple-choices]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300]. + # + class HTTPMultipleChoices < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + HTTPMultipleChoice = HTTPMultipleChoices + + # Response class for <tt>Moved Permanently</tt> responses (status code 301). + # + # The <tt>Moved Permanently</tt> response indicates that links or records + # returning this response should be updated to use the given URL. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-301-moved-permanently]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301]. + # + class HTTPMovedPermanently < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Found</tt> responses (status code 302). + # + # The <tt>Found</tt> response indicates that the client + # should look at (browse to) another URL. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-302-found]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302]. + # + class HTTPFound < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + HTTPMovedTemporarily = HTTPFound + + # Response class for <tt>See Other</tt> responses (status code 303). + # + # The response to the request can be found under another Gem::URI using the GET method. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303]. + # + class HTTPSeeOther < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Modified</tt> responses (status code 304). + # + # Indicates that the resource has not been modified since the version + # specified by the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304]. + # + class HTTPNotModified < HTTPRedirection + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Use Proxy</tt> responses (status code 305). + # + # The requested resource is available only through a proxy, + # whose address is provided in the response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-305-use-proxy]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305]. + # + class HTTPUseProxy < HTTPRedirection + # :stopdoc: + HAS_BODY = false + end + + # Response class for <tt>Temporary Redirect</tt> responses (status code 307). + # + # The request should be repeated with another Gem::URI; + # however, future requests should still use the original Gem::URI. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-307-temporary-redirect]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307]. + # + class HTTPTemporaryRedirect < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Permanent Redirect</tt> responses (status code 308). + # + # This and all future requests should be directed to the given Gem::URI. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-308-permanent-redirect]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308]. + # + class HTTPPermanentRedirect < HTTPRedirection + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Bad Request</tt> responses (status code 400). + # + # The server cannot or will not process the request due to an apparent client error. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400]. + # + class HTTPBadRequest < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unauthorized</tt> responses (status code 401). + # + # Authentication is required, but either was not provided or failed. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401]. + # + class HTTPUnauthorized < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Payment Required</tt> responses (status code 402). + # + # Reserved for future use. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402]. + # + class HTTPPaymentRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Forbidden</tt> responses (status code 403). + # + # The request contained valid data and was understood by the server, + # but the server is refusing action. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403]. + # + class HTTPForbidden < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Found</tt> responses (status code 404). + # + # The requested resource could not be found but may be available in the future. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404]. + # + class HTTPNotFound < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Method Not Allowed</tt> responses (status code 405). + # + # The request method is not supported for the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-405-method-not-allowed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405]. + # + class HTTPMethodNotAllowed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Acceptable</tt> responses (status code 406). + # + # The requested resource is capable of generating only content + # that not acceptable according to the Accept headers sent in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-406-not-acceptable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406]. + # + class HTTPNotAcceptable < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Proxy Authentication Required</tt> responses (status code 407). + # + # The client must first authenticate itself with the proxy. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-re]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407]. + # + class HTTPProxyAuthenticationRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Request Gem::Timeout</tt> responses (status code 408). + # + # The server timed out waiting for the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-408-request-timeout]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408]. + # + class HTTPRequestTimeout < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestTimeOut = HTTPRequestTimeout + + # Response class for <tt>Conflict</tt> responses (status code 409). + # + # The request could not be processed because of conflict in the current state of the resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409]. + # + class HTTPConflict < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Gone</tt> responses (status code 410). + # + # The resource requested was previously in use but is no longer available + # and will not be available again. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-410-gone]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410]. + # + class HTTPGone < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Length Required</tt> responses (status code 411). + # + # The request did not specify the length of its content, + # which is required by the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-411-length-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411]. + # + class HTTPLengthRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Precondition Failed</tt> responses (status code 412). + # + # The server does not meet one of the preconditions + # specified in the request headers. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-412-precondition-failed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412]. + # + class HTTPPreconditionFailed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Payload Too Large</tt> responses (status code 413). + # + # The request is larger than the server is willing or able to process. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413]. + # + class HTTPPayloadTooLarge < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestEntityTooLarge = HTTPPayloadTooLarge + + # Response class for <tt>Gem::URI Too Long</tt> responses (status code 414). + # + # The Gem::URI provided was too long for the server to process. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-414-uri-too-long]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414]. + # + class HTTPURITooLong < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestURITooLong = HTTPURITooLong + HTTPRequestURITooLarge = HTTPRequestURITooLong + + # Response class for <tt>Unsupported Media Type</tt> responses (status code 415). + # + # The request entity has a media type which the server or resource does not support. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-415-unsupported-media-type]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415]. + # + class HTTPUnsupportedMediaType < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Range Not Satisfiable</tt> responses (status code 416). + # + # The request entity has a media type which the server or resource does not support. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-416-range-not-satisfiable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416]. + # + class HTTPRangeNotSatisfiable < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable + + # Response class for <tt>Expectation Failed</tt> responses (status code 417). + # + # The server cannot meet the requirements of the Expect request-header field. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-417-expectation-failed]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417]. + # + class HTTPExpectationFailed < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # 418 I'm a teapot - RFC 2324; a joke RFC + # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#418. + + # 420 Enhance Your Calm - Twitter + + # Response class for <tt>Misdirected Request</tt> responses (status code 421). + # + # The request was directed at a server that is not able to produce a response. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-421-misdirected-request]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421]. + # + class HTTPMisdirectedRequest < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unprocessable Entity</tt> responses (status code 422). + # + # The request was well-formed but had semantic errors. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422]. + # + class HTTPUnprocessableEntity < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Locked (WebDAV)</tt> responses (status code 423). + # + # The requested resource is locked. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423]. + # + class HTTPLocked < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Failed Dependency (WebDAV)</tt> responses (status code 424). + # + # The request failed because it depended on another request and that request failed. + # See {424 Failed Dependency (WebDAV)}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424]. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.4]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424]. + # + class HTTPFailedDependency < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # 425 Too Early + # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#425. + + # Response class for <tt>Upgrade Required</tt> responses (status code 426). + # + # The client should switch to the protocol given in the Upgrade header field. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-426-upgrade-required]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426]. + # + class HTTPUpgradeRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Precondition Required</tt> responses (status code 428). + # + # The origin server requires the request to be conditional. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428]. + # + class HTTPPreconditionRequired < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Too Many Requests</tt> responses (status code 429). + # + # The user has sent too many requests in a given amount of time. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-4]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429]. + # + class HTTPTooManyRequests < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Request Header Fields Too Large</tt> responses (status code 431). + # + # An individual header field is too large, + # or all the header fields collectively, are too large. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-5]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431]. + # + class HTTPRequestHeaderFieldsTooLarge < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Unavailable For Legal Reasons</tt> responses (status code 451). + # + # A server operator has received a legal demand to deny access to a resource or to a set of resources + # that includes the requested resource. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451]. + # - {RFC 7725}[https://www.rfc-editor.org/rfc/rfc7725.html#section-3]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451]. + # + class HTTPUnavailableForLegalReasons < HTTPClientError + # :stopdoc: + HAS_BODY = true + end + # 444 No Response - Nginx + # 449 Retry With - Microsoft + # 450 Blocked by Windows Parental Controls - Microsoft + # 499 Client Closed Request - Nginx + + # Response class for <tt>Internal Server Error</tt> responses (status code 500). + # + # An unexpected condition was encountered and no more specific message is suitable. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500]. + # + class HTTPInternalServerError < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Not Implemented</tt> responses (status code 501). + # + # The server either does not recognize the request method, + # or it lacks the ability to fulfil the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-501-not-implemented]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501]. + # + class HTTPNotImplemented < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Bad Gateway</tt> responses (status code 502). + # + # The server was acting as a gateway or proxy + # and received an invalid response from the upstream server. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-502-bad-gateway]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502]. + # + class HTTPBadGateway < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Service Unavailable</tt> responses (status code 503). + # + # The server cannot handle the request + # (because it is overloaded or down for maintenance). + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503]. + # + class HTTPServiceUnavailable < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Gateway Gem::Timeout</tt> responses (status code 504). + # + # The server was acting as a gateway or proxy + # and did not receive a timely response from the upstream server. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504]. + # + class HTTPGatewayTimeout < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + HTTPGatewayTimeOut = HTTPGatewayTimeout + + # Response class for <tt>HTTP Version Not Supported</tt> responses (status code 505). + # + # The server does not support the HTTP version used in the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505]. + # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-505-http-version-not-suppor]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505]. + # + class HTTPVersionNotSupported < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Variant Also Negotiates</tt> responses (status code 506). + # + # Transparent content negotiation for the request results in a circular reference. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506]. + # - {RFC 2295}[https://www.rfc-editor.org/rfc/rfc2295#section-8.1]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506]. + # + class HTTPVariantAlsoNegotiates < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Insufficient Storage (WebDAV)</tt> responses (status code 507). + # + # The server is unable to store the representation needed to complete the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507]. + # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.5]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507]. + # + class HTTPInsufficientStorage < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Loop Detected (WebDAV)</tt> responses (status code 508). + # + # The server detected an infinite loop while processing the request. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508]. + # - {RFC 5942}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.2]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508]. + # + class HTTPLoopDetected < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + # 509 Bandwidth Limit Exceeded - Apache bw/limited extension + + # Response class for <tt>Not Extended</tt> responses (status code 510). + # + # Further extensions to the request are required for the server to fulfill it. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510]. + # - {RFC 2774}[https://www.rfc-editor.org/rfc/rfc2774.html#section-7]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510]. + # + class HTTPNotExtended < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + + # Response class for <tt>Network Authentication Required</tt> responses (status code 511). + # + # The client needs to authenticate to gain network access. + # + # :include: doc/net-http/included_getters.rdoc + # + # References: + # + # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511]. + # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-6]. + # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511]. + # + class HTTPNetworkAuthenticationRequired < HTTPServerError + # :stopdoc: + HAS_BODY = true + end + +end + +class Gem::Net::HTTPResponse + # :stopdoc: + CODE_CLASS_TO_OBJ = { + '1' => Gem::Net::HTTPInformation, + '2' => Gem::Net::HTTPSuccess, + '3' => Gem::Net::HTTPRedirection, + '4' => Gem::Net::HTTPClientError, + '5' => Gem::Net::HTTPServerError + }.freeze + CODE_TO_OBJ = { + '100' => Gem::Net::HTTPContinue, + '101' => Gem::Net::HTTPSwitchProtocol, + '102' => Gem::Net::HTTPProcessing, + '103' => Gem::Net::HTTPEarlyHints, + + '200' => Gem::Net::HTTPOK, + '201' => Gem::Net::HTTPCreated, + '202' => Gem::Net::HTTPAccepted, + '203' => Gem::Net::HTTPNonAuthoritativeInformation, + '204' => Gem::Net::HTTPNoContent, + '205' => Gem::Net::HTTPResetContent, + '206' => Gem::Net::HTTPPartialContent, + '207' => Gem::Net::HTTPMultiStatus, + '208' => Gem::Net::HTTPAlreadyReported, + '226' => Gem::Net::HTTPIMUsed, + + '300' => Gem::Net::HTTPMultipleChoices, + '301' => Gem::Net::HTTPMovedPermanently, + '302' => Gem::Net::HTTPFound, + '303' => Gem::Net::HTTPSeeOther, + '304' => Gem::Net::HTTPNotModified, + '305' => Gem::Net::HTTPUseProxy, + '307' => Gem::Net::HTTPTemporaryRedirect, + '308' => Gem::Net::HTTPPermanentRedirect, + + '400' => Gem::Net::HTTPBadRequest, + '401' => Gem::Net::HTTPUnauthorized, + '402' => Gem::Net::HTTPPaymentRequired, + '403' => Gem::Net::HTTPForbidden, + '404' => Gem::Net::HTTPNotFound, + '405' => Gem::Net::HTTPMethodNotAllowed, + '406' => Gem::Net::HTTPNotAcceptable, + '407' => Gem::Net::HTTPProxyAuthenticationRequired, + '408' => Gem::Net::HTTPRequestTimeout, + '409' => Gem::Net::HTTPConflict, + '410' => Gem::Net::HTTPGone, + '411' => Gem::Net::HTTPLengthRequired, + '412' => Gem::Net::HTTPPreconditionFailed, + '413' => Gem::Net::HTTPPayloadTooLarge, + '414' => Gem::Net::HTTPURITooLong, + '415' => Gem::Net::HTTPUnsupportedMediaType, + '416' => Gem::Net::HTTPRangeNotSatisfiable, + '417' => Gem::Net::HTTPExpectationFailed, + '421' => Gem::Net::HTTPMisdirectedRequest, + '422' => Gem::Net::HTTPUnprocessableEntity, + '423' => Gem::Net::HTTPLocked, + '424' => Gem::Net::HTTPFailedDependency, + '426' => Gem::Net::HTTPUpgradeRequired, + '428' => Gem::Net::HTTPPreconditionRequired, + '429' => Gem::Net::HTTPTooManyRequests, + '431' => Gem::Net::HTTPRequestHeaderFieldsTooLarge, + '451' => Gem::Net::HTTPUnavailableForLegalReasons, + + '500' => Gem::Net::HTTPInternalServerError, + '501' => Gem::Net::HTTPNotImplemented, + '502' => Gem::Net::HTTPBadGateway, + '503' => Gem::Net::HTTPServiceUnavailable, + '504' => Gem::Net::HTTPGatewayTimeout, + '505' => Gem::Net::HTTPVersionNotSupported, + '506' => Gem::Net::HTTPVariantAlsoNegotiates, + '507' => Gem::Net::HTTPInsufficientStorage, + '508' => Gem::Net::HTTPLoopDetected, + '510' => Gem::Net::HTTPNotExtended, + '511' => Gem::Net::HTTPNetworkAuthenticationRequired, + }.freeze +end diff --git a/lib/rubygems/vendor/net-http/lib/net/http/status.rb b/lib/rubygems/vendor/net-http/lib/net/http/status.rb new file mode 100644 index 0000000000..9110b108b8 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/status.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require_relative '../http' + +if $0 == __FILE__ + require 'open-uri' + File.foreach(__FILE__) do |line| + puts line + break if line.start_with?('end') + end + puts + puts "Gem::Net::HTTP::STATUS_CODES = {" + url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv" + Gem::URI(url).read.each_line do |line| + code, mes, = line.split(',') + next if ['(Unused)', 'Unassigned', 'Description'].include?(mes) + puts " #{code} => '#{mes}'," + end + puts "} # :nodoc:" +end + +Gem::Net::HTTP::STATUS_CODES = { + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + 103 => 'Early Hints', + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + 226 => 'IM Used', + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Content Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + 421 => 'Misdirected Request', + 422 => 'Unprocessable Content', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Too Early', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + 451 => 'Unavailable For Legal Reasons', + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 510 => 'Not Extended (OBSOLETED)', + 511 => 'Network Authentication Required', +} # :nodoc: diff --git a/lib/rubygems/vendor/net-http/lib/net/https.rb b/lib/rubygems/vendor/net-http/lib/net/https.rb new file mode 100644 index 0000000000..f104c85c81 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +=begin + += net/https -- SSL/TLS enhancement for Gem::Net::HTTP. + + This file has been merged with net/http. There is no longer any need to + require_relative 'https' to use HTTPS. + + See Gem::Net::HTTP for details on how to make HTTPS connections. + +== Info + 'OpenSSL for Ruby 2' project + Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org> + All rights reserved. + +== Licence + This program is licensed under the same licence as Ruby. + (See the file 'LICENCE'.) + +=end + +require_relative 'http' +require 'openssl' diff --git a/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb new file mode 100644 index 0000000000..53d34d8d98 --- /dev/null +++ b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb @@ -0,0 +1,544 @@ +# frozen_string_literal: true +# +# = net/protocol.rb +# +#-- +# Copyright (c) 1999-2004 Yukihiro Matsumoto +# Copyright (c) 1999-2004 Minero Aoki +# +# written and maintained by Minero Aoki <aamine@loveruby.net> +# +# This program is free software. You can re-distribute and/or +# modify this program under the same terms as Ruby itself, +# Ruby Distribute License or GNU General Public License. +# +# $Id$ +#++ +# +# WARNING: This file is going to remove. +# Do not rely on the implementation written in this file. +# + +require 'socket' +require_relative '../../../timeout/lib/timeout' +require 'io/wait' + +module Gem::Net # :nodoc: + + class Protocol #:nodoc: internal use only + VERSION = "0.2.2" + + private + def Protocol.protocol_param(name, val) + module_eval(<<-End, __FILE__, __LINE__ + 1) + def #{name} + #{val} + end + End + end + + def ssl_socket_connect(s, timeout) + if timeout + while true + raise Gem::Net::OpenTimeout if timeout <= 0 + start = Process.clock_gettime Process::CLOCK_MONOTONIC + # to_io is required because SSLSocket doesn't have wait_readable yet + case s.connect_nonblock(exception: false) + when :wait_readable; s.to_io.wait_readable(timeout) + when :wait_writable; s.to_io.wait_writable(timeout) + else; break + end + timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start + end + else + s.connect + end + end + end + + + class ProtocolError < StandardError; end + class ProtoSyntaxError < ProtocolError; end + class ProtoFatalError < ProtocolError; end + class ProtoUnknownError < ProtocolError; end + class ProtoServerError < ProtocolError; end + class ProtoAuthError < ProtocolError; end + class ProtoCommandError < ProtocolError; end + class ProtoRetriableError < ProtocolError; end + ProtocRetryError = ProtoRetriableError + + ## + # OpenTimeout, a subclass of Gem::Timeout::Error, is raised if a connection cannot + # be created within the open_timeout. + + class OpenTimeout < Gem::Timeout::Error; end + + ## + # ReadTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the + # response cannot be read within the read_timeout. + + class ReadTimeout < Gem::Timeout::Error + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end + + ## + # WriteTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the + # response cannot be written within the write_timeout. Not raised on Windows. + + class WriteTimeout < Gem::Timeout::Error + def initialize(io = nil) + @io = io + end + attr_reader :io + + def message + msg = super + if @io + msg = "#{msg} with #{@io.inspect}" + end + msg + end + end + + + class BufferedIO #:nodoc: internal use only + def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil) + @io = io + @read_timeout = read_timeout + @write_timeout = write_timeout + @continue_timeout = continue_timeout + @debug_output = debug_output + @rbuf = ''.b + @rbuf_empty = true + @rbuf_offset = 0 + end + + attr_reader :io + attr_accessor :read_timeout + attr_accessor :write_timeout + attr_accessor :continue_timeout + attr_accessor :debug_output + + def inspect + "#<#{self.class} io=#{@io}>" + end + + def eof? + @io.eof? + end + + def closed? + @io.closed? + end + + def close + @io.close + end + + # + # Read + # + + public + + def read(len, dest = ''.b, ignore_eof = false) + LOG "reading #{len} bytes..." + read_bytes = 0 + begin + while read_bytes + rbuf_size < len + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end + rbuf_fill + end + s = rbuf_consume(len - read_bytes) + read_bytes += s.bytesize + dest << s + rescue EOFError + raise unless ignore_eof + end + LOG "read #{read_bytes} bytes" + dest + end + + def read_all(dest = ''.b) + LOG 'reading all...' + read_bytes = 0 + begin + while true + if s = rbuf_consume_all + read_bytes += s.bytesize + dest << s + end + rbuf_fill + end + rescue EOFError + ; + end + LOG "read #{read_bytes} bytes" + dest + end + + def readuntil(terminator, ignore_eof = false) + offset = @rbuf_offset + begin + until idx = @rbuf.index(terminator, offset) + offset = @rbuf.bytesize + rbuf_fill + end + return rbuf_consume(idx + terminator.bytesize - @rbuf_offset) + rescue EOFError + raise unless ignore_eof + return rbuf_consume + end + end + + def readline + readuntil("\n").chop + end + + private + + BUFSIZE = 1024 * 16 + + def rbuf_fill + tmp = @rbuf_empty ? @rbuf : nil + case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false) + when String + @rbuf_empty = false + if rv.equal?(tmp) + @rbuf_offset = 0 + else + @rbuf << rv + rv.clear + end + return + when :wait_readable + (io = @io.to_io).wait_readable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io) + # continue looping + when :wait_writable + # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable. + # http://www.openssl.org/support/faq.html#PROG10 + (io = @io.to_io).wait_writable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io) + # continue looping + when nil + raise EOFError, 'end of file reached' + end while true + end + + def rbuf_flush + if @rbuf_empty + @rbuf.clear + @rbuf_offset = 0 + end + nil + end + + def rbuf_size + @rbuf.bytesize - @rbuf_offset + end + + def rbuf_consume_all + rbuf_consume if rbuf_size > 0 + end + + def rbuf_consume(len = nil) + if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize) + s = @rbuf + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + elsif len.nil? + s = @rbuf.byteslice(@rbuf_offset..-1) + @rbuf = ''.b + @rbuf_offset = 0 + @rbuf_empty = true + else + s = @rbuf.byteslice(@rbuf_offset, len) + @rbuf_offset += len + @rbuf_empty = @rbuf_offset == @rbuf.bytesize + rbuf_flush + end + + @debug_output << %Q[-> #{s.dump}\n] if @debug_output + s + end + + # + # Write + # + + public + + def write(*strs) + writing { + write0(*strs) + } + end + + alias << write + + def writeline(str) + writing { + write0 str + "\r\n" + } + end + + private + + def writing + @written_bytes = 0 + @debug_output << '<- ' if @debug_output + yield + @debug_output << "\n" if @debug_output + bytes = @written_bytes + @written_bytes = nil + bytes + end + + def write0(*strs) + @debug_output << strs.map(&:dump).join if @debug_output + orig_written_bytes = @written_bytes + strs.each_with_index do |str, i| + need_retry = true + case len = @io.write_nonblock(str, exception: false) + when Integer + @written_bytes += len + len -= str.bytesize + if len == 0 + if strs.size == i+1 + return @written_bytes - orig_written_bytes + else + need_retry = false + # next string + end + elsif len < 0 + str = str.byteslice(len, -len) + else # len > 0 + need_retry = false + # next string + end + # continue looping + when :wait_writable + (io = @io.to_io).wait_writable(@write_timeout) or raise Gem::Net::WriteTimeout.new(io) + # continue looping + end while need_retry + end + end + + # + # Logging + # + + private + + def LOG_off + @save_debug_out = @debug_output + @debug_output = nil + end + + def LOG_on + @debug_output = @save_debug_out + end + + def LOG(msg) + return unless @debug_output + @debug_output << msg + "\n" + end + end + + + class InternetMessageIO < BufferedIO #:nodoc: internal use only + def initialize(*, **) + super + @wbuf = nil + end + + # + # Read + # + + def each_message_chunk + LOG 'reading message...' + LOG_off() + read_bytes = 0 + while (line = readuntil("\r\n")) != ".\r\n" + read_bytes += line.size + yield line.delete_prefix('.') + end + LOG_on() + LOG "read message (#{read_bytes} bytes)" + end + + # *library private* (cannot handle 'break') + def each_list_item + while (str = readuntil("\r\n")) != ".\r\n" + yield str.chop + end + end + + def write_message_0(src) + prev = @written_bytes + each_crlf_line(src) do |line| + write0 dot_stuff(line) + end + @written_bytes - prev + end + + # + # Write + # + + def write_message(src) + LOG "writing message from #{src.class}" + LOG_off() + len = writing { + using_each_crlf_line { + write_message_0 src + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end + + def write_message_by_block(&block) + LOG 'writing message from block' + LOG_off() + len = writing { + using_each_crlf_line { + begin + block.call(WriteAdapter.new(self.method(:write_message_0))) + rescue LocalJumpError + # allow `break' from writer block + end + } + } + LOG_on() + LOG "wrote #{len} bytes" + len + end + + private + + def dot_stuff(s) + s.sub(/\A\./, '..') + end + + def using_each_crlf_line + @wbuf = ''.b + yield + if not @wbuf.empty? # unterminated last line + write0 dot_stuff(@wbuf.chomp) + "\r\n" + elsif @written_bytes == 0 # empty src + write0 "\r\n" + end + write0 ".\r\n" + @wbuf = nil + end + + def each_crlf_line(src) + buffer_filling(@wbuf, src) do + while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/) + yield line.chomp("\n") + "\r\n" + end + end + end + + def buffer_filling(buf, src) + case src + when String # for speeding up. + 0.step(src.size - 1, 1024) do |i| + buf << src[i, 1024] + yield + end + when File # for speeding up. + while s = src.read(1024) + buf << s + yield + end + else # generic reader + src.each do |str| + buf << str + yield if buf.size > 1024 + end + yield unless buf.empty? + end + end + end + + + # + # The writer adapter class + # + class WriteAdapter + def initialize(writer) + @writer = writer + end + + def inspect + "#<#{self.class} writer=#{@writer.inspect}>" + end + + def write(str) + @writer.call(str) + end + + alias print write + + def <<(str) + write str + self + end + + def puts(str = '') + write str.chomp("\n") + "\n" + end + + def printf(*args) + write sprintf(*args) + end + end + + + class ReadAdapter #:nodoc: internal use only + def initialize(block) + @block = block + end + + def inspect + "#<#{self.class}>" + end + + def <<(str) + call_block(str, &@block) if @block + end + + private + + # This method is needed because @block must be called by yield, + # not Proc#call. You can see difference when using `break' in + # the block. + def call_block(str) + yield str + end + end + + + module NetPrivate #:nodoc: obsolete + Socket = ::Gem::Net::InternetMessageIO + end + +end # module Gem::Net diff --git a/lib/rubygems/optparse/lib/optionparser.rb b/lib/rubygems/vendor/optparse/lib/optionparser.rb index 4b9b40d82a..4b9b40d82a 100644 --- a/lib/rubygems/optparse/lib/optionparser.rb +++ b/lib/rubygems/vendor/optparse/lib/optionparser.rb diff --git a/lib/rubygems/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb index 1e50bda769..d39d9dd4e0 100644 --- a/lib/rubygems/optparse/lib/optparse.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -7,7 +7,7 @@ # # See Gem::OptionParser for documentation. # - +require 'set' unless defined?(Set) #-- # == Developer Documentation (not for RDoc output) @@ -48,7 +48,7 @@ # # == Gem::OptionParser # -# === New to \Gem::OptionParser? +# === New to +Gem::OptionParser+? # # See the {Tutorial}[optparse/tutorial.rdoc]. # @@ -73,7 +73,7 @@ # # === Minimal example # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -92,7 +92,7 @@ # Gem::OptionParser can be used to automatically generate help for the commands you # write: # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # Options = Struct.new(:name) # @@ -130,7 +130,7 @@ # option name in all caps. If an option is used without the required argument, # an exception will be raised. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -143,7 +143,7 @@ # Used: # # $ ruby optparse-test.rb -r -# optparse-test.rb:9:in `<main>': missing argument: -r (Gem::OptionParser::MissingArgument) +# optparse-test.rb:9:in '<main>': missing argument: -r (Gem::OptionParser::MissingArgument) # $ ruby optparse-test.rb -r my-library # You required my-library! # @@ -152,14 +152,14 @@ # Gem::OptionParser supports the ability to coerce command line arguments # into objects for us. # -# Gem::OptionParser comes with a few ready-to-use kinds of type +# Gem::OptionParser comes with a few ready-to-use kinds of type # coercion. They are: # -# - Date -- Anything accepted by +Date.parse+ -# - DateTime -- Anything accepted by +DateTime.parse+ -# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ -# - URI -- Anything accepted by +URI.parse+ -# - Shellwords -- Anything accepted by +Shellwords.shellwords+ +# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+) +# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+) +# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+) +# - URI -- Anything accepted by +Gem::URI.parse+ (need to require +optparse/uri+) +# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+) # - String -- Any non-empty string # - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040) # - Float -- Any float. (e.g. 10, 3.14, -100E+13) @@ -183,8 +183,8 @@ # as a +Time+. If it succeeds, that time will be passed to the # handler block. Otherwise, an exception will be raised. # -# require 'rubygems/optparse/lib/optparse' -# require 'rubygems/optparse/lib/optparse/time' +# require 'rubygems/vendor/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse/time' # Gem::OptionParser.new do |parser| # parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| # p time @@ -206,7 +206,7 @@ # It specifies which conversion block to call whenever a class is specified. # The example below uses it to fetch a +User+ object before the +on+ handler receives it. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # User = Struct.new(:id, :name) # @@ -236,13 +236,13 @@ # $ ruby optparse-test.rb --user 2 # #<struct User id=2, name="Gandalf"> # $ ruby optparse-test.rb --user 3 -# optparse-test.rb:15:in `block in find_user': No User Found for id 3 (RuntimeError) +# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError) # # === Store options to a Hash # # The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -268,8 +268,8 @@ # effect of specifying various options. This is probably the best way to learn # the features of +optparse+. # -# require 'rubygems/optparse/lib/optparse' -# require 'rubygems/optparse/lib/optparse/time' +# require 'rubygems/vendor/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse/time' # require 'ostruct' # require 'pp' # @@ -425,7 +425,9 @@ # If you have any questions, file a ticket at http://bugs.ruby-lang.org. # class Gem::OptionParser - Gem::OptionParser::Version = "0.3.0" + # The version string + VERSION = "0.8.0" + Version = VERSION # for compatibility # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -438,6 +440,8 @@ class Gem::OptionParser # and resolved against a list of acceptable values. # module Completion + # :nodoc: + def self.regexp(key, icase) Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) end @@ -459,7 +463,11 @@ class Gem::OptionParser candidates end - def candidate(key, icase = false, pat = nil) + def self.completable?(key) + String.try_convert(key) or defined?(key.id2name) + end + + def candidate(key, icase = false, pat = nil, &_) Completion.candidate(key, icase, pat, &method(:each)) end @@ -494,7 +502,6 @@ class Gem::OptionParser end end - # # Map from option/keyword string to object with completion. # @@ -502,7 +509,6 @@ class Gem::OptionParser include Completion end - # # Individual switch class. Not important to the user. # @@ -510,6 +516,8 @@ class Gem::OptionParser # RequiredArgument, etc. # class Switch + # :nodoc: + attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block # @@ -542,11 +550,11 @@ class Gem::OptionParser def initialize(pattern = nil, conv = nil, short = nil, long = nil, arg = nil, - desc = ([] if short or long), block = nil, &_block) + desc = ([] if short or long), block = nil, values = nil, &_block) raise if Array === pattern block ||= _block - @pattern, @conv, @short, @long, @arg, @desc, @block = - pattern, conv, short, long, arg, desc, block + @pattern, @conv, @short, @long, @arg, @desc, @block, @values = + pattern, conv, short, long, arg, desc, block, values end # @@ -579,11 +587,15 @@ class Gem::OptionParser # exception. # def conv_arg(arg, val = []) # :nodoc: + v, = *val if conv val = conv.call(*val) else val = proc {|v| v}.call(*val) end + if @values + @values.include?(val) or raise InvalidArgument, v + end return arg, block, val end private :conv_arg @@ -664,7 +676,7 @@ class Gem::OptionParser (sopts+lopts).each do |opt| # "(-x -c -r)-l[left justify]" - if /^--\[no-\](.+)$/ =~ opt + if /\A--\[no-\](.+)$/ =~ opt o = $1 yield("--#{o}", desc.join("")) yield("--no-#{o}", desc.join("")) @@ -697,6 +709,11 @@ class Gem::OptionParser q.object_group(self) {pretty_print_contents(q)} end + def omitted_argument(val) # :nodoc: + val.pop if val.size == 3 and val.last.nil? + val + end + # # Switch that takes no arguments. # @@ -710,10 +727,10 @@ class Gem::OptionParser conv_arg(arg) end - def self.incompatible_argument_styles(*) + def self.incompatible_argument_styles(*) # :nodoc: end - def self.pattern + def self.pattern # :nodoc: Object end @@ -730,7 +747,7 @@ class Gem::OptionParser # # Raises an exception if argument is not present. # - def parse(arg, argv) + def parse(arg, argv, &_) unless arg raise MissingArgument if argv.empty? arg = argv.shift @@ -755,7 +772,7 @@ class Gem::OptionParser if arg conv_arg(*parse_arg(arg, &error)) else - conv_arg(arg) + omitted_argument conv_arg(arg) end end @@ -774,13 +791,14 @@ class Gem::OptionParser # def parse(arg, argv, &error) if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0])) - return nil, block, nil + return nil, block end opt = (val = parse_arg(val, &error))[1] val = conv_arg(*val) if opt and !arg argv.shift else + omitted_argument val val[0] = nil end val @@ -798,6 +816,8 @@ class Gem::OptionParser # matching pattern and converter pair. Also provides summary feature. # class List + # :nodoc: + # Map from acceptable argument types to pattern and converter pairs. attr_reader :atype @@ -837,7 +857,7 @@ class Gem::OptionParser def accept(t, pat = /.*/m, &block) if pat pat.respond_to?(:match) or - raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2)) + raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2)) else pat = t if t.respond_to?(:match) end @@ -1020,7 +1040,6 @@ class Gem::OptionParser DefaultList.short['-'] = Switch::NoArgument.new {} DefaultList.long[''] = Switch::NoArgument.new {throw :terminate} - COMPSYS_HEADER = <<'XXX' # :nodoc: typeset -A opt_args @@ -1033,11 +1052,31 @@ XXX to << "#compdef #{name}\n" to << COMPSYS_HEADER visit(:compsys, {}, {}) {|o, d| - to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n] + to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n] } to << " '*:file:_files' && return 0\n" end + def help_exit + if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?}) + less = ENV["LESS"] + args = [{"LESS" => "#{less} -Fe"}, pager, "w"] + print = proc do |f| + f.puts help + rescue Errno::EPIPE + # pager terminated + end + if Process.respond_to?(:fork) and false + IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)} + # unreachable + end + IO.popen(*args, &print) + else + puts help + end + exit + end + # # Default options for ARGV, which never appear in option summary. # @@ -1049,8 +1088,7 @@ XXX # Officious['help'] = proc do |parser| Switch::NoArgument.new do |arg| - puts parser.help - exit + parser.help_exit end end @@ -1071,7 +1109,7 @@ XXX # Officious['*-completion-zsh'] = proc do |parser| Switch::OptionalArgument.new do |arg| - parser.compsys(STDOUT, arg) + parser.compsys($stdout, arg) exit end end @@ -1084,7 +1122,7 @@ XXX Switch::OptionalArgument.new do |pkg| if pkg begin - require 'rubygems/optparse/lib/optparse/version' + require_relative 'optparse/version' rescue LoadError else show_version(*pkg.split(/,/)) or @@ -1129,6 +1167,10 @@ XXX default.to_i + 1 end end + + # + # See self.inc + # def inc(*args) self.class.inc(*args) end @@ -1167,11 +1209,19 @@ XXX def terminate(arg = nil) self.class.terminate(arg) end + # + # See #terminate. + # def self.terminate(arg = nil) throw :terminate, arg end @stack = [DefaultList] + # + # Returns the global top option list. + # + # Do not use directly. + # def self.top() DefaultList end # @@ -1192,9 +1242,9 @@ XXX # # Directs to reject specified class argument. # - # +t+:: Argument class specifier, any object including Class. + # +type+:: Argument class specifier, any object including Class. # - # reject(t) + # reject(type) # def reject(*args, &blk) top.reject(*args, &blk) end # @@ -1245,7 +1295,15 @@ XXX # to $0. # def program_name - @program_name || File.basename($0, '.*') + @program_name || strip_ext(File.basename($0)) + end + + private def strip_ext(name) # :nodoc: + exts = /#{ + require "rbconfig" + Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" ")) + }\z/o + name.sub(exts, "") end # for experimental cascading :-) @@ -1284,10 +1342,24 @@ XXX end end + # + # Shows warning message with the program name + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#warn. + # def warn(mesg = $!) super("#{program_name}: #{mesg}") end + # + # Shows message with the program name then aborts. + # + # +mesg+:: Message, defaulted to +$!+. + # + # See Kernel#abort. + # def abort(mesg = $!) super("#{program_name}: #{mesg}") end @@ -1309,6 +1381,9 @@ XXX # # Pushes a new List. # + # If a block is given, yields +self+ and returns the result of the + # block, otherwise returns +self+. + # def new @stack.push(List.new) if block_given? @@ -1407,6 +1482,7 @@ XXX klass = nil q, a = nil has_arg = false + values = nil opts.each do |o| # argument class @@ -1420,7 +1496,7 @@ XXX end # directly specified pattern(any object possible to match) - if (!(String === o || Symbol === o)) and o.respond_to?(:match) + if !Completion.completable?(o) and o.respond_to?(:match) pattern = notwice(o, pattern, 'pattern') if pattern.respond_to?(:convert) conv = pattern.method(:convert).to_proc @@ -1434,7 +1510,12 @@ XXX case o when Proc, Method block = notwice(o, block, 'block') - when Array, Hash + when Array, Hash, Set + if Array === o + o, v = o.partition {|v,| Completion.completable?(v)} + values = notwice(v, values, 'values') unless v.empty? + next if o.empty? + end case pattern when CompletingHash when nil @@ -1444,11 +1525,13 @@ XXX raise ArgumentError, "argument pattern given twice" end o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}} + when Range + values = notwice(o, values, 'values') when Module raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4)) when *ArgumentStyle.keys style = notwice(ArgumentStyle[o], style, 'style') - when /^--no-([^\[\]=\s]*)(.+)?/ + when /\A--no-([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') not_pattern, not_conv = search(:atype, o) unless not_style @@ -1459,7 +1542,7 @@ XXX (q = q.downcase).tr!('_', '-') long << "no-#{q}" nolong << q - when /^--\[no-\]([^\[\]=\s]*)(.+)?/ + when /\A--\[no-\]([^\[\]=\s]*)(.+)?/ q, a = $1, $2 o = notwice(a ? Object : TrueClass, klass, 'type') if a @@ -1472,7 +1555,7 @@ XXX not_pattern, not_conv = search(:atype, FalseClass) unless not_style not_style = Switch::NoArgument nolong << "no-#{o}" - when /^--([^\[\]=\s]*)(.+)?/ + when /\A--([^\[\]=\s]*)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1482,7 +1565,7 @@ XXX ldesc << "--#{q}" (o = q.downcase).tr!('_', '-') long << o - when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ + when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ q, a = $1, $2 o = notwice(Object, klass, 'type') if a @@ -1493,7 +1576,7 @@ XXX end sdesc << "-#{q}" short << Regexp.new(q) - when /^-(.)(.+)?/ + when /\A-(.)(.+)?/ q, a = $1, $2 if a o = notwice(NilClass, klass, 'type') @@ -1502,7 +1585,7 @@ XXX end sdesc << "-#{q}" short << q - when /^=/ + when /\A=/ style = notwice(default_style.guess(arg = o), style, 'style') default_pattern, conv = search(:atype, Object) unless default_pattern else @@ -1511,12 +1594,18 @@ XXX end default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern + if Range === values and klass + unless (!values.begin or klass === values.begin) and + (!values.end or klass === values.end) + raise ArgumentError, "range does not match class" + end + end if !(short.empty? and long.empty?) if has_arg and default_style == Switch::NoArgument default_style = Switch::RequiredArgument end s = (style || default_style).new(pattern || default_pattern, - conv, sdesc, ldesc, arg, desc, block) + conv, sdesc, ldesc, arg, desc, block, values) elsif !block if style or pattern raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller) @@ -1525,13 +1614,19 @@ XXX else short << pattern s = (style || default_style).new(pattern, - conv, nil, nil, arg, desc, block) + conv, nil, nil, arg, desc, block, values) end return s, short, long, (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style), nolong end + # ---- + # Option definition phase methods + # + # These methods are used to define options, or to construct an + # Gem::OptionParser instance in other words. + # :call-seq: # define(*params, &block) # @@ -1607,6 +1702,13 @@ XXX top.append(string, nil, nil) end + # ---- + # Arguments parse phase methods + # + # These methods parse +argv+, convert, and store the results by + # calling handlers. As these methods do not modify +self+, +self+ + # can be frozen. + # # Parses command line arguments +argv+ in order. When a block is given, # each non-option argument is yielded. When optional +into+ keyword @@ -1616,21 +1718,21 @@ XXX # # Returns the rest of +argv+ left unparsed. # - def order(*argv, into: nil, &nonopt) + def order(*argv, **keywords, &nonopt) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - order!(argv, into: into, &nonopt) + order!(argv, **keywords, &nonopt) end # # Same as #order, but removes switches destructively. # Non-option arguments remain in +argv+. # - def order!(argv = default_argv, into: nil, &nonopt) + def order!(argv = default_argv, into: nil, **keywords, &nonopt) setter = ->(name, val) {into[name.to_sym] = val} if into - parse_in_order(argv, setter, &nonopt) + parse_in_order(argv, setter, **keywords, &nonopt) end - def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc: opt, arg, val, rest = nil nonopt ||= proc {|a| throw :terminate, a} argv.unshift(arg) if arg = catch(:terminate) { @@ -1641,19 +1743,24 @@ XXX opt, rest = $1, $2 opt.tr!('_', '-') begin - sw, = complete(:long, opt, true) - if require_exact && !sw.long.include?(arg) - throw :terminate, arg unless raise_unknown - raise InvalidOption, arg + if exact + sw, = search(:long, opt) + else + sw, = complete(:long, opt, true) end rescue ParseError throw :terminate, arg unless raise_unknown raise $!.set_option(arg, true) + else + unless sw + throw :terminate, arg unless raise_unknown + raise InvalidOption, arg + end end begin opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)} - val = cb.call(val) if cb - setter.call(sw.switch_name, val) if setter + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, rest) end @@ -1671,7 +1778,7 @@ XXX val = arg.delete_prefix('-') has_arg = true rescue InvalidOption - raise if require_exact + raise if exact # if no short options match, try completion with long # options. sw, = complete(:long, opt) @@ -1691,8 +1798,8 @@ XXX end begin argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-') - val = cb.call(val) if cb - setter.call(sw.switch_name, val) if setter + val = callback!(cb, 1, val) if cb + callback!(setter, 2, sw.switch_name, val) if setter rescue ParseError raise $!.set_option(arg, arg.length > 2) end @@ -1718,6 +1825,19 @@ XXX end private :parse_in_order + # Calls callback with _val_. + def callback!(cb, max_arity, *args) # :nodoc: + args.compact! + + if (size = args.size) < max_arity and cb.to_proc.lambda? + (arity = cb.arity) < 0 and arity = (1-arity) + arity = max_arity if arity > max_arity + args[arity - 1] = nil if arity > size + end + cb.call(*args) + end + private :callback! + # # Parses command line arguments +argv+ in permutation mode and returns # list of non-option arguments. When optional +into+ keyword @@ -1725,18 +1845,18 @@ XXX # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other # similar object). # - def permute(*argv, into: nil) + def permute(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - permute!(argv, into: into) + permute!(argv, **keywords) end # # Same as #permute, but removes switches destructively. # Non-option arguments remain in +argv+. # - def permute!(argv = default_argv, into: nil) + def permute!(argv = default_argv, **keywords) nonopts = [] - order!(argv, into: into, &nonopts.method(:<<)) + order!(argv, **keywords) {|nonopt| nonopts << nonopt} argv[0, 0] = nonopts argv end @@ -1748,20 +1868,20 @@ XXX # values are stored there via <code>[]=</code> method (so it can be Hash, # or OpenStruct, or other similar object). # - def parse(*argv, into: nil) + def parse(*argv, **keywords) argv = argv[0].dup if argv.size == 1 and Array === argv[0] - parse!(argv, into: into) + parse!(argv, **keywords) end # # Same as #parse, but removes switches destructively. # Non-option arguments remain in +argv+. # - def parse!(argv = default_argv, into: nil) + def parse!(argv = default_argv, **keywords) if ENV.include?('POSIXLY_CORRECT') - order!(argv, into: into) + order!(argv, **keywords) else - permute!(argv, into: into) + permute!(argv, **keywords) end end @@ -1775,18 +1895,30 @@ XXX # # params["bar"] = "x" # --bar x # # params["zot"] = "z" # --zot Z # - def getopts(*args) + # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings). + # + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true) + # # params[:a] = true # -a + # # params[:b] = "1" # -b1 + # # params[:foo] = "1" # --foo + # # params[:bar] = "x" # --bar x + # # params[:zot] = "z" # --zot Z + # + def getopts(*args, symbolize_names: false, **keywords) argv = Array === args.first ? args.shift : default_argv single_options, *long_options = *args result = {} + setter = (symbolize_names ? + ->(name, val) {result[name.to_sym] = val} + : ->(name, val) {result[name] = val}) single_options.scan(/(.)(:)?/) do |opt, val| if val - result[opt] = nil + setter[opt, nil] define("-#{opt} VAL") else - result[opt] = false + setter[opt, false] define("-#{opt}") end end if single_options @@ -1795,23 +1927,23 @@ XXX arg, desc = arg.split(';', 2) opt, val = arg.split(':', 2) if val - result[opt] = val.empty? ? nil : val + setter[opt, (val unless val.empty?)] define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) else - result[opt] = false + setter[opt, false] define("--#{opt}", *[desc].compact) end end - parse_in_order(argv, result.method(:[]=)) + parse_in_order(argv, setter, **keywords) result end # # See #getopts. # - def self.getopts(*args) - new.getopts(*args) + def self.getopts(*args, symbolize_names: false) + new.getopts(*args, symbolize_names: symbolize_names) end # @@ -1854,7 +1986,7 @@ XXX visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw} } exc = ambiguous ? AmbiguousOption : InvalidOption - raise exc.new(opt, additional: self.method(:additional_message).curry[typ]) + raise exc.new(opt, additional: proc {|o| additional_message(typ, o)}) end private :complete @@ -1872,6 +2004,9 @@ XXX DidYouMean.formatter.message_for(all_candidates & checker.correct(opt)) end + # + # Return candidates for +word+. + # def candidate(word) list = [] case word @@ -1913,26 +2048,34 @@ XXX # The optional +into+ keyword argument works exactly like that accepted in # method #parse. # - def load(filename = nil, into: nil) + def load(filename = nil, **keywords) unless filename basename = File.basename($0, '.*') - return true if load(File.expand_path(basename, '~/.options'), into: into) rescue nil + return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil basename << ".options" + if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty? + # https://specifications.freedesktop.org/basedir-spec/latest/#variables + # + # If $XDG_CONFIG_HOME is either not set or empty, a default + # equal to $HOME/.config should be used. + xdg = ['~/.config', true] + end return [ - # XDG - ENV['XDG_CONFIG_HOME'], - '~/.config', + xdg, + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), # Haiku - '~/config/settings', - ].any? {|dir| + ['~/config/settings', true], + ].any? {|dir, expand| next if !dir or dir.empty? - load(File.expand_path(basename, dir), into: into) rescue nil + filename = File.join(dir, basename) + filename = File.expand_path(filename) if expand + load(filename, **keywords) rescue nil } end begin - parse(*File.readlines(filename, chomp: true), into: into) + parse(*File.readlines(filename, chomp: true), **keywords) true rescue Errno::ENOENT, Errno::ENOTDIR false @@ -1945,10 +2088,10 @@ XXX # # +env+ defaults to the basename of the program. # - def environment(env = File.basename($0, '.*')) + def environment(env = File.basename($0, '.*'), **keywords) env = ENV[env] || ENV[env.upcase] or return require 'shellwords' - parse(*Shellwords.shellwords(env)) + parse(*Shellwords.shellwords(env), **keywords) end # @@ -2084,10 +2227,23 @@ XXX f |= Regexp::IGNORECASE if /i/ =~ o f |= Regexp::MULTILINE if /m/ =~ o f |= Regexp::EXTENDED if /x/ =~ o - k = o.delete("imx") - k = nil if k.empty? + case o = o.delete("imx") + when "" + when "u" + s = s.encode(Encoding::UTF_8) + when "e" + s = s.encode(Encoding::EUC_JP) + when "s" + s = s.encode(Encoding::SJIS) + when "n" + f |= Regexp::NOENCODING + else + raise Gem::OptionParser::InvalidArgument, "unknown regexp option - #{o}" + end + else + s ||= all end - Regexp.new(s || all, f, k) + Regexp.new(s, f) end # @@ -2101,6 +2257,7 @@ XXX # Reason which caused the error. Reason = 'parse error' + # :nodoc: def initialize(*args, additional: nil) @additional = additional @arg0, = args @@ -2120,9 +2277,10 @@ XXX argv end + DIR = File.join(__dir__, '') def self.filter_backtrace(array) unless $DEBUG - array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~)) + array.delete_if {|bt| bt.start_with?(DIR)} end array end @@ -2251,19 +2409,19 @@ XXX # Parses +self+ destructively in order and returns +self+ containing the # rest arguments left unparsed. # - def order!(&blk) options.order!(self, &blk) end + def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end # # Parses +self+ destructively in permutation mode and returns +self+ # containing the rest arguments left unparsed. # - def permute!() options.permute!(self) end + def permute!(**keywords) options.permute!(self, **keywords) end # # Parses +self+ destructively and returns +self+ containing the # rest arguments left unparsed. # - def parse!() options.parse!(self) end + def parse!(**keywords) options.parse!(self, **keywords) end # # Substitution of getopts is possible as follows. Also see @@ -2276,8 +2434,8 @@ XXX # rescue Gem::OptionParser::ParseError # end # - def getopts(*args) - options.getopts(self, *args) + def getopts(*args, symbolize_names: false, **keywords) + options.getopts(self, *args, symbolize_names: symbolize_names, **keywords) end # @@ -2287,7 +2445,8 @@ XXX super obj.instance_eval {@optparse = nil} end - def initialize(*args) + + def initialize(*args) # :nodoc: super @optparse = nil end diff --git a/lib/rubygems/optparse/lib/optparse/ac.rb b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb index e84d01bf91..28a5b1b33e 100644 --- a/lib/rubygems/optparse/lib/optparse/ac.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb @@ -1,7 +1,11 @@ # frozen_string_literal: false require_relative '../optparse' +# +# autoconf-like options. +# class Gem::OptionParser::AC < Gem::OptionParser + # :stopdoc: private def _check_ac_args(name, block) @@ -14,6 +18,7 @@ class Gem::OptionParser::AC < Gem::OptionParser end ARG_CONV = proc {|val| val.nil? ? true : val} + private_constant :ARG_CONV def _ac_arg_enable(prefix, name, help_string, block) _check_ac_args(name, block) @@ -29,16 +34,27 @@ class Gem::OptionParser::AC < Gem::OptionParser enable end + # :startdoc: + public + # Define <tt>--enable</tt> / <tt>--disable</tt> style option + # + # Appears as <tt>--enable-<i>name</i></tt> in help message. def ac_arg_enable(name, help_string, &block) _ac_arg_enable("enable", name, help_string, block) end + # Define <tt>--enable</tt> / <tt>--disable</tt> style option + # + # Appears as <tt>--disable-<i>name</i></tt> in help message. def ac_arg_disable(name, help_string, &block) _ac_arg_enable("disable", name, help_string, block) end + # Define <tt>--with</tt> / <tt>--without</tt> style option + # + # Appears as <tt>--with-<i>name</i></tt> in help message. def ac_arg_with(name, help_string, &block) _check_ac_args(name, block) diff --git a/lib/rubygems/optparse/lib/optparse/date.rb b/lib/rubygems/vendor/optparse/lib/optparse/date.rb index d9a9f4f48a..d9a9f4f48a 100644 --- a/lib/rubygems/optparse/lib/optparse/date.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/date.rb diff --git a/lib/rubygems/optparse/lib/optparse/kwargs.rb b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb index 6987a5ed62..70762f033b 100644 --- a/lib/rubygems/optparse/lib/optparse/kwargs.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb @@ -7,12 +7,17 @@ class Gem::OptionParser # # :include: ../../doc/optparse/creates_option.rdoc # - def define_by_keywords(options, meth, **opts) - meth.parameters.each do |type, name| + # Defines options which set in to _options_ for keyword parameters + # of _method_. + # + # Parameters for each keywords are given as elements of _params_. + # + def define_by_keywords(options, method, **params) + method.parameters.each do |type, name| case type when :key, :keyreq op, cl = *(type == :key ? %w"[ ]" : ["", ""]) - define("--#{name}=#{op}#{name.upcase}#{cl}", *opts[name]) do |o| + define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o| options[name] = o end end diff --git a/lib/rubygems/optparse/lib/optparse/shellwords.rb b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb index d47ad60255..d47ad60255 100644 --- a/lib/rubygems/optparse/lib/optparse/shellwords.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb diff --git a/lib/rubygems/optparse/lib/optparse/time.rb b/lib/rubygems/vendor/optparse/lib/optparse/time.rb index c59e1e4ced..c59e1e4ced 100644 --- a/lib/rubygems/optparse/lib/optparse/time.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/time.rb diff --git a/lib/rubygems/vendor/optparse/lib/optparse/uri.rb b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb new file mode 100644 index 0000000000..398127479a --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# -*- ruby -*- + +require_relative '../optparse' +require_relative '../../../uri/lib/uri' + +Gem::OptionParser.accept(Gem::URI) {|s,| Gem::URI.parse(s) if s} diff --git a/lib/rubygems/optparse/lib/optparse/version.rb b/lib/rubygems/vendor/optparse/lib/optparse/version.rb index 5d79e9db44..e39889ae87 100644 --- a/lib/rubygems/optparse/lib/optparse/version.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/version.rb @@ -2,6 +2,11 @@ # Gem::OptionParser internal utility class << Gem::OptionParser + # + # Shows version string in packages if Version is defined. + # + # +pkgs+:: package list + # def show_version(*pkgs) progname = ARGV.options.program_name result = false @@ -47,6 +52,8 @@ class << Gem::OptionParser result end + # :stopdoc: + def each_const(path, base = ::Object) path.split(/::|\//).inject(base) do |klass, name| raise NameError, path unless Module === klass @@ -68,4 +75,6 @@ class << Gem::OptionParser end end end + + # :startdoc: end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb new file mode 100644 index 0000000000..818e947477 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb @@ -0,0 +1,53 @@ +require_relative "pub_grub/package" +require_relative "pub_grub/static_package_source" +require_relative "pub_grub/term" +require_relative "pub_grub/version_range" +require_relative "pub_grub/version_constraint" +require_relative "pub_grub/version_union" +require_relative "pub_grub/version_solver" +require_relative "pub_grub/incompatibility" +require_relative 'pub_grub/solve_failure' +require_relative 'pub_grub/failure_writer' +require_relative 'pub_grub/version' + +module Gem::PubGrub + # Minimal logger that doesn't require the 'logger' gem + class NullLogger + def info(&block); end + def debug(&block); end + def warn(&block); end + def error(&block); end + end + + class StderrLogger + def info(&block) + $stderr.puts "INFO: #{block.call}" if block + end + + def debug(&block) + $stderr.puts "DEBUG: #{block.call}" if block + end + + def warn(&block) + $stderr.puts "WARN: #{block.call}" if block + end + + def error(&block) + $stderr.puts "ERROR: #{block.call}" if block + end + end + + class << self + attr_writer :logger + + def logger + @logger || default_logger + end + + private + + def default_logger + @logger = $DEBUG ? StderrLogger.new : NullLogger.new + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb new file mode 100644 index 0000000000..7a11cf0933 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb @@ -0,0 +1,20 @@ +module Gem::PubGrub + class Assignment + attr_reader :term, :cause, :decision_level, :index + def initialize(term, cause, decision_level, index) + @term = term + @cause = cause + @decision_level = decision_level + @index = index + end + + def self.decision(package, version, decision_level, index) + term = Term.new(VersionConstraint.exact(package, version), true) + new(term, :decision, decision_level, index) + end + + def decision? + cause == :decision + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb new file mode 100644 index 0000000000..c8dbf2a5ab --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb @@ -0,0 +1,169 @@ +require_relative 'version_constraint' +require_relative 'incompatibility' + +module Gem::PubGrub + # Types: + # + # Where possible, Gem::PubGrub will accept user-defined types, so long as they quack. + # + # ## "Package": + # + # This class will be used to represent the various packages being solved for. + # .to_s will be called when displaying errors and debugging info, it should + # probably return the package's name. + # It must also have a reasonable definition of #== and #hash + # + # Example classes: String ("rails") + # + # + # ## "Version": + # + # This class will be used to represent a single version number. + # + # Versions don't need to store their associated package, however they will + # only be compared against other versions of the same package. + # + # It must be Comparible (and implement <=> reasonably) + # + # Example classes: Gem::Version, Integer + # + # + # ## "Dependency" + # + # This class represents the requirement one package has on another. It is + # returned by dependencies_for(package, version) and will be passed to + # parse_dependency to convert it to a format Gem::PubGrub understands. + # + # It must also have a reasonable definition of #== + # + # Example classes: String ("~> 1.0"), Gem::Requirement + # + class BasicPackageSource + # Override me! + # + # This is called per package to find all possible versions of a package. + # + # It is called at most once per-package + # + # Returns: Array of versions for a package, in preferred order of selection + def all_versions_for(package) + raise NotImplementedError + end + + # Override me! + # + # Returns: Hash in the form of { package => requirement, ... } + def dependencies_for(package, version) + raise NotImplementedError + end + + # Override me! + # + # Convert a (user-defined) dependency into a format Gem::PubGrub understands. + # + # Package is passed to this method but for many implementations is not + # needed. + # + # Returns: either a Gem::PubGrub::VersionRange, Gem::PubGrub::VersionUnion, or a + # Gem::PubGrub::VersionConstraint + def parse_dependency(package, dependency) + raise NotImplementedError + end + + # Override me! + # + # If not overridden, this will call dependencies_for with the root package. + # + # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for) + def root_dependencies + dependencies_for(@root_package, @root_version) + end + + def initialize + @root_package = Package.root + @root_version = Package.root_version + + @sorted_versions = Hash.new do |h,k| + if k == @root_package + h[k] = [@root_version] + else + h[k] = all_versions_for(k).sort + end + end + + @cached_dependencies = Hash.new do |packages, package| + if package == @root_package + packages[package] = { + @root_version => root_dependencies + } + else + packages[package] = Hash.new do |versions, version| + versions[version] = dependencies_for(package, version) + end + end + end + end + + def versions_for(package, range=VersionRange.any) + range.select_versions(@sorted_versions[package]) + end + + def no_versions_incompatibility_for(_package, unsatisfied_term) + cause = Incompatibility::NoVersions.new(unsatisfied_term) + + Incompatibility.new([unsatisfied_term], cause: cause) + end + + def incompatibilities_for(package, version) + package_deps = @cached_dependencies[package] + sorted_versions = @sorted_versions[package] + package_deps[version].map do |dep_package, dep_constraint_name| + low = high = sorted_versions.index(version) + + # find version low such that all >= low share the same dep + while low > 0 && + package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name + low -= 1 + end + low = + if low == 0 + nil + else + sorted_versions[low] + end + + # find version high such that all < high share the same dep + while high < sorted_versions.length && + package_deps[sorted_versions[high]][dep_package] == dep_constraint_name + high += 1 + end + high = + if high == sorted_versions.length + nil + else + sorted_versions[high] + end + + range = VersionRange.new(min: low, max: high, include_min: !low.nil?) + + self_constraint = VersionConstraint.new(package, range: range) + + if !@packages.include?(dep_package) + # no such package -> this version is invalid + end + + dep_constraint = parse_dependency(dep_package, dep_constraint_name) + if !dep_constraint + # falsey indicates this dependency was invalid + cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name) + return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)] + elsif !dep_constraint.is_a?(VersionConstraint) + # Upgrade range/union to VersionConstraint + dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint) + end + + Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency) + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb new file mode 100644 index 0000000000..d8bfde0286 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb @@ -0,0 +1,182 @@ +module Gem::PubGrub + class FailureWriter + def initialize(root) + @root = root + + # { Incompatibility => Integer } + @derivations = {} + + # [ [ String, Integer or nil ] ] + @lines = [] + + # { Incompatibility => Integer } + @line_numbers = {} + + count_derivations(root) + end + + def write + return @root.to_s unless @root.conflict? + + visit(@root) + + padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length + + @lines.map do |message, number| + next "" if message.empty? + + lead = number ? "(#{number}) " : "" + lead = lead.ljust(padding) + message = message.gsub("\n", "\n" + " " * (padding + 2)) + "#{lead}#{message}" + end.join("\n") + end + + private + + def write_line(incompatibility, message, numbered:) + if numbered + number = @line_numbers.length + 1 + @line_numbers[incompatibility] = number + end + + @lines << [message, number] + end + + def visit(incompatibility, conclusion: false) + raise unless incompatibility.conflict? + + numbered = conclusion || @derivations[incompatibility] > 1; + conjunction = conclusion || incompatibility == @root ? "So," : "And" + + cause = incompatibility.cause + + if cause.conflict.conflict? && cause.other.conflict? + conflict_line = @line_numbers[cause.conflict] + other_line = @line_numbers[cause.other] + + if conflict_line && other_line + write_line( + incompatibility, + "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif conflict_line || other_line + with_line = conflict_line ? cause.conflict : cause.other + without_line = conflict_line ? cause.other : cause.conflict + line = @line_numbers[with_line] + + visit(without_line); + write_line( + incompatibility, + "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.", + numbered: numbered + ) + else + single_line_conflict = single_line?(cause.conflict.cause) + single_line_other = single_line?(cause.other.cause) + + if single_line_conflict || single_line_other + first = single_line_other ? cause.conflict : cause.other + second = single_line_other ? cause.other : cause.conflict + visit(first) + visit(second) + write_line( + incompatibility, + "Thus, #{incompatibility}.", + numbered: numbered + ) + else + visit(cause.conflict, conclusion: true) + @lines << ["", nil] + visit(cause.other) + + write_line( + incompatibility, + "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.", + numbered: numbered + ) + end + end + elsif cause.conflict.conflict? || cause.other.conflict? + derived = cause.conflict.conflict? ? cause.conflict : cause.other + ext = cause.conflict.conflict? ? cause.other : cause.conflict + + derived_line = @line_numbers[derived] + if derived_line + write_line( + incompatibility, + "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.", + numbered: numbered + ) + elsif collapsible?(derived) + derived_cause = derived.cause + if derived_cause.conflict.conflict? + collapsed_derived = derived_cause.conflict + collapsed_ext = derived_cause.other + else + collapsed_derived = derived_cause.other + collapsed_ext = derived_cause.conflict + end + + visit(collapsed_derived) + + write_line( + incompatibility, + "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.", + numbered: numbered + ) + else + visit(derived) + write_line( + incompatibility, + "#{conjunction} because #{ext},\n#{incompatibility}.", + numbered: numbered + ) + end + else + write_line( + incompatibility, + "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.", + numbered: numbered + ) + end + end + + def single_line?(cause) + !cause.conflict.conflict? && !cause.other.conflict? + end + + def collapsible?(incompatibility) + return false if @derivations[incompatibility] > 1 + + cause = incompatibility.cause + # If incompatibility is derived from two derived incompatibilities, + # there are too many transitive causes to display concisely. + return false if cause.conflict.conflict? && cause.other.conflict? + + # If incompatibility is derived from two external incompatibilities, it + # tends to be confusing to collapse it. + return false unless cause.conflict.conflict? || cause.other.conflict? + + # If incompatibility's internal cause is numbered, collapsing it would + # get too noisy. + complex = cause.conflict.conflict? ? cause.conflict : cause.other + + !@line_numbers.has_key?(complex) + end + + def count_derivations(incompatibility) + if @derivations.has_key?(incompatibility) + @derivations[incompatibility] += 1 + else + @derivations[incompatibility] = 1 + if incompatibility.conflict? + cause = incompatibility.cause + count_derivations(cause.conflict) + count_derivations(cause.other) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb new file mode 100644 index 0000000000..b5652b5e01 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb @@ -0,0 +1,150 @@ +module Gem::PubGrub + class Incompatibility + ConflictCause = Struct.new(:incompatibility, :satisfier) do + alias_method :conflict, :incompatibility + alias_method :other, :satisfier + end + + InvalidDependency = Struct.new(:package, :constraint) do + end + + NoVersions = Struct.new(:constraint) do + end + + attr_reader :terms, :cause + + def initialize(terms, cause:, custom_explanation: nil) + @cause = cause + @terms = cleanup_terms(terms) + @custom_explanation = custom_explanation + + if cause == :dependency && @terms.length != 2 + raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}" + end + end + + def hash + cause.hash ^ terms.hash + end + + def eql?(other) + cause.eql?(other.cause) && + terms.eql?(other.terms) + end + + def failure? + terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?) + end + + def conflict? + ConflictCause === cause + end + + # Returns all external incompatibilities in this incompatibility's + # derivation graph + def external_incompatibilities + if conflict? + [ + cause.conflict, + cause.other + ].flat_map(&:external_incompatibilities) + else + [this] + end + end + + def to_s + return @custom_explanation if @custom_explanation + + case cause + when :root + "(root dependency)" + when :dependency + "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}" + when Gem::PubGrub::Incompatibility::InvalidDependency + "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}" + when Gem::PubGrub::Incompatibility::NoVersions + "no versions satisfy #{cause.constraint}" + when Gem::PubGrub::Incompatibility::ConflictCause + if failure? + "version solving has failed" + elsif terms.length == 1 + term = terms[0] + if term.positive? + if term.constraint.any? + "#{term.package} cannot be used" + else + "#{term.to_s(allow_every: true)} cannot be used" + end + else + "#{term.invert} is required" + end + else + if terms.all?(&:positive?) + if terms.length == 2 + "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}" + else + "one of #{terms.map(&:to_s).join(" or ")} must be false" + end + elsif terms.all?(&:negative?) + if terms.length == 2 + "either #{terms[0].invert} or #{terms[1].invert}" + else + "one of #{terms.map(&:invert).join(" or ")} must be true"; + end + else + positive = terms.select(&:positive?) + negative = terms.select(&:negative?).map(&:invert) + + if positive.length == 1 + "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}" + else + "if #{positive.join(" and ")} then #{negative.join(" or ")}" + end + end + end + else + raise "unhandled cause: #{cause.inspect}" + end + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def pretty_print(q) + q.group 2, "#<#{self.class}", ">" do + q.breakable + q.text to_s + + q.breakable + q.text " caused by " + q.pp @cause + end + end + + private + + def cleanup_terms(terms) + terms.each do |term| + raise "#{term.inspect} must be a term" unless term.is_a?(Term) + end + + if terms.length != 1 && ConflictCause === cause + terms = terms.reject do |term| + term.positive? && Package.root?(term.package) + end + end + + # Optimized simple cases + return terms if terms.length <= 1 + return terms if terms.length == 2 && terms[0].package != terms[1].package + + terms.group_by(&:package).map do |package, common_terms| + common_terms.inject do |acc, term| + acc.intersect(term) + end + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb new file mode 100644 index 0000000000..6baa908f60 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class Package + + attr_reader :name + + def initialize(name) + @name = name + end + + def inspect + "#<#{self.class} #{name.inspect}>" + end + + def <=>(other) + name <=> other.name + end + + ROOT = Package.new(:root) + ROOT_VERSION = 0 + + def self.root + ROOT + end + + def self.root_version + ROOT_VERSION + end + + def self.root?(package) + if package.respond_to?(:root?) + package.root? + else + package == root + end + end + + def to_s + name.to_s + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb new file mode 100644 index 0000000000..f6a6ae6964 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb @@ -0,0 +1,121 @@ +require_relative 'assignment' + +module Gem::PubGrub + class PartialSolution + attr_reader :assignments, :decisions + attr_reader :attempted_solutions + + def initialize + reset! + + @attempted_solutions = 1 + @backtracking = false + end + + def decision_level + @decisions.length + end + + def relation(term) + package = term.package + return :overlap if !@terms.key?(package) + + @relation_cache[package][term] ||= + @terms[package].relation(term) + end + + def satisfies?(term) + relation(term) == :subset + end + + def derive(term, cause) + add_assignment(Assignment.new(term, cause, decision_level, assignments.length)) + end + + def satisfier(term) + assignment = + @assignments_by[term.package].bsearch do |assignment_by| + @cumulative_assignments[assignment_by].satisfies?(term) + end + + assignment || raise("#{term} unsatisfied") + end + + # A list of unsatisfied terms + def unsatisfied + @required.keys.reject do |package| + @decisions.key?(package) + end.map do |package| + @terms[package] + end + end + + def decide(package, version) + @attempted_solutions += 1 if @backtracking + @backtracking = false; + + decisions[package] = version + assignment = Assignment.decision(package, version, decision_level, assignments.length) + add_assignment(assignment) + end + + def backtrack(previous_level) + @backtracking = true + + new_assignments = assignments.select do |assignment| + assignment.decision_level <= previous_level + end + + new_decisions = Hash[decisions.first(previous_level)] + + reset! + + @decisions = new_decisions + + new_assignments.each do |assignment| + add_assignment(assignment) + end + end + + private + + def reset! + # { Array<Assignment> } + @assignments = [] + + # { Package => Array<Assignment> } + @assignments_by = Hash.new { |h,k| h[k] = [] } + @cumulative_assignments = {}.compare_by_identity + + # { Package => Package::Version } + @decisions = {} + + # { Package => Term } + @terms = {} + @relation_cache = Hash.new { |h,k| h[k] = {} } + + # { Package => Boolean } + @required = {} + end + + def add_assignment(assignment) + term = assignment.term + package = term.package + + @assignments << assignment + @assignments_by[package] << assignment + + @required[package] = true if term.positive? + + if @terms.key?(package) + old_term = @terms[package] + @terms[package] = old_term.intersect(term) + else + @terms[package] = term + end + @relation_cache[package].clear + + @cumulative_assignments[assignment] = @terms[package] + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb new file mode 100644 index 0000000000..60ca3ca2ea --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb @@ -0,0 +1,45 @@ +module Gem::PubGrub + module RubyGems + extend self + + def requirement_to_range(requirement) + ranges = requirement.requirements.map do |(op, ver)| + case op + when "~>" + name = "~> #{ver}" + bump = ver.class.new(ver.bump.to_s + ".A") + VersionRange.new(name: name, min: ver, max: bump, include_min: true) + when ">" + VersionRange.new(min: ver) + when ">=" + VersionRange.new(min: ver, include_min: true) + when "<" + VersionRange.new(max: ver) + when "<=" + VersionRange.new(max: ver, include_max: true) + when "=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true) + when "!=" + VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert + else + raise "bad version specifier: #{op}" + end + end + + ranges.inject(&:intersect) + end + + def requirement_to_constraint(package, requirement) + Gem::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement)) + end + + def parse_range(dep) + requirement_to_range(Gem::Requirement.new(dep)) + end + + def parse_constraint(package, dep) + range = parse_range(dep) + Gem::PubGrub::VersionConstraint.new(package, range: range) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb new file mode 100644 index 0000000000..c4181d2b25 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb @@ -0,0 +1,19 @@ +require_relative 'failure_writer' + +module Gem::PubGrub + class SolveFailure < StandardError + attr_reader :incompatibility + + def initialize(incompatibility) + @incompatibility = incompatibility + end + + def to_s + "Could not find compatible versions\n\n#{explanation}" + end + + def explanation + @explanation ||= FailureWriter.new(@incompatibility).write + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb new file mode 100644 index 0000000000..9e1de7d7a1 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb @@ -0,0 +1,61 @@ +require_relative 'package' +require_relative 'rubygems' +require_relative 'version_constraint' +require_relative 'incompatibility' +require_relative 'basic_package_source' + +module Gem::PubGrub + class StaticPackageSource < BasicPackageSource + class DSL + def initialize(packages, root_deps) + @packages = packages + @root_deps = root_deps + end + + def root(deps:) + @root_deps.update(deps) + end + + def add(name, version, deps: {}) + version = Gem::Version.new(version) + @packages[name] ||= {} + raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version) + @packages[name][version] = clean_deps(name, version, deps) + end + + private + + # Exclude redundant self-referencing dependencies + def clean_deps(name, version, deps) + deps.reject {|dep_name, req| name == dep_name && Gem::PubGrub::RubyGems.parse_range(req).include?(version) } + end + end + + def initialize + @root_deps = {} + @packages = {} + + yield DSL.new(@packages, @root_deps) + + super() + end + + def all_versions_for(package) + @packages[package].keys + end + + def root_dependencies + @root_deps + end + + def dependencies_for(package, version) + @packages[package][version] + end + + def parse_dependency(package, dependency) + return false unless @packages.key?(package) + + Gem::PubGrub::RubyGems.parse_constraint(package, dependency) + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb new file mode 100644 index 0000000000..b9874cdece --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb @@ -0,0 +1,42 @@ +module Gem::PubGrub + class Strategy + def initialize(source) + @source = source + + @root_package = Package.root + @root_version = Package.root_version + + @version_indexes = Hash.new do |h,k| + if k == @root_package + h[k] = { @root_version => 0 } + else + h[k] = @source.all_versions_for(k).each.with_index.to_h + end + end + end + + def next_package_and_version(unsatisfied) + package, range = next_term_to_try_from(unsatisfied) + + [package, most_preferred_version_of(package, range)] + end + + private + + def most_preferred_version_of(package, range) + versions = @source.versions_for(package, range) + + indexes = @version_indexes[package] + versions.min_by { |version| indexes[version] || Float::INFINITY } + end + + def next_term_to_try_from(unsatisfied) + unsatisfied.min_by do |package, range| + matching_versions = @source.versions_for(package, range) + higher_versions = @source.versions_for(package, range.upper_invert) + + [matching_versions.count <= 1 ? 0 : 1, higher_versions.count] + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb new file mode 100644 index 0000000000..bb26bdc911 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb @@ -0,0 +1,105 @@ +module Gem::PubGrub + class Term + attr_reader :package, :constraint, :positive + + def initialize(constraint, positive) + @constraint = constraint + @package = @constraint.package + @positive = positive + end + + def to_s(allow_every: false) + if positive + @constraint.to_s(allow_every: allow_every) + else + "not #{@constraint}" + end + end + + def hash + constraint.hash ^ positive.hash + end + + def eql?(other) + positive == other.positive && + constraint.eql?(other.constraint) + end + + def invert + self.class.new(@constraint, !@positive) + end + alias_method :inverse, :invert + + def intersect(other) + raise ArgumentError, "packages must match" if package != other.package + + if positive? && other.positive? + self.class.new(constraint.intersect(other.constraint), true) + elsif negative? && other.negative? + self.class.new(constraint.union(other.constraint), false) + else + positive = positive? ? self : other + negative = negative? ? self : other + self.class.new(positive.constraint.intersect(negative.constraint.invert), true) + end + end + + def difference(other) + intersect(other.invert) + end + + def relation(other) + if positive? && other.positive? + constraint.relation(other.constraint) + elsif negative? && other.positive? + if constraint.allows_all?(other.constraint) + :disjoint + else + :overlap + end + elsif positive? && other.negative? + if !other.constraint.allows_any?(constraint) + :subset + elsif other.constraint.allows_all?(constraint) + :disjoint + else + :overlap + end + elsif negative? && other.negative? + if constraint.allows_all?(other.constraint) + :subset + else + :overlap + end + else + raise + end + end + + def normalized_constraint + @normalized_constraint ||= positive ? constraint : constraint.invert + end + + def satisfies?(other) + raise ArgumentError, "packages must match" unless package == other.package + + relation(other) == :subset + end + + def positive? + @positive + end + + def negative? + !positive? + end + + def empty? + @empty ||= normalized_constraint.empty? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb new file mode 100644 index 0000000000..5701bf0656 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb @@ -0,0 +1,3 @@ +module Gem::PubGrub + VERSION = "0.5.0" +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb new file mode 100644 index 0000000000..ee998b3271 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb @@ -0,0 +1,129 @@ +require_relative 'version_range' + +module Gem::PubGrub + class VersionConstraint + attr_reader :package, :range + + # @param package [Gem::PubGrub::Package] + # @param range [Gem::PubGrub::VersionRange] + def initialize(package, range: nil) + @package = package + @range = range + end + + def hash + package.hash ^ range.hash + end + + def ==(other) + package == other.package && + range == other.range + end + + def eql?(other) + package.eql?(other.package) && + range.eql?(other.range) + end + + class << self + def exact(package, version) + range = VersionRange.new(min: version, max: version, include_min: true, include_max: true) + new(package, range: range) + end + + def any(package) + new(package, range: VersionRange.any) + end + + def empty(package) + new(package, range: VersionRange.empty) + end + end + + def intersect(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.intersect(other.range)) + end + + def union(other) + unless package == other.package + raise ArgumentError, "Can only intersect between VersionConstraint of the same package" + end + + self.class.new(package, range: range.union(other.range)) + end + + def invert + new_range = range.invert + self.class.new(package, range: new_range) + end + + def difference(other) + intersect(other.invert) + end + + def allows_all?(other) + range.allows_all?(other.range) + end + + def allows_any?(other) + range.intersects?(other.range) + end + + def subset?(other) + other.allows_all?(self) + end + + def overlap?(other) + other.allows_any?(self) + end + + def disjoint?(other) + !overlap?(other) + end + + def relation(other) + if subset?(other) + :subset + elsif overlap?(other) + :overlap + else + :disjoint + end + end + + def to_s(allow_every: false) + if Package.root?(package) + package.to_s + elsif allow_every && any? + "every version of #{package}" + else + "#{package} #{constraint_string}" + end + end + + def constraint_string + if any? + ">= 0" + else + range.to_s + end + end + + def empty? + range.empty? + end + + # Does this match every version of the package + def any? + range.any? + end + + def inspect + "#<#{self.class} #{self}>" + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb new file mode 100644 index 0000000000..fa0e2d5742 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb @@ -0,0 +1,423 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionRange + attr_reader :min, :max, :include_min, :include_max + + alias_method :include_min?, :include_min + alias_method :include_max?, :include_max + + class Empty < VersionRange + undef_method :min, :max + undef_method :include_min, :include_min? + undef_method :include_max, :include_max? + + def initialize + end + + def empty? + true + end + + def eql?(other) + other.empty? + end + + def hash + [].hash + end + + def intersects?(_) + false + end + + def intersect(other) + self + end + + def allows_all?(other) + other.empty? + end + + def include?(_) + false + end + + def any? + false + end + + def to_s + "(no versions)" + end + + def ==(other) + other.class == self.class + end + + def invert + VersionRange.any + end + + def select_versions(_) + [] + end + end + + EMPTY = Empty.new + Empty.singleton_class.undef_method(:new) + + def self.empty + EMPTY + end + + def self.any + new + end + + def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil) + raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true + raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true + + @min = min + @max = max + @include_min = include_min + @include_max = include_max + @name = name + end + + def hash + @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash + end + + def eql?(other) + if other.is_a?(VersionRange) + !other.empty? && + min.eql?(other.min) && + max.eql?(other.max) && + include_min.eql?(other.include_min) && + include_max.eql?(other.include_max) + else + ranges.eql?(other.ranges) + end + end + + def ranges + [self] + end + + def include?(version) + compare_version(version) == 0 + end + + # Partitions passed versions into [lower, within, higher] + # + # versions must be sorted + def partition_versions(versions) + min_index = + if !min || versions.empty? + 0 + elsif include_min? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min } + end + + lower = versions.slice(0, min_index) + versions = versions.slice(min_index, versions.size) + + max_index = + if !max || versions.empty? + versions.size + elsif include_max? + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max } + else + (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max } + end + + [ + lower, + versions.slice(0, max_index), + versions.slice(max_index, versions.size) + ] + end + + # Returns versions which are included by this range. + # + # versions must be sorted + def select_versions(versions) + return versions if any? + + partition_versions(versions)[1] + end + + def compare_version(version) + if min + case version <=> min + when -1 + return -1 + when 0 + return -1 if !include_min + when 1 + end + end + + if max + case version <=> max + when -1 + when 0 + return 1 if !include_max + when 1 + return 1 + end + end + + 0 + end + + def strictly_lower?(other) + return false if !max || !other.min + + case max <=> other.min + when 0 + !include_max || !other.include_min + when -1 + true + when 1 + false + end + end + + def strictly_higher?(other) + other.strictly_lower?(self) + end + + def intersects?(other) + return false if other.empty? + return other.intersects?(self) if other.is_a?(VersionUnion) + !strictly_lower?(other) && !strictly_higher?(other) + end + alias_method :allows_any?, :intersects? + + def intersect(other) + return other if other.empty? + return other.intersect(self) if other.is_a?(VersionUnion) + + min_range = + if !min + other + elsif !other.min + self + else + case min <=> other.min + when 0 + include_min ? other : self + when -1 + other + when 1 + self + end + end + + max_range = + if !max + other + elsif !other.max + self + else + case max <=> other.max + when 0 + include_max ? other : self + when -1 + self + when 1 + other + end + end + + if !min_range.equal?(max_range) && min_range.min && max_range.max + case min_range.min <=> max_range.max + when -1 + when 0 + if !min_range.include_min || !max_range.include_max + return EMPTY + end + when 1 + return EMPTY + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + # The span covered by two ranges + # + # If self and other are contiguous, this builds a union of the two ranges. + # (if they aren't you are probably calling the wrong method) + def span(other) + return self if other.empty? + + min_range = + if !min + self + elsif !other.min + other + else + case min <=> other.min + when 0 + include_min ? self : other + when -1 + self + when 1 + other + end + end + + max_range = + if !max + self + elsif !other.max + other + else + case max <=> other.max + when 0 + include_max ? self : other + when -1 + other + when 1 + self + end + end + + VersionRange.new( + min: min_range.min, + include_min: min_range.include_min, + max: max_range.max, + include_max: max_range.include_max + ) + end + + def union(other) + return other.union(self) if other.is_a?(VersionUnion) + + if contiguous_to?(other) + span(other) + else + VersionUnion.union([self, other]) + end + end + + def contiguous_to?(other) + return false if other.empty? + return true if any? + + intersects?(other) || contiguous_below?(other) || contiguous_above?(other) + end + + def contiguous_below?(other) + return false if !max || !other.min + + max == other.min && (include_max || other.include_min) + end + + def contiguous_above?(other) + other.contiguous_below?(self) + end + + def allows_all?(other) + return true if other.empty? + + if other.is_a?(VersionUnion) + return VersionUnion.new([self]).allows_all?(other) + end + + return false if max && !other.max + return false if min && !other.min + + if min + case min <=> other.min + when -1 + when 0 + return false if !include_min && other.include_min + when 1 + return false + end + end + + if max + case max <=> other.max + when -1 + return false + when 0 + return false if !include_max && other.include_max + when 1 + end + end + + true + end + + def any? + !min && !max + end + + def empty? + false + end + + def to_s + @name ||= constraints.join(", ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def upper_invert + return self.class.empty unless max + + VersionRange.new(min: max, include_min: !include_max) + end + + def invert + return self.class.empty if any? + + low = -> { VersionRange.new(max: min, include_max: !include_min) } + high = -> { VersionRange.new(min: max, include_min: !include_max) } + + if !min + high.call + elsif !max + low.call + else + low.call.union(high.call) + end + end + + def ==(other) + self.class == other.class && + min == other.min && + max == other.max && + include_min == other.include_min && + include_max == other.include_max + end + + private + + def constraints + return ["any"] if any? + return ["= #{min}"] if min.to_s == max.to_s + + c = [] + c << "#{include_min ? ">=" : ">"} #{min}" if min + c << "#{include_max ? "<=" : "<"} #{max}" if max + c + end + + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb new file mode 100644 index 0000000000..3341d8fe3b --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb @@ -0,0 +1,236 @@ +require_relative 'partial_solution' +require_relative 'term' +require_relative 'incompatibility' +require_relative 'solve_failure' +require_relative 'strategy' + +module Gem::PubGrub + class VersionSolver + attr_reader :logger + attr_reader :source + attr_reader :solution + attr_reader :strategy + + def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Gem::PubGrub.logger) + @logger = logger + + @source = source + @strategy = strategy + + # { package => [incompatibility, ...]} + @incompatibilities = Hash.new do |h, k| + h[k] = [] + end + + @seen_incompatibilities = {} + + @solution = PartialSolution.new + + add_incompatibility Incompatibility.new([ + Term.new(VersionConstraint.any(root), false) + ], cause: :root) + + propagate(root) + end + + def solved? + solution.unsatisfied.empty? + end + + # Returns true if there is more work to be done, false otherwise + def work + unsatisfied_terms = solution.unsatisfied + if unsatisfied_terms.empty? + logger.info { "Solution found after #{solution.attempted_solutions} attempts:" } + solution.decisions.each do |package, version| + next if Package.root?(package) + logger.info { "* #{package} #{version}" } + end + + return false + end + + next_package = choose_package_version_from(unsatisfied_terms) + propagate(next_package) + + true + end + + def solve + while work; end + + solution.decisions + end + + alias_method :result, :solve + + private + + def propagate(initial_package) + changed = [initial_package] + while package = changed.shift + @incompatibilities[package].reverse_each do |incompatibility| + result = propagate_incompatibility(incompatibility) + if result == :conflict + root_cause = resolve_conflict(incompatibility) + changed.clear + changed << propagate_incompatibility(root_cause) + elsif result # should be a Package + changed << result + end + end + changed.uniq! + end + end + + def propagate_incompatibility(incompatibility) + unsatisfied = nil + incompatibility.terms.each do |term| + relation = solution.relation(term) + if relation == :disjoint + return nil + elsif relation == :overlap + # If more than one term is inconclusive, we can't deduce anything + return nil if unsatisfied + unsatisfied = term + end + end + + if !unsatisfied + return :conflict + end + + logger.debug { "derived: #{unsatisfied.invert}" } + + solution.derive(unsatisfied.invert, incompatibility) + + unsatisfied.package + end + + def choose_package_version_from(unsatisfied_terms) + remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h + + package, version = strategy.next_package_and_version(remaining) + + logger.debug { "attempting #{package} #{version}" } + + if version.nil? + unsatisfied_term = unsatisfied_terms.find { |t| t.package == package } + add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term) + return package + end + + conflict = false + + source.incompatibilities_for(package, version).each do |incompatibility| + if @seen_incompatibilities.include?(incompatibility) + logger.debug { "knew: #{incompatibility}" } + next + end + @seen_incompatibilities[incompatibility] = true + + add_incompatibility incompatibility + + conflict ||= incompatibility.terms.all? do |term| + term.package == package || solution.satisfies?(term) + end + end + + unless conflict + logger.info { "selected #{package} #{version}" } + + solution.decide(package, version) + else + logger.info { "conflict: #{conflict.inspect}" } + end + + package + end + + def resolve_conflict(incompatibility) + logger.info { "conflict: #{incompatibility}" } + + new_incompatibility = nil + + while !incompatibility.failure? + most_recent_term = nil + most_recent_satisfier = nil + difference = nil + + previous_level = 1 + + incompatibility.terms.each do |term| + satisfier = solution.satisfier(term) + + if most_recent_satisfier.nil? + most_recent_term = term + most_recent_satisfier = satisfier + elsif most_recent_satisfier.index < satisfier.index + previous_level = [previous_level, most_recent_satisfier.decision_level].max + most_recent_term = term + most_recent_satisfier = satisfier + difference = nil + else + previous_level = [previous_level, satisfier.decision_level].max + end + + if most_recent_term == term + difference = most_recent_satisfier.term.difference(most_recent_term) + if difference.empty? + difference = nil + else + difference_satisfier = solution.satisfier(difference.inverse) + previous_level = [previous_level, difference_satisfier.decision_level].max + end + end + end + + if previous_level < most_recent_satisfier.decision_level || + most_recent_satisfier.decision? + + logger.info { "backtracking to #{previous_level}" } + solution.backtrack(previous_level) + + if new_incompatibility + add_incompatibility(new_incompatibility) + end + + return incompatibility + end + + new_terms = [] + new_terms += incompatibility.terms - [most_recent_term] + new_terms += most_recent_satisfier.cause.terms.reject { |term| + term.package == most_recent_satisfier.term.package + } + if difference + new_terms << difference.invert + end + + new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause)) + + if incompatibility.to_s == new_incompatibility.to_s + logger.info { "!! failed to resolve conflicts, this shouldn't have happened" } + break + end + + incompatibility = new_incompatibility + + partially = difference ? " partially" : "" + logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" } + logger.info { "! which is caused by #{most_recent_satisfier.cause}" } + logger.info { "! thus #{incompatibility}" } + end + + raise SolveFailure.new(incompatibility) + end + + def add_incompatibility(incompatibility) + logger.debug { "fact: #{incompatibility}" } + incompatibility.terms.each do |term| + package = term.package + @incompatibilities[package] << incompatibility + end + end + end +end diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb new file mode 100644 index 0000000000..4166318a98 --- /dev/null +++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb @@ -0,0 +1,178 @@ +# frozen_string_literal: true + +module Gem::PubGrub + class VersionUnion + attr_reader :ranges + + def self.normalize_ranges(ranges) + ranges = ranges.flat_map do |range| + range.ranges + end + + ranges.reject!(&:empty?) + + return [] if ranges.empty? + + mins, ranges = ranges.partition { |r| !r.min } + original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] } + ranges = [original_ranges.shift] + original_ranges.each do |range| + if ranges.last.contiguous_to?(range) + ranges << ranges.pop.span(range) + else + ranges << range + end + end + + ranges + end + + def self.union(ranges, normalize: true) + ranges = normalize_ranges(ranges) if normalize + + if ranges.size == 0 + VersionRange.empty + elsif ranges.size == 1 + ranges[0] + else + new(ranges) + end + end + + def initialize(ranges) + raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) } + @ranges = ranges + end + + def hash + ranges.hash + end + + def eql?(other) + ranges.eql?(other.ranges) + end + + def include?(version) + !!ranges.bsearch {|r| r.compare_version(version) } + end + + def select_versions(all_versions) + versions = [] + ranges.inject(all_versions) do |acc, range| + _, matching, higher = range.partition_versions(acc) + versions.concat matching + higher + end + versions + end + + def intersects?(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + if my_range.intersects?(other_range) + return true + end + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + end + alias_method :allows_any?, :intersects? + + def allows_all?(other) + my_ranges = ranges.dup + + my_range = my_ranges.shift + + other.ranges.all? do |other_range| + while my_range + break if my_range.allows_all?(other_range) + my_range = my_ranges.shift + end + + !!my_range + end + end + + def empty? + false + end + + def any? + false + end + + def intersect(other) + my_ranges = ranges.dup + other_ranges = other.ranges.dup + new_ranges = [] + + my_range = my_ranges.shift + other_range = other_ranges.shift + while my_range && other_range + new_ranges << my_range.intersect(other_range) + + if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max) + other_range = other_ranges.shift + else + my_range = my_ranges.shift + end + end + new_ranges.reject!(&:empty?) + VersionUnion.union(new_ranges, normalize: false) + end + + def upper_invert + ranges.last.upper_invert + end + + def invert + ranges.map(&:invert).inject(:intersect) + end + + def union(other) + VersionUnion.union([self, other]) + end + + def to_s + output = [] + + ranges = self.ranges.dup + while !ranges.empty? + ne = [] + range = ranges.shift + while !ranges.empty? && ranges[0].min.to_s == range.max.to_s + ne << range.max + range = range.span(ranges.shift) + end + + ne.map! {|x| "!= #{x}" } + if ne.empty? + output << range.to_s + elsif range.any? + output << ne.join(', ') + else + output << "#{range}, #{ne.join(', ')}" + end + end + + output.join(" OR ") + end + + def inspect + "#<#{self.class} #{to_s}>" + end + + def ==(other) + self.class == other.class && + self.ranges == other.ranges + end + end +end diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb new file mode 100644 index 0000000000..4f48e0642b --- /dev/null +++ b/lib/rubygems/vendor/resolv/lib/resolv.rb @@ -0,0 +1,3499 @@ +# frozen_string_literal: true + +require 'socket' +require_relative '../../../vendored_timeout' +require 'io/wait' +require_relative '../../../vendored_securerandom' +require 'rbconfig' + +# Gem::Resolv is a thread-aware DNS resolver library written in Ruby. Gem::Resolv can +# handle multiple DNS requests concurrently without blocking the entire Ruby +# interpreter. +# +# See also resolv-replace.rb to replace the libc resolver with Gem::Resolv. +# +# Gem::Resolv can look up various DNS resources using the DNS module directly. +# +# Examples: +# +# p Gem::Resolv.getaddress "www.ruby-lang.org" +# p Gem::Resolv.getname "210.251.121.214" +# +# Gem::Resolv::DNS.open do |dns| +# ress = dns.getresources "www.ruby-lang.org", Gem::Resolv::DNS::Resource::IN::A +# p ress.map(&:address) +# ress = dns.getresources "ruby-lang.org", Gem::Resolv::DNS::Resource::IN::MX +# p ress.map { |r| [r.exchange.to_s, r.preference] } +# end +# +# +# == Bugs +# +# * NIS is not supported. +# * /etc/nsswitch.conf is not supported. + +class Gem::Resolv + + # The version string + VERSION = "0.7.0" + + ## + # Looks up the first IP address for +name+. + + def self.getaddress(name) + DefaultResolver.getaddress(name) + end + + ## + # Looks up all IP address for +name+. + + def self.getaddresses(name) + DefaultResolver.getaddresses(name) + end + + ## + # Iterates over all IP addresses for +name+. + + def self.each_address(name, &block) + DefaultResolver.each_address(name, &block) + end + + ## + # Looks up the hostname of +address+. + + def self.getname(address) + DefaultResolver.getname(address) + end + + ## + # Looks up all hostnames for +address+. + + def self.getnames(address) + DefaultResolver.getnames(address) + end + + ## + # Iterates over all hostnames for +address+. + + def self.each_name(address, &proc) + DefaultResolver.each_name(address, &proc) + end + + ## + # Creates a new Gem::Resolv using +resolvers+. + # + # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and + # and a DNS resolver. If +resolvers+ is a hash, uses the hash as + # configuration for the DNS resolver. + + def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil)) + if !keyword_not_set && !arg_not_set + warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1 + end + + @resolvers = case resolvers + when Hash, nil + [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))] + else + resolvers + end + end + + ## + # Looks up the first IP address for +name+. + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("no address for #{name}") + end + + ## + # Looks up all IP address for +name+. + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+. + + def each_address(name) + if AddressRegex =~ name + yield name + return + end + yielded = false + @resolvers.each {|r| + r.each_address(name) {|address| + yield address.to_s + yielded = true + } + return if yielded + } + end + + ## + # Looks up the hostname of +address+. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("no name for #{address}") + end + + ## + # Looks up all hostnames for +address+. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+. + + def each_name(address) + yielded = false + @resolvers.each {|r| + r.each_name(address) {|name| + yield name.to_s + yielded = true + } + return if yielded + } + end + + ## + # Indicates a failure to resolve a name or address. + + class ResolvError < StandardError; end + + ## + # Indicates a timeout resolving a name or address. + + class ResolvTimeout < Gem::Timeout::Error; end + + ## + # Gem::Resolv::Hosts is a hostname resolver that uses the system hosts file. + + class Hosts + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/ + begin + require 'win32/resolv' unless defined?(Win32::Resolv) + hosts = Win32::Resolv.get_hosts_path || IO::NULL + rescue LoadError + end + end + # The default file name for host names + DefaultFileName = hosts || '/etc/hosts' + + ## + # Creates a new Gem::Resolv::Hosts, using +filename+ for its data source. + + def initialize(filename = DefaultFileName) + @filename = filename + @mutex = Thread::Mutex.new + @initialized = nil + end + + def lazy_initialize # :nodoc: + @mutex.synchronize { + unless @initialized + @name2addr = {} + @addr2name = {} + File.open(@filename, 'rb') {|f| + f.each {|line| + line.sub!(/#.*/, '') + addr, *hostnames = line.split(/\s+/) + next unless addr + (@addr2name[addr] ||= []).concat(hostnames) + hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr} + } + } + @name2addr.each {|name, arr| arr.reverse!} + @initialized = true + end + } + self + end + + ## + # Gets the IP address of +name+ from the hosts file. + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("#{@filename} has no name: #{name}") + end + + ## + # Gets all IP addresses for +name+ from the hosts file. + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the hosts file. + + def each_address(name, &proc) + lazy_initialize + @name2addr[name]&.each(&proc) + end + + ## + # Gets the hostname of +address+ from the hosts file. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("#{@filename} has no address: #{address}") + end + + ## + # Gets all hostnames for +address+ from the hosts file. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+ retrieved from the hosts file. + + def each_name(address, &proc) + lazy_initialize + @addr2name[address]&.each(&proc) + end + end + + ## + # Gem::Resolv::DNS is a DNS stub resolver. + # + # Information taken from the following places: + # + # * STD0013 + # * RFC 1035 + # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters + # * etc. + + class DNS + + ## + # Default DNS Port + + Port = 53 + + ## + # Default DNS UDP packet size + + UDPSize = 512 + + ## + # Creates a new DNS resolver. See Gem::Resolv::DNS.new for argument details. + # + # Yields the created DNS resolver to the block, if given, otherwise + # returns it. + + def self.open(*args) + dns = new(*args) + return dns unless block_given? + begin + yield dns + ensure + dns.close + end + end + + ## + # Creates a new DNS resolver. + # + # +config_info+ can be: + # + # nil:: Uses /etc/resolv.conf. + # String:: Path to a file using /etc/resolv.conf's format. + # Hash:: Must contain :nameserver, :search and :ndots keys. + # :nameserver_port can be used to specify port number of nameserver address. + # :raise_timeout_errors can be used to raise timeout errors + # as exceptions instead of treating the same as an NXDOMAIN response. + # + # The value of :nameserver should be an address string or + # an array of address strings. + # - :nameserver => '8.8.8.8' + # - :nameserver => ['8.8.8.8', '8.8.4.4'] + # + # The value of :nameserver_port should be an array of + # pair of nameserver address and port number. + # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] + # + # Example: + # + # Gem::Resolv::DNS.new(:nameserver => ['210.251.121.21'], + # :search => ['ruby-lang.org'], + # :ndots => 1) + + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config = Config.new(config_info) + @initialized = nil + end + + # Sets the resolver timeouts. This may be a single positive number + # or an array of positive numbers representing timeouts in seconds. + # If an array is specified, a DNS request will retry and wait for + # each successive interval in the array until a successful response + # is received. Specifying +nil+ reverts to the default timeouts: + # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] + # + # Example: + # + # dns.timeouts = 3 + # + def timeouts=(values) + @config.timeouts = values + end + + def lazy_initialize # :nodoc: + @mutex.synchronize { + unless @initialized + @config.lazy_initialize + @initialized = true + end + } + self + end + + ## + # Closes the DNS resolver. + + def close + @mutex.synchronize { + if @initialized + @initialized = false + end + } + end + + ## + # Gets the IP address of +name+ from the DNS resolver. + # + # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved address will + # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6 + + def getaddress(name) + each_address(name) {|address| return address} + raise ResolvError.new("DNS result has no information for #{name}") + end + + ## + # Gets all IP addresses for +name+ from the DNS resolver. + # + # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will + # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6 + + def getaddresses(name) + ret = [] + each_address(name) {|address| ret << address} + return ret + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the DNS + # resolver. + # + # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will + # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6 + + def each_address(name) + if use_ipv6? + each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} + end + each_resource(name, Resource::IN::A) {|resource| yield resource.address} + end + + def use_ipv6? # :nodoc: + @config.lazy_initialize unless @config.instance_variable_get(:@initialized) + + use_ipv6 = @config.use_ipv6? + unless use_ipv6.nil? + return use_ipv6 + end + + begin + list = Socket.ip_address_list + rescue NotImplementedError + return true + end + list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } + end + private :use_ipv6? + + ## + # Gets the hostname for +address+ from the DNS resolver. + # + # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved + # name will be a Gem::Resolv::DNS::Name. + + def getname(address) + each_name(address) {|name| return name} + raise ResolvError.new("DNS result has no information for #{address}") + end + + ## + # Gets all hostnames for +address+ from the DNS resolver. + # + # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved + # names will be Gem::Resolv::DNS::Name instances. + + def getnames(address) + ret = [] + each_name(address) {|name| ret << name} + return ret + end + + ## + # Iterates over all hostnames for +address+ retrieved from the DNS + # resolver. + # + # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved + # names will be Gem::Resolv::DNS::Name instances. + + def each_name(address) + case address + when Name + ptr = address + when IPv4, IPv6 + ptr = address.to_name + when IPv4::Regex + ptr = IPv4.create(address).to_name + when IPv6::Regex + ptr = IPv6.create(address).to_name + else + raise ResolvError.new("cannot interpret as address: #{address}") + end + each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name} + end + + ## + # Look up the +typeclass+ DNS resource of +name+. + # + # +name+ must be a Gem::Resolv::DNS::Name or a String. + # + # +typeclass+ should be one of the following: + # + # * Gem::Resolv::DNS::Resource::IN::A + # * Gem::Resolv::DNS::Resource::IN::AAAA + # * Gem::Resolv::DNS::Resource::IN::ANY + # * Gem::Resolv::DNS::Resource::IN::CNAME + # * Gem::Resolv::DNS::Resource::IN::HINFO + # * Gem::Resolv::DNS::Resource::IN::MINFO + # * Gem::Resolv::DNS::Resource::IN::MX + # * Gem::Resolv::DNS::Resource::IN::NS + # * Gem::Resolv::DNS::Resource::IN::PTR + # * Gem::Resolv::DNS::Resource::IN::SOA + # * Gem::Resolv::DNS::Resource::IN::TXT + # * Gem::Resolv::DNS::Resource::IN::WKS + # + # Returned resource is represented as a Gem::Resolv::DNS::Resource instance, + # i.e. Gem::Resolv::DNS::Resource::IN::A. + + def getresource(name, typeclass) + each_resource(name, typeclass) {|resource| return resource} + raise ResolvError.new("DNS result has no information for #{name}") + end + + ## + # Looks up all +typeclass+ DNS resources for +name+. See #getresource for + # argument details. + + def getresources(name, typeclass) + ret = [] + each_resource(name, typeclass) {|resource| ret << resource} + return ret + end + + ## + # Iterates over all +typeclass+ DNS resources for +name+. See + # #getresource for argument details. + + def each_resource(name, typeclass, &proc) + fetch_resource(name, typeclass) {|reply, reply_name| + extract_resources(reply, reply_name, typeclass, &proc) + } + end + + # :stopdoc: + + def fetch_resource(name, typeclass) + lazy_initialize + truncated = {} + requesters = {} + udp_requester = begin + make_udp_requester + rescue Errno::EACCES + # fall back to TCP + end + senders = {} + + begin + @config.resolv(name) do |candidate, tout, nameserver, port| + msg = Message.new + msg.rd = 1 + msg.add_question(candidate, typeclass) + + requester = requesters.fetch([nameserver, port]) do + if !truncated[candidate] && udp_requester + udp_requester + else + requesters[[nameserver, port]] = make_tcp_requester(nameserver, port) + end + end + + unless sender = senders[[candidate, requester, nameserver, port]] + sender = requester.sender(msg, candidate, nameserver, port) + next if !sender + senders[[candidate, requester, nameserver, port]] = sender + end + reply, reply_name = requester.request(sender, tout) + case reply.rcode + when RCode::NoError + if reply.tc == 1 and not Requester::TCP === requester + # Retry via TCP: + truncated[candidate] = true + redo + else + yield(reply, reply_name) + end + return + when RCode::NXDomain + raise Config::NXDomain.new(reply_name.to_s) + else + raise Config::OtherResolvError.new(reply_name.to_s) + end + end + ensure + udp_requester&.close + requesters.each_value { |requester| requester&.close } + end + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + if nameserver_port.length == 1 + Requester::ConnectedUDP.new(*nameserver_port[0]) + else + Requester::UnconnectedUDP.new(*nameserver_port) + end + end + + def make_tcp_requester(host, port) # :nodoc: + return Requester::TCP.new(host, port) + rescue Errno::ECONNREFUSED + # Treat a refused TCP connection attempt to a nameserver like a timeout, + # as Gem::Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a + # hint to try the next nameserver: + raise ResolvTimeout + end + + def extract_resources(msg, name, typeclass) # :nodoc: + if typeclass < Resource::ANY + n0 = Name.create(name) + msg.each_resource {|n, ttl, data| + yield data if n0 == n + } + end + yielded = false + n0 = Name.create(name) + msg.each_resource {|n, ttl, data| + if n0 == n + case data + when typeclass + yield data + yielded = true + when Resource::CNAME + n0 = data.name + end + end + } + return if yielded + msg.each_resource {|n, ttl, data| + if n0 == n + case data + when typeclass + yield data + end + end + } + end + + def self.random(arg) # :nodoc: + begin + Gem::SecureRandom.random_number(arg) + rescue NotImplementedError + rand(arg) + end + end + + RequestID = {} # :nodoc: + RequestIDMutex = Thread::Mutex.new # :nodoc: + + def self.allocate_request_id(host, port) # :nodoc: + id = nil + RequestIDMutex.synchronize { + h = (RequestID[[host, port]] ||= {}) + begin + id = random(0x0000..0xffff) + end while h[id] + h[id] = true + } + id + end + + def self.free_request_id(host, port, id) # :nodoc: + RequestIDMutex.synchronize { + key = [host, port] + if h = RequestID[key] + h.delete id + if h.empty? + RequestID.delete key + end + end + } + end + + case RUBY_PLATFORM + when *[ + # https://www.rfc-editor.org/rfc/rfc6056.txt + # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations + /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/, + /darwin/, # the same as FreeBSD + ] then + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + udpsock.bind(bind_host, 0) + end + else + # Sequential port assignment + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + # Ephemeral port number range recommended by RFC 6056 + port = random(1024..65535) + udpsock.bind(bind_host, port) + rescue Errno::EADDRINUSE, # POSIX + Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5) + Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4). + retry + end + end + + class Requester # :nodoc: + def initialize + @senders = {} + @socks = nil + end + + def request(sender, tout) + start = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timelimit = start + tout + begin + sender.send + rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this + Errno::ENETUNREACH + raise ResolvTimeout + end + while true + before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout = timelimit - before_select + if timeout <= 0 + raise ResolvTimeout + end + if @socks.size == 1 + select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil + else + select_result = IO.select(@socks, nil, nil, timeout) + end + if !select_result + after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) + next if after_select < timelimit + raise ResolvTimeout + end + begin + reply, from = recv_reply(select_result[0]) + rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD + Errno::ECONNRESET # Windows + # No name server running on the server? + # Don't wait anymore. + raise ResolvTimeout + end + begin + msg = Message.decode(reply) + rescue DecodeError + next # broken DNS message ignored + end + if sender == sender_for(from, msg) + break + else + # unexpected DNS message ignored + end + end + return msg, sender.data + end + + def sender_for(addr, msg) + @senders[[addr,msg.id]] + end + + def close + socks = @socks + @socks = nil + socks&.each(&:close) + end + + class Sender # :nodoc: + def initialize(msg, data, sock) + @msg = msg + @data = data + @sock = sock + end + end + + class UnconnectedUDP < Requester # :nodoc: + def initialize(*nameserver_port) + super() + @nameserver_port = nameserver_port + @initialized = false + @mutex = Thread::Mutex.new + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + @socks_hash = {} + @socks = [] + @nameserver_port.each {|host, port| + if host.index(':') + bind_host = "::" + af = Socket::AF_INET6 + else + bind_host = "0.0.0.0" + af = Socket::AF_INET + end + next if @socks_hash[bind_host] + begin + sock = UDPSocket.new(af) + rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT + next # The kernel doesn't support the address family. + end + @socks << sock + @socks_hash[bind_host] = sock + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, bind_host) + } + } + self + end + + def recv_reply(readable_socks) + lazy_initialize + reply, from = readable_socks[0].recvfrom(UDPSize) + return reply, [from[3],from[1]] + end + + def sender(msg, data, host, port=Port) + host = Addrinfo.ip(host).ip_address + lazy_initialize + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return nil if !sock + service = [host, port] + id = DNS.allocate_request_id(host, port) + request = msg.encode + request[0,2] = [id].pack('n') + return @senders[[service, id]] = + Sender.new(request, data, sock, host, port) + end + + def close + @mutex.synchronize { + if @initialized + super + @senders.each_key {|service, id| + DNS.free_request_id(service[0], service[1], id) + } + @initialized = false + end + } + end + + class Sender < Requester::Sender # :nodoc: + def initialize(msg, data, sock, host, port) + super(msg, data, sock) + @host = host + @port = port + end + attr_reader :data + + def send + raise "@sock is nil." if @sock.nil? + @sock.send(@msg, 0, @host, @port) + end + end + end + + class ConnectedUDP < Requester # :nodoc: + def initialize(host, port=Port) + super() + @host = host + @port = port + @mutex = Thread::Mutex.new + @initialized = false + end + + def lazy_initialize + @mutex.synchronize { + next if @initialized + @initialized = true + is_ipv6 = @host.index(':') + sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) + @socks = [sock] + sock.do_not_reverse_lookup = true + DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") + sock.connect(@host, @port) + } + self + end + + def recv_reply(readable_socks) + lazy_initialize + reply = readable_socks[0].recv(UDPSize) + return reply, nil + end + + def sender(msg, data, host=@host, port=@port) + lazy_initialize + unless host == @host && port == @port + raise RequestError.new("host/port don't match: #{host}:#{port}") + end + id = DNS.allocate_request_id(@host, @port) + request = msg.encode + request[0,2] = [id].pack('n') + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) + end + + def close + @mutex.synchronize do + if @initialized + super + @senders.each_key {|from, id| + DNS.free_request_id(@host, @port, id) + } + @initialized = false + end + end + end + + class Sender < Requester::Sender # :nodoc: + def send + raise "@sock is nil." if @sock.nil? + @sock.send(@msg, 0) + end + attr_reader :data + end + end + + class MDNSOneShot < UnconnectedUDP # :nodoc: + def sender(msg, data, host, port=Port) + lazy_initialize + id = DNS.allocate_request_id(host, port) + request = msg.encode + request[0,2] = [id].pack('n') + sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] + return @senders[id] = + UnconnectedUDP::Sender.new(request, data, sock, host, port) + end + + def sender_for(addr, msg) + lazy_initialize + @senders[msg.id] + end + end + + class TCP < Requester # :nodoc: + def initialize(host, port=Port) + super() + @host = host + @port = port + sock = TCPSocket.new(@host, @port) + @socks = [sock] + @senders = {} + end + + def recv_reply(readable_socks) + len = readable_socks[0].read(2).unpack('n')[0] + reply = @socks[0].read(len) + return reply, nil + end + + def sender(msg, data, host=@host, port=@port) + unless host == @host && port == @port + raise RequestError.new("host/port don't match: #{host}:#{port}") + end + id = DNS.allocate_request_id(@host, @port) + request = msg.encode + request[0,2] = [request.length, id].pack('nn') + return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) + end + + class Sender < Requester::Sender # :nodoc: + def send + @sock.print(@msg) + @sock.flush + end + attr_reader :data + end + + def close + super + @senders.each_key {|from,id| + DNS.free_request_id(@host, @port, id) + } + end + end + + ## + # Indicates a problem with the DNS request. + + class RequestError < StandardError + end + end + + class Config # :nodoc: + def initialize(config_info=nil) + @mutex = Thread::Mutex.new + @config_info = config_info + @initialized = nil + @timeouts = nil + end + + def timeouts=(values) + if values + values = Array(values) + values.each do |t| + Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric" + t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive" + end + @timeouts = values + else + @timeouts = nil + end + end + + def Config.parse_resolv_conf(filename) + nameserver = [] + search = nil + ndots = 1 + File.open(filename, 'rb') {|f| + f.each {|line| + line.sub!(/[#;].*/, '') + keyword, *args = line.split(/\s+/) + next unless keyword + case keyword + when 'nameserver' + nameserver.concat(args.each(&:freeze)) + when 'domain' + next if args.empty? + search = [args[0].freeze] + when 'search' + next if args.empty? + search = args.each(&:freeze) + when 'options' + args.each {|arg| + case arg + when /\Andots:(\d+)\z/ + ndots = $1.to_i + end + } + end + } + } + return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze + end + + def Config.default_config_hash(filename="/etc/resolv.conf") + if File.exist? filename + Config.parse_resolv_conf(filename) + elsif defined?(Win32::Resolv) + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + config_hash + else + {} + end + end + + def lazy_initialize + @mutex.synchronize { + unless @initialized + @nameserver_port = [] + @use_ipv6 = nil + @search = nil + @ndots = 1 + case @config_info + when nil + config_hash = Config.default_config_hash + when String + config_hash = Config.parse_resolv_conf(@config_info) + when Hash + config_hash = @config_info.dup + if String === config_hash[:nameserver] + config_hash[:nameserver] = [config_hash[:nameserver]] + end + if String === config_hash[:search] + config_hash[:search] = [config_hash[:search]] + end + else + raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") + end + if config_hash.include? :nameserver + @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } + end + if config_hash.include? :nameserver_port + @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } + end + if config_hash.include? :use_ipv6 + @use_ipv6 = config_hash[:use_ipv6] + end + @search = config_hash[:search] if config_hash.include? :search + @ndots = config_hash[:ndots] if config_hash.include? :ndots + @raise_timeout_errors = config_hash[:raise_timeout_errors] + + if @nameserver_port.empty? + @nameserver_port << ['0.0.0.0', Port] + end + if @search + @search = @search.map {|arg| Label.split(arg) } + else + hostname = Socket.gethostname + if /\./ =~ hostname + @search = [Label.split($')] + else + @search = [[]] + end + end + + if !@nameserver_port.kind_of?(Array) || + @nameserver_port.any? {|ns_port| + !(Array === ns_port) || + ns_port.length != 2 + !(String === ns_port[0]) || + !(Integer === ns_port[1]) + } + raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") + end + + if !@search.kind_of?(Array) || + !@search.all? {|ls| ls.all? {|l| Label::Str === l } } + raise ArgumentError.new("invalid search config: #{@search.inspect}") + end + + if !@ndots.kind_of?(Integer) + raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}") + end + + @initialized = true + end + } + self + end + + def single? + lazy_initialize + if @nameserver_port.length == 1 + return @nameserver_port[0] + else + return nil + end + end + + def nameserver_port + @nameserver_port + end + + def use_ipv6? + @use_ipv6 + end + + def generate_candidates(name) + candidates = nil + name = Name.create(name) + if name.absolute? + candidates = [name] + else + if @ndots <= name.length - 1 + candidates = [Name.new(name.to_a)] + else + candidates = [] + end + candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) + fname = Name.create("#{name}.") + if !candidates.include?(fname) + candidates << fname + end + end + return candidates + end + + InitialTimeout = 5 + + def generate_timeouts + ts = [InitialTimeout] + ts << ts[-1] * 2 / @nameserver_port.length + ts << ts[-1] * 2 + ts << ts[-1] * 2 + return ts + end + + def resolv(name) + candidates = generate_candidates(name) + timeouts = @timeouts || generate_timeouts + timeout_error = false + begin + candidates.each {|candidate| + begin + timeouts.each {|tout| + @nameserver_port.each {|nameserver, port| + begin + yield candidate, tout, nameserver, port + rescue ResolvTimeout + end + } + } + timeout_error = true + raise ResolvError.new("DNS resolv timeout: #{name}") + rescue NXDomain + end + } + rescue ResolvError + raise if @raise_timeout_errors && timeout_error + end + end + + ## + # Indicates no such domain was found. + + class NXDomain < ResolvError + end + + ## + # Indicates some other unhandled resolver error was encountered. + + class OtherResolvError < ResolvError + end + end + + module OpCode # :nodoc: + Query = 0 + IQuery = 1 + Status = 2 + Notify = 4 + Update = 5 + end + + module RCode # :nodoc: + NoError = 0 + FormErr = 1 + ServFail = 2 + NXDomain = 3 + NotImp = 4 + Refused = 5 + YXDomain = 6 + YXRRSet = 7 + NXRRSet = 8 + NotAuth = 9 + NotZone = 10 + BADVERS = 16 + BADSIG = 16 + BADKEY = 17 + BADTIME = 18 + BADMODE = 19 + BADNAME = 20 + BADALG = 21 + end + + ## + # Indicates that the DNS response was unable to be decoded. + + class DecodeError < StandardError + end + + ## + # Indicates that the DNS request was unable to be encoded. + + class EncodeError < StandardError + end + + module Label # :nodoc: + def self.split(arg) + labels = [] + arg.scan(/[^\.]+/) {labels << Str.new($&)} + return labels + end + + class Str # :nodoc: + def initialize(string) + @string = string + # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] + # This assumes @string is given in ASCII compatible encoding. + @downcase = string.b.downcase + end + attr_reader :string, :downcase + + def to_s + return @string + end + + def inspect + return "#<#{self.class} #{self}>" + end + + def ==(other) + return self.class == other.class && @downcase == other.downcase + end + + def eql?(other) + return self == other + end + + def hash + return @downcase.hash + end + end + end + + ## + # A representation of a DNS name. + + class Name + + ## + # Creates a new DNS name from +arg+. +arg+ can be: + # + # Name:: returns +arg+. + # String:: Creates a new Name. + + def self.create(arg) + case arg + when Name + return arg + when String + return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false) + else + raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}") + end + end + + def initialize(labels, absolute=true) # :nodoc: + labels = labels.map {|label| + case label + when String then Label::Str.new(label) + when Label::Str then label + else + raise ArgumentError, "unexpected label: #{label.inspect}" + end + } + @labels = labels + @absolute = absolute + end + + def inspect # :nodoc: + "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" + end + + ## + # True if this name is absolute. + + def absolute? + return @absolute + end + + def ==(other) # :nodoc: + return false unless Name === other + return false unless @absolute == other.absolute? + return @labels == other.to_a + end + + alias eql? == # :nodoc: + + ## + # Returns true if +other+ is a subdomain. + # + # Example: + # + # domain = Gem::Resolv::DNS::Name.create("y.z") + # p Gem::Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true + # p Gem::Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true + # p Gem::Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false + # p Gem::Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false + # p Gem::Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false + # p Gem::Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false + # + + def subdomain_of?(other) + raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other + return false if @absolute != other.absolute? + other_len = other.length + return false if @labels.length <= other_len + return @labels[-other_len, other_len] == other.to_a + end + + def hash # :nodoc: + return @labels.hash ^ @absolute.hash + end + + def to_a # :nodoc: + return @labels + end + + def length # :nodoc: + return @labels.length + end + + def [](i) # :nodoc: + return @labels[i] + end + + ## + # returns the domain name as a string. + # + # The domain name doesn't have a trailing dot even if the name object is + # absolute. + # + # Example: + # + # p Gem::Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z" + # p Gem::Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z" + + def to_s + return @labels.join('.') + end + end + + class Message # :nodoc: + @@identifier = -1 + + def initialize(id = (@@identifier += 1) & 0xffff) + @id = id + @qr = 0 + @opcode = 0 + @aa = 0 + @tc = 0 + @rd = 0 # recursion desired + @ra = 0 # recursion available + @rcode = 0 + @question = [] + @answer = [] + @authority = [] + @additional = [] + end + + attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode + attr_reader :question, :answer, :authority, :additional + + def ==(other) + return @id == other.id && + @qr == other.qr && + @opcode == other.opcode && + @aa == other.aa && + @tc == other.tc && + @rd == other.rd && + @ra == other.ra && + @rcode == other.rcode && + @question == other.question && + @answer == other.answer && + @authority == other.authority && + @additional == other.additional + end + + def add_question(name, typeclass) + @question << [Name.create(name), typeclass] + end + + def each_question + @question.each {|name, typeclass| + yield name, typeclass + } + end + + def add_answer(name, ttl, data) + @answer << [Name.create(name), ttl, data] + end + + def each_answer + @answer.each {|name, ttl, data| + yield name, ttl, data + } + end + + def add_authority(name, ttl, data) + @authority << [Name.create(name), ttl, data] + end + + def each_authority + @authority.each {|name, ttl, data| + yield name, ttl, data + } + end + + def add_additional(name, ttl, data) + @additional << [Name.create(name), ttl, data] + end + + def each_additional + @additional.each {|name, ttl, data| + yield name, ttl, data + } + end + + def each_resource + each_answer {|name, ttl, data| yield name, ttl, data} + each_authority {|name, ttl, data| yield name, ttl, data} + each_additional {|name, ttl, data| yield name, ttl, data} + end + + def encode + return MessageEncoder.new {|msg| + msg.put_pack('nnnnnn', + @id, + (@qr & 1) << 15 | + (@opcode & 15) << 11 | + (@aa & 1) << 10 | + (@tc & 1) << 9 | + (@rd & 1) << 8 | + (@ra & 1) << 7 | + (@rcode & 15), + @question.length, + @answer.length, + @authority.length, + @additional.length) + @question.each {|q| + name, typeclass = q + msg.put_name(name) + msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue) + } + [@answer, @authority, @additional].each {|rr| + rr.each {|r| + name, ttl, data = r + msg.put_name(name) + msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl) + msg.put_length16 {data.encode_rdata(msg)} + } + } + }.to_s + end + + class MessageEncoder # :nodoc: + def initialize + @data = ''.dup + @names = {} + yield self + end + + def to_s + return @data + end + + def put_bytes(d) + @data << d + end + + def put_pack(template, *d) + @data << d.pack(template) + end + + def put_length16 + length_index = @data.length + @data << "\0\0" + data_start = @data.length + yield + data_end = @data.length + @data[length_index, 2] = [data_end - data_start].pack("n") + end + + def put_string(d) + self.put_pack("C", d.length) + @data << d + end + + def put_string_list(ds) + ds.each {|d| + self.put_string(d) + } + end + + def put_name(d, compress: true) + put_labels(d.to_a, compress: compress) + end + + def put_labels(d, compress: true) + d.each_index {|i| + domain = d[i..-1] + if compress && idx = @names[domain] + self.put_pack("n", 0xc000 | idx) + return + else + if @data.length < 0x4000 + @names[domain] = @data.length + end + self.put_label(d[i]) + end + } + @data << "\0" + end + + def put_label(d) + self.put_string(d.to_s) + end + end + + def Message.decode(m) + o = Message.new(0) + MessageDecoder.new(m) {|msg| + id, flag, qdcount, ancount, nscount, arcount = + msg.get_unpack('nnnnnn') + o.id = id + o.tc = (flag >> 9) & 1 + o.rcode = flag & 15 + return o unless o.tc.zero? + + o.qr = (flag >> 15) & 1 + o.opcode = (flag >> 11) & 15 + o.aa = (flag >> 10) & 1 + o.rd = (flag >> 8) & 1 + o.ra = (flag >> 7) & 1 + (1..qdcount).each { + name, typeclass = msg.get_question + o.add_question(name, typeclass) + } + (1..ancount).each { + name, ttl, data = msg.get_rr + o.add_answer(name, ttl, data) + } + (1..nscount).each { + name, ttl, data = msg.get_rr + o.add_authority(name, ttl, data) + } + (1..arcount).each { + name, ttl, data = msg.get_rr + o.add_additional(name, ttl, data) + } + } + return o + end + + class MessageDecoder # :nodoc: + def initialize(data) + @data = data + @index = 0 + @limit = data.bytesize + yield self + end + + def inspect + "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" + end + + def get_length16 + len, = self.get_unpack('n') + save_limit = @limit + @limit = @index + len + d = yield(len) + if @index < @limit + raise DecodeError.new("junk exists") + elsif @limit < @index + raise DecodeError.new("limit exceeded") + end + @limit = save_limit + return d + end + + def get_bytes(len = @limit - @index) + raise DecodeError.new("limit exceeded") if @limit < @index + len + d = @data.byteslice(@index, len) + @index += len + return d + end + + def get_unpack(template) + len = 0 + template.each_byte {|byte| + byte = "%c" % byte + case byte + when ?c, ?C + len += 1 + when ?n + len += 2 + when ?N + len += 4 + else + raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'") + end + } + raise DecodeError.new("limit exceeded") if @limit < @index + len + arr = @data.unpack("@#{@index}#{template}") + @index += len + return arr + end + + def get_string + raise DecodeError.new("limit exceeded") if @limit <= @index + len = @data.getbyte(@index) + raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len + d = @data.byteslice(@index + 1, len) + @index += 1 + len + return d + end + + def get_string_list + strings = [] + while @index < @limit + strings << self.get_string + end + strings + end + + def get_list + [].tap do |values| + while @index < @limit + values << yield + end + end + end + + def get_name + return Name.new(self.get_labels) + end + + def get_labels + prev_index = @index + save_index = nil + d = [] + size = -1 + while true + raise DecodeError.new("limit exceeded") if @limit <= @index + case @data.getbyte(@index) + when 0 + @index += 1 + if save_index + @index = save_index + end + return d + when 192..255 + idx = self.get_unpack('n')[0] & 0x3fff + if prev_index <= idx + raise DecodeError.new("non-backward name pointer") + end + prev_index = idx + if !save_index + save_index = @index + end + @index = idx + else + l = self.get_label + d << l + size += 1 + l.string.bytesize + raise DecodeError.new("name label data exceed 255 octets") if size > 255 + end + end + end + + def get_label + return Label::Str.new(self.get_string) + end + + def get_question + name = self.get_name + type, klass = self.get_unpack("nn") + return name, Resource.get_class(type, klass) + end + + def get_rr + name = self.get_name + type, klass, ttl = self.get_unpack('nnN') + typeclass = Resource.get_class(type, klass) + res = self.get_length16 do + begin + typeclass.decode_rdata self + rescue => e + raise DecodeError, e.message, e.backtrace + end + end + res.instance_variable_set :@ttl, ttl + return name, ttl, res + end + end + end + + ## + # SvcParams for service binding RRs. [RFC9460] + + class SvcParams + include Enumerable + + ## + # Create a list of SvcParams with the given initial content. + # + # +params+ has to be an enumerable of +SvcParam+s. + # If its content has +SvcParam+s with the duplicate key, + # the one appears last takes precedence. + + def initialize(params = []) + @params = {} + + params.each do |param| + add param + end + end + + ## + # Get SvcParam for the given +key+ in this list. + + def [](key) + @params[canonical_key(key)] + end + + ## + # Get the number of SvcParams in this list. + + def count + @params.count + end + + ## + # Get whether this list is empty. + + def empty? + @params.empty? + end + + ## + # Add the SvcParam +param+ to this list, overwriting the existing one with the same key. + + def add(param) + @params[param.class.key_number] = param + end + + ## + # Remove the +SvcParam+ with the given +key+ and return it. + + def delete(key) + @params.delete(canonical_key(key)) + end + + ## + # Enumerate the +SvcParam+s in this list. + + def each(&block) + return enum_for(:each) unless block + @params.each_value(&block) + end + + def encode(msg) # :nodoc: + @params.keys.sort.each do |key| + msg.put_pack('n', key) + msg.put_length16 do + @params.fetch(key).encode(msg) + end + end + end + + def self.decode(msg) # :nodoc: + params = msg.get_list do + key, = msg.get_unpack('n') + msg.get_length16 do + SvcParam::ClassHash[key].decode(msg) + end + end + + return self.new(params) + end + + private + + def canonical_key(key) # :nodoc: + case key + when Integer + key + when /\Akey(\d+)\z/ + Integer($1) + when Symbol + SvcParam::ClassHash[key].key_number + else + raise TypeError, 'key must be either String or Symbol' + end + end + end + + ## + # Base class for SvcParam. [RFC9460] + + class SvcParam + + ## + # Get the presentation name of the SvcParamKey. + + def self.key_name + const_get(:KeyName) + end + + ## + # Get the registered number of the SvcParamKey. + + def self.key_number + const_get(:KeyNumber) + end + + ClassHash = Hash.new do |h, key| # :nodoc: + case key + when Integer + Generic.create(key) + when /\Akey(?<key>\d+)\z/ + Generic.create(key.to_int) + when Symbol + raise KeyError, "unknown key #{key}" + else + raise TypeError, 'key must be either String or Symbol' + end + end + + ## + # Generic SvcParam abstract class. + + class Generic < SvcParam + + ## + # SvcParamValue in wire-format byte string. + + attr_reader :value + + ## + # Create generic SvcParam + + def initialize(value) + @value = value + end + + def encode(msg) # :nodoc: + msg.put_bytes(@value) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_bytes) + end + + def self.create(key_number) + c = Class.new(Generic) + key_name = :"key#{key_number}" + c.const_set(:KeyName, key_name) + c.const_set(:KeyNumber, key_number) + self.const_set(:"Key#{key_number}", c) + ClassHash[key_name] = ClassHash[key_number] = c + return c + end + end + + ## + # "mandatory" SvcParam -- Mandatory keys in service binding RR + + class Mandatory < SvcParam + KeyName = :mandatory + KeyNumber = 0 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Mandatory keys. + + attr_reader :keys + + ## + # Initialize "mandatory" ScvParam. + + def initialize(keys) + @keys = keys.map(&:to_int) + end + + def encode(msg) # :nodoc: + @keys.sort.each do |key| + msg.put_pack('n', key) + end + end + + def self.decode(msg) # :nodoc: + keys = msg.get_list { msg.get_unpack('n')[0] } + return self.new(keys) + end + end + + ## + # "alpn" SvcParam -- Additional supported protocols + + class ALPN < SvcParam + KeyName = :alpn + KeyNumber = 1 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Supported protocol IDs. + + attr_reader :protocol_ids + + ## + # Initialize "alpn" ScvParam. + + def initialize(protocol_ids) + @protocol_ids = protocol_ids.map(&:to_str) + end + + def encode(msg) # :nodoc: + msg.put_string_list(@protocol_ids) + end + + def self.decode(msg) # :nodoc: + return self.new(msg.get_string_list) + end + end + + ## + # "no-default-alpn" SvcParam -- No support for default protocol + + class NoDefaultALPN < SvcParam + KeyName = :'no-default-alpn' + KeyNumber = 2 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + def encode(msg) # :nodoc: + # no payload + end + + def self.decode(msg) # :nodoc: + return self.new + end + end + + ## + # "port" SvcParam -- Port for alternative endpoint + + class Port < SvcParam + KeyName = :port + KeyNumber = 3 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Port number. + + attr_reader :port + + ## + # Initialize "port" ScvParam. + + def initialize(port) + @port = port.to_int + end + + def encode(msg) # :nodoc: + msg.put_pack('n', @port) + end + + def self.decode(msg) # :nodoc: + port, = msg.get_unpack('n') + return self.new(port) + end + end + + ## + # "ipv4hint" SvcParam -- IPv4 address hints + + class IPv4Hint < SvcParam + KeyName = :ipv4hint + KeyNumber = 4 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv4 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv4hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv4.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) } + return self.new(addresses) + end + end + + ## + # "ipv6hint" SvcParam -- IPv6 address hints + + class IPv6Hint < SvcParam + KeyName = :ipv6hint + KeyNumber = 6 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # Set of IPv6 addresses. + + attr_reader :addresses + + ## + # Initialize "ipv6hint" ScvParam. + + def initialize(addresses) + @addresses = addresses.map {|address| IPv6.create(address) } + end + + def encode(msg) # :nodoc: + @addresses.each do |address| + msg.put_bytes(address.address) + end + end + + def self.decode(msg) # :nodoc: + addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) } + return self.new(addresses) + end + end + + ## + # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461] + + class DoHPath < SvcParam + KeyName = :dohpath + KeyNumber = 7 + ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc: + + ## + # URI template for DoH queries. + + attr_reader :template + + ## + # Initialize "dohpath" ScvParam. + + def initialize(template) + @template = template.encode('utf-8') + end + + def encode(msg) # :nodoc: + msg.put_bytes(@template) + end + + def self.decode(msg) # :nodoc: + template = msg.get_bytes.force_encoding('utf-8') + return self.new(template) + end + end + end + + ## + # A DNS query abstract class. + + class Query + def encode_rdata(msg) # :nodoc: + raise EncodeError.new("#{self.class} is query.") + end + + def self.decode_rdata(msg) # :nodoc: + raise DecodeError.new("#{self.class} is query.") + end + end + + ## + # A DNS resource abstract class. + + class Resource < Query + + ## + # Remaining Time To Live for this Resource. + + attr_reader :ttl + + ClassHash = Module.new do + module_function + + def []=(type_class_value, klass) + type_value, class_value = type_class_value + Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass) + end + end + + def encode_rdata(msg) # :nodoc: + raise NotImplementedError.new + end + + def self.decode_rdata(msg) # :nodoc: + raise NotImplementedError.new + end + + def ==(other) # :nodoc: + return false unless self.class == other.class + s_ivars = self.instance_variables + s_ivars.sort! + s_ivars.delete :@ttl + o_ivars = other.instance_variables + o_ivars.sort! + o_ivars.delete :@ttl + return s_ivars == o_ivars && + s_ivars.collect {|name| self.instance_variable_get name} == + o_ivars.collect {|name| other.instance_variable_get name} + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + h = 0 + vars = self.instance_variables + vars.delete :@ttl + vars.each {|name| + h ^= self.instance_variable_get(name).hash + } + return h + end + + def self.get_class(type_value, class_value) # :nodoc: + cache = :"Type#{type_value}_Class#{class_value}" + + return (const_defined?(cache) && const_get(cache)) || + Generic.create(type_value, class_value) + end + + ## + # A generic resource abstract class. + + class Generic < Resource + + ## + # Creates a new generic resource. + + def initialize(data) + @data = data + end + + ## + # Data for this generic resource. + + attr_reader :data + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(data) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(msg.get_bytes) + end + + def self.create(type_value, class_value) # :nodoc: + c = Class.new(Generic) + c.const_set(:TypeValue, type_value) + c.const_set(:ClassValue, class_value) + Generic.const_set("Type#{type_value}_Class#{class_value}", c) + ClassHash[[type_value, class_value]] = c + return c + end + end + + ## + # Domain Name resource abstract class. + + class DomainName < Resource + + ## + # Creates a new DomainName from +name+. + + def initialize(name) + @name = name + end + + ## + # The name of this DomainName. + + attr_reader :name + + def encode_rdata(msg) # :nodoc: + msg.put_name(@name) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(msg.get_name) + end + end + + # Standard (class generic) RRs + + ClassValue = nil # :nodoc: + + ## + # An authoritative name server. + + class NS < DomainName + TypeValue = 2 # :nodoc: + end + + ## + # The canonical name for an alias. + + class CNAME < DomainName + TypeValue = 5 # :nodoc: + end + + ## + # Start Of Authority resource. + + class SOA < Resource + + TypeValue = 6 # :nodoc: + + ## + # Creates a new SOA record. See the attr documentation for the + # details of each argument. + + def initialize(mname, rname, serial, refresh, retry_, expire, minimum) + @mname = mname + @rname = rname + @serial = serial + @refresh = refresh + @retry = retry_ + @expire = expire + @minimum = minimum + end + + ## + # Name of the host where the master zone file for this zone resides. + + attr_reader :mname + + ## + # The person responsible for this domain name. + + attr_reader :rname + + ## + # The version number of the zone file. + + attr_reader :serial + + ## + # How often, in seconds, a secondary name server is to check for + # updates from the primary name server. + + attr_reader :refresh + + ## + # How often, in seconds, a secondary name server is to retry after a + # failure to check for a refresh. + + attr_reader :retry + + ## + # Time in seconds that a secondary name server is to use the data + # before refreshing from the primary name server. + + attr_reader :expire + + ## + # The minimum number of seconds to be used for TTL values in RRs. + + attr_reader :minimum + + def encode_rdata(msg) # :nodoc: + msg.put_name(@mname) + msg.put_name(@rname) + msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum) + end + + def self.decode_rdata(msg) # :nodoc: + mname = msg.get_name + rname = msg.get_name + serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN') + return self.new( + mname, rname, serial, refresh, retry_, expire, minimum) + end + end + + ## + # A Pointer to another DNS name. + + class PTR < DomainName + TypeValue = 12 # :nodoc: + end + + ## + # Host Information resource. + + class HINFO < Resource + + TypeValue = 13 # :nodoc: + + ## + # Creates a new HINFO running +os+ on +cpu+. + + def initialize(cpu, os) + @cpu = cpu + @os = os + end + + ## + # CPU architecture for this resource. + + attr_reader :cpu + + ## + # Operating system for this resource. + + attr_reader :os + + def encode_rdata(msg) # :nodoc: + msg.put_string(@cpu) + msg.put_string(@os) + end + + def self.decode_rdata(msg) # :nodoc: + cpu = msg.get_string + os = msg.get_string + return self.new(cpu, os) + end + end + + ## + # Mailing list or mailbox information. + + class MINFO < Resource + + TypeValue = 14 # :nodoc: + + def initialize(rmailbx, emailbx) + @rmailbx = rmailbx + @emailbx = emailbx + end + + ## + # Domain name responsible for this mail list or mailbox. + + attr_reader :rmailbx + + ## + # Mailbox to use for error messages related to the mail list or mailbox. + + attr_reader :emailbx + + def encode_rdata(msg) # :nodoc: + msg.put_name(@rmailbx) + msg.put_name(@emailbx) + end + + def self.decode_rdata(msg) # :nodoc: + rmailbx = msg.get_string + emailbx = msg.get_string + return self.new(rmailbx, emailbx) + end + end + + ## + # Mail Exchanger resource. + + class MX < Resource + + TypeValue= 15 # :nodoc: + + ## + # Creates a new MX record with +preference+, accepting mail at + # +exchange+. + + def initialize(preference, exchange) + @preference = preference + @exchange = exchange + end + + ## + # The preference for this MX. + + attr_reader :preference + + ## + # The host of this MX. + + attr_reader :exchange + + def encode_rdata(msg) # :nodoc: + msg.put_pack('n', @preference) + msg.put_name(@exchange) + end + + def self.decode_rdata(msg) # :nodoc: + preference, = msg.get_unpack('n') + exchange = msg.get_name + return self.new(preference, exchange) + end + end + + ## + # Unstructured text resource. + + class TXT < Resource + + TypeValue = 16 # :nodoc: + + def initialize(first_string, *rest_strings) + @strings = [first_string, *rest_strings] + end + + ## + # Returns an Array of Strings for this TXT record. + + attr_reader :strings + + ## + # Returns the concatenated string from +strings+. + + def data + @strings.join("") + end + + def encode_rdata(msg) # :nodoc: + msg.put_string_list(@strings) + end + + def self.decode_rdata(msg) # :nodoc: + strings = msg.get_string_list + return self.new(*strings) + end + end + + ## + # Location resource + + class LOC < Resource + + TypeValue = 29 # :nodoc: + + def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude) + @version = version + @ssize = Gem::Resolv::LOC::Size.create(ssize) + @hprecision = Gem::Resolv::LOC::Size.create(hprecision) + @vprecision = Gem::Resolv::LOC::Size.create(vprecision) + @latitude = Gem::Resolv::LOC::Coord.create(latitude) + @longitude = Gem::Resolv::LOC::Coord.create(longitude) + @altitude = Gem::Resolv::LOC::Alt.create(altitude) + end + + ## + # Returns the version value for this LOC record which should always be 00 + + attr_reader :version + + ## + # The spherical size of this LOC + # in meters using scientific notation as 2 integers of XeY + + attr_reader :ssize + + ## + # The horizontal precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :hprecision + + ## + # The vertical precision using ssize type values + # in meters using scientific notation as 2 integers of XeY + # for precision use value/2 e.g. 2m = +/-1m + + attr_reader :vprecision + + ## + # The latitude for this LOC where 2**31 is the equator + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :latitude + + ## + # The longitude for this LOC where 2**31 is the prime meridian + # in thousandths of an arc second as an unsigned 32bit integer + + attr_reader :longitude + + ## + # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid + # in centimeters as an unsigned 32bit integer + + attr_reader :altitude + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@version) + msg.put_bytes(@ssize.scalar) + msg.put_bytes(@hprecision.scalar) + msg.put_bytes(@vprecision.scalar) + msg.put_bytes(@latitude.coordinates) + msg.put_bytes(@longitude.coordinates) + msg.put_bytes(@altitude.altitude) + end + + def self.decode_rdata(msg) # :nodoc: + version = msg.get_bytes(1) + ssize = msg.get_bytes(1) + hprecision = msg.get_bytes(1) + vprecision = msg.get_bytes(1) + latitude = msg.get_bytes(4) + longitude = msg.get_bytes(4) + altitude = msg.get_bytes(4) + return self.new( + version, + Gem::Resolv::LOC::Size.new(ssize), + Gem::Resolv::LOC::Size.new(hprecision), + Gem::Resolv::LOC::Size.new(vprecision), + Gem::Resolv::LOC::Coord.new(latitude,"lat"), + Gem::Resolv::LOC::Coord.new(longitude,"lon"), + Gem::Resolv::LOC::Alt.new(altitude) + ) + end + end + + ## + # A Query type requesting any RR. + + class ANY < Query + TypeValue = 255 # :nodoc: + end + + ## + # CAA resource record defined in RFC 8659 + # + # These records identify certificate authority allowed to issue + # certificates for the given domain. + + class CAA < Resource + TypeValue = 257 + + ## + # Creates a new CAA for +flags+, +tag+ and +value+. + + def initialize(flags, tag, value) + unless (0..255) === flags + raise ArgumentError.new('flags must be an Integer between 0 and 255') + end + unless (1..15) === tag.bytesize + raise ArgumentError.new('length of tag must be between 1 and 15') + end + + @flags = flags + @tag = tag + @value = value + end + + ## + # Flags for this property: + # - Bit 0 : 0 = not critical, 1 = critical + + attr_reader :flags + + ## + # Property tag ("issue", "issuewild", "iodef"...). + + attr_reader :tag + + ## + # Property value. + + attr_reader :value + + ## + # Whether the critical flag is set on this property. + + def critical? + flags & 0x80 != 0 + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack('C', @flags) + msg.put_string(@tag) + msg.put_bytes(@value) + end + + def self.decode_rdata(msg) # :nodoc: + flags, = msg.get_unpack('C') + tag = msg.get_string + value = msg.get_bytes + self.new flags, tag, value + end + end + + ClassInsensitiveTypes = [ # :nodoc: + NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA + ] + + ## + # module IN contains ARPA Internet specific RRs. + + module IN + + ClassValue = 1 # :nodoc: + + ClassInsensitiveTypes.each {|s| + c = Class.new(s) + c.const_set(:TypeValue, s::TypeValue) + c.const_set(:ClassValue, ClassValue) + ClassHash[[s::TypeValue, ClassValue]] = c + self.const_set(s.name.sub(/.*::/, ''), c) + } + + ## + # IPv4 Address resource + + class A < Resource + TypeValue = 1 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new A for +address+. + + def initialize(address) + @address = IPv4.create(address) + end + + ## + # The Gem::Resolv::IPv4 address for this A. + + attr_reader :address + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(IPv4.new(msg.get_bytes(4))) + end + end + + ## + # Well Known Service resource. + + class WKS < Resource + TypeValue = 11 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + def initialize(address, protocol, bitmap) + @address = IPv4.create(address) + @protocol = protocol + @bitmap = bitmap + end + + ## + # The host these services run on. + + attr_reader :address + + ## + # IP protocol number for these services. + + attr_reader :protocol + + ## + # A bit map of enabled services on this host. + # + # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP + # service (port 25). If this bit is set, then an SMTP server should + # be listening on TCP port 25; if zero, SMTP service is not + # supported. + + attr_reader :bitmap + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + msg.put_pack("n", @protocol) + msg.put_bytes(@bitmap) + end + + def self.decode_rdata(msg) # :nodoc: + address = IPv4.new(msg.get_bytes(4)) + protocol, = msg.get_unpack("n") + bitmap = msg.get_bytes + return self.new(address, protocol, bitmap) + end + end + + ## + # An IPv6 address record. + + class AAAA < Resource + TypeValue = 28 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + ## + # Creates a new AAAA for +address+. + + def initialize(address) + @address = IPv6.create(address) + end + + ## + # The Gem::Resolv::IPv6 address for this AAAA. + + attr_reader :address + + def encode_rdata(msg) # :nodoc: + msg.put_bytes(@address.address) + end + + def self.decode_rdata(msg) # :nodoc: + return self.new(IPv6.new(msg.get_bytes(16))) + end + end + + ## + # SRV resource record defined in RFC 2782 + # + # These records identify the hostname and port that a service is + # available at. + + class SRV < Resource + TypeValue = 33 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + + # Create a SRV resource record. + # + # See the documentation for #priority, #weight, #port and #target + # for +priority+, +weight+, +port and +target+ respectively. + + def initialize(priority, weight, port, target) + @priority = priority.to_int + @weight = weight.to_int + @port = port.to_int + @target = Name.create(target) + end + + # The priority of this target host. + # + # A client MUST attempt to contact the target host with the + # lowest-numbered priority it can reach; target hosts with the same + # priority SHOULD be tried in an order defined by the weight field. + # The range is 0-65535. Note that it is not widely implemented and + # should be set to zero. + + attr_reader :priority + + # A server selection mechanism. + # + # The weight field specifies a relative weight for entries with the + # same priority. Larger weights SHOULD be given a proportionately + # higher probability of being selected. The range of this number is + # 0-65535. Domain administrators SHOULD use Weight 0 when there + # isn't any server selection to do, to make the RR easier to read + # for humans (less noisy). Note that it is not widely implemented + # and should be set to zero. + + attr_reader :weight + + # The port on this target host of this service. + # + # The range is 0-65535. + + attr_reader :port + + # The domain name of the target host. + # + # A target of "." means that the service is decidedly not available + # at this domain. + + attr_reader :target + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_pack("n", @weight) + msg.put_pack("n", @port) + msg.put_name(@target, compress: false) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + weight, = msg.get_unpack("n") + port, = msg.get_unpack("n") + target = msg.get_name + return self.new(priority, weight, port, target) + end + end + + ## + # Common implementation for SVCB-compatible resource records. + + class ServiceBinding + + ## + # Create a service binding resource record. + + def initialize(priority, target, params = []) + @priority = priority.to_int + @target = Name.create(target) + @params = SvcParams.new(params) + end + + ## + # The priority of this target host. + # + # The range is 0-65535. + # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode. + + attr_reader :priority + + ## + # The domain name of the target host. + + attr_reader :target + + ## + # The service parameters for the target host. + + attr_reader :params + + ## + # Whether this RR is in AliasMode. + + def alias_mode? + self.priority == 0 + end + + ## + # Whether this RR is in ServiceMode. + + def service_mode? + !alias_mode? + end + + def encode_rdata(msg) # :nodoc: + msg.put_pack("n", @priority) + msg.put_name(@target, compress: false) + @params.encode(msg) + end + + def self.decode_rdata(msg) # :nodoc: + priority, = msg.get_unpack("n") + target = msg.get_name + params = SvcParams.decode(msg) + return self.new(priority, target, params) + end + end + + ## + # SVCB resource record [RFC9460] + + class SVCB < ServiceBinding + TypeValue = 64 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end + + ## + # HTTPS resource record [RFC9460] + + class HTTPS < ServiceBinding + TypeValue = 65 + ClassValue = IN::ClassValue + ClassHash[[TypeValue, ClassValue]] = self # :nodoc: + end + end + end + end + + ## + # A Gem::Resolv::DNS IPv4 address. + + class IPv4 + + Regex256 = /0 + |1(?:[0-9][0-9]?)? + |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? + |[3-9][0-9]?/x # :nodoc: + + ## + # Regular expression IPv4 addresses must match. + Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + + ## + # Creates a new IPv4 address from +arg+ which may be: + # + # IPv4:: returns +arg+. + # String:: +arg+ must match the IPv4::Regex constant + + def self.create(arg) + case arg + when IPv4 + return arg + when Regex + if (0..255) === (a = $1.to_i) && + (0..255) === (b = $2.to_i) && + (0..255) === (c = $3.to_i) && + (0..255) === (d = $4.to_i) + return self.new([a, b, c, d].pack("CCCC")) + else + raise ArgumentError.new("IPv4 address with invalid value: " + arg) + end + else + raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}") + end + end + + def initialize(address) # :nodoc: + unless address.kind_of?(String) + raise ArgumentError, 'IPv4 address must be a string' + end + unless address.length == 4 + raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" + end + @address = address + end + + ## + # A String representation of this IPv4 address. + + ## + # The raw IPv4 address as a String. + + attr_reader :address + + def to_s # :nodoc: + return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC")) + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + ## + # Turns this IPv4 address into a Gem::Resolv::DNS::Name. + + def to_name + return DNS::Name.create( + '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse) + end + + def ==(other) # :nodoc: + return @address == other.address + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @address.hash + end + end + + ## + # A Gem::Resolv::DNS IPv6 address. + + class IPv6 + + ## + # IPv6 address format a:b:c:d:e:f:g:h + Regex_8Hex = /\A + (?:[0-9A-Fa-f]{1,4}:){7} + [0-9A-Fa-f]{1,4} + \z/x + + ## + # Compressed IPv6 address format a::b + + Regex_CompressedHex = /\A + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + \z/x + + ## + # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z + + Regex_6Hex4Dec = /\A + ((?:[0-9A-Fa-f]{1,4}:){6,6}) + (\d+)\.(\d+)\.(\d+)\.(\d+) + \z/x + + ## + # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z + + Regex_CompressedHex4Dec = /\A + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}:)*) + (\d+)\.(\d+)\.(\d+)\.(\d+) + \z/x + + ## + # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1 + Regex_8HexLinkLocal = /\A + [Ff][Ee]80 + (?::[0-9A-Fa-f]{1,4}){7} + %[-0-9A-Za-z._~]+ + \z/x + + ## + # Compressed IPv6 link local address format fe80::b%em1 + + Regex_CompressedHexLinkLocal = /\A + [Ff][Ee]80: + (?: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: + ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + | + :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) + )? + :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+ + \z/x + + ## + # A composite IPv6 address Regexp. + + Regex = / + (?:#{Regex_8Hex}) | + (?:#{Regex_CompressedHex}) | + (?:#{Regex_6Hex4Dec}) | + (?:#{Regex_CompressedHex4Dec}) | + (?:#{Regex_8HexLinkLocal}) | + (?:#{Regex_CompressedHexLinkLocal}) + /x + + ## + # Creates a new IPv6 address from +arg+ which may be: + # + # IPv6:: returns +arg+. + # String:: +arg+ must match one of the IPv6::Regex* constants + + def self.create(arg) + case arg + when IPv6 + return arg + when String + address = ''.b + if Regex_8Hex =~ arg + arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} + elsif Regex_CompressedHex =~ arg + prefix = $1 + suffix = $2 + a1 = ''.b + a2 = ''.b + prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} + suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} + omitlen = 16 - a1.length - a2.length + address << a1 << "\0" * omitlen << a2 + elsif Regex_6Hex4Dec =~ arg + prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i + if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d + prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} + address << [a, b, c, d].pack('CCCC') + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + elsif Regex_CompressedHex4Dec =~ arg + prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i + if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d + a1 = ''.b + a2 = ''.b + prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} + suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} + omitlen = 12 - a1.length - a2.length + address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC') + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + else + raise ArgumentError.new("not numeric IPv6 address: " + arg) + end + return IPv6.new(address) + else + raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") + end + end + + def initialize(address) # :nodoc: + unless address.kind_of?(String) && address.length == 16 + raise ArgumentError.new('IPv6 address must be 16 bytes') + end + @address = address + end + + ## + # The raw IPv6 address as a String. + + attr_reader :address + + def to_s # :nodoc: + sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::') + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + ## + # Turns this IPv6 address into a Gem::Resolv::DNS::Name. + #-- + # ip6.arpa should be searched too. [RFC3152] + + def to_name + return DNS::Name.new( + @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa']) + end + + def ==(other) # :nodoc: + return @address == other.address + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @address.hash + end + end + + ## + # Gem::Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly + # makes queries to the mDNS addresses without understanding anything about + # multicast ports. + # + # Information taken form the following places: + # + # * RFC 6762 + + class MDNS < DNS + + ## + # Default mDNS Port + + Port = 5353 + + ## + # Default IPv4 mDNS address + + AddressV4 = '224.0.0.251' + + ## + # Default IPv6 mDNS address + + AddressV6 = 'ff02::fb' + + ## + # Default mDNS addresses + + Addresses = [ + [AddressV4, Port], + [AddressV6, Port], + ] + + ## + # Creates a new one-shot Multicast DNS (mDNS) resolver. + # + # +config_info+ can be: + # + # nil:: + # Uses the default mDNS addresses + # + # Hash:: + # Must contain :nameserver or :nameserver_port like + # Gem::Resolv::DNS#initialize. + + def initialize(config_info=nil) + if config_info then + super({ nameserver_port: Addresses }.merge(config_info)) + else + super(nameserver_port: Addresses) + end + end + + ## + # Iterates over all IP addresses for +name+ retrieved from the mDNS + # resolver, provided name ends with "local". If the name does not end in + # "local" no records will be returned. + # + # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will + # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6 + + def each_address(name) + name = Gem::Resolv::DNS::Name.create(name) + + return unless name[-1].to_s == 'local' + + super(name) + end + + def make_udp_requester # :nodoc: + nameserver_port = @config.nameserver_port + Requester::MDNSOneShot.new(*nameserver_port) + end + + end + + module LOC # :nodoc: + + ## + # A Gem::Resolv::LOC::Size + + class Size + + # Regular expression LOC size must match. + + Regex = /^(\d+\.*\d*)[m]$/ + + ## + # Creates a new LOC::Size from +arg+ which may be: + # + # LOC::Size:: returns +arg+. + # String:: +arg+ must match the LOC::Size::Regex constant + + def self.create(arg) + case arg + when Size + return arg + when String + scalar = '' + if Regex =~ arg + scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C") + else + raise ArgumentError.new("not a properly formed Size string: " + arg) + end + return Size.new(scalar) + else + raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(scalar) + @scalar = scalar + end + + ## + # The raw size + + attr_reader :scalar + + def to_s # :nodoc: + s = @scalar.unpack("H2").join.to_s + return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @scalar == other.scalar + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @scalar.hash + end + + end + + ## + # A Gem::Resolv::LOC::Coord + + class Coord + + # Regular expression LOC Coord must match. + + Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ + + ## + # Creates a new LOC::Coord from +arg+ which may be: + # + # LOC::Coord:: returns +arg+. + # String:: +arg+ must match the LOC::Coord::Regex constant + + def self.create(arg) + case arg + when Coord + return arg + when String + coordinates = '' + if Regex =~ arg && $1.to_f < 180 + m = $~ + hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1 + coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) + + (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N") + orientation = m[4][/[NS]/] ? 'lat' : 'lon' + else + raise ArgumentError.new("not a properly formed Coord string: " + arg) + end + return Coord.new(coordinates,orientation) + else + raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(coordinates,orientation) + unless coordinates.kind_of?(String) + raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") + end + unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/] + raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') + end + @coordinates = coordinates + @orientation = orientation + end + + ## + # The raw coordinates + + attr_reader :coordinates + + ## The orientation of the hemisphere as 'lat' or 'lon' + + attr_reader :orientation + + def to_s # :nodoc: + c = @coordinates.unpack("N").join.to_i + val = (c - (2**31)).abs + fracsecs = (val % 1e3).to_i.to_s + val = val / 1e3 + secs = (val % 60).to_i.to_s + val = val / 60 + mins = (val % 60).to_i.to_s + degs = (val / 60).to_i.to_s + posi = (c >= 2**31) + case posi + when true + hemi = @orientation[/^lat$/] ? "N" : "E" + else + hemi = @orientation[/^lon$/] ? "W" : "S" + end + return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @coordinates == other.coordinates + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @coordinates.hash + end + + end + + ## + # A Gem::Resolv::LOC::Alt + + class Alt + + # Regular expression LOC Alt must match. + + Regex = /^([+-]*\d+\.*\d*)[m]$/ + + ## + # Creates a new LOC::Alt from +arg+ which may be: + # + # LOC::Alt:: returns +arg+. + # String:: +arg+ must match the LOC::Alt::Regex constant + + def self.create(arg) + case arg + when Alt + return arg + when String + altitude = '' + if Regex =~ arg + altitude = [($1.to_f*(1e2))+(1e7)].pack("N") + else + raise ArgumentError.new("not a properly formed Alt string: " + arg) + end + return Alt.new(altitude) + else + raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") + end + end + + # Internal use; use self.create. + def initialize(altitude) + @altitude = altitude + end + + ## + # The raw altitude + + attr_reader :altitude + + def to_s # :nodoc: + a = @altitude.unpack("N").join.to_i + return ((a.to_f/1e2)-1e5).to_s + "m" + end + + def inspect # :nodoc: + return "#<#{self.class} #{self}>" + end + + def ==(other) # :nodoc: + return @altitude == other.altitude + end + + def eql?(other) # :nodoc: + return self == other + end + + def hash # :nodoc: + return @altitude.hash + end + + end + + end + + ## + # Default resolver to use for Gem::Resolv class methods. + + DefaultResolver = self.new + + ## + # Replaces the resolvers in the default resolver with +new_resolvers+. This + # allows resolvers to be changed for resolv-replace. + + def DefaultResolver.replace_resolvers new_resolvers + @resolvers = new_resolvers + end + + ## + # Address Regexp to use for matching IP addresses. + + AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ + +end diff --git a/lib/rubygems/vendor/securerandom/lib/securerandom.rb b/lib/rubygems/vendor/securerandom/lib/securerandom.rb new file mode 100644 index 0000000000..b6f1d71ad3 --- /dev/null +++ b/lib/rubygems/vendor/securerandom/lib/securerandom.rb @@ -0,0 +1,102 @@ +# -*- coding: us-ascii -*- +# frozen_string_literal: true + +require 'random/formatter' + +# == Secure random number generator interface. +# +# This library is an interface to secure random number generators which are +# suitable for generating session keys in HTTP cookies, etc. +# +# You can use this library in your application by requiring it: +# +# require 'rubygems/vendor/securerandom/lib/securerandom' +# +# It supports the following secure random number generators: +# +# * openssl +# * /dev/urandom +# * Win32 +# +# Gem::SecureRandom is extended by the Random::Formatter module which +# defines the following methods: +# +# * alphanumeric +# * base64 +# * choose +# * gen_random +# * hex +# * rand +# * random_bytes +# * random_number +# * urlsafe_base64 +# * uuid +# +# These methods are usable as class methods of Gem::SecureRandom such as +# +Gem::SecureRandom.hex+. +# +# If a secure random number generator is not available, +# +NotImplementedError+ is raised. + +module Gem::SecureRandom + + # The version + VERSION = "0.4.1" + + class << self + # Returns a random binary string containing +size+ bytes. + # + # See Random.bytes + def bytes(n) + return gen_random(n) + end + + # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2 + def alphanumeric(n = nil, chars: ALPHANUMERIC) + n = 16 if n.nil? + choose(chars, n) + end if RUBY_VERSION < '3.3' + + private + + # :stopdoc: + + # Implementation using OpenSSL + def gen_random_openssl(n) + return OpenSSL::Random.random_bytes(n) + end + + # Implementation using system random device + def gen_random_urandom(n) + ret = Random.urandom(n) + unless ret + raise NotImplementedError, "No random device" + end + unless ret.length == n + raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes" + end + ret + end + + begin + # Check if Random.urandom is available + Random.urandom(1) + alias gen_random gen_random_urandom + rescue RuntimeError + begin + require 'openssl' + rescue NoMethodError + raise NotImplementedError, "No random device" + else + alias gen_random gen_random_openssl + end + end + + # :startdoc: + + # Generate random data bytes for Random::Formatter + public :gen_random + end +end + +Gem::SecureRandom.extend(Random::Formatter) diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb new file mode 100644 index 0000000000..376b8c0e2b --- /dev/null +++ b/lib/rubygems/vendor/timeout/lib/timeout.rb @@ -0,0 +1,201 @@ +# frozen_string_literal: true +# Timeout long-running blocks +# +# == Synopsis +# +# require 'rubygems/vendor/timeout/lib/timeout' +# status = Gem::Timeout.timeout(5) { +# # Something that should be interrupted if it takes more than 5 seconds... +# } +# +# == Description +# +# Gem::Timeout provides a way to auto-terminate a potentially long-running +# operation if it hasn't finished in a fixed amount of time. +# +# == Copyright +# +# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. +# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan + +module Gem::Timeout + # The version + VERSION = "0.4.4" + + # Internal error raised to when a timeout is triggered. + class ExitException < Exception + def exception(*) # :nodoc: + self + end + end + + # Raised by Gem::Timeout.timeout when the block times out. + class Error < RuntimeError + def self.handle_timeout(message) # :nodoc: + exc = ExitException.new(message) + + begin + yield exc + rescue ExitException => e + raise new(message) if exc.equal?(e) + raise + end + end + end + + # :stopdoc: + CONDVAR = ConditionVariable.new + QUEUE = Queue.new + QUEUE_MUTEX = Mutex.new + TIMEOUT_THREAD_MUTEX = Mutex.new + @timeout_thread = nil + private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX + + class Request + attr_reader :deadline + + def initialize(thread, timeout, exception_class, message) + @thread = thread + @deadline = GET_TIME.call(Process::CLOCK_MONOTONIC) + timeout + @exception_class = exception_class + @message = message + + @mutex = Mutex.new + @done = false # protected by @mutex + end + + def done? + @mutex.synchronize do + @done + end + end + + def expired?(now) + now >= @deadline + end + + def interrupt + @mutex.synchronize do + unless @done + @thread.raise @exception_class, @message + @done = true + end + end + end + + def finished + @mutex.synchronize do + @done = true + end + end + end + private_constant :Request + + def self.create_timeout_thread + watcher = Thread.new do + requests = [] + while true + until QUEUE.empty? and !requests.empty? # wait to have at least one request + req = QUEUE.pop + requests << req unless req.done? + end + closest_deadline = requests.min_by(&:deadline).deadline + + now = 0.0 + QUEUE_MUTEX.synchronize do + while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty? + CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now) + end + end + + requests.each do |req| + req.interrupt if req.expired?(now) + end + requests.reject!(&:done?) + end + end + ThreadGroup::Default.add(watcher) unless watcher.group.enclosed? + watcher.name = "Gem::Timeout stdlib thread" + watcher.thread_variable_set(:"\0__detached_thread__", true) + watcher + end + private_class_method :create_timeout_thread + + def self.ensure_timeout_thread_created + unless @timeout_thread and @timeout_thread.alive? + # If the Mutex is already owned we are in a signal handler. + # In that case, just return and let the main thread create the @timeout_thread. + return if TIMEOUT_THREAD_MUTEX.owned? + TIMEOUT_THREAD_MUTEX.synchronize do + unless @timeout_thread and @timeout_thread.alive? + @timeout_thread = create_timeout_thread + end + end + end + end + + # We keep a private reference so that time mocking libraries won't break + # Gem::Timeout. + GET_TIME = Process.method(:clock_gettime) + private_constant :GET_TIME + + # :startdoc: + + # Perform an operation in a block, raising an error if it takes longer than + # +sec+ seconds to complete. + # + # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number + # or nil may be used, including Floats to specify fractional seconds. A + # value of 0 or +nil+ will execute the block without any timeout. + # Any negative number will raise an ArgumentError. + # +klass+:: Exception Class to raise if the block fails to terminate + # in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error + # +message+:: Error message to raise with Exception Class. + # Omitting will use the default, "execution expired" + # + # Returns the result of the block *if* the block completed before + # +sec+ seconds, otherwise throws an exception, based on the value of +klass+. + # + # The exception thrown to terminate the given block cannot be rescued inside + # the block unless +klass+ is given explicitly. However, the block can use + # ensure to prevent the handling of the exception. For that reason, this + # method cannot be relied on to enforce timeouts for untrusted blocks. + # + # If a scheduler is defined, it will be used to handle the timeout by invoking + # Scheduler#timeout_after. + # + # Note that this is both a method of module Gem::Timeout, so you can <tt>include + # Gem::Timeout</tt> into your classes so they have a #timeout method, as well as + # a module method, so you can call it directly as Gem::Timeout.timeout(). + def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+ + return yield(sec) if sec == nil or sec.zero? + raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec + + message ||= "execution expired" + + if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after) + return scheduler.timeout_after(sec, klass || Error, message, &block) + end + + Gem::Timeout.ensure_timeout_thread_created + perform = Proc.new do |exc| + request = Request.new(Thread.current, sec, exc, message) + QUEUE_MUTEX.synchronize do + QUEUE << request + CONDVAR.signal + end + begin + return yield(sec) + ensure + request.finished + end + end + + if klass + perform.call(klass) + else + Error.handle_timeout(message, &perform) + end + end + module_function :timeout +end diff --git a/lib/rubygems/tsort/lib/tsort.rb b/lib/rubygems/vendor/tsort/lib/tsort.rb index f825f14257..9dd7c09521 100644 --- a/lib/rubygems/tsort/lib/tsort.rb +++ b/lib/rubygems/vendor/tsort/lib/tsort.rb @@ -32,7 +32,7 @@ # method, which fetches the array of child nodes and then iterates over that # array using the user-supplied block. # -# require 'rubygems/tsort/lib/tsort' +# require 'rubygems/vendor/tsort/lib/tsort' # # class Hash # include Gem::TSort @@ -52,7 +52,7 @@ # # A very simple `make' like tool can be implemented as follows: # -# require 'rubygems/tsort/lib/tsort' +# require 'rubygems/vendor/tsort/lib/tsort' # # class Make # def initialize @@ -122,6 +122,9 @@ # module Gem::TSort + + VERSION = "0.2.0" + class Cyclic < StandardError end diff --git a/lib/rubygems/vendor/uri/lib/uri.rb b/lib/rubygems/vendor/uri/lib/uri.rb new file mode 100644 index 0000000000..4691b122b2 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: false +# Gem::URI is a module providing classes to handle Uniform Resource Identifiers +# (RFC2396[https://www.rfc-editor.org/rfc/rfc2396]). +# +# == Features +# +# * Uniform way of handling URIs. +# * Flexibility to introduce custom Gem::URI schemes. +# * Flexibility to have an alternate Gem::URI::Parser (or just different patterns +# and regexp's). +# +# == Basic example +# +# require 'rubygems/vendor/uri/lib/uri' +# +# uri = Gem::URI("http://foo.com/posts?id=30&limit=5#time=1305298413") +# #=> #<Gem::URI::HTTP http://foo.com/posts?id=30&limit=5#time=1305298413> +# +# uri.scheme #=> "http" +# uri.host #=> "foo.com" +# uri.path #=> "/posts" +# uri.query #=> "id=30&limit=5" +# uri.fragment #=> "time=1305298413" +# +# uri.to_s #=> "http://foo.com/posts?id=30&limit=5#time=1305298413" +# +# == Adding custom URIs +# +# module Gem::URI +# class RSYNC < Generic +# DEFAULT_PORT = 873 +# end +# register_scheme 'RSYNC', RSYNC +# end +# #=> Gem::URI::RSYNC +# +# Gem::URI.scheme_list +# #=> {"FILE"=>Gem::URI::File, "FTP"=>Gem::URI::FTP, "HTTP"=>Gem::URI::HTTP, +# # "HTTPS"=>Gem::URI::HTTPS, "LDAP"=>Gem::URI::LDAP, "LDAPS"=>Gem::URI::LDAPS, +# # "MAILTO"=>Gem::URI::MailTo, "RSYNC"=>Gem::URI::RSYNC} +# +# uri = Gem::URI("rsync://rsync.foo.com") +# #=> #<Gem::URI::RSYNC rsync://rsync.foo.com> +# +# == RFC References +# +# A good place to view an RFC spec is http://www.ietf.org/rfc.html. +# +# Here is a list of all related RFC's: +# - RFC822[https://www.rfc-editor.org/rfc/rfc822] +# - RFC1738[https://www.rfc-editor.org/rfc/rfc1738] +# - RFC2255[https://www.rfc-editor.org/rfc/rfc2255] +# - RFC2368[https://www.rfc-editor.org/rfc/rfc2368] +# - RFC2373[https://www.rfc-editor.org/rfc/rfc2373] +# - RFC2396[https://www.rfc-editor.org/rfc/rfc2396] +# - RFC2732[https://www.rfc-editor.org/rfc/rfc2732] +# - RFC3986[https://www.rfc-editor.org/rfc/rfc3986] +# +# == Class tree +# +# - Gem::URI::Generic (in uri/generic.rb) +# - Gem::URI::File - (in uri/file.rb) +# - Gem::URI::FTP - (in uri/ftp.rb) +# - Gem::URI::HTTP - (in uri/http.rb) +# - Gem::URI::HTTPS - (in uri/https.rb) +# - Gem::URI::LDAP - (in uri/ldap.rb) +# - Gem::URI::LDAPS - (in uri/ldaps.rb) +# - Gem::URI::MailTo - (in uri/mailto.rb) +# - Gem::URI::Parser - (in uri/common.rb) +# - Gem::URI::REGEXP - (in uri/common.rb) +# - Gem::URI::REGEXP::PATTERN - (in uri/common.rb) +# - Gem::URI::Util - (in uri/common.rb) +# - Gem::URI::Error - (in uri/common.rb) +# - Gem::URI::InvalidURIError - (in uri/common.rb) +# - Gem::URI::InvalidComponentError - (in uri/common.rb) +# - Gem::URI::BadURIError - (in uri/common.rb) +# +# == Copyright Info +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# Documentation:: +# Akira Yamada <akira@ruby-lang.org> +# Dmitry V. Sabanin <sdmitry@lrn.ru> +# Vincent Batts <vbatts@hashbangbash.com> +# License:: +# Copyright (c) 2001 akira yamada <akira@ruby-lang.org> +# You can redistribute it and/or modify it under the same term as Ruby. +# + +module Gem::URI +end + +require_relative 'uri/version' +require_relative 'uri/common' +require_relative 'uri/generic' +require_relative 'uri/file' +require_relative 'uri/ftp' +require_relative 'uri/http' +require_relative 'uri/https' +require_relative 'uri/ldap' +require_relative 'uri/ldaps' +require_relative 'uri/mailto' +require_relative 'uri/ws' +require_relative 'uri/wss' diff --git a/lib/rubygems/vendor/uri/lib/uri/common.rb b/lib/rubygems/vendor/uri/lib/uri/common.rb new file mode 100644 index 0000000000..e9bdfa6a07 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/common.rb @@ -0,0 +1,922 @@ +# frozen_string_literal: true +#-- +# = uri/common.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative "rfc2396_parser" +require_relative "rfc3986_parser" + +module Gem::URI + # The default parser instance for RFC 2396. + RFC2396_PARSER = RFC2396_Parser.new + Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor) + + # The default parser instance for RFC 3986. + RFC3986_PARSER = RFC3986_Parser.new + Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor) + + # The default parser instance. + DEFAULT_PARSER = RFC3986_PARSER + Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor) + + # Set the default parser instance. + def self.parser=(parser = RFC3986_PARSER) + remove_const(:Parser) if defined?(::Gem::URI::Parser) + const_set("Parser", parser.class) + + remove_const(:PARSER) if defined?(::Gem::URI::PARSER) + const_set("PARSER", parser) + + remove_const(:REGEXP) if defined?(::Gem::URI::REGEXP) + remove_const(:PATTERN) if defined?(::Gem::URI::PATTERN) + if Parser == RFC2396_Parser + const_set("REGEXP", Gem::URI::RFC2396_REGEXP) + const_set("PATTERN", Gem::URI::RFC2396_REGEXP::PATTERN) + end + + Parser.new.regexp.each_pair do |sym, str| + remove_const(sym) if const_defined?(sym, false) + const_set(sym, str) + end + end + self.parser = RFC3986_PARSER + + def self.const_missing(const) # :nodoc: + if const == :REGEXP + warn "Gem::URI::REGEXP is obsolete. Use Gem::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE + Gem::URI::RFC2396_REGEXP + elsif value = RFC2396_PARSER.regexp[const] + warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE + value + elsif value = RFC2396_Parser.const_get(const) + warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE + value + else + super + end + end + + module Util # :nodoc: + def make_components_hash(klass, array_hash) + tmp = {} + if array_hash.kind_of?(Array) && + array_hash.size == klass.component.size - 1 + klass.component[1..-1].each_index do |i| + begin + tmp[klass.component[i + 1]] = array_hash[i].clone + rescue TypeError + tmp[klass.component[i + 1]] = array_hash[i] + end + end + + elsif array_hash.kind_of?(Hash) + array_hash.each do |key, value| + begin + tmp[key] = value.clone + rescue TypeError + tmp[key] = value + end + end + else + raise ArgumentError, + "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})" + end + tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase + + return tmp + end + module_function :make_components_hash + end + + module Schemes # :nodoc: + class << self + ReservedChars = ".+-" + EscapedChars = "\u01C0\u01C1\u01C2" + # Use Lo category chars as escaped chars for TruffleRuby, which + # does not allow Symbol categories as identifiers. + + def escape(name) + unless name and name.ascii_only? + return nil + end + name.upcase.tr(ReservedChars, EscapedChars) + end + + def unescape(name) + name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase + end + + def find(name) + const_get(name, false) if name and const_defined?(name, false) + end + + def register(name, klass) + unless scheme = escape(name) + raise ArgumentError, "invalid character as scheme - #{name}" + end + const_set(scheme, klass) + end + + def list + constants.map { |name| + [unescape(name.to_s), const_get(name)] + }.to_h + end + end + end + private_constant :Schemes + + # Registers the given +klass+ as the class to be instantiated + # when parsing a \Gem::URI with the given +scheme+: + # + # Gem::URI.register_scheme('MS_SEARCH', Gem::URI::Generic) # => Gem::URI::Generic + # Gem::URI.scheme_list['MS_SEARCH'] # => Gem::URI::Generic + # + # Note that after calling String#upcase on +scheme+, it must be a valid + # constant name. + def self.register_scheme(scheme, klass) + Schemes.register(scheme, klass) + end + + # Returns a hash of the defined schemes: + # + # Gem::URI.scheme_list + # # => + # {"MAILTO"=>Gem::URI::MailTo, + # "LDAPS"=>Gem::URI::LDAPS, + # "WS"=>Gem::URI::WS, + # "HTTP"=>Gem::URI::HTTP, + # "HTTPS"=>Gem::URI::HTTPS, + # "LDAP"=>Gem::URI::LDAP, + # "FILE"=>Gem::URI::File, + # "FTP"=>Gem::URI::FTP} + # + # Related: Gem::URI.register_scheme. + def self.scheme_list + Schemes.list + end + + # :stopdoc: + INITIAL_SCHEMES = scheme_list + private_constant :INITIAL_SCHEMES + Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor) + # :startdoc: + + # Returns a new object constructed from the given +scheme+, +arguments+, + # and +default+: + # + # - The new object is an instance of <tt>Gem::URI.scheme_list[scheme.upcase]</tt>. + # - The object is initialized by calling the class initializer + # using +scheme+ and +arguments+. + # See Gem::URI::Generic.new. + # + # Examples: + # + # values = ['john.doe', 'www.example.com', '123', nil, '/forum/questions/', nil, 'tag=networking&order=newest', 'top'] + # Gem::URI.for('https', *values) + # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # Gem::URI.for('foo', *values, default: Gem::URI::HTTP) + # # => #<Gem::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # + def self.for(scheme, *arguments, default: Generic) + const_name = Schemes.escape(scheme) + + uri_class = INITIAL_SCHEMES[const_name] + uri_class ||= Schemes.find(const_name) + uri_class ||= default + + return uri_class.new(scheme, *arguments) + end + + # + # Base class for all Gem::URI exceptions. + # + class Error < StandardError; end + # + # Not a Gem::URI. + # + class InvalidURIError < Error; end + # + # Not a Gem::URI component. + # + class InvalidComponentError < Error; end + # + # Gem::URI is valid, bad usage is not. + # + class BadURIError < Error; end + + # Returns a 9-element array representing the parts of the \Gem::URI + # formed from the string +uri+; + # each array element is a string or +nil+: + # + # names = %w[scheme userinfo host port registry path opaque query fragment] + # values = Gem::URI.split('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # names.zip(values) + # # => + # [["scheme", "https"], + # ["userinfo", "john.doe"], + # ["host", "www.example.com"], + # ["port", "123"], + # ["registry", nil], + # ["path", "/forum/questions/"], + # ["opaque", nil], + # ["query", "tag=networking&order=newest"], + # ["fragment", "top"]] + # + def self.split(uri) + PARSER.split(uri) + end + + # Returns a new \Gem::URI object constructed from the given string +uri+: + # + # Gem::URI.parse('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # Gem::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top') + # # => #<Gem::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top> + # + # It's recommended to first Gem::URI::RFC2396_PARSER.escape string +uri+ + # if it may contain invalid Gem::URI characters. + # + def self.parse(uri) + PARSER.parse(uri) + end + + # Merges the given Gem::URI strings +str+ + # per {RFC 2396}[https://www.rfc-editor.org/rfc/rfc2396.html]. + # + # Each string in +str+ is converted to an + # {RFC3986 Gem::URI}[https://www.rfc-editor.org/rfc/rfc3986.html] before being merged. + # + # Examples: + # + # Gem::URI.join("http://example.com/","main.rbx") + # # => #<Gem::URI::HTTP http://example.com/main.rbx> + # + # Gem::URI.join('http://example.com', 'foo') + # # => #<Gem::URI::HTTP http://example.com/foo> + # + # Gem::URI.join('http://example.com', '/foo', '/bar') + # # => #<Gem::URI::HTTP http://example.com/bar> + # + # Gem::URI.join('http://example.com', '/foo', 'bar') + # # => #<Gem::URI::HTTP http://example.com/bar> + # + # Gem::URI.join('http://example.com', '/foo/', 'bar') + # # => #<Gem::URI::HTTP http://example.com/foo/bar> + # + def self.join(*str) + DEFAULT_PARSER.join(*str) + end + + # + # == Synopsis + # + # Gem::URI::extract(str[, schemes][,&blk]) + # + # == Args + # + # +str+:: + # String to extract URIs from. + # +schemes+:: + # Limit Gem::URI matching to specific schemes. + # + # == Description + # + # Extracts URIs from a string. If block given, iterates through all matched URIs. + # Returns nil if block given or array with matches. + # + # == Usage + # + # require "rubygems/vendor/uri/lib/uri" + # + # Gem::URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.") + # # => ["http://foo.example.com/bla", "mailto:test@example.com"] + # + def self.extract(str, schemes = nil, &block) # :nodoc: + warn "Gem::URI.extract is obsolete", uplevel: 1 if $VERBOSE + PARSER.extract(str, schemes, &block) + end + + # + # == Synopsis + # + # Gem::URI::regexp([match_schemes]) + # + # == Args + # + # +match_schemes+:: + # Array of schemes. If given, resulting regexp matches to URIs + # whose scheme is one of the match_schemes. + # + # == Description + # + # Returns a Regexp object which matches to Gem::URI-like strings. + # The Regexp object returned by this method includes arbitrary + # number of capture group (parentheses). Never rely on its number. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # # extract first Gem::URI from html_string + # html_string.slice(Gem::URI.regexp) + # + # # remove ftp URIs + # html_string.sub(Gem::URI.regexp(['ftp']), '') + # + # # You should not rely on the number of parentheses + # html_string.scan(Gem::URI.regexp) do |*matches| + # p $& + # end + # + def self.regexp(schemes = nil)# :nodoc: + warn "Gem::URI.regexp is obsolete", uplevel: 1 if $VERBOSE + PARSER.make_regexp(schemes) + end + + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i) + end + TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc: + TBLENCWWWCOMP_[' '] = '+' + TBLENCWWWCOMP_.freeze + TBLDECWWWCOMP_ = {} # :nodoc: + 256.times do |i| + h, l = i>>4, i&15 + TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr + TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr + end + TBLDECWWWCOMP_['+'] = ' ' + TBLDECWWWCOMP_.freeze + + # Returns a URL-encoded string derived from the given string +str+. + # + # The returned string: + # + # - Preserves: + # + # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>. + # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>, + # and <tt>'0'..'9'</tt>. + # + # Example: + # + # Gem::URI.encode_www_form_component('*.-_azAZ09') + # # => "*.-_azAZ09" + # + # - Converts: + # + # - Character <tt>' '</tt> to character <tt>'+'</tt>. + # - Any other character to "percent notation"; + # the percent notation for character <i>c</i> is <tt>'%%%X' % c.ord</tt>. + # + # Example: + # + # Gem::URI.encode_www_form_component('Here are some punctuation characters: ,;?:') + # # => "Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A" + # + # Encoding: + # + # - If +str+ has encoding Encoding::ASCII_8BIT, argument +enc+ is ignored. + # - Otherwise +str+ is converted first to Encoding::UTF_8 + # (with suitable character replacements), + # and then to encoding +enc+. + # + # In either case, the returned string has forced encoding Encoding::US_ASCII. + # + # Related: Gem::URI.encode_uri_component (encodes <tt>' '</tt> as <tt>'%20'</tt>). + def self.encode_www_form_component(str, enc=nil) + _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_, str, enc) + end + + # Returns a string decoded from the given \URL-encoded string +str+. + # + # The given string is first encoded as Encoding::ASCII-8BIT (using String#b), + # then decoded (as below), and finally force-encoded to the given encoding +enc+. + # + # The returned string: + # + # - Preserves: + # + # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>. + # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>, + # and <tt>'0'..'9'</tt>. + # + # Example: + # + # Gem::URI.decode_www_form_component('*.-_azAZ09') + # # => "*.-_azAZ09" + # + # - Converts: + # + # - Character <tt>'+'</tt> to character <tt>' '</tt>. + # - Each "percent notation" to an ASCII character. + # + # Example: + # + # Gem::URI.decode_www_form_component('Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A') + # # => "Here are some punctuation characters: ,;?:" + # + # Related: Gem::URI.decode_uri_component (preserves <tt>'+'</tt>). + def self.decode_www_form_component(str, enc=Encoding::UTF_8) + _decode_uri_component(/\+|%\h\h/, str, enc) + end + + # Like Gem::URI.encode_www_form_component, except that <tt>' '</tt> (space) + # is encoded as <tt>'%20'</tt> (instead of <tt>'+'</tt>). + def self.encode_uri_component(str, enc=nil) + _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCURICOMP_, str, enc) + end + + # Like Gem::URI.decode_www_form_component, except that <tt>'+'</tt> is preserved. + def self.decode_uri_component(str, enc=Encoding::UTF_8) + _decode_uri_component(/%\h\h/, str, enc) + end + + # Returns a string derived from the given string +str+ with + # Gem::URI-encoded characters matching +regexp+ according to +table+. + def self._encode_uri_component(regexp, table, str, enc) + str = str.to_s.dup + if str.encoding != Encoding::ASCII_8BIT + if enc && enc != Encoding::ASCII_8BIT + str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) + str.encode!(enc, fallback: ->(x){"&##{x.ord};"}) + end + str.force_encoding(Encoding::ASCII_8BIT) + end + str.gsub!(regexp, table) + str.force_encoding(Encoding::US_ASCII) + end + private_class_method :_encode_uri_component + + # Returns a string decoding characters matching +regexp+ from the + # given \URL-encoded string +str+. + def self._decode_uri_component(regexp, str, enc) + raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str) + str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc) + end + private_class_method :_decode_uri_component + + # Returns a URL-encoded string derived from the given + # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes] + # +enum+. + # + # The result is suitable for use as form data + # for an \HTTP request whose <tt>Content-Type</tt> is + # <tt>'application/x-www-form-urlencoded'</tt>. + # + # The returned string consists of the elements of +enum+, + # each converted to one or more URL-encoded strings, + # and all joined with character <tt>'&'</tt>. + # + # Simple examples: + # + # Gem::URI.encode_www_form([['foo', 0], ['bar', 1], ['baz', 2]]) + # # => "foo=0&bar=1&baz=2" + # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2}) + # # => "foo=0&bar=1&baz=2" + # + # The returned string is formed using method Gem::URI.encode_www_form_component, + # which converts certain characters: + # + # Gem::URI.encode_www_form('f#o': '/', 'b-r': '$', 'b z': '@') + # # => "f%23o=%2F&b-r=%24&b+z=%40" + # + # When +enum+ is Array-like, each element +ele+ is converted to a field: + # + # - If +ele+ is an array of two or more elements, + # the field is formed from its first two elements + # (and any additional elements are ignored): + # + # name = Gem::URI.encode_www_form_component(ele[0], enc) + # value = Gem::URI.encode_www_form_component(ele[1], enc) + # "#{name}=#{value}" + # + # Examples: + # + # Gem::URI.encode_www_form([%w[foo bar], %w[baz bat bah]]) + # # => "foo=bar&baz=bat" + # Gem::URI.encode_www_form([['foo', 0], ['bar', :baz, 'bat']]) + # # => "foo=0&bar=baz" + # + # - If +ele+ is an array of one element, + # the field is formed from <tt>ele[0]</tt>: + # + # Gem::URI.encode_www_form_component(ele[0]) + # + # Example: + # + # Gem::URI.encode_www_form([['foo'], [:bar], [0]]) + # # => "foo&bar&0" + # + # - Otherwise the field is formed from +ele+: + # + # Gem::URI.encode_www_form_component(ele) + # + # Example: + # + # Gem::URI.encode_www_form(['foo', :bar, 0]) + # # => "foo&bar&0" + # + # The elements of an Array-like +enum+ may be mixture: + # + # Gem::URI.encode_www_form([['foo', 0], ['bar', 1, 2], ['baz'], :bat]) + # # => "foo=0&bar=1&baz&bat" + # + # When +enum+ is Hash-like, + # each +key+/+value+ pair is converted to one or more fields: + # + # - If +value+ is + # {Array-convertible}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects], + # each element +ele+ in +value+ is paired with +key+ to form a field: + # + # name = Gem::URI.encode_www_form_component(key, enc) + # value = Gem::URI.encode_www_form_component(ele, enc) + # "#{name}=#{value}" + # + # Example: + # + # Gem::URI.encode_www_form({foo: [:bar, 1], baz: [:bat, :bam, 2]}) + # # => "foo=bar&foo=1&baz=bat&baz=bam&baz=2" + # + # - Otherwise, +key+ and +value+ are paired to form a field: + # + # name = Gem::URI.encode_www_form_component(key, enc) + # value = Gem::URI.encode_www_form_component(value, enc) + # "#{name}=#{value}" + # + # Example: + # + # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2}) + # # => "foo=0&bar=1&baz=2" + # + # The elements of a Hash-like +enum+ may be mixture: + # + # Gem::URI.encode_www_form({foo: [0, 1], bar: 2}) + # # => "foo=0&foo=1&bar=2" + # + def self.encode_www_form(enum, enc=nil) + enum.map do |k,v| + if v.nil? + encode_www_form_component(k, enc) + elsif v.respond_to?(:to_ary) + v.to_ary.map do |w| + str = encode_www_form_component(k, enc) + unless w.nil? + str << '=' + str << encode_www_form_component(w, enc) + end + end.join('&') + else + str = encode_www_form_component(k, enc) + str << '=' + str << encode_www_form_component(v, enc) + end + end.join('&') + end + + # Returns name/value pairs derived from the given string +str+, + # which must be an ASCII string. + # + # The method may be used to decode the body of Net::HTTPResponse object +res+ + # for which <tt>res['Content-Type']</tt> is <tt>'application/x-www-form-urlencoded'</tt>. + # + # The returned data is an array of 2-element subarrays; + # each subarray is a name/value pair (both are strings). + # Each returned string has encoding +enc+, + # and has had invalid characters removed via + # {String#scrub}[rdoc-ref:String#scrub]. + # + # A simple example: + # + # Gem::URI.decode_www_form('foo=0&bar=1&baz') + # # => [["foo", "0"], ["bar", "1"], ["baz", ""]] + # + # The returned strings have certain conversions, + # similar to those performed in Gem::URI.decode_www_form_component: + # + # Gem::URI.decode_www_form('f%23o=%2F&b-r=%24&b+z=%40') + # # => [["f#o", "/"], ["b-r", "$"], ["b z", "@"]] + # + # The given string may contain consecutive separators: + # + # Gem::URI.decode_www_form('foo=0&&bar=1&&baz=2') + # # => [["foo", "0"], ["", ""], ["bar", "1"], ["", ""], ["baz", "2"]] + # + # A different separator may be specified: + # + # Gem::URI.decode_www_form('foo=0--bar=1--baz', separator: '--') + # # => [["foo", "0"], ["bar", "1"], ["baz", ""]] + # + def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false) + raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only? + ary = [] + return ary if str.empty? + enc = Encoding.find(enc) + str.b.each_line(separator) do |string| + string.chomp!(separator) + key, sep, val = string.partition('=') + if isindex + if sep.empty? + val = key + key = +'' + end + isindex = false + end + + if use__charset_ and key == '_charset_' and e = get_encoding(val) + enc = e + use__charset_ = false + end + + key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + if val + val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_) + else + val = +'' + end + + ary << [key, val] + end + ary.each do |k, v| + k.force_encoding(enc) + k.scrub! + v.force_encoding(enc) + v.scrub! + end + ary + end + + private +=begin command for WEB_ENCODINGS_ + curl https://encoding.spec.whatwg.org/encodings.json| + ruby -rjson -e 'H={} + h={ + "shift_jis"=>"Windows-31J", + "euc-jp"=>"cp51932", + "iso-2022-jp"=>"cp50221", + "x-mac-cyrillic"=>"macCyrillic", + } + JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x| + Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next + x["labels"].each{|y|H[y]=n} + } + puts "{" + H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]} + puts "}" +' +=end + WEB_ENCODINGS_ = { + "unicode-1-1-utf-8"=>"utf-8", + "utf-8"=>"utf-8", + "utf8"=>"utf-8", + "866"=>"ibm866", + "cp866"=>"ibm866", + "csibm866"=>"ibm866", + "ibm866"=>"ibm866", + "csisolatin2"=>"iso-8859-2", + "iso-8859-2"=>"iso-8859-2", + "iso-ir-101"=>"iso-8859-2", + "iso8859-2"=>"iso-8859-2", + "iso88592"=>"iso-8859-2", + "iso_8859-2"=>"iso-8859-2", + "iso_8859-2:1987"=>"iso-8859-2", + "l2"=>"iso-8859-2", + "latin2"=>"iso-8859-2", + "csisolatin3"=>"iso-8859-3", + "iso-8859-3"=>"iso-8859-3", + "iso-ir-109"=>"iso-8859-3", + "iso8859-3"=>"iso-8859-3", + "iso88593"=>"iso-8859-3", + "iso_8859-3"=>"iso-8859-3", + "iso_8859-3:1988"=>"iso-8859-3", + "l3"=>"iso-8859-3", + "latin3"=>"iso-8859-3", + "csisolatin4"=>"iso-8859-4", + "iso-8859-4"=>"iso-8859-4", + "iso-ir-110"=>"iso-8859-4", + "iso8859-4"=>"iso-8859-4", + "iso88594"=>"iso-8859-4", + "iso_8859-4"=>"iso-8859-4", + "iso_8859-4:1988"=>"iso-8859-4", + "l4"=>"iso-8859-4", + "latin4"=>"iso-8859-4", + "csisolatincyrillic"=>"iso-8859-5", + "cyrillic"=>"iso-8859-5", + "iso-8859-5"=>"iso-8859-5", + "iso-ir-144"=>"iso-8859-5", + "iso8859-5"=>"iso-8859-5", + "iso88595"=>"iso-8859-5", + "iso_8859-5"=>"iso-8859-5", + "iso_8859-5:1988"=>"iso-8859-5", + "arabic"=>"iso-8859-6", + "asmo-708"=>"iso-8859-6", + "csiso88596e"=>"iso-8859-6", + "csiso88596i"=>"iso-8859-6", + "csisolatinarabic"=>"iso-8859-6", + "ecma-114"=>"iso-8859-6", + "iso-8859-6"=>"iso-8859-6", + "iso-8859-6-e"=>"iso-8859-6", + "iso-8859-6-i"=>"iso-8859-6", + "iso-ir-127"=>"iso-8859-6", + "iso8859-6"=>"iso-8859-6", + "iso88596"=>"iso-8859-6", + "iso_8859-6"=>"iso-8859-6", + "iso_8859-6:1987"=>"iso-8859-6", + "csisolatingreek"=>"iso-8859-7", + "ecma-118"=>"iso-8859-7", + "elot_928"=>"iso-8859-7", + "greek"=>"iso-8859-7", + "greek8"=>"iso-8859-7", + "iso-8859-7"=>"iso-8859-7", + "iso-ir-126"=>"iso-8859-7", + "iso8859-7"=>"iso-8859-7", + "iso88597"=>"iso-8859-7", + "iso_8859-7"=>"iso-8859-7", + "iso_8859-7:1987"=>"iso-8859-7", + "sun_eu_greek"=>"iso-8859-7", + "csiso88598e"=>"iso-8859-8", + "csisolatinhebrew"=>"iso-8859-8", + "hebrew"=>"iso-8859-8", + "iso-8859-8"=>"iso-8859-8", + "iso-8859-8-e"=>"iso-8859-8", + "iso-ir-138"=>"iso-8859-8", + "iso8859-8"=>"iso-8859-8", + "iso88598"=>"iso-8859-8", + "iso_8859-8"=>"iso-8859-8", + "iso_8859-8:1988"=>"iso-8859-8", + "visual"=>"iso-8859-8", + "csisolatin6"=>"iso-8859-10", + "iso-8859-10"=>"iso-8859-10", + "iso-ir-157"=>"iso-8859-10", + "iso8859-10"=>"iso-8859-10", + "iso885910"=>"iso-8859-10", + "l6"=>"iso-8859-10", + "latin6"=>"iso-8859-10", + "iso-8859-13"=>"iso-8859-13", + "iso8859-13"=>"iso-8859-13", + "iso885913"=>"iso-8859-13", + "iso-8859-14"=>"iso-8859-14", + "iso8859-14"=>"iso-8859-14", + "iso885914"=>"iso-8859-14", + "csisolatin9"=>"iso-8859-15", + "iso-8859-15"=>"iso-8859-15", + "iso8859-15"=>"iso-8859-15", + "iso885915"=>"iso-8859-15", + "iso_8859-15"=>"iso-8859-15", + "l9"=>"iso-8859-15", + "iso-8859-16"=>"iso-8859-16", + "cskoi8r"=>"koi8-r", + "koi"=>"koi8-r", + "koi8"=>"koi8-r", + "koi8-r"=>"koi8-r", + "koi8_r"=>"koi8-r", + "koi8-ru"=>"koi8-u", + "koi8-u"=>"koi8-u", + "dos-874"=>"windows-874", + "iso-8859-11"=>"windows-874", + "iso8859-11"=>"windows-874", + "iso885911"=>"windows-874", + "tis-620"=>"windows-874", + "windows-874"=>"windows-874", + "cp1250"=>"windows-1250", + "windows-1250"=>"windows-1250", + "x-cp1250"=>"windows-1250", + "cp1251"=>"windows-1251", + "windows-1251"=>"windows-1251", + "x-cp1251"=>"windows-1251", + "ansi_x3.4-1968"=>"windows-1252", + "ascii"=>"windows-1252", + "cp1252"=>"windows-1252", + "cp819"=>"windows-1252", + "csisolatin1"=>"windows-1252", + "ibm819"=>"windows-1252", + "iso-8859-1"=>"windows-1252", + "iso-ir-100"=>"windows-1252", + "iso8859-1"=>"windows-1252", + "iso88591"=>"windows-1252", + "iso_8859-1"=>"windows-1252", + "iso_8859-1:1987"=>"windows-1252", + "l1"=>"windows-1252", + "latin1"=>"windows-1252", + "us-ascii"=>"windows-1252", + "windows-1252"=>"windows-1252", + "x-cp1252"=>"windows-1252", + "cp1253"=>"windows-1253", + "windows-1253"=>"windows-1253", + "x-cp1253"=>"windows-1253", + "cp1254"=>"windows-1254", + "csisolatin5"=>"windows-1254", + "iso-8859-9"=>"windows-1254", + "iso-ir-148"=>"windows-1254", + "iso8859-9"=>"windows-1254", + "iso88599"=>"windows-1254", + "iso_8859-9"=>"windows-1254", + "iso_8859-9:1989"=>"windows-1254", + "l5"=>"windows-1254", + "latin5"=>"windows-1254", + "windows-1254"=>"windows-1254", + "x-cp1254"=>"windows-1254", + "cp1255"=>"windows-1255", + "windows-1255"=>"windows-1255", + "x-cp1255"=>"windows-1255", + "cp1256"=>"windows-1256", + "windows-1256"=>"windows-1256", + "x-cp1256"=>"windows-1256", + "cp1257"=>"windows-1257", + "windows-1257"=>"windows-1257", + "x-cp1257"=>"windows-1257", + "cp1258"=>"windows-1258", + "windows-1258"=>"windows-1258", + "x-cp1258"=>"windows-1258", + "x-mac-cyrillic"=>"macCyrillic", + "x-mac-ukrainian"=>"macCyrillic", + "chinese"=>"gbk", + "csgb2312"=>"gbk", + "csiso58gb231280"=>"gbk", + "gb2312"=>"gbk", + "gb_2312"=>"gbk", + "gb_2312-80"=>"gbk", + "gbk"=>"gbk", + "iso-ir-58"=>"gbk", + "x-gbk"=>"gbk", + "gb18030"=>"gb18030", + "big5"=>"big5", + "big5-hkscs"=>"big5", + "cn-big5"=>"big5", + "csbig5"=>"big5", + "x-x-big5"=>"big5", + "cseucpkdfmtjapanese"=>"cp51932", + "euc-jp"=>"cp51932", + "x-euc-jp"=>"cp51932", + "csiso2022jp"=>"cp50221", + "iso-2022-jp"=>"cp50221", + "csshiftjis"=>"Windows-31J", + "ms932"=>"Windows-31J", + "ms_kanji"=>"Windows-31J", + "shift-jis"=>"Windows-31J", + "shift_jis"=>"Windows-31J", + "sjis"=>"Windows-31J", + "windows-31j"=>"Windows-31J", + "x-sjis"=>"Windows-31J", + "cseuckr"=>"euc-kr", + "csksc56011987"=>"euc-kr", + "euc-kr"=>"euc-kr", + "iso-ir-149"=>"euc-kr", + "korean"=>"euc-kr", + "ks_c_5601-1987"=>"euc-kr", + "ks_c_5601-1989"=>"euc-kr", + "ksc5601"=>"euc-kr", + "ksc_5601"=>"euc-kr", + "windows-949"=>"euc-kr", + "utf-16be"=>"utf-16be", + "utf-16"=>"utf-16le", + "utf-16le"=>"utf-16le", + } # :nodoc: + Ractor.make_shareable(WEB_ENCODINGS_) if defined?(Ractor) + + # :nodoc: + # return encoding or nil + # http://encoding.spec.whatwg.org/#concept-encoding-get + def self.get_encoding(label) + Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil + end +end # module Gem::URI + +module Gem + + # + # Returns a \Gem::URI object derived from the given +uri+, + # which may be a \Gem::URI string or an existing \Gem::URI object: + # + # require 'rubygems/vendor/uri/lib/uri' + # # Returns a new Gem::URI. + # uri = Gem::URI('http://github.com/ruby/ruby') + # # => #<Gem::URI::HTTP http://github.com/ruby/ruby> + # # Returns the given Gem::URI. + # Gem::URI(uri) + # # => #<Gem::URI::HTTP http://github.com/ruby/ruby> + # + # You must require 'rubygems/vendor/uri/lib/uri' to use this method. + # + def URI(uri) + if uri.is_a?(Gem::URI::Generic) + uri + elsif uri = String.try_convert(uri) + Gem::URI.parse(uri) + else + raise ArgumentError, + "bad argument (expected Gem::URI object or Gem::URI string)" + end + end + module_function :URI +end diff --git a/lib/rubygems/vendor/uri/lib/uri/file.rb b/lib/rubygems/vendor/uri/lib/uri/file.rb new file mode 100644 index 0000000000..391c499716 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/file.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_relative 'generic' + +module Gem::URI + + # + # The "file" Gem::URI is defined by RFC8089. + # + class File < Generic + # A Default port of nil for Gem::URI::File. + DEFAULT_PORT = nil + + # + # An Array of the available components for Gem::URI::File. + # + COMPONENT = [ + :scheme, + :host, + :path + ].freeze + + # + # == Description + # + # Creates a new Gem::URI::File object from components, with syntax checking. + # + # The components accepted are +host+ and +path+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[host, path]</code>. + # + # A path from e.g. the File class should be escaped before + # being passed. + # + # Examples: + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri1 = Gem::URI::File.build(['host.example.com', '/path/file.zip']) + # uri1.to_s # => "file://host.example.com/path/file.zip" + # + # uri2 = Gem::URI::File.build({:host => 'host.example.com', + # :path => '/ruby/src'}) + # uri2.to_s # => "file://host.example.com/ruby/src" + # + # uri3 = Gem::URI::File.build({:path => Gem::URI::RFC2396_PARSER.escape('/path/my file.txt')}) + # uri3.to_s # => "file:///path/my%20file.txt" + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + super(tmp) + end + + # Protected setter for the host component +v+. + # + # See also Gem::URI::Generic.host=. + # + def set_host(v) + v = "" if v.nil? || v == "localhost" + @host = v + end + + # do nothing + def set_port(v) + end + + # raise InvalidURIError + def check_userinfo(user) + raise Gem::URI::InvalidURIError, "cannot set userinfo for file Gem::URI" + end + + # raise InvalidURIError + def check_user(user) + raise Gem::URI::InvalidURIError, "cannot set user for file Gem::URI" + end + + # raise InvalidURIError + def check_password(user) + raise Gem::URI::InvalidURIError, "cannot set password for file Gem::URI" + end + + # do nothing + def set_userinfo(v) + end + + # do nothing + def set_user(v) + end + + # do nothing + def set_password(v) + end + end + + register_scheme 'FILE', File +end diff --git a/lib/rubygems/vendor/uri/lib/uri/ftp.rb b/lib/rubygems/vendor/uri/lib/uri/ftp.rb new file mode 100644 index 0000000000..7517813029 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/ftp.rb @@ -0,0 +1,267 @@ +# frozen_string_literal: false +# = uri/ftp.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'generic' + +module Gem::URI + + # + # FTP Gem::URI syntax is defined by RFC1738 section 3.2. + # + # This class will be redesigned because of difference of implementations; + # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it + # is a good summary about the de facto spec. + # https://datatracker.ietf.org/doc/html/draft-hoffman-ftp-uri-04 + # + class FTP < Generic + # A Default port of 21 for Gem::URI::FTP. + DEFAULT_PORT = 21 + + # + # An Array of the available components for Gem::URI::FTP. + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, + :path, :typecode + ].freeze + + # + # Typecode is "a", "i", or "d". + # + # * "a" indicates a text file (the FTP command was ASCII) + # * "i" indicates a binary file (FTP command IMAGE) + # * "d" indicates the contents of a directory should be displayed + # + TYPECODE = ['a', 'i', 'd'].freeze + + # Typecode prefix ";type=". + TYPECODE_PREFIX = ';type='.freeze + + def self.new2(user, password, host, port, path, + typecode = nil, arg_check = true) # :nodoc: + # Do not use this method! Not tested. [Bug #7301] + # This methods remains just for compatibility, + # Keep it undocumented until the active maintainer is assigned. + typecode = nil if typecode.size == 0 + if typecode && !TYPECODE.include?(typecode) + raise ArgumentError, + "bad typecode is specified: #{typecode}" + end + + # do escape + + self.new('ftp', + [user, password], + host, port, nil, + typecode ? path + TYPECODE_PREFIX + typecode : path, + nil, nil, nil, arg_check) + end + + # + # == Description + # + # Creates a new Gem::URI::FTP object from components, with syntax checking. + # + # The components accepted are +userinfo+, +host+, +port+, +path+, and + # +typecode+. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, typecode]</code>. + # + # If the path supplied is absolute, it will be escaped in order to + # make it absolute in the Gem::URI. + # + # Examples: + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri1 = Gem::URI::FTP.build(['user:password', 'ftp.example.com', nil, + # '/path/file.zip', 'i']) + # uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i" + # + # uri2 = Gem::URI::FTP.build({:host => 'ftp.example.com', + # :path => 'ruby/src'}) + # uri2.to_s # => "ftp://ftp.example.com/ruby/src" + # + def self.build(args) + + # Fix the incoming path to be generic URL syntax + # FTP path -> URL path + # foo/bar /foo/bar + # /foo/bar /%2Ffoo/bar + # + if args.kind_of?(Array) + args[3] = '/' + args[3].sub(/^\//, '%2F') + else + args[:path] = '/' + args[:path].sub(/^\//, '%2F') + end + + tmp = Util::make_components_hash(self, args) + + if tmp[:typecode] + if tmp[:typecode].size == 1 + tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode] + end + tmp[:path] << tmp[:typecode] + end + + return super(tmp) + end + + # + # == Description + # + # Creates a new Gem::URI::FTP object from generic URL components with no + # syntax checking. + # + # Unlike build(), this method does not escape the path component as + # required by RFC1738; instead it is treated as per RFC2396. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+, and +fragment+, in that order. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = nil, + arg_check = false) + raise InvalidURIError unless path + path = path.sub(/^\//,'') + path.sub!(/^%2F/,'/') + super(scheme, userinfo, host, port, registry, path, opaque, + query, fragment, parser, arg_check) + @typecode = nil + if tmp = @path.index(TYPECODE_PREFIX) + typecode = @path[tmp + TYPECODE_PREFIX.size..-1] + @path = @path[0..tmp - 1] + + if arg_check + self.typecode = typecode + else + self.set_typecode(typecode) + end + end + end + + # typecode accessor. + # + # See Gem::URI::FTP::COMPONENT. + attr_reader :typecode + + # Validates typecode +v+, + # returns +true+ or +false+. + # + def check_typecode(v) + if TYPECODE.include?(v) + return true + else + raise InvalidComponentError, + "bad typecode(expected #{TYPECODE.join(', ')}): #{v}" + end + end + private :check_typecode + + # Private setter for the typecode +v+. + # + # See also Gem::URI::FTP.typecode=. + # + def set_typecode(v) + @typecode = v + end + protected :set_typecode + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the typecode +v+ + # (with validation). + # + # See also Gem::URI::FTP.check_typecode. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("ftp://john@ftp.example.com/my_file.img") + # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img> + # uri.typecode = "i" + # uri + # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img;type=i> + # + def typecode=(typecode) + check_typecode(typecode) + set_typecode(typecode) + typecode + end + + def merge(oth) # :nodoc: + tmp = super(oth) + if self != tmp + tmp.set_typecode(oth.typecode) + end + + return tmp + end + + # Returns the path from an FTP Gem::URI. + # + # RFC 1738 specifically states that the path for an FTP Gem::URI does not + # include the / which separates the Gem::URI path from the Gem::URI host. Example: + # + # <code>ftp://ftp.example.com/pub/ruby</code> + # + # The above Gem::URI indicates that the client should connect to + # ftp.example.com then cd to pub/ruby from the initial login directory. + # + # If you want to cd to an absolute directory, you must include an + # escaped / (%2F) in the path. Example: + # + # <code>ftp://ftp.example.com/%2Fpub/ruby</code> + # + # This method will then return "/pub/ruby". + # + def path + return @path.sub(/^\//,'').sub(/^%2F/,'/') + end + + # Private setter for the path of the Gem::URI::FTP. + def set_path(v) + super("/" + v.sub(/^\//, "%2F")) + end + protected :set_path + + # Returns a String representation of the Gem::URI::FTP. + def to_s + save_path = nil + if @typecode + save_path = @path + @path = @path + TYPECODE_PREFIX + @typecode + end + str = super + if @typecode + @path = save_path + end + + return str + end + end + + register_scheme 'FTP', FTP +end diff --git a/lib/rubygems/vendor/uri/lib/uri/generic.rb b/lib/rubygems/vendor/uri/lib/uri/generic.rb new file mode 100644 index 0000000000..d0bc77dfda --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/generic.rb @@ -0,0 +1,1592 @@ +# frozen_string_literal: true + +# = uri/generic.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'common' +autoload :IPSocket, 'socket' +autoload :IPAddr, 'ipaddr' + +module Gem::URI + + # + # Base class for all Gem::URI classes. + # Implements generic Gem::URI syntax as per RFC 2396. + # + class Generic + include Gem::URI + + # + # A Default port of nil for Gem::URI::Generic. + # + DEFAULT_PORT = nil + + # + # Returns default port. + # + def self.default_port + self::DEFAULT_PORT + end + + # + # Returns default port. + # + def default_port + self.class.default_port + end + + # + # An Array of the available components for Gem::URI::Generic. + # + COMPONENT = [ + :scheme, + :userinfo, :host, :port, :registry, + :path, :opaque, + :query, + :fragment + ].freeze + + # + # Components of the Gem::URI in the order. + # + def self.component + self::COMPONENT + end + + USE_REGISTRY = false # :nodoc: + + def self.use_registry # :nodoc: + self::USE_REGISTRY + end + + # + # == Synopsis + # + # See ::new. + # + # == Description + # + # At first, tries to create a new Gem::URI::Generic instance using + # Gem::URI::Generic::build. But, if exception Gem::URI::InvalidComponentError is raised, + # then it does Gem::URI::RFC2396_PARSER.escape all Gem::URI components and tries again. + # + def self.build2(args) + begin + return self.build(args) + rescue InvalidComponentError + if args.kind_of?(Array) + return self.build(args.collect{|x| + if x.is_a?(String) + Gem::URI::RFC2396_PARSER.escape(x) + else + x + end + }) + elsif args.kind_of?(Hash) + tmp = {} + args.each do |key, value| + tmp[key] = if value + Gem::URI::RFC2396_PARSER.escape(value) + else + value + end + end + return self.build(tmp) + end + end + end + + # + # == Synopsis + # + # See ::new. + # + # == Description + # + # Creates a new Gem::URI::Generic instance from components of Gem::URI::Generic + # with check. Components are: scheme, userinfo, host, port, registry, path, + # opaque, query, and fragment. You can provide arguments either by an Array or a Hash. + # See ::new for hash keys to use or for order of array items. + # + def self.build(args) + if args.kind_of?(Array) && + args.size == ::Gem::URI::Generic::COMPONENT.size + tmp = args.dup + elsif args.kind_of?(Hash) + tmp = ::Gem::URI::Generic::COMPONENT.collect do |c| + if args.include?(c) + args[c] + else + nil + end + end + else + component = self.component rescue ::Gem::URI::Generic::COMPONENT + raise ArgumentError, + "expected Array of or Hash of components of #{self} (#{component.join(', ')})" + end + + tmp << nil + tmp << true + return self.new(*tmp) + end + + # + # == Args + # + # +scheme+:: + # Protocol scheme, i.e. 'http','ftp','mailto' and so on. + # +userinfo+:: + # User name and password, i.e. 'sdmitry:bla'. + # +host+:: + # Server host name. + # +port+:: + # Server port. + # +registry+:: + # Registry of naming authorities. + # +path+:: + # Path on server. + # +opaque+:: + # Opaque part. + # +query+:: + # Query data. + # +fragment+:: + # Part of the Gem::URI after '#' character. + # +parser+:: + # Parser for internal use [Gem::URI::DEFAULT_PARSER by default]. + # +arg_check+:: + # Check arguments [false by default]. + # + # == Description + # + # Creates a new Gem::URI::Generic instance from ``generic'' components without check. + # + def initialize(scheme, + userinfo, host, port, registry, + path, opaque, + query, + fragment, + parser = DEFAULT_PARSER, + arg_check = false) + @scheme = nil + @user = nil + @password = nil + @host = nil + @port = nil + @path = nil + @query = nil + @opaque = nil + @fragment = nil + @parser = parser == DEFAULT_PARSER ? nil : parser + + if arg_check + self.scheme = scheme + self.hostname = host + self.port = port + self.userinfo = userinfo + self.path = path + self.query = query + self.opaque = opaque + self.fragment = fragment + else + self.set_scheme(scheme) + self.set_host(host) + self.set_port(port) + self.set_userinfo(userinfo) + self.set_path(path) + self.query = query + self.set_opaque(opaque) + self.fragment=(fragment) + end + if registry + raise InvalidURIError, + "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)" + end + + @scheme&.freeze + self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2) + self.set_port(self.default_port) if self.default_port && !@port + end + + # + # Returns the scheme component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz").scheme #=> "http" + # + attr_reader :scheme + + # Returns the host component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz").host #=> "foo" + # + # It returns nil if no host component exists. + # + # Gem::URI("mailto:foo@example.org").host #=> nil + # + # The component does not contain the port number. + # + # Gem::URI("http://foo:8080/bar/baz").host #=> "foo" + # + # Since IPv6 addresses are wrapped with brackets in URIs, + # this method returns IPv6 addresses wrapped with brackets. + # This form is not appropriate to pass to socket methods such as TCPSocket.open. + # If unwrapped host names are required, use the #hostname method. + # + # Gem::URI("http://[::1]/bar/baz").host #=> "[::1]" + # Gem::URI("http://[::1]/bar/baz").hostname #=> "::1" + # + attr_reader :host + + # Returns the port component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz").port #=> 80 + # Gem::URI("http://foo:8080/bar/baz").port #=> 8080 + # + attr_reader :port + + def registry # :nodoc: + nil + end + + # Returns the path component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz").path #=> "/bar/baz" + # + attr_reader :path + + # Returns the query component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar" + # + attr_reader :query + + # Returns the opaque part of the Gem::URI. + # + # Gem::URI("mailto:foo@example.org").opaque #=> "foo@example.org" + # Gem::URI("http://foo/bar/baz").opaque #=> nil + # + # The portion of the path that does not make use of the slash '/'. + # The path typically refers to an absolute path or an opaque part. + # (See RFC2396 Section 3 and 5.2.) + # + attr_reader :opaque + + # Returns the fragment component of the Gem::URI. + # + # Gem::URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies" + # + attr_reader :fragment + + # Returns the parser to be used. + # + # Unless the +parser+ is defined, DEFAULT_PARSER is used. + # + def parser + if !defined?(@parser) || !@parser + DEFAULT_PARSER + else + @parser || DEFAULT_PARSER + end + end + + # Replaces self by other Gem::URI object. + # + def replace!(oth) + if self.class != oth.class + raise ArgumentError, "expected #{self.class} object" + end + + component.each do |c| + self.__send__("#{c}=", oth.__send__(c)) + end + end + private :replace! + + # + # Components of the Gem::URI in the order. + # + def component + self.class.component + end + + # + # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME. + # + def check_scheme(v) + if v && parser.regexp[:SCHEME] !~ v + raise InvalidComponentError, + "bad component(expected scheme component): #{v}" + end + + return true + end + private :check_scheme + + # Protected setter for the scheme component +v+. + # + # See also Gem::URI::Generic.scheme=. + # + def set_scheme(v) + @scheme = v&.downcase + end + protected :set_scheme + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the scheme component +v+ + # (with validation). + # + # See also Gem::URI::Generic.check_scheme. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.scheme = "https" + # uri.to_s #=> "https://my.example.com" + # + def scheme=(v) + check_scheme(v) + set_scheme(v) + v + end + + # + # Checks the +user+ and +password+. + # + # If +password+ is not provided, then +user+ is + # split, using Gem::URI::Generic.split_userinfo, to + # pull +user+ and +password. + # + # See also Gem::URI::Generic.check_user, Gem::URI::Generic.check_password. + # + def check_userinfo(user, password = nil) + if !password + user, password = split_userinfo(user) + end + check_user(user) + check_password(password, user) + + return true + end + private :check_userinfo + + # + # Checks the user +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :USERINFO. + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_user(v) + if @opaque + raise InvalidURIError, + "cannot set user with opaque" + end + + return v unless v + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad component(expected userinfo component or user component): #{v}" + end + + return true + end + private :check_user + + # + # Checks the password +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :USERINFO. + # + # Can not have a registry or opaque component defined, + # with a user component defined. + # + def check_password(v, user = @user) + if @opaque + raise InvalidURIError, + "cannot set password with opaque" + end + return v unless v + + if !user + raise InvalidURIError, + "password component depends user component" + end + + if parser.regexp[:USERINFO] !~ v + raise InvalidComponentError, + "bad password component" + end + + return true + end + private :check_password + + # + # Sets userinfo, argument is string like 'name:pass'. + # + def userinfo=(userinfo) + if userinfo.nil? + return nil + end + check_userinfo(*userinfo) + set_userinfo(*userinfo) + # returns userinfo + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the +user+ component + # (with validation). + # + # See also Gem::URI::Generic.check_user. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.user = "sam" + # uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com" + # + def user=(user) + check_user(user) + set_user(user) + # returns user + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the +password+ component + # (with validation). + # + # See also Gem::URI::Generic.check_password. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com") + # uri.password = "V3ry_S3nsit1ve" + # uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com" + # + def password=(password) + check_password(password) + set_password(password) + # returns password + end + + # Protected setter for the +user+ component, and +password+ if available + # (with validation). + # + # See also Gem::URI::Generic.userinfo=. + # + def set_userinfo(user, password = nil) + unless password + user, password = split_userinfo(user) + end + @user = user + @password = password + + [@user, @password] + end + protected :set_userinfo + + # Protected setter for the user component +v+. + # + # See also Gem::URI::Generic.user=. + # + def set_user(v) + set_userinfo(v, nil) + v + end + protected :set_user + + # Protected setter for the password component +v+. + # + # See also Gem::URI::Generic.password=. + # + def set_password(v) + @password = v + # returns v + end + protected :set_password + + # Returns the userinfo +ui+ as <code>[user, password]</code> + # if properly formatted as 'user:password'. + def split_userinfo(ui) + return nil, nil unless ui + user, password = ui.split(':', 2) + + return user, password + end + private :split_userinfo + + # Escapes 'user:password' +v+ based on RFC 1738 section 3.1. + def escape_userpass(v) + parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/ + end + private :escape_userpass + + # Returns the userinfo, either as 'user' or 'user:password'. + def userinfo + if @user.nil? + nil + elsif @password.nil? + @user + else + @user + ':' + @password + end + end + + # Returns the user component (without Gem::URI decoding). + def user + @user + end + + # Returns the password component (without Gem::URI decoding). + def password + @password + end + + # Returns the authority info (array of user, password, host and + # port), if any is set. Or returns +nil+. + def authority + return @user, @password, @host, @port if @user || @password || @host || @port + end + + # Returns the user component after Gem::URI decoding. + def decoded_user + Gem::URI.decode_uri_component(@user) if @user + end + + # Returns the password component after Gem::URI decoding. + def decoded_password + Gem::URI.decode_uri_component(@password) if @password + end + + # + # Checks the host +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :HOST. + # + # Can not have a registry or opaque component defined, + # with a host component defined. + # + def check_host(v) + return v unless v + + if @opaque + raise InvalidURIError, + "cannot set host with registry or opaque" + elsif parser.regexp[:HOST] !~ v + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + return true + end + private :check_host + + # Protected setter for the host component +v+. + # + # See also Gem::URI::Generic.host=. + # + def set_host(v) + @host = v + end + protected :set_host + + # Protected setter for the authority info (+user+, +password+, +host+ + # and +port+). If +port+ is +nil+, +default_port+ will be set. + # + protected def set_authority(user, password, host, port = nil) + @user, @password, @host, @port = user, password, host, port || self.default_port + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the host component +v+ + # (with validation). + # + # See also Gem::URI::Generic.check_host. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.host = "foo.com" + # uri.to_s #=> "http://foo.com" + # + def host=(v) + check_host(v) + set_host(v) + set_userinfo(nil) + v + end + + # Extract the host part of the Gem::URI and unwrap brackets for IPv6 addresses. + # + # This method is the same as Gem::URI::Generic#host except + # brackets for IPv6 (and future IP) addresses are removed. + # + # uri = Gem::URI("http://[::1]/bar") + # uri.hostname #=> "::1" + # uri.host #=> "[::1]" + # + def hostname + v = self.host + v&.start_with?('[') && v.end_with?(']') ? v[1..-2] : v + end + + # Sets the host part of the Gem::URI as the argument with brackets for IPv6 addresses. + # + # This method is the same as Gem::URI::Generic#host= except + # the argument can be a bare IPv6 address. + # + # uri = Gem::URI("http://foo/bar") + # uri.hostname = "::1" + # uri.to_s #=> "http://[::1]/bar" + # + # If the argument seems to be an IPv6 address, + # it is wrapped with brackets. + # + def hostname=(v) + v = "[#{v}]" if !(v&.start_with?('[') && v&.end_with?(']')) && v&.index(':') + self.host = v + end + + # + # Checks the port +v+ component for RFC2396 compliance + # and against the +parser+ Regexp for :PORT. + # + # Can not have a registry or opaque component defined, + # with a port component defined. + # + def check_port(v) + return v unless v + + if @opaque + raise InvalidURIError, + "cannot set port with registry or opaque" + elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v + raise InvalidComponentError, + "bad component(expected port component): #{v.inspect}" + end + + return true + end + private :check_port + + # Protected setter for the port component +v+. + # + # See also Gem::URI::Generic.port=. + # + def set_port(v) + v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer) + @port = v + end + protected :set_port + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the port component +v+ + # (with validation). + # + # See also Gem::URI::Generic.check_port. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.port = 8080 + # uri.to_s #=> "http://my.example.com:8080" + # + def port=(v) + check_port(v) + set_port(v) + set_userinfo(nil) + port + end + + def check_registry(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + private :check_registry + + def set_registry(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + protected :set_registry + + def registry=(v) # :nodoc: + raise InvalidURIError, "cannot set registry" + end + + # + # Checks the path +v+ component for RFC2396 compliance + # and against the +parser+ Regexp + # for :ABS_PATH and :REL_PATH. + # + # Can not have a opaque component defined, + # with a path component defined. + # + def check_path(v) + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if v && @opaque + raise InvalidURIError, + "path conflicts with opaque" + end + + # If scheme is ftp, path may be relative. + # See RFC 1738 section 3.2.2, and RFC 2396. + if @scheme && @scheme != "ftp" + if v && v != '' && parser.regexp[:ABS_PATH] !~ v + raise InvalidComponentError, + "bad component(expected absolute path component): #{v}" + end + else + if v && v != '' && parser.regexp[:ABS_PATH] !~ v && + parser.regexp[:REL_PATH] !~ v + raise InvalidComponentError, + "bad component(expected relative path component): #{v}" + end + end + + return true + end + private :check_path + + # Protected setter for the path component +v+. + # + # See also Gem::URI::Generic.path=. + # + def set_path(v) + @path = v + end + protected :set_path + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the path component +v+ + # (with validation). + # + # See also Gem::URI::Generic.check_path. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com/pub/files") + # uri.path = "/faq/" + # uri.to_s #=> "http://my.example.com/faq/" + # + def path=(v) + check_path(v) + set_path(v) + v + end + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the query component +v+. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com/?id=25") + # uri.query = "id=1" + # uri.to_s #=> "http://my.example.com/?id=1" + # + def query=(v) + return @query = nil unless v + raise InvalidURIError, "query conflicts with opaque" if @opaque + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v) + v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @query = v + end + + # + # Checks the opaque +v+ component for RFC2396 compliance and + # against the +parser+ Regexp for :OPAQUE. + # + # Can not have a host, port, user, or path component defined, + # with an opaque component defined. + # + def check_opaque(v) + return v unless v + + # raise if both hier and opaque are not nil, because: + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + if @host || @port || @user || @path # userinfo = @user + ':' + @password + raise InvalidURIError, + "cannot set opaque with host, port, userinfo or path" + elsif v && parser.regexp[:OPAQUE] !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + return true + end + private :check_opaque + + # Protected setter for the opaque component +v+. + # + # See also Gem::URI::Generic.opaque=. + # + def set_opaque(v) + @opaque = v + end + protected :set_opaque + + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the opaque component +v+ + # (with validation). + # + # See also Gem::URI::Generic.check_opaque. + # + def opaque=(v) + check_opaque(v) + set_opaque(v) + v + end + + # + # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT. + # + # + # == Args + # + # +v+:: + # String + # + # == Description + # + # Public setter for the fragment component +v+ + # (with validation). + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com/?id=25#time=1305212049") + # uri.fragment = "time=1305212086" + # uri.to_s #=> "http://my.example.com/?id=25#time=1305212086" + # + def fragment=(v) + return @fragment = nil unless v + + x = v.to_str + v = x.dup if x.equal? v + v.encode!(Encoding::UTF_8) rescue nil + v.delete!("\t\r\n") + v.force_encoding(Encoding::ASCII_8BIT) + v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord} + v.force_encoding(Encoding::US_ASCII) + @fragment = v + end + + # + # Returns true if Gem::URI is hierarchical. + # + # == Description + # + # Gem::URI has components listed in order of decreasing significance from left to right, + # see RFC3986 https://www.rfc-editor.org/rfc/rfc3986 1.2.3. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com/") + # uri.hierarchical? + # #=> true + # uri = Gem::URI.parse("mailto:joe@example.com") + # uri.hierarchical? + # #=> false + # + def hierarchical? + if @path + true + else + false + end + end + + # + # Returns true if Gem::URI has a scheme (e.g. http:// or https://) specified. + # + def absolute? + if @scheme + true + else + false + end + end + alias absolute absolute? + + # + # Returns true if Gem::URI does not have a scheme (e.g. http:// or https://) specified. + # + def relative? + !absolute? + end + + # + # Returns an Array of the path split on '/'. + # + def split_path(path) + path.split("/", -1) + end + private :split_path + + # + # Merges a base path +base+, with relative path +rel+, + # returns a modified base path. + # + def merge_path(base, rel) + + # RFC2396, Section 5.2, 5) + # RFC2396, Section 5.2, 6) + base_path = split_path(base) + rel_path = split_path(rel) + + # RFC2396, Section 5.2, 6), a) + base_path << '' if base_path.last == '..' + while i = base_path.index('..') + base_path.slice!(i - 1, 2) + end + + if (first = rel_path.first) and first.empty? + base_path.clear + rel_path.shift + end + + # RFC2396, Section 5.2, 6), c) + # RFC2396, Section 5.2, 6), d) + rel_path.push('') if rel_path.last == '.' || rel_path.last == '..' + rel_path.delete('.') + + # RFC2396, Section 5.2, 6), e) + tmp = [] + rel_path.each do |x| + if x == '..' && + !(tmp.empty? || tmp.last == '..') + tmp.pop + else + tmp << x + end + end + + add_trailer_slash = !tmp.empty? + if base_path.empty? + base_path = [''] # keep '/' for root directory + elsif add_trailer_slash + base_path.pop + end + while x = tmp.shift + if x == '..' + # RFC2396, Section 4 + # a .. or . in an absolute path has no special meaning + base_path.pop if base_path.size > 1 + else + # if x == '..' + # valid absolute (but abnormal) path "/../..." + # else + # valid absolute path + # end + base_path << x + tmp.each {|t| base_path << t} + add_trailer_slash = false + break + end + end + base_path.push('') if add_trailer_slash + + return base_path.join('/') + end + private :merge_path + + # + # == Args + # + # +oth+:: + # Gem::URI or String + # + # == Description + # + # Destructive form of #merge. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.merge!("/main.rbx?page=1") + # uri.to_s # => "http://my.example.com/main.rbx?page=1" + # + def merge!(oth) + t = merge(oth) + if self == t + nil + else + replace!(t) + self + end + end + + # + # == Args + # + # +oth+:: + # Gem::URI or String + # + # == Description + # + # Merges two URIs. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.merge("/main.rbx?page=1") + # # => "http://my.example.com/main.rbx?page=1" + # + def merge(oth) + rel = parser.__send__(:convert_to_uri, oth) + + if rel.absolute? + #raise BadURIError, "both Gem::URI are absolute" if absolute? + # hmm... should return oth for usability? + return rel + end + + unless self.absolute? + raise BadURIError, "both Gem::URI are relative" + end + + base = self.dup + + authority = rel.authority + + # RFC2396, Section 5.2, 2) + if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query + base.fragment=(rel.fragment) if rel.fragment + return base + end + + base.query = nil + base.fragment=(nil) + + # RFC2396, Section 5.2, 4) + if authority + base.set_authority(*authority) + base.set_path(rel.path) + elsif base.path && rel.path + base.set_path(merge_path(base.path, rel.path)) + end + + # RFC2396, Section 5.2, 7) + base.query = rel.query if rel.query + base.fragment=(rel.fragment) if rel.fragment + + return base + end # merge + alias + merge + + # :stopdoc: + def route_from_path(src, dst) + case dst + when src + # RFC2396, Section 4.2 + return '' + when %r{(?:\A|/)\.\.?(?:/|\z)} + # dst has abnormal absolute path, + # like "/./", "/../", "/x/../", ... + return dst.dup + end + + src_path = src.scan(%r{[^/]*/}) + dst_path = dst.scan(%r{[^/]*/?}) + + # discard same parts + while !dst_path.empty? && dst_path.first == src_path.first + src_path.shift + dst_path.shift + end + + tmp = dst_path.join + + # calculate + if src_path.empty? + if tmp.empty? + return './' + elsif dst_path.first.include?(':') # (see RFC2396 Section 5) + return './' + tmp + else + return tmp + end + end + + return '../' * src_path.size + tmp + end + private :route_from_path + # :startdoc: + + # :stopdoc: + def route_from0(oth) + oth = parser.__send__(:convert_to_uri, oth) + if self.relative? + raise BadURIError, + "relative Gem::URI: #{self}" + end + if oth.relative? + raise BadURIError, + "relative Gem::URI: #{oth}" + end + + if self.scheme != oth.scheme + return self, self.dup + end + rel = Gem::URI::Generic.new(nil, # it is relative Gem::URI + self.userinfo, self.host, self.port, + nil, self.path, self.opaque, + self.query, self.fragment, parser) + + if rel.userinfo != oth.userinfo || + rel.host.to_s.downcase != oth.host.to_s.downcase || + rel.port != oth.port + + if self.userinfo.nil? && self.host.nil? + return self, self.dup + end + + rel.set_port(nil) if rel.port == oth.default_port + return rel, rel + end + rel.set_userinfo(nil) + rel.set_host(nil) + rel.set_port(nil) + + if rel.path && rel.path == oth.path + rel.set_path('') + rel.query = nil if rel.query == oth.query + return rel, rel + elsif rel.opaque && rel.opaque == oth.opaque + rel.set_opaque('') + rel.query = nil if rel.query == oth.query + return rel, rel + end + + # you can modify `rel', but cannot `oth'. + return oth, rel + end + private :route_from0 + # :startdoc: + + # + # == Args + # + # +oth+:: + # Gem::URI or String + # + # == Description + # + # Calculates relative path from oth to self. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse('http://my.example.com/main.rbx?page=1') + # uri.route_from('http://my.example.com') + # #=> #<Gem::URI::Generic /main.rbx?page=1> + # + def route_from(oth) + # you can modify `rel', but cannot `oth'. + begin + oth, rel = route_from0(oth) + rescue + raise $!.class, $!.message + end + if oth == rel + return rel + end + + rel.set_path(route_from_path(oth.path, self.path)) + if rel.path == './' && self.query + # "./?foo" -> "?foo" + rel.set_path('') + end + + return rel + end + + alias - route_from + + # + # == Args + # + # +oth+:: + # Gem::URI or String + # + # == Description + # + # Calculates relative path to oth from self. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse('http://my.example.com') + # uri.route_to('http://my.example.com/main.rbx?page=1') + # #=> #<Gem::URI::Generic /main.rbx?page=1> + # + def route_to(oth) + parser.__send__(:convert_to_uri, oth).route_from(self) + end + + # + # Returns normalized Gem::URI. + # + # require 'rubygems/vendor/uri/lib/uri' + # + # Gem::URI("HTTP://my.EXAMPLE.com").normalize + # #=> #<Gem::URI::HTTP http://my.example.com/> + # + # Normalization here means: + # + # * scheme and host are converted to lowercase, + # * an empty path component is set to "/". + # + def normalize + uri = dup + uri.normalize! + uri + end + + # + # Destructive version of #normalize. + # + def normalize! + if path&.empty? + set_path('/') + end + if scheme && scheme != scheme.downcase + set_scheme(self.scheme.downcase) + end + if host && host != host.downcase + set_host(self.host.downcase) + end + end + + # + # Constructs String from Gem::URI. + # + def to_s + str = ''.dup + if @scheme + str << @scheme + str << ':' + end + + if @opaque + str << @opaque + else + if @host || %w[file postgres].include?(@scheme) + str << '//' + end + if self.userinfo + str << self.userinfo + str << '@' + end + if @host + str << @host + end + if @port && @port != self.default_port + str << ':' + str << @port.to_s + end + if (@host || @port) && !@path.empty? && !@path.start_with?('/') + str << '/' + end + str << @path + if @query + str << '?' + str << @query + end + end + if @fragment + str << '#' + str << @fragment + end + str + end + alias to_str to_s + + # + # Compares two URIs. + # + def ==(oth) + if self.class == oth.class + self.normalize.component_ary == oth.normalize.component_ary + else + false + end + end + + # Returns the hash value. + def hash + self.component_ary.hash + end + + # Compares with _oth_ for Hash. + def eql?(oth) + self.class == oth.class && + parser == oth.parser && + self.component_ary.eql?(oth.component_ary) + end + + # Returns an Array of the components defined from the COMPONENT Array. + def component_ary + component.collect do |x| + self.__send__(x) + end + end + protected :component_ary + + # == Args + # + # +components+:: + # Multiple Symbol arguments defined in Gem::URI::HTTP. + # + # == Description + # + # Selects specified components from Gem::URI. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse('http://myuser:mypass@my.example.com/test.rbx') + # uri.select(:userinfo, :host, :path) + # # => ["myuser:mypass", "my.example.com", "/test.rbx"] + # + def select(*components) + components.collect do |c| + if component.include?(c) + self.__send__(c) + else + raise ArgumentError, + "expected of components of #{self.class} (#{self.class.component.join(', ')})" + end + end + end + + def inspect # :nodoc: + "#<#{self.class} #{self}>" + end + + # + # == Args + # + # +v+:: + # Gem::URI or String + # + # == Description + # + # Attempts to parse other Gem::URI +oth+, + # returns [parsed_oth, self]. + # + # == Usage + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("http://my.example.com") + # uri.coerce("http://foo.com") + # #=> [#<Gem::URI::HTTP http://foo.com>, #<Gem::URI::HTTP http://my.example.com>] + # + def coerce(oth) + case oth + when String + oth = parser.parse(oth) + else + super + end + + return oth, self + end + + # Returns a proxy Gem::URI. + # The proxy Gem::URI is obtained from environment variables such as http_proxy, + # ftp_proxy, no_proxy, etc. + # If there is no proper proxy, nil is returned. + # + # If the optional parameter +env+ is specified, it is used instead of ENV. + # + # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.) + # are examined, too. + # + # But http_proxy and HTTP_PROXY is treated specially under CGI environment. + # It's because HTTP_PROXY may be set by Proxy: header. + # So HTTP_PROXY is not used. + # http_proxy is not used too if the variable is case insensitive. + # CGI_HTTP_PROXY can be used instead. + def find_proxy(env=ENV) + raise BadURIError, "relative Gem::URI: #{self}" if self.relative? + name = self.scheme.downcase + '_proxy' + proxy_uri = nil + if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI? + # HTTP_PROXY conflicts with *_proxy for proxy settings and + # HTTP_* for header information in CGI. + # So it should be careful to use it. + pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k } + case pairs.length + when 0 # no proxy setting anyway. + proxy_uri = nil + when 1 + k, _ = pairs.shift + if k == 'http_proxy' && env[k.upcase] == nil + # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env[name] + else + proxy_uri = nil + end + else # http_proxy is safe to use because ENV is case sensitive. + proxy_uri = env.to_hash[name] + end + if !proxy_uri + # Use CGI_HTTP_PROXY. cf. libwww-perl. + proxy_uri = env["CGI_#{name.upcase}"] + end + elsif name == 'http_proxy' + if RUBY_ENGINE == 'jruby' && p_addr = ENV_JAVA['http.proxyHost'] + p_port = ENV_JAVA['http.proxyPort'] + if p_user = ENV_JAVA['http.proxyUser'] + p_pass = ENV_JAVA['http.proxyPass'] + proxy_uri = "http://#{p_user}:#{p_pass}@#{p_addr}:#{p_port}" + else + proxy_uri = "http://#{p_addr}:#{p_port}" + end + else + unless proxy_uri = env[name] + if proxy_uri = env[name.upcase] + warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1 + end + end + end + else + proxy_uri = env[name] || env[name.upcase] + end + + if proxy_uri.nil? || proxy_uri.empty? + return nil + end + + if self.hostname + begin + addr = IPSocket.getaddress(self.hostname) + return nil if /\A127\.|\A::1\z/ =~ addr + rescue SocketError + end + end + + name = 'no_proxy' + if no_proxy = env[name] || env[name.upcase] + return nil unless Gem::URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy) + end + Gem::URI.parse(proxy_uri) + end + + def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc: + hostname = hostname.downcase + dothostname = ".#{hostname}" + no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port| + if !p_port || port == p_port.to_i + if p_host.start_with?('.') + return false if hostname.end_with?(p_host.downcase) + else + return false if dothostname.end_with?(".#{p_host.downcase}") + end + if addr + begin + return false if IPAddr.new(p_host).include?(addr) + rescue IPAddr::InvalidAddressError + next + end + end + end + } + true + end + end +end diff --git a/lib/rubygems/vendor/uri/lib/uri/http.rb b/lib/rubygems/vendor/uri/lib/uri/http.rb new file mode 100644 index 0000000000..99c78358ac --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/http.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: false +# = uri/http.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'generic' + +module Gem::URI + + # + # The syntax of HTTP URIs is defined in RFC1738 section 3.3. + # + # Note that the Ruby Gem::URI library allows HTTP URLs containing usernames and + # passwords. This is not legal as per the RFC, but used to be + # supported in Internet Explorer 5 and 6, before the MS04-004 security + # update. See <URL:http://support.microsoft.com/kb/834489>. + # + class HTTP < Generic + # A Default port of 80 for Gem::URI::HTTP. + DEFAULT_PORT = 80 + + # An Array of the available components for Gem::URI::HTTP. + COMPONENT = %i[ + scheme + userinfo host port + path + query + fragment + ].freeze + + # + # == Description + # + # Creates a new Gem::URI::HTTP object from components, with syntax checking. + # + # The components accepted are userinfo, host, port, path, query, and + # fragment. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, query, fragment]</code>. + # + # Example: + # + # uri = Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar') + # + # uri = Gem::URI::HTTP.build([nil, "www.example.com", nil, "/path", + # "query", 'fragment']) + # + # Currently, if passed userinfo components this method generates + # invalid HTTP URIs as per RFC 1738. + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + super(tmp) + end + + # Do not allow empty host names, as they are not allowed by RFC 3986. + def check_host(v) + ret = super + + if ret && v.empty? + raise InvalidComponentError, + "bad component(expected host component): #{v}" + end + + ret + end + + # + # == Description + # + # Returns the full path for an HTTP request, as required by Net::HTTP::Get. + # + # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query. + # Otherwise, the path is simply Gem::URI#path. + # + # Example: + # + # uri = Gem::URI::HTTP.build(path: '/foo/bar', query: 'test=true') + # uri.request_uri # => "/foo/bar?test=true" + # + def request_uri + return unless @path + + url = @query ? "#@path?#@query" : @path.dup + url.start_with?(?/.freeze) ? url : ?/ + url + end + + # + # == Description + # + # Returns the authority for an HTTP uri, as defined in + # https://www.rfc-editor.org/rfc/rfc3986#section-3.2. + # + # + # Example: + # + # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').authority #=> "www.example.com" + # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').authority #=> "www.example.com:8000" + # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').authority #=> "www.example.com" + # + def authority + if port == default_port + host + else + "#{host}:#{port}" + end + end + + # + # == Description + # + # Returns the origin for an HTTP uri, as defined in + # https://www.rfc-editor.org/rfc/rfc6454. + # + # + # Example: + # + # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').origin #=> "http://www.example.com" + # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').origin #=> "http://www.example.com:8000" + # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').origin #=> "http://www.example.com" + # Gem::URI::HTTPS.build(host: 'www.example.com', path: '/foo/bar').origin #=> "https://www.example.com" + # + def origin + "#{scheme}://#{authority}" + end + end + + register_scheme 'HTTP', HTTP +end diff --git a/lib/rubygems/vendor/uri/lib/uri/https.rb b/lib/rubygems/vendor/uri/lib/uri/https.rb new file mode 100644 index 0000000000..6e8e732e1d --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/https.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# = uri/https.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'http' + +module Gem::URI + + # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather + # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs; + # see Gem::URI::HTTP. + class HTTPS < HTTP + # A Default port of 443 for Gem::URI::HTTPS + DEFAULT_PORT = 443 + end + + register_scheme 'HTTPS', HTTPS +end diff --git a/lib/rubygems/vendor/uri/lib/uri/ldap.rb b/lib/rubygems/vendor/uri/lib/uri/ldap.rb new file mode 100644 index 0000000000..1a08b5ab7e --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/ldap.rb @@ -0,0 +1,261 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# Author:: +# Takaaki Tateishi <ttate@jaist.ac.jp> +# Akira Yamada <akira@ruby-lang.org> +# License:: +# Gem::URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada. +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'generic' + +module Gem::URI + + # + # LDAP Gem::URI SCHEMA (described in RFC2255). + #-- + # ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]] + #++ + class LDAP < Generic + + # A Default port of 389 for Gem::URI::LDAP. + DEFAULT_PORT = 389 + + # An Array of the available components for Gem::URI::LDAP. + COMPONENT = [ + :scheme, + :host, :port, + :dn, + :attributes, + :scope, + :filter, + :extensions, + ].freeze + + # Scopes available for the starting point. + # + # * SCOPE_BASE - the Base DN + # * SCOPE_ONE - one level under the Base DN, not including the base DN and + # not including any entries under this + # * SCOPE_SUB - subtrees, all entries at all levels + # + SCOPE = [ + SCOPE_ONE = 'one', + SCOPE_SUB = 'sub', + SCOPE_BASE = 'base', + ].freeze + + # + # == Description + # + # Creates a new Gem::URI::LDAP object from components, with syntax checking. + # + # The components accepted are host, port, dn, attributes, + # scope, filter, and extensions. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[host, port, dn, attributes, scope, filter, extensions]</code>. + # + # Example: + # + # uri = Gem::URI::LDAP.build({:host => 'ldap.example.com', + # :dn => '/dc=example'}) + # + # uri = Gem::URI::LDAP.build(["ldap.example.com", nil, + # "/dc=example;dc=com", "query", nil, nil, nil]) + # + def self.build(args) + tmp = Util::make_components_hash(self, args) + + if tmp[:dn] + tmp[:path] = tmp[:dn] + end + + query = [] + [:extensions, :filter, :scope, :attributes].collect do |x| + next if !tmp[x] && query.size == 0 + query.unshift(tmp[x]) + end + + tmp[:query] = query.join('?') + + return super(tmp) + end + + # + # == Description + # + # Creates a new Gem::URI::LDAP object from generic Gem::URI components as per + # RFC 2396. No LDAP-specific syntax checking is performed. + # + # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+, + # +opaque+, +query+, and +fragment+, in that order. + # + # Example: + # + # uri = Gem::URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil, + # "/dc=example;dc=com", nil, "query", nil) + # + # See also Gem::URI::Generic.new. + # + def initialize(*arg) + super(*arg) + + if @fragment + raise InvalidURIError, 'bad LDAP URL' + end + + parse_dn + parse_query + end + + # Private method to cleanup +dn+ from using the +path+ component attribute. + def parse_dn + raise InvalidURIError, 'bad LDAP URL' unless @path + @dn = @path[1..-1] + end + private :parse_dn + + # Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+ + # from using the +query+ component attribute. + def parse_query + @attributes = nil + @scope = nil + @filter = nil + @extensions = nil + + if @query + attrs, scope, filter, extensions = @query.split('?') + + @attributes = attrs if attrs && attrs.size > 0 + @scope = scope if scope && scope.size > 0 + @filter = filter if filter && filter.size > 0 + @extensions = extensions if extensions && extensions.size > 0 + end + end + private :parse_query + + # Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+. + def build_path_query + @path = '/' + @dn + + query = [] + [@extensions, @filter, @scope, @attributes].each do |x| + next if !x && query.size == 0 + query.unshift(x) + end + @query = query.join('?') + end + private :build_path_query + + # Returns dn. + def dn + @dn + end + + # Private setter for dn +val+. + def set_dn(val) + @dn = val + build_path_query + @dn + end + protected :set_dn + + # Setter for dn +val+. + def dn=(val) + set_dn(val) + val + end + + # Returns attributes. + def attributes + @attributes + end + + # Private setter for attributes +val+. + def set_attributes(val) + @attributes = val + build_path_query + @attributes + end + protected :set_attributes + + # Setter for attributes +val+. + def attributes=(val) + set_attributes(val) + val + end + + # Returns scope. + def scope + @scope + end + + # Private setter for scope +val+. + def set_scope(val) + @scope = val + build_path_query + @scope + end + protected :set_scope + + # Setter for scope +val+. + def scope=(val) + set_scope(val) + val + end + + # Returns filter. + def filter + @filter + end + + # Private setter for filter +val+. + def set_filter(val) + @filter = val + build_path_query + @filter + end + protected :set_filter + + # Setter for filter +val+. + def filter=(val) + set_filter(val) + val + end + + # Returns extensions. + def extensions + @extensions + end + + # Private setter for extensions +val+. + def set_extensions(val) + @extensions = val + build_path_query + @extensions + end + protected :set_extensions + + # Setter for extensions +val+. + def extensions=(val) + set_extensions(val) + val + end + + # Checks if Gem::URI has a path. + # For Gem::URI::LDAP this will return +false+. + def hierarchical? + false + end + end + + register_scheme 'LDAP', LDAP +end diff --git a/lib/rubygems/vendor/uri/lib/uri/ldaps.rb b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb new file mode 100644 index 0000000000..b7a5b50e27 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: false +# = uri/ldap.rb +# +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'ldap' + +module Gem::URI + + # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather + # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs; + # see Gem::URI::LDAP. + class LDAPS < LDAP + # A Default port of 636 for Gem::URI::LDAPS + DEFAULT_PORT = 636 + end + + register_scheme 'LDAPS', LDAPS +end diff --git a/lib/rubygems/vendor/uri/lib/uri/mailto.rb b/lib/rubygems/vendor/uri/lib/uri/mailto.rb new file mode 100644 index 0000000000..7ae544d194 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/mailto.rb @@ -0,0 +1,293 @@ +# frozen_string_literal: false +# = uri/mailto.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'generic' + +module Gem::URI + + # + # RFC6068, the mailto URL scheme. + # + class MailTo < Generic + include RFC2396_REGEXP + + # A Default port of nil for Gem::URI::MailTo. + DEFAULT_PORT = nil + + # An Array of the available components for Gem::URI::MailTo. + COMPONENT = [ :scheme, :to, :headers ].freeze + + # :stopdoc: + # "hname" and "hvalue" are encodings of an RFC 822 header name and + # value, respectively. As with "to", all URL reserved characters must + # be encoded. + # + # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it + # consists of zero or more comma-separated mail addresses, possibly + # including "phrase" and "comment" components. Note that all URL + # reserved characters in "to" must be encoded: in particular, + # parentheses, commas, and the percent sign ("%"), which commonly occur + # in the "mailbox" syntax. + # + # Within mailto URLs, the characters "?", "=", "&" are reserved. + + # ; RFC 6068 + # hfields = "?" hfield *( "&" hfield ) + # hfield = hfname "=" hfvalue + # hfname = *qchar + # hfvalue = *qchar + # qchar = unreserved / pct-encoded / some-delims + # some-delims = "!" / "$" / "'" / "(" / ")" / "*" + # / "+" / "," / ";" / ":" / "@" + # + # ; RFC3986 + # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + # pct-encoded = "%" HEXDIG HEXDIG + HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/ + # practical regexp for email address + # https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address + EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/ + # :startdoc: + + # + # == Description + # + # Creates a new Gem::URI::MailTo object from components, with syntax checking. + # + # Components can be provided as an Array or Hash. If an Array is used, + # the components must be supplied as <code>[to, headers]</code>. + # + # If a Hash is used, the keys are the component names preceded by colons. + # + # The headers can be supplied as a pre-encoded string, such as + # <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays + # like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>. + # + # Examples: + # + # require 'rubygems/vendor/uri/lib/uri' + # + # m1 = Gem::URI::MailTo.build(['joe@example.com', 'subject=Ruby']) + # m1.to_s # => "mailto:joe@example.com?subject=Ruby" + # + # m2 = Gem::URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]]) + # m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com" + # + # m3 = Gem::URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]}) + # m3.to_s # => "mailto:listman@example.com?subject=subscribe" + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + + case tmp[:to] + when Array + tmp[:opaque] = tmp[:to].join(',') + when String + tmp[:opaque] = tmp[:to].dup + else + tmp[:opaque] = '' + end + + if tmp[:headers] + query = + case tmp[:headers] + when Array + tmp[:headers].collect { |x| + if x.kind_of?(Array) + x[0] + '=' + x[1..-1].join + else + x.to_s + end + }.join('&') + when Hash + tmp[:headers].collect { |h,v| + h + '=' + v + }.join('&') + else + tmp[:headers].to_s + end + unless query.empty? + tmp[:opaque] << '?' << query + end + end + + super(tmp) + end + + # + # == Description + # + # Creates a new Gem::URI::MailTo object from generic URL components with + # no syntax checking. + # + # This method is usually called from Gem::URI::parse, which checks + # the validity of each component. + # + def initialize(*arg) + super(*arg) + + @to = nil + @headers = [] + + # The RFC3986 parser does not normally populate opaque + @opaque = "?#{@query}" if @query && !@opaque + + unless @opaque + raise InvalidComponentError, + "missing opaque part for mailto URL" + end + to, header = @opaque.split('?', 2) + # allow semicolon as a addr-spec separator + # http://support.microsoft.com/kb/820868 + unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to + raise InvalidComponentError, + "unrecognised opaque part for mailtoURL: #{@opaque}" + end + + if arg[10] # arg_check + self.to = to + self.headers = header + else + set_to(to) + set_headers(header) + end + end + + # The primary e-mail address of the URL, as a String. + attr_reader :to + + # E-mail headers set by the URL, as an Array of Arrays. + attr_reader :headers + + # Checks the to +v+ component. + def check_to(v) + return true unless v + return true if v.size == 0 + + v.split(/[,;]/).each do |addr| + # check url safety as path-rootless + if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as Gem::URI #{addr.dump}" + end + + # check addr-spec + # don't s/\+/ /g + addr.gsub!(/%\h\h/, Gem::URI::TBLDECWWWCOMP_) + if EMAIL_REGEXP !~ addr + raise InvalidComponentError, + "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}" + end + end + + true + end + private :check_to + + # Private setter for to +v+. + def set_to(v) + @to = v + end + protected :set_to + + # Setter for to +v+. + def to=(v) + check_to(v) + set_to(v) + v + end + + # Checks the headers +v+ component against either + # * HEADER_REGEXP + def check_headers(v) + return true unless v + return true if v.size == 0 + if HEADER_REGEXP !~ v + raise InvalidComponentError, + "bad component(expected opaque component): #{v}" + end + + true + end + private :check_headers + + # Private setter for headers +v+. + def set_headers(v) + @headers = [] + if v + v.split('&').each do |x| + @headers << x.split(/=/, 2) + end + end + end + protected :set_headers + + # Setter for headers +v+. + def headers=(v) + check_headers(v) + set_headers(v) + v + end + + # Constructs String from Gem::URI. + def to_s + @scheme + ':' + + if @to + @to + else + '' + end + + if @headers.size > 0 + '?' + @headers.collect{|x| x.join('=')}.join('&') + else + '' + end + + if @fragment + '#' + @fragment + else + '' + end + end + + # Returns the RFC822 e-mail text equivalent of the URL, as a String. + # + # Example: + # + # require 'rubygems/vendor/uri/lib/uri' + # + # uri = Gem::URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr") + # uri.to_mailtext + # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n" + # + def to_mailtext + to = Gem::URI.decode_www_form_component(@to) + head = '' + body = '' + @headers.each do |x| + case x[0] + when 'body' + body = Gem::URI.decode_www_form_component(x[1]) + when 'to' + to << ', ' + Gem::URI.decode_www_form_component(x[1]) + else + head << Gem::URI.decode_www_form_component(x[0]).capitalize + ': ' + + Gem::URI.decode_www_form_component(x[1]) + "\n" + end + end + + "To: #{to} +#{head} +#{body} +" + end + alias to_rfc822text to_mailtext + end + + register_scheme 'MAILTO', MailTo +end diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb new file mode 100644 index 0000000000..2bb4181649 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb @@ -0,0 +1,547 @@ +# frozen_string_literal: false +#-- +# = uri/common.rb +# +# Author:: Akira Yamada <akira@ruby-lang.org> +# License:: +# You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +module Gem::URI + # + # Includes Gem::URI::REGEXP::PATTERN + # + module RFC2396_REGEXP + # + # Patterns used to parse Gem::URI's + # + module PATTERN + # :stopdoc: + + # RFC 2396 (Gem::URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # alpha = lowalpha | upalpha + ALPHA = "a-zA-Z" + # alphanum = alpha | digit + ALNUM = "#{ALPHA}\\d" + + # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" | + # "a" | "b" | "c" | "d" | "e" | "f" + HEX = "a-fA-F\\d" + # escaped = "%" hex hex + ESCAPED = "%[#{HEX}]{2}" + # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" | + # "(" | ")" + # unreserved = alphanum | mark + UNRESERVED = "\\-_.!~*'()#{ALNUM}" + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," + # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + # "$" | "," | "[" | "]" (RFC 2732) + RESERVED = ";/?:@&=+$,\\[\\]" + + # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum + DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # toplabel = alpha | alpha *( alphanum | "-" ) alphanum + TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)" + # hostname = *( domainlabel "." ) toplabel [ "." ] + HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?" + + # :startdoc: + end # PATTERN + + # :startdoc: + end # REGEXP + + # Class that parses String's into Gem::URI's. + # + # It contains a Hash set of patterns and Regexp's that match and validate. + # + class RFC2396_Parser + include RFC2396_REGEXP + + # + # == Synopsis + # + # Gem::URI::RFC2396_Parser.new([opts]) + # + # == Args + # + # The constructor accepts a hash as options for parser. + # Keys of options are pattern names of Gem::URI components + # and values of options are pattern strings. + # The constructor generates set of regexps for parsing URIs. + # + # You can use the following keys: + # + # * :ESCAPED (Gem::URI::PATTERN::ESCAPED in default) + # * :UNRESERVED (Gem::URI::PATTERN::UNRESERVED in default) + # * :DOMLABEL (Gem::URI::PATTERN::DOMLABEL in default) + # * :TOPLABEL (Gem::URI::PATTERN::TOPLABEL in default) + # * :HOSTNAME (Gem::URI::PATTERN::HOSTNAME in default) + # + # == Examples + # + # p = Gem::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})") + # u = p.parse("http://example.jp/%uABCD") #=> #<Gem::URI::HTTP http://example.jp/%uABCD> + # Gem::URI.parse(u.to_s) #=> raises Gem::URI::InvalidURIError + # + # s = "http://example.com/ABCD" + # u1 = p.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD> + # u2 = Gem::URI.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD> + # u1 == u2 #=> true + # u1.eql?(u2) #=> false + # + def initialize(opts = {}) + @pattern = initialize_pattern(opts) + @pattern.each_value(&:freeze) + @pattern.freeze + + @regexp = initialize_regexp(@pattern) + @regexp.each_value(&:freeze) + @regexp.freeze + end + + # The Hash of patterns. + # + # See also #initialize_pattern. + attr_reader :pattern + + # The Hash of Regexp. + # + # See also #initialize_regexp. + attr_reader :regexp + + # Returns a split Gem::URI against +regexp[:ABS_URI]+. + def split(uri) + case uri + when '' + # null uri + + when @regexp[:ABS_URI] + scheme, opaque, userinfo, host, port, + registry, path, query, fragment = $~[1..-1] + + # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + # hier_part = ( net_path | abs_path ) [ "?" query ] + # opaque_part = uric_no_slash *uric + + # abs_path = "/" path_segments + # net_path = "//" authority [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + if !scheme + raise InvalidURIError, + "bad Gem::URI (absolute but no scheme): #{uri}" + end + if !opaque && (!path && (!host && !registry)) + raise InvalidURIError, + "bad Gem::URI (absolute but no path): #{uri}" + end + + when @regexp[:REL_URI] + scheme = nil + opaque = nil + + userinfo, host, port, registry, + rel_segment, abs_path, query, fragment = $~[1..-1] + if rel_segment && abs_path + path = rel_segment + abs_path + elsif rel_segment + path = rel_segment + elsif abs_path + path = abs_path + end + + # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + + # net_path = "//" authority [ abs_path ] + # abs_path = "/" path_segments + # rel_path = rel_segment [ abs_path ] + + # authority = server | reg_name + # server = [ [ userinfo "@" ] hostport ] + + else + raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri}" + end + + path = '' if !path && !opaque # (see RFC2396 Section 5.2) + ret = [ + scheme, + userinfo, host, port, # X + registry, # X + path, # Y + opaque, # Y + query, + fragment + ] + return ret + end + + # + # == Args + # + # +uri+:: + # String + # + # == Description + # + # Parses +uri+ and constructs either matching Gem::URI scheme object + # (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or Gem::URI::Generic. + # + # == Usage + # + # Gem::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john") + # #=> #<Gem::URI::LDAP ldap://ldap.example.com/dc=example?user=john> + # + def parse(uri) + Gem::URI.for(*self.split(uri), self) + end + + # + # == Args + # + # +uris+:: + # an Array of Strings + # + # == Description + # + # Attempts to parse and merge a set of URIs. + # + def join(*uris) + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # + # :call-seq: + # extract( str ) + # extract( str, schemes ) + # extract( str, schemes ) {|item| block } + # + # == Args + # + # +str+:: + # String to search + # +schemes+:: + # Patterns to apply to +str+ + # + # == Description + # + # Attempts to parse and merge a set of URIs. + # If no +block+ given, then returns the result, + # else it calls +block+ for each element in result. + # + # See also #make_regexp. + # + def extract(str, schemes = nil) + if block_given? + str.scan(make_regexp(schemes)) { yield $& } + nil + else + result = [] + str.scan(make_regexp(schemes)) { result.push $& } + result + end + end + + # Returns Regexp that is default +self.regexp[:ABS_URI_REF]+, + # unless +schemes+ is provided. Then it is a Regexp.union with +self.pattern[:X_ABS_URI]+. + def make_regexp(schemes = nil) + unless schemes + @regexp[:ABS_URI_REF] + else + /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x + end + end + + # + # :call-seq: + # escape( str ) + # escape( str, unsafe ) + # + # == Args + # + # +str+:: + # String to make safe + # +unsafe+:: + # Regexp to apply. Defaults to +self.regexp[:UNSAFE]+ + # + # == Description + # + # Constructs a safe String from +str+, removing unsafe characters, + # replacing them with codes. + # + def escape(str, unsafe = @regexp[:UNSAFE]) + unless unsafe.kind_of?(Regexp) + # perhaps unsafe is String object + unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false) + end + str.gsub(unsafe) do + us = $& + tmp = '' + us.each_byte do |uc| + tmp << sprintf('%%%02X', uc) + end + tmp + end.force_encoding(Encoding::US_ASCII) + end + + # + # :call-seq: + # unescape( str ) + # unescape( str, escaped ) + # + # == Args + # + # +str+:: + # String to remove escapes from + # +escaped+:: + # Regexp to apply. Defaults to +self.regexp[:ESCAPED]+ + # + # == Description + # + # Removes escapes from +str+. + # + def unescape(str, escaped = @regexp[:ESCAPED]) + enc = str.encoding + enc = Encoding::UTF_8 if enc == Encoding::US_ASCII + str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) } + end + + TO_S = Kernel.instance_method(:to_s) # :nodoc: + if TO_S.respond_to?(:bind_call) + def inspect # :nodoc: + TO_S.bind_call(self) + end + else + def inspect # :nodoc: + TO_S.bind(self).call + end + end + + private + + # Constructs the default Hash of patterns. + def initialize_pattern(opts = {}) + ret = {} + ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED) + ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED + ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED + ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL + ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL + ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME) + + # RFC 2396 (Gem::URI Generic Syntax) + # RFC 2732 (IPv6 Literal Addresses in URL's) + # RFC 2373 (IPv6 Addressing Architecture) + + # uric = reserved | unreserved | escaped + ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})" + # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" | + # "&" | "=" | "+" | "$" | "," + ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})" + # query = *uric + ret[:QUERY] = query = "#{uric}*" + # fragment = *uric + ret[:FRAGMENT] = fragment = "#{uric}*" + + # hostname = *( domainlabel "." ) toplabel [ "." ] + # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986 + unless hostname + ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+" + end + + # RFC 2373, APPENDIX B: + # IPv6address = hexpart [ ":" IPv4address ] + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ] + # hexseq = hex4 *( ":" hex4) + # hex4 = 1*4HEXDIG + # + # XXX: This definition has a flaw. "::" + IPv4address must be + # allowed too. Here is a replacement. + # + # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT + ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}" + # hex4 = 1*4HEXDIG + hex4 = "[#{PATTERN::HEX}]{1,4}" + # lastpart = hex4 | IPv4address + lastpart = "(?:#{hex4}|#{ipv4addr})" + # hexseq1 = *( hex4 ":" ) hex4 + hexseq1 = "(?:#{hex4}:)*#{hex4}" + # hexseq2 = *( hex4 ":" ) lastpart + hexseq2 = "(?:#{hex4}:)*#{lastpart}" + # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ] + ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)" + + # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT + # unused + + # ipv6reference = "[" IPv6address "]" (RFC 2732) + ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]" + + # host = hostname | IPv4address + # host = hostname | IPv4address | IPv6reference (RFC 2732) + ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})" + # port = *digit + ret[:PORT] = port = '\d*' + # hostport = host [ ":" port ] + ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?" + + # userinfo = *( unreserved | escaped | + # ";" | ":" | "&" | "=" | "+" | "$" | "," ) + ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*" + + # pchar = unreserved | escaped | + # ":" | "@" | "&" | "=" | "+" | "$" | "," + pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})" + # param = *pchar + param = "#{pchar}*" + # segment = *pchar *( ";" param ) + segment = "#{pchar}*(?:;#{param})*" + # path_segments = segment *( "/" segment ) + ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*" + + # server = [ [ userinfo "@" ] hostport ] + server = "(?:#{userinfo}@)?#{hostport}" + # reg_name = 1*( unreserved | escaped | "$" | "," | + # ";" | ":" | "@" | "&" | "=" | "+" ) + ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+" + # authority = server | reg_name + authority = "(?:#{server}|#{reg_name})" + + # rel_segment = 1*( unreserved | escaped | + # ";" | "@" | "&" | "=" | "+" | "$" | "," ) + ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+" + + # scheme = alpha *( alpha | digit | "+" | "-" | "." ) + ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*" + + # abs_path = "/" path_segments + ret[:ABS_PATH] = abs_path = "/#{path_segments}" + # rel_path = rel_segment [ abs_path ] + ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?" + # net_path = "//" authority [ abs_path ] + ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?" + + # hier_part = ( net_path | abs_path ) [ "?" query ] + ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?" + # opaque_part = uric_no_slash *uric + ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*" + + # absoluteURI = scheme ":" ( hier_part | opaque_part ) + ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})" + # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ] + ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?" + + # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ] + ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?" + + ret[:X_ABS_URI] = " + (#{scheme}): (?# 1: scheme) + (?: + (#{opaque_part}) (?# 2: opaque) + | + (?:(?: + //(?: + (?:(?:(#{userinfo})@)? (?# 3: userinfo) + (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port) + | + (#{reg_name}) (?# 6: registry) + ) + | + (?!//)) (?# XXX: '//' is the mark for hostport) + (#{abs_path})? (?# 7: path) + )(?:\\?(#{query}))? (?# 8: query) + ) + (?:\\#(#{fragment}))? (?# 9: fragment) + " + + ret[:X_REL_URI] = " + (?: + (?: + // + (?: + (?:(#{userinfo})@)? (?# 1: userinfo) + (#{host})?(?::(\\d*))? (?# 2: host, 3: port) + | + (#{reg_name}) (?# 4: registry) + ) + ) + | + (#{rel_segment}) (?# 5: rel_segment) + )? + (#{abs_path})? (?# 6: abs_path) + (?:\\?(#{query}))? (?# 7: query) + (?:\\#(#{fragment}))? (?# 8: fragment) + " + + ret + end + + # Constructs the default Hash of Regexp's. + def initialize_regexp(pattern) + ret = {} + + # for Gem::URI::split + ret[:ABS_URI] = Regexp.new('\A\s*+' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED) + ret[:REL_URI] = Regexp.new('\A\s*+' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED) + + # for Gem::URI::extract + ret[:URI_REF] = Regexp.new(pattern[:URI_REF]) + ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED) + ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED) + + # for Gem::URI::escape/unescape + ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED]) + ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]") + + # for Generic#initialize + ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z") + ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z") + ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z") + ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z") + ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z") + ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z") + ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z") + ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z") + ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z") + ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z") + + ret + end + + # Returns +uri+ as-is if it is Gem::URI, or convert it to Gem::URI if it is + # a String. + def convert_to_uri(uri) + if uri.is_a?(Gem::URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected Gem::URI object or Gem::URI string)" + end + end + + end # class Parser + + # Backward compatibility for Gem::URI::REGEXP::PATTERN::* + RFC2396_Parser.new.pattern.each_pair do |sym, str| + unless RFC2396_REGEXP::PATTERN.const_defined?(sym, false) + RFC2396_REGEXP::PATTERN.const_set(sym, str) + end + end +end # module Gem::URI diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb new file mode 100644 index 0000000000..3b6961abf6 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb @@ -0,0 +1,206 @@ +# frozen_string_literal: true +module Gem::URI + class RFC3986_Parser # :nodoc: + # Gem::URI defined in RFC3986 + HOST = %r[ + (?<IP-literal>\[(?: + (?<IPv6address> + (?:\h{1,4}:){6} + (?<ls32>\h{1,4}:\h{1,4} + | (?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d) + \.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>) + ) + | ::(?:\h{1,4}:){5}\g<ls32> + | \h{1,4}?::(?:\h{1,4}:){4}\g<ls32> + | (?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32> + | (?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32> + | (?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32> + | (?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32> + | (?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4} + | (?:(?:\h{1,4}:){,6}\h{1,4})?:: + ) + | (?<IPvFuture>v\h++\.[!$&-.0-9:;=A-Z_a-z~]++) + )\]) + | \g<IPv4address> + | (?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*+) + ]x + + USERINFO = /(?:%\h\h|[!$&-.0-9:;=A-Z_a-z~])*+/ + + SCHEME = %r[[A-Za-z][+\-.0-9A-Za-z]*+].source + SEG = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/])].source + SEG_NC = %r[(?:%\h\h|[!$&-.0-9;=@A-Z_a-z~])].source + FRAGMENT = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+].source + + RFC3986_URI = %r[\A + (?<seg>#{SEG}){0} + (?<Gem::URI> + (?<scheme>#{SCHEME}): + (?<hier-part>// + (?<authority> + (?:(?<userinfo>#{USERINFO.source})@)? + (?<host>#{HOST.source.delete(" \n")}) + (?::(?<port>\d*+))? + ) + (?<path-abempty>(?:/\g<seg>*+)?) + | (?<path-absolute>/((?!/)\g<seg>++)?) + | (?<path-rootless>(?!/)\g<seg>++) + | (?<path-empty>) + ) + (?:\?(?<query>[^\#]*+))? + (?:\#(?<fragment>#{FRAGMENT}))? + )\z]x + + RFC3986_relative_ref = %r[\A + (?<seg>#{SEG}){0} + (?<relative-ref> + (?<relative-part>// + (?<authority> + (?:(?<userinfo>#{USERINFO.source})@)? + (?<host>#{HOST.source.delete(" \n")}(?<!/))? + (?::(?<port>\d*+))? + ) + (?<path-abempty>(?:/\g<seg>*+)?) + | (?<path-absolute>/\g<seg>*+) + | (?<path-noscheme>#{SEG_NC}++(?:/\g<seg>*+)?) + | (?<path-empty>) + ) + (?:\?(?<query>[^#]*+))? + (?:\#(?<fragment>#{FRAGMENT}))? + )\z]x + attr_reader :regexp + + def initialize + @regexp = default_regexp.each_value(&:freeze).freeze + end + + def split(uri) #:nodoc: + begin + uri = uri.to_str + rescue NoMethodError + raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}" + end + uri.ascii_only? or + raise InvalidURIError, "Gem::URI must be ascii only #{uri.dump}" + if m = RFC3986_URI.match(uri) + query = m["query"] + scheme = m["scheme"] + opaque = m["path-rootless"] + if opaque + opaque << "?#{query}" if query + [ scheme, + nil, # userinfo + nil, # host + nil, # port + nil, # registry + nil, # path + opaque, + nil, # query + m["fragment"] + ] + else # normal + [ scheme, + m["userinfo"], + m["host"], + m["port"], + nil, # registry + (m["path-abempty"] || + m["path-absolute"] || + m["path-empty"]), + nil, # opaque + query, + m["fragment"] + ] + end + elsif m = RFC3986_relative_ref.match(uri) + [ nil, # scheme + m["userinfo"], + m["host"], + m["port"], + nil, # registry, + (m["path-abempty"] || + m["path-absolute"] || + m["path-noscheme"] || + m["path-empty"]), + nil, # opaque + m["query"], + m["fragment"] + ] + else + raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}" + end + end + + def parse(uri) # :nodoc: + Gem::URI.for(*self.split(uri), self) + end + + def join(*uris) # :nodoc: + uris[0] = convert_to_uri(uris[0]) + uris.inject :merge + end + + # Compatibility for RFC2396 parser + def extract(str, schemes = nil, &block) # :nodoc: + warn "Gem::URI::RFC3986_PARSER.extract is obsolete. Use Gem::URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.extract(str, schemes, &block) + end + + # Compatibility for RFC2396 parser + def make_regexp(schemes = nil) # :nodoc: + warn "Gem::URI::RFC3986_PARSER.make_regexp is obsolete. Use Gem::URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE + RFC2396_PARSER.make_regexp(schemes) + end + + # Compatibility for RFC2396 parser + def escape(str, unsafe = nil) # :nodoc: + warn "Gem::URI::RFC3986_PARSER.escape is obsolete. Use Gem::URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE + unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str) + end + + # Compatibility for RFC2396 parser + def unescape(str, escaped = nil) # :nodoc: + warn "Gem::URI::RFC3986_PARSER.unescape is obsolete. Use Gem::URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE + escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str) + end + + @@to_s = Kernel.instance_method(:to_s) + if @@to_s.respond_to?(:bind_call) + def inspect + @@to_s.bind_call(self) + end + else + def inspect + @@to_s.bind(self).call + end + end + + private + + def default_regexp # :nodoc: + { + SCHEME: %r[\A#{SCHEME}\z]o, + USERINFO: %r[\A#{USERINFO}\z]o, + HOST: %r[\A#{HOST}\z]o, + ABS_PATH: %r[\A/#{SEG}*+\z]o, + REL_PATH: %r[\A(?!/)#{SEG}++\z]o, + QUERY: %r[\A(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+\z], + FRAGMENT: %r[\A#{FRAGMENT}\z]o, + OPAQUE: %r[\A(?:[^/].*)?\z], + PORT: /\A[\x09\x0a\x0c\x0d ]*+\d*[\x09\x0a\x0c\x0d ]*\z/, + } + end + + def convert_to_uri(uri) + if uri.is_a?(Gem::URI::Generic) + uri + elsif uri = String.try_convert(uri) + parse(uri) + else + raise ArgumentError, + "bad argument (expected Gem::URI object or Gem::URI string)" + end + end + + end # class Parser +end # module Gem::URI diff --git a/lib/rubygems/vendor/uri/lib/uri/version.rb b/lib/rubygems/vendor/uri/lib/uri/version.rb new file mode 100644 index 0000000000..7ee577887b --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/version.rb @@ -0,0 +1,6 @@ +module Gem::URI + # :stopdoc: + VERSION = '1.1.1'.freeze + VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze + # :startdoc: +end diff --git a/lib/rubygems/vendor/uri/lib/uri/ws.rb b/lib/rubygems/vendor/uri/lib/uri/ws.rb new file mode 100644 index 0000000000..0dd2a7a1bb --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/ws.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: false +# = uri/ws.rb +# +# Author:: Matt Muller <mamuller@amazon.com> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'generic' + +module Gem::URI + + # + # The syntax of WS URIs is defined in RFC6455 section 3. + # + # Note that the Ruby Gem::URI library allows WS URLs containing usernames and + # passwords. This is not legal as per the RFC, but used to be + # supported in Internet Explorer 5 and 6, before the MS04-004 security + # update. See <URL:http://support.microsoft.com/kb/834489>. + # + class WS < Generic + # A Default port of 80 for Gem::URI::WS. + DEFAULT_PORT = 80 + + # An Array of the available components for Gem::URI::WS. + COMPONENT = %i[ + scheme + userinfo host port + path + query + ].freeze + + # + # == Description + # + # Creates a new Gem::URI::WS object from components, with syntax checking. + # + # The components accepted are userinfo, host, port, path, and query. + # + # The components should be provided either as an Array, or as a Hash + # with keys formed by preceding the component names with a colon. + # + # If an Array is used, the components must be passed in the + # order <code>[userinfo, host, port, path, query]</code>. + # + # Example: + # + # uri = Gem::URI::WS.build(host: 'www.example.com', path: '/foo/bar') + # + # uri = Gem::URI::WS.build([nil, "www.example.com", nil, "/path", "query"]) + # + # Currently, if passed userinfo components this method generates + # invalid WS URIs as per RFC 1738. + # + def self.build(args) + tmp = Util.make_components_hash(self, args) + super(tmp) + end + + # + # == Description + # + # Returns the full path for a WS Gem::URI, as required by Net::HTTP::Get. + # + # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query. + # Otherwise, the path is simply Gem::URI#path. + # + # Example: + # + # uri = Gem::URI::WS.build(path: '/foo/bar', query: 'test=true') + # uri.request_uri # => "/foo/bar?test=true" + # + def request_uri + return unless @path + + url = @query ? "#@path?#@query" : @path.dup + url.start_with?(?/.freeze) ? url : ?/ + url + end + end + + register_scheme 'WS', WS +end diff --git a/lib/rubygems/vendor/uri/lib/uri/wss.rb b/lib/rubygems/vendor/uri/lib/uri/wss.rb new file mode 100644 index 0000000000..0b91d334bb --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/wss.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: false +# = uri/wss.rb +# +# Author:: Matt Muller <mamuller@amazon.com> +# License:: You can redistribute it and/or modify it under the same term as Ruby. +# +# See Gem::URI for general documentation +# + +require_relative 'ws' + +module Gem::URI + + # The default port for WSS URIs is 443, and the scheme is 'wss:' rather + # than 'ws:'. Other than that, WSS URIs are identical to WS URIs; + # see Gem::URI::WS. + class WSS < WS + # A Default port of 443 for Gem::URI::WSS + DEFAULT_PORT = 443 + end + + register_scheme 'WSS', WSS +end diff --git a/lib/rubygems/vendored_net_http.rb b/lib/rubygems/vendored_net_http.rb new file mode 100644 index 0000000000..a84c52a947 --- /dev/null +++ b/lib/rubygems/vendored_net_http.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/net/http.rb +# We should avoid to load it again +require_relative "vendor/net-http/lib/net/http" unless defined?(Gem::Net::HTTP) diff --git a/lib/rubygems/vendored_optparse.rb b/lib/rubygems/vendored_optparse.rb new file mode 100644 index 0000000000..a5611d32f0 --- /dev/null +++ b/lib/rubygems/vendored_optparse.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/optparse/lib/optparse" diff --git a/lib/rubygems/vendored_pub_grub.rb b/lib/rubygems/vendored_pub_grub.rb new file mode 100644 index 0000000000..844d243ab3 --- /dev/null +++ b/lib/rubygems/vendored_pub_grub.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/pub_grub/lib/pub_grub" diff --git a/lib/rubygems/vendored_securerandom.rb b/lib/rubygems/vendored_securerandom.rb new file mode 100644 index 0000000000..859b6d7d7a --- /dev/null +++ b/lib/rubygems/vendored_securerandom.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/securerandom/lib/securerandom" diff --git a/lib/rubygems/vendored_timeout.rb b/lib/rubygems/vendored_timeout.rb new file mode 100644 index 0000000000..45541928e6 --- /dev/null +++ b/lib/rubygems/vendored_timeout.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/timeout.rb +# We should avoid to load it again +require_relative "vendor/timeout/lib/timeout" unless defined?(Gem::Timeout) diff --git a/lib/rubygems/vendored_tsort.rb b/lib/rubygems/vendored_tsort.rb new file mode 100644 index 0000000000..c3d815650d --- /dev/null +++ b/lib/rubygems/vendored_tsort.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/tsort/lib/tsort" diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb index 903b7de99d..306733c1d7 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true -require_relative "deprecate" +#-- +# Workaround for directly loading Gem::Version in some cases +module Gem; end +#++ ## # The Version class processes string versions into comparable @@ -27,153 +30,168 @@ require_relative "deprecate" # 4. 0.9 # # If you want to specify a version restriction that includes both prereleases -# and regular releases of the 1.x series this is the best way: +# and regular releases of 1.x or later versions: # -# s.add_dependency 'example', '>= 1.0.0.a', '< 2.0.0' +# s.add_dependency 'example', '>= 1.0.0.a' # # == How Software Changes # -# Users expect to be able to specify a version constraint that gives them -# some reasonable expectation that new versions of a library will work with -# their software if the version constraint is true, and not work with their -# software if the version constraint is false. In other words, the perfect -# system will accept all compatible versions of the library and reject all -# incompatible versions. -# -# Libraries change in 3 ways (well, more than 3, but stay focused here!). -# -# 1. The change may be an implementation detail only and have no effect on -# the client software. -# 2. The change may add new features, but do so in a way that client software -# written to an earlier version is still compatible. -# 3. The change may change the public interface of the library in such a way -# that old software is no longer compatible. -# -# Some examples are appropriate at this point. Suppose I have a Stack class -# that supports a <tt>push</tt> and a <tt>pop</tt> method. +# Libraries generally change in 3 ways: # -# === Examples of Category 1 changes: +# 1. The change is an implementation detail, bug fix, security fix, or +# optimization, and has no behavioral effect on the software using it. # -# * Switch from an array based implementation to a linked-list based -# implementation. -# * Provide an automatic (and transparent) backing store for large stacks. +# 2. The change adds new features, and software using those new features is +# not compatible with previous versions of the library, but software using +# previous versions of the library is compatible with the change. # -# === Examples of Category 2 changes might be: +# 3. The change modifies the public interface of some part of the library in +# such a way that software that uses that part of the library must be +# modified to work. # -# * Add a <tt>depth</tt> method to return the current depth of the stack. -# * Add a <tt>top</tt> method that returns the current top of stack (without -# changing the stack). -# * Change <tt>push</tt> so that it returns the item pushed (previously it -# had no usable return value). -# -# === Examples of Category 3 changes might be: -# -# * Changes <tt>pop</tt> so that it no longer returns a value (you must use -# <tt>top</tt> to get the top of the stack). -# * Rename the methods to <tt>push_item</tt> and <tt>pop_item</tt>. -# -# == RubyGems Rational Versioning +# == RubyGems Rational Versioning (the recommended approach) # # * Versions shall be represented by three non-negative integers, separated -# by periods (e.g. 3.1.4). The first integers is the "major" version +# by periods (e.g. 3.1.4). The first integer is the "major" version # number, the second integer is the "minor" version number, and the third -# integer is the "build" number. +# integer is the "patch" version number. # -# * A category 1 change (implementation detail) will increment the build -# number. +# * A category 1 change (implementation detail, bug fix, or security fix) +# will increment the patch number. # # * A category 2 change (backwards compatible) will increment the minor -# version number and reset the build number. -# -# * A category 3 change (incompatible) will increment the major build number -# and reset the minor and build numbers. -# -# * Any "public" release of a gem should have a different version. Normally -# that means incrementing the build number. This means a developer can -# generate builds all day long, but as soon as they make a public release, -# the version must be updated. -# -# === Examples -# -# Let's work through a project lifecycle using our Stack example from above. +# version number and reset the patch number. # -# Version 0.0.1:: The initial Stack class is release. -# Version 0.0.2:: Switched to a linked=list implementation because it is -# cooler. -# Version 0.1.0:: Added a <tt>depth</tt> method. -# Version 1.0.0:: Added <tt>top</tt> and made <tt>pop</tt> return nil -# (<tt>pop</tt> used to return the old top item). -# Version 1.1.0:: <tt>push</tt> now returns the value pushed (it used it -# return nil). -# Version 1.1.1:: Fixed a bug in the linked list implementation. -# Version 1.1.2:: Fixed a bug introduced in the last fix. +# * A category 3 change (incompatible) will increment the major version number +# and reset the minor and patch numbers. # -# Client A needs a stack with basic push/pop capability. They write to the -# original interface (no <tt>top</tt>), so their version constraint looks like: +# * Any "public" release of a gem should have a different version. # -# gem 'stack', '>= 0.0' +# == Optimistic Vs. Pessimistic Dependency Versioning # -# Essentially, any version is OK with Client A. An incompatible change to -# the library will cause them grief, but they are willing to take the chance -# (we call Client A optimistic). -# -# Client B is just like Client A except for two things: (1) They use the -# <tt>depth</tt> method and (2) they are worried about future -# incompatibilities, so they write their version constraint like this: -# -# gem 'stack', '~> 0.1' -# -# The <tt>depth</tt> method was introduced in version 0.1.0, so that version -# or anything later is fine, as long as the version stays below version 1.0 -# where incompatibilities are introduced. We call Client B pessimistic -# because they are worried about incompatible future changes (it is OK to be -# pessimistic!). -# -# == Preventing Version Catastrophe: -# -# From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html -# -# Let's say you're depending on the fnord gem version 2.y.z. If you -# specify your dependency as ">= 2.0.0" then, you're good, right? What -# happens if fnord 3.0 comes out and it isn't backwards compatible -# with 2.y.z? Your stuff will break as a result of using ">=". The -# better route is to specify your dependency with an "approximate" version -# specifier ("~>"). They're a tad confusing, so here is how the dependency -# specifiers work: -# -# Specification From ... To (exclusive) -# ">= 3.0" 3.0 ... ∞ -# "~> 3.0" 3.0 ... 4.0 -# "~> 3.0.0" 3.0.0 ... 3.1 -# "~> 3.5" 3.5 ... 4.0 -# "~> 3.5.0" 3.5.0 ... 3.6 -# "~> 3" 3.0 ... 4.0 -# -# For the last example, single-digit versions are automatically extended with -# a zero to give a sensible result. +# Users expect to be able to specify a version constraint that gives them +# a reasonable expectation that new versions of a library will work with +# their software if the version constraint is true, and not work with their +# software if the version constraint is false. In other words, the perfect +# system will accept all compatible versions of the library and reject all +# incompatible versions. Unfortunately, there is no perfect system, as you +# cannot predict the future. You can never know whether a future version of +# a library will contain which type of change. +# +# There are two common outlooks on dependency versioning: +# +# 1. Optimistic. This does not set an upper bound on a dependency. It is +# possible that a future version of a dependency will break the software, +# and in that case, the dependency version will need to be updated and +# changes will need to be made. +# +# 2. Pessimistic. This assumes all major version changes of a dependency will +# break the software, and that patch or minor changes of a dependency will +# not break the software. If there is a major version of a dependency +# released, the dependency version must be updated in order to use it, even +# if no code changes are actually needed. +# +# In general, optimistic versioning is superior to pessimistic versioning. +# Pessimistic versioning is often wrong in both directions. Dependencies can +# release patch or minor versions that contain incompatibilities. One +# common reason is that a security fix may require a backwards-incompatible API +# change. In this case, even though pessimistic versioning was used, it +# didn't even save effort, as you still need to make code changes and adjust +# dependency versions. Similarly, for all but the smallest dependencies, just +# because the dependency made a backwards incompatible change to one interface +# doesn't mean the dependency made a backwards incompatible change to an +# interface that the software is using. It is a common problem that a +# dependency will release a new major version and the software does not require +# any changes in order to use it. In this case, being pessimistic results in +# additional work for no benefit. +# +# When a library uses pessimistic versioning of dependencies, it causes +# significant problems if that library is not diligent about updating +# dependency versions and any library is depending on that library. +# For example: +# +# * Library A is currently on release 1.2.3. +# +# * Library B is at version 2.3.4 and has a pessimistic dependency on +# library A, using ~> 1.0 (>= 1.0, < 2). +# +# * Library C is at version 3.4.5 and has an optimistic dependency on +# library A, using >= 1.0. +# +# * Library D has optimistic dependencies on both libraries B and C. +# +# * Library A releases a new major version, 2.0.0, with new features, which +# is mostly backwards compatible, but does contain some backwards +# incompatible changes. +# +# * Library B would work with A 2.0.0, but cannot use it due to pessimistic +# versioning. +# +# * Library C wants to use the new features in the major release of library +# A to implement its own new features, so it does so, bumps the +# dependency version of A to >= 2.0, and releases version 3.5.0. +# +# * Library D cannot upgrade to the new version of library C, because it +# depends on library B, which has a pessimistic dependency on library A. +# +# * Library C releases a security fix patch version 3.5.1 to fix a +# vulnerability present in all previous versions. +# +# * Library D is now in a terrible situation. It cannot upgrade to library +# C 3.5.1, as that requires library A > 2.0, because it depends on library +# B, which requires library A > 1.0, < 2, even though library B would work +# fine with library A 2.0.0. +# +# This type of situation brought on by pessimistic versioning is unfortunately +# both common and serious in practice. +# +# This is not to say that optimistic versioning never causes a problem. +# However, with optimistic versioning, if there is a problem, it can be solved +# with the addition of a single dependency. For example, continuing the +# previous example: +# +# * Library A releases a new major version, 3.0.0, which makes backwards +# incompatible changes that break library C. +# +# * Until library C releases an updated version with new changes, library +# D only needs to set a specific dependency on library A for > 2.0, < 3, +# until library C is updated to work with the new version of library A. +# +# Both optimistic versioning and pessimistic versioning have problems in +# certain cases. However, it's significantly easier to fix optimistic +# versioning problems than to fix pessimistic versioning problems. +# +# That is not to say that pessimistic versioning is never appropriate. If the +# dependency is a library that adds a single method, where any change resulting +# in a major version bump would probably break a library using it, then using +# pessimistic versioning may be warranted. Additionally, if a dependency has +# already announced or committed backwards incompatible changes that would +# break a library's use of it, then having that library use a pessimistic +# version constraint would likely be warranted. However, outside of +# specific situations, you should avoid using pessimistic versioning, as the +# costs typically exceed the benefits. class Gem::Version include Comparable VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc: - ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze # :nodoc: + ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc: + RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc: ## # A string representation of this Version. def version - @version.dup + @version end - alias to_s version + alias_method :to_s, :version ## # True if the +version+ string matches RubyGems' requirements. def self.correct?(version) - nil_versions_are_discouraged! if version.nil? - - !!(version.to_s =~ ANCHORED_VERSION_PATTERN) + version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -182,15 +200,10 @@ class Gem::Version # # ver1 = Version.create('1.3.17') # -> (Version object) # ver2 = Version.create(ver1) # -> (ver1) - # ver3 = Version.create(nil) # -> nil def self.create(input) if self === input # check yourself before you wreck yourself input - elsif input.nil? - nil_versions_are_discouraged! - - nil else new input end @@ -201,19 +214,11 @@ class Gem::Version @@release = {} def self.new(version) # :nodoc: - return super unless Gem::Version == self + return super unless self == Gem::Version @@all[version] ||= super end - def self.nil_versions_are_discouraged! - unless Gem::Deprecate.skip - warn "nil versions are discouraged and will be deprecated in Rubygems 4" - end - end - - private_class_method :nil_versions_are_discouraged! - ## # Constructs a Version from the +version+ string. A version string is a # series of digits or ASCII letters separated by dots. @@ -224,10 +229,19 @@ class Gem::Version end # If version is an empty string convert it to 0 - version = 0 if version.is_a?(String) && version =~ /\A\s*\Z/ + version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version)) + + @version = version.to_s - @version = version.to_s.strip.gsub("-",".pre.") + # optimization to avoid allocation when given an integer, since we know + # it's to_s won't have any spaces or dashes + unless version.is_a?(Integer) + @version = @version.strip + @version.gsub!("-",".pre.") + end + @version = -@version @segments = nil + @sort_key = compute_sort_key end ## @@ -252,7 +266,7 @@ class Gem::Version # same precision. Version "1.0" is not the same as version "1". def eql?(other) - self.class === other && @version == other._version + self.class === other && @version == other.version end def hash # :nodoc: @@ -272,7 +286,7 @@ class Gem::Version # string for backwards (RubyGems 1.3.5 and earlier) compatibility. def marshal_dump - [version] + [@version] end ## @@ -280,19 +294,18 @@ class Gem::Version # 1.3.5 and earlier) compatibility. def marshal_load(array) - initialize array[0] + string = array[0] + raise TypeError, "wrong version string" unless string.is_a?(String) + + initialize string end def yaml_initialize(tag, map) # :nodoc: - @version = map["version"] + @version = -map["version"] @segments = nil @hash = nil end - def to_yaml_properties # :nodoc: - ["@version"] - end - def encode_with(coder) # :nodoc: coder.add "version", @version end @@ -302,7 +315,7 @@ class Gem::Version def prerelease? unless instance_variable_defined? :@prerelease - @prerelease = !!(@version =~ /[a-zA-Z]/) + @prerelease = /[a-zA-Z]/.match?(version) end @prerelease end @@ -330,7 +343,7 @@ class Gem::Version end ## - # A recommended version for use with a ~> Requirement. + # A recommended version for use with a >= Requirement. def approximate_recommendation segments = self.segments @@ -339,7 +352,7 @@ class Gem::Version segments.pop while segments.size > 2 segments.push 0 while segments.size < 2 - recommendation = "~> #{segments.join(".")}" + recommendation = ">= #{segments.join(".")}" recommendation += ".a" if prerelease? recommendation end @@ -347,71 +360,113 @@ class Gem::Version ## # Compares this version with +other+ returning -1, 0, or 1 if the # other version is larger, the same, or smaller than this - # one. Attempts to compare to something that's not a - # <tt>Gem::Version</tt> or a valid version String return +nil+. + # one. +other+ must be an instance of Gem::Version, comparing with + # other types may raise an exception. def <=>(other) - return self <=> self.class.new(other) if (String === other) && self.class.correct?(other) - - return unless Gem::Version === other - return 0 if @version == other._version || canonical_segments == other.canonical_segments - - lhsegments = canonical_segments - rhsegments = other.canonical_segments - - lhsize = lhsegments.size - rhsize = rhsegments.size - limit = (lhsize > rhsize ? lhsize : rhsize) - 1 - - i = 0 - - while i <= limit - lhs, rhs = lhsegments[i] || 0, rhsegments[i] || 0 - i += 1 - - next if lhs == rhs - return -1 if String === lhs && Numeric === rhs - return 1 if Numeric === lhs && String === rhs - - return lhs <=> rhs + if Gem::Version === other + # Fast path for comparison when available. + if @sort_key && other.sort_key + return @sort_key <=> other.sort_key + end + + return 0 if @version == other.version || canonical_segments == other.canonical_segments + + lhsegments = canonical_segments + rhsegments = other.canonical_segments + + lhsize = lhsegments.size + rhsize = rhsegments.size + limit = (lhsize > rhsize ? rhsize : lhsize) + + i = 0 + + while i < limit + lhs = lhsegments[i] + rhs = rhsegments[i] + i += 1 + + next if lhs == rhs + return -1 if String === lhs && Numeric === rhs + return 1 if Numeric === lhs && String === rhs + + return lhs <=> rhs + end + + lhs = lhsegments[i] + + if lhs.nil? + rhs = rhsegments[i] + + while i < rhsize + return 1 if String === rhs + return -1 unless rhs.zero? + rhs = rhsegments[i += 1] + end + else + while i < lhsize + return -1 if String === lhs + return 1 unless lhs.zero? + lhs = lhsegments[i += 1] + end + end + + 0 + elsif String === other + return unless self.class.correct?(other) + self <=> self.class.new(other) end - - return 0 end + # remove trailing zeros segments before first letter or at the end of the version def canonical_segments - @canonical_segments ||= - _split_segments.map! do |segments| - segments.reverse_each.drop_while {|s| s == 0 }.reverse - end.reduce(&:concat) + @canonical_segments ||= begin + # remove trailing 0 segments, using dot or letter as anchor + # may leave a trailing dot which will be ignored by partition_segments + canonical_version = @version.sub(/(?<=[a-zA-Z.])[.0]+\z/, "") + # remove 0 segments before the first letter in a prerelease version + canonical_version.sub!(/(?<=\.|\A)[0.]+(?=[a-zA-Z])/, "") if prerelease? + partition_segments(canonical_version) + end end def freeze prerelease? + _segments canonical_segments super end protected - def _version - @version + attr_reader :sort_key # :nodoc: + + def compute_sort_key + return if prerelease? + + segments = canonical_segments + return if segments.size > 5 + + key = 0 + RADIX_OPT.each_with_index do |radix, i| + seg = segments.fetch(i, 0) + return nil if seg >= radix + key = key * radix + seg + end + + key end def _segments # segments is lazy so it can pick up version values that come from # old marshaled versions, which don't go through marshal_load. # since this version object is cached in @@all, its @segments should be frozen - - @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s| - /^\d+$/ =~ s ? s.to_i : s - end.freeze + @segments ||= partition_segments(@version) end - def _split_segments - string_start = _segments.index {|s| s.is_a?(String) } - string_segments = segments - numeric_segments = string_segments.slice!(0, string_start || string_segments.size) - return numeric_segments, string_segments + def partition_segments(ver) + ver.scan(/\d+|[a-z]+/i).map! do |s| + /\A\d/.match?(s) ? s.to_i : -s + end.freeze end end diff --git a/lib/rubygems/version_option.rb b/lib/rubygems/version_option.rb index a487a0bc24..7910fd3d1b 100644 --- a/lib/rubygems/version_option.rb +++ b/lib/rubygems/version_option.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -11,7 +12,6 @@ require_relative "../rubygems" # Mixin methods for --version and --platform Gem::Command options. module Gem::VersionOption - ## # Add the --platform option to the option parser. @@ -25,8 +25,7 @@ module Gem::VersionOption end add_option("--platform PLATFORM", Gem::Platform, - "Specify the platform of gem to #{task}", *wrap) do - |value, options| + "Specify the platform of gem to #{task}", *wrap) do |value, options| unless options[:added_platform] Gem.platforms = [Gem::Platform::RUBY] options[:added_platform] = true @@ -56,8 +55,7 @@ module Gem::VersionOption end add_option("-v", "--version VERSION", Gem::Requirement, - "Specify version of gem to #{task}", *wrap) do - |value, options| + "Specify version of gem to #{task}", *wrap) do |value, options| # Allow handling for multiple --version operators if options[:version] && !options[:version].none? options[:version].concat([value]) diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb new file mode 100644 index 0000000000..10556871b2 --- /dev/null +++ b/lib/rubygems/win_platform.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "rbconfig" + +module Gem + ## + # An Array of Regexps that match windows Ruby platforms. + + WIN_PATTERNS = [ + /bccwin/i, + /djgpp/i, + /mingw/i, + /mswin/i, + /wince/i, + ].freeze + + @@win_platform = nil + + ## + # Is this a windows platform? + + def self.win_platform? + if @@win_platform.nil? + ruby_platform = RbConfig::CONFIG["host_os"] + @@win_platform = !WIN_PATTERNS.find {|r| ruby_platform =~ r }.nil? + end + + @@win_platform + end +end diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb new file mode 100644 index 0000000000..b2547b136b --- /dev/null +++ b/lib/rubygems/yaml_serializer.rb @@ -0,0 +1,845 @@ +# frozen_string_literal: true + +unless defined?(Psych::VERSION) + module Psych + class Exception < ::RuntimeError; end + class SyntaxError < Exception; end + class DisallowedClass < Exception; end + class BadAlias < Exception; end + class AliasesNotEnabled < BadAlias; end + end +end + +module Gem + module YAMLSerializer + Scalar = Struct.new(:value, :tag, :anchor, keyword_init: true) + + Mapping = Struct.new(:pairs, :tag, :anchor, keyword_init: true) do + def initialize(pairs: [], tag: nil, anchor: nil) + super + end + end + + Sequence = Struct.new(:items, :tag, :anchor, keyword_init: true) do + def initialize(items: [], tag: nil, anchor: nil) + super + end + end + + AliasRef = Struct.new(:name, keyword_init: true) + + class Parser + MAPPING_KEY_RE = /^((?:[^#:]|:[^ ])+):(?:[ ]+(.*))?$/ + MAX_NESTING_DEPTH = 1_000 + + def initialize(source) + @lines = source.split("\n") + @anchors = {} + @depth = 0 + strip_document_prefix + end + + def parse + return nil if @lines.empty? + + root = nil + while @lines.any? + before = @lines.size + node = parse_node(-1) + @lines.shift if @lines.size == before && @lines.any? + + if root.is_a?(Mapping) && node.is_a?(Mapping) + root.pairs.concat(node.pairs) + elsif root.nil? + root = node + end + end + root + end + + private + + def strip_document_prefix + return if @lines.empty? + return unless @lines[0]&.start_with?("---") + + if @lines[0].strip == "---" + @lines.shift + else + @lines[0] = @lines[0].sub(/^---\s*/, "") + end + end + + def parse_node(base_indent) + @depth += 1 + raise_max_nesting! if @depth > MAX_NESTING_DEPTH + + skip_blank_and_comments + return nil if @lines.empty? + + line = @lines[0] + stripped = line.lstrip + indent = line.size - stripped.size + return nil if indent < base_indent + + return parse_alias_ref if stripped.start_with?("*") + + anchor = consume_anchor + + if anchor + line = @lines[0] + stripped = line.lstrip + end + + if stripped.start_with?("- ") || stripped == "-" + parse_sequence(indent, anchor) + elsif stripped.start_with?("\"") && stripped.end_with?("\"") + # We don't need to care about the following case here: + # 1. "value with comment" # ... + # 2. "key": "value" + # + # 1. must not happen because YAMLSerializer doesn't emit any + # comment. YAMLSerializer parses only YAML that is generated + # by YAMLSerializer. + # + # 2. must not happen because #parse_node isn't used non + # top-level mapping. Non top-level mapping always uses + # #parse_mapping. Top-level mapping never use the '"key": + # "value"' form because all top-level keys + # ("!ruby/object:Gem::Specification"'s keys) are known and + # #emit_specification doesn't quote anything. + parse_plain_scalar(indent, anchor) + elsif stripped.start_with?("'") && stripped.end_with?("'") + # See also the above note for double quotation. + parse_plain_scalar(indent, anchor) + elsif stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:") + parse_mapping(indent, anchor) + elsif stripped.start_with?("!ruby/object:") + parse_tagged_node(indent, anchor) + elsif stripped.start_with?("|") + modifier = stripped[1..].to_s.strip + @lines.shift + register_anchor(anchor, Scalar.new(value: parse_block_scalar(indent, modifier))) + else + parse_plain_scalar(indent, anchor) + end + ensure + @depth -= 1 + end + + def parse_sequence(indent, anchor) + items = [] + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless line.size - stripped.size == indent && + (stripped.start_with?("- ") || stripped == "-") + content = @lines.shift.lstrip[1..].strip + item_anchor, content = extract_item_anchor(content) + item = parse_sequence_item(content, indent) + items << register_anchor(item_anchor, item) + end + register_anchor(anchor, Sequence.new(items: items)) + end + + def parse_sequence_item(content, indent) + if content.start_with?("*") + parse_inline_alias(content) + elsif content.empty? + @lines.any? && current_indent > indent ? parse_node(indent) : nil + elsif content.start_with?("!ruby/object:") + parse_tagged_content(content.strip, indent) + elsif content.start_with?("!binary ") + parse_binary_value(content, indent) + elsif content.start_with?("-") + @lines.unshift("#{" " * (indent + 2)}#{content}") + parse_node(indent) + elsif content =~ MAPPING_KEY_RE && !content.start_with?("!ruby/object:") + @lines.unshift("#{" " * (indent + 2)}#{content}") + parse_node(indent) + elsif content.start_with?("|") + Scalar.new(value: parse_block_scalar(indent, content[1..].to_s.strip)) + else + parse_inline_scalar(content, indent) + end + end + + def parse_mapping(indent, anchor) + pairs = [] + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless line.size - stripped.size == indent && + stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:") + key = $1.strip + @lines.shift + val = strip_comment($2.to_s.strip) + + key = decode_binary_tag(key) if key.start_with?("!binary ") + + val_anchor, val = consume_value_anchor(val) + value = parse_mapping_value(val, indent) + value = register_anchor(val_anchor, value) if val_anchor + + pairs << [Scalar.new(value: key), value] + end + register_anchor(anchor, Mapping.new(pairs: pairs)) + end + + def parse_mapping_value(val, indent) + if val.start_with?("*") + parse_inline_alias(val) + elsif val.start_with?("!ruby/object:") + parse_tagged_content(val.strip, indent) + elsif val.start_with?("!binary ") + parse_binary_value(val, indent) + elsif val.empty? + next_stripped = nil + next_indent = nil + if @lines.any? + next_stripped = @lines[0].lstrip + next_indent = @lines[0].size - next_stripped.size + end + if next_stripped && + (next_stripped.start_with?("- ") || next_stripped == "-") && + next_indent == indent + parse_node(indent) + else + parse_node(indent + 1) + end + elsif val == "[]" + Sequence.new + elsif val == "{}" + Mapping.new + elsif val.start_with?("|") + Scalar.new(value: parse_block_scalar(indent, val[1..].to_s.strip)) + else + parse_inline_scalar(val, indent) + end + end + + def parse_tagged_node(indent, anchor) + tag = @lines.shift.strip + nested = parse_node(indent) + apply_tag(nested, tag, anchor) + end + + def parse_tagged_content(tag, indent) + nested = parse_node(indent) + apply_tag(nested, tag, nil) + end + + def apply_tag(node, tag, anchor) + if node.is_a?(Mapping) + node.tag = tag + node.anchor = anchor + node + else + Mapping.new(pairs: [[Scalar.new(value: "value"), node]], tag: tag, anchor: anchor) + end + end + + def parse_block_scalar(base_indent, modifier) + parts = [] + block_indent = nil + + while @lines.any? + line = @lines[0] + if line.strip.empty? + parts << "\n" + @lines.shift + else + line_indent = line.size - line.lstrip.size + break if line_indent <= base_indent + block_indent ||= line_indent + parts << @lines.shift[block_indent..].to_s << "\n" + end + end + + res = parts.join + res.chomp! if modifier == "-" && res.end_with?("\n") + res + end + + def parse_plain_scalar(indent, anchor) + result = coerce(@lines.shift.strip) + return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence) + + while result.is_a?(String) && @lines.any? && + !@lines[0].strip.empty? && current_indent > indent + result << " " << @lines.shift.strip + end + register_anchor(anchor, Scalar.new(value: result)) + end + + def parse_inline_scalar(val, indent) + result = coerce(val) + return result if result.is_a?(Mapping) || result.is_a?(Sequence) + + while result.is_a?(String) && @lines.any? && + !@lines[0].strip.empty? && current_indent > indent + result << " " << @lines.shift.strip + end + Scalar.new(value: result) + end + + def coerce(val, depth = 0) + raise_max_nesting! if depth > MAX_NESTING_DEPTH + + val = val.sub(/^! /, "") if val.start_with?("! ") + + if val =~ /^"(.*)"$/ + $1.gsub(/\\["nrt\\]/) do |m| + case m + when '\\"' then '"' + when "\\n" then "\n" + when "\\r" then "\r" + when "\\t" then "\t" + when "\\\\" then "\\" + end + end + elsif val =~ /^'(.*)'$/ + $1.gsub(/''/, "'") + elsif val == "true" + true + elsif val == "false" + false + elsif ["~", "null"].include?(val) + nil + elsif val == "{}" + Mapping.new + elsif val =~ /^\[(.*)\]$/ + inner = $1.strip + return Sequence.new if inner.empty? + items = inner.split(/\s*,\s*/).reject(&:empty?).map {|e| Scalar.new(value: coerce(e, depth + 1)) } + Sequence.new(items: items) + elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val) + begin + Time.new(val) + rescue ArgumentError + # date-only format like "2024-06-15" is not supported by Time.new + if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val) + Time.utc($1.to_i, $2.to_i, $3.to_i) + else + val + end + end + elsif /^-?\d+$/.match?(val) + val.to_i + else + val + end + end + + def decode_binary_tag(str) + content = str.sub(/\A!binary\s+/, "") + content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/ + content.unpack1("m") + end + + def parse_binary_value(val, indent) + rest = val.sub(/\A!binary\s+/, "") + if rest.start_with?("|") + content = parse_block_scalar(indent, rest[1..].to_s.strip) + Scalar.new(value: content.unpack1("m")) + else + Scalar.new(value: decode_binary_tag(val)) + end + end + + def parse_alias_ref + AliasRef.new(name: @lines.shift.lstrip[1..].strip) + end + + def parse_inline_alias(content) + AliasRef.new(name: content[1..].strip) + end + + def current_indent + line = @lines[0] + line.size - line.lstrip.size + end + + def consume_anchor + line = @lines[0] + stripped = line.lstrip + return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/ + + anchor = $1 + @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "") + anchor + end + + def extract_item_anchor(content) + return [nil, content] unless content =~ /^&(\S+)/ + + anchor = $1 + [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")] + end + + def consume_value_anchor(val) + return [nil, val] unless val =~ /^&(\S+)\s+/ + + anchor = $1 + [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")] + end + + def register_anchor(name, node) + if name + @anchors[name] = node + node.anchor = name if node.respond_to?(:anchor=) + end + node + end + + def raise_max_nesting! + message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})" + if defined?(Psych::VERSION) + raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil) + else + raise Psych::SyntaxError, message + end + end + + def skip_blank_and_comments + while @lines.any? + line = @lines[0] + stripped = line.lstrip + break unless stripped.empty? || stripped.start_with?("#") + @lines.shift + end + end + + def strip_comment(val) + return val unless val.include?("#") + return val if val.lstrip.start_with?("#") + + in_single = false + in_double = false + escape = false + + val.each_char.with_index do |ch, i| + if escape + escape = false + next + end + + if in_single + in_single = false if ch == "'" + elsif in_double + if ch == "\\" + escape = true + elsif ch == '"' + in_double = false + end + else + case ch + when "'" then in_single = true + when '"' then in_double = true + when "#" then return val[0...i].rstrip + end + end + end + + val + end + end + + class Builder + VALID_OPS = %w[= != > < >= <= ~>].freeze + ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze + MAX_ALIAS_RESOLUTIONS = 1_000 + + def initialize(permitted_classes: [], permitted_symbols: [], aliases: true) + @permitted_classes = permitted_classes.map {|c| "!ruby/object:#{c}" } + @permitted_symbols = permitted_symbols + @aliases = aliases + @anchor_values = {} + @alias_count = 0 + end + + def build(node) + return nil if node.nil? + + result = build_node(node) + + if result.is_a?(Hash) && result[:tag] == "!ruby/object:Gem::Specification" + build_specification(result) + else + result + end + end + + private + + def build_node(node) + case node + when nil then nil + when AliasRef then resolve_alias(node) + when Scalar then store_anchor(node.anchor, node.value) + when Mapping then build_mapping(node) + when Sequence then store_anchor(node.anchor, node.items.map {|item| build_node(item) }) + else node # already a Ruby object + end + end + + def resolve_alias(node) + raise Psych::AliasesNotEnabled unless @aliases + @alias_count += 1 + if @alias_count > MAX_ALIAS_RESOLUTIONS + raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})" + end + unless @anchor_values.key?(node.name) + klass = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias + raise klass, "An alias referenced an unknown anchor: #{node.name}" + end + @anchor_values.fetch(node.name) + end + + def store_anchor(name, value) + @anchor_values[name] = value if name + value + end + + def build_mapping(node) + validate_tag!(node.tag) if node.tag + + result = case node.tag + when "!ruby/object:Gem::Version" + build_version(node) + when "!ruby/object:Gem::Platform" + build_platform(node) + when "!ruby/object:Gem::Requirement", "!ruby/object:Gem::Version::Requirement" + build_requirement(node) + when "!ruby/object:Gem::Dependency" + build_dependency(node) + when nil + build_hash(node) + when "!ruby/object:Gem::Specification" + hash = build_hash(node) + hash[:tag] = node.tag + hash + else + raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}" + end + + store_anchor(node.anchor, result) + end + + def build_hash(node) + result = {} + node.pairs.each do |key_node, value_node| + key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s + value = build_node(value_node) + + if ARRAY_FIELDS.include?(key) + value = normalize_array_field(value) + end + + result[key] = value + end + result + end + + def build_version(node) + hash = pairs_to_hash(node) + Gem::Version.new((hash["version"] || hash["value"]).to_s) + end + + PLATFORM_FIELDS = %w[cpu os version].freeze + PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze + + def build_platform(node) + hash = pairs_to_hash(node) + if (hash.keys & PLATFORM_FIELDS).any? + Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]]) + elsif hash["value"].is_a?(Array) + # Malformed platform (e.g. sequence instead of mapping). + # Return the raw value so yaml_initialize handles it like Psych does. + hash["value"] + else + plat = Gem::Platform.allocate + hash.each do |k, v| + plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k) + end + plat + end + end + + def build_requirement(node) + r = Gem::Requirement.allocate + hash = pairs_to_hash(node) + reqs = hash["requirements"] || hash["value"] + + if reqs.is_a?(Array) && !reqs.empty? + safe_reqs = [] + reqs.each do |item| + if item.is_a?(Array) && item.size == 2 + op = item[0].to_s + ver = item[1] + if VALID_OPS.include?(op) + version_obj = ver.is_a?(Gem::Version) ? ver : Gem::Version.new(ver.to_s) + safe_reqs << [op, version_obj] + end + elsif item.is_a?(String) + parsed = Gem::Requirement.parse(item) + safe_reqs << parsed + end + rescue Gem::Requirement::BadRequirementError, Gem::Version::BadVersionError + # Skip malformed items silently + end + reqs = safe_reqs unless safe_reqs.empty? + end + + r.instance_variable_set(:@requirements, reqs) + r + end + + def build_dependency(node) + hash = pairs_to_hash(node) + d = Gem::Dependency.allocate + d.instance_variable_set(:@name, hash["name"]) + + d.instance_variable_set(:@requirement, hash["requirement"] || hash["version_requirements"]) + + raw_type = hash["type"] + if raw_type + name = raw_type.to_s.sub(/^:/, "") + validate_symbol!(name) + type = name.to_sym + else + type = :runtime + end + d.instance_variable_set(:@type, type) + + d.instance_variable_set(:@prerelease, ["true", true].include?(hash["prerelease"])) + d.instance_variable_set(:@version_requirements, d.instance_variable_get(:@requirement)) + d + end + + def build_specification(hash) + spec = Gem::Specification.allocate + + normalize_specification_version!(hash) + normalize_array_fields!(hash) + + spec.yaml_initialize("!ruby/object:Gem::Specification", hash) + spec + end + + def pairs_to_hash(node) + result = {} + node.pairs.each do |key_node, value_node| + key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s + result[key] = build_node(value_node) + end + result + end + + def validate_tag!(tag) + return if @permitted_classes.include?(tag) + raise_disallowed_class!(tag) + end + + def raise_disallowed_class!(tag) + if defined?(Psych::VERSION) + raise Psych::DisallowedClass.new("load", tag) + else + raise Psych::DisallowedClass, "Tried to load unspecified class: #{tag}" + end + end + + def validate_symbol!(name) + return if @permitted_symbols.empty? || @permitted_symbols.include?(name) + + label = ":#{name}" + if defined?(Psych::VERSION) + raise Psych::DisallowedClass.new("load", label) + else + raise Psych::DisallowedClass, "Tried to load unspecified class: #{label}" + end + end + + def normalize_specification_version!(hash) + val = hash["specification_version"] + return unless val && !val.is_a?(Integer) + hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val) + end + + def normalize_array_fields!(hash) + ARRAY_FIELDS.each do |field| + hash[field] = normalize_array_field(hash[field]) if hash[field] + end + end + + def normalize_array_field(value) + if value.is_a?(Hash) + value.values.flatten.compact + elsif !value.is_a?(Array) && value + [value].flatten.compact + else + value + end + end + end + + class Emitter + def emit(obj) + "---#{emit_node(obj, 0)}" + end + + private + + def emit_node(obj, indent, quote: false) + case obj + when Gem::Specification then emit_specification(obj, indent) + when Gem::Version then emit_version(obj, indent) + when Gem::Platform then emit_platform(obj, indent) + when Gem::Requirement then emit_requirement(obj, indent) + when Gem::Dependency then emit_dependency(obj, indent) + when Hash then emit_hash(obj, indent) + when Array then emit_array(obj, indent) + when Time then emit_time(obj) + when String then emit_string(obj, indent, quote: quote) + when NilClass + "\n" + when Numeric, Symbol, TrueClass, FalseClass + " #{obj.inspect}\n" + else + " #{obj.to_s.inspect}\n" + end + end + + def emit_specification(spec, indent) + parts = [" !ruby/object:Gem::Specification\n"] + parts << "#{pad(indent)}name:#{emit_node(spec.name, indent + 2)}" + parts << "#{pad(indent)}version:#{emit_node(spec.version, indent + 2)}" + parts << "#{pad(indent)}platform: #{spec.platform}\n" + if spec.platform.to_s != spec.original_platform.to_s + parts << "#{pad(indent)}original_platform: #{spec.original_platform}\n" + end + + attributes = Gem::Specification.attribute_names.map(&:to_s).sort - %w[name version platform] + attributes.each do |name| + val = spec.instance_variable_get("@#{name}") + next if val.nil? + parts << "#{pad(indent)}#{name}:#{emit_node(val, indent + 2)}" + end + + res = parts.join + res << "\n" unless res.end_with?("\n") + res + end + + def emit_version(ver, indent) + " !ruby/object:Gem::Version\n" \ + "#{pad(indent)}version: #{emit_node(ver.version.to_s, indent + 2).lstrip}" + end + + def emit_platform(plat, indent) + " !ruby/object:Gem::Platform\n" \ + "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \ + "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \ + "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}" + end + + def emit_requirement(req, indent) + " !ruby/object:Gem::Requirement\n" \ + "#{pad(indent)}requirements:#{emit_node(req.requirements, indent + 2)}" + end + + def emit_dependency(dep, indent) + [ + " !ruby/object:Gem::Dependency\n", + "#{pad(indent)}name: #{emit_node(dep.name, indent + 2).lstrip}", + "#{pad(indent)}requirement:#{emit_node(dep.requirement, indent + 2)}", + "#{pad(indent)}type: #{emit_node(dep.type, indent + 2).lstrip}", + "#{pad(indent)}prerelease: #{emit_node(dep.prerelease?, indent + 2).lstrip}", + "#{pad(indent)}version_requirements:#{emit_node(dep.requirement, indent + 2)}", + ].join + end + + def emit_hash(hash, indent) + if hash.empty? + " {}\n" + else + parts = ["\n"] + hash.each do |k, v| + is_symbol = k.is_a?(Symbol) || (k.is_a?(String) && k.start_with?(":")) + key_str = k.is_a?(Symbol) ? k.inspect : k.to_s + parts << "#{pad(indent)}#{key_str}:#{emit_node(v, indent + 2, quote: is_symbol)}" + end + parts.join + end + end + + def emit_array(arr, indent) + if arr.empty? + " []\n" + else + parts = ["\n"] + arr.each do |v| + parts << "#{pad(indent)}-#{emit_node(v, indent + 2)}" + end + parts.join + end + end + + def emit_time(time) + " #{time.utc.strftime("%Y-%m-%d %H:%M:%S.%N Z")}\n" + end + + def emit_string(str, indent, quote: false) + if str.include?("\n") + emit_block_scalar(str, indent) + elsif needs_quoting?(str, quote) + " #{str.to_s.inspect}\n" + else + " #{str}\n" + end + end + + def emit_block_scalar(str, indent) + parts = [str.end_with?("\n") ? " |\n" : " |-\n"] + str.each_line do |line| + parts << "#{pad(indent + 2)}#{line}" + end + res = parts.join + res << "\n" unless res.end_with?("\n") + res + end + + def needs_quoting?(str, quote) + quote || str.empty? || + str =~ /^[!*&:@%$]/ || str =~ /^-?\d+(\.\d+)?$/ || str =~ /^[<>=-]/ || + str == "true" || str == "false" || str == "nil" || + str.include?(":") || str.include?("#") || str.include?("[") || str.include?("]") || + str.include?("{") || str.include?("}") || str.include?(",") + end + + def pad(indent) + " " * indent + end + end + + module_function + + def dump(obj) + Emitter.new.emit(obj) + end + + def load(str, permitted_classes: [], permitted_symbols: [], aliases: true) + raise TypeError, "no implicit conversion of nil into String" if str.nil? + return nil if str.empty? + + ast = Parser.new(str).parse + return nil if ast.nil? + + Builder.new( + permitted_classes: permitted_classes, + permitted_symbols: permitted_symbols, + aliases: aliases + ).build(ast) + end + end +end |
