diff options
Diffstat (limited to 'lib/rubygems')
260 files changed, 26209 insertions, 8265 deletions
diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb index 499483d9e9..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) or !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,11 +147,11 @@ 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 and - dep.requirement.satisfied_by? installed_spec.version + dep.name == installed_spec.name && + dep.requirement.satisfied_by?(installed_spec.version) end end diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index 665b87fc0e..0380fceece 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. @@ -47,7 +48,7 @@ class Gem::BasicSpecification # directory. def gem_build_complete_path # :nodoc: - File.join extension_dir, 'gem.build_complete' + File.join extension_dir, "gem.build_complete" end ## @@ -75,15 +76,21 @@ class Gem::BasicSpecification elsif missing_extensions? @ignored = true - if Gem::Platform::RUBY == platform || Gem::Platform.local === platform - warn "Ignoring #{full_name} because its extensions are not built. " + - "Try: gem pristine #{name} --version #{version}" + 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 def default_gem? @@ -95,7 +102,7 @@ class Gem::BasicSpecification # 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 ## @@ -103,15 +110,13 @@ class Gem::BasicSpecification def extensions_dir Gem.default_ext_dir_for(base_dir) || - File.join(base_dir, 'extensions', Gem::Platform.local.to_s, + File.join(base_dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version) 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 @@ -131,10 +136,10 @@ class Gem::BasicSpecification # default Ruby platform. def full_name - if platform == Gem::Platform::RUBY or platform.nil? - "#{name}-#{version}".dup.tap(&Gem::UNTAINT) + if platform == Gem::Platform::RUBY || platform.nil? + "#{name}-#{version}" else - "#{name}-#{version}-#{platform}".dup.tap(&Gem::UNTAINT) + "#{name}-#{version}-#{platform}" end end @@ -144,15 +149,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,7 +165,7 @@ 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 ## @@ -170,18 +175,14 @@ 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 @@ -271,9 +272,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 +289,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(',')}}" - else - self.raw_require_paths.first - end - else - "lib" # default value for require_paths for bundler/inline - end + dirs = if raw_require_paths + if raw_require_paths.size > 1 + "{#{raw_require_paths.join(",")}}" + else + 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 +324,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 9ce0a2378e..dd2fd77418 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -2,48 +2,18 @@ module Gem::BundlerVersionFinder def self.bundler_version - version, _ = bundler_version_with_reason + v = ENV["BUNDLER_VERSION"] - return unless version + v ||= bundle_update_bundler_version + return if v == true - Gem::Version.new(version) - end - - def self.bundler_version_with_reason - if v = ENV["BUNDLER_VERSION"] - return [v, "`$BUNDLER_VERSION`"] - end - if v = bundle_update_bundler_version - return if v == true - return [v, "`bundle update --bundler`"] - end - v, lockfile = lockfile_version - if v - return [v, "your #{lockfile}"] - end - end + v ||= lockfile_version + return unless v - def self.missing_version_message - return unless vr = bundler_version_with_reason - <<-EOS -Could not find 'bundler' (#{vr.first}) required by #{vr.last}. -To update to the latest version installed on your system, run `bundle update --bundler`. -To install the missing version, run `gem install bundler:#{vr.first}` - EOS + Gem::Version.new(v) end - def self.compatible?(spec) - return true unless spec.name == "bundler".freeze - return true unless bundler_version = self.bundler_version - - spec.version.segments.first == bundler_version.segments.first - end - - def self.filter!(specs) - return unless bundler_version = self.bundler_version - - specs.reject! {|spec| spec.version.segments.first != bundler_version.segments.first } - + def self.prioritize!(specs) exact_match_index = specs.find_index {|spec| spec.version == bundler_version } return unless exact_match_index @@ -51,7 +21,7 @@ To install the missing version, run `gem install bundler:#{vr.first}` end def self.bundle_update_bundler_version - return unless File.basename($0) == "bundle".freeze + return unless File.basename($0) == "bundle" return unless "update".start_with?(ARGV.first || " ") bundler_version = nil update_index = nil @@ -68,23 +38,21 @@ To install the missing version, run `gem install bundler:#{vr.first}` private_class_method :bundle_update_bundler_version def self.lockfile_version - return unless lockfile = lockfile_contents - lockfile, contents = lockfile - lockfile ||= "lockfile" + return unless contents = lockfile_contents regexp = /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/ return unless contents =~ regexp - [$1, lockfile] + $1 end private_class_method :lockfile_version def self.lockfile_contents gemfile = ENV["BUNDLE_GEMFILE"] - gemfile = nil if gemfile && gemfile.empty? + 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 @@ -99,11 +67,11 @@ To install the missing version, run `gem install bundler:#{vr.first}` lockfile = case gemfile when "gems.rb" then "gems.locked" else "#{gemfile}.lock" - end.dup.tap(&Gem::UNTAINT) + end return unless File.file?(lockfile) - [lockfile, File.read(lockfile)] + File.read(lockfile) end private_class_method :lockfile_contents 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 9f935e6285..ec498a8b94 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'optparse' -require_relative 'requirement' -require_relative 'user_interaction' +require_relative "vendored_optparse" +require_relative "requirement" +require_relative "user_interaction" ## # Base class for all Gem commands. When creating a new gem command, define @@ -19,9 +20,7 @@ require_relative 'user_interaction' class Gem::Command include Gem::UserInteraction - OptionParser.accept Symbol do |value| - value.to_sym - end + Gem::OptionParser.accept Symbol, &:to_sym ## # The name of the command. @@ -76,7 +75,7 @@ class Gem::Command when Array @extra_args = value when String - @extra_args = value.split(' ') + @extra_args = value.split(" ") end end @@ -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 @@ -159,11 +158,11 @@ class Gem::Command gem = "'#{gem_name}' (#{version})" msg = String.new "Could not find a valid gem #{gem}" - if errors and !errors.empty? + if errors && !errors.empty? msg << ", here is why:\n" errors.each {|x| msg << " #{x.wordy}\n" } else - if required_by and gem != required_by + if required_by && gem != required_by msg << " (required by #{required_by}) in any repository" else msg << " in any repository" @@ -186,12 +185,12 @@ class Gem::Command def get_all_gem_names args = options[:args] - if args.nil? or args.empty? + if args.nil? || args.empty? raise Gem::CommandLineError, "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 @@ -216,14 +219,14 @@ class Gem::Command def get_one_gem_name args = options[:args] - if args.nil? or args.empty? + if args.nil? || args.empty? raise Gem::CommandLineError, "Please specify a gem name on the command line (e.g. gem build GEMNAME)" end 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 @@ -344,7 +347,7 @@ class Gem::Command ## # Add a command-line option and handler to the command. # - # See OptionParser#make_switch for an explanation of +opts+. + # See Gem::OptionParser#make_switch for an explanation of +opts+. # # +handler+ will be called with two values, the value of the argument and # the options hash. @@ -355,6 +358,8 @@ class Gem::Command def add_option(*opts, &handler) # :yields: value, options group_name = Symbol === opts.first ? opts.shift : :options + raise "Do not pass an empty string in opts" if opts.include?("") + @option_groups[group_name] << [opts, handler] end @@ -391,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 @@ -423,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 ## @@ -455,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 @@ -471,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: @@ -483,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 @@ -510,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 @@ -520,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 @@ -538,7 +540,7 @@ class Gem::Command # command. def create_option_parser - @parser = OptionParser.new + @parser = Gem::OptionParser.new add_parser_options @@ -552,9 +554,9 @@ class Gem::Command end def configure_options(header, option_list) - return if option_list.nil? or option_list.empty? + return if option_list.nil? || option_list.empty? - header = header.to_s.empty? ? '' : "#{header} " + header = header.to_s.empty? ? "" : "#{header} " @parser.separator " #{header}Options:" option_list.each do |args, handler| @@ -563,7 +565,7 @@ class Gem::Command end end - @parser.separator '' + @parser.separator "" end ## @@ -576,27 +578,27 @@ class Gem::Command # ---------------------------------------------------------------- # Add the options common to all commands. - add_common_option('-h', '--help', - 'Get help on this command') do |value, options| + add_common_option("-h", "--help", + "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| + add_common_option("-V", "--[no-]verbose", + "Set the verbose level of output") do |value, _options| # Set us to "really verbose" so the progress meter works - if Gem.configuration.verbose and value + if Gem.configuration.verbose && value Gem.configuration.verbose = 1 else Gem.configuration.verbose = value 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 @@ -604,31 +606,35 @@ class Gem::Command # commands. Both options are actually handled before the other # options get parsed. - add_common_option('--config-file FILE', - 'Use this config file instead of default') do + add_common_option("--config-file FILE", + "Use this config file instead of default") do end - add_common_option('--backtrace', - 'Show stack backtrace on errors') do + add_common_option("--backtrace", + "Show stack backtrace on errors") do end - add_common_option('--debug', - 'Turn on Ruby debugging') do + add_common_option("--debug", + "Turn on Ruby debugging") do end - add_common_option('--norc', - 'Avoid loading any .gemrc file') do + add_common_option("--norc", + "Avoid loading any .gemrc file") do end # :stopdoc: - HELP = <<-HELP.freeze + HELP = <<-HELP RubyGems is a package manager for Ruby. Usage: gem -h/--help gem -v/--version - gem command [arguments...] [options...] + gem [global options...] command [arguments...] [options...] + + Global options: + -C PATH run as if gem was started in <PATH> + instead of the current working directory Examples: gem install rake diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 2409550882..8e578dc196 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/command' -require 'rubygems/user_interaction' -require 'rubygems/text' +require_relative "command" +require_relative "user_interaction" +require_relative "text" ## # The command manager registers and installs all the individual sub-commands @@ -43,6 +44,7 @@ class Gem::CommandManager :contents, :dependency, :environment, + :exec, :fetch, :generate_index, :help, @@ -58,6 +60,7 @@ class Gem::CommandManager :push, :query, :rdoc, + :rebuild, :search, :server, :signin, @@ -73,14 +76,16 @@ class Gem::CommandManager ].freeze ALIAS_COMMANDS = { - 'i' => 'install', + "i" => "install", + "login" => "signin", + "logout" => "signout", }.freeze ## # Return the authoritative instance of the command manager. def self.instance - @command_manager ||= new + @instance ||= new end ## @@ -95,14 +100,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| @@ -137,7 +142,7 @@ 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 ## @@ -145,8 +150,13 @@ class Gem::CommandManager def run(args, build_args=nil) process_args(args, build_args) - rescue StandardError, Timeout::Error => ex - alert_error clean_text("While executing gem ... (#{ex.class})\n #{ex}") + rescue StandardError, Gem::Timeout::Error => ex + if ex.respond_to?(:detailed_message) + msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 } + else + msg = ex.message + end + alert_error clean_text("While executing gem ... (#{ex.class})\n #{msg}") ui.backtrace ex terminate_interaction(1) @@ -162,20 +172,26 @@ class Gem::CommandManager end case args.first - when '-h', '--help' then + when "-h", "--help" then say Gem::Command::HELP terminate_interaction 0 - when '-v', '--version' then + when "-v", "--version" then say Gem::VERSION terminate_interaction 0 + when "-C" then + args.shift + start_point = args.shift + if Dir.exist?(start_point) + Dir.chdir(start_point) { invoke_command(args, build_args) } + else + alert_error clean_text("#{start_point} isn't a directory.") + terminate_interaction 1 + end when /^-/ then alert_error clean_text("Invalid option: #{args.first}. See 'gem --help'.") terminate_interaction 1 else - cmd_name = args.shift.downcase - cmd = find_command cmd_name - cmd.deprecation_warning if cmd.deprecated? - cmd.invoke_with_build_args args, build_args + invoke_command(args, build_args) end end @@ -186,7 +202,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 @@ -223,11 +239,19 @@ class Gem::CommandManager load_error = e end Gem::Commands.const_get(const_name).new - rescue Exception => e + rescue StandardError => e e = load_error if load_error alert_error clean_text("Loading command: #{command_name} (#{e.class})\n\t#{e}") ui.backtrace e end end + + 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 end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index fff5f7c76f..2ec8324141 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -1,31 +1,37 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/package' -require 'rubygems/version_option' + +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' + 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| + 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| + 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: @@ -71,17 +77,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 998df0621b..72dcf1dd17 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -1,92 +1,70 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/security' + +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 => [] - - OptionParser.accept OpenSSL::X509::Certificate do |certificate_file| - begin - certificate = OpenSSL::X509::Certificate.new File.read certificate_file - rescue Errno::ENOENT - raise OptionParser::InvalidArgument, "#{certificate_file}: does not exist" - rescue OpenSSL::X509::CertificateError - raise OptionParser::InvalidArgument, - "#{certificate_file}: invalid X509 certificate" - end - [certificate, certificate_file] - end - - OptionParser.accept OpenSSL::PKey::RSA do |key_file| - begin - passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - key = OpenSSL::PKey::RSA.new File.read(key_file), passphrase - rescue Errno::ENOENT - raise OptionParser::InvalidArgument, "#{key_file}: does not exist" - rescue OpenSSL::PKey::RSAError - raise OptionParser::InvalidArgument, "#{key_file}: invalid RSA key" - end - - raise OptionParser::InvalidArgument, - "#{key_file}: private key not found" unless key.private? - - key - end + super "cert", "Manage RubyGems certificates and signing settings", + add: [], remove: [], list: [], build: [], sign: [] - add_option('-a', '--add CERT', OpenSSL::X509::Certificate, - 'Add a trusted certificate.') do |(cert, _), options| - options[:add] << cert + add_option("-a", "--add CERT", + "Add a trusted certificate.") do |cert_file, options| + options[:add] << open_cert(cert_file) end - add_option('-l', '--list [FILTER]', - 'List trusted certificates where the', - 'subject contains FILTER') do |filter, options| - filter ||= '' + add_option("-l", "--list [FILTER]", + "List trusted certificates where the", + "subject contains FILTER") do |filter, options| + filter ||= "" options[:list] << filter end - add_option('-r', '--remove FILTER', - 'Remove trusted certificates where the', - 'subject contains FILTER') do |filter, options| + add_option("-r", "--remove FILTER", + "Remove trusted certificates where the", + "subject contains FILTER") do |filter, options| options[:remove] << filter end - add_option('-b', '--build EMAIL_ADDR', - 'Build private key and self-signed', - 'certificate for EMAIL_ADDR') do |email_address, options| + add_option("-b", "--build EMAIL_ADDR", + "Build private key and self-signed", + "certificate for EMAIL_ADDR") do |email_address, options| options[:build] << email_address end - add_option('-C', '--certificate CERT', OpenSSL::X509::Certificate, - 'Signing certificate for --sign') do |(cert, cert_file), options| - options[:issuer_cert] = cert + add_option("-C", "--certificate CERT", + "Signing certificate for --sign") do |cert_file, options| + options[:issuer_cert] = open_cert(cert_file) options[:issuer_cert_file] = cert_file end - add_option('-K', '--private-key KEY', OpenSSL::PKey::RSA, - 'Key for --sign or --build') do |key, options| - options[:key] = key + add_option("-K", "--private-key KEY", + "Key for --sign or --build") do |key_file, options| + options[:key] = open_private_key(key_file) + end + + add_option("-A", "--key-algorithm ALGORITHM", + "Select which key algorithm to use for --build") do |algorithm, options| + options[:key_algorithm] = algorithm end - add_option('-s', '--sign CERT', - 'Signs CERT with the key from -K', - 'and the certificate from -C') do |cert_file, options| - raise OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless + add_option("-s", "--sign CERT", + "Signs CERT with the key from -K", + "and the certificate from -C") do |cert_file, options| + raise Gem::OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless File.file? cert_file options[:sign] << cert_file end - add_option('-d', '--days NUMBER_OF_DAYS', - 'Days before the certificate expires') do |days, options| + add_option("-d", "--days NUMBER_OF_DAYS", + "Days before the certificate expires") do |days, options| options[:expiration_length_days] = days.to_i end - add_option('-R', '--re-sign', - 'Re-signs the certificate from -C with the key from -K') do |resign, options| + add_option("-R", "--re-sign", + "Re-signs the certificate from -C with the key from -K") do |resign, options| options[:resign] = resign end end @@ -97,7 +75,39 @@ class Gem::Commands::CertCommand < Gem::Command say "Added '#{certificate.subject}'" end + def check_openssl + return if Gem::HAVE_OPENSSL + + alert_error "OpenSSL library is required for the cert command" + terminate_interaction 1 + end + + def open_cert(certificate_file) + check_openssl + OpenSSL::X509::Certificate.new File.read certificate_file + rescue Errno::ENOENT + raise Gem::OptionParser::InvalidArgument, "#{certificate_file}: does not exist" + rescue OpenSSL::X509::CertificateError + raise Gem::OptionParser::InvalidArgument, + "#{certificate_file}: invalid X509 certificate" + end + + def open_private_key(key_file) + check_openssl + passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] + key = OpenSSL::PKey.read File.read(key_file), passphrase + raise Gem::OptionParser::InvalidArgument, + "#{key_file}: private key not found" unless key.private? + key + rescue Errno::ENOENT + raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist" + rescue OpenSSL::PKey::PKeyError, ArgumentError + raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key" + end + def execute + check_openssl + options[:add].each do |certificate| add_certificate certificate end @@ -126,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 @@ -143,7 +153,7 @@ class Gem::Commands::CertCommand < Gem::Command def build_cert(email, key) # :nodoc: expiration_length_days = options[:expiration_length_days] || - Gem.configuration.cert_expiration_length_days + Gem.configuration.cert_expiration_length_days cert = Gem::Security.create_cert_email( email, @@ -157,19 +167,20 @@ class Gem::Commands::CertCommand < Gem::Command def build_key # :nodoc: return options[:key] if options[:key] - passphrase = ask_for_password 'Passphrase for your Private Key:' + passphrase = ask_for_password "Passphrase for your Private Key:" say "\n" - passphrase_confirmation = ask_for_password 'Please repeat the passphrase for your Private Key:' + passphrase_confirmation = ask_for_password "Please repeat the passphrase for your Private Key:" say "\n" raise Gem::CommandLineError, "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation - key = Gem::Security.create_key - key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase + 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", 0o600, passphrase - return key, key_path + [key, key_path] end def certificates_matching(filter) @@ -250,14 +261,14 @@ For further reading on signing gems see `ri Gem::Security`. def load_default_key key_file = File.join Gem.default_key_path key = File.read key_file - passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] - options[:key] = OpenSSL::PKey::RSA.new key, passphrase + 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" terminate_interaction 1 - rescue OpenSSL::PKey::RSAError + rescue OpenSSL::PKey::PKeyError alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid" @@ -280,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] @@ -311,4 +322,4 @@ For further reading on signing gems see `ri Gem::Security`. # It's simple, but is all we need email =~ /\A.+@.+\z/ end -end if Gem::HAVE_OPENSSL +end diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 8b8eda53cf..fb23dd9cb4 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -1,63 +1,68 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' -require 'rubygems/validator' -require 'rubygems/doctor' + +require_relative "../command" +require_relative "../version_option" +require_relative "../validator" +require_relative "../doctor" class Gem::Commands::CheckCommand < Gem::Command include Gem::VersionOption def initialize - super 'check', 'Check a gem repository for added or missing files', - :alien => true, :doctor => false, :dry_run => false, :gems => true + super "check", "Check a gem repository for added or missing files", + alien: true, doctor: false, dry_run: false, gems: true - add_option('-a', '--[no-]alien', + add_option("-a", "--[no-]alien", 'Report "unmanaged" or rogue files in the', - 'gem repository') do |value, options| + "gem repository") do |value, options| options[:alien] = value end - add_option('--[no-]doctor', - 'Clean up uninstalled gems and broken', - 'specifications') do |value, options| + add_option("--[no-]doctor", + "Clean up uninstalled gems and broken", + "specifications") do |value, options| options[:doctor] = value end - add_option('--[no-]dry-run', - 'Do not remove files, only report what', - 'would be removed') do |value, options| + add_option("--[no-]dry-run", + "Do not remove files, only report what", + "would be removed") do |value, options| options[:dry_run] = value end - add_option('--[no-]gems', - 'Check installed gems for problems') do |value, options| + add_option("--[no-]gems", + "Check installed gems for problems") do |value, options| options[:gems] = value end - add_version_option 'check' + add_version_option "check" end def check_gems - say 'Checking 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 end def doctor - say 'Checking for files from uninstalled gems...' + say "Checking for files from uninstalled gems..." say Gem.path.each do |gem_repo| @@ -72,11 +77,11 @@ class Gem::Commands::CheckCommand < Gem::Command end def arguments # :nodoc: - 'GEMNAME name of gem to check' + "GEMNAME name of gem to check" end def defaults_str # :nodoc: - '--gems --alien' + "--gems --alien" end def description # :nodoc: diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 662badce33..08fb598cea 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -1,35 +1,36 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/dependency_list' -require 'rubygems/uninstaller' + +require_relative "../command" +require_relative "../dependency_list" +require_relative "../uninstaller" 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 + super "cleanup", + "Clean up old versions of installed gems", + force: false, install_dir: Gem.dir, + check_dev: true - add_option('-n', '-d', '--dry-run', - 'Do not uninstall gems') do |value, options| + add_option("-n", "-d", "--dry-run", + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end - add_option(:Deprecated, '--dryrun', - 'Do not uninstall gems') do |value, options| + add_option(:Deprecated, "--dryrun", + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end - deprecate_option('--dryrun', extra_msg: 'Use --dry-run instead') + deprecate_option("--dryrun", extra_msg: "Use --dry-run instead") - add_option('-D', '--[no-]check-development', - 'Check development dependencies while uninstalling', - '(default: true)') do |value, options| + add_option("-D", "--[no-]check-development", + "Check development dependencies while uninstalling", + "(default: true)") do |value, options| options[:check_dev] = value end - add_option('--[no-]user-install', - 'Cleanup in user\'s home directory instead', - 'of GEM_HOME.') do |value, options| + add_option("--[no-]user-install", + "Cleanup in user's home directory instead", + "of GEM_HOME.") do |value, options| options[:user_install] = value end @@ -74,7 +75,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,9 +88,9 @@ 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 @@ -116,13 +117,13 @@ If no gems are named all gems in GEM_HOME are cleaned. 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 - Gem::Specification.to_a - end + @candidate_gems = if options[:args].empty? + Gem::Specification.to_a + else + options[:args].map do |gem_name| + Gem::Specification.find_all_by_name gem_name + end.flatten + end end def get_gems_to_cleanup @@ -130,9 +131,7 @@ 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 @@ -149,7 +148,7 @@ If no gems are named all gems in GEM_HOME are cleaned. @primary_gems = {} Gem::Specification.each do |spec| - if @primary_gems[spec.name].nil? or + if @primary_gems[spec.name].nil? || @primary_gems[spec.name].version < spec.version @primary_gems[spec.name] = spec end @@ -167,8 +166,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 diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb index f17aed64db..807158d9c9 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -1,39 +1,40 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../version_option" class Gem::Commands::ContentsCommand < Gem::Command include Gem::VersionOption def initialize - super 'contents', 'Display the contents of the installed gems', - :specdirs => [], :lib_only => false, :prefix => true, - :show_install_dir => false + super "contents", "Display the contents of the installed gems", + specdirs: [], lib_only: false, prefix: true, + show_install_dir: false add_version_option - add_option('--all', + add_option("--all", "Contents for all gems") do |all, options| options[:all] = all end - add_option('-s', '--spec-dir a,b,c', Array, + add_option("-s", "--spec-dir a,b,c", Array, "Search for gems under specific paths") do |spec_dirs, options| options[:specdirs] = spec_dirs end - add_option('-l', '--[no-]lib-only', + add_option("-l", "--[no-]lib-only", "Only return files in the Gem's lib_dirs") do |lib_only, options| options[:lib_only] = lib_only end - add_option('--[no-]prefix', + add_option("--[no-]prefix", "Don't include installed path prefix") do |prefix, options| options[:prefix] = prefix end - add_option('--[no-]show-install-dir', - 'Show only the gem install dir') do |show, options| + add_option("--[no-]show-install-dir", + "Show only the gem install dir") do |show, options| options[:show_install_dir] = show end @@ -77,7 +78,7 @@ prefix or only the files that are requireable. gem_contents name end - terminate_interaction 1 unless found or names.length > 1 + terminate_interaction 1 unless found || names.length > 1 end end @@ -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, "")] @@ -103,13 +104,13 @@ prefix or only the files that are requireable. def files_in_default_gem(spec) spec.files.map do |file| case file - when /\A#{spec.bindir}\// + when %r{\A#{spec.bindir}/} # $' is POSTMATCH - [RbConfig::CONFIG['bindir'], $'] + [RbConfig::CONFIG["bindir"], $'] when /\.so\z/ - [RbConfig::CONFIG['archdir'], file] + [RbConfig::CONFIG["archdir"], file] else - [RbConfig::CONFIG['rubylibdir'], file] + [RbConfig::CONFIG["rubylibdir"], file] end end end @@ -177,7 +178,7 @@ prefix or only the files that are requireable. @spec_dirs.sort.each {|dir| say dir } end - return nil + nil end def specification_directories # :nodoc: diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb index e472d8fa8d..9aaefae999 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -1,28 +1,28 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../version_option" class Gem::Commands::DependencyCommand < Gem::Command include Gem::LocalRemoteOptions include Gem::VersionOption def initialize - super 'dependency', - 'Show the dependencies of an installed gem', - :version => Gem::Requirement.default, :domain => :local + super "dependency", + "Show the dependencies of an installed gem", + 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| + add_option("-R", "--[no-]reverse-dependencies", + "Include reverse dependencies in the output") do |value, options| options[:reverse_dependencies] = value end - add_option('-p', '--pipe', + add_option("-p", "--pipe", "Pipe Format (name --version ver)") do |value, options| options[:pipe_format] = value end @@ -53,47 +53,46 @@ use with other commands. "#{program_name} REGEXP" end - def fetch_remote_specs(dependency) # :nodoc: + def fetch_remote_specs(name, requirement, prerelease) # :nodoc: fetcher = Gem::SpecFetcher.fetcher - ss, = fetcher.spec_for_dependency dependency + specs_type = prerelease ? :complete : :released + + ss = if name.nil? + fetcher.detect(specs_type) { true } + else + fetcher.detect(specs_type) do |name_tuple| + name === name_tuple.name && requirement.satisfied_by?(name_tuple.version) + end + end - ss.map {|spec, _| spec } + ss.map {|tuple, source| source.fetch_spec(tuple) } end - def fetch_specs(name_pattern, dependency) # :nodoc: + def fetch_specs(name_pattern, requirement, prerelease) # :nodoc: specs = [] if local? specs.concat Gem::Specification.stubs.find_all {|spec| - name_pattern =~ spec.name and - dependency.requirement.satisfied_by? spec.version + name_matches = name_pattern ? name_pattern =~ spec.name : true + version_matches = requirement.satisfied_by?(spec.version) + + name_matches && version_matches }.map(&:to_spec) end - specs.concat fetch_remote_specs dependency if remote? + specs.concat fetch_remote_specs name_pattern, requirement, prerelease if remote? ensure_specs specs specs.uniq.sort end - def gem_dependency(pattern, version, prerelease) # :nodoc: - dependency = Gem::Deprecate.skip_during do - Gem::Dependency.new pattern, version - end - - dependency.prerelease = prerelease - - dependency - end - 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 @@ -119,11 +118,9 @@ use with other commands. ensure_local_only_reverse_dependencies pattern = name_pattern options[:args] + requirement = Gem::Requirement.new options[:version] - dependency = - gem_dependency pattern, options[:version], options[:prerelease] - - specs = fetch_specs pattern, dependency + specs = fetch_specs pattern, requirement, options[:prerelease] reverse = reverse_dependencies specs @@ -135,8 +132,8 @@ use with other commands. end def ensure_local_only_reverse_dependencies # :nodoc: - if options[:reverse_dependencies] and remote? and not local? - alert_error 'Only reverse dependencies for local gems are supported.' + if options[:reverse_dependencies] && remote? && !local? + alert_error "Only reverse dependencies for local gems are supported." terminate_interaction 1 end end @@ -144,7 +141,7 @@ use with other commands. def ensure_specs(specs) # :nodoc: return unless specs.empty? - patterns = options[:args].join ',' + patterns = options[:args].join "," say "No gems found matching #{patterns} (#{options[:version]})" if Gem.configuration.verbose @@ -153,23 +150,15 @@ use with other commands. def print_dependencies(spec, level = 0) # :nodoc: response = String.new - response << ' ' * level + "Gem #{spec.full_name}\n" + response << " " * level + "Gem #{spec.full_name}\n" unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| - response << ' ' * level + " #{dep}\n" + spec.dependencies.sort_by(&:name).each do |dep| + response << " " * level + " #{dep}\n" end end response end - def remote_specs(dependency) # :nodoc: - fetcher = Gem::SpecFetcher.fetcher - - ss, _ = fetcher.spec_for_dependency dependency - - ss.map {|s,o| s } - end - def reverse_dependencies(specs) # :nodoc: reverse = Hash.new {|h, k| h[k] = [] } @@ -192,7 +181,7 @@ use with other commands. sp.dependencies.each do |dep| dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep - if spec.name == dep.name and + if spec.name == dep.name && dep.requirement.satisfied_by?(spec.version) result << [sp.full_name, dep] end @@ -205,9 +194,9 @@ use with other commands. private def name_pattern(args) - args << '' if args.empty? + return if args.empty? - if args.length == 1 and args.first =~ /\A(.*)(i)?\z/m + if args.length == 1 && args.first =~ /\A(.*)(i)?\z/m flags = $2 ? Regexp::IGNORECASE : nil Regexp.new $1, flags else diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index 37429fb836..8ed0996069 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -1,21 +1,23 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" class Gem::Commands::EnvironmentCommand < Gem::Command def initialize - super 'environment', 'Display information about the RubyGems environment' + super "environment", "Display information about the RubyGems environment" end def arguments # :nodoc: args = <<-EOF - gemdir display the path where gems are installed - gempath display path used to search for gems + home display the path where gems are installed. Aliases: gemhome, gemdir, GEM_HOME + path display path used to search for gems. Aliases: gempath, GEM_PATH + user_gemhome display the path where gems are installed when `--user-install` is given. Aliases: user_gemdir version display the gem format version remotesources display the remote gem servers platform display the supported gem platforms <omitted> display everything EOF - return args.gsub(/^\s+/, '') + args.gsub(/^\s+/, "") end def description # :nodoc: @@ -80,6 +82,8 @@ lib/rubygems/defaults/operating_system.rb Gem.dir when /^gempath/, /^path/, /^GEM_PATH/ then Gem.path.join(File::PATH_SEPARATOR) + when /^user_gemdir/, /^user_gemhome/ then + Gem.user_dir when /^remotesources/ then Gem.sources.to_a.join("\n") when /^platform/ then @@ -104,9 +108,7 @@ 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" @@ -138,7 +140,7 @@ lib/rubygems/defaults/operating_system.rb out << " - GEM CONFIGURATION:\n" Gem.configuration.each do |name, value| - value = value.gsub(/./, '*') if name == 'gemcutter_key' + value = value.gsub(/./, "*") if name == "gemcutter_key" out << " - #{name.inspect} => #{value.inspect}\n" end @@ -149,7 +151,7 @@ lib/rubygems/defaults/operating_system.rb out << " - SHELL PATH:\n" - shell_path = ENV['PATH'].split(File::PATH_SEPARATOR) + shell_path = ENV["PATH"].split(File::PATH_SEPARATOR) add_path out, shell_path out @@ -169,6 +171,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..d588804290 --- /dev/null +++ b/lib/rubygems/commands/exec_command.rb @@ -0,0 +1,249 @@ +# 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 + gem_paths = { "GEM_HOME" => Gem.paths.home, "GEM_PATH" => Gem.paths.path.join(File::PATH_SEPARATOR), "GEM_SPEC_CACHE" => Gem.paths.spec_cache_dir }.compact + + 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! + ensure + ENV.update(gem_paths) if gem_paths + Gem.clear_paths + 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 = File.join(Gem.dir, "gem_exec") + + 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::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] + + exe = 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? + if (spec = Gem.loaded_specs[executable]) && (exe = spec.executable) + contains_executable << spec + else + alert_error "Failed to load executable `#{executable}`," \ + " are you sure the gem `#{options[:gem_name]}` contains it?" + terminate_interaction 1 + end + 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 + + load Gem.activate_bin_path(contains_executable.first.name, exe, ">= 0.a") + ensure + 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 6a1b346dd3..f7f5b62306 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -1,14 +1,20 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../version_option" class Gem::Commands::FetchCommand < Gem::Command include Gem::LocalRemoteOptions include Gem::VersionOption def initialize - super 'fetch', 'Download a gem and place it in the current directory' + defaults = { + suggest_alternate: true, + version: Gem::Requirement.default, + } + + super "fetch", "Download a gem and place it in the current directory", defaults add_bulk_threshold_option add_proxy_option @@ -18,10 +24,14 @@ class Gem::Commands::FetchCommand < Gem::Command add_version_option add_platform_option add_prerelease_option + + add_option "--[no-]suggestions", "Suggest alternates when gems are not found" do |value, options| + options[:suggest_alternate] = value + end end def arguments # :nodoc: - 'GEMNAME name of gem to download' + "GEMNAME name of gem to download" end def defaults_str # :nodoc: @@ -42,15 +52,27 @@ then repackaging it. "#{program_name} GEMNAME [GEMNAME ...]" 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 fetch 'my_gem:1.0.0' 'my_other_gem:~>2.0.0'`" + terminate_interaction 1 + end + end + def execute - version = options[:version] || Gem::Requirement.default + check_version + version = options[:version] platform = Gem.platforms.last - gem_names = get_all_gem_names + gem_names = get_all_gem_names_and_versions - gem_names.each do |gem_name| - dep = Gem::Dependency.new gem_name, version + gem_names.each do |gem_name, gem_version| + gem_version ||= version + dep = Gem::Dependency.new gem_name, gem_version dep.prerelease = options[:prerelease] + suppress_suggestions = !options[:suggest_alternate] specs_and_sources, errors = Gem::SpecFetcher.fetcher.spec_for_dependency dep @@ -60,15 +82,13 @@ then repackaging it. specs_and_sources = filtered unless filtered.empty? end - spec, source = specs_and_sources.max_by {|s,| s.version } + spec, source = specs_and_sources.max_by {|s,| s } if spec.nil? - show_lookup_failure gem_name, version, errors, options[:domain] + show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain] next end - source.download spec - say "Downloaded #{spec.full_name}" end end diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index 93e25ef5e4..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 'rubygems/command' -require 'rubygems/indexer' -## -# Generates a index files for use as a gem server. -# -# See `gem help generate_index` +require_relative "../command" -class Gem::Commands::GenerateIndexCommand < Gem::Command - def initialize - super 'generate_index', - 'Generates the index files for a gem server directory', - :directory => '.', :build_modern => true +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 - add_option '-d', '--directory=DIRNAME', - 'repository base dir containing gems subdir' do |dir, options| - options[:directory] = File.expand_path dir - end + def execute + alert_error "Install the rubygems-generate_index gem for the generate_index command" + end - add_option '--[no-]modern', - 'Generate indexes for RubyGems', - '(always true)' do |value, options| - options[:build_modern] = value + 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 - 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 + # 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 - 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 not File.exist?(options[:directory]) or - not 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 4e8d7600fb..1619b152e7 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" class Gem::Commands::HelpCommand < Gem::Command # :stopdoc: - EXAMPLES = <<-EOF.freeze + EXAMPLES = <<-EOF Some examples of 'gem' usage. * Install 'rake', either from local directory or remote server: @@ -52,13 +53,13 @@ Some examples of 'gem' usage. gem update --system EOF - GEM_DEPENDENCIES = <<-EOF.freeze + GEM_DEPENDENCIES = <<-EOF A gem dependencies file allows installation of a consistent set of gems across 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: @@ -171,7 +172,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 ================================== @@ -229,7 +230,7 @@ default. This may be overridden with the :development_group option: EOF - PLATFORMS = <<-'EOF'.freeze + PLATFORMS = <<-'EOF' RubyGems platforms are composed of three parts, a CPU, an OS, and a version. These values are taken from values in rbconfig.rb. You can view your current platform by running `gem environment`. @@ -268,7 +269,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], @@ -280,7 +281,7 @@ platform. # :startdoc: def initialize - super 'help', "Provide help on the 'gem' command" + super "help", "Provide help on the 'gem' command" @command_manager = Gem::CommandManager.instance end @@ -323,16 +324,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" + wrap_indent = " " * (margin_width + desc_width) + 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 +343,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 +367,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 9ca6ae364f..f65c639662 100644 --- a/lib/rubygems/commands/info_command.rb +++ b/lib/rubygems/commands/info_command.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/query_utils' +require_relative "../command" +require_relative "../query_utils" class Gem::Commands::InfoCommand < Gem::Command include Gem::QueryUtils 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 - remove_option('-d') + remove_option("-d") defaults[:details] = true defaults[:exact] = true diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index 4d36c69d51..2091634a29 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/install_update_options' -require 'rubygems/dependency_installer' -require 'rubygems/local_remote_options' -require 'rubygems/validator' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../install_update_options" +require_relative "../dependency_installer" +require_relative "../local_remote_options" +require_relative "../validator" +require_relative "../version_option" +require_relative "../update_suggestion" ## # Gem installer command line tool @@ -17,19 +19,20 @@ class Gem::Commands::InstallCommand < Gem::Command include Gem::VersionOption include Gem::LocalRemoteOptions include Gem::InstallUpdateOptions + include Gem::UpdateSuggestion 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) - super 'install', 'Install a gem into the local repository', defaults + super "install", "Install a gem into the local repository", defaults add_install_update_options add_local_remote_options @@ -45,9 +48,9 @@ class Gem::Commands::InstallCommand < Gem::Command end def defaults_str # :nodoc: - "--both --version '#{Gem::Requirement.default}' --no-force\n" + - "--install-dir #{Gem.dir} --lock\n" + - install_update_defaults_str + "--both --version '#{Gem::Requirement.default}' --no-force\n" \ + "--install-dir #{Gem.dir} --lock\n" + + install_update_defaults_str end def description # :nodoc: @@ -133,16 +136,9 @@ 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] and 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 and - get_all_gem_names.size > 1 + 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'`" terminate_interaction 1 @@ -157,9 +153,8 @@ You can use `i` command instead of `install`. @installed_specs = [] - ENV.delete 'GEM_PATH' if options[:install_dir].nil? + ENV.delete "GEM_PATH" if options[:install_dir].nil? - check_install_dir check_version load_hooks @@ -168,11 +163,13 @@ You can use `i` command instead of `install`. show_installed + say update_suggestion if eligible_for_update? + terminate_interaction exit_code end def install_from_gemdeps # :nodoc: - require 'rubygems/request_set' + require_relative "../request_set" rs = Gem::RequestSet.new specs = rs.install_from_gemdeps options do |req, inst| @@ -191,8 +188,8 @@ You can use `i` command instead of `install`. end def install_gem(name, version) # :nodoc: - return if options[:conservative] and - not Gem::Dependency.new(name, version).matching_specs.empty? + return if options[:conservative] && + !Gem::Dependency.new(name, version).matching_specs.empty? req = Gem::Requirement.create(version) @@ -247,20 +244,21 @@ You can use `i` command instead of `install`. def load_hooks # :nodoc: if options[:install_as_default] - require 'rubygems/install_default_message' + require_relative "../install_default_message" else - require 'rubygems/install_message' + require_relative "../install_message" end - require 'rubygems/rdoc' + require_relative "../rdoc" end def show_install_errors(errors) # :nodoc: return unless errors errors.each do |x| - return unless Gem::SourceFetchProblem === x + next unless Gem::SourceFetchProblem === x - msg = "Unable to pull data from '#{x.source.uri}': #{x.error.message}" + require_relative "../uri" + msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}" alert_warning msg end @@ -269,7 +267,7 @@ You can use `i` command instead of `install`. def show_installed # :nodoc: return if @installed_specs.empty? - gems = @installed_specs.length == 1 ? 'gem' : 'gems' + gems = @installed_specs.length == 1 ? "gem" : "gems" say "#{@installed_specs.length} #{gems} installed" end end diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index 5c99d3d73d..fab4b73814 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/query_utils' + +require_relative "../command" +require_relative "../query_utils" ## # Searches for gems starting with the supplied argument. @@ -9,9 +10,9 @@ class Gem::Commands::ListCommand < Gem::Command include Gem::QueryUtils def initialize - super 'list', 'Display local gems whose name matches REGEXP', - :name => //, :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + super "list", "Display local gems whose name matches REGEXP", + 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 f1dc1ac586..f7fd5ada16 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" class Gem::Commands::LockCommand < Gem::Command def initialize - super 'lock', 'Generate a lockdown list of gems', - :strict => false + super "lock", "Generate a lockdown list of gems", + strict: false - add_option '-s', '--[no-]strict', - 'fail if unable to satisfy a dependency' do |strict, options| + add_option "-s", "--[no-]strict", + "fail if unable to satisfy a dependency" do |strict, options| options[:strict] = strict end end diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index 86671a93b2..b91a8db12d 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" unless defined? Gem::Commands::MirrorCommand class Gem::Commands::MirrorCommand < Gem::Command def initialize - super('mirror', 'Mirror all gem files (requires rubygems-mirror)') + super("mirror", "Mirror all gem files (requires rubygems-mirror)") begin - Gem::Specification.find_by_name('rubygems-mirror').activate + Gem::Specification.find_by_name("rubygems-mirror").activate rescue Gem::LoadError # no-op end diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index 8012a9a0e1..0fe90dc8b8 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -1,18 +1,19 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../version_option" class Gem::Commands::OpenCommand < Gem::Command include Gem::VersionOption def initialize - super 'open', 'Open gem sources in editor' + super "open", "Open gem sources in editor" - add_option('-e', '--editor COMMAND', String, + add_option("-e", "--editor COMMAND", String, "Prepends COMMAND to gem path. Could be used to specify editor.") do |command, options| options[:editor] = command || get_env_editor end - add_option('-v', '--version VERSION', String, + add_option("-v", "--version VERSION", String, "Opens specific gem version") do |version| options[:version] = version end @@ -40,10 +41,10 @@ class Gem::Commands::OpenCommand < Gem::Command end def get_env_editor - ENV['GEM_EDITOR'] || - ENV['VISUAL'] || - ENV['EDITOR'] || - 'vi' + ENV["GEM_EDITOR"] || + ENV["VISUAL"] || + ENV["EDITOR"] || + "vi" end def execute @@ -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 3579bfc3ba..08a9221a26 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/spec_fetcher' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../spec_fetcher" +require_relative "../version_option" class Gem::Commands::OutdatedCommand < Gem::Command include Gem::LocalRemoteOptions include Gem::VersionOption def initialize - super 'outdated', 'Display all gems that need updates' + super "outdated", "Display all gems that need updates" add_local_remote_options add_platform_option diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index dd49027469..12bfe3a834 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/gemcutter_utilities' -require 'rubygems/text' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../gemcutter_utilities" +require_relative "../text" class Gem::Commands::OwnerCommand < Gem::Command include Gem::Text @@ -12,7 +13,12 @@ class Gem::Commands::OwnerCommand < Gem::Command def description # :nodoc: <<-EOF The owner command lets you add and remove owners of a gem on a push -server (the default is https://rubygems.org). +server (the default is https://rubygems.org). Multiple owners can be +added or removed at the same time, if the flag is given multiple times. + +The supported user identifiers are dependent on the push server. +For rubygems.org, both e-mail and handle are supported, even though the +user identifier field is called "email". The owner of a gem has the permission to push new versions, yank existing versions or edit the HTML page of the gem. Be careful of who you give push @@ -29,23 +35,23 @@ permission to. end def initialize - super 'owner', 'Manage gem owners of a gem on the push server' + super "owner", "Manage gem owners of a gem on the push server" add_proxy_option add_key_option add_otp_option - defaults.merge! :add => [], :remove => [] + defaults.merge! add: [], remove: [] - add_option '-a', '--add EMAIL', 'Add an owner' do |value, options| + add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options| options[:add] << value end - add_option '-r', '--remove EMAIL', 'Remove an owner' do |value, options| + add_option "-r", "--remove OLD_OWNER", "Remove an owner by user identifier" do |value, options| options[:remove] << value end - add_option '-h', '--host HOST', - 'Use another gemcutter-compatible host', - ' (e.g. https://rubygems.org)' do |value, options| + add_option "-h", "--host HOST", + "Use another gemcutter-compatible host", + " (e.g. https://rubygems.org)" do |value, options| options[:host] = value end end @@ -73,7 +79,7 @@ permission to. say "Owners for gem: #{name}" owners.each do |owner| - say "- #{owner['email'] || owner['handle'] || owner['id']}" + say "- #{owner["email"] || owner["handle"] || owner["id"]}" end end end @@ -88,14 +94,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 @@ -103,7 +109,7 @@ permission to. def send_owner_request(method, name, owner) rubygems_api_request method, "api/v1/gems/#{name}/owners", scope: get_owner_scope(method: method) do |request| - request.set_form_data 'email' => owner + request.set_form_data "email" => owner request.add_field "Authorization", api_key end end diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 143105981e..456d897df2 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -1,62 +1,73 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/package' -require 'rubygems/installer' -require 'rubygems/version_option' + +require_relative "../command" +require_relative "../package" +require_relative "../installer" +require_relative "../version_option" class Gem::Commands::PristineCommand < Gem::Command include Gem::VersionOption 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 - - add_option('--all', - 'Restore all installed gems to pristine', - 'condition') do |value, options| + 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 + + add_option("--all", + "Restore all installed gems to pristine", + "condition") do |value, options| options[:all] = value end - add_option('--skip=gem_name', - 'used on --all, skip if name == gem_name') do |value, options| + add_option("--skip=gem_name", + "used on --all, skip if name == gem_name") do |value, options| options[:skip] ||= [] options[:skip] << value end - add_option('--[no-]extensions', - 'Restore gems with extensions', - 'in addition to regular gems') do |value, options| + add_option("--[no-]extensions", + "Restore gems with extensions", + "in addition to regular gems") do |value, options| options[:extensions_set] = true options[:extensions] = value end - add_option('--only-executables', - 'Only restore executables') do |value, options| + 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 end - add_option('--only-plugins', - 'Only restore plugins') do |value, options| + add_option("--only-plugins", + "Only restore plugins") do |value, options| options[:only_plugins] = value end - add_option('-E', '--[no-]env-shebang', - 'Rewrite executables with a shebang', - 'of /usr/bin/env') do |value, options| + add_option("-E", "--[no-]env-shebang", + "Rewrite executables with a shebang", + "of /usr/bin/env") do |value, options| options[:env_shebang] = value end - add_option('-n', '--bindir DIR', - 'Directory where executables are', - 'located') do |value, options| + add_option("-i", "--install-dir DIR", + "Gem repository to get binstubs and plugins installed") do |value, options| + options[:install_dir] = File.expand_path(value) + end + + add_option("-n", "--bindir DIR", + "Directory where executables are", + "located") do |value, options| options[:bin_dir] = File.expand_path(value) end - add_version_option('restore to', 'pristine condition') + add_version_option("restore to", "pristine condition") end def arguments # :nodoc: @@ -64,7 +75,7 @@ class Gem::Commands::PristineCommand < Gem::Command end def defaults_str # :nodoc: - '--extensions' + "--extensions" end def description # :nodoc: @@ -93,22 +104,24 @@ extensions will be restored. def execute specs = if options[:all] - Gem::Specification.map - - # `--extensions` must be explicitly given to pristine only gems - # with extensions. - elsif options[:extensions_set] and - options[:extensions] and options[:args].empty? - Gem::Specification.select do |spec| - spec.extensions and not spec.extensions.empty? - end - else - get_all_gem_names.sort.map do |gem_name| - Gem::Specification.find_all_by_name(gem_name, options[:version]).reverse - end.flatten - end - - specs = specs.select{|spec| RUBY_ENGINE == spec.platform || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } + Gem::Specification.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| + spec.extensions && !spec.extensions.empty? + end + elsif options[:only_missing_extensions] + Gem::Specification.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 + end + + specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? raise Gem::Exception, @@ -123,22 +136,22 @@ extensions will be restored. next 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? or options[:extensions] or options[:only_executables] or options[:only_plugins] + unless spec.extensions.empty? || options[:extensions] || options[:only_executables] || options[:only_plugins] say "Skipped #{spec.full_name}, it needs to compile an extension" next end gem = spec.cache_file - unless File.exist? gem or options[:only_executables] or options[:only_plugins] - require 'rubygems/remote_fetcher' + unless File.exist?(gem) || options[:only_executables] || options[:only_plugins] + require_relative "../remote_fetcher" say "Cached gem for #{spec.full_name} not found, attempting to fetch..." @@ -158,26 +171,27 @@ extensions will be restored. if options.include? :env_shebang options[:env_shebang] else - install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS['install'] - install_defaults.to_s['--env-shebang'] + install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS["install"] + install_defaults.to_s["--env-shebang"] 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 => 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] installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_bin elsif options[:only_plugins] - installer = Gem::Installer.for_spec(spec) + installer = Gem::Installer.for_spec(spec, installer_options) installer.generate_plugins else installer = Gem::Installer.at(gem, installer_options) diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 1a9a1932f8..591ddc3a80 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/gemcutter_utilities' -require 'rubygems/package' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../gemcutter_utilities" +require_relative "../package" class Gem::Commands::PushCommand < Gem::Command include Gem::LocalRemoteOptions @@ -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 @user_defined_host = false @@ -37,9 +38,9 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo add_key_option add_otp_option - add_option('--host HOST', - 'Push to another gemcutter-compatible host', - ' (e.g. https://rubygems.org)') do |value, options| + add_option("--host HOST", + "Push to another gemcutter-compatible host", + " (e.g. https://rubygems.org)") do |value, options| options[:host] = value @user_defined_host = true end @@ -52,14 +53,14 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo default_gem_server, push_host = get_hosts_for(gem_name) @host = if @user_defined_host - options[:host] - elsif default_gem_server - default_gem_server - elsif push_host - push_host - else - options[:host] - end + options[:host] + elsif default_gem_server + default_gem_server + elsif push_host + push_host + else + options[:host] + end sign_in @host, scope: get_push_scope @@ -74,7 +75,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}..." diff --git a/lib/rubygems/commands/query_command.rb b/lib/rubygems/commands/query_command.rb index 789afd6509..3b527974a3 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/query_utils' -require 'rubygems/deprecate' + +require_relative "../command" +require_relative "../query_utils" +require_relative "../deprecate" class Gem::Commands::QueryCommand < Gem::Command extend Gem::Deprecate @@ -9,7 +10,7 @@ class Gem::Commands::QueryCommand < Gem::Command include Gem::QueryUtils - alias warning_without_suggested_alternatives deprecation_warning + alias_method :warning_without_suggested_alternatives, :deprecation_warning def deprecation_warning warning_without_suggested_alternatives @@ -17,15 +18,14 @@ class Gem::Commands::QueryCommand < Gem::Command alert_warning message unless Gem::Deprecate.skip end - def initialize(name = 'query', - summary = 'Query gem information in local or remote repositories') + def initialize(name = "query", summary = "Query gem information in local or remote repositories") super name, summary, - :name => //, :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_option('-n', '--name-matches REGEXP', - 'Name of gem(s) to query on matches the', - 'provided REGEXP') do |value, options| + 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 diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index e8c9e84b29..977c90b8c4 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -1,35 +1,36 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' -require 'rubygems/rdoc' -require 'fileutils' + +require_relative "../command" +require_relative "../version_option" +require_relative "../rdoc" +require "fileutils" class Gem::Commands::RdocCommand < Gem::Command include Gem::VersionOption def initialize - super 'rdoc', 'Generates RDoc for pre-installed gems', - :version => Gem::Requirement.default, - :include_rdoc => false, :include_ri => true, :overwrite => false + super "rdoc", "Generates RDoc for pre-installed gems", + version: Gem::Requirement.default, + include_rdoc: false, include_ri: true, overwrite: false - add_option('--all', - 'Generate RDoc/RI documentation for all', - 'installed gems') do |value, options| + add_option("--all", + "Generate RDoc/RI documentation for all", + "installed gems") do |value, options| options[:all] = value end - add_option('--[no-]rdoc', - 'Generate RDoc HTML') do |value, options| + add_option("--[no-]rdoc", + "Generate RDoc HTML") do |value, options| options[:include_rdoc] = value end - add_option('--[no-]ri', - 'Generate RI data') do |value, options| + add_option("--[no-]ri", + "Generate RI data") do |value, options| options[:include_ri] = value end - add_option('--[no-]overwrite', - 'Overwrite installed documents') do |value, options| + add_option("--[no-]overwrite", + "Overwrite installed documents") do |value, options| options[:overwrite] = value end @@ -61,15 +62,15 @@ Use --overwrite to force rebuilding of documentation. def execute specs = if options[:all] - Gem::Specification.to_a - else - get_all_gem_names.map do |name| - Gem::Specification.find_by_name name, options[:version] - end.flatten.uniq - end + Gem::Specification.to_a + else + get_all_gem_names.map do |name| + Gem::Specification.find_by_name name, options[:version] + end.flatten.uniq + end if specs.empty? - alert_error 'No matching gems found' + alert_error "No matching gems found" terminate_interaction 1 end @@ -79,17 +80,11 @@ Use --overwrite to force rebuilding of documentation. doc.force = options[:overwrite] if options[:overwrite] - FileUtils.rm_rf File.join(spec.doc_dir, 'ri') - FileUtils.rm_rf File.join(spec.doc_dir, 'rdoc') + FileUtils.rm_rf File.join(spec.doc_dir, "ri") + FileUtils.rm_rf File.join(spec.doc_dir, "rdoc") end - begin - doc.generate - rescue Errno::ENOENT => e - e.message =~ / - / - alert_error "Unable to document #{spec.full_name}, #{$'} 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..77a474ef1d --- /dev/null +++ b/lib/rubygems/commands/rebuild_command.rb @@ -0,0 +1,262 @@ +# frozen_string_literal: true + +require "date" +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 aeb2119235..50e161ac9b 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/query_utils' + +require_relative "../command" +require_relative "../query_utils" class Gem::Commands::SearchCommand < Gem::Command include Gem::QueryUtils def initialize - super 'search', 'Display remote gems whose name matches REGEXP', - :name => //, :domain => :remote, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + super "search", "Display remote gems whose name matches REGEXP", + 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 594cf77f66..f1dde4aa02 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -1,88 +1,26 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/server' -require 'rubygems/deprecate' -class Gem::Commands::ServerCommand < Gem::Command - extend Gem::Deprecate - rubygems_deprecate_command +require_relative "../command" - def initialize - super 'server', 'Documentation and gem repository HTTP server', - :port => 8808, :gemdir => [], :daemon => false - - OptionParser.accept :Port do |port| - if port =~ /\A\d+\z/ - port = Integer port - raise OptionParser::InvalidArgument, "#{port}: not a port number" if - port > 65535 - - port - else - begin - Socket.getservbyname port - rescue SocketError - raise OptionParser::InvalidArgument, "#{port}: no such named service" - end +unless defined? Gem::Commands::ServerCommand + class Gem::Commands::ServerCommand < Gem::Command + def initialize + super("server", "Starts up a web server that hosts the RDoc (requires rubygems-server)") + begin + Gem::Specification.find_by_name("rubygems-server").activate + rescue Gem::LoadError + # no-op end end - add_option '-p', '--port=PORT', :Port, - 'port to listen on' do |port, options| - options[:port] = port - end - - add_option '-d', '--dir=GEMDIR', - 'directories from which to serve gems', - 'multiple directories may be provided' do |gemdir, options| - options[:gemdir] << File.expand_path(gemdir) - end - - add_option '--[no-]daemon', 'run as a daemon' do |daemon, options| - options[:daemon] = daemon - end - - add_option '-b', '--bind=HOST,HOST', - 'addresses to bind', Array do |address, options| - options[:addresses] ||= [] - options[:addresses].push(*address) + def description # :nodoc: + <<-EOF +The server command has been moved to the rubygems-server gem. + EOF end - add_option '-l', '--launch[=COMMAND]', - 'launches a browser window', - "COMMAND defaults to 'start' on Windows", - "and 'open' on all other platforms" do |launch, options| - launch ||= Gem.win_platform? ? 'start' : 'open' - options[:launch] = launch + def execute + alert_error "Install the rubygems-server gem for the server command" end end - - def defaults_str # :nodoc: - "--port 8808 --dir #{Gem.dir} --no-daemon" - end - - def description # :nodoc: - <<-EOF -The server command starts up a web server that hosts the RDoc for your -installed gems and can operate as a server for installation of gems on other -machines. - -The cache files for installed gems must exist to use the server as a source -for gem installation. - -To install gems from a running server, use `gem install GEMNAME --source -http://gem_server_host:8808` - -You can set up a shortcut to gem server documentation using the URL: - - http://localhost:8808/rdoc?q=%s - Firefox - http://localhost:8808/rdoc?q=* - LaunchBar - - EOF - end - - def execute - options[:gemdir] = Gem.path if options[:gemdir].empty? - Gem::Server.run options - end end diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index 47e215c149..3f38074280 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -1,107 +1,106 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" ## # Installs RubyGems itself. This command is ordinarily only available from a # 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 - require 'tmpdir' - - 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 - - add_option '--previous-version=VERSION', - 'Previous version of RubyGems', - 'Used for changelog processing' do |version, options| + 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 + + add_option "--previous-version=VERSION", + "Previous version of RubyGems", + "Used for changelog processing" do |version, options| options[:previous_version] = version end - add_option '--prefix=PREFIX', - 'Prefix path for installing RubyGems', - 'Will not affect gem repository location' do |prefix, options| + add_option "--prefix=PREFIX", + "Prefix path for installing RubyGems", + "Will not affect gem repository location" do |prefix, options| options[:prefix] = File.expand_path prefix end - add_option '--destdir=DESTDIR', - 'Root directory to install RubyGems into', - 'Mainly used for packaging RubyGems' do |destdir, options| + add_option "--destdir=DESTDIR", + "Root directory to install RubyGems into", + "Mainly used for packaging RubyGems" do |destdir, options| options[:destdir] = File.expand_path destdir end - add_option '--[no-]vendor', - 'Install into vendorlibdir not sitelibdir' do |vendor, options| - options[:site_or_vendor] = vendor ? 'vendorlibdir' : 'sitelibdir' + add_option "--[no-]vendor", + "Install into vendorlibdir not sitelibdir" do |vendor, options| + options[:site_or_vendor] = vendor ? "vendorlibdir" : "sitelibdir" end - add_option '--[no-]format-executable', - 'Makes `gem` match ruby', - 'If Ruby is ruby18, gem will be gem18' do |value, options| + add_option "--[no-]format-executable", + "Makes `gem` match ruby", + "If Ruby is ruby18, gem will be gem18" do |value, options| options[:format_executable] = value end - add_option '--[no-]document [TYPES]', Array, - 'Generate documentation for RubyGems', - 'List the documentation types you wish to', - 'generate. For example: rdoc,ri' do |value, options| + add_option "--[no-]document [TYPES]", Array, + "Generate documentation for RubyGems", + "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 - end + else value + end end - add_option '--[no-]rdoc', - 'Generate RDoc documentation for RubyGems' do |value, options| + add_option "--[no-]rdoc", + "Generate RDoc documentation for RubyGems" do |value, options| if value - options[:document] << 'rdoc' + options[:document] << "rdoc" else - options[:document].delete 'rdoc' + options[:document].delete "rdoc" end options[:document].uniq! end - add_option '--[no-]ri', - 'Generate RI documentation for RubyGems' do |value, options| + add_option "--[no-]ri", + "Generate RI documentation for RubyGems" do |value, options| if value - options[:document] << 'ri' + options[:document] << "ri" else - options[:document].delete 'ri' + options[:document].delete "ri" end options[:document].uniq! end - add_option '--[no-]regenerate-binstubs', - 'Regenerate gem binstubs' do |value, options| + add_option "--[no-]regenerate-binstubs", + "Regenerate gem binstubs" do |value, options| options[:regenerate_binstubs] = value end - add_option '--[no-]regenerate-plugins', - 'Regenerate gem plugins' do |value, options| + add_option "--[no-]regenerate-plugins", + "Regenerate gem plugins" do |value, options| options[:regenerate_plugins] = value end - add_option '-f', '--[no-]force', - 'Forcefully overwrite binstubs' do |value, options| + add_option "-f", "--[no-]force", + "Forcefully overwrite binstubs" do |value, options| options[:force] = value end - add_option('-E', '--[no-]env-shebang', - 'Rewrite executables with a shebang', - 'of /usr/bin/env') do |value, options| + add_option("-E", "--[no-]env-shebang", + "Rewrite executables with a shebang", + "of /usr/bin/env") do |value, options| options[:env_shebang] = value end @@ -109,7 +108,7 @@ class Gem::Commands::SetupCommand < Gem::Command end def check_ruby_version - required_version = Gem::Requirement.new '>= 2.3.0' + 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}" @@ -135,7 +134,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 @@ -149,16 +148,9 @@ By default, this RubyGems will install gem as: def execute @verbose = Gem.configuration.really_verbose - install_destdir = options[:destdir] - - unless install_destdir.empty? - ENV['GEM_HOME'] ||= File.join(install_destdir, - Gem.default_dir.gsub(/^[a-zA-Z]:/, '')) - end - check_ruby_version - require 'fileutils' + require "fileutils" if Gem.configuration.really_verbose extend FileUtils::Verbose else @@ -166,8 +158,8 @@ By default, this RubyGems will install gem as: end extend MakeDirs - lib_dir, bin_dir = make_destination_dirs install_destdir - man_dir = generate_default_man_dir install_destdir + lib_dir, bin_dir = make_destination_dirs + man_dir = generate_default_man_dir install_lib lib_dir @@ -189,8 +181,8 @@ By default, this RubyGems will install gem as: say "RubyGems #{Gem::VERSION} installed" - regenerate_binstubs if options[:regenerate_binstubs] - regenerate_plugins if options[:regenerate_plugins] + regenerate_binstubs(bin_dir) if options[:regenerate_binstubs] + regenerate_plugins(bin_dir) if options[:regenerate_plugins] uninstall_old_gemcutter @@ -203,7 +195,7 @@ By default, this RubyGems will install gem as: end if options[:previous_version].empty? - options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, '0') + options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0") end options[:previous_version] = Gem::Version.new(options[:previous_version]) @@ -225,7 +217,7 @@ By default, this RubyGems will install gem as: end if documentation_success - if options[:document].include? 'rdoc' + if options[:document].include? "rdoc" say "Rdoc documentation was installed. You may now invoke:" say " gem server" say "and then peruse beautifully formatted documentation for your gems" @@ -236,7 +228,7 @@ By default, this RubyGems will install gem as: say end - if options[:document].include? 'ri' + if options[:document].include? "ri" say "Ruby Interactive (ri) documentation was installed. ri is kind of like man " say "pages for Ruby libraries. You may access it like this:" say " ri Classname" @@ -251,42 +243,41 @@ 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 Dir.chdir path do - bin_files = Dir['*'] + bin_file = "gem" - bin_files -= %w[update_rubygems] + require "tmpdir" - bin_files.each do |bin_file| - dest_file = target_bin_path(bin_dir, bin_file) - bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}" + dest_file = target_bin_path(bin_dir, bin_file) + bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}" - begin - bin = File.readlines bin_file - bin[0] = shebang + begin + bin = File.readlines bin_file + bin[0] = shebang - File.open bin_tmp_file, 'w' do |fp| - fp.puts bin.join - end - - install bin_tmp_file, dest_file, :mode => prog_mode - bin_file_names << dest_file - ensure - rm bin_tmp_file + File.open bin_tmp_file, "w" do |fp| + fp.puts bin.join end - next unless Gem.win_platform? + install bin_tmp_file, dest_file, mode: prog_mode + bin_file_names << dest_file + ensure + rm bin_tmp_file + end + + next unless Gem.win_platform? - begin - bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat" + begin + bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat" - File.open bin_cmd_file, 'w' do |file| - file.puts <<-TEXT + 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 @@ -294,12 +285,11 @@ By default, this RubyGems will install gem as: :WinNT @"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %* TEXT - end - - install bin_cmd_file, "#{dest_file}.bat", :mode => prog_mode - ensure - rm bin_cmd_file end + + install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode + ensure + rm bin_cmd_file end end end @@ -307,7 +297,7 @@ By default, this RubyGems will install gem as: def shebang if options[:env_shebang] - ruby_name = RbConfig::CONFIG['ruby_install_name'] + ruby_name = RbConfig::CONFIG["ruby_install_name"] @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path } "#!#{@env_path} #{ruby_name}\n" else @@ -316,8 +306,8 @@ By default, this RubyGems will install gem as: end def install_lib(lib_dir) - libs = { 'RubyGems' => 'lib' } - libs['Bundler'] = 'bundler/lib' + libs = { "RubyGems" => "lib" } + libs["Bundler"] = "bundler/lib" libs.each do |tool, path| say "Installing #{tool}" if @verbose @@ -330,7 +320,7 @@ By default, this RubyGems will install gem as: end def install_rdoc - gem_doc_dir = File.join Gem.dir, 'doc' + gem_doc_dir = File.join Gem.dir, "doc" rubygems_name = "rubygems-#{Gem::VERSION}" rubygems_doc_dir = File.join gem_doc_dir, rubygems_name @@ -340,23 +330,23 @@ By default, this RubyGems will install gem as: # ignore end - if File.writable? gem_doc_dir and - (not File.exist? rubygems_doc_dir or - File.writable? rubygems_doc_dir) + if File.writable?(gem_doc_dir) && + (!File.exist?(rubygems_doc_dir) || + File.writable?(rubygems_doc_dir)) say "Removing old RubyGems RDoc and ri" if @verbose - Dir[File.join(Gem.dir, 'doc', 'rubygems-[0-9]*')].each do |dir| + Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir| rm_rf dir end - require 'rubygems/rdoc' + require_relative "../rdoc" - fake_spec = Gem::Specification.new 'rubygems', Gem::VERSION + fake_spec = Gem::Specification.new "rubygems", Gem::VERSION def fake_spec.full_gem_path - File.expand_path '../../../..', __FILE__ + File.expand_path "../../..", __dir__ end - generate_ri = options[:document].include? 'ri' - generate_rdoc = options[:document].include? 'rdoc' + generate_ri = options[:document].include? "ri" + generate_rdoc = options[:document].include? "rdoc" rdoc = Gem::RDoc.new fake_spec, generate_rdoc, generate_ri rdoc.generate @@ -367,28 +357,33 @@ 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) - specs_dir = Gem.default_specifications_dir - specs_dir = File.join(options[:destdir], specs_dir) unless Gem.win_platform? - mkdir_p specs_dir, :mode => 0755 - - bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } + current_default_spec = Gem::Specification.default_stubs.find {|s| s.name == "bundler" } + specs_dir = if current_default_spec && default_dir == Gem.default_dir + Gem::Specification.remove_spec current_default_spec + loaded_from = current_default_spec.loaded_from + File.delete(loaded_from) + File.dirname(loaded_from) + else + target_specs_dir = File.join(default_dir, "specifications", "default") + mkdir_p target_specs_dir, mode: 0o755 + target_specs_dir + end - # Remove bundler-*.gemspec in default specification directory. - Dir.entries(specs_dir). - select {|gs| gs.start_with?("bundler-") }. - each {|gs| File.delete(File.join(specs_dir, gs)) } + 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, "#{bundler_spec.full_name}.gemspec") - Gem.write_binary(default_spec_path, bundler_spec.to_ruby) + 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(Gem.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 @@ -396,111 +391,91 @@ 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 - bundler_bin_dir = File.join(options[:destdir], bundler_bin_dir) unless Gem.win_platform? - 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 'rubygems/installer' + require_relative "../installer" Dir.chdir("bundler") do - built_gem = Gem::Package.build(bundler_spec) + built_gem = Gem::Package.build(new_bundler_spec) begin - 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, wrappers: true) - installer.install + 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 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(install_destdir) + def make_destination_dirs lib_dir, bin_dir = Gem.default_rubygems_dirs unless lib_dir - lib_dir, bin_dir = generate_default_dirs(install_destdir) + 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(install_destdir) + def generate_default_man_dir prefix = options[:prefix] if prefix.empty? - man_dir = RbConfig::CONFIG['mandir'] + man_dir = RbConfig::CONFIG["mandir"] return unless man_dir else - man_dir = File.join prefix, 'man' - end - - unless install_destdir.empty? - man_dir = File.join install_destdir, man_dir.gsub(/^[a-zA-Z]:/, '') + man_dir = File.join prefix, "man" end - man_dir + prepend_destdir_if_present(man_dir) end - def generate_default_dirs(install_destdir) + def generate_default_dirs prefix = options[:prefix] site_or_vendor = options[:site_or_vendor] if prefix.empty? lib_dir = RbConfig::CONFIG[site_or_vendor] - bin_dir = RbConfig::CONFIG['bindir'] + bin_dir = RbConfig::CONFIG["bindir"] else - # Apple installed RubyGems into libdir, and RubyGems <= 1.1.0 gets - # confused about installation location, so switch back to - # sitelibdir/vendorlibdir. - if defined?(APPLE_GEM_HOME) and - # just in case Apple and RubyGems don't get this patched up proper. - (prefix == RbConfig::CONFIG['libdir'] or - # this one is important - prefix == File.join(RbConfig::CONFIG['libdir'], 'ruby')) - lib_dir = RbConfig::CONFIG[site_or_vendor] - bin_dir = RbConfig::CONFIG['bindir'] - else - lib_dir = File.join prefix, 'lib' - bin_dir = File.join prefix, 'bin' - end + lib_dir = File.join prefix, "lib" + bin_dir = File.join prefix, "bin" end - unless install_destdir.empty? - lib_dir = File.join install_destdir, lib_dir.gsub(/^[a-zA-Z]:/, '') - bin_dir = File.join install_destdir, bin_dir.gsub(/^[a-zA-Z]:/, '') - end - - [lib_dir, bin_dir] + [prepend_destdir_if_present(lib_dir), prepend_destdir_if_present(bin_dir)] end def files_in(dir) Dir.chdir dir do - Dir.glob(File.join('**', '*'), File::FNM_DOTMATCH). - select{|f| !File.directory?(f) } + Dir.glob(File.join("**", "*"), File::FNM_DOTMATCH). + select {|f| !File.directory?(f) } end end def remove_old_bin_files(bin_dir) old_bin_files = { - 'gem_mirror' => 'gem mirror', - 'gem_server' => 'gem server', - 'gemlock' => 'gem lock', - 'gemri' => 'ri', - 'gemwhich' => 'gem which', - 'index_gem_repository.rb' => 'gem generate_index', + "gem_mirror" => "gem mirror", + "gem_server" => "gem server", + "gemlock" => "gem lock", + "gemri" => "ri", + "gemwhich" => "gem which", + "index_gem_repository.rb" => "gem generate_index", } old_bin_files.each do |old_bin_file, new_name| @@ -509,7 +484,7 @@ By default, this RubyGems will install gem as: deprecation_message = "`#{old_bin_file}` has been deprecated. Use `#{new_name}` instead." - File.open old_bin_path, 'w' do |fp| + File.open old_bin_path, "w" do |fp| fp.write <<-EOF #!#{Gem.ruby} @@ -519,15 +494,15 @@ abort "#{deprecation_message}" next unless Gem.win_platform? - File.open "#{old_bin_path}.bat", 'w' do |fp| + File.open "#{old_bin_path}.bat", "w" do |fp| fp.puts %(@ECHO.#{deprecation_message}) end end end def remove_old_lib_files(lib_dir) - lib_dirs = { File.join(lib_dir, 'rubygems') => 'lib/rubygems' } - lib_dirs[File.join(lib_dir, 'bundler')] = 'bundler/lib/bundler' + lib_dirs = { File.join(lib_dir, "rubygems") => "lib/rubygems" } + lib_dirs[File.join(lib_dir, "bundler")] = "bundler/lib/bundler" lib_dirs.each do |old_lib_dir, new_lib_dir| lib_files = files_in(new_lib_dir) @@ -535,11 +510,11 @@ abort "#{deprecation_message}" to_remove = old_lib_files - lib_files - gauntlet_rubygems = File.join(lib_dir, 'gauntlet_rubygems.rb') + gauntlet_rubygems = File.join(lib_dir, "gauntlet_rubygems.rb") to_remove << gauntlet_rubygems if File.exist? gauntlet_rubygems to_remove.delete_if do |file| - file.start_with? 'defaults' + file.start_with? "defaults" end remove_file_list(to_remove, old_lib_dir) @@ -565,7 +540,7 @@ abort "#{deprecation_message}" end def show_release_notes - release_notes = File.join Dir.pwd, 'CHANGELOG.md' + release_notes = File.join Dir.pwd, "CHANGELOG.md" release_notes = if File.exist? release_notes @@ -582,7 +557,7 @@ abort "#{deprecation_message}" history_string = "" - until versions.length == 0 or + until versions.length == 0 || versions.shift <= options[:previous_version] do history_string += version_lines.shift + text.shift end @@ -596,19 +571,20 @@ abort "#{deprecation_message}" end def uninstall_old_gemcutter - require 'rubygems/uninstaller' + 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 - def regenerate_binstubs - require "rubygems/commands/pristine_command" + def regenerate_binstubs(bindir) + require_relative "pristine_command" say "Regenerating binstubs" args = %w[--all --only-executables --silent] + args << "--bindir=#{bindir}" if options[:env_shebang] args << "--env-shebang" end @@ -617,11 +593,13 @@ abort "#{deprecation_message}" command.invoke(*args) end - def regenerate_plugins - require "rubygems/commands/pristine_command" + def regenerate_plugins(bindir) + require_relative "pristine_command" say "Regenerating plugins" args = %w[--all --only-plugins --silent] + args << "--bindir=#{bindir}" + args << "--install-dir=#{default_dir}" command = Gem::Commands::PristineCommand.new command.invoke(*args) @@ -629,6 +607,25 @@ abort "#{deprecation_message}" private + def default_dir + prefix = options[:prefix] + + if prefix.empty? + dir = Gem.default_dir + else + dir = prefix + end + + prepend_destdir_if_present(dir) + end + + def prepend_destdir_if_present(path) + destdir = options[:destdir] + return path if destdir.empty? + + File.join(options[:destdir], path.gsub(/^[a-zA-Z]:/, "")) + end + def install_file_list(files, dest_dir) files.each do |file| install_file file, dest_dir @@ -639,10 +636,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) @@ -658,10 +655,10 @@ abort "#{deprecation_message}" def target_bin_path(bin_dir, bin_file) bin_file_formatted = if options[:format_executable] - Gem.default_exec_format % bin_file - else - bin_file - end + Gem.default_exec_format % bin_file + else + bin_file + end File.join bin_dir, bin_file_formatted end diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb index 2e19c8333c..0f77908c5b 100644 --- a/lib/rubygems/commands/signin_command.rb +++ b/lib/rubygems/commands/signin_command.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/gemcutter_utilities' + +require_relative "../command" +require_relative "../gemcutter_utilities" class Gem::Commands::SigninCommand < Gem::Command include Gem::GemcutterUtilities def initialize - super 'signin', 'Sign in to any gemcutter-compatible host. '\ - 'It defaults to https://rubygems.org' + super "signin", "Sign in to any gemcutter-compatible host. "\ + "It defaults to https://rubygems.org" - add_option('--host HOST', 'Push to another gemcutter-compatible host') do |value, options| + add_option("--host HOST", "Push to another gemcutter-compatible host") do |value, options| options[:host] = value end @@ -17,10 +18,10 @@ class Gem::Commands::SigninCommand < Gem::Command end def description # :nodoc: - 'The signin command executes host sign in for a push server (the default is'\ - ' https://rubygems.org). The host can be provided with the host flag or can'\ - ' be inferred from the provided gem. Host resolution matches the resolution'\ - ' strategy for the push command.' + "The signin command executes host sign in for a push server (the default is"\ + " https://rubygems.org). The host can be provided with the host flag or can"\ + " be inferred from the provided gem. Host resolution matches the resolution"\ + " strategy for the push command." end def usage # :nodoc: diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb index ebbe746cb4..bdd01e4393 100644 --- a/lib/rubygems/commands/signout_command.rb +++ b/lib/rubygems/commands/signout_command.rb @@ -1,14 +1,15 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" class Gem::Commands::SignoutCommand < Gem::Command def initialize - super 'signout', 'Sign out from all the current sessions.' + super "signout", "Sign out from all the current sessions." end def description # :nodoc: - 'The `signout` command is used to sign out from all current sessions,'\ - ' allowing you to sign in using a different set of credentials.' + "The `signout` command is used to sign out from all current sessions,"\ + " allowing you to sign in using a different set of credentials." end def usage # :nodoc: @@ -19,13 +20,13 @@ class Gem::Commands::SignoutCommand < Gem::Command credentials_path = Gem.configuration.credentials_path if !File.exist?(credentials_path) - alert_error 'You are not currently signed in.' + alert_error "You are not currently signed in." elsif !File.writable?(credentials_path) alert_error "File '#{Gem.configuration.credentials_path}' is read-only."\ - ' Please make sure it is writable.' + " Please make sure it is writable." else Gem.configuration.unset_api_key! - say 'You have successfully signed out from all sessions.' + say "You have successfully signed out from all sessions." end end end diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index f74fb12e42..976f4a4ea2 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -1,40 +1,41 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/remote_fetcher' -require 'rubygems/spec_fetcher' -require 'rubygems/local_remote_options' + +require_relative "../command" +require_relative "../remote_fetcher" +require_relative "../spec_fetcher" +require_relative "../local_remote_options" class Gem::Commands::SourcesCommand < Gem::Command include Gem::LocalRemoteOptions def initialize - require 'fileutils' + require "fileutils" - super 'sources', - 'Manage the sources and cache file RubyGems uses to search for gems' + super "sources", + "Manage the sources and cache file RubyGems uses to search for gems" - add_option '-a', '--add SOURCE_URI', 'Add source' do |value, options| + add_option "-a", "--add SOURCE_URI", "Add source" do |value, options| options[:add] = value end - add_option '-l', '--list', 'List sources' do |value, options| + add_option "-l", "--list", "List sources" do |value, options| options[:list] = value end - add_option '-r', '--remove SOURCE_URI', 'Remove source' do |value, options| + add_option "-r", "--remove SOURCE_URI", "Remove source" do |value, options| 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 - add_option '-u', '--update', 'Update source cache' do |value, options| + add_option "-u", "--update", "Update source cache" do |value, options| options[:update] = value end - add_option '-f', '--[no-]force', "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options| + add_option "-f", "--[no-]force", "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options| options[:force] = value end @@ -58,11 +59,11 @@ 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 #{source_uri}:\n\t#{e.message}" + say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}" terminate_interaction 1 end end @@ -70,7 +71,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 @@ -80,10 +81,10 @@ Do you want to add this source? end def check_rubygems_https(source_uri) # :nodoc: - uri = URI source_uri + uri = Gem::URI source_uri - if uri.scheme and uri.scheme.downcase == 'http' and - 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,21 +99,21 @@ 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 def defaults_str # :nodoc: - '--list' + "--list" end def description # :nodoc: @@ -138,8 +139,8 @@ do not recognize you should remove them. RubyGems has been configured to serve gems via the following URLs through its history: -* http://gems.rubyforge.org (RubyGems 1.3.6 and earlier) -* https://rubygems.org/ (RubyGems 1.3.7 through 1.8.25) +* http://gems.rubyforge.org (RubyGems 1.3.5 and earlier) +* http://rubygems.org (RubyGems 1.3.6 through 1.8.30, and 2.0.0) * https://rubygems.org (RubyGems 2.0.1 and newer) Since all of these sources point to the same set of gems you only need one @@ -193,13 +194,13 @@ 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 + if Gem.sources.include? source_uri Gem.sources.delete source_uri Gem.configuration.write say "#{source_uri} removed from sources" + else + say "source #{source_uri} not present in cache" end end @@ -215,9 +216,9 @@ To remove a source use the --remove argument: def remove_cache_file(desc, path) # :nodoc: FileUtils.rm_rf path - if not File.exist?(path) + if !File.exist?(path) say "*** Removed #{desc} source cache ***" - elsif not File.writable?(path) + elsif !File.writable?(path) say "*** Unable to remove #{desc} source cache (write protected) ***" else say "*** Unable to remove #{desc} source cache ***" diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index 3fddaaaf30..a21ed35be3 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/version_option' -require 'rubygems/package' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../version_option" +require_relative "../package" class Gem::Commands::SpecificationCommand < Gem::Command include Gem::LocalRemoteOptions @@ -11,28 +12,28 @@ class Gem::Commands::SpecificationCommand < Gem::Command def initialize Gem.load_yaml - super 'specification', 'Display gem specification (in yaml)', - :domain => :local, :version => Gem::Requirement.default, - :format => :yaml + super "specification", "Display gem specification (in yaml)", + domain: :local, version: Gem::Requirement.default, + format: :yaml - add_version_option('examine') + add_version_option("examine") add_platform_option add_prerelease_option - add_option('--all', 'Output specifications for all versions of', - 'the gem') do |value, options| + add_option("--all", "Output specifications for all versions of", + "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 @@ -88,7 +89,7 @@ Specific fields in the specification can be extracted in YAML format: raise Gem::CommandLineError, "Unsupported version type: '#{v}'" end - if !req.none? and options[:all] + if !req.none? && options[:all] alert_error "Specify --all or -v, not both" terminate_interaction 1 end @@ -102,11 +103,15 @@ Specific fields in the specification can be extracted in YAML format: field = get_one_optional_argument raise Gem::CommandLineError, "--ruby and FIELD are mutually exclusive" if - field and options[:format] == :ruby + field && options[:format] == :ruby 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? @@ -129,11 +134,11 @@ Specific fields in the specification can be extracted in YAML format: platform = get_platform_from_requirements(options) if platform - specs = specs.select{|s| s.platform.to_s == platform } + specs = specs.select {|s| s.platform.to_s == platform } end unless options[:all] - specs = [specs.max_by {|s| s.version }] + specs = [specs.max_by(&:version)] end specs.each do |s| @@ -143,7 +148,7 @@ Specific fields in the specification can be extracted in YAML format: when :ruby then s.to_ruby when :marshal then Marshal.dump s else s.to_yaml - end + end say "\n" end diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index badc9905c1..0be2b85159 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'rubygems/command' + +require_relative "../command" class Gem::Commands::StaleCommand < Gem::Command def initialize - super('stale', 'List gems along with access times') + super("stale", "List gems along with access times") end def description # :nodoc: @@ -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 1540b2f0fb..2a77ec72cf 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' -require 'rubygems/uninstaller' -require 'fileutils' + +require_relative "../command" +require_relative "../version_option" +require_relative "../uninstaller" +require "fileutils" ## # Gem uninstaller command line tool @@ -13,78 +14,77 @@ class Gem::Commands::UninstallCommand < Gem::Command include Gem::VersionOption def initialize - super 'uninstall', 'Uninstall gems from the local repository', - :version => Gem::Requirement.default, :user_install => true, - :check_dev => false, :vendor => false + super "uninstall", "Uninstall gems from the local repository", + version: Gem::Requirement.default, user_install: true, + check_dev: false, vendor: false - add_option('-a', '--[no-]all', - 'Uninstall all matching versions' - ) do |value, options| + add_option("-a", "--[no-]all", + "Uninstall all matching versions") do |value, options| options[:all] = value end - add_option('-I', '--[no-]ignore-dependencies', - 'Ignore dependency requirements while', - 'uninstalling') do |value, options| + add_option("-I", "--[no-]ignore-dependencies", + "Ignore dependency requirements while", + "uninstalling") do |value, options| options[:ignore] = value end - add_option('-D', '--[no-]check-development', - 'Check development dependencies while uninstalling', - '(default: false)') do |value, options| + add_option("-D", "--[no-]check-development", + "Check development dependencies while uninstalling", + "(default: false)") do |value, options| options[:check_dev] = value end - add_option('-x', '--[no-]executables', - 'Uninstall applicable executables without', - 'confirmation') do |value, options| + add_option("-x", "--[no-]executables", + "Uninstall applicable executables without", + "confirmation") do |value, options| options[:executables] = value end - add_option('-i', '--install-dir DIR', - 'Directory to uninstall gem from') do |value, options| + add_option("-i", "--install-dir DIR", + "Directory to uninstall gem from") do |value, options| options[:install_dir] = File.expand_path(value) end - add_option('-n', '--bindir DIR', - 'Directory to remove executables from') do |value, options| + add_option("-n", "--bindir DIR", + "Directory to remove executables from") do |value, options| options[:bin_dir] = File.expand_path(value) end - add_option('--[no-]user-install', - 'Uninstall from user\'s home directory', - 'in addition to GEM_HOME.') do |value, options| + add_option("--[no-]user-install", + "Uninstall from user's home directory", + "in addition to GEM_HOME.") do |value, options| options[:user_install] = value end - add_option('--[no-]format-executable', - 'Assume executable names match Ruby\'s prefix and suffix.') do |value, options| + add_option("--[no-]format-executable", + "Assume executable names match Ruby's prefix and suffix.") do |value, options| options[:format_executable] = value end - add_option('--[no-]force', - 'Uninstall all versions of the named gems', - 'ignoring dependencies') do |value, options| + add_option("--[no-]force", + "Uninstall all versions of the named gems", + "ignoring dependencies") do |value, options| options[:force] = value end - add_option('--[no-]abort-on-dependent', - 'Prevent uninstalling gems that are', - 'depended on by other gems.') do |value, options| + add_option("--[no-]abort-on-dependent", + "Prevent uninstalling gems that are", + "depended on by other gems.") do |value, options| options[:abort_on_dependent] = value end add_version_option add_platform_option - add_option('--vendor', - 'Uninstall gem from the vendor directory.', - 'Only for use by gem repackagers.') do |value, options| + add_option("--vendor", + "Uninstall gem from the vendor directory.", + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir - raise OptionParser::InvalidOption.new 'your platform is not supported' + raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end - alert_warning 'Use your OS package manager to uninstall vendor gems' + alert_warning "Use your OS package manager to uninstall vendor gems" options[:vendor] = true options[:install_dir] = Gem.vendor_dir end @@ -95,8 +95,8 @@ class Gem::Commands::UninstallCommand < Gem::Command end def defaults_str # :nodoc: - "--version '#{Gem::Requirement.default}' --no-force " + - "--user-install" + "--version '#{Gem::Requirement.default}' --no-force " \ + "--user-install" end def description # :nodoc: @@ -114,8 +114,8 @@ that is a dependency of an existing gem. You can use the end def check_version # :nodoc: - if options[:version] != Gem::Requirement.default and - get_all_gem_names.size > 1 + 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'`" terminate_interaction 1 @@ -125,7 +125,10 @@ that is a dependency of an existing gem. You can use the def execute check_version - if options[:all] and not options[:args].empty? + # 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] uninstall_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 @@ -165,15 +168,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 +183,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" + + alert("In order to remove #{spec.name}, please execute:\n" \ "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}") 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 8d90d08eb4..c2fc720297 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/version_option' -require 'rubygems/security_option' -require 'rubygems/remote_fetcher' -require 'rubygems/package' + +require_relative "../command" +require_relative "../version_option" +require_relative "../security_option" +require_relative "../remote_fetcher" +require_relative "../package" # forward-declare @@ -17,18 +18,18 @@ class Gem::Commands::UnpackCommand < Gem::Command include Gem::SecurityOption def initialize - require 'fileutils' + require "fileutils" - super 'unpack', 'Unpack an installed gem to the current directory', - :version => Gem::Requirement.default, - :target => Dir.pwd + super "unpack", "Unpack an installed gem to the current directory", + version: Gem::Requirement.default, + target: Dir.pwd - add_option('--target=DIR', - 'target directory for unpacking') do |value, options| + 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,19 +96,17 @@ 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| + File.open destination, "w" do |io| io.write metadata end else - basename = File.basename path, '.gem' + basename = File.basename path, ".gem" target_dir = File.expand_path basename, options[:target] package = Gem::Package.new path, security_policy @@ -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 91d93e398c..8e80d46856 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,13 +1,14 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/command_manager' -require 'rubygems/dependency_installer' -require 'rubygems/install_update_options' -require 'rubygems/local_remote_options' -require 'rubygems/spec_fetcher' -require 'rubygems/version_option' -require 'rubygems/install_message' # must come before rdoc for messaging -require 'rubygems/rdoc' + +require_relative "../command" +require_relative "../command_manager" +require_relative "../dependency_installer" +require_relative "../install_update_options" +require_relative "../local_remote_options" +require_relative "../spec_fetcher" +require_relative "../version_option" +require_relative "../install_message" # must come before rdoc for messaging +require_relative "../rdoc" class Gem::Commands::UpdateCommand < Gem::Command include Gem::InstallUpdateOptions @@ -20,26 +21,26 @@ class Gem::Commands::UpdateCommand < Gem::Command def initialize options = { - :force => false, + force: false, } options.merge!(install_update_options) - super 'update', 'Update installed gems to the latest version', options + super "update", "Update installed gems to the latest version", options add_install_update_options - OptionParser.accept Gem::Version do |value| + Gem::OptionParser.accept Gem::Version do |value| Gem::Version.new value value end - add_option('--system [VERSION]', Gem::Version, - 'Update the RubyGems system software') do |value, options| - value = true unless value + add_option("--system [VERSION]", Gem::Version, + "Update the RubyGems system software") do |value, opts| + value ||= true - options[:system] = value + opts[:system] = value end add_local_remote_options @@ -56,7 +57,7 @@ class Gem::Commands::UpdateCommand < Gem::Command def defaults_str # :nodoc: "--no-force --install-dir #{Gem.dir}\n" + - install_update_defaults_str + install_update_defaults_str end def description # :nodoc: @@ -118,15 +119,19 @@ command to remove old versions. updated = update_gems gems_to_update - updated_names = updated.map {|spec| spec.name } + installed_names = highest_installed_gems.keys + 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 if updated.empty? say "Nothing to update" else - say "Gems updated: #{updated_names.join(' ')}" - say "Gems already up-to-date: #{not_updated_names.join(' ')}" unless not_updated_names.empty? + 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? end def fetch_remote_gems(spec) # :nodoc: @@ -151,7 +156,7 @@ command to remove old versions. Gem::Specification.dirs = Gem.user_dir if options[:user_install] Gem::Specification.each do |spec| - if hig[spec.name].nil? or hig[spec.name].version < spec.version + if hig[spec.name].nil? || hig[spec.name].version < spec.version hig[spec.name] = spec end end @@ -162,30 +167,28 @@ command to remove old versions. def highest_remote_name_tuple(spec) # :nodoc: spec_tuples = fetch_remote_gems spec - matching_gems = spec_tuples.select do |g,_| - g.name == spec.name and g.match_platform? - end - - highest_remote_gem = matching_gems.max - - highest_remote_gem ||= [Gem::NameTuple.null] + highest_remote_gem = spec_tuples.max + return unless highest_remote_gem highest_remote_gem.first end - def install_rubygems(version) # :nodoc: + def install_rubygems(spec) # :nodoc: args = update_rubygems_arguments + version = spec.version - update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}" + update_dir = File.join spec.base_dir, "gems", "rubygems-update-#{version}" Dir.chdir update_dir do say "Installing RubyGems #{version}" unless options[:silent] installed = preparing_gem_layout_for(version) do - system Gem.ruby, '--disable-gems', 'setup.rb', *args + 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 @@ -213,32 +215,24 @@ command to remove old versions. version = options[:system] update_latest = version == true - if update_latest - version = Gem::Version.new Gem::VERSION - requirement = Gem::Requirement.new ">= #{Gem::VERSION}" - else + unless update_latest version = Gem::Version.new version requirement = Gem::Requirement.new version + + return version, requirement end + version = Gem::Version.new Gem::VERSION + requirement = Gem::Requirement.new ">= #{Gem::VERSION}" + rubygems_update = Gem::Specification.new - rubygems_update.name = 'rubygems-update' + rubygems_update.name = "rubygems-update" rubygems_update.version = version - hig = { - 'rubygems-update' => rubygems_update, - } - - gems_to_update = which_to_update hig, options[:args], :system - up_ver = gems_to_update.first.version - - target = if update_latest - up_ver - else - version - end + 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) @@ -249,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 @@ -286,40 +280,34 @@ command to remove old versions. check_oldest_rubygems version - update_gem 'rubygems-update', version + installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement + installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version + return if installed_gems.empty? - installed_gems = Gem::Specification.find_all_by_name 'rubygems-update', requirement - version = installed_gems.first.version - - install_rubygems version + install_rubygems installed_gems.first end def update_rubygems_arguments # :nodoc: args = [] - args << '--silent' if options[:silent] - 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 or - Gem::Version.new(options[:system]) >= Gem::Version.new(2) + args << "--silent" if options[:silent] + 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 args end - def which_to_update(highest_installed_gems, gem_names, system = false) + def which_to_update(highest_installed_gems, gem_names) result = [] - highest_installed_gems.each do |l_name, l_spec| - next if not gem_names.empty? and + highest_installed_gems.each do |_l_name, l_spec| + next if !gem_names.empty? && gem_names.none? {|name| name == l_spec.name } highest_remote_tup = highest_remote_name_tuple l_spec - highest_remote_ver = highest_remote_tup.version - highest_installed_ver = l_spec.version + next unless highest_remote_tup - if system or (highest_installed_ver < highest_remote_ver) - result << Gem::NameTuple.new(l_spec.name, [highest_installed_ver, highest_remote_ver].max, highest_remote_tup.platform) - end + result << highest_remote_tup end result @@ -335,18 +323,10 @@ command to remove old versions. # def oldest_supported_version @oldest_supported_version ||= - if 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") - elsif Gem.ruby_version > Gem::Version.new("2.6.a") - Gem::Version.new("3.0.1") - elsif Gem.ruby_version > Gem::Version.new("2.5.a") - Gem::Version.new("2.7.3") - elsif Gem.ruby_version > Gem::Version.new("2.4.a") - Gem::Version.new("2.6.8") + if Gem.ruby_version > Gem::Version.new("3.1.a") + Gem::Version.new("3.3.3") else - Gem::Version.new("2.5.2") + Gem::Version.new("3.2.3") end end end diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb index d42ab18395..5ed4d9d142 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -1,17 +1,18 @@ # frozen_string_literal: true -require 'rubygems/command' + +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 + super "which", "Find the location of a library file you can require", + search_gems_first: false, show_all: false - add_option '-a', '--[no-]all', 'show all matching files' do |show_all, options| + add_option "-a", "--[no-]all", "show all matching files" do |show_all, options| options[:show_all] = show_all end - add_option '-g', '--[no-]gems-first', - 'search gems before non-gems' do |gems_first, options| + add_option "-g", "--[no-]gems-first", + "search gems before non-gems" do |gems_first, options| options[:search_gems_first] = gems_first end end @@ -39,7 +40,7 @@ requiring to see why it does not behave as you expect. found = true options[:args].each do |arg| - arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, '') + arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, "") dirs = $LOAD_PATH spec = Gem::Specification.find_by_path arg @@ -71,7 +72,7 @@ requiring to see why it does not behave as you expect. dirs.each do |dir| Gem.suffixes.each do |ext| full_path = File.join dir, "#{package_name}#{ext}" - if File.exist? full_path and not File.directory? full_path + if File.exist?(full_path) && !File.directory?(full_path) result << full_path return result unless options[:show_all] end diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index a7930253d6..fbdc262549 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems/command' -require 'rubygems/local_remote_options' -require 'rubygems/version_option' -require 'rubygems/gemcutter_utilities' + +require_relative "../command" +require_relative "../local_remote_options" +require_relative "../version_option" +require_relative "../gemcutter_utilities" class Gem::Commands::YankCommand < Gem::Command include Gem::LocalRemoteOptions @@ -28,15 +29,15 @@ data you will need to change them immediately and yank your gem. end def initialize - super 'yank', 'Remove a pushed gem from the index' + super "yank", "Remove a pushed gem from the index" add_version_option("remove") add_platform_option("remove") add_otp_option - add_option('--host HOST', - 'Yank from another gemcutter-compatible host', - ' (e.g. https://rubygems.org)') do |value, options| + add_option("--host HOST", + "Yank from another gemcutter-compatible host", + " (e.g. https://rubygems.org)") do |value, options| options[:host] = value end @@ -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) @@ -76,10 +77,10 @@ data you will need to change them immediately and yank your gem. request.add_field("Authorization", api_key) data = { - 'gem_name' => name, - 'version' => version, + "gem_name" => name, + "version" => version, } - data['platform'] = platform if platform + data["platform"] = platform if platform request.set_form_data data end @@ -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 index f1d452ea04..0d9df56f8a 100644 --- a/lib/rubygems/compatibility.rb +++ b/lib/rubygems/compatibility.rb @@ -1,5 +1,4 @@ # frozen_string_literal: true -# :stopdoc: #-- # This file contains all sorts of little compatibility hacks that we've @@ -8,10 +7,13 @@ # # 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 #++ -# TODO remove at RubyGems 4 module Gem + # :stopdoc: + RubyGemsVersion = VERSION deprecate_constant(:RubyGemsVersion) @@ -24,17 +26,16 @@ module Gem rubylibdir ].freeze - unless defined?(ConfigMap) + if defined?(ConfigMap) + RbConfigPriorities.each do |key| + ConfigMap[key.to_sym] = RbConfig::CONFIG[key] + end + else ## # 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 9dc41a2995..7874ad0dc9 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/user_interaction' -require 'rbconfig' +require_relative "user_interaction" +require "rbconfig" ## # Gem::ConfigFile RubyGems options and gem command options from gemrc. @@ -39,13 +40,15 @@ require 'rbconfig' class Gem::ConfigFile include Gem::UserInteraction - DEFAULT_BACKTRACE = false + DEFAULT_BACKTRACE = true DEFAULT_BULK_THRESHOLD = 1000 DEFAULT_VERBOSITY = true DEFAULT_UPDATE_SOURCES = true DEFAULT_CONCURRENT_DOWNLOADS = 8 DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365 DEFAULT_IPV4_FALLBACK_ENABLED = false + # TODO: Use false as default value for this option in RubyGems 4.0 + DEFAULT_INSTALL_EXTENSION_IN_LIB = true ## # For Ruby packagers to set configuration defaults. Set in @@ -71,7 +74,7 @@ class Gem::ConfigFile # :startdoc: - SYSTEM_WIDE_CONFIG_FILE = File.join SYSTEM_CONFIG_PATH, 'gemrc' + SYSTEM_WIDE_CONFIG_FILE = File.join SYSTEM_CONFIG_PATH, "gemrc" ## # List of arguments supplied to the config file object. @@ -142,6 +145,11 @@ 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) @@ -182,40 +190,54 @@ class Gem::ConfigFile @update_sources = DEFAULT_UPDATE_SOURCES @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS - @ipv4_fallback_enabled = ENV['IPV4_FALLBACK_ENABLED'] == 'true' || DEFAULT_IPV4_FALLBACK_ENABLED + @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB + @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED 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 @hash = operating_system_config.merge platform_config - unless args.index '--norc' + unless args.index "--norc" @hash = @hash.merge system_config @hash = @hash.merge user_config @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 + install_extension_in_lib ipv4_fallback_enabled 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 + # TODO: We should handle concurrent_downloads same as other options @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 - @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 +262,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: @@ -269,7 +291,7 @@ if you believe they were disclosed to a third party. # Location of RubyGems.org credentials def credentials_path - credentials = File.join Gem.user_home, '.gem', 'credentials' + credentials = File.join Gem.user_home, ".gem", "credentials" if File.exist? credentials credentials else @@ -281,10 +303,10 @@ if you believe they were disclosed to a third party. check_credentials_permissions @api_keys = if File.exist? credentials_path - load_file(credentials_path) - else - @hash - end + load_file(credentials_path) + else + @hash + end if @api_keys.key? :rubygems_api_key @rubygems_api_key = @api_keys[:rubygems_api_key] @@ -320,13 +342,12 @@ if you believe they were disclosed to a third party. config = load_file(credentials_path).merge(host => api_key) dirname = File.dirname credentials_path - Dir.mkdir(dirname) unless File.exist? dirname - - Gem.load_yaml + require "fileutils" + FileUtils.mkdir_p(dirname) - permissions = 0600 & (~File.umask) - File.open(credentials_path, 'w', permissions) do |f| - f.write config.to_yaml + permissions = 0o600 & (~File.umask) + File.open(credentials_path, "w", permissions) do |f| + f.write self.class.dump_with_rubygems_yaml(config) end load_api_keys # reload @@ -342,20 +363,18 @@ 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)) + if config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") } 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 @@ -367,7 +386,21 @@ if you believe they were disclosed to a third party. # True if the backtrace option has been specified, or debug is on. def backtrace - @backtrace or $DEBUG + @backtrace || $DEBUG + end + + # Check state file is writable. Creates empty file if not present to ensure we can write to it. + def state_file_writable? + if File.exist?(state_file_name) + File.writable?(state_file_name) + else + require "fileutils" + FileUtils.mkdir_p File.dirname(state_file_name) + File.open(state_file_name, "w") {} + true + end + rescue Errno::EACCES + false end # The name of the configuration file. @@ -375,6 +408,25 @@ if you believe they were disclosed to a third party. @config_file_name || Gem.config_file end + # The name of the state file. + def state_file_name + Gem.state_file + end + + # Reads time of last update check from state file + def last_update_check + if File.readable?(state_file_name) + File.read(state_file_name).to_i + else + 0 + end + end + + # Writes time of last update check to state file + def last_update_check=(timestamp) + File.write(state_file_name, timestamp.to_s) if state_file_writable? + end + # Delegates to @hash def each(&block) hash = @hash.dup @@ -388,7 +440,7 @@ if you believe they were disclosed to a third party. yield :backtrace, @backtrace yield :bulk_threshold, @bulk_threshold - yield 'config_file_name', @config_file_name if @config_file_name + yield "config_file_name", @config_file_name if @config_file_name hash.each(&block) end @@ -404,7 +456,7 @@ if you believe they were disclosed to a third party. when /^--debug$/ then $DEBUG = true - warn 'NOTE: Debugging mode prints all exceptions even when rescued' + warn "NOTE: Debugging mode prints all exceptions even when rescued" else @args << arg end @@ -433,6 +485,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 @@ -442,26 +497,25 @@ 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 << 'debug' + 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. def write - unless File.exist?(File.dirname(config_file_name)) - FileUtils.mkdir_p File.dirname(config_file_name) - end + require "fileutils" + FileUtils.mkdir_p File.dirname(config_file_name) - File.open config_file_name, 'w' do |io| + File.open config_file_name, "w" do |io| io.write to_yaml end end @@ -477,17 +531,68 @@ if you believe they were disclosed to a third party. end def ==(other) # :nodoc: - self.class === other and - @backtrace == other.backtrace and - @bulk_threshold == other.bulk_threshold and - @verbose == other.verbose and - @update_sources == other.update_sources and + self.class === other && + @backtrace == other.backtrace && + @bulk_threshold == other.bulk_threshold && + @verbose == other.verbose && + @update_sources == other.update_sources && @hash == other.hash end 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) + + content.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 + + content.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.is_a?(Hash) && v.empty? + nil + else + v + end + end + + content + end + private def set_config_file_name(args) @@ -500,7 +605,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 e722225739..4e09b95c44 100644 --- a/lib/rubygems/core_ext/kernel_gem.rb +++ b/lib/rubygems/core_ext/kernel_gem.rb @@ -1,12 +1,6 @@ # frozen_string_literal: true -## -# RubyGems adds the #gem method to allow activation of specific gem versions -# and overrides the #require method on Kernel to make gems appear as if they -# live on the <code>$LOAD_PATH</code>. See the documentation of these methods -# for further detail. module Kernel - ## # Use Kernel#gem to activate a specific version of +gem_name+. # @@ -39,12 +33,12 @@ module Kernel # GEM_SKIP=libA:libB ruby -I../libA -I../libB ./mycode.rb def gem(gem_name, *requirements) # :doc: - skip_list = (ENV['GEM_SKIP'] || "").split(/:/) + 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 @@ -71,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 4b867c55e9..073966b696 100644 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -1,24 +1,24 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'monitor' +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) - alias gem_original_require require + # :stopdoc: + alias_method :gem_original_require, :require private :gem_original_require + # :startdoc: end - file = Gem::KERNEL_WARN_IGNORES_INTERNAL_ENTRIES ? "<internal:#{__FILE__}>" : __FILE__ - module_eval <<'RUBY', file, __LINE__ + 1 ## # When RubyGems is required, Kernel#require is replaced with our own which # is capable of loading gems on demand. @@ -33,143 +33,117 @@ module Kernel # The normal <tt>require</tt> functionality of returning false if # that file has already been loaded is preserved. - def require(path) - 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 + def require(path) # :doc: + 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 + Kernel.send(:gem, name, Gem::Requirement.default_prerelease) unless + resolved_path - # If there are no unresolved deps, then we can use just try - # normal require handle loading a gem from the rescue below. + next + end - if Gem::Specification.unresolved_deps.empty? - 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. - # 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 + if Gem::Specification.unresolved_deps.empty? + next + end - if Gem::Specification.find_active_stub_by_path(path) - RUBYGEMS_ACTIVATION_MONITOR.exit - return gem_original_require(path) - end + # 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 - # 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.find_active_stub_by_path(path) + 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 + # Attempt to find +path+ in any unresolved gems... - # Check that all the found specs are just different - # versions of the same gem - names = found_specs.map(&:name).uniq + found_specs = Gem::Specification.find_in_unresolved path - if names.size > 1 - RUBYGEMS_ACTIVATION_MONITOR.exit - raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ', '}" - 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 - # Ok, now find a gem that has no conflicts, starting - # at the highest version. - valid = found_specs.find {|s| !s.has_conflicts? } + found_specs.each(&:activate) - 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 + # We found +path+ directly in an unresolved gem. Now we figure out, of + # the possible found specs, which one we should activate. + else - valid.activate - end + # Check that all the found specs are just different + # versions of the same gem + names = found_specs.map(&:name).uniq - RUBYGEMS_ACTIVATION_MONITOR.exit - return gem_original_require(path) - rescue LoadError => load_error - RUBYGEMS_ACTIVATION_MONITOR.enter + if names.size > 1 + raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ", "}" + end - begin - if load_error.path == path and Gem.try_activate(path) - require_again = true + # 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 - ensure - RUBYGEMS_ACTIVATION_MONITOR.exit end - return gem_original_require(path) if require_again + begin + gem_original_require(path) + rescue LoadError => load_error + if load_error.path == path && + RUBYGEMS_ACTIVATION_MONITOR.synchronize { Gem.try_activate(path) } - 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}" + return gem_original_require(path) end + + raise load_error end end -RUBY private :require - end diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb index 3373cfdd3b..9dc9f2218c 100644 --- a/lib/rubygems/core_ext/kernel_warn.rb +++ b/lib/rubygems/core_ext/kernel_warn.rb @@ -1,54 +1,49 @@ # frozen_string_literal: true -# `uplevel` keyword argument of Kernel#warn is available since ruby 2.5. -if RUBY_VERSION >= "2.5" && !Gem::KERNEL_WARN_IGNORES_INTERNAL_ENTRIES +module Kernel + rubygems_path = "#{__dir__}/" # Frames to be skipped start with this path. - module Kernel - rubygems_path = "#{__dir__}/" # Frames to be skipped start with this path. + original_warn = instance_method(:warn) - original_warn = instance_method(:warn) + remove_method :warn + class << self remove_method :warn + end - class << self - remove_method :warn + 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 end - module_function define_method(:warn) {|*messages, **kw| - unless uplevel = kw[:uplevel] - if Gem.java_platform? - return original_warn.bind(self).call(*messages) - else - return original_warn.bind(self).call(*messages, **kw) + # Ensure `uplevel` fits a `long` + uplevel, = [uplevel].pack("l!").unpack("l!") + + if uplevel >= 0 + start = 0 + while uplevel >= 0 + loc, = caller_locations(start, 1) + unless loc + # No more backtrace + start += uplevel + break end - end - # Ensure `uplevel` fits a `long` - uplevel, = [uplevel].pack("l!").unpack("l!") - - if uplevel >= 0 - start = 0 - while uplevel >= 0 - loc, = caller_locations(start, 1) - unless loc - # No more backtrace - start += uplevel - break - end - - start += 1 - - if path = loc.path - unless path.start_with?(rubygems_path) or path.start_with?('<internal:') - # Non-rubygems frames - uplevel -= 1 - end - end + start += 1 + + next unless path = loc.path + unless path.start_with?(rubygems_path, "<internal:") + # Non-rubygems frames + uplevel -= 1 end - kw[:uplevel] = start end + kw[:uplevel] = start + end - original_warn.bind(self).call(*messages, **kw) - } - end + original_warn.bind(self).call(*messages, **kw) + } end diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb index 3d9740c579..018c49dbeb 100644 --- a/lib/rubygems/core_ext/tcpsocket_init.rb +++ b/lib/rubygems/core_ext/tcpsocket_init.rb @@ -1,4 +1,6 @@ -require 'socket' +# frozen_string_literal: true + +require "socket" module CoreExtensions module TCPSocketExt @@ -11,13 +13,13 @@ module CoreExtensions IPV4_DELAY_SECONDS = 0.1 def initialize(host, serv, *rest) - mutex = Mutex.new + mutex = Thread::Mutex.new addrs = [] threads = [] - cond_var = ConditionVariable.new + 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 e95bc06792..1bd208feb9 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true + module Gem - DEFAULT_HOST = "https://rubygems.org".freeze + DEFAULT_HOST = "https://rubygems.org" @post_install_hooks ||= [] @done_installing_hooks ||= [] @@ -20,10 +21,10 @@ module Gem # specified in the environment def self.default_spec_cache_dir - default_spec_cache_dir = File.join Gem.user_home, '.gem', 'specs' + 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 @@ -34,21 +35,7 @@ module Gem # specified in the environment def self.default_dir - path = if defined? RUBY_FRAMEWORK_VERSION - [ - File.dirname(RbConfig::CONFIG['sitedir']), - 'Gems', - RbConfig::CONFIG['ruby_version'], - ] - else - [ - RbConfig::CONFIG['rubylibprefix'], - 'gems', - RbConfig::CONFIG['ruby_version'], - ] - end - - @default_dir ||= File.join(*path) + @default_dir ||= File.join(RbConfig::CONFIG["rubylibprefix"], "gems", RbConfig::CONFIG["ruby_version"]) end ## @@ -73,7 +60,7 @@ module Gem # Path to specification files of default gems. def self.default_specifications_dir - File.join(Gem.default_dir, "specifications", "default") + @default_specifications_dir ||= File.join(Gem.default_dir, "specifications", "default") end ## @@ -93,9 +80,9 @@ 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'], '/') + File.expand_path File.join(ENV["HOMEDRIVE"] || ENV["SystemDrive"], "/") else File.expand_path "/" end @@ -107,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 ## @@ -117,7 +104,7 @@ module Gem gem_dir = File.join(Gem.user_home, ".gem") gem_dir = File.join(Gem.data_home, "gem") unless File.exist?(gem_dir) parts = [gem_dir, ruby_engine] - parts << RbConfig::CONFIG['ruby_version'] unless RbConfig::CONFIG['ruby_version'].empty? + parts << RbConfig::CONFIG["ruby_version"] unless RbConfig::CONFIG["ruby_version"].empty? File.join parts end @@ -125,14 +112,14 @@ 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 ## # Finds the user's config file def self.find_config_file - gemrc = File.join Gem.user_home, '.gemrc' + gemrc = File.join Gem.user_home, ".gemrc" if File.exist? gemrc gemrc else @@ -144,21 +131,35 @@ 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") 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 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 + @state_home ||= ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state") end ## @@ -175,7 +176,7 @@ module Gem path = [] path << user_dir if user_home && File.exist?(user_home) path << default_dir - path << vendor_dir if vendor_dir and File.directory? vendor_dir + path << vendor_dir if vendor_dir && File.directory?(vendor_dir) path end @@ -183,9 +184,13 @@ 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 =~ /%s/ + unless exec_format.include?("%s") raise Gem::Exception, "[BUG] invalid exec_format #{exec_format.inspect}, no %s" end @@ -197,11 +202,7 @@ module Gem # The default directory for binaries def self.default_bindir - if defined? RUBY_FRAMEWORK_VERSION # mac framework support - '/usr/local/bin' - else # generic install - RbConfig::CONFIG['bindir'] - end + RbConfig::CONFIG["bindir"] end def self.ruby_engine @@ -235,24 +236,36 @@ 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 ## # Directory where vendor gems are installed. def self.vendor_dir # :nodoc: - if vendor_dir = ENV['GEM_VENDOR'] + if vendor_dir = ENV["GEM_VENDOR"] return vendor_dir.dup end - return nil unless RbConfig::CONFIG.key? 'vendordir' + return nil unless RbConfig::CONFIG.key? "vendordir" - File.join RbConfig::CONFIG['vendordir'], 'gems', - RbConfig::CONFIG['ruby_version'] + File.join RbConfig::CONFIG["vendordir"], "gems", + RbConfig::CONFIG["ruby_version"] end ## diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index 3721204ab2..d1bf074441 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 @@ -97,14 +96,14 @@ class Gem::Dependency end def pretty_print(q) # :nodoc: - q.group 1, 'Gem::Dependency.new(', ')' do + q.group 1, "Gem::Dependency.new(", ")" do q.pp name - q.text ',' + q.text "," q.breakable q.pp requirement - q.text ',' + q.text "," q.breakable q.pp type @@ -115,7 +114,7 @@ class Gem::Dependency # What does this dependency require? def requirement - return @requirement if defined?(@requirement) and @requirement + return @requirement if defined?(@requirement) && @requirement # @version_requirements and @version_requirement are legacy ivar # names, and supported here because older gems need to keep @@ -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 ## @@ -197,14 +196,14 @@ class Gem::Dependency reqs = other.requirement.requirements return false unless reqs.length == 1 - return false unless reqs.first.first == '=' + return false unless reqs.first.first == "=" version = reqs.first.last requirement.satisfied_by? version end - alias === =~ + alias_method :===, :=~ ## # :call-seq: @@ -230,10 +229,10 @@ class Gem::Dependency version = Gem::Version.new version - return true if requirement.none? and not version.prerelease? - return false if version.prerelease? and - not allow_prerelease and - not prerelease? + return true if requirement.none? && !version.prerelease? + return false if version.prerelease? && + !allow_prerelease && + !prerelease? requirement.satisfied_by? version end @@ -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 @@ -277,7 +276,10 @@ class Gem::Dependency requirement.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version) end.map(&:to_spec) - Gem::BundlerVersionFinder.filter!(matches) if filters_bundler? + if prioritizes_bundler? + require_relative "bundler_version_finder" + Gem::BundlerVersionFinder.prioritize!(matches) + end if platform_only matches.reject! do |spec| @@ -295,8 +297,8 @@ class Gem::Dependency @requirement.specific? end - def filters_bundler? - name == "bundler".freeze && !specific? + def prioritizes_bundler? + name == "bundler" && !specific? end def to_specs @@ -320,16 +322,16 @@ 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 - return matches.first if prerelease? - - # Move prereleases to the end of the list for >= 0 requirements - pre, matches = matches.partition {|spec| spec.version.prerelease? } - matches += pre if requirement == Gem::Requirement.default + unless prerelease? + # Consider prereleases only as a fallback + pre, matches = matches.partition {|spec| spec.version.prerelease? } + matches = pre if matches.empty? + end matches.first end diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 400a5de5cf..b119dca1cf 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true -require 'rubygems' -require 'rubygems/dependency_list' -require 'rubygems/package' -require 'rubygems/installer' -require 'rubygems/spec_fetcher' -require 'rubygems/user_interaction' -require 'rubygems/available_set' -require 'rubygems/deprecate' + +require_relative "../rubygems" +require_relative "dependency_list" +require_relative "package" +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. @@ -16,18 +17,18 @@ class Gem::DependencyInstaller 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, + install_as_default: false, }.freeze ## @@ -65,7 +66,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] @@ -109,7 +110,7 @@ class Gem::DependencyInstaller # gems should be considered. def consider_local? - @domain == :both or @domain == :local + @domain == :both || @domain == :local end ## @@ -117,7 +118,7 @@ class Gem::DependencyInstaller # gems should be considered. def consider_remote? - @domain == :both or @domain == :remote + @domain == :both || @domain == :remote end ## @@ -162,13 +163,11 @@ class Gem::DependencyInstaller 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 if @errors @@ -178,7 +177,6 @@ class Gem::DependencyInstaller 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 @@ -197,7 +195,7 @@ class Gem::DependencyInstaller def in_background(what) # :nodoc: fork_happened = false - if @build_docs_in_background and Process.respond_to?(:fork) + if @build_docs_in_background && Process.respond_to?(:fork) begin Process.fork do yield @@ -230,22 +228,22 @@ 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, + 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, } options[:install_dir] = @install_dir if @only_install_dir @@ -268,7 +266,7 @@ class Gem::DependencyInstaller end def install_development_deps # :nodoc: - if @development and @dev_shallow + if @development && @dev_shallow :shallow elsif @development :all @@ -289,17 +287,15 @@ class Gem::DependencyInstaller installer_set.force = @force if consider_local? - if dep_or_name =~ /\.gem$/ and File.file? dep_or_name + if dep_or_name =~ /\.gem$/ && File.file?(dep_or_name) 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 bcf436cd03..ad5e59e8c1 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'tsort' -require 'rubygems/deprecate' +require_relative "vendored_tsort" +require_relative "deprecate" ## # Gem::DependencyList is used for installing and uninstalling gems in the @@ -20,7 +21,7 @@ class Gem::DependencyList attr_reader :specs include Enumerable - include TSort + include Gem::TSort ## # Allows enabling/disabling use of development dependencies @@ -104,7 +105,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 ## @@ -119,11 +120,11 @@ class Gem::DependencyList each do |spec| spec.runtime_dependencies.each do |dep| inst = Gem::Specification.any? do |installed_spec| - dep.name == installed_spec.name and - dep.requirement.satisfied_by? installed_spec.version + dep.name == installed_spec.name && + dep.requirement.satisfied_by?(installed_spec.version) end - unless inst or @specs.find {|s| s.satisfies_requirement? dep } + unless inst || @specs.find {|s| s.satisfies_requirement? dep } unsatisfied[spec.name] << dep return unsatisfied if quick end @@ -175,7 +176,7 @@ class Gem::DependencyList def remove_specs_unsatisfied_by(dependencies) specs.reject! do |spec| dep = dependencies[spec.name] - dep and not dep.requirement.satisfied_by? spec.version + dep && !dep.requirement.satisfied_by?(spec.version) end end diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 8c822cda95..58a6c5b7dc 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -1,28 +1,75 @@ # frozen_string_literal: true + ## -# Provides a single method +deprecate+ to be used to declare when -# something is going away. +# 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.klass_method +# def self.some_class_method +# # ... +# end +# +# def some_instance_method # # ... # end # -# def instance_method +# def some_old_method # # ... # end # # extend Gem::Deprecate -# deprecate :instance_method, "X.z", 2011, 4 +# deprecate :some_instance_method, "X.z", 2011, 4 +# rubygems_deprecate :some_old_method, "Modern#some_new_method" # # class << self # extend Gem::Deprecate -# deprecate :klass_method, :none, 2011, 4 +# 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 - def self.skip # :nodoc: @skip ||= false end @@ -35,7 +82,8 @@ module Gem::Deprecate # Temporarily turn off warnings. Intended for tests only. def skip_during - Gem::Deprecate.skip, original = true, Gem::Deprecate.skip + original = Gem::Deprecate.skip + Gem::Deprecate.skip = true yield ensure Gem::Deprecate.skip = original @@ -56,12 +104,13 @@ module Gem::Deprecate old = "_deprecated_#{name}" alias_method old, name define_method name do |*args, &block| - klass = self.kind_of? Module + klass = is_a? 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(":")}", + 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 @@ -81,12 +130,13 @@ module Gem::Deprecate old = "_deprecated_#{name}" alias_method old, name define_method name do |*args, &block| - klass = self.kind_of? Module + klass = is_a? 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(":")}", + 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 @@ -96,22 +146,22 @@ module Gem::Deprecate end # Deprecation method to deprecate Rubygems commands - def rubygems_deprecate_command + def rubygems_deprecate_command(version = Gem::Deprecate.next_rubygems_major_version) 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", + 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 module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during - end diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb index ef31aeddd0..56b7c081eb 100644 --- a/lib/rubygems/doctor.rb +++ b/lib/rubygems/doctor.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'rubygems' -require 'rubygems/user_interaction' + +require_relative "../rubygems" +require_relative "user_interaction" ## # Cleans up after a partially-failed uninstall or for an invalid @@ -19,20 +20,20 @@ class Gem::Doctor # subdirectory. REPOSITORY_EXTENSION_MAP = [ # :nodoc: - ['specifications', '.gemspec'], - ['build_info', '.info'], - ['cache', '.gem'], - ['doc', ''], - ['extensions', ''], - ['gems', ''], - ['plugins', ''], + ["specifications", ".gemspec"], + ["build_info", ".info"], + ["cache", ".gem"], + ["doc", ""], + ["extensions", ""], + ["gems", ""], + ["plugins", ""], ].freeze missing = Gem::REPOSITORY_SUBDIRECTORIES.sort - - REPOSITORY_EXTENSION_MAP.map {|(k,_)| k }.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,14 +53,14 @@ 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 ## # Are we doctoring a gem repository? def gem_repository? - not installed_specs.empty? + !installed_specs.empty? end ## @@ -74,8 +75,8 @@ class Gem::Doctor Gem.use_paths @gem_repository.to_s unless gem_repository? - say 'This directory does not appear to be a RubyGems repository, ' + - 'skipping' + say "This directory does not appear to be a RubyGems repository, " \ + "skipping" say return end @@ -103,25 +104,25 @@ 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 and 'default' == basename - next if 'plugins' == sub_directory and 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' + type = File.directory?(child) ? "directory" : "file" action = if @dry_run - 'Extra' - else - FileUtils.rm_r(child) - 'Removed' - end + "Extra" + else + FileUtils.rm_r(child) + "Removed" + end say "#{action} #{type} #{sub_directory}/#{File.basename(child)}" end diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index abee20651e..be6c34dc85 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. @@ -59,11 +60,8 @@ module Gem private def build_message - if name == "bundler" && message = Gem::BundlerVersionFinder.missing_version_message - return message - end 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 @@ -136,11 +134,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 @@ -171,13 +165,12 @@ module Gem # An English description of the error. def wordy - @source.uri.password = 'REDACTED' unless @source.uri.password.nil? - "Unable to download data from #{@source.uri} - #{@error.message}" + "Unable to download data from #{Gem::Uri.redact(@source.uri)} - #{@error.message}" end ## # 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 55755ddfba..0308b4687f 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true -require 'rubygems/deprecate' -require 'rubygems/unknown_command_spell_checker' +require_relative "deprecate" +require_relative "unknown_command_spell_checker" ## # Base exception class for RubyGems. All exception raised by RubyGems are a @@ -24,10 +24,14 @@ class Gem::UnknownCommandError < Gem::Exception return if defined?(@attached) if defined?(DidYouMean::SPELL_CHECKERS) && defined?(DidYouMean::Correctable) - DidYouMean::SPELL_CHECKERS['Gem::UnknownCommandError'] = - Gem::UnknownCommandSpellChecker + if DidYouMean.respond_to?(:correct_error) + DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker) + else + DidYouMean::SPELL_CHECKERS["Gem::UnknownCommandError"] = + Gem::UnknownCommandSpellChecker - prepend DidYouMean::Correctable + prepend DidYouMean::Correctable + end end @attached = true @@ -150,7 +154,7 @@ class Gem::ImpossibleDependenciesError < Gem::Exception def build_message # :nodoc: requester = @request.requester - requester = requester ? requester.spec.full_name : 'The user' + requester = requester ? requester.spec.full_name : "The user" dependency = @request.dependency message = "#{requester} requires #{dependency} but it conflicted:\n".dup @@ -168,6 +172,7 @@ class Gem::ImpossibleDependenciesError < Gem::Exception end class Gem::InstallError < Gem::Exception; end + class Gem::RuntimeRequirementNotMetError < Gem::InstallError attr_accessor :suggestion def message @@ -210,6 +215,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 @@ -217,15 +232,13 @@ class Gem::SystemExitException < SystemExit ## # The exit code for the process - attr_accessor :exit_code + alias_method :exit_code, :status ## # Creates a new SystemExitException with the given +exit_code+ def initialize(exit_code) - @exit_code = exit_code - - super "Exiting RubyGems with exit_code #{exit_code}" + super exit_code, "Exiting RubyGems with exit_code #{exit_code}" end end @@ -250,9 +263,9 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError # Gem::Resolver::DependencyRequest +dep+ def initialize(dep, platform_mismatch=nil) - if platform_mismatch and !platform_mismatch.empty? + 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}'" @@ -279,8 +292,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: diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb index bdd5bd9d82..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. @@ -10,9 +11,10 @@ module Gem::Ext; end -require_relative 'ext/build_error' -require_relative 'ext/builder' -require_relative 'ext/configure_builder' -require_relative 'ext/ext_conf_builder' -require_relative 'ext/rake_builder' -require_relative 'ext/cmake_builder' +require_relative "ext/build_error" +require_relative "ext/builder" +require_relative "ext/configure_builder" +require_relative "ext/ext_conf_builder" +require_relative "ext/rake_builder" +require_relative "ext/cmake_builder" +require_relative "ext/cargo_builder" diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb index 8ef57ed91a..0329c1eec3 100644 --- a/lib/rubygems/ext/build_error.rb +++ b/lib/rubygems/ext/build_error.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true + ## # Raised when there is an error while building extensions. -require_relative '../exceptions' +require_relative "../exceptions" class Gem::Ext::BuildError < Gem::InstallError end diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 14d5dde413..be1ba3031c 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -1,11 +1,13 @@ # 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 '../user_interaction' +require_relative "../user_interaction" +require_relative "../shellwords" class Gem::Ext::Builder include Gem::UserInteraction @@ -17,63 +19,93 @@ class Gem::Ext::Builder $1.downcase end - def self.make(dest_path, results, make_dir = Dir.pwd) - unless File.exist? File.join(make_dir, 'Makefile') - raise Gem::InstallError, 'Makefile not found' + def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"]) + unless File.exist? File.join(make_dir, "Makefile") + raise Gem::InstallError, "Makefile not found" end # try to find make program from Ruby configure arguments first - RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/ - make_program = ENV['MAKE'] || ENV['make'] || $1 - unless make_program - make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make' - end - make_program = Shellwords.split(make_program) + RbConfig::CONFIG["configure_args"] =~ /with-make-prog\=(\w+)/ + make_program_name = ENV["MAKE"] || ENV["make"] || $1 + make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make" + make_program = Shellwords.split(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"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" - destdir = 'DESTDIR=%s' % ENV['DESTDIR'] + env = [destdir] + + if sitedir + env << format("sitearchdir=%s", sitedir) + env << format("sitelibdir=%s", sitedir) + end - ['clean', '', 'install'].each do |target| + targets.each do |target| # Pass DESTDIR via command line to override what's in MAKEFLAGS cmd = [ *make_program, - destdir, + *env, target, ].reject(&:empty?) begin run(cmd, results, "make #{target}".rstrip, make_dir) rescue Gem::InstallError - raise unless target == 'clean' # ignore clean failure + raise unless target == "clean" # ignore clean failure end end end - def self.run(command, results, command_name = nil, dir = Dir.pwd) + def self.ruby + # Gem.ruby is quoted if it contains whitespace + cmd = Shellwords.split(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}" - results << command.shelljoin + results << Shellwords.join(command) require "open3" # Set $SOURCE_DATE_EPOCH for the subprocess. - env = {'SOURCE_DATE_EPOCH' => Gem.source_date_epoch_string} + build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env) output, status = begin - Open3.capture2e(env, *command, :chdir => dir) - rescue => error + Open3.popen2e(build_env, *command, chdir: dir) do |_stdin, stdouterr, wait_thread| + 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 - ENV['RUBYGEMS_GEMDEPS'] = rubygems_gemdeps + ENV["RUBYGEMS_GEMDEPS"] = rubygems_gemdeps end unless status.success? @@ -121,6 +153,8 @@ class Gem::Ext::Builder Gem::Ext::RakeBuilder when /CMakeLists.txt/ then Gem::Ext::CmakeBuilder + when /Cargo.toml/ then + Gem::Ext::CargoBuilder.new else build_error("No builder for extension '#{extension}'") end @@ -162,7 +196,7 @@ EOF 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 @@ -178,7 +212,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,11 +234,11 @@ EOF # Writes +output+ to gem_make.out in the extension install directory. def write_gem_make_out(output) # :nodoc: - destination = File.join @spec.extension_dir, 'gem_make.out' + destination = File.join @spec.extension_dir, "gem_make.out" FileUtils.mkdir_p @spec.extension_dir - File.open destination, 'wb' do |io| + File.open destination, "wb" do |io| io.puts output end diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb new file mode 100644 index 0000000000..86a0e73f28 --- /dev/null +++ b/lib/rubygems/ext/cargo_builder.rb @@ -0,0 +1,359 @@ +# frozen_string_literal: true + +require_relative "../shellwords" + +# This class is used by rubygems to build Rust extensions. It is a thin-wrapper +# over the `cargo rustc` command which takes care of building Rust code in a way +# that Ruby can use. +class Gem::Ext::CargoBuilder < Gem::Ext::Builder + attr_accessor :spec, :runner, :profile + + def initialize + require_relative "../command" + require_relative "cargo_builder/link_flag_converter" + + @runner = self.class.method(:run) + @profile = :release + end + + def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + require "tempfile" + require "fileutils" + + # 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) + + # 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 + + def build_env + build_env = rb_config_env + build_env["RUBY_STATIC"] = "true" if ruby_static? && ENV.key?("RUBY_STATIC") + cfg = "--cfg=rb_sys_gem --cfg=rubygems --cfg=rubygems_#{Gem::VERSION.tr(".", "_")}" + build_env["RUSTFLAGS"] = [ENV["RUSTFLAGS"], cfg].compact.join(" ") + build_env + end + + 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", 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, 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, crate_name) + [ + *linker_args, + *mkmf_libpath, + *rustc_dynamic_linker_flags(dest_dir, crate_name), + *rustc_lib_flags(dest_dir), + *platform_specific_rustc_args(dest_dir), + ] + end + + def platform_specific_rustc_args(dest_dir, flags = []) + if mingw_target? + # On mingw platforms, mkmf adds libruby to the linker flags + flags += libruby_args(dest_dir) + + # Make sure ALSR is used on mingw + # see https://github.com/rust-lang/rust/pull/75406/files + flags += ["-C", "link-arg=-Wl,--dynamicbase"] + flags += ["-C", "link-arg=-Wl,--disable-auto-image-base"] + + # If the gem is installed on a host with build tools installed, but is + # run on one that isn't the missing libraries will cause the extension + # to fail on start. + flags += ["-C", "link-arg=-static-libgcc"] + elsif darwin_target? + # Ventura does not always have this flag enabled + flags += ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"] + end + + flags + end + + # 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")) + linker = cc_flag.shift + link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] } + + return mswin_link_args if linker == "cl" + + ["-C", "linker=#{linker}", *link_args] + end + + def mswin_link_args + args = [] + args += ["-l", makefile_config("LIBRUBYARG_SHARED").chomp(".lib")] + args += split_flags("LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] } + args += split_flags("LOCAL_LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] } + args + end + + def libruby_args(dest_dir) + libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED") + raw_libs = Shellwords.split(libs) + raw_libs.flat_map {|l| ldflag_to_link_modifier(l) } + end + + def ruby_static? + return true if %w[1 true].include?(ENV["RUBY_STATIC"]) + + makefile_config("ENABLE_SHARED") == "no" + end + + def cargo_dylib_path(dest_path, crate_name) + 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 cargo_crate_name(cargo_dir, manifest_path, results) + require "open3" + Gem.load_yaml + + 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 + + unless status.success? + if Gem.configuration.really_verbose + puts output + else + results << output + end + + exit_reason = + if status.exited? + ", exit code #{status.exitstatus}" + elsif status.signaled? + ", uncaught signal #{status.termsig}" + end + + raise Gem::InstallError, "cargo metadata failed#{exit_reason}" + end + + # cargo metadata output is specified as json, but with the + # --format-version 1 option the output is compatible with YAML, so we can + # avoid the json dependency + metadata = Gem::SafeYAML.safe_load(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 normalize_path(path) + return path unless File::ALT_SEPARATOR + + path.tr(File::ALT_SEPARATOR, File::SEPARATOR) + end + + def rustc_dynamic_linker_flags(dest_dir, crate_name) + split_flags("DLDFLAGS"). + map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }. + compact. + flat_map {|arg| ldflag_to_link_modifier(arg) } + end + + def rustc_lib_flags(dest_dir) + split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) } + end + + def split_flags(var) + Shellwords.split(RbConfig::CONFIG.fetch(var, "")) + end + + def ldflag_to_link_modifier(arg) + LinkFlagConverter.convert(arg) + end + + def msvc_target? + makefile_config("target_os").include?("msvc") + end + + def darwin_target? + makefile_config("target_os").include?("darwin") + end + + def mingw_target? + makefile_config("target_os").include?("mingw") + end + + def win_target? + target_platform = RbConfig::CONFIG["target_os"] + !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r } + end + + # Interpolate substitution vars in the arg (i.e. $(DEFFILE)) + def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name) + var_matches = input_arg.match(/\$\((\w+)\)/) + + return input_arg unless var_matches + + var_name = var_matches[1] + + return input_arg if var_name.nil? || var_name.chomp.empty? + + case var_name + # On windows, it is assumed that mkmf has setup an exports file for the + # extension, so we have to create one ourselves. + when "DEFFILE" + write_deffile(dest_dir, crate_name) + else + RbConfig::CONFIG[var_name] + end + end + + 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_#{crate_name}" + end + + deffile_path + end + + # We have to basically reimplement <code>RbConfig::CONFIG['SOEXT']</code> 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")}"] + end + + def makefile_config(var_name) + val = RbConfig::MAKEFILE_CONFIG[var_name] + + return unless val + + RbConfig.expand(val.dup) + end + + # Error raised when no cdylib artifact was created + class DylibNotFoundError < StandardError + def initialize(dir) + files = Dir.glob(File.join(dir, "**", "*")).map {|f| "- #{f}" }.join "\n" + + super <<~MSG + Dynamic library not found for Rust extension (in #{dir}) + + Make sure you set "crate-type" in Cargo.toml to "cdylib" + + Found files: + #{files} + MSG + end + end +end diff --git a/lib/rubygems/ext/cargo_builder/link_flag_converter.rb b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb new file mode 100644 index 0000000000..e4d196cb10 --- /dev/null +++ b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class Gem::Ext::CargoBuilder < Gem::Ext::Builder + # Converts Ruby link flags into something cargo understands + class LinkFlagConverter + FILTERED_PATTERNS = [ + /compress-debug-sections/, # Not supported by all linkers, and not required for Rust + ].freeze + + def self.convert(arg) + return [] if FILTERED_PATTERNS.any? {|p| p.match?(arg) } + + case arg.chomp + when /^-L\s*(.+)$/ + ["-L", "native=#{$1}"] + when /^--library=(\w+\S+)$/, /^-l\s*(\w+\S+)$/ + ["-l", $1] + when /^-l\s*([^:\s])+/ # -lfoo, but not -l:libfoo.a + ["-l", $1] + when /^-F\s*(.*)$/ + ["-l", "framework=#{$1}"] + else + ["-C", "link-args=#{arg}"] + end + end + end +end diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb index 269e876cfa..b162664784 100644 --- a/lib/rubygems/ext/cmake_builder.rb +++ b/lib/rubygems/ext/cmake_builder.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true -require_relative '../command' 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')) + unless File.exist?(File.join(cmake_dir, "Makefile")) + require_relative "../command" cmd = ["cmake", ".", "-DCMAKE_INSTALL_PREFIX=#{dest_path}", *Gem::Command.build_args] run cmd, results, class_name, cmake_dir diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index eb2f9fce61..6b8590ba2e 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. @@ -7,7 +8,7 @@ class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args=[], lib_dir=nil, configure_dir=Dir.pwd) - unless File.exist?(File.join(configure_dir, 'Makefile')) + unless File.exist?(File.join(configure_dir, "Makefile")) cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args] run cmd, results, class_name, configure_dir diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index fede270417..fb68a7a8cc 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -1,88 +1,63 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'shellwords' - class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder def self.build(extension, dest_path, results, args=[], lib_dir=nil, extension_dir=Dir.pwd) - require 'fileutils' - require 'tempfile' + require "fileutils" + require "tempfile" tmp_dest = Dir.mktmpdir(".gem.", extension_dir) # Some versions of `mktmpdir` return absolute paths, which will break make - # if the paths contain spaces. However, on Ruby 1.9.x on Windows, relative - # paths cause all C extension builds to fail. + # if the paths contain spaces. # - # As such, we convert to a relative path unless we are using Ruby 1.9.x on - # Windows. This means that when using Ruby 1.9.x on Windows, paths with - # spaces do not work. - # - # Details: https://github.com/rubygems/rubygems/issues/977#issuecomment-171544940 - tmp_dest = get_relative_path(tmp_dest, extension_dir) - - Tempfile.open %w[siteconf .rb], extension_dir do |siteconf| - siteconf.puts "require 'rbconfig'" - siteconf.puts "dest_path = #{tmp_dest.dump}" - %w[sitearchdir sitelibdir].each do |dir| - siteconf.puts "RbConfig::MAKEFILE_CONFIG['#{dir}'] = dest_path" - siteconf.puts "RbConfig::CONFIG['#{dir}'] = dest_path" - end - - siteconf.close - - destdir = ENV["DESTDIR"] - - begin - # workaround for https://github.com/oracle/truffleruby/issues/2115 - siteconf_path = RUBY_ENGINE == "truffleruby" ? siteconf.path.dup : siteconf.path - cmd = Gem.ruby.shellsplit << "-I" << File.expand_path("../../..", __FILE__) << - "-r" << get_relative_path(siteconf_path, extension_dir) << File.basename(extension) - cmd.push(*args) - - begin - run(cmd, results, class_name, extension_dir) do |s, r| - mkmf_log = File.join(extension_dir, 'mkmf.log') - if File.exist? mkmf_log - unless s.success? - r << "To see why this extension failed to compile, please check" \ - " the mkmf.log which can be found here:\n" - r << " " + File.join(dest_path, 'mkmf.log') + "\n" - end - FileUtils.mv mkmf_log, dest_path - end + # As such, we convert to a relative path. + tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir) + + destdir = ENV["DESTDIR"] + + begin + cmd = ruby << File.basename(extension) + cmd.push(*args) + + run(cmd, results, class_name, extension_dir) do |s, r| + mkmf_log = File.join(extension_dir, "mkmf.log") + if File.exist? mkmf_log + unless s.success? + r << "To see why this extension failed to compile, please check" \ + " the mkmf.log which can be found here:\n" + r << " " + File.join(dest_path, "mkmf.log") + "\n" end - siteconf.unlink + FileUtils.mv mkmf_log, dest_path end + end - ENV["DESTDIR"] = nil + ENV["DESTDIR"] = nil - make dest_path, results, extension_dir + make dest_path, results, extension_dir, tmp_dest_relative - if tmp_dest - full_tmp_dest = File.join(extension_dir, tmp_dest) + full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - # TODO remove in RubyGems 3 - if Gem.install_extension_in_lib and lib_dir - 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 - end + if Gem.install_extension_in_lib && lib_dir + 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 + end - FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| - destent = ent.class.new(dest_path, ent.rel) - destent.exist? or FileUtils.mv(ent.path, destent.path) - end - end - ensure - ENV["DESTDIR"] = destdir - siteconf.close! + 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 + + make dest_path, results, extension_dir, tmp_dest_relative, ["clean"] + ensure + ENV["DESTDIR"] = destdir end results @@ -90,10 +65,8 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder 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[0..base.length - 1] = "." if path.start_with?(base) path end end diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 64a6c0eb80..0171807b39 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -1,27 +1,28 @@ # frozen_string_literal: true + +require_relative "../shellwords" + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require "shellwords" - 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 + if /mkrf_conf/i.match?(File.basename(extension)) run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir) end - rake = ENV['rake'] + rake = ENV["rake"] if rake - rake = rake.shellsplit + rake = Shellwords.split(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'] + rake = [Gem.default_exec_format % "rake"] end end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index a36674503e..8335a0ad03 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -1,18 +1,14 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems' -require 'rubygems/command_manager' -require 'rubygems/deprecate' - -## -# Load additional plugins from $LOAD_PATH - -Gem.load_env_plugins rescue nil +require_relative "../rubygems" +require_relative "command_manager" +require_relative "deprecate" ## # Run an instance of the gem program. @@ -37,16 +33,23 @@ class Gem::GemRunner do_configuration args + begin + Gem.load_env_plugins + rescue StandardError + nil + end + Gem.load_plugins + cmd = @command_manager_class.instance cmd.command_names.each do |command_name| config_args = Gem.configuration[command_name] config_args = case config_args when String - config_args.split ' ' + config_args.split " " else Array(config_args) - end + end Gem::Command.add_specific_extra_args command_name, config_args end @@ -58,7 +61,7 @@ class Gem::GemRunner # other arguments in the list. def extract_build_args(args) # :nodoc: - return [] unless offset = args.index('--') + return [] unless offset = args.index("--") build_args = args.slice!(offset...args.length) @@ -75,5 +78,3 @@ class Gem::GemRunner Gem::Command.extra_args = Gem.configuration[:gem] end end - -Gem.load_plugins diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index 3687e776e2..a8361b7ff1 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true -require 'rubygems/remote_fetcher' -require 'rubygems/text' + +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 @@ -19,8 +22,8 @@ module Gem::GemcutterUtilities # Add the --key option def add_key_option - add_option('-k', '--key KEYNAME', Symbol, - 'Use the given API key', + add_option("-k", "--key KEYNAME", Symbol, + "Use the given API key", "from #{Gem.configuration.credentials_path}") do |value,options| options[:key] = value end @@ -30,8 +33,9 @@ module Gem::GemcutterUtilities # Add the --otp option def add_otp_option - add_option('--otp CODE', - 'Digit code for multifactor authentication') do |value, options| + add_option("--otp CODE", + "Digit code for multifactor authentication", + "You can also use the environment variable GEM_HOST_OTP_CODE") do |value, options| options[:otp] = value end end @@ -52,6 +56,13 @@ module Gem::GemcutterUtilities end ## + # The OTP code from the command options or from the user's configuration. + + def otp + options[:otp] || ENV["GEM_HOST_OTP_CODE"] + end + + ## # The host to connect to either from the RUBYGEMS_HOST environment variable # or from the user's configuration @@ -61,9 +72,8 @@ module Gem::GemcutterUtilities @host ||= begin - env_rubygems_host = ENV['RUBYGEMS_HOST'] - env_rubygems_host = nil if - env_rubygems_host and env_rubygems_host.empty? + env_rubygems_host = ENV["RUBYGEMS_HOST"] + env_rubygems_host = nil if env_rubygems_host&.empty? env_rubygems_host || configured_host end @@ -74,8 +84,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 @@ -84,8 +94,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}" @@ -93,11 +103,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 @@ -110,27 +120,27 @@ 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"] = options[:otp] if options[:otp] - request.body = URI.encode_www_form({:api_key => api_key }.merge(update_scope_params)) + request.basic_auth identifier, password + request["OTP"] = otp if otp + 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 @@ -140,27 +150,34 @@ module Gem::GemcutterUtilities # key. def sign_in(sign_in_host = nil, scope: nil) - sign_in_host ||= self.host + sign_in_host ||= host return if api_key pretty_host = pretty_host(sign_in_host) 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(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"] = options[:otp] if options[:otp] - request.body = URI.encode_www_form({ name: key_name }.merge(scope_params)) + sign_in_host, credentials: credentials, scope: scope) do |request| + request.basic_auth identifier, password + request["OTP"] = otp if otp + request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params)) end with_response response do |resp| @@ -187,16 +204,23 @@ module Gem::GemcutterUtilities # block was given or shows the response body to the user. # # If the response was not successful, shows an error to the user including - # the +error_prefix+ and the response body. + # the +error_prefix+ and the response body. If the response was a permanent redirect, + # shows an error to the user including the redirect location. 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 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) + terminate_interaction(ERROR_CODE) else message = response.body message = "#{error_prefix}: #{message}" if error_prefix @@ -211,7 +235,7 @@ module Gem::GemcutterUtilities # +response+ text and no otp provided by options. def set_api_key(host, key) - if host == Gem::DEFAULT_HOST + if default_host? Gem.configuration.rubygems_api_key = key else Gem.configuration.set_api_key host, key @@ -221,37 +245,96 @@ 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"] = options[:otp] if options[:otp] + req["OTP"] = otp if otp block.call(req) end end - def ask_otp - say 'You have enabled multi-factor authentication. Please enter OTP code.' - options[:otp] = ask 'Code: ' + 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 #{url_with_port} 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." + + 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 + + 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 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) - if Gem::DEFAULT_HOST == host - 'RubyGems.org' + if default_host? + "RubyGems.org" else host end end def get_scope_params(scope) - scope_params = {} + scope_params = { index_rubygems: 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 "#{scope} [y/N]: " - scope_params[scope] = true if selected =~ /^[yY](es)?$/ + 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 @@ -259,6 +342,32 @@ module Gem::GemcutterUtilities scope_params end + def default_host? + host == Gem::DEFAULT_HOST + end + + 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 identifier, password + end + + with_response response do |resp| + Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body)) + end + end + + def get_mfa_params(profile) + mfa_level = profile["mfa"] + params = {} + 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 + params + end + def get_key_name(scope) hostname = Socket.gethostname || "unknown-host" user = ENV["USER"] || ENV["USERNAME"] || "unknown-user" @@ -274,6 +383,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..abf65efe37 --- /dev/null +++ b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb @@ -0,0 +1,105 @@ +# 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) + require "cgi" + + return if uri.query.nil? + CGI.parse(uri.query).dig("code", 0) + 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..0fdd1d5bf4 --- /dev/null +++ b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb @@ -0,0 +1,78 @@ +# 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 + else + request.basic_auth credentials[:email], credentials[:password] + 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 e595459c87..0000000000 --- a/lib/rubygems/indexer.rb +++ /dev/null @@ -1,425 +0,0 @@ -# frozen_string_literal: true -require 'rubygems' -require 'rubygems/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? or 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 and not 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 - 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? or 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 index f68fd2fd04..0640eaaf08 100644 --- a/lib/rubygems/install_default_message.rb +++ b/lib/rubygems/install_default_message.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'rubygems' -require 'rubygems/user_interaction' + +require_relative "../rubygems" +require_relative "user_interaction" ## # A post-install hook that displays "Successfully installed diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb index 3c13888a84..a24e26b918 100644 --- a/lib/rubygems/install_message.rb +++ b/lib/rubygems/install_message.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'rubygems' -require 'rubygems/user_interaction' + +require_relative "../rubygems" +require_relative "user_interaction" ## # A default post-install hook that displays "Successfully installed diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 54a3950b64..aad207a718 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems' -require 'rubygems/security_option' +require_relative "../rubygems" +require_relative "security_option" ## # Mixin methods for install and update options for Gem::Commands @@ -18,106 +19,106 @@ module Gem::InstallUpdateOptions # Add the install/update options to the option parser. def add_install_update_options - add_option(:"Install/Update", '-i', '--install-dir DIR', - 'Gem repository directory to get installed', - 'gems') do |value, options| + add_option(:"Install/Update", "-i", "--install-dir DIR", + "Gem repository directory to get installed", + "gems") do |value, options| options[:install_dir] = File.expand_path(value) end - add_option(:"Install/Update", '-n', '--bindir DIR', - 'Directory where executables will be', - 'placed when the gem is installed') do |value, options| + add_option(:"Install/Update", "-n", "--bindir DIR", + "Directory where executables will be", + "placed when the gem is installed") do |value, options| options[:bin_dir] = File.expand_path(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| + 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 - end + else value + end end - add_option(:"Install/Update", '--build-root DIR', - 'Temporary installation root. Useful for building', - 'packages. Do not use this when installing remote gems.') do |value, options| + add_option(:"Install/Update", "--build-root DIR", + "Temporary installation root. Useful for building", + "packages. Do not use this when installing remote gems.") do |value, options| options[:build_root] = File.expand_path(value) end - add_option(:"Install/Update", '--vendor', - 'Install gem into the vendor directory.', - 'Only for use by gem repackagers.') do |value, options| + add_option(:"Install/Update", "--vendor", + "Install gem into the vendor directory.", + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir - raise OptionParser::InvalidOption.new 'your platform is not supported' + raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end options[:vendor] = true options[:install_dir] = Gem.vendor_dir end - add_option(:"Install/Update", '-N', '--no-document', - 'Disable documentation generation') do |value, options| + add_option(:"Install/Update", "-N", "--no-document", + "Disable documentation generation") do |_value, options| options[:document] = [] end - add_option(:"Install/Update", '-E', '--[no-]env-shebang', + add_option(:"Install/Update", "-E", "--[no-]env-shebang", "Rewrite the shebang line on installed", "scripts to use /usr/bin/env") do |value, options| options[:env_shebang] = value end - add_option(:"Install/Update", '-f', '--[no-]force', - 'Force gem to install, bypassing dependency', - 'checks') do |value, options| + add_option(:"Install/Update", "-f", "--[no-]force", + "Force gem to install, bypassing dependency", + "checks") do |value, options| options[:force] = value end - add_option(:"Install/Update", '-w', '--[no-]wrappers', - 'Use bin wrappers for executables', - 'Not available on dosish platforms') do |value, options| + add_option(:"Install/Update", "-w", "--[no-]wrappers", + "Use bin wrappers for executables", + "Not available on dosish platforms") do |value, options| options[:wrappers] = value end add_security_option - add_option(:"Install/Update", '--ignore-dependencies', - 'Do not install any required dependent gems') do |value, options| + add_option(:"Install/Update", "--ignore-dependencies", + "Do not install any required dependent gems") do |value, options| options[:ignore_dependencies] = value end - add_option(:"Install/Update", '--[no-]format-executable', - 'Make installed executable names match Ruby.', - 'If Ruby is ruby18, foo_exec will be', - 'foo_exec18') do |value, options| + add_option(:"Install/Update", "--[no-]format-executable", + "Make installed executable names match Ruby.", + "If Ruby is ruby18, foo_exec will be", + "foo_exec18") do |value, options| options[:format_executable] = value end - add_option(:"Install/Update", '--[no-]user-install', - 'Install in user\'s home directory instead', - 'of GEM_HOME.') do |value, options| + add_option(:"Install/Update", "--[no-]user-install", + "Install in user's home directory instead", + "of GEM_HOME.") do |value, options| options[:user_install] = value end 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 @@ -133,49 +134,49 @@ module Gem::InstallUpdateOptions options[:post_install_message] = value end - 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| + 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| 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 OptionParser::InvalidArgument, + raise Gem::OptionParser::InvalidArgument, "cannot find gem dependencies file #{message}" end options[:gemdeps] = v end - 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 } + 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(&:intern) end - add_option(:"Install/Update", '--default', - 'Add the gem\'s full specification to', - 'specifications/default and extract only its bin') do |v,o| + add_option(:"Install/Update", "--default", + "Add the gem's full specification to", + "specifications/default and extract only its bin") do |v,_o| options[:install_as_default] = v end - add_option(:"Install/Update", '--explain', - 'Rather than install the gems, indicate which would', - 'be installed') do |v,o| + add_option(:"Install/Update", "--explain", + "Rather than install the gems, indicate which would", + "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| + add_option(:"Install/Update", "--[no-]lock", + "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| + add_option(:"Install/Update", "--[no-]suggestions", + "Suggest alternates when gems are not found") do |v,_o| options[:suggest_alternate] = v end end @@ -185,7 +186,7 @@ module Gem::InstallUpdateOptions def install_update_options { - :document => %w[ri], + document: %w[ri], } end @@ -193,7 +194,6 @@ module Gem::InstallUpdateOptions # Default description for the gem install and update commands. def install_update_defaults_str - '--document=ri' + "--document=ri" end - end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 7af51056b7..8f6f9a5aa8 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/command' -require 'rubygems/installer_uninstaller_utils' -require 'rubygems/exceptions' -require 'rubygems/deprecate' -require 'rubygems/package' -require 'rubygems/ext' -require 'rubygems/user_interaction' +require_relative "installer_uninstaller_utils" +require_relative "exceptions" +require_relative "deprecate" +require_relative "package" +require_relative "ext" +require_relative "user_interaction" ## # The installer installs the files contained in the .gem into the Gem.home. @@ -68,19 +68,28 @@ class Gem::Installer @path_warning = false - @install_lock = Mutex.new - class << self - ## - # True if we've warned about PATH not including Gem.bindir + # + # 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. - attr_accessor :path_warning + def inherited(klass) + if klass.name == "Bundler::RubyGemsGemInstaller" + require "rubygems/command" + end + + super(klass) + end ## - # Certain aspects of the install process are not thread-safe. This lock is - # used to allow multiple threads to install Gems at the same time. + # True if we've warned about PATH not including Gem.bindir - attr_reader :install_lock + attr_accessor :path_warning ## # Overrides the executable format. @@ -117,14 +126,14 @@ class Gem::Installer @spec = spec end - def extract_files(destination_dir, pattern = '*') + def extract_files(destination_dir, pattern = "*") FileUtils.mkdir_p destination_dir spec.files.each do |file| file = File.join destination_dir, file next if File.exist? file FileUtils.mkdir_p File.dirname(file) - File.open file, 'w' do |fp| + File.open file, "w" do |fp| fp.puts "# #{file}" end end @@ -169,7 +178,7 @@ class Gem::Installer # :post_install_message:: Print gem post install message if true def initialize(package, options={}) - require 'fileutils' + require "fileutils" @options = options @package = package @@ -180,10 +189,12 @@ class Gem::Installer @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) + 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 end end @@ -193,8 +204,8 @@ class Gem::Installer # # If +@force+ is set +filename+ is overwritten. # - # If +filename+ exists and is a RubyGems wrapper for different gem the user - # is consulted. + # If +filename+ exists and it is a RubyGems wrapper for a different gem, then + # the user is consulted. # # If +filename+ exists and +@bin_dir+ is Gem.default_bindir (/usr/local) the # user is consulted. @@ -211,34 +222,45 @@ class Gem::Installer ruby_executable = false existing = nil - File.open generated_bin, 'rb' do |io| - next unless io.gets =~ /^#!/ # shebang + File.open generated_bin, "rb" do |io| + line = io.gets + shebang = /^#!.*ruby/o + + # 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&.match?(shebang) + io.gets # blankline - # TODO detect a specially formatted comment instead of trying - # to run a regexp against Ruby code. - next unless io.gets =~ /This file was generated by RubyGems/ + # TODO: detect a specially formatted comment instead of trying + # to find a string inside Ruby code. + 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\( | load \s Gem\.activate_bin_path\( ) (['"])(.*?)(\2), - }x, 3) + /x, 3) end return if spec.name == existing # somebody has written to RubyGems' directory, overwrite, too bad - return if Gem.default_bindir != @bin_dir and not ruby_executable + return if Gem.default_bindir != @bin_dir && !ruby_executable question = "#{spec.name}'s executable \"#{filename}\" conflicts with ".dup if ruby_executable - question << (existing || 'an unknown executable') + question << (existing || "an unknown executable") return if ask_yes_no "#{question}\nOverwrite the executable?", false @@ -267,8 +289,6 @@ class Gem::Installer def spec @package.spec - rescue Gem::Package::Error => e - raise Gem::InstallError, "invalid gem: #{e.message}" end ## @@ -285,8 +305,6 @@ class Gem::Installer def install pre_install_checks - FileUtils.rm_f File.join gem_home, 'specifications', spec.spec_name - run_pre_install_hooks # Set loaded_from to ensure extension_dir is correct @@ -301,7 +319,7 @@ class Gem::Installer 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 @@ -326,39 +344,37 @@ class Gem::Installer say spec.post_install_message if options[:post_install_message] && !spec.post_install_message.nil? - Gem::Installer.install_lock.synchronize { Gem::Specification.reset } + Gem::Specification.add_spec(spec) + + load_plugin 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 @@ -374,11 +390,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 @@ -404,10 +420,10 @@ class Gem::Installer # True if the gems in the system satisfy +dependency+. def installation_satisfies_dependency?(dependency) - return true if @options[:development] and dependency.type == :development + return true if @options[:development] && dependency.type == :development return true if installed_specs.detect {|s| dependency.matches_spec? s } return false if @only_install_dir - not dependency.matching_specs.empty? + !dependency.matching_specs.empty? end ## @@ -440,23 +456,20 @@ class Gem::Installer # specifications directory. def write_spec - File.open spec_file, 'w' do |file| - spec.installed_by_version = Gem.rubygems_version - - file.puts spec.to_ruby_for_cache + spec.installed_by_version = Gem.rubygems_version - file.fsync rescue nil # for filesystems without fsync(2) - end + Gem.write_binary(spec_file, spec.to_ruby_for_cache) end ## # 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 - File.open(default_spec_file, "w") do |file| - file.puts spec.to_ruby - end + Gem.write_binary(default_spec_file, spec.to_ruby) end ## @@ -466,7 +479,7 @@ class Gem::Installer if Gem.win_platform? script_name = formatted_program_filename(filename) + ".bat" script_path = File.join bindir, File.basename(script_name) - File.open script_path, 'w' do |file| + File.open script_path, "w" do |file| file.puts windows_stub_script(bindir, filename) end @@ -475,28 +488,19 @@ class Gem::Installer end def generate_bin # :nodoc: - return if spec.executables.nil? or spec.executables.empty? + return if spec.executables.nil? || spec.executables.empty? ensure_writable_dir @bin_dir spec.executables.each do |filename| - filename.tap(&Gem::UNTAINT) bin_path = File.join gem_dir, spec.bindir, filename - - unless File.exist? bin_path - if File.symlink? bin_path - alert_warning "`#{bin_path}` is dangling symlink pointing to `#{File.readlink bin_path}`" - else - alert_warning "`#{bin_path}` does not exist, maybe `gem pristine #{spec.name}` will fix it?" - end - next - end + 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' + require "fileutils" FileUtils.chmod dir_mode, bin_path end @@ -511,7 +515,7 @@ class Gem::Installer end def generate_plugins # :nodoc: - latest = Gem::Installer.install_lock.synchronize { Gem::Specification.latest_spec_for(spec.name) } + latest = Gem::Specification.latest_spec_for(spec.name) return if latest && latest.version > spec.version ensure_writable_dir @plugins_dir @@ -521,6 +525,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 +534,17 @@ 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' + 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.open bin_script_path, "wb", 0o755 do |file| file.print app_script_text(filename) - file.chmod(options[:prog_mode] || 0755) + file.chmod(options[:prog_mode] || 0o755) end verbose bin_script_path @@ -557,13 +563,13 @@ class Gem::Installer if File.exist? dst if File.symlink? dst link = File.readlink(dst).split File::SEPARATOR - cur_version = Gem::Version.create(link[-3].sub(/^.*-/, '')) + cur_version = Gem::Version.create(link[-3].sub(/^.*-/, "")) return if spec.version < cur_version end 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 @@ -585,9 +591,8 @@ class Gem::Installer # def shebang(bin_file_name) - ruby_name = RbConfig::CONFIG['ruby_install_name'] if @env_shebang 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. @@ -598,7 +603,7 @@ class Gem::Installer if which = Gem.configuration[:custom_shebang] # replace bin_file_name with "ruby" to avoid endless loops - which = which.gsub(/ #{bin_file_name}$/," #{RbConfig::CONFIG['ruby_install_name']}") + which = which.gsub(/ #{bin_file_name}$/," #{ruby_install_name}") which = which.gsub(/\$(\w+)/) do case $1 @@ -614,14 +619,12 @@ class Gem::Installer end "#!#{which}" - elsif not ruby_name - "#!#{Gem.ruby}#{opts}" - elsif opts - "#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}" - else + elsif @env_shebang # Create a plain shebang line. @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path } - "#!#{@env_path} #{ruby_name}" + "#!#{@env_path} #{ruby_install_name}" + else + "#{bash_prolog_script}#!#{Gem.ruby}#{opts}" end end @@ -631,7 +634,6 @@ class Gem::Installer def ensure_loadable_spec ruby = spec.to_ruby_for_cache - ruby.tap(&Gem::UNTAINT) begin eval ruby @@ -652,36 +654,41 @@ 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] + @bin_dir = options[:bin_dir] + @development = options[:development] + @build_root = options[:build_root] + + @build_args = options[:build_args] + + @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 = options[:bin_dir] || Gem.bindir(gem_home) - @development = options[:development] - @build_root = options[:build_root] + @bin_dir ||= Gem.bindir(@gem_home) - @build_args = options[:build_args] || Gem::Command.build_args + @plugins_dir = Gem.plugindir(@gem_home) unless @build_root.nil? - @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, '')) - @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, '')) - @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, '')) + @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, "")) + @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, "")) + @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, "")) alert_warning "You build with buildroot.\n Build root: #{@build_root}\n Bin dir: #{@bin_dir}\n Gem home: #{@gem_home}\n Plugins dir: #{@plugins_dir}" end end @@ -692,7 +699,7 @@ class Gem::Installer 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 - path = ENV['PATH'] + path = ENV["PATH"] path = path.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR if Gem.win_platform? @@ -703,7 +710,7 @@ class Gem::Installer path = path.split(File::PATH_SEPARATOR) unless path.include? user_bin_dir - unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV['HOME'], '~')) + 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 end @@ -711,24 +718,27 @@ class Gem::Installer 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 - if spec.raw_require_paths.any?{|path| path =~ /\R/ } + if spec.raw_require_paths.any? {|path| path =~ /\R/ } raise Gem::InstallError, "#{spec} has an invalid require_paths" end - if spec.extensions.any?{|ext| ext =~ /\R/ } + if spec.extensions.any? {|ext| ext =~ /\R/ } raise Gem::InstallError, "#{spec} has an invalid extensions" end - unless spec.specification_version.to_s =~ /\A\d+\z/ + if /\R/.match?(spec.platform.to_s) + raise Gem::InstallError, "#{spec.platform} is an invalid platform" + end + + unless /\A\d+\z/.match?(spec.specification_version.to_s) raise Gem::InstallError, "#{spec} has an invalid specification_version" end @@ -745,9 +755,9 @@ class Gem::Installer # 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 + <<-TEXT #{shebang bin_file_name} # # This file was generated by RubyGems. @@ -757,14 +767,14 @@ class Gem::Installer # 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) - version = str + #{explicit_version_requirement(spec.name)} ARGV.shift end end @@ -778,18 +788,36 @@ end TEXT end + def gemdeps_load(name) + return "" if name == "bundler" + + <<-TEXT + +Gem.use_gemdeps +TEXT + end + + def explicit_version_requirement(name) + code = "version = str" + return code unless name == "bundler" + + code += <<-TEXT + + ENV['BUNDLER_VERSION'] = str +TEXT + end + ## # return the stub script text used to launch the true Ruby script def windows_stub_script(bindir, bin_file_name) - rb_config = RbConfig::CONFIG 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 @@ -797,7 +825,7 @@ TEXT TEXT elsif bindir.downcase.start_with? rb_topdir.downcase # stub within ruby folder, but not standard bin. Portable - require 'pathname' + require "pathname" from = Pathname.new bindir to = Pathname.new "#{rb_topdir}/bin" rel = to.relative_path_from from @@ -819,7 +847,7 @@ TEXT # configure scripts and rakefiles or mkrf_conf files. def build_extensions - builder = Gem::Ext::Builder.new spec, @build_args + builder = Gem::Ext::Builder.new spec, build_args builder.build_extensions end @@ -906,17 +934,17 @@ TEXT # extensions. def write_build_info_file - return if @build_args.empty? + return if build_args.empty? - build_info_dir = File.join gem_home, 'build_info' + 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" - File.open build_info_file, 'w' do |io| - @build_args.each do |arg| + File.open build_info_file, "w" do |io| + build_args.each do |arg| io.puts arg end end @@ -928,17 +956,85 @@ TEXT # Writes the .gem file to the cache directory def write_cache_file - cache_file = File.join gem_home, 'cache', spec.file_name + cache_file = File.join gem_home, "cache", spec.file_name @package.copy_to cache_file end def ensure_writable_dir(dir) # :nodoc: begin - Dir.mkdir dir, *[options[:dir_mode] && 0755].compact + Dir.mkdir dir, *[options[:dir_mode] && 0o755].compact rescue SystemCallError raise unless File.directory? dir end 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" + Gem::Command.build_args + end + end + + def rb_config + RbConfig::CONFIG + end + + def ruby_install_name + rb_config["ruby_install_name"] + end + + def load_relative_enabled? + rb_config["LIBRUBY_RELATIVE"] == "yes" + end + + def bash_prolog_script + if load_relative_enabled? + script = +<<~EOS + bindir="${0%/*}" + EOS + + script << %(exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n) + + <<~EOS + #!/bin/sh + # -*- ruby -*- + _=_\\ + =begin + #{script.chomp} + =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.map do |plugin| + File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}") + end + Gem.load_plugin_files(plugin_files) + end end diff --git a/lib/rubygems/installer_test_case.rb b/lib/rubygems/installer_test_case.rb deleted file mode 100644 index 416dac7be6..0000000000 --- a/lib/rubygems/installer_test_case.rb +++ /dev/null @@ -1,247 +0,0 @@ -# frozen_string_literal: true -require 'rubygems/test_case' -require 'rubygems/installer' - -class Gem::Installer - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :bin_dir - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :build_args - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :gem_dir - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :force - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :format - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :gem_home - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :env_shebang - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :ignore_dependencies - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :format_executable - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :security_policy - - ## - # Available through requiring rubygems/installer_test_case - - attr_writer :wrappers -end - -## -# A test case for Gem::Installer. - -class Gem::InstallerTestCase < Gem::TestCase - def setup - super - - Gem::Installer.path_warning = false - end - - ## - # The path where installed executables live - - def util_inst_bindir - File.join @gemhome, "bin" - end - - ## - # Adds an executable named "executable" to +spec+ with the given +shebang+. - # - # The executable is also written to the bin dir in @tmpdir and the installed - # gem directory for +spec+. - - def util_make_exec(spec = @spec, shebang = "#!/usr/bin/ruby", bindir = "bin") - spec.executables = %w[executable] - spec.bindir = bindir - - exec_path = spec.bin_file "executable" - write_file exec_path do |io| - io.puts shebang - end - - bin_path = File.join @tempdir, "bin", "executable" - write_file bin_path do |io| - io.puts shebang - end - end - - ## - # Creates the following instance variables: - # - # @spec:: - # a spec named 'a', intended for regular installs - # - # @gem:: - # the path to a built gem from @spec - # - # And returns a Gem::Installer for the @spec that installs into @gemhome - - def setup_base_installer(force = true) - @gem = setup_base_gem - util_installer @spec, @gemhome, false, force - end - - ## - # Creates the following instance variables: - # - # @spec:: - # a spec named 'a', intended for regular installs - # - # And returns a gem built for the @spec - - def setup_base_gem - @spec = setup_base_spec - util_build_gem @spec - @spec.cache_file - end - - ## - # Sets up a generic specification for testing the rubygems installer - # - # And returns it - - def setup_base_spec - quick_gem 'a' do |spec| - util_make_exec spec - end - end - - ## - # Creates the following instance variables: - # - # @spec:: - # a spec named 'a', intended for regular installs - # @user_spec:: - # a spec named 'b', intended for user installs - # - # @gem:: - # the path to a built gem from @spec - # @user_gem:: - # the path to a built gem from @user_spec - # - # And returns a Gem::Installer for the @user_spec that installs into Gem.user_dir - - def setup_base_user_installer - @user_spec = quick_gem 'b' do |spec| - util_make_exec spec - end - - util_build_gem @user_spec - - @user_gem = @user_spec.cache_file - - util_installer @user_spec, Gem.user_dir, :user - end - - ## - # Sets up the base @gem, builds it and returns an installer for it. - # - def util_setup_installer(&block) - @gem = setup_base_gem - - util_setup_gem(&block) - end - - ## - # Builds the @spec gem and returns an installer for it. The built gem - # includes: - # - # bin/executable - # lib/code.rb - # ext/a/mkrf_conf.rb - - def util_setup_gem(ui = @ui, force = true) - @spec.files << File.join('lib', 'code.rb') - @spec.extensions << File.join('ext', 'a', 'mkrf_conf.rb') - - Dir.chdir @tempdir do - FileUtils.mkdir_p 'bin' - FileUtils.mkdir_p 'lib' - FileUtils.mkdir_p File.join('ext', 'a') - - File.open File.join('bin', 'executable'), 'w' do |f| - f.puts "raise 'ran executable'" - end - - File.open File.join('lib', 'code.rb'), 'w' do |f| - f.puts '1' - end - - File.open File.join('ext', 'a', 'mkrf_conf.rb'), 'w' do |f| - f << <<-EOF - File.open 'Rakefile', 'w' do |rf| rf.puts "task :default" end - EOF - end - - yield @spec if block_given? - - use_ui ui do - FileUtils.rm_f @gem - - @gem = Gem::Package.build @spec - end - end - - Gem::Installer.at @gem, :force => force - end - - ## - # Creates an installer for +spec+ that will install into +gem_home+. If - # +user+ is true a user-install will be performed. - - def util_installer(spec, gem_home, user=false, force=true) - Gem::Installer.at(spec.cache_file, - :install_dir => gem_home, - :user_install => user, - :force => force) - end - - @@symlink_supported = nil - - # This is needed for Windows environment without symlink support enabled (the default - # for non admin) to be able to skip test for features using symlinks. - def symlink_supported? - if @@symlink_supported.nil? - begin - File.symlink("", "") - rescue Errno::ENOENT, Errno::EEXIST - @@symlink_supported = true - rescue NotImplementedError, SystemCallError - @@symlink_supported = false - end - end - @@symlink_supported - end -end diff --git a/lib/rubygems/installer_uninstaller_utils.rb b/lib/rubygems/installer_uninstaller_utils.rb index 2c8b7c635e..c5c2a52bab 100644 --- a/lib/rubygems/installer_uninstaller_utils.rb +++ b/lib/rubygems/installer_uninstaller_utils.rb @@ -4,17 +4,16 @@ # 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? - require 'pathname' + require "pathname" spec.plugins.each do |plugin| plugin_script_path = File.join plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}" - File.open plugin_script_path, 'wb' do |file| + File.open plugin_script_path, "wb" do |file| file.puts "require_relative '#{Pathname.new(plugin).relative_path_from(Pathname.new(plugins_dir))}'" end @@ -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 2d0509eb03..51a61213a5 100644 --- a/lib/rubygems/local_remote_options.rb +++ b/lib/rubygems/local_remote_options.rb @@ -1,32 +1,32 @@ # 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 'rubygems' +require_relative "vendor/uri/lib/uri" +require_relative "../rubygems" ## # Mixin methods for local and remote Gem::Command options. module Gem::LocalRemoteOptions - ## - # Allows OptionParser to handle HTTP URIs. + # Allows Gem::OptionParser to handle HTTP URIs. def accept_uri_http - OptionParser.accept URI::HTTP do |value| + Gem::OptionParser.accept Gem::URI::HTTP do |value| begin - uri = URI.parse value - rescue URI::InvalidURIError - raise OptionParser::InvalidArgument, value + uri = Gem::URI.parse value + rescue Gem::URI::InvalidURIError + raise Gem::OptionParser::InvalidArgument, value end valid_uri_schemes = ["http", "https", "file", "s3"] unless valid_uri_schemes.include?(uri.scheme) - msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map{|s| "#{s}://" }}" + msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map {|s| "#{s}://" }}" raise ArgumentError, msg end @@ -38,18 +38,18 @@ module Gem::LocalRemoteOptions # Add local/remote options to the command line parser. def add_local_remote_options - add_option(:"Local/Remote", '-l', '--local', - 'Restrict operations to the LOCAL domain') do |value, options| + add_option(:"Local/Remote", "-l", "--local", + "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| + add_option(:"Local/Remote", "-r", "--remote", + "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| + add_option(:"Local/Remote", "-b", "--both", + "Allow LOCAL and REMOTE operations") do |_value, options| options[:domain] = :both end @@ -64,10 +64,9 @@ module Gem::LocalRemoteOptions # Add the --bulk-threshold option def add_bulk_threshold_option - add_option(:"Local/Remote", '-B', '--bulk-threshold COUNT', + 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 @@ -76,9 +75,8 @@ module Gem::LocalRemoteOptions # Add the --clear-sources option def add_clear_sources_option - add_option(:"Local/Remote", '--clear-sources', - 'Clear the gem sources') do |value, options| - + add_option(:"Local/Remote", "--clear-sources", + "Clear the gem sources") do |_value, options| Gem.sources = nil options[:sources_cleared] = true end @@ -90,9 +88,9 @@ module Gem::LocalRemoteOptions def add_proxy_option accept_uri_http - add_option(:"Local/Remote", '-p', '--[no-]http-proxy [URL]', URI::HTTP, - 'Use HTTP proxy for remote operations') do |value, options| - options[:http_proxy] = (value == false) ? :no_proxy : value + 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 Gem.configuration[:http_proxy] = options[:http_proxy] end end @@ -103,10 +101,9 @@ module Gem::LocalRemoteOptions def add_source_option accept_uri_http - add_option(:"Local/Remote", '-s', '--source URL', URI::HTTP, - 'Append URL to list of remote gem sources') do |source, options| - - source << '/' if source !~ /\/\z/ + add_option(:"Local/Remote", "-s", "--source URL", Gem::URI::HTTP, + "Append URL to list of remote gem sources") do |source, options| + source << "/" unless source.end_with?("/") if options.delete :sources_cleared Gem.sources = [source] @@ -120,8 +117,8 @@ module Gem::LocalRemoteOptions # Add the --update-sources option def add_update_sources_option - add_option(:Deprecated, '-u', '--[no-]update-sources', - 'Update local source cache') do |value, options| + add_option(:Deprecated, "-u", "--[no-]update-sources", + "Update local source cache") do |value, _options| Gem.configuration.update_sources = value end end @@ -146,5 +143,4 @@ module Gem::LocalRemoteOptions def remote? options[:domain] == :remote || options[:domain] == :both end - end diff --git a/lib/rubygems/mock_gem_ui.rb b/lib/rubygems/mock_gem_ui.rb deleted file mode 100644 index ec244fb7c6..0000000000 --- a/lib/rubygems/mock_gem_ui.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true -require 'rubygems/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 3d0afa3094..3f4a6fcf3d 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 or 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 ## @@ -86,12 +85,11 @@ class Gem::NameTuple "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>" end - alias to_s inspect # :nodoc: + alias_method :to_s, :inspect # :nodoc: def <=>(other) - [@name, @version, @platform == Gem::Platform::RUBY ? -1 : 1] <=> - [other.name, other.version, - other.platform == Gem::Platform::RUBY ? -1 : 1] + [@name, @version, Gem::Platform.sort_priority(@platform)] <=> + [other.name, other.version, Gem::Platform.sort_priority(other.platform)] end include Comparable @@ -103,8 +101,8 @@ class Gem::NameTuple def ==(other) case other when self.class - @name == other.name and - @version == other.version and + @name == other.name && + @version == other.version && @platform == other.platform when Array to_a == other diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index a4ae3e9ea5..1d5d764237 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.rb @@ -1,9 +1,17 @@ # 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 + +require_relative "../rubygems" +require_relative "security" +require_relative "user_interaction" + +## # Example using a Gem::Package # # Builds a .gem file given a Gem::Specification. A .gem file is a tarball @@ -41,10 +49,6 @@ # #files are the files in the .gem tar file, not the Ruby files in the gem # #extract_files and #contents automatically call #verify -require "rubygems" -require 'rubygems/security' -require 'rubygems/user_interaction' - class Gem::Package include Gem::UserInteraction @@ -55,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 @@ -66,8 +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 format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir) end end @@ -139,18 +148,18 @@ class Gem::Package def self.new(gem, security_policy = nil) gem = if gem.is_a?(Gem::Package::Source) - gem - elsif gem.respond_to? :read - Gem::Package::IOSource.new gem - else - Gem::Package::FileSource.new gem - end + gem + elsif gem.respond_to? :read + Gem::Package::IOSource.new gem + else + 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 - return super unless gem.start.include? 'MD5SUM =' + return super unless gem.start.include? "MD5SUM =" Gem::Package::Old.new gem end @@ -170,22 +179,22 @@ class Gem::Package tar = Gem::Package::TarReader.new io tar.each_entry do |entry| case entry.full_name - when 'metadata' then + when "metadata" then metadata = entry.read - when 'metadata.gz' then + when "metadata.gz" then metadata = Gem::Util.gunzip entry.read end end end - return spec, metadata + [spec, metadata] end ## # Creates a new package that will read or write to the file +gem+. def initialize(gem, security_policy) # :notnew: - require 'zlib' + require "zlib" @gem = gem @@ -221,9 +230,9 @@ 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| - YAML.dump checksums_by_algorithm, gz_io + Psych.dump checksums_by_algorithm, gz_io end end end @@ -233,7 +242,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 @@ -241,7 +250,7 @@ class Gem::Package end end - @checksums['data.tar.gz'] = digests + @checksums["data.tar.gz"] = digests end ## @@ -258,8 +267,8 @@ class Gem::Package next unless stat.file? 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? + File.open file, "rb" do |src_io| + copy_stream(src_io, dst_io) end end end @@ -269,13 +278,13 @@ 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 end - @checksums['metadata.gz'] = digests + @checksums["metadata.gz"] = digests end ## @@ -327,7 +336,7 @@ EOM gem_tar = Gem::Package::TarReader.new io gem_tar.each do |entry| - next unless entry.full_name == 'data.tar.gz' + next unless entry.full_name == "data.tar.gz" open_tar_gz entry do |pkg_tar| pkg_tar.each do |contents_entry| @@ -338,6 +347,8 @@ EOM return @contents end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -346,18 +357,21 @@ EOM def digest(entry) # :nodoc: algorithms = if @checksums - @checksums.keys - else - [Gem::Security::DIGEST_NAME].compact - end - - algorithms.each do |algorithm| - digester = Gem::Security.create_digest(algorithm) + @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 - 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 @@ -373,19 +387,21 @@ 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 reader.each do |entry| - next unless entry.full_name == 'data.tar.gz' + next unless entry.full_name == "data.tar.gz" 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 ## @@ -400,46 +416,71 @@ EOM # extracted. def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: - directories = [] if dir_mode + destination_dir = File.realpath(destination_dir) + + directories = [] + symlinks = [] + open_tar_gz io do |tar| tar.each do |entry| - next unless File.fnmatch pattern, entry.full_name, File::FNM_DOTMATCH + full_name = entry.full_name + next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH + + destination = install_location full_name, destination_dir - destination = install_location entry.full_name, destination_dir + if entry.symlink? + link_target = entry.header.linkname + real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination)) + + raise Gem::Package::SymlinkError.new(full_name, real_destination, destination_dir) unless + normalize_path(real_destination).start_with? normalize_path(destination_dir + "/") + + 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 else File.dirname destination end - directories << mkdir if directories - mkdir_p_safe mkdir, mkdir_options, destination_dir, entry.full_name - - File.open destination, 'wb' do |out| - out.write entry.read - FileUtils.chmod file_mode(entry.header.mode), destination - end if entry.file? + unless directories.include?(mkdir) + FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?) + directories << mkdir + end - File.symlink(entry.header.linkname, destination) if entry.symlink? + if entry.file? + File.open(destination, "wb") {|out| copy_stream(entry, out) } + FileUtils.chmod file_mode(entry.header.mode) & ~File.umask, destination + end verbose destination end end - if directories - directories.uniq! + symlinks.each do |name, target, destination, real_destination| + if File.exist?(real_destination) + File.symlink(target, destination) + else + alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring" + end + end + + if dir_mode File.chmod(dir_mode, *directories) end end def file_mode(mode) # :nodoc: - ((mode & 0111).zero? ? data_mode : prog_mode) || 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 + # gets put into an unsigned short). + (mode & ((1 << 16) - 1)) end ## @@ -464,25 +505,14 @@ EOM def install_location(filename, destination_dir) # :nodoc: raise Gem::Package::PathError.new(filename, destination_dir) if - filename.start_with? '/' + filename.start_with? "/" - destination_dir = File.expand_path(File.realpath(destination_dir)) - destination = File.expand_path(File.join(destination_dir, filename)) + destination_dir = File.realpath(destination_dir) + destination = File.expand_path(filename, destination_dir) raise Gem::Package::PathError.new(destination, destination_dir) unless - destination.start_with? destination_dir + '/' + normalize_path(destination).start_with? normalize_path(destination_dir + "/") - begin - real_destination = File.expand_path(File.realpath(destination)) - rescue - # it's fine if the destination doesn't exist, because rm -rf'ing it can't cause any damage - nil - else - raise Gem::Package::PathError.new(real_destination, destination_dir) unless - real_destination.start_with? destination_dir + '/' - end - - destination.tap(&Gem::UNTAINT) destination end @@ -494,30 +524,14 @@ EOM end end - def mkdir_p_safe(mkdir, mkdir_options, destination_dir, file_name) - destination_dir = File.realpath(File.expand_path(destination_dir)) - parts = mkdir.split(File::SEPARATOR) - parts.reduce do |path, basename| - path = File.realpath(path) unless path == "" - path = File.expand_path(path + File::SEPARATOR + basename) - lstat = File.lstat path rescue nil - if !lstat || !lstat.directory? - unless normalize_path(path).start_with? normalize_path(destination_dir) and (FileUtils.mkdir path, **mkdir_options rescue false) - raise Gem::Package::PathError.new(file_name, destination_dir) - end - end - path - end - end - ## # Loads a Gem::Specification from the TarEntry +entry+ def load_spec(entry) # :nodoc: case entry.full_name - when 'metadata' then + when "metadata" then @spec = Gem::Specification.from_yaml entry.read - when 'metadata.gz' then + when "metadata.gz" then Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio| @spec = Gem::Specification.from_yaml gzio.read end @@ -541,7 +555,7 @@ EOM def read_checksums(gem) Gem.load_yaml - @checksums = gem.seek 'checksums.yaml.gz' do |entry| + @checksums = gem.seek "checksums.yaml.gz" do |entry| Zlib::GzipReader.wrap entry do |gz_io| Gem::SafeYAML.safe_load gz_io.read end @@ -553,7 +567,7 @@ EOM # certificate and key are not present only checksum generation is set up. def setup_signer(signer_options: {}) - passphrase = ENV['GEM_PRIVATE_KEY_PASSPHRASE'] + passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] if @spec.signing_key @signer = Gem::Security::Signer.new( @@ -564,10 +578,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 @@ -609,8 +623,7 @@ EOM verify_checksums @digests, @checksums - @security_policy.verify_signatures @spec, @digests, @signatures if - @security_policy + @security_policy&.verify_signatures @spec, @digests, @signatures true rescue Gem::Security::Exception @@ -619,7 +632,7 @@ 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 @@ -660,10 +673,10 @@ EOM case file_name when "metadata", "metadata.gz" then load_spec entry - when 'data.tar.gz' then + when "data.tar.gz" then verify_gz entry end - rescue + rescue StandardError warn "Exception while verifying #{@gem.path}" raise end @@ -677,16 +690,16 @@ EOM end unless @spec - raise Gem::Package::FormatError.new 'package metadata is missing', @gem + raise Gem::Package::FormatError.new "package metadata is missing", @gem end - unless @files.include? 'data.tar.gz' + 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) and 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 @@ -695,19 +708,30 @@ EOM def verify_gz(entry) # :nodoc: Zlib::GzipReader.wrap entry do |gzio| - gzio.read 16384 until gzio.eof? # gzip checksum verification + # TODO: read into a buffer once zlib supports it + gzio.read 16_384 until gzio.eof? # gzip checksum verification end rescue Zlib::GzipFile::Error => e raise Gem::Package::FormatError.new(e.message, entry.full_name) end + + if RUBY_ENGINE == "truffleruby" + def copy_stream(src, dst) # :nodoc: + dst.write src.read + end + else + def copy_stream(src, dst) # :nodoc: + IO.copy_stream(src, dst) + end + end end -require 'rubygems/package/digest_io' -require 'rubygems/package/source' -require 'rubygems/package/file_source' -require 'rubygems/package/io_source' -require 'rubygems/package/old' -require 'rubygems/package/tar_header' -require 'rubygems/package/tar_reader' -require 'rubygems/package/tar_reader/entry' -require 'rubygems/package/tar_writer' +require_relative "package/digest_io" +require_relative "package/source" +require_relative "package/file_source" +require_relative "package/io_source" +require_relative "package/old" +require_relative "package/tar_header" +require_relative "package/tar_reader" +require_relative "package/tar_reader/entry" +require_relative "package/tar_writer" 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 114a950c77..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. @@ -22,10 +23,10 @@ class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all end def with_write_io(&block) - File.open path, 'wb', &block + File.open path, "wb", &block end def with_read_io(&block) - File.open path, 'rb', &block + File.open path, "rb", &block end end diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb index 7d7383110b..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 @@ -32,10 +33,14 @@ class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all def with_read_io yield io + ensure + io.rewind end def with_write_io yield io + ensure + io.rewind end def path diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb index 25317ef23f..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. @@ -19,8 +20,8 @@ class Gem::Package::Old < Gem::Package # cannot be written. def initialize(gem, security_policy) - require 'fileutils' - require 'zlib' + require "fileutils" + require "zlib" Gem.load_yaml @contents = nil @@ -41,7 +42,7 @@ class Gem::Package::Old < Gem::Package read_until_dashes io # spec header = file_list io - @contents = header.map {|file| file['path'] } + @contents = header.map {|file| file["path"] } end end @@ -59,7 +60,7 @@ class Gem::Package::Old < Gem::Package raise Gem::Exception, errstr unless header header.each do |entry| - full_name = entry['path'] + full_name = entry["path"] destination = install_location full_name, destination_dir @@ -69,17 +70,17 @@ 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 - file_data.length != entry['size'].to_i + file_data.length != entry["size"].to_i 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| + File.open destination, "wb", file_mode(entry["mode"]) do |out| out.write file_data end @@ -119,7 +120,7 @@ class Gem::Package::Old < Gem::Package loop do line = io.gets - return if line.chomp == '__END__' + return if line.chomp == "__END__" break unless line end @@ -145,7 +146,7 @@ class Gem::Package::Old < Gem::Package begin @spec = Gem::Specification.from_yaml yaml - rescue YAML::SyntaxError + rescue Psych::SyntaxError raise Gem::Exception, "Failed to parse gem specification out of gem file" end rescue ArgumentError @@ -160,7 +161,7 @@ class Gem::Package::Old < Gem::Package return true unless @security_policy raise Gem::Security::Exception, - 'old format gems do not contain signatures and cannot be verified' if + "old format gems do not contain signatures and cannot be verified" if @security_policy.verify_data true 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 ce9b49e3eb..087f13f6c9 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,42 +56,42 @@ class Gem::Package::TarHeader ## # Pack format for a tar header - PACK_FORMAT = 'a100' + # name - 'a8' + # mode - 'a8' + # uid - 'a8' + # gid - 'a12' + # size - 'a12' + # mtime - 'a7a' + # chksum - 'a' + # typeflag - 'a100' + # linkname - 'a6' + # magic - 'a2' + # version - 'a32' + # uname - 'a32' + # gname - 'a8' + # devmajor - 'a8' + # devminor - 'a155' # prefix + PACK_FORMAT = "a100" + # name + "a8" + # mode + "a8" + # uid + "a8" + # gid + "a12" + # size + "a12" + # mtime + "a7a" + # chksum + "a" + # typeflag + "a100" + # linkname + "a6" + # magic + "a2" + # version + "a32" + # uname + "a32" + # gname + "a8" + # devmajor + "a8" + # devminor + "a155" # prefix ## # Unpack format for a tar header - UNPACK_FORMAT = 'A100' + # name - 'A8' + # mode - 'A8' + # uid - 'A8' + # gid - 'A12' + # size - 'A12' + # mtime - 'A8' + # checksum - 'A' + # typeflag - 'A100' + # linkname - 'A6' + # magic - 'A2' + # version - 'A32' + # uname - 'A32' + # gname - 'A8' + # devmajor - 'A8' + # devminor - 'A155' # prefix + UNPACK_FORMAT = "A100" + # name + "A8" + # mode + "A8" + # uid + "A8" + # gid + "A12" + # size + "A12" + # mtime + "A8" + # checksum + "A" + # typeflag + "A100" + # linkname + "A6" + # magic + "A2" + # version + "A32" + # uname + "A32" + # gname + "A8" + # devmajor + "A8" + # devminor + "A155" # prefix attr_reader(*FIELDS) @@ -99,32 +102,33 @@ class Gem::Package::TarHeader def self.from(stream) header = stream.read 512 - empty = (EMPTY_HEADER == header) + empty = (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: empty 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,21 +151,23 @@ 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 @@ -173,23 +180,23 @@ class Gem::Package::TarHeader end def ==(other) # :nodoc: - self.class === other and - @checksum == other.checksum and - @devmajor == other.devmajor and - @devminor == other.devminor and - @gid == other.gid and - @gname == other.gname and - @linkname == other.linkname and - @magic == other.magic and - @mode == other.mode and - @mtime == other.mtime and - @name == other.name and - @prefix == other.prefix and - @size == other.size and - @typeflag == other.typeflag and - @uid == other.uid and - @uname == other.uname and - @version == other.version + self.class === other && + @checksum == other.checksum && + @devmajor == other.devmajor && + @devminor == other.devminor && + @gid == other.gid && + @gname == other.gname && + @linkname == other.linkname && + @magic == other.magic && + @mode == other.mode && + @mtime == other.mtime && + @name == other.name && + @prefix == other.prefix && + @size == other.size && + @typeflag == other.typeflag && + @uid == other.uid && + @uname == other.uname && + @version == other.version end def to_s # :nodoc: @@ -208,7 +215,7 @@ class Gem::Package::TarHeader private def calculate_checksum(header) - header.unpack("C*").inject {|a, b| a + b } + header.sum(0) end def header(checksum = @checksum) @@ -238,6 +245,6 @@ class Gem::Package::TarHeader 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 e7c5620533..25f9b2f945 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) @@ -53,44 +51,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,10 +92,10 @@ class Gem::Package::TarReader return unless found - return yield found + yield found ensure rewind end end -require 'rubygems/package/tar_reader/entry' +require_relative "tar_reader/entry" diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb index 5865599d3a..5e9d9af5c6 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 ## @@ -68,18 +93,16 @@ class Gem::Package::TarReader::Entry @header.name end rescue ArgumentError => e - raise unless e.message == 'string contains null byte' + raise unless e.message == "string contains null byte" raise Gem::Package::TarInvalidError, - 'tar is corrupt, name contains null byte' + "tar is corrupt, name contains null byte" end ## # 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 +140,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 +186,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_test_case.rb b/lib/rubygems/package/tar_test_case.rb deleted file mode 100644 index 1161d0a5a8..0000000000 --- a/lib/rubygems/package/tar_test_case.rb +++ /dev/null @@ -1,139 +0,0 @@ -# frozen_string_literal: true -require 'rubygems/test_case' -require 'rubygems/package' - -## -# A test case for Gem::Package::Tar* classes - -class Gem::Package::TarTestCase < Gem::TestCase - def ASCIIZ(str, length) - str + "\0" * (length - str.length) - end - - def SP(s) - s + " " - end - - def SP_Z(s) - s + " \0" - end - - def Z(s) - s + "\0" - end - - def assert_headers_equal(expected, actual) - expected = expected.to_s unless String === expected - actual = actual.to_s unless String === actual - - fields = %w[ - name 100 - mode 8 - uid 8 - gid 8 - size 12 - mtime 12 - checksum 8 - typeflag 1 - linkname 100 - magic 6 - version 2 - uname 32 - gname 32 - devmajor 8 - devminor 8 - prefix 155 - ] - - offset = 0 - - until fields.empty? do - name = fields.shift - length = fields.shift.to_i - - if name == "checksum" - chksum_off = offset - offset += length - next - end - - assert_equal expected[offset, length], actual[offset, length], - "Field #{name} of the tar header differs." - - offset += length - end - - assert_equal expected[chksum_off, 8], actual[chksum_off, 8] - end - - def calc_checksum(header) - sum = header.unpack("C*").inject{|s,a| s + a } - SP(Z(to_oct(sum, 6))) - end - - def header(type, fname, dname, length, mode, mtime, checksum = nil, linkname = "") - checksum ||= " " * 8 - - arr = [ # struct tarfile_entry_posix - ASCIIZ(fname, 100), # char name[100]; ASCII + (Z unless filled) - Z(to_oct(mode, 7)), # char mode[8]; 0 padded, octal null - Z(to_oct(0, 7)), # char uid[8]; ditto - Z(to_oct(0, 7)), # char gid[8]; ditto - Z(to_oct(length, 11)), # char size[12]; 0 padded, octal, null - Z(to_oct(mtime, 11)), # char mtime[12]; 0 padded, octal, null - checksum, # char checksum[8]; 0 padded, octal, null, space - type, # char typeflag[1]; file: "0" dir: "5" - ASCIIZ(linkname, 100), # char linkname[100]; ASCII + (Z unless filled) - "ustar\0", # char magic[6]; "ustar\0" - "00", # char version[2]; "00" - ASCIIZ("wheel", 32), # char uname[32]; ASCIIZ - ASCIIZ("wheel", 32), # char gname[32]; ASCIIZ - Z(to_oct(0, 7)), # char devmajor[8]; 0 padded, octal, null - Z(to_oct(0, 7)), # char devminor[8]; 0 padded, octal, null - ASCIIZ(dname, 155), # char prefix[155]; ASCII + (Z unless filled) - ] - - h = arr.join - ret = h + "\0" * (512 - h.size) - assert_equal(512, ret.size) - ret - end - - def tar_dir_header(name, prefix, mode, mtime) - h = header("5", name, prefix, 0, mode, mtime) - checksum = calc_checksum(h) - header("5", name, prefix, 0, mode, mtime, checksum) - end - - def tar_file_header(fname, dname, mode, length, mtime) - h = header("0", fname, dname, length, mode, mtime) - checksum = calc_checksum(h) - header("0", fname, dname, length, mode, mtime, checksum) - end - - def tar_symlink_header(fname, prefix, mode, mtime, linkname) - h = header("2", fname, prefix, 0, mode, mtime, nil, linkname) - checksum = calc_checksum(h) - header("2", fname, prefix, 0, mode, mtime, checksum, linkname) - end - - def to_oct(n, pad_size) - "%0#{pad_size}o" % n - end - - def util_entry(tar) - io = TempIO.new tar - - header = Gem::Package::TarHeader.from io - - Gem::Package::TarReader::Entry.new header, io - end - - def util_dir_entry - util_entry tar_dir_header("foo", "bar", 0, Time.now) - end - - def util_symlink_entry - util_entry tar_symlink_header("foo", "bar", 0, Time.now, "link") - end -end diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb index 877cc167c9..b24bdb63e7 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 @@ -113,9 +116,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: Gem.source_date_epoch @io.write header @io.pos = final_pos @@ -166,7 +169,7 @@ class Gem::Package::TarWriter def add_file_signed(name, mode, signer) digest_algorithms = [ signer.digest_algorithm, - Gem::Security.create_digest('SHA512'), + Gem::Security.create_digest("SHA512"), ].compact.uniq digests = add_file_digest name, mode, digest_algorithms do |io| @@ -189,7 +192,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 +209,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 +235,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 +289,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 @@ -304,17 +307,17 @@ class Gem::Package::TarWriter raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)") end - prefix = '' + prefix = "" if name.bytesize > 100 - parts = name.split('/', -1) # parts are never empty here + parts = name.split("/", -1) # parts are never empty here name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/") - prefix = parts.join('/') # if empty, then it's impossible to split (parts is empty too) + prefix = parts.join("/") # if empty, then it's impossible to split (parts is empty too) while !parts.empty? && (prefix.bytesize > 155 || name.empty?) - name = parts.pop + '/' + name - prefix = parts.join('/') + name = parts.pop + "/" + name + prefix = parts.join("/") end - if name.bytesize > 100 or prefix.empty? + if name.bytesize > 100 || prefix.empty? raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)") end @@ -323,6 +326,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 d5a2885a64..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 @@ -20,9 +21,9 @@ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -require 'rubygems' -require 'rubygems/package' -require 'rake/packagetask' +require_relative "../rubygems" +require_relative "package" +require "rake/packagetask" ## # Create a package based upon a Gem::Specification. Gem packages, as well as @@ -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 @@ -113,7 +114,7 @@ class Gem::PackageTask < Rake::PackageTask Gem::Package.build gem_spec verbose trace do - mv gem_file, '..' + mv gem_file, ".." end end end diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index 8103caf324..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 @@ -72,12 +72,7 @@ class Gem::PathSupport # Return the default Gem path def default_path - gem_path = Gem.default_path + [@home] - - if defined?(APPLE_GEM_HOME) - gem_path << APPLE_GEM_HOME - end - gem_path + Gem.default_path + [@home] end def expand(path) diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index fd1c0a62ac..48b7344aee 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require "rubygems/deprecate" + +require_relative "deprecate" ## # Available list of platforms for targeting Gem installations. @@ -12,20 +13,28 @@ 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) + @local ||= begin + arch = RbConfig::CONFIG["arch"] + arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch) + new(arch) + end end def self.match(platform) match_platforms?(platform, Gem.platforms) end + class << self + extend Gem::Deprecate + rubygems_deprecate :match, "Gem::Platform.match_spec? or match_gem?" + end + def self.match_platforms?(platform, platforms) + platform = Gem::Platform.new(platform) unless platform.is_a?(Gem::Platform) platforms.any? do |local_platform| - platform.nil? or - local_platform == platform or - (local_platform != Gem::Platform::RUBY and local_platform =~ platform) + platform.nil? || + local_platform == platform || + (local_platform != Gem::Platform::RUBY && platform =~ local_platform) end end private_class_method :match_platforms? @@ -34,10 +43,24 @@ 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) + platform == Gem::Platform::RUBY ? -1 : 1 end def self.installable?(spec) @@ -52,7 +75,7 @@ class Gem::Platform case arch when Gem::Platform::CURRENT then Gem::Platform.local - when Gem::Platform::RUBY, nil, '' then + when Gem::Platform::RUBY, nil, "" then Gem::Platform::RUBY else super @@ -64,9 +87,9 @@ class Gem::Platform when Array then @cpu, @os, @version = arch when String then - arch = arch.split '-' + arch = arch.split "-" - if arch.length > 2 and arch.last !~ /\d/ # reassemble x86-linux-gnu + if arch.length > 2 && !arch.last.match?(/\d+(\.\d+)?$/) # reassemble x86-linux-{libc} extra = arch.pop arch.last << "-#{extra}" end @@ -74,44 +97,47 @@ class Gem::Platform cpu = arch.shift @cpu = case cpu - when /i\d86/ then 'x86' + when /i\d86/ then "x86" else cpu - end + end - if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ # for command-line + if arch.length == 2 && arch.last.match?(/^\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 /hpux(\d+)?/ then [ 'hpux', $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-?((?!gnu)\w+)?/ then [ 'linux', $1 ] - when /mingw32/ then [ 'mingw32', nil ] + 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? and os =~ /32$/ + os = $1 + version = $3 + @cpu = "x86" if @cpu.nil? && os =~ /32$/ [os, version] - when /netbsdelf/ then [ 'netbsdelf', nil ] - when /openbsd(\d+\.\d+)?/ then [ 'openbsd', $1 ] - when /bitrig(\d+\.\d+)?/ then [ 'bitrig', $1 ] - when /solaris(\d+\.\d+)?/ then [ 'solaris', $1 ] + 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 ] - end + when /^(\w+_platform)(\d+)?/ then [$1, $2] + else ["unknown", nil] + end when Gem::Platform then @cpu = arch.cpu @os = arch.os @@ -126,7 +152,7 @@ class Gem::Platform end def to_s - to_a.compact.join '-' + to_a.compact.join "-" end ## @@ -134,10 +160,10 @@ class Gem::Platform # the same CPU, OS and version. def ==(other) - self.class === other and to_a == other.to_a + self.class === other && to_a == other.to_a end - alias :eql? :== + alias_method :eql?, :== def hash # :nodoc: to_a.hash @@ -146,23 +172,53 @@ class Gem::Platform ## # Does +other+ match this platform? Two platforms match if they have the # same CPU, or either has a CPU of 'universal', they have the same OS, and - # they have the same version, or either has no version. + # 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). + # + # 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 + # as a wildcard for a binary gem platform (as for other OSes), for the + # runtime platform "no version" stands for 'gnu'. To be able to distinguish + # these, the method receiver is the gem platform, while the argument is + # the runtime platform. + # + #-- + # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb` def ===(other) return nil unless Gem::Platform === other + # universal-mingw32 matches x64-mingw-ucrt + return true if (@cpu == "universal" || other.cpu == "universal") && + @os.start_with?("mingw") && other.os.start_with?("mingw") + # cpu - ([nil,'universal'].include?(@cpu) or [nil, 'universal'].include?(other.cpu) or @cpu == other.cpu or - (@cpu == 'arm' and other.cpu.start_with?("arm"))) and + ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu || + (@cpu == "arm" && other.cpu.start_with?("arm"))) && + + # os + @os == other.os && + + # version + ( + (@os != "linux" && (@version.nil? || other.version.nil?)) || + (@os == "linux" && (normalized_linux_version == other.normalized_linux_version || ["musl#{@version}", "musleabi#{@version}", "musleabihf#{@version}"].include?(other.version))) || + @version == other.version + ) + end + + #-- + # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb` - # os - @os == other.os and + def normalized_linux_version + return nil unless @version - # version - (@version.nil? or other.version.nil? or @version == other.version) + without_gnu_nor_abi_modifiers = @version.sub(/\Agnu/, "").sub(/eabi(hf)?\Z/, "") + return nil if without_gnu_nor_abi_modifiers.empty? + + without_gnu_nor_abi_modifiers end ## @@ -175,19 +231,19 @@ 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 - end + 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 else @@ -201,11 +257,11 @@ class Gem::Platform # A pure-Ruby gem that may use Gem::Specification#extensions to build # binary files. - RUBY = 'ruby'.freeze + RUBY = "ruby" ## # A platform-specific gem that is built for the packaging Ruby's platform. # This will be replaced with Gem::Platform::local. - CURRENT = 'current'.freeze + CURRENT = "current" end diff --git a/lib/rubygems/psych_additions.rb b/lib/rubygems/psych_additions.rb deleted file mode 100644 index 1ddd74421c..0000000000 --- a/lib/rubygems/psych_additions.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true -# This exists just to satisfy bugs in marshal'd gemspecs that -# contain a reference to YAML::PrivateType. We prune these out -# in Specification._load, but if we don't have the constant, Marshal -# blows up. - -module Psych # :nodoc: - class PrivateType # :nodoc: - end -end diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb index 6f399a289e..24857adb9d 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 @@ -7,12 +8,16 @@ module Gem end unless respond_to? :create def visit_String(str) - return super unless str == '=' # or whatever you want + return super unless str == "=" # or whatever you want quote = Psych::Nodes::Scalar::SINGLE_QUOTED @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 diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb index ea0f260ab4..a95a759401 100644 --- a/lib/rubygems/query_utils.rb +++ b/lib/rubygems/query_utils.rb @@ -1,52 +1,51 @@ # frozen_string_literal: true -require 'rubygems/local_remote_options' -require 'rubygems/spec_fetcher' -require 'rubygems/version_option' -require 'rubygems/text' +require_relative "local_remote_options" +require_relative "spec_fetcher" +require_relative "version_option" +require_relative "text" module Gem::QueryUtils - include Gem::Text include Gem::LocalRemoteOptions include Gem::VersionOption def add_query_options - add_option('-i', '--[no-]installed', - 'Check for installed gem') do |value, options| + add_option("-i", "--[no-]installed", + "Check for installed gem") do |value, options| 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 add_version_option command, "for use with --installed" - add_option('-d', '--[no-]details', - 'Display detailed information of gem(s)') do |value, options| + add_option("-d", "--[no-]details", + "Display detailed information of gem(s)") do |value, options| options[:details] = value end - add_option('--[no-]versions', - 'Display only gem names') do |value, options| + add_option("--[no-]versions", + "Display only gem names") do |value, options| options[:versions] = value options[:details] = false unless value end - add_option('-a', '--all', - 'Display all gem versions') do |value, options| + add_option("-a", "--all", + "Display all gem versions") do |value, options| options[:all] = value end - add_option('-e', '--exact', - 'Name of gem(s) to query on matches the', - 'provided STRING') do |value, options| + add_option("-e", "--exact", + "Name of gem(s) to query on matches the", + "provided STRING") do |value, options| options[:exact] = value end - add_option('--[no-]prerelease', - 'Display prerelease versions') do |value, options| + add_option("--[no-]prerelease", + "Display prerelease versions") do |value, options| options[:prerelease] = value end @@ -54,14 +53,14 @@ module Gem::QueryUtils end def defaults_str # :nodoc: - "--local --name-matches // --no-details --versions --no-installed" + "--local --no-details --versions --no-installed" end def execute - gem_names = Array(options[:name]) - - if !args.empty? - gem_names = options[:exact] ? args.map{|arg| /\A#{Regexp.escape(arg)}\Z/ } : args.map{|arg| /#{arg}/i } + gem_names = if args.empty? + [options[:name]] + else + options[:exact] ? args.map {|arg| /\A#{Regexp.escape(arg)}\Z/ } : args.map {|arg| /#{arg}/i } end terminate_interaction(check_installed_gems(gem_names)) if check_installed_gems? @@ -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 @@ -96,7 +95,7 @@ module Gem::QueryUtils end def gem_name? - !options[:name].source.empty? + !options[:name].nil? end def prerelease @@ -112,14 +111,14 @@ module Gem::QueryUtils end def display_header(type) - if (ui.outs.tty? and Gem.configuration.verbose) or both? + if (ui.outs.tty? && Gem.configuration.verbose) || both? say say "*** #{type} GEMS ***" say 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? @@ -129,12 +128,10 @@ module Gem::QueryUtils display_header("LOCAL") specs = Gem::Specification.find_all do |s| - s.name =~ name and req =~ s.version - end + name_matches = name ? s.name =~ name : true + version_matches = show_prereleases? || !s.version.prerelease? - dep = Gem::Deprecate.skip_during { Gem::Dependency.new name, req } - specs.select! do |s| - dep.match?(s.name, s.version, show_prereleases?) + name_matches && version_matches end spec_tuples = specs.map do |spec| @@ -149,19 +146,19 @@ module Gem::QueryUtils fetcher = Gem::SpecFetcher.fetcher - spec_tuples = if name.respond_to?(:source) && name.source.empty? - fetcher.detect(specs_type) { true } - else - fetcher.detect(specs_type) do |name_tuple| - name === name_tuple.name - end - end + spec_tuples = if name.nil? + fetcher.detect(specs_type) { true } + else + fetcher.detect(specs_type) do |name_tuple| + name === name_tuple.name && options[:version].satisfied_by?(name_tuple.version) + end + end output_query_results(spec_tuples) end def specs_type - if options[:all] + if options[:all] || options[:version].specific? if options[:prerelease] :complete else @@ -178,7 +175,7 @@ module Gem::QueryUtils # Check if gem +name+ version +version+ is installed. def installed?(name, req = Gem::Requirement.default) - Gem::Specification.any? {|s| s.name =~ name and req =~ s.version } + Gem::Specification.any? {|s| s.name =~ name && req =~ s.version } end def output_query_results(spec_tuples) @@ -199,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] = [] } @@ -244,8 +241,8 @@ module Gem::QueryUtils return unless options[:versions] list = - if platforms.empty? or options[:details] - name_tuples.map {|n| n.version }.uniq + if platforms.empty? || options[:details] + name_tuples.map(&:version).uniq else platforms.sort.reverse.map do |version, pls| out = version.to_s @@ -259,14 +256,14 @@ module Gem::QueryUtils if pls != [Gem::Platform::RUBY] platform_list = [pls.delete(Gem::Platform::RUBY), *pls.sort].compact - out = platform_list.unshift(out).join(' ') + out = platform_list.unshift(out).join(" ") end out end end - entry << " (#{list.join ', '})" + entry << " (#{list.join ", "})" end def make_entry(entry_tuples, platforms) @@ -285,22 +282,22 @@ module Gem::QueryUtils end def spec_authors(entry, spec) - authors = "Author#{spec.authors.length > 1 ? 's' : ''}: ".dup - authors << spec.authors.join(', ') + authors = "Author#{spec.authors.length > 1 ? "s" : ""}: ".dup + authors << spec.authors.join(", ") entry << format_text(authors, 68, 4) end def spec_homepage(entry, spec) - return if spec.homepage.nil? or spec.homepage.empty? + return if spec.homepage.nil? || spec.homepage.empty? entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4) end def spec_license(entry, spec) - return if spec.license.nil? or spec.license.empty? + return if spec.license.nil? || spec.license.empty? - licenses = "License#{spec.licenses.length > 1 ? 's' : ''}: ".dup - licenses << spec.licenses.join(', ') + licenses = "License#{spec.licenses.length > 1 ? "s" : ""}: ".dup + licenses << spec.licenses.join(", ") entry << "\n" << format_text(licenses, 68, 4) end @@ -308,15 +305,15 @@ module Gem::QueryUtils return unless spec.loaded_from if specs.length == 1 - default = spec.default_gem? ? ' (default)' : nil + default = spec.default_gem? ? " (default)" : nil entry << "\n" << " Installed at#{default}: #{spec.base_dir}" else - label = 'Installed at' + 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}" - label = ' ' * label.length + default = ", default" if s.default_gem? + entry << "\n" << " #{label} (#{version}#{default}): #{s.base_dir}" + label = " " * label.length end end end @@ -329,16 +326,16 @@ module Gem::QueryUtils return unless non_ruby if platforms.length == 1 - title = platforms.values.length == 1 ? 'Platform' : 'Platforms' - entry << " #{title}: #{platforms.values.sort.join(', ')}\n" + title = platforms.values.length == 1 ? "Platform" : "Platforms" + 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}: " - data = format_text pls.sort.join(', '), 68, label.length + data = format_text pls.sort.join(", "), 68, label.length data[0, label.length] = label entry << data << "\n" end @@ -349,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 c40bb7d9f1..907dcd9431 100644 --- a/lib/rubygems/rdoc.rb +++ b/lib/rubygems/rdoc.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true -require 'rubygems' + +require_relative "../rubygems" begin - require 'rdoc/rubygems_hook' + require "rdoc/rubygems_hook" module Gem RDoc = ::RDoc::RubygemsHook end diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index e3d4bfbeba..c3a41592f6 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true -require 'rubygems' -require 'rubygems/request' -require 'rubygems/request/connection_pools' -require 'rubygems/s3_uri_signer' -require 'rubygems/uri_formatter' -require 'rubygems/uri_parsing' -require 'rubygems/user_interaction' -require 'resolv' + +require_relative "../rubygems" +require_relative "request" +require_relative "request/connection_pools" +require_relative "s3_uri_signer" +require_relative "uri_formatter" +require_relative "uri" +require_relative "user_interaction" ## # RemoteFetcher handles the details of fetching gems and gem information from @@ -14,30 +14,24 @@ require 'resolv' class Gem::RemoteFetcher include Gem::UserInteraction - include Gem::UriParsing ## # A FetchError exception wraps up the various possible IO and HTTP failures # that could happen while downloading from the internet. class FetchError < Gem::Exception - include Gem::UriParsing - ## # The URI which was being accessed when the exception happened. attr_accessor :uri, :original_uri def initialize(message, uri) - super message - - uri = parse_uri(uri) - - @original_uri = uri.dup + uri = Gem::Uri.new(uri) - uri.password = 'REDACTED' if uri.respond_to?(:password) && uri.password + super uri.redact_credentials_from(message) - @uri = uri.to_s + @original_uri = uri.to_s + @uri = uri.redacted.to_s end def to_s # :nodoc: @@ -59,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 @@ -79,16 +73,16 @@ class Gem::RemoteFetcher # fetching the gem. def initialize(proxy=nil, dns=nil, headers={}) - require 'rubygems/core_ext/tcpsocket_init' if Gem.configuration.ipv4_fallback_enabled - require 'net/http' - require 'stringio' - require 'uri' + require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled + require_relative "vendored_net_http" + require "stringio" + require_relative "vendor/uri/lib/uri" Socket.do_not_reverse_lookup = true @proxy = proxy @pools = {} - @pool_lock = Mutex.new + @pool_lock = Thread::Mutex.new @cert_files = Gem::Request.get_cert_files @headers = headers @@ -121,7 +115,7 @@ class Gem::RemoteFetcher cache_dir = if Dir.pwd == install_dir # see fetch_command install_dir - elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && (not 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" @@ -131,26 +125,30 @@ class Gem::RemoteFetcher 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 = parse_uri(source_uri) + 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 case scheme - when 'http', 'https', 's3' then + when "http", "https", "s3" then unless File.exist? local_gem_path begin verbose "Downloading gem #{gem_file_name}" 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 @@ -160,15 +158,15 @@ 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 + when "file" then begin path = source_uri.path - path = File.dirname(path) if File.extname(path) == '.gem' + path = File.dirname(path) if File.extname(path) == ".gem" - remote_gem_path = Gem::Util.correct_for_windows_path(File.join(path, 'gems', gem_file_name)) + remote_gem_path = Gem::Util.correct_for_windows_path(File.join(path, "gems", gem_file_name)) FileUtils.cp(remote_gem_path, local_gem_path) rescue Errno::EACCES @@ -176,13 +174,13 @@ class Gem::RemoteFetcher end verbose "Using local gem #{local_gem_path}" - when nil then # TODO test for local overriding cache + when nil then # TODO: test for local overriding cache source_path = if Gem.win_platform? && source_uri.scheme && - !source_uri.path.include?(':') - "#{source_uri.scheme}:#{source_uri.path}" - else - source_uri.path - end + !source_uri.path.include?(":") + "#{source_uri.scheme}:#{source_uri.path}" + else + source_uri.path + end source_path = Gem::UriFormatter.new(source_path).unescape @@ -212,23 +210,23 @@ 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 - raise FetchError.new('too many redirects', uri) if depth > 10 + 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'] + unless location = response["Location"] raise FetchError.new("redirecting but no redirect location was given", uri) end - location = parse_uri location + location = Gem::Uri.new location if https?(uri) && !https?(location) raise FetchError.new("redirecting to non-https resource: #{location}", uri) @@ -240,13 +238,13 @@ class Gem::RemoteFetcher end end - alias :fetch_https :fetch_http + alias_method :fetch_https, :fetch_http ## # Downloads +uri+ and returns it as a String. def fetch_path(uri, mtime = nil, head = false) - uri = parse_uri uri + uri = Gem::Uri.new uri unless uri.scheme raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" @@ -254,7 +252,7 @@ class Gem::RemoteFetcher data = send "fetch_#{uri.scheme}", uri, mtime, head - if data and !head and uri.to_s.end_with?(".gz") + if data && !head && uri.to_s.end_with?(".gz") begin data = Gem::Util.gunzip data rescue Zlib::GzipFile::Error @@ -263,7 +261,7 @@ 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 @@ -287,7 +285,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) @@ -295,7 +297,7 @@ class Gem::RemoteFetcher return Gem.read_binary(path) end - if update and path + if update && path Gem.write_binary(path, data) end @@ -303,8 +305,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) @@ -319,11 +321,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 diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb index 1ed0fbcb99..9116785231 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true -require 'net/http' -require 'rubygems/user_interaction' + +require_relative "vendored_net_http" +require_relative "user_interaction" class Gem::Request extend Gem::UserInteraction @@ -17,11 +18,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,22 +30,27 @@ 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", File.dirname(__FILE__)) + pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__) Dir.glob(pattern) end def self.configure_connection_for_https(connection, cert_files) - raise Gem::Exception.new('OpenSSl is not available. Install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources') unless Gem::HAVE_OPENSSL + raise Gem::Exception.new("OpenSSL is not available. Install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources") unless Gem::HAVE_OPENSSL connection.use_ssl = true connection.verify_mode = @@ -96,8 +102,10 @@ class Gem::Request return unless cert case error_number when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then + require "time" "Certificate #{cert.subject} expired at #{cert.not_after.iso8601}" when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then + require "time" "Certificate #{cert.subject} not valid until #{cert.not_before.iso8601}" when OpenSSL::X509::V_ERR_CERT_REJECTED then "Certificate #{cert.subject} is rejected" @@ -138,13 +146,13 @@ class Gem::Request Gem::UriFormatter.new(@uri.password).unescape end - request.add_field 'User-Agent', @user_agent - request.add_field 'Connection', 'keep-alive' - request.add_field 'Keep-Alive', '30' + request.add_field "User-Agent", @user_agent + request.add_field "Connection", "keep-alive" + request.add_field "Keep-Alive", "30" if @last_modified - require 'time' - request.add_field 'If-Modified-Since', @last_modified.httpdate + require "time" + request.add_field "If-Modified-Since", @last_modified.httpdate end yield request if block_given? @@ -156,24 +164,23 @@ class Gem::Request # Returns a proxy URI for the given +scheme+ if one is set in the # environment variables. - def self.get_proxy_from_env(scheme = 'http') - _scheme = scheme.downcase - _SCHEME = scheme.upcase - env_proxy = ENV["#{_scheme}_proxy"] || ENV["#{_SCHEME}_PROXY"] + def self.get_proxy_from_env(scheme = "http") + 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 and uri.user.nil? and uri.password.nil? - user = ENV["#{_scheme}_proxy_user"] || ENV["#{_SCHEME}_PROXY_USER"] - password = ENV["#{_scheme}_proxy_pass"] || ENV["#{_SCHEME}_PROXY_PASS"] + if uri && uri.user.nil? && uri.password.nil? + 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 @@ -189,16 +196,16 @@ class Gem::Request bad_response = false begin - @requests[connection.object_id] += 1 + @requests[connection] += 1 - verbose "#{request.method} #{@uri}" + verbose "#{request.method} #{Gem::Uri.redact(@uri)}" file_name = File.basename(@uri.path) # perform download progress reporter only for gems 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 @@ -221,30 +228,29 @@ class Gem::Request end verbose "#{response.code} #{response.message}" - - rescue Net::HTTPBadResponse + rescue Gem::Net::HTTPBadResponse verbose "bad response" reset connection - raise Gem::RemoteFetcher::FetchError.new('too many bad responses', @uri) if bad_response + raise Gem::RemoteFetcher::FetchError.new("too many bad responses", @uri) if bad_response 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 + raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri) + # 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 + raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried reset connection @@ -261,7 +267,7 @@ class Gem::Request # Resets HTTP connection +connection+. def reset(connection) - @requests.delete connection.object_id + @requests.delete connection connection.finish connection.start @@ -271,22 +277,22 @@ class Gem::Request ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}".dup ruby_version = RUBY_VERSION - ruby_version += 'dev' if RUBY_PATCHLEVEL == -1 + ruby_version += "dev" if RUBY_PATCHLEVEL == -1 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 << ")" - ua << " #{RUBY_ENGINE}" if RUBY_ENGINE != 'ruby' + ua << " #{RUBY_ENGINE}" if RUBY_ENGINE != "ruby" ua end end -require 'rubygems/request/http_pool' -require 'rubygems/request/https_pool' -require 'rubygems/request/connection_pools' +require_relative "request/http_pool" +require_relative "request/https_pool" +require_relative "request/connection_pools" diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index 7f3988952c..6c1b04ab65 100644 --- a/lib/rubygems/request/connection_pools.rb +++ b/lib/rubygems/request/connection_pools.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class Gem::Request::ConnectionPools # :nodoc: - @client = Net::HTTP + @client = Gem::Net::HTTP class << self attr_accessor :client @@ -11,7 +11,7 @@ class Gem::Request::ConnectionPools # :nodoc: @proxy_uri = proxy_uri @cert_files = cert_files @pools = {} - @pool_mutex = Mutex.new + @pool_mutex = Thread::Mutex.new end def pool_for(uri) @@ -28,7 +28,7 @@ class Gem::Request::ConnectionPools # :nodoc: end def close_all - @pools.each_value {|pool| pool.close_all } + @pools.each_value(&:close_all) end private @@ -37,15 +37,15 @@ class Gem::Request::ConnectionPools # :nodoc: # Returns list of no_proxy entries (if any) from the environment def get_no_proxy_from_env - env_no_proxy = ENV['no_proxy'] || ENV['NO_PROXY'] + env_no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"] - return [] if env_no_proxy.nil? or env_no_proxy.empty? + return [] if env_no_proxy.nil? || env_no_proxy.empty? env_no_proxy.split(/\s*,\s*/) end def https?(uri) - uri.scheme.downcase == 'https' + uri.scheme.casecmp("https").zero? end def no_proxy?(host, env_no_proxy) @@ -78,7 +78,7 @@ class Gem::Request::ConnectionPools # :nodoc: no_proxy = get_no_proxy_from_env - if proxy_uri and not no_proxy?(hostname, no_proxy) + if proxy_uri && !no_proxy?(hostname, no_proxy) proxy_hostname = proxy_uri.respond_to?(:hostname) ? proxy_uri.hostname : proxy_uri.host net_http_args + [ proxy_hostname, diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index 9985bbafa6..52543de41f 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 @@ -12,7 +13,7 @@ class Gem::Request::HTTPPool # :nodoc: @http_args = http_args @cert_files = cert_files @proxy_uri = proxy_uri - @queue = SizedQueue.new 1 + @queue = Thread::SizedQueue.new 1 @queue << nil end @@ -26,7 +27,7 @@ class Gem::Request::HTTPPool # :nodoc: def close_all until @queue.empty? - if connection = @queue.pop(true) and connection.started? + if (connection = @queue.pop(true)) && connection.started? connection.finish 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 5190cfc904..875df7e019 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require 'tsort' + +require_relative "vendored_tsort" ## # A RequestSet groups a request to activate a set of dependencies. @@ -15,7 +16,7 @@ require 'tsort' # #=> ["nokogiri-1.6.0", "mini_portile-0.5.1", "pg-0.17.0"] class Gem::RequestSet - include TSort + include Gem::TSort ## # Array of gems to install even if already installed @@ -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 @@ -151,7 +152,7 @@ class Gem::RequestSet @prerelease = options[:prerelease] requests = [] - download_queue = Queue.new + download_queue = Thread::Queue.new # Create a thread-safe list of gems to download sorted_requests.each do |req| @@ -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 @@ -254,7 +255,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 } @@ -287,7 +289,7 @@ class Gem::RequestSet installed ensure - ENV['GEM_HOME'] = gem_home + ENV["GEM_HOME"] = gem_home end ## @@ -303,7 +305,7 @@ class Gem::RequestSet end end - require "rubygems/dependency_installer" + require_relative "dependency_installer" inst = Gem::DependencyInstaller.new options inst.installed_gems.replace specs @@ -322,7 +324,7 @@ class Gem::RequestSet @git_set.root_dir = @install_dir - lock_file = "#{File.expand_path(path)}.lock".dup.tap(&Gem::UNTAINT) + lock_file = "#{File.expand_path(path)}.lock" begin tokenizer = Gem::RequestSet::Lockfile::Tokenizer.from_file lock_file parser = tokenizer.make_parser self, [] @@ -337,32 +339,32 @@ class Gem::RequestSet end def pretty_print(q) # :nodoc: - q.group 2, '[RequestSet:', ']' do + q.group 2, "[RequestSet:", "]" do q.breakable if @remote - q.text 'remote' + q.text "remote" q.breakable end if @prerelease - q.text 'prerelease' + q.text "prerelease" q.breakable end if @development_shallow - q.text 'shallow development' + q.text "shallow development" q.breakable elsif @development - q.text 'development' + q.text "development" q.breakable end if @soft_missing - q.text 'soft missing' + q.text "soft missing" end - q.group 2, '[dependencies:', ']' do + q.group 2, "[dependencies:", "]" do q.breakable @dependencies.map do |dep| q.text dep.to_s @@ -371,10 +373,10 @@ class Gem::RequestSet end q.breakable - q.text 'sets:' + q.text "sets:" q.breakable - q.pp @sets.map {|set| set.class } + q.pp @sets.map(&:class) end end @@ -424,11 +426,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) @@ -443,14 +445,14 @@ class Gem::RequestSet def tsort_each_child(node) # :nodoc: node.spec.dependencies.each do |dep| - next if dep.type == :development and not @development + next if dep.type == :development && !@development match = @requests.find do |r| - dep.match? r.spec.name, r.spec.version, @prerelease + dep.match?(r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease) end unless match - next if dep.type == :development and @development_shallow + next if dep.type == :development && @development_shallow next if @soft_missing raise Gem::DependencyError, "Unresolved dependency found during sorting - #{dep} (requested by #{node.spec.full_name})" @@ -461,6 +463,6 @@ class Gem::RequestSet end end -require 'rubygems/request_set/gem_dependency_api' -require 'rubygems/request_set/lockfile' -require 'rubygems/request_set/lockfile/tokenizer' +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 7188b07346..4347d22ccb 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,136 +33,136 @@ 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' - mswin64 = Gem::Platform.new 'x64-mswin64' - x86_mingw = Gem::Platform.new 'x86-mingw32' - x64_mingw = Gem::Platform.new 'x64-mingw32' + mswin = Gem::Platform.new "x86-mswin32" + mswin64 = Gem::Platform.new "x64-mswin64" + x86_mingw = Gem::Platform.new "x86-mingw32" + 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' - tilde_gt_1_8_0 = Gem::Requirement.new '~> 1.8.0' - tilde_gt_1_9_0 = Gem::Requirement.new '~> 1.9.0' - tilde_gt_2_0_0 = Gem::Requirement.new '~> 2.0.0' - tilde_gt_2_1_0 = Gem::Requirement.new '~> 2.1.0' + gt_eq_0 = Gem::Requirement.new ">= 0" + tilde_gt_1_8_0 = Gem::Requirement.new "~> 1.8.0" + tilde_gt_1_9_0 = Gem::Requirement.new "~> 1.9.0" + tilde_gt_2_0_0 = Gem::Requirement.new "~> 2.0.0" + 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 ## @@ -214,7 +215,7 @@ class Gem::RequestSet::GemDependencyAPI git_source :github do |repo_name| repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/" - "git://github.com/#{repo_name}.git" + "https://github.com/#{repo_name}.git" end git_source :bitbucket do |repo_name| @@ -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 @@ -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 @@ -371,7 +372,7 @@ class Gem::RequestSet::GemDependencyAPI duplicate = @dependencies.include? name @dependencies[name] = - if requirements.empty? and not source_set + if requirements.empty? && !source_set Gem::Requirement.default elsif source_set Gem::Requirement.source_set @@ -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 @@ -637,8 +636,8 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta # :development. Only one group may be specified. def gemspec(options = {}) - name = options.delete(:name) || '{,*}' - path = options.delete(:path) || '.' + name = options.delete(:name) || "{,*}" + path = options.delete(:path) || "." development_group = options.delete(:development_group) || :development spec = find_gemspec name, path @@ -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 @@ -697,11 +695,11 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta def pin_gem_source(name, type = :default, source = nil) source_description = case type - when :default then '(default)' + when :default then "(default)" when :path then "path: #{source}" when :git then "git: #{source}" when :source then "source: #{source}" - else '(unknown)' + else "(unknown)" end raise ArgumentError, @@ -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 @@ -788,20 +785,20 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta engine_version = options[:engine_version] raise ArgumentError, - 'You must specify engine_version along with the Ruby engine' if - engine and not engine_version + "You must specify engine_version along with the Ruby engine" if + engine && !engine_version 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 and engine != Gem.ruby_engine - message = "Your Ruby engine is #{Gem.ruby_engine}, " + + if engine && engine != 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 8f8f142fff..c446b3ae51 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 @@ -56,10 +57,10 @@ class Gem::RequestSet::Lockfile deps[name] = if [Gem::Resolver::VendorSpecification, Gem::Resolver::GitSpecification].include? spec.class - Gem::Requirement.source_set - else - requirement - end + Gem::Requirement.source_set + else + requirement + end end deps @@ -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| - next if request.spec.name == 'bundler' + 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 @@ -156,7 +152,7 @@ class Gem::RequestSet::Lockfile if dest.index(base) == 0 offset = dest[base.size + 1..-1] - return '.' unless offset + return "." unless offset offset else @@ -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}" @@ -224,7 +220,7 @@ class Gem::RequestSet::Lockfile def write content = to_s - File.open "#{@gem_deps_file}.lock", 'w' do |io| + File.open "#{@gem_deps_file}.lock", "w" do |io| io.write content end end @@ -236,4 +232,4 @@ class Gem::RequestSet::Lockfile end end -require 'rubygems/request_set/lockfile/tokenizer' +require_relative "lockfile/tokenizer" diff --git a/lib/rubygems/request_set/lockfile/parser.rb b/lib/rubygems/request_set/lockfile/parser.rb index 8c12b435af..e751a1445e 100644 --- a/lib/rubygems/request_set/lockfile/parser.rb +++ b/lib/rubygems/request_set/lockfile/parser.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::RequestSet::Lockfile::Parser ### # Parses lockfiles @@ -19,18 +20,18 @@ class Gem::RequestSet::Lockfile::Parser @tokens.skip :newline case token.value - when 'DEPENDENCIES' then + when "DEPENDENCIES" then parse_DEPENDENCIES - when 'GIT' then + when "GIT" then parse_GIT - when 'GEM' then + when "GEM" then parse_GEM - when 'PATH' then + when "PATH" then parse_PATH - when 'PLATFORMS' then + when "PLATFORMS" then parse_PLATFORMS else - token = get until @tokens.empty? or peek.first == :section + 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}" @@ -44,20 +45,20 @@ class Gem::RequestSet::Lockfile::Parser def get(expected_types = nil, expected_value = nil) # :nodoc: token = @tokens.shift - if expected_types and not Array(expected_types).include? token.type + if expected_types && !Array(expected_types).include?(token.type) unget token - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + + 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 and expected_value != token.value + if expected_value && expected_value != token.value unget token - message = "unexpected token [#{token.type.inspect}, #{token.value.inspect}], " + - "expected [#{expected_types.inspect}, " + + 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 @@ -67,7 +68,7 @@ class Gem::RequestSet::Lockfile::Parser end def parse_DEPENDENCIES # :nodoc: - while not @tokens.empty? and :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text requirements = [] @@ -110,8 +111,8 @@ class Gem::RequestSet::Lockfile::Parser def parse_GEM # :nodoc: sources = [] - while [:entry, 'remote'] == peek.first(2) do - get :entry, 'remote' + while peek.first(2) == [:entry, "remote"] do + get :entry, "remote" data = get(:text).value skip :newline @@ -120,14 +121,14 @@ class Gem::RequestSet::Lockfile::Parser sources << Gem::Source.new(Gem::DEFAULT_HOST) if sources.empty? - get :entry, 'specs' + get :entry, "specs" skip :newline set = Gem::Resolver::LockSet.new sources last_specs = nil - while not @tokens.empty? and :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text name = token.value column = token.column @@ -144,8 +145,8 @@ class Gem::RequestSet::Lockfile::Parser type = token.type data = token.value - if type == :text and column == 4 - version, platform = data.split '-', 2 + if type == :text && column == 4 + version, platform = data.split "-", 2 platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY @@ -171,26 +172,26 @@ class Gem::RequestSet::Lockfile::Parser end def parse_GIT # :nodoc: - get :entry, 'remote' + get :entry, "remote" repository = get(:text).value skip :newline - get :entry, 'revision' + get :entry, "revision" revision = get(:text).value skip :newline type = peek.type value = peek.value - if type == :entry and %w[branch ref tag].include? value + if type == :entry && %w[branch ref tag].include?(value) get get :text skip :newline end - get :entry, 'specs' + get :entry, "specs" skip :newline @@ -199,7 +200,7 @@ class Gem::RequestSet::Lockfile::Parser last_spec = nil - while not @tokens.empty? and :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text name = token.value column = token.column @@ -214,7 +215,7 @@ class Gem::RequestSet::Lockfile::Parser type = token.type data = token.value - if type == :text and column == 4 + if type == :text && column == 4 last_spec = set.add_git_spec name, data, repository, revision, true else dependency = parse_dependency name, data @@ -234,19 +235,19 @@ class Gem::RequestSet::Lockfile::Parser end def parse_PATH # :nodoc: - get :entry, 'remote' + get :entry, "remote" directory = get(:text).value skip :newline - get :entry, 'specs' + get :entry, "specs" skip :newline set = Gem::Resolver::VendorSet.new last_spec = nil - while not @tokens.empty? and :text == peek.first do + while !@tokens.empty? && peek.first == :text do token = get :text name = token.value column = token.column @@ -261,7 +262,7 @@ class Gem::RequestSet::Lockfile::Parser type = token.type data = token.value - if type == :text and column == 4 + if type == :text && column == 4 last_spec = set.add_vendor_gem name, directory else dependency = parse_dependency name, data @@ -281,7 +282,7 @@ class Gem::RequestSet::Lockfile::Parser end def parse_PLATFORMS # :nodoc: - while not @tokens.empty? and :text == peek.first do + while !@tokens.empty? && peek.first == :text do name = get(:text).value @platforms << name @@ -331,7 +332,7 @@ class Gem::RequestSet::Lockfile::Parser set.find_all(requirement) end.compact.first - specification && specification.version + specification&.version end ## diff --git a/lib/rubygems/request_set/lockfile/tokenizer.rb b/lib/rubygems/request_set/lockfile/tokenizer.rb index 6918e8e1a5..65cef3baa0 100644 --- a/lib/rubygems/request_set/lockfile/tokenizer.rb +++ b/lib/rubygems/request_set/lockfile/tokenizer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true -require 'rubygems/request_set/lockfile/parser' + +# ) frozen_string_literal: true +require_relative "parser" class Gem::RequestSet::Lockfile::Tokenizer Token = Struct.new :type, :value, :column, :line @@ -26,7 +28,7 @@ class Gem::RequestSet::Lockfile::Tokenizer end def skip(type) - @tokens.shift while not @tokens.empty? and peek.type == type + @tokens.shift while !@tokens.empty? && peek.type == type end ## @@ -48,7 +50,7 @@ class Gem::RequestSet::Lockfile::Tokenizer def next_token @tokens.shift end - alias :shift :next_token + alias_method :shift, :next_token def peek @tokens.first || EOF @@ -57,7 +59,7 @@ class Gem::RequestSet::Lockfile::Tokenizer private def tokenize(input) - require 'strscan' + require "strscan" s = StringScanner.new input until s.eos? do @@ -73,13 +75,14 @@ class Gem::RequestSet::Lockfile::Tokenizer end @tokens << - case - when s.scan(/\r?\n/) then + if s.scan(/\r?\n/) + token = Token.new(:newline, nil, *token_pos(pos)) @line_pos = s.pos @line += 1 token - when s.scan(/[A-Z]+/) then + elsif s.scan(/[A-Z]+/) + if leading_whitespace text = s.matched text += s.scan(/[^\s)]*/).to_s # in case of no match @@ -87,20 +90,27 @@ class Gem::RequestSet::Lockfile::Tokenizer else Token.new(:section, s.matched, *token_pos(pos)) end - when s.scan(/([a-z]+):\s/) then + elsif s.scan(/([a-z]+):\s/) + s.pos -= 1 # rewind for possible newline Token.new(:entry, s[1], *token_pos(pos)) - when s.scan(/\(/) then + elsif s.scan(/\(/) + Token.new(:l_paren, nil, *token_pos(pos)) - when s.scan(/\)/) then + elsif s.scan(/\)/) + Token.new(:r_paren, nil, *token_pos(pos)) - when s.scan(/<=|>=|=|~>|<|>|!=/) then + elsif s.scan(/<=|>=|=|~>|<|>|!=/) + Token.new(:requirement, s.matched, *token_pos(pos)) - when s.scan(/,/) then + elsif s.scan(/,/) + Token.new(:comma, nil, *token_pos(pos)) - when s.scan(/!/) then + elsif s.scan(/!/) + Token.new(:bang, nil, *token_pos(pos)) - when s.scan(/[^\s),!]*/) then + elsif s.scan(/[^\s),!]*/) + Token.new(:text, s.matched, *token_pos(pos)) else raise "BUG: can't create token for: #{s.string[s.pos..-1].inspect}" diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb index 6721de4055..02543cb14a 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require "rubygems/deprecate" + +require_relative "version" ## # A Requirement is a set of one or more version restrictions. It supports a @@ -9,14 +10,14 @@ require "rubygems/deprecate" # together in RubyGems. class Gem::Requirement - 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 && v.release < r.bump }, + 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 && v.release < r.bump }, }.freeze SOURCE_SET_REQUIREMENT = Struct.new(:for_lockfile).new "!" # :nodoc: @@ -27,7 +28,7 @@ class Gem::Requirement ## # 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 @@ -61,7 +62,7 @@ class Gem::Requirement input when Gem::Version, Array then new input - when '!' then + when "!" then source_set else if input.respond_to? :to_str @@ -73,11 +74,11 @@ class Gem::Requirement end def self.default - new '>= 0' + new ">= 0" end def self.default_prerelease - new '>= 0.a' + new ">= 0.a" end ### @@ -119,7 +120,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 +156,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 +164,7 @@ class Gem::Requirement "#{op} #{version}" end.uniq - " (#{list.join ', '})" + " (#{list.join ", "})" end ## @@ -194,24 +195,19 @@ class Gem::Requirement end def marshal_dump # :nodoc: - fix_syck_default_key_in_requirements - [@requirements] end def marshal_load(array) # :nodoc: @requirements = array[0] - fix_syck_default_key_in_requirements + raise TypeError, "wrong @requirements" unless Array === @requirements end def yaml_initialize(tag, vals) # :nodoc: vals.each do |ivar, val| instance_variable_set "@#{ivar}", val end - - Gem.load_yaml - fix_syck_default_key_in_requirements end def init_with(coder) # :nodoc: @@ -223,7 +219,7 @@ class Gem::Requirement end def encode_with(coder) # :nodoc: - coder.add 'requirements', @requirements + coder.add "requirements", @requirements end ## @@ -235,7 +231,7 @@ class Gem::Requirement end def pretty_print(q) # :nodoc: - q.group 1, 'Gem::Requirement.new(', ')' do + q.group 1, "Gem::Requirement.new(", ")" do q.pp as_list end end @@ -246,12 +242,11 @@ class Gem::Requirement def satisfied_by?(version) raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless Gem::Version === version - # #28965: syck has a bug with unquoted '=' YAML.loading as YAML::DefaultKey - requirements.all? {|op, rv| (OPS[op] || OPS["="]).call version, rv } + requirements.all? {|op, rv| OPS[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. @@ -259,7 +254,7 @@ class Gem::Requirement def specific? return true if @requirements.length > 1 # GIGO, > 1, > 2 is silly - not %w[> >=].include? @requirements.first.first # grab the operator + !%w[> >=].include? @requirements.first.first # grab the operator end def to_s # :nodoc: @@ -290,17 +285,9 @@ class Gem::Requirement @_tilde_requirements ||= _sorted_requirements.select {|r| r.first == "~>" } end - private - - def fix_syck_default_key_in_requirements # :nodoc: - Gem.load_yaml - - # Fixup the Syck DefaultKey bug - @requirements.each do |r| - if r[0].kind_of? Gem::SyckDefaultKey - r[0] = "=" - end - end + def initialize_copy(other) # :nodoc: + @requirements = other.requirements.dup + super end end diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 71c35ea3d3..115c716b6b 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -require 'rubygems/dependency' -require 'rubygems/exceptions' -require 'rubygems/util/list' + +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,14 +11,14 @@ require 'rubygems/util/list' # all the requirements. class Gem::Resolver - require 'rubygems/resolver/molinillo' + require_relative "vendored_molinillo" ## # If the DEBUG_RESOLVER environment variable is set then debugging mode is # enabled for the resolver. This will display information about the state # of the resolver while a set of dependencies is being resolved. - DEBUG_RESOLVER = !ENV['DEBUG_RESOLVER'].nil? + DEBUG_RESOLVER = !ENV["DEBUG_RESOLVER"].nil? ## # Set to true if all development dependencies should be considered. @@ -37,8 +38,6 @@ class Gem::Resolver ## # List of dependencies that could not be found in the configured sources. - attr_reader :missing - attr_reader :stats ## @@ -48,8 +47,7 @@ class Gem::Resolver attr_accessor :skip_gems ## - # When a missing dependency, don't stop. Just go on and record what was - # missing. + # attr_accessor :soft_missing @@ -74,7 +72,7 @@ class Gem::Resolver case sets.length when 0 then - raise ArgumentError, 'one set in the composition must be non-nil' + raise ArgumentError, "one set in the composition must be non-nil" when 1 then sets.first else @@ -105,7 +103,6 @@ class Gem::Resolver @development = false @development_shallow = false @ignore_dependencies = false - @missing = [] @skip_gems = {} @soft_missing = false @stats = Gem::Resolver::Stats.new @@ -114,7 +111,7 @@ class Gem::Resolver def explain(stage, *data) # :nodoc: return unless DEBUG_RESOLVER - d = data.map {|x| x.pretty_inspect }.join(", ") + d = data.map(&:pretty_inspect).join(", ") $stderr.printf "%10s %s\n", stage.to_s.upcase, d end @@ -124,7 +121,7 @@ class Gem::Resolver data = yield $stderr.printf "%10s (%d entries)\n", stage.to_s.upcase, data.size unless data.empty? - require 'pp' + require "pp" PP.pp data, $stderr end end @@ -144,7 +141,7 @@ class Gem::Resolver activation_request = Gem::Resolver::ActivationRequest.new spec, dep, possible - return spec, activation_request + [spec, activation_request] end def requests(s, act, reqs=[]) # :nodoc: @@ -153,10 +150,10 @@ class Gem::Resolver s.fetch_development_dependencies if @development s.dependencies.reverse_each do |d| - next if d.type == :development and not @development - next if d.type == :development and @development_shallow and + next if d.type == :development && !@development + next if d.type == :development && @development_shallow && act.development? - next if d.type == :development and @development_shallow and + next if d.type == :development && @development_shallow && act.parent reqs << Gem::Resolver::DependencyRequest.new(d, act) @@ -170,29 +167,28 @@ class Gem::Resolver reqs end - include Molinillo::UI + include Gem::Molinillo::UI def output - @output ||= debug? ? $stdout : File.open(IO::NULL, 'w') + @output ||= debug? ? $stdout : File.open(IO::NULL, "w") end def debug? DEBUG_RESOLVER end - include Molinillo::SpecificationProvider + include Gem::Molinillo::SpecificationProvider ## # Proceed with resolution! Returns an array of ActivationRequest objects. 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 + Gem::Molinillo::Resolver.new(self, self).resolve(@needed.map {|d| DependencyRequest.new d, nil }).tsort.map(&:payload).compact + rescue Gem::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) and !debug? + @output.close if defined?(@output) && !debug? end ## @@ -212,7 +208,7 @@ class Gem::Resolver matching_platform = select_local_platforms all - return matching_platform, all + [matching_platform, all] end ## @@ -227,7 +223,6 @@ class Gem::Resolver 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 @@ -246,7 +241,7 @@ class Gem::Resolver sources.each do |source| groups[source]. - sort_by {|spec| [spec.version, Gem::Platform.local =~ spec.platform ? 1 : 0] }. + sort_by {|spec| [spec.version, spec.platform =~ Gem::Platform.local ? 1 : 0] }. # rubocop:disable Performance/RegexpMatch map {|spec| ActivationRequest.new spec, dependency }. each {|activation_request| activation_requests << activation_request } end @@ -274,7 +269,6 @@ class Gem::Resolver end def allow_missing?(dependency) - @missing << dependency @soft_missing end @@ -318,30 +312,30 @@ class Gem::Resolver private :amount_constrained end -require 'rubygems/resolver/activation_request' -require 'rubygems/resolver/conflict' -require 'rubygems/resolver/dependency_request' -require 'rubygems/resolver/requirement_list' -require 'rubygems/resolver/stats' - -require 'rubygems/resolver/set' -require 'rubygems/resolver/api_set' -require 'rubygems/resolver/composed_set' -require 'rubygems/resolver/best_set' -require 'rubygems/resolver/current_set' -require 'rubygems/resolver/git_set' -require 'rubygems/resolver/index_set' -require 'rubygems/resolver/installer_set' -require 'rubygems/resolver/lock_set' -require 'rubygems/resolver/vendor_set' -require 'rubygems/resolver/source_set' - -require 'rubygems/resolver/specification' -require 'rubygems/resolver/spec_specification' -require 'rubygems/resolver/api_specification' -require 'rubygems/resolver/git_specification' -require 'rubygems/resolver/index_specification' -require 'rubygems/resolver/installed_specification' -require 'rubygems/resolver/local_specification' -require 'rubygems/resolver/lock_specification' -require 'rubygems/resolver/vendor_specification' +require_relative "resolver/activation_request" +require_relative "resolver/conflict" +require_relative "resolver/dependency_request" +require_relative "resolver/requirement_list" +require_relative "resolver/stats" + +require_relative "resolver/set" +require_relative "resolver/api_set" +require_relative "resolver/composed_set" +require_relative "resolver/best_set" +require_relative "resolver/current_set" +require_relative "resolver/git_set" +require_relative "resolver/index_set" +require_relative "resolver/installer_set" +require_relative "resolver/lock_set" +require_relative "resolver/vendor_set" +require_relative "resolver/source_set" + +require_relative "resolver/specification" +require_relative "resolver/spec_specification" +require_relative "resolver/api_specification" +require_relative "resolver/git_specification" +require_relative "resolver/index_specification" +require_relative "resolver/installed_specification" +require_relative "resolver/local_specification" +require_relative "resolver/lock_specification" +require_relative "resolver/vendor_specification" diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb index ae35681db9..fc9ff58f57 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 ## @@ -130,12 +127,12 @@ class Gem::Resolver::ActivationRequest end def pretty_print(q) # :nodoc: - q.group 2, '[Activation request', ']' do + q.group 2, "[Activation request", "]" do q.breakable q.pp @spec q.breakable - q.text ' for ' + q.text " for " q.pp @request end end diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index 21c9b8920c..3e4dadc40f 100644 --- a/lib/rubygems/resolver/api_set.rb +++ b/lib/rubygems/resolver/api_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The global rubygems pool, available via the rubygems.org API. # Returns instances of APISpecification. @@ -26,13 +27,13 @@ class Gem::Resolver::APISet < Gem::Resolver::Set # API URL +dep_uri+ which is described at # https://guides.rubygems.org/rubygems-org-api - def initialize(dep_uri = 'https://index.rubygems.org/info/') + 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 + '..' + @uri = dep_uri + ".." @data = Hash.new {|h,k| h[k] = [] } @source = Gem::Source.new @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) @@ -83,12 +85,12 @@ class Gem::Resolver::APISet < Gem::Resolver::Set end def pretty_print(q) # :nodoc: - q.group 2, '[APISet', ']' do + q.group 2, "[APISet", "]" do q.breakable q.text "URI: #{@dep_uri}" q.breakable - q.text 'gem names:' + q.text "gem names:" q.pp @data.keys end end diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb index 685c39558d..643b857107 100644 --- a/lib/rubygems/resolver/api_set/gem_parser.rb +++ b/lib/rubygems/resolver/api_set/gem_parser.rb @@ -1,12 +1,15 @@ # frozen_string_literal: true class Gem::Resolver::APISet::GemParser + EMPTY_ARRAY = [].freeze + private_constant :EMPTY_ARRAY + 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) } : EMPTY_ARRAY + requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : EMPTY_ARRAY [version, platform, dependencies, requirements] end @@ -15,6 +18,7 @@ class Gem::Resolver::APISet::GemParser def parse_dependency(string) dependency = string.split(":") 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 b5aa0b71d4..a14bcbfeb1 100644 --- a/lib/rubygems/resolver/api_specification.rb +++ b/lib/rubygems/resolver/api_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Represents a specification retrieved via the rubygems.org API. # @@ -21,7 +22,7 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification # Creates an APISpecification for the given +set+ from the rubygems.org # +api_data+. # - # See https://guides.rubygems.org/rubygems-org-api/#misc_methods for the + # See https://guides.rubygems.org/rubygems-org-api/#misc-methods for the # format of the +api_data+. def initialize(set, api_data) @@ -40,10 +41,10 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification end def ==(other) # :nodoc: - self.class === other and - @set == other.set and - @name == other.name and - @version == other.version and + self.class === other && + @set == other.set && + @name == other.name && + @version == other.version && @platform == other.platform end @@ -62,7 +63,7 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification end def pretty_print(q) # :nodoc: - q.group 2, '[APISpecification', ']' do + q.group 2, "[APISpecification", "]" do q.breakable q.text "name: #{name}" @@ -73,7 +74,7 @@ class Gem::Resolver::APISpecification < Gem::Resolver::Specification q.text "platform: #{platform}" q.breakable - q.text 'dependencies:' + q.text "dependencies:" q.breakable q.pp @dependencies diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index 300ea8015c..a983f8c6b6 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. # @@ -25,7 +26,7 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet end def find_all(req) # :nodoc: - pick_sets if @remote and @sets.empty? + pick_sets if @remote && @sets.empty? super rescue Gem::RemoteFetcher::FetchError => e @@ -35,15 +36,15 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet end def prefetch(reqs) # :nodoc: - pick_sets if @remote and @sets.empty? + pick_sets if @remote && @sets.empty? super end def pretty_print(q) # :nodoc: - q.group 2, '[BestSet', ']' do + q.group 2, "[BestSet", "]" do q.breakable - q.text 'sets:' + q.text "sets:" q.breakable q.pp @sets @@ -59,11 +60,11 @@ class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet def replace_failed_api_set(error) # :nodoc: uri = error.original_uri - uri = URI uri unless URI === uri - uri = uri + "." + uri = Gem::URI uri unless Gem::URI === uri + uri += "." raise error unless api_set = @sets.find do |set| - Gem::Resolver::APISet === set and set.dep_uri == uri + Gem::Resolver::APISet === set && set.dep_uri == uri end index_set = Gem::Resolver::IndexSet.new api_set.source diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb index 226da1e1e0..8a714ad447 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,7 +44,7 @@ class Gem::Resolver::ComposedSet < Gem::Resolver::Set end def errors - @errors + @sets.map {|set| set.errors }.flatten + @errors + @sets.map(&:errors).flatten end ## diff --git a/lib/rubygems/resolver/conflict.rb b/lib/rubygems/resolver/conflict.rb index 4c4588d7e8..367a36b43d 100644 --- a/lib/rubygems/resolver/conflict.rb +++ b/lib/rubygems/resolver/conflict.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Used internally to indicate that a dependency conflicted # with a spec that would be activated. @@ -27,9 +28,9 @@ class Gem::Resolver::Conflict end def ==(other) # :nodoc: - self.class === other and - @dependency == other.dependency and - @activated == other.activated and + self.class === other && + @dependency == other.dependency && + @activated == other.activated && @failed_dep == other.failed_dep end @@ -54,7 +55,7 @@ class Gem::Resolver::Conflict activated = @activated.spec.full_name dependency = @failed_dep.dependency requirement = dependency.requirement - alternates = dependency.matching_specs.map {|spec| spec.full_name } + alternates = dependency.matching_specs.map(&:full_name) unless alternates.empty? matching = <<-MATCHING.chomp @@ -63,10 +64,7 @@ class Gem::Resolver::Conflict %s MATCHING - matching = matching % [ - dependency, - alternates.join(', '), - ] + matching = format(matching, dependency, alternates.join(", ")) end explanation = <<-EXPLANATION @@ -81,12 +79,7 @@ class Gem::Resolver::Conflict %s EXPLANATION - explanation % [ - activated, requirement, - request_path(@activated).reverse.join(", depends on\n "), - request_path(@failed_dep).reverse.join(", depends on\n "), - matching - ] + format(explanation, activated, requirement, request_path(@activated).reverse.join(", depends on\n "), request_path(@failed_dep).reverse.join(", depends on\n "), matching) end ## @@ -97,21 +90,21 @@ class Gem::Resolver::Conflict end def pretty_print(q) # :nodoc: - q.group 2, '[Dependency conflict: ', ']' do + q.group 2, "[Dependency conflict: ", "]" do q.breakable - q.text 'activated ' + q.text "activated " q.pp @activated q.breakable - q.text ' dependency ' + q.text " dependency " q.pp @dependency q.breakable if @dependency == @failed_dep - q.text ' failed' + q.text " failed" else - q.text ' failed dependency ' + q.text " failed dependency " q.pp @failed_dep end end @@ -131,7 +124,7 @@ class Gem::Resolver::Conflict current = current.parent when Gem::Resolver::DependencyRequest then - path << "#{current.dependency}" + path << current.dependency.to_s current = current.requester else @@ -139,7 +132,7 @@ class Gem::Resolver::Conflict end end - path = ['user request (gem command or Gemfile)'] if path.empty? + path = ["user request (gem command or Gemfile)"] if path.empty? path 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 356aadb3b2..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. @@ -95,12 +96,12 @@ class Gem::Resolver::DependencyRequest end def pretty_print(q) # :nodoc: - q.group 2, '[Dependency request ', ']' do + q.group 2, "[Dependency request ", "]" do q.breakable q.text @dependency.to_s q.breakable - q.text ' requested by ' + q.text " requested by " q.pp @requester end end diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb index eac51f15ad..89342ff80d 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,7 @@ class Gem::Resolver::GitSet < Gem::Resolver::Set def initialize # :nodoc: super() - @git = ENV['git'] || 'git' + @git = ENV["git"] || "git" @need_submodules = {} @repositories = {} @root_dir = Gem.dir @@ -104,7 +105,7 @@ class Gem::Resolver::GitSet < Gem::Resolver::Set end def pretty_print(q) # :nodoc: - q.group 2, '[GitSet', ']' do + q.group 2, "[GitSet", "]" do next if @repositories.empty? q.breakable diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb index 555dcffc22..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:+ @@ -6,9 +7,9 @@ class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification def ==(other) # :nodoc: - self.class === other and - @set == other.set and - @spec == other.spec and + self.class === other && + @set == other.set && + @spec == other.spec && @source == other.source end @@ -21,7 +22,7 @@ class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification # the executables. def install(options = {}) - require 'rubygems/installer' + require_relative "../installer" installer = Gem::Installer.for_spec spec, options @@ -35,7 +36,7 @@ class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification end def pretty_print(q) # :nodoc: - q.group 2, '[GitSpecification', ']' do + q.group 2, "[GitSpecification", "]" do q.breakable q.text "name: #{name}" @@ -43,7 +44,7 @@ class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification q.text "version: #{version}" q.breakable - q.text 'dependencies:' + q.text "dependencies:" q.breakable q.pp dependencies diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb index 9390e34255..0b4f376452 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,24 +44,24 @@ 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 end def pretty_print(q) # :nodoc: - q.group 2, '[IndexSet', ']' do + q.group 2, "[IndexSet", "]" do q.breakable - q.text 'sources:' + q.text "sources:" q.breakable q.pp @f.sources q.breakable - q.text 'specs:' + q.text "specs:" q.breakable diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb index 9ea76f40ba..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+ @@ -21,7 +22,8 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification @name = name @version = version @source = source - @platform = platform.to_s + @platform = Gem::Platform.new(platform.to_s) + @original_platform = platform.to_s @spec = nil end @@ -66,21 +68,21 @@ 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: - q.group 2, '[Index specification', ']' do + q.group 2, "[Index specification", "]" do q.breakable q.text full_name - unless Gem::Platform::RUBY == @platform + unless @platform == Gem::Platform::RUBY q.breakable q.text @platform.to_s end q.breakable - q.text 'source ' + q.text "source " q.pp @source end end @@ -91,7 +93,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification def spec # :nodoc: @spec ||= begin - tuple = Gem::NameTuple.new @name, @version, @platform + tuple = Gem::NameTuple.new @name, @version, @original_platform @source.fetch_spec tuple end diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb index 167ba1439e..8280ae4672 100644 --- a/lib/rubygems/resolver/installed_specification.rb +++ b/lib/rubygems/resolver/installed_specification.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + ## # An InstalledSpecification represents a gem that is already installed # locally. class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification def ==(other) # :nodoc: - self.class === other and - @set == other.set and + self.class === other && + @set == other.set && @spec == other.spec end @@ -24,13 +25,13 @@ 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 def pretty_print(q) # :nodoc: - q.group 2, '[InstalledSpecification', ']' do + q.group 2, "[InstalledSpecification", "]" do q.breakable q.text "name: #{name}" @@ -41,7 +42,7 @@ class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification q.text "platform: #{platform}" q.breakable - q.text 'dependencies:' + q.text "dependencies:" q.breakable q.pp spec.dependencies end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index 60181315b0..d9fe36c589 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 @@ -61,38 +62,37 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set found = find_all request found.delete_if do |s| - s.version.prerelease? and not s.local? + s.version.prerelease? && !s.local? end unless dependency.prerelease? found = found.select do |s| - Gem::Source::SpecificFile === s.source or - Gem::Platform::RUBY == s.platform or - Gem::Platform.local === s.platform + Gem::Source::SpecificFile === s.source || + Gem::Platform.match_spec?(s) end found = found.sort_by do |s| - [s.version, s.platform == Gem::Platform::RUBY ? -1 : 1] + [s.version, Gem::Platform.sort_priority(s.platform)] end newest = found.last + unless newest + exc = Gem::UnsatisfiableDependencyError.new request + exc.errors = errors + + raise exc + end + unless @force - found_matching_metadata = found.select do |spec| + found_matching_metadata = found.reverse.find do |spec| metadata_satisfied?(spec) end - if found_matching_metadata.empty? - if newest - ensure_required_ruby_version_met(newest.spec) - ensure_required_rubygems_version_met(newest.spec) - else - exc = Gem::UnsatisfiableDependencyError.new request - exc.errors = errors - - raise exc - end + if found_matching_metadata.nil? + ensure_required_ruby_version_met(newest.spec) + ensure_required_rubygems_version_met(newest.spec) else - newest = found_matching_metadata.last + newest = found_matching_metadata end end @@ -111,14 +111,14 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set # Should local gems should be considered? def consider_local? # :nodoc: - @domain == :both or @domain == :local + @domain == :both || @domain == :local end ## # Should remote gems should be considered? def consider_remote? # :nodoc: - @domain == :both or @domain == :remote + @domain == :both || @domain == :remote end ## @@ -137,8 +137,8 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set dep = req.dependency - return res if @ignore_dependencies and - @always_install.none? {|spec| dep.match? spec } + return res if @ignore_dependencies && + @always_install.none? {|spec| dep.match? spec } name = dep.name @@ -148,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 @@ -161,18 +163,15 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set if local_spec = @local_source.find_gem(name, dep.requirement) 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.delete_if do |spec| - spec.version.prerelease? and not dep.prerelease? - 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 @@ -188,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 ## @@ -219,16 +216,16 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set end def pretty_print(q) # :nodoc: - q.group 2, '[InstallerSet', ']' do + q.group 2, "[InstallerSet", "]" do q.breakable q.text "domain: #{@domain}" q.breakable - q.text 'specs: ' + q.text "specs: " q.pp @specs.keys q.breakable - q.text 'always install: ' + q.text "always install: " q.pp @always_install end end @@ -266,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 9c69c4ab74..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 @@ -17,7 +18,7 @@ class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification end def pretty_print(q) # :nodoc: - q.group 2, '[LocalSpecification', ']' do + q.group 2, "[LocalSpecification", "]" do q.breakable q.text "name: #{name}" @@ -28,7 +29,7 @@ class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification q.text "platform: #{platform}" q.breakable - q.text 'dependencies:' + q.text "dependencies:" q.breakable q.pp dependencies diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb index eabf217aba..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. @@ -54,7 +55,7 @@ class Gem::Resolver::LockSet < Gem::Resolver::Set dep = Gem::Dependency.new name, version found = @specs.find do |spec| - dep.matches_spec? spec and spec.platform == platform + dep.matches_spec?(spec) && spec.platform == platform end tuple = Gem::NameTuple.new found.name, found.version, found.platform @@ -63,18 +64,18 @@ class Gem::Resolver::LockSet < Gem::Resolver::Set end def pretty_print(q) # :nodoc: - q.group 2, '[LockSet', ']' do + q.group 2, "[LockSet", "]" do q.breakable - q.text 'source:' + q.text "source:" q.breakable q.pp @source q.breakable - q.text 'specs:' + 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 cdb8e4e425..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). # @@ -29,7 +30,7 @@ class Gem::Resolver::LockSpecification < Gem::Resolver::Specification def install(options = {}) destination = options[:install_dir] || Gem.dir - if File.exist? File.join(destination, 'specifications', spec.spec_name) + if File.exist? File.join(destination, "specifications", spec.spec_name) yield nil return end @@ -45,7 +46,7 @@ class Gem::Resolver::LockSpecification < Gem::Resolver::Specification end def pretty_print(q) # :nodoc: - q.group 2, '[LockSpecification', ']' do + q.group 2, "[LockSpecification", "]" do q.breakable q.text "name: #{@name}" @@ -59,7 +60,7 @@ class Gem::Resolver::LockSpecification < Gem::Resolver::Specification unless @dependencies.empty? q.breakable - q.text 'dependencies:' + q.text "dependencies:" q.breakable q.pp @dependencies end @@ -71,7 +72,7 @@ class Gem::Resolver::LockSpecification < Gem::Resolver::Specification def spec @spec ||= Gem::Specification.find do |spec| - spec.name == @name and spec.version == @version + spec.name == @name && spec.version == @version end @spec ||= Gem::Specification.new do |s| diff --git a/lib/rubygems/resolver/molinillo.rb b/lib/rubygems/resolver/molinillo.rb deleted file mode 100644 index 2357f41bee..0000000000 --- a/lib/rubygems/resolver/molinillo.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -require 'rubygems/resolver/molinillo/lib/molinillo' 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/gem_metadata.rb b/lib/rubygems/resolver/molinillo/lib/molinillo/gem_metadata.rb deleted file mode 100644 index 6b5ada7ade..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.7.0'.freeze -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 8046e18ea1..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. @@ -20,7 +21,6 @@ class Gem::Resolver::Set attr_accessor :prerelease def initialize # :nodoc: - require 'uri' @prerelease = false @remote = true @errors = [] diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index bf8c23184e..296cf41078 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. # 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 8c6fc9afcf..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 @@ -93,7 +94,7 @@ class Gem::Resolver::Specification # specification. def install(options = {}) - require 'rubygems/installer' + require_relative "../installer" gem = download options diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb index 64b458f504..9920976b2a 100644 --- a/lib/rubygems/resolver/stats.rb +++ b/lib/rubygems/resolver/stats.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::Resolver::Stats def initialize @max_depth = 0 @@ -32,7 +33,7 @@ class Gem::Resolver::Stats @iterations += 1 end - PATTERN = "%20s: %d\n".freeze + PATTERN = "%20s: %d\n" def display $stdout.puts "=== Resolver Statistics ===" diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb index 48c640d8c9..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. @@ -69,7 +70,7 @@ class Gem::Resolver::VendorSet < Gem::Resolver::Set end def pretty_print(q) # :nodoc: - q.group 2, '[VendorSet', ']' do + q.group 2, "[VendorSet", "]" do next if @directories.empty? q.breakable diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb index 8dfe5940f2..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:+ @@ -6,9 +7,9 @@ class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification def ==(other) # :nodoc: - self.class === other and - @set == other.set and - @spec == other.spec and + self.class === other && + @set == other.set && + @spec == other.spec && @source == other.source end diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index f1f9229ca5..7c95a9d4f5 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,6 +1,6 @@ -require 'base64' -require 'digest' -require 'rubygems/openssl' +# frozen_string_literal: true + +require_relative "openssl" ## # S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems @@ -12,7 +12,7 @@ class Gem::S3URISigner end def to_s # :nodoc: - "#{super}" + super.to_s end end @@ -22,7 +22,7 @@ class Gem::S3URISigner end def to_s # :nodoc: - "#{super}" + super.to_s end end @@ -34,7 +34,7 @@ class Gem::S3URISigner ## # 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 @@ -49,7 +49,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 @@ -88,7 +88,7 @@ class Gem::S3URISigner "AWS4-HMAC-SHA256", date_time, credential_info, - Digest::SHA256.hexdigest(canonical_request), + OpenSSL::Digest::SHA256.hexdigest(canonical_request), ].join("\n") end @@ -136,29 +136,29 @@ 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 'rubygems/request' - require 'rubygems/request/connection_pools' - require 'json' + require_relative "vendored_net_http" + require_relative "request" + require_relative "request/connection_pools" + require "json" iam_info = ec2_metadata_request(EC2_IAM_INFO) # Expected format: arn:aws:iam::<id>:instance-profile/<role_name> - role_name = iam_info['InstanceProfileArn'].split('/').last + role_name = iam_info["InstanceProfileArn"].split("/").last ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name) end def ec2_metadata_request(url) - uri = URI(url) + uri = Gem::URI(url) @request_pool ||= create_request_pool(uri) - request = Gem::Request.new(uri, Net::HTTP::Get, nil, @request_pool) + request = Gem::Request.new(uri, Gem::Net::HTTP::Get, nil, @request_pool) response = request.fetch 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}") @@ -172,6 +172,6 @@ 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_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..b81d1a0a47 --- /dev/null +++ b/lib/rubygems/safe_marshal.rb @@ -0,0 +1,74 @@ +# 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], + }.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..740be113e5 --- /dev/null +++ b/lib/rubygems/safe_marshal/reader.rb @@ -0,0 +1,308 @@ +# 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 + + def initialize(io) + @io = io + end + + def read! + read_header + root = read_element + raise UnconsumedBytesError 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_byte + @io.getbyte + 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 + when nil + raise EOFError, "Unexpected EOF" + 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 + when nil + raise EOFError, "Unexpected EOF" + 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 = -@io.read(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 = @io.read(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 = @io.read(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 + elements = Array.new(length) do + read_element + end + Elements::Array.new(elements) + end + + def read_object_with_ivars + object = read_element + ivars = Array.new(read_integer) 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 + pairs = Array.new(read_integer) 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) + ivars = Array.new(read_integer) do + [read_element, read_element] + end + Elements::WithIvars.new(object, ivars) + end + + def read_nil + Elements::Nil::NIL + end + + def read_float + string = @io.read(read_integer) + Elements::Float.new(string) + end + + def read_bignum + sign = read_byte + data = @io.read(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..a9f1d048d4 --- /dev/null +++ b/lib/rubygems/safe_marshal/visitors/to_ruby.rb @@ -0,0 +1,415 @@ +# 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 + until 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 + + marshal_string = "\x04\bIu:\tTime".b + marshal_string.concat(s.size + 5) + marshal_string << s + marshal_string.concat(internal.size + 5) + + internal.each do |k, v| + marshal_string.concat(":") + marshal_string.concat(k.size + 5) + marshal_string.concat(k.to_s) + 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[o.offset] + end + + def visit_Gem_SafeMarshal_Elements_SymbolLink(o) + @symbols[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) + 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 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 29312ad5a1..6a02a48230 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 @@ -17,8 +18,6 @@ module Gem Gem::Specification Gem::Version Gem::Version::Requirement - YAML::Syck::DefaultKey - Syck::DefaultKey ].freeze PERMITTED_SYMBOLS = %w[ @@ -26,34 +25,21 @@ module Gem runtime ].freeze - if ::YAML.respond_to? :safe_load - def self.safe_load(input) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new('3.1.0.pre1') - ::YAML.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: true) - else - ::YAML.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') - ::YAML.safe_load(input, permitted_classes: [::Symbol]) - else - ::YAML.safe_load(input, [::Symbol]) - end - end - else - unless Gem::Deprecate.skip - warn "YAML 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) - ::YAML.load input - end + def self.safe_load(input) + ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled) + end - def self.load(input) - ::YAML.load input - end + def self.load(input) + ::Psych.safe_load(input, permitted_classes: [::Symbol]) end end end diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb index c80639af6d..69ba87b07f 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/exceptions' -require_relative 'openssl' +require_relative "exceptions" +require_relative "openssl" ## # = Signing gems @@ -152,6 +153,7 @@ require_relative 'openssl' # certificate for EMAIL_ADDR # -C, --certificate CERT Signing certificate for --sign # -K, --private-key KEY Key for --sign or --build +# -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA. # -s, --sign CERT Signs CERT with the key from -K # and the certificate from -C # -d, --days NUMBER_OF_DAYS Days before the certificate expires @@ -260,7 +262,7 @@ require_relative 'openssl' # 2. Grab the public key from the gemspec # # gem spec some_signed_gem-1.0.gem cert_chain | \ -# ruby -ryaml -e 'puts YAML.load($stdin)' > public_key.crt +# ruby -rpsych -e 'puts Psych.load($stdin)' > public_key.crt # # 3. Generate a SHA1 hash of the data.tar.gz # @@ -317,15 +319,13 @@ require_relative 'openssl' # * Honor extension restrictions # * Might be better to store the certificate chain as a PKCS#7 or PKCS#12 # file, instead of an array embedded in the metadata. -# * Flexible signature and key algorithms, not hard-coded to RSA and SHA1. # # == Original author # # Paul Duncan <pabs@pablotron.org> -# http://pablotron.org/ +# https://pablotron.org/ module Gem::Security - ## # Gem::Security default exception type @@ -334,31 +334,33 @@ module Gem::Security ## # Used internally to select the signing digest from all computed digests - DIGEST_NAME = 'SHA256' # :nodoc: + DIGEST_NAME = "SHA256" # :nodoc: ## - # Algorithm for creating the key pair used to sign gems + # Length of keys created by RSA and DSA keys - KEY_ALGORITHM = - if defined?(OpenSSL::PKey::RSA) - OpenSSL::PKey::RSA - end + RSA_DSA_KEY_LENGTH = 3072 + + ## + # Default algorithm to use when building a key pair + + DEFAULT_KEY_ALGORITHM = "RSA" ## - # Length of keys created by KEY_ALGORITHM + # Named curve used for Elliptic Curve - KEY_LENGTH = 3072 + EC_NAME = "secp384r1" ## # Cipher used to encrypt the key pair used to sign gems. # Must be in the list returned by OpenSSL::Cipher.ciphers - KEY_CIPHER = OpenSSL::Cipher.new('AES-256-CBC') if defined?(OpenSSL::Cipher) + KEY_CIPHER = OpenSSL::Cipher.new("AES-256-CBC") if defined?(OpenSSL::Cipher) ## # One day in seconds - ONE_DAY = 86400 + ONE_DAY = 86_400 ## # One year in seconds @@ -374,10 +376,10 @@ module Gem::Security # * The certificate contains a subject key identifier EXTENSIONS = { - 'basicConstraints' => 'CA:FALSE', - 'keyUsage' => - 'keyEncipherment,dataEncipherment,digitalSignature', - 'subjectKeyIdentifier' => 'hash', + "basicConstraints" => "CA:FALSE", + "keyUsage" => + "keyEncipherment,dataEncipherment,digitalSignature", + "subjectKeyIdentifier" => "hash", }.freeze def self.alt_name_or_x509_entry(certificate, x509_entry) @@ -396,11 +398,10 @@ 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 = key.public_key + cert.public_key = get_public_key(key) cert.version = 2 cert.serial = serial @@ -419,6 +420,19 @@ module Gem::Security end ## + # Gets the right public key from a PKey instance + + def self.get_public_key(key) + # Ruby 3.0 (Ruby/OpenSSL 2.2) or later + return OpenSSL::PKey.read(key.public_to_der) if key.respond_to?(:public_to_der) + return key.public_key unless key.is_a?(OpenSSL::PKey::EC) + + ec_key = OpenSSL::PKey::EC.new(key.group.curve_name) + ec_key.public_key = key.public_key + ec_key + end + + ## # Creates a self-signed certificate with an issuer and subject from +email+, # a subject alternative name of +email+ and the given +extensions+ for the # +key+. @@ -435,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 @@ -446,39 +459,44 @@ 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 ## - # Creates a new key pair of the specified +length+ and +algorithm+. The - # default is a 3072 bit RSA key. - - def self.create_key(length = KEY_LENGTH, algorithm = KEY_ALGORITHM) - algorithm.new length + # Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC + # are supported. + + def self.create_key(algorithm) + if defined?(OpenSSL::PKey) + case algorithm.downcase + when "dsa" + OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH) + when "rsa" + OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH) + when "ec" + OpenSSL::PKey::EC.generate(EC_NAME) + else + raise Gem::Security::Exception, + "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported." + end + end end ## # Turns +email_address+ into an OpenSSL::X509::Name def self.email_to_name(email_address) - email_address = email_address.gsub(/[^\w@.-]+/i, '_') + email_address = email_address.gsub(/[^\w@.-]+/i, "_") - cn, dcs = email_address.split '@' + cn, dcs = email_address.split "@" - dcs = dcs.split '.' + dcs = dcs.split "." - name = "CN=#{cn}/#{dcs.map {|dc| "DC=#{dc}" }.join '/'}" - - OpenSSL::X509::Name.parse name + OpenSSL::X509::Name.new([ + ["CN", cn], + *dcs.map {|dc| ["DC", dc] }, + ]) end ## @@ -487,12 +505,11 @@ 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.public_key.to_pem == private_key.public_key.to_pem + expired_certificate.subject.to_s unless + expired_certificate.check_private_key(private_key) unless expired_certificate.subject.to_s == expired_certificate.issuer.to_s @@ -500,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 @@ -524,23 +541,22 @@ 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 alt_name = certificate.extensions.find do |extension| - extension.oid == 'subjectAltName' + extension.oid == "subjectAltName" end - extensions = extensions.merge 'subjectAltName' => alt_name.value if + extensions = extensions.merge "subjectAltName" => alt_name.value if alt_name issuer_alt_name = signing_cert.extensions.find do |extension| - extension.oid == 'subjectAltName' + extension.oid == "subjectAltName" end - extensions = extensions.merge 'issuerAltName' => issuer_alt_name.value if + extensions = extensions.merge "issuerAltName" => issuer_alt_name.value if issuer_alt_name signed = create_cert signee_subject, signee_key, age, extensions, serial @@ -556,7 +572,7 @@ module Gem::Security def self.trust_dir return @trust_dir if @trust_dir - dir = File.join Gem.user_home, '.gem', 'trust' + dir = File.join Gem.user_home, ".gem", "trust" @trust_dir ||= Gem::Security::TrustDir.new dir end @@ -573,11 +589,11 @@ 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| - if passphrase and cipher + File.open path, "wb", permissions do |io| + if passphrase && cipher io.write pemmable.to_pem cipher, passphrase else io.write pemmable.to_pem @@ -588,13 +604,12 @@ module Gem::Security end reset - end if Gem::HAVE_OPENSSL - require 'rubygems/security/policy' - require 'rubygems/security/policies' - require 'rubygems/security/trust_dir' + require_relative "security/policy" + require_relative "security/policies" + require_relative "security/trust_dir" end -require 'rubygems/security/signer' +require_relative "security/signer" diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb index 8f6ad99316..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 + "No Security", + verify_data: false, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -23,13 +23,13 @@ module Gem::Security # easily spoofed, and is not recommended. AlmostNoSecurity = Policy.new( - 'Almost No Security', - :verify_data => true, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + "Almost No Security", + verify_data: true, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -40,13 +40,13 @@ module Gem::Security # is not recommended. LowSecurity = Policy.new( - 'Low Security', - :verify_data => true, - :verify_signer => true, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + "Low Security", + verify_data: true, + verify_signer: true, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -59,13 +59,13 @@ module Gem::Security # gem off as unsigned. MediumSecurity = Policy.new( - 'Medium Security', - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => false + "Medium Security", + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: false ) ## @@ -78,38 +78,37 @@ module Gem::Security # a reasonable guarantee that the contents of the gem have not been altered. HighSecurity = Policy.new( - 'High Security', - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => true + "High Security", + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: true ) ## # Policy used to verify a certificate and key when signing a gem SigningPolicy = Policy.new( - 'Signing Policy', - :verify_data => false, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => false, - :only_signed => false + "Signing Policy", + verify_data: false, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: false, + only_signed: false ) ## # Hash of configured security policies Policies = { - 'NoSecurity' => NoSecurity, - 'AlmostNoSecurity' => AlmostNoSecurity, - 'LowSecurity' => LowSecurity, - 'MediumSecurity' => MediumSecurity, - 'HighSecurity' => HighSecurity, + "NoSecurity" => NoSecurity, + "AlmostNoSecurity" => AlmostNoSecurity, + "LowSecurity" => LowSecurity, + "MediumSecurity" => MediumSecurity, + "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 7629d64796..7b86ac5763 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require 'rubygems/user_interaction' + +require_relative "../user_interaction" ## # A Gem::Security::Policy object encapsulates the settings for verifying @@ -53,8 +54,8 @@ class Gem::Security::Policy # and is valid for the given +time+. def check_chain(chain, time) - raise Gem::Security::Exception, 'missing signing chain' unless chain - raise Gem::Security::Exception, 'empty signing chain' if chain.empty? + raise Gem::Security::Exception, "missing signing chain" unless chain + raise Gem::Security::Exception, "empty signing chain" if chain.empty? begin chain.each_cons 2 do |issuer, cert| @@ -83,21 +84,21 @@ class Gem::Security::Policy # If the +issuer+ is +nil+ no verification is performed. def check_cert(signer, issuer, time) - raise Gem::Security::Exception, 'missing signing certificate' unless + raise Gem::Security::Exception, "missing signing certificate" unless signer message = "certificate #{signer.subject}" - if not_before = signer.not_before and not_before > time + if (not_before = signer.not_before) && not_before > time raise Gem::Security::Exception, "#{message} not valid before #{not_before}" end - if not_after = signer.not_after and not_after < time + if (not_after = signer.not_after) && not_after < time raise Gem::Security::Exception, "#{message} not valid after #{not_after}" end - if issuer and not signer.verify issuer.public_key + if issuer && !signer.verify(issuer.public_key) raise Gem::Security::Exception, "#{message} was not issued by #{issuer.subject}" end @@ -109,15 +110,15 @@ class Gem::Security::Policy # Ensures the public key of +key+ matches the public key in +signer+ def check_key(signer, key) - unless signer and key + unless signer && key return true unless @only_signed - raise Gem::Security::Exception, 'missing key or signature' + raise Gem::Security::Exception, "missing key or signature" end raise Gem::Security::Exception, "certificate #{signer.subject} does not match the signing key" unless - signer.public_key.to_pem == key.public_key.to_pem + signer.check_private_key(key) true end @@ -127,14 +128,14 @@ class Gem::Security::Policy # +time+. def check_root(chain, time) - raise Gem::Security::Exception, 'missing signing chain' unless chain + raise Gem::Security::Exception, "missing signing chain" unless chain root = chain.first - raise Gem::Security::Exception, 'missing root certificate' unless root + 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 @@ -146,11 +147,11 @@ class Gem::Security::Policy # the digests of the two certificates match according to +digester+ def check_trust(chain, digester, trust_dir) - raise Gem::Security::Exception, 'missing signing chain' unless chain + raise Gem::Security::Exception, "missing signing chain" unless chain root = chain.first - raise Gem::Security::Exception, 'missing root certificate' unless root + raise Gem::Security::Exception, "missing root certificate" unless root path = Gem::Security.trust_dir.cert_path root @@ -164,13 +165,13 @@ class Gem::Security::Policy end save_cert = OpenSSL::X509::Certificate.new File.read path - save_dgst = digester.digest save_cert.public_key.to_s + save_dgst = digester.digest save_cert.public_key.to_pem - pkey_str = root.public_key.to_s + pkey_str = root.public_key.to_pem 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 @@ -182,7 +183,7 @@ class Gem::Security::Policy def subject(certificate) # :nodoc: certificate.extensions.each do |extension| - next unless extension.oid == 'subjectAltName' + next unless extension.oid == "subjectAltName" return extension.value end @@ -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,13 +222,13 @@ 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 if @verify_data - raise Gem::Security::Exception, 'no digests provided (probable bug)' if - signer_digests.nil? or signer_digests.empty? + raise Gem::Security::Exception, "no digests provided (probable bug)" if + signer_digests.nil? || signer_digests.empty? else signer_digests = {} end @@ -248,7 +245,7 @@ class Gem::Security::Policy if @only_trusted check_trust chain, digester, trust_dir - elsif signatures.empty? and digests.empty? + elsif signatures.empty? && digests.empty? # trust is irrelevant if there's no signatures to verify else alert_warning "#{subject signer} is not trusted for #{full_name}" @@ -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 6c85ab08d2..5732fb57fd 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -1,8 +1,9 @@ # frozen_string_literal: true + ## # Basic OpenSSL-based package signing class. -require "rubygems/user_interaction" +require_relative "../user_interaction" class Gem::Security::Signer include Gem::UserInteraction @@ -42,7 +43,7 @@ class Gem::Security::Signer def self.re_sign_cert(expired_cert, expired_cert_path, private_key) return unless expired_cert.not_after < Time.now - expiry = expired_cert.not_after.strftime('%Y%m%d%H%M%S') + expiry = expired_cert.not_after.strftime("%Y%m%d%H%M%S") expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}" new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file) @@ -83,8 +84,8 @@ class Gem::Security::Signer @digest_name = Gem::Security::DIGEST_NAME @digest_algorithm = Gem::Security.create_digest(@digest_name) - if @key && !@key.is_a?(OpenSSL::PKey::RSA) - @key = OpenSSL::PKey::RSA.new(File.read(@key), @passphrase) + if @key && !@key.is_a?(OpenSSL::PKey::PKey) + @key = OpenSSL::PKey.read(File.read(@key), @passphrase) end if @cert_chain @@ -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 @@ -139,9 +140,9 @@ class Gem::Security::Signer def sign(data) return unless @key - raise Gem::Security::Exception, 'no certs provided' if @cert_chain.empty? + raise Gem::Security::Exception, "no certs provided" if @cert_chain.empty? - if @cert_chain.length == 1 and @cert_chain.last.not_after < Time.now + if @cert_chain.length == 1 && @cert_chain.last.not_after < Time.now alert("Your certificate has expired, trying to re-sign it...") re_sign_key( @@ -174,16 +175,23 @@ 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::RSA.new(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 if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem - expiry = old_cert.not_after.strftime('%Y%m%d%H%M%S') + expiry = old_cert.not_after.strftime("%Y%m%d%H%M%S") old_cert_file = "gem-public_cert.pem.expired.#{expiry}" old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file) diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb index 456947274c..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 ## @@ -41,16 +42,14 @@ class Gem::Security::TrustDir def each_certificate return enum_for __method__ unless block_given? - glob = File.join @dir, '*.pem' + 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 @@ -104,15 +103,15 @@ class Gem::Security::TrustDir # permissions. def verify - require 'fileutils' + require "fileutils" if File.exist? @dir raise Gem::Security::Exception, "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 3403aaaf05..3a101fe9db 100644 --- a/lib/rubygems/security_option.rb +++ b/lib/rubygems/security_option.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 'rubygems' +require_relative "../rubygems" # forward-declare @@ -19,23 +20,23 @@ end module Gem::SecurityOption def add_security_option - OptionParser.accept Gem::Security::Policy do |value| - require 'rubygems/security' + Gem::OptionParser.accept Gem::Security::Policy do |value| + require_relative "security" - raise OptionParser::InvalidArgument, 'OpenSSL not installed' unless + raise Gem::OptionParser::InvalidArgument, "OpenSSL not installed" unless defined?(Gem::Security::HighSecurity) policy = Gem::Security::Policies[value] unless policy valid = Gem::Security::Policies.keys.sort - raise OptionParser::InvalidArgument, "#{value} (#{valid.join ', '} are valid)" + raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ", "} are valid)" end policy end - add_option(:"Install/Update", '-P', '--trust-policy POLICY', + add_option(:"Install/Update", "-P", "--trust-policy POLICY", Gem::Security::Policy, - 'Specify gem trust policy') do |value, options| + "Specify gem trust policy") do |value, options| options[:security_policy] = value end end diff --git a/lib/rubygems/server.rb b/lib/rubygems/server.rb deleted file mode 100644 index 2c2805f31c..0000000000 --- a/lib/rubygems/server.rb +++ /dev/null @@ -1,882 +0,0 @@ -# frozen_string_literal: true -require 'zlib' -require 'erb' -require 'uri' - -require 'rubygems' -require 'rubygems/rdoc' - -## -# Gem::Server and allows users to serve gems for consumption by -# `gem --remote-install`. -# -# gem_server starts an HTTP server on the given port and serves the following: -# * "/" - Browsing of gem spec files for installed gems -# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index -# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs -# name/version/platform index -# * "/quick/" - Individual gemspecs -# * "/gems" - Direct access to download the installable gems -# * "/rdoc?q=" - Search for installed rdoc documentation -# -# == Usage -# -# gem_server = Gem::Server.new Gem.dir, 8089, false -# gem_server.run -# -#-- -# TODO Refactor into a real WEBrick servlet to remove code duplication. - -class Gem::Server - attr_reader :spec_dirs - - include ERB::Util - include Gem::UserInteraction - - SEARCH = <<-ERB.freeze - <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc"> - <div id="search" style="float:right"> - <label for="q">Filter/Search</label> - <input id="q" type="text" style="width:10em" name="q"> - <button type="submit" style="display:none"></button> - </div> - </form> - ERB - - DOC_TEMPLATE = <<-'ERB'.freeze - <?xml version="1.0" encoding="iso-8859-1"?> - <!DOCTYPE html - PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> - - <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> - <head> - <title>RubyGems Documentation Index</title> - <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> - </head> - <body> - <div id="fileHeader"> -<%= SEARCH %> - <h1>RubyGems Documentation Index</h1> - </div> - <!-- banner header --> - - <div id="bodyContent"> - <div id="contextContent"> - <div id="description"> - <h1>Summary</h1> - <p>There are <%=values["gem_count"]%> gems installed:</p> - <p> - <%= values["specs"].map { |v| "<a href=\"##{u v["name"]}\">#{h v["name"]}</a>" }.join ', ' %>. - <h1>Gems</h1> - - <dl> - <% values["specs"].each do |spec| %> - <dt> - <% if spec["first_name_entry"] then %> - <a name="<%=h spec["name"]%>"></a> - <% end %> - - <b><%=h spec["name"]%> <%=h spec["version"]%></b> - - <% if spec["ri_installed"] || spec["rdoc_installed"] then %> - <a href="<%=spec["doc_path"]%>">[rdoc]</a> - <% else %> - <span title="rdoc not installed">[rdoc]</span> - <% end %> - - <% if spec["homepage"] then %> - <a href="<%=uri_encode spec["homepage"]%>" title="<%=h spec["homepage"]%>">[www]</a> - <% else %> - <span title="no homepage available">[www]</span> - <% end %> - - <% if spec["has_deps"] then %> - - depends on - <%= spec["dependencies"].map { |v| "<a href=\"##{u v["name"]}\">#{h v["name"]}</a>" }.join ', ' %>. - <% end %> - </dt> - <dd> - <%=spec["summary"]%> - <% if spec["executables"] then %> - <br/> - - <% if spec["only_one_executable"] then %> - Executable is - <% else %> - Executables are - <%end%> - - <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{h v["executable"]}</span>"}.join ', ' %>. - - <%end%> - <br/> - <br/> - </dd> - <% end %> - </dl> - - </div> - </div> - </div> - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - </div> - </body> - </html> - ERB - - # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108 - RDOC_CSS = <<-CSS.freeze -body { - font-family: Verdana,Arial,Helvetica,sans-serif; - font-size: 90%; - margin: 0; - margin-left: 40px; - padding: 0; - background: white; -} - -h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; } -h1 { font-size: 150%; } -h2,h3,h4 { margin-top: 1em; } - -a { background: #eef; color: #039; text-decoration: none; } -a:hover { background: #039; color: #eef; } - -/* Override the base stylesheets Anchor inside a table cell */ -td > a { - background: transparent; - color: #039; - text-decoration: none; -} - -/* and inside a section title */ -.section-title > a { - background: transparent; - color: #eee; - text-decoration: none; -} - -/* === Structural elements =================================== */ - -div#index { - margin: 0; - margin-left: -40px; - padding: 0; - font-size: 90%; -} - - -div#index a { - margin-left: 0.7em; -} - -div#index .section-bar { - margin-left: 0px; - padding-left: 0.7em; - background: #ccc; - font-size: small; -} - - -div#classHeader, div#fileHeader { - width: auto; - color: white; - padding: 0.5em 1.5em 0.5em 1.5em; - margin: 0; - margin-left: -40px; - border-bottom: 3px solid #006; -} - -div#classHeader a, div#fileHeader a { - background: inherit; - color: white; -} - -div#classHeader td, div#fileHeader td { - background: inherit; - color: white; -} - - -div#fileHeader { - background: #057; -} - -div#classHeader { - background: #048; -} - - -.class-name-in-header { - font-size: 180%; - font-weight: bold; -} - - -div#bodyContent { - padding: 0 1.5em 0 1.5em; -} - -div#description { - padding: 0.5em 1.5em; - background: #efefef; - border: 1px dotted #999; -} - -div#description h1,h2,h3,h4,h5,h6 { - color: #125;; - background: transparent; -} - -div#validator-badges { - text-align: center; -} -div#validator-badges img { border: 0; } - -div#copyright { - color: #333; - background: #efefef; - font: 0.75em sans-serif; - margin-top: 5em; - margin-bottom: 0; - padding: 0.5em 2em; -} - - -/* === Classes =================================== */ - -table.header-table { - color: white; - font-size: small; -} - -.type-note { - font-size: small; - color: #DEDEDE; -} - -.xxsection-bar { - background: #eee; - color: #333; - padding: 3px; -} - -.section-bar { - color: #333; - border-bottom: 1px solid #999; - margin-left: -20px; -} - - -.section-title { - background: #79a; - color: #eee; - padding: 3px; - margin-top: 2em; - margin-left: -30px; - border: 1px solid #999; -} - -.top-aligned-row { vertical-align: top } -.bottom-aligned-row { vertical-align: bottom } - -/* --- Context section classes ----------------------- */ - -.context-row { } -.context-item-name { font-family: monospace; font-weight: bold; color: black; } -.context-item-value { font-size: small; color: #448; } -.context-item-desc { color: #333; padding-left: 2em; } - -/* --- Method classes -------------------------- */ -.method-detail { - background: #efefef; - padding: 0; - margin-top: 0.5em; - margin-bottom: 1em; - border: 1px dotted #ccc; -} -.method-heading { - color: black; - background: #ccc; - border-bottom: 1px solid #666; - padding: 0.2em 0.5em 0 0.5em; -} -.method-signature { color: black; background: inherit; } -.method-name { font-weight: bold; } -.method-args { font-style: italic; } -.method-description { padding: 0 0.5em 0 0.5em; } - -/* --- Source code sections -------------------- */ - -a.source-toggle { font-size: 90%; } -div.method-source-code { - background: #262626; - color: #ffdead; - margin: 1em; - padding: 0.5em; - border: 1px dashed #999; - overflow: hidden; -} - -div.method-source-code pre { color: #ffdead; overflow: hidden; } - -/* --- Ruby keyword styles --------------------- */ - -.standalone-code { background: #221111; color: #ffdead; overflow: hidden; } - -.ruby-constant { color: #7fffd4; background: transparent; } -.ruby-keyword { color: #00ffff; background: transparent; } -.ruby-ivar { color: #eedd82; background: transparent; } -.ruby-operator { color: #00ffee; background: transparent; } -.ruby-identifier { color: #ffdead; background: transparent; } -.ruby-node { color: #ffa07a; background: transparent; } -.ruby-comment { color: #b22222; font-weight: bold; background: transparent; } -.ruby-regexp { color: #ffa07a; background: transparent; } -.ruby-value { color: #7fffd4; background: transparent; } - CSS - - RDOC_NO_DOCUMENTATION = <<-'ERB'.freeze -<?xml version="1.0" encoding="iso-8859-1"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> - <head> - <title>Found documentation</title> - <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> - </head> - <body> - <div id="fileHeader"> -<%= SEARCH %> - <h1>No documentation found</h1> - </div> - - <div id="bodyContent"> - <div id="contextContent"> - <div id="description"> - <p>No gems matched <%= h query.inspect %></p> - - <p> - Back to <a href="/">complete gem index</a> - </p> - - </div> - </div> - </div> - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - </div> - </body> -</html> - ERB - - RDOC_SEARCH_TEMPLATE = <<-'ERB'.freeze -<?xml version="1.0" encoding="iso-8859-1"?> -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" - "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> - <head> - <title>Found documentation</title> - <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" /> - </head> - <body> - <div id="fileHeader"> -<%= SEARCH %> - <h1>Found documentation</h1> - </div> - <!-- banner header --> - - <div id="bodyContent"> - <div id="contextContent"> - <div id="description"> - <h1>Summary</h1> - <p><%=doc_items.length%> documentation topics found.</p> - <h1>Topics</h1> - - <dl> - <% doc_items.each do |doc_item| %> - <dt> - <b><%=doc_item[:name]%></b> - <a href="<%=u doc_item[:url]%>">[rdoc]</a> - </dt> - <dd> - <%=h doc_item[:summary]%> - <br/> - <br/> - </dd> - <% end %> - </dl> - - <p> - Back to <a href="/">complete gem index</a> - </p> - - </div> - </div> - </div> - <div id="validator-badges"> - <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p> - </div> - </body> -</html> - ERB - - def self.run(options) - new(options[:gemdir], options[:port], options[:daemon], - options[:launch], options[:addresses]).run - end - - def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil) - begin - require 'webrick' - rescue LoadError - abort "webrick is not found. You may need to `gem install webrick` to install webrick." - end - - Gem::RDoc.load_rdoc - Socket.do_not_reverse_lookup = true - - @gem_dirs = Array gem_dirs - @port = port - @daemon = daemon - @launch = launch - @addresses = addresses - - logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL - @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger - - @spec_dirs = @gem_dirs.map {|gem_dir| File.join gem_dir, 'specifications' } - @spec_dirs.reject! {|spec_dir| !File.directory? spec_dir } - - reset_gems - - @have_rdoc_4_plus = nil - end - - def add_date(res) - res['date'] = @spec_dirs.map do |spec_dir| - File.stat(spec_dir).mtime - end.max - end - - def uri_encode(str) - str.gsub(URI::UNSAFE) do |match| - match.each_byte.map {|c| sprintf('%%%02X', c.ord) }.join - end - end - - def doc_root(gem_name) - if have_rdoc_4_plus? - "/doc_root/#{u gem_name}/" - else - "/doc_root/#{u gem_name}/rdoc/index.html" - end - end - - def have_rdoc_4_plus? - @have_rdoc_4_plus ||= - Gem::Requirement.new('>= 4.0.0.preview2').satisfied_by? Gem::RDoc.rdoc_version - end - - def latest_specs(req, res) - reset_gems - - res['content-type'] = 'application/x-gzip' - - add_date res - - latest_specs = Gem::Specification.latest_specs - - specs = latest_specs.sort.map do |spec| - platform = spec.original_platform || Gem::Platform::RUBY - [spec.name, spec.version, platform] - end - - specs = Marshal.dump specs - - if req.path =~ /\.gz$/ - specs = Gem::Util.gzip specs - res['content-type'] = 'application/x-gzip' - else - res['content-type'] = 'application/octet-stream' - end - - if req.request_method == 'HEAD' - res['content-length'] = specs.length - else - res.body << specs - end - end - - ## - # Creates server sockets based on the addresses option. If no addresses - # were given a server socket for all interfaces is created. - - def listen(addresses = @addresses) - addresses = [nil] unless addresses - - listeners = 0 - - addresses.each do |address| - begin - @server.listen address, @port - @server.listeners[listeners..-1].each do |listener| - host, port = listener.addr.values_at 2, 1 - host = "[#{host}]" if host =~ /:/ # we don't reverse lookup - say "Server started at http://#{host}:#{port}" - end - - listeners = @server.listeners.length - rescue SystemCallError - next - end - end - - if @server.listeners.empty? - say "Unable to start a server." - say "Check for running servers or your --bind and --port arguments" - terminate_interaction 1 - end - end - - def prerelease_specs(req, res) - reset_gems - - res['content-type'] = 'application/x-gzip' - - add_date res - - specs = Gem::Specification.select do |spec| - spec.version.prerelease? - end.sort.map do |spec| - platform = spec.original_platform || Gem::Platform::RUBY - [spec.name, spec.version, platform] - end - - specs = Marshal.dump specs - - if req.path =~ /\.gz$/ - specs = Gem::Util.gzip specs - res['content-type'] = 'application/x-gzip' - else - res['content-type'] = 'application/octet-stream' - end - - if req.request_method == 'HEAD' - res['content-length'] = specs.length - else - res.body << specs - end - end - - def quick(req, res) - reset_gems - - res['content-type'] = 'text/plain' - add_date res - - case req.request_uri.path - when %r{^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)\.gemspec\.rz$} then - marshal_format, full_name = $1, $2 - specs = Gem::Specification.find_all_by_full_name(full_name) - - selector = full_name.inspect - - if specs.empty? - res.status = 404 - res.body = "No gems found matching #{selector}" - elsif specs.length > 1 - res.status = 500 - res.body = "Multiple gems found matching #{selector}" - elsif marshal_format - res['content-type'] = 'application/x-deflate' - res.body << Gem.deflate(Marshal.dump(specs.first)) - end - else - raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." - end - end - - def root(req, res) - reset_gems - - add_date res - - raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless - req.path == '/' - - specs = [] - total_file_count = 0 - - Gem::Specification.each do |spec| - total_file_count += spec.files.size - deps = spec.dependencies.map do |dep| - { - "name" => dep.name, - "type" => dep.type, - "version" => dep.requirement.to_s, - } - end - - deps = deps.sort_by {|dep| [dep["name"].downcase, dep["version"]] } - deps.last["is_last"] = true unless deps.empty? - - # executables - executables = spec.executables.sort.collect {|exec| {"executable" => exec} } - executables = nil if executables.empty? - executables.last["is_last"] = true if executables - - # Pre-process spec homepage for safety reasons - begin - homepage_uri = URI.parse(spec.homepage) - if [URI::HTTP, URI::HTTPS].member? homepage_uri.class - homepage_uri = spec.homepage - else - homepage_uri = "." - end - rescue URI::InvalidURIError - homepage_uri = "." - end - - specs << { - "authors" => spec.authors.sort.join(", "), - "date" => spec.date.to_s, - "dependencies" => deps, - "doc_path" => doc_root(spec.full_name), - "executables" => executables, - "only_one_executable" => (executables && executables.size == 1), - "full_name" => spec.full_name, - "has_deps" => !deps.empty?, - "homepage" => homepage_uri, - "name" => spec.name, - "rdoc_installed" => Gem::RDoc.new(spec).rdoc_installed?, - "ri_installed" => Gem::RDoc.new(spec).ri_installed?, - "summary" => spec.summary, - "version" => spec.version.to_s, - } - end - - specs << { - "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others", - "dependencies" => [], - "doc_path" => doc_root("rubygems-#{Gem::VERSION}"), - "executables" => [{"executable" => 'gem', "is_last" => true}], - "only_one_executable" => true, - "full_name" => "rubygems-#{Gem::VERSION}", - "has_deps" => false, - "homepage" => "https://guides.rubygems.org/", - "name" => 'rubygems', - "ri_installed" => true, - "summary" => "RubyGems itself", - "version" => Gem::VERSION, - } - - specs = specs.sort_by {|spec| [spec["name"].downcase, spec["version"]] } - specs.last["is_last"] = true - - # tag all specs with first_name_entry - last_spec = nil - specs.each do |spec| - is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase) - spec["first_name_entry"] = is_first - last_spec = spec - end - - # create page from template - template = ERB.new(DOC_TEMPLATE) - res['content-type'] = 'text/html' - - values = { "gem_count" => specs.size.to_s, "specs" => specs, - "total_file_count" => total_file_count.to_s } - - # suppress 1.9.3dev warning about unused variable - values = values - - result = template.result binding - res.body = result - end - - ## - # Can be used for quick navigation to the rdoc documentation. You can then - # define a search shortcut for your browser. E.g. in Firefox connect - # 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can - # directly open the ActionPack documentation by typing 'rdoc actionp'. If - # there are multiple hits for the search term, they are presented as a list - # with links. - # - # Search algorithm aims for an intuitive search: - # 1. first try to find the gems and documentation folders which name - # starts with the search term - # 2. search for entries, that *contain* the search term - # 3. show all the gems - # - # If there is only one search hit, user is immediately redirected to the - # documentation for the particular gem, otherwise a list with results is - # shown. - # - # === Additional trick - install documentation for Ruby core - # - # Note: please adjust paths accordingly use for example 'locate yaml.rb' and - # 'gem environment' to identify directories, that are specific for your - # local installation - # - # 1. install Ruby sources - # cd /usr/src - # sudo apt-get source ruby - # - # 2. generate documentation - # rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \ - # /usr/lib/ruby/1.8 ruby1.8-1.8.7.72 - # - # By typing 'rdoc core' you can now access the core documentation - - def rdoc(req, res) - query = req.query['q'] - show_rdoc_for_pattern("#{query}*", res) && return - show_rdoc_for_pattern("*#{query}*", res) && return - - template = ERB.new RDOC_NO_DOCUMENTATION - - res['content-type'] = 'text/html' - res.body = template.result binding - end - - ## - # Updates the server to use the latest installed gems. - - def reset_gems # :nodoc: - Gem::Specification.dirs = @gem_dirs - end - - ## - # Returns true and prepares http response, if rdoc for the requested gem - # name pattern was found. - # - # The search is based on the file system content, not on the gems metadata. - # This allows additional documentation folders like 'core' for the Ruby core - # documentation - just put it underneath the main doc folder. - - def show_rdoc_for_pattern(pattern, res) - found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select do |path| - File.exist? File.join(path, 'rdoc/index.html') - end - case found_gems.length - when 0 - return false - when 1 - new_path = File.basename(found_gems[0]) - res.status = 302 - res['Location'] = doc_root new_path - return true - else - doc_items = [] - found_gems.each do |file_name| - base_name = File.basename(file_name) - doc_items << { - :name => base_name, - :url => doc_root(new_path), - :summary => '', - } - end - - template = ERB.new(RDOC_SEARCH_TEMPLATE) - res['content-type'] = 'text/html' - result = template.result binding - res.body = result - return true - end - end - - def run - listen - - WEBrick::Daemon.start if @daemon - - @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs) - @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs) - - @server.mount_proc "/latest_specs.#{Gem.marshal_version}", - method(:latest_specs) - @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz", - method(:latest_specs) - - @server.mount_proc "/prerelease_specs.#{Gem.marshal_version}", - method(:prerelease_specs) - @server.mount_proc "/prerelease_specs.#{Gem.marshal_version}.gz", - method(:prerelease_specs) - - @server.mount_proc "/quick/", method(:quick) - - @server.mount_proc("/gem-server-rdoc-style.css") do |req, res| - res['content-type'] = 'text/css' - add_date res - res.body << RDOC_CSS - end - - @server.mount_proc "/", method(:root) - - @server.mount_proc "/rdoc", method(:rdoc) - - file_handlers = { - '/gems' => '/cache/', - } - - if have_rdoc_4_plus? - @server.mount '/doc_root', RDoc::Servlet, '/doc_root' - else - file_handlers['/doc_root'] = '/doc/' - end - - @gem_dirs.each do |gem_dir| - file_handlers.each do |mount_point, mount_dir| - @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler, - File.join(gem_dir, mount_dir), true) - end - end - - trap("INT") { @server.shutdown; exit! } - trap("TERM") { @server.shutdown; exit! } - - launch if @launch - - @server.start - end - - def specs(req, res) - reset_gems - - add_date res - - specs = Gem::Specification.sort_by(&:sort_obj).map do |spec| - platform = spec.original_platform || Gem::Platform::RUBY - [spec.name, spec.version, platform] - end - - specs = Marshal.dump specs - - if req.path =~ /\.gz$/ - specs = Gem::Util.gzip specs - res['content-type'] = 'application/x-gzip' - else - res['content-type'] = 'application/octet-stream' - end - - if req.request_method == 'HEAD' - res['content-length'] = specs.length - else - res.body << specs - end - end - - def launch - listeners = @server.listeners.map{|l| l.addr[2] } - - # TODO: 0.0.0.0 == any, not localhost. - host = listeners.any?{|l| l == '0.0.0.0' } ? 'localhost' : listeners.first - - say "Launching browser to http://#{host}:#{@port}" - - system("#{@launch} http://#{host}:#{@port}") - end -end diff --git a/lib/rubygems/shellwords.rb b/lib/rubygems/shellwords.rb new file mode 100644 index 0000000000..741dccb363 --- /dev/null +++ b/lib/rubygems/shellwords.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +autoload :Shellwords, "shellwords" diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb index 37e03cdfae..d90e311b65 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require "rubygems/text" +require_relative "text" ## # A Source knows how to list and fetch gems from a RubyGems marshal index. # @@ -12,9 +12,9 @@ class Gem::Source include Gem::Text FILES = { # :nodoc: - :released => 'specs', - :latest => 'latest_specs', - :prerelease => 'prerelease_specs', + released: "specs", + latest: "latest_specs", + prerelease: "prerelease_specs", }.freeze ## @@ -26,15 +26,8 @@ class Gem::Source # Creates a new Source which will use the index located at +uri+. def initialize(uri) - begin - unless uri.kind_of? URI - uri = URI.parse(uri.to_s) - end - rescue URI::InvalidURIError - raise if Gem::Source == self.class - end - - @uri = uri + require_relative "uri" + @uri = Gem::Uri.parse!(uri) @update_cache = nil end @@ -51,25 +44,23 @@ 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 def ==(other) # :nodoc: - self.class === other and @uri == other.uri + self.class === other && @uri == other.uri end alias_method :eql?, :== # :nodoc: @@ -78,15 +69,15 @@ 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 + 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 + index_uri = uri.dup + index_uri.host = "index.rubygems.org" + index_uri + else + uri + end bundler_api_uri = enforce_trailing_slash(fetch_uri) @@ -109,8 +100,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 @@ -144,11 +134,16 @@ 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 - source_uri.path << '.rz' + source_uri.path << ".rz" spec = fetcher.fetch_path source_uri spec = Gem::Util.inflate spec @@ -157,13 +152,14 @@ class Gem::Source require "fileutils" FileUtils.mkdir_p cache_dir - File.open local_spec, 'wb' do |io| + File.open local_spec, "wb" do |io| io.write spec 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 ## @@ -193,8 +189,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 @@ -216,13 +213,13 @@ class Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, '[Remote:', ']' do + q.group 2, "[Remote:", "]" do q.breakable q.text @uri.to_s if api = uri q.breakable - q.text 'API URI: ' + q.text "API URI: " q.text api.to_s end end @@ -236,13 +233,13 @@ class Gem::Source private def enforce_trailing_slash(uri) - uri.merge(uri.path.gsub(/\/+$/, '') + '/') + uri.merge(uri.path.gsub(%r{/+$}, "") + "/") end end -require 'rubygems/source/git' -require 'rubygems/source/installed' -require 'rubygems/source/specific_file' -require 'rubygems/source/local' -require 'rubygems/source/lock' -require 'rubygems/source/vendor' +require_relative "source/git" +require_relative "source/installed" +require_relative "source/specific_file" +require_relative "source/local" +require_relative "source/lock" +require_relative "source/vendor" diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index 9876adc24e..bda63c6844 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -49,16 +49,16 @@ class Gem::Source::Git < Gem::Source # will be checked out when the gem is installed. def initialize(name, repository, reference, submodules = false) - super repository - + require_relative "../uri" + @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' + @git = ENV["git"] || "git" end def <=>(other) @@ -70,16 +70,14 @@ class Gem::Source::Git < Gem::Source -1 when Gem::Source then 1 - else - nil end end def ==(other) # :nodoc: - super and - @name == other.name and - @repository == other.repository and - @reference == other.reference and + super && + @name == other.name && + @repository == other.repository && + @reference == other.reference && @need_submodules == other.need_submodules end @@ -92,17 +90,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, "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, "fetch", "--quiet", "--force", "--tags", install_dir - success = system @git, 'reset', '--quiet', '--hard', rev_parse + success = system @git, "reset", "--quiet", "--hard", rev_parse if @need_submodules - _, status = Open3.capture2e(@git, 'submodule', 'update', '--quiet', '--init', '--recursive') + require "open3" + _, status = Open3.capture2e(@git, "submodule", "update", "--quiet", "--init", "--recursive") success &&= status.success? end @@ -119,11 +118,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', - @repository, 'refs/heads/*:refs/heads/*' + system @git, "fetch", "--quiet", "--force", "--tags", + @repository, "refs/heads/*:refs/heads/*" end else - system @git, 'clone', '--quiet', '--bare', '--no-hardlinks', + system @git, "clone", "--quiet", "--bare", "--no-hardlinks", @repository, repo_cache_dir end end @@ -132,7 +131,7 @@ class Gem::Source::Git < Gem::Source # Directory where git gems get unpacked and so-forth. def base_dir # :nodoc: - File.join @root_dir, 'bundler' + File.join @root_dir, "bundler" end ## @@ -154,11 +153,11 @@ class Gem::Source::Git < Gem::Source def install_dir # :nodoc: return unless File.exist? repo_cache_dir - File.join base_dir, 'gems', "#{@name}-#{dir_shortref}" + File.join base_dir, "gems", "#{@name}-#{dir_shortref}" end def pretty_print(q) # :nodoc: - q.group 2, '[Git: ', ']' do + q.group 2, "[Git: ", "]" do q.breakable q.text @repository @@ -171,7 +170,7 @@ class Gem::Source::Git < Gem::Source # The directory where the git gem's repository will be cached. def repo_cache_dir # :nodoc: - File.join @root_dir, 'cache', 'bundler', 'git', "#{@name}-#{uri_hash}" + File.join @root_dir, "cache", "bundler", "git", "#{@name}-#{uri_hash}" end ## @@ -181,7 +180,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, "rev-parse", @reference).strip end raise Gem::Exception, @@ -200,7 +199,7 @@ class Gem::Source::Git < Gem::Source return [] unless install_dir Dir.chdir install_dir do - Dir['{,*,*/*}.gemspec'].map do |spec_file| + Dir["{,*,*/*}.gemspec"].map do |spec_file| directory = File.dirname spec_file file = File.basename spec_file @@ -210,7 +209,7 @@ class Gem::Source::Git < Gem::Source spec.base_dir = base_dir spec.extension_dir = - File.join base_dir, 'extensions', Gem::Platform.local.to_s, + File.join base_dir, "extensions", Gem::Platform.local.to_s, Gem.extension_api_version, "#{name}-#{dir_shortref}" spec.full_gem_path = File.dirname spec.loaded_from if spec @@ -222,19 +221,19 @@ class Gem::Source::Git < Gem::Source 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 'digest' # required here to avoid deadlocking in Gem.activate_bin_path (because digest is a gem on 2.5+) + 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 end - Digest::SHA1.hexdigest normalized + OpenSSL::Digest::SHA1.hexdigest normalized end end diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb index 7e1dd7af5a..cbe12a0516 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,6 @@ class Gem::Source::Installed < Gem::Source end def pretty_print(q) # :nodoc: - q.text '[Installed]' + q.text "[Installed]" end end diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index 078b06203f..d81d8343a8 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] + keys = @specs ? @specs.keys.sort : "NOT LOADED" + 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,25 @@ 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: 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.max_by(&:version) end def fetch_spec(name) # :nodoc: @@ -113,7 +109,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,7 +117,7 @@ class Gem::Source::Local < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, '[Local gems:', ']' do + q.group 2, "[Local gems:", "]" do q.breakable q.seplist @specs.keys do |v| q.text v.full_name 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 24db1440dd..e9b2753646 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,7 +42,7 @@ class Gem::Source::SpecificFile < Gem::Source end def pretty_print(q) # :nodoc: - q.group 2, '[SpecificFile:', ']' do + q.group 2, "[SpecificFile:", "]" do q.breakable q.text @path 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 13b25b63dc..33db64fbc1 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,20 +44,16 @@ 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) - require "uri" - src = case obj - when URI - Gem::Source.new(obj) when Gem::Source obj else - Gem::Source.new(URI.parse(obj)) - end + Gem::Source.new(obj) + end @sources << src unless @sources.include?(src) src @@ -130,7 +126,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 } @@ -141,7 +137,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 b2bcadc49c..610edf25c9 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -1,9 +1,10 @@ # frozen_string_literal: true -require 'rubygems/remote_fetcher' -require 'rubygems/user_interaction' -require 'rubygems/errors' -require 'rubygems/text' -require 'rubygems/name_tuple' + +require_relative "remote_fetcher" +require_relative "user_interaction" +require_relative "errors" +require_relative "text" +require_relative "name_tuple" ## # SpecFetcher handles metadata updates from remote gem repositories. @@ -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 @@ -91,14 +92,14 @@ 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| if dependency.match?(tup) - if matching_platform and !Gem::Platform.match_gem?(tup.platform, tup.name) + if matching_platform && !Gem::Platform.match_gem?(tup.platform, tup.name) pm = ( rejected_specs[dependency] ||= \ Gem::PlatformMismatch.new(tup.name, tup.version)) @@ -121,9 +122,9 @@ class Gem::SpecFetcher end end - tuples = tuples.sort_by {|x| x[0] } + tuples = tuples.sort_by {|x| x[0].version } - return [tuples, errors] + [tuples, errors] end ## @@ -154,16 +155,14 @@ class Gem::SpecFetcher 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,19 +170,19 @@ class Gem::SpecFetcher # alternative gem names. def suggest_gems_from_name(gem_name, type = :latest, num_results = 5) - gem_name = gem_name.downcase.tr('_-', '') + gem_name = gem_name.downcase.tr("_-", "") max = gem_name.size / 2 names = available_specs(type).first.values.flatten(1) matches = names.map do |n| next unless n.match_platform? - [n.name, 0] if n.name.downcase.tr('_-', '').include?(gem_name) + [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('_-', '') + distance = levenshtein_distance gem_name, n.name.downcase.tr("_-", "") next if distance >= max return [n.name] if distance == 0 [n.name, distance] @@ -191,12 +190,12 @@ class Gem::SpecFetcher end matches = if matches.empty? && type != :prerelease - suggest_gems_from_name gem_name, :prerelease - else - matches.uniq.sort_by {|name, dist| dist } - end + suggest_gems_from_name gem_name, :prerelease + else + 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 +213,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] @@ -253,7 +250,7 @@ class Gem::SpecFetcher 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 7206c3eaf0..29139cf725 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -1,16 +1,19 @@ # frozen_string_literal: true -# -*- coding: utf-8 -*- + +# #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/deprecate' -require 'rubygems/basic_specification' -require 'rubygems/stub_specification' -require 'rubygems/specification_policy' -require 'rubygems/util/list' +require_relative "deprecate" +require_relative "basic_specification" +require_relative "stub_specification" +require_relative "platform" +require_relative "util/list" + +require "rbconfig" ## # The Specification class contains the information for a gem. Typically @@ -74,42 +77,38 @@ class Gem::Specification < Gem::BasicSpecification # key should be equal to the CURRENT_SPECIFICATION_VERSION. SPECIFICATION_VERSION_HISTORY = { # :nodoc: - -1 => ['(RubyGems versions up to and including 0.7 did not have versioned specifications)'], - 1 => [ + -1 => ["(RubyGems versions up to and including 0.7 did not have versioned specifications)"], + 1 => [ 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"', '"test_file=x" is a shortcut for "test_files=[x]"', ], 2 => [ 'Added "required_rubygems_version"', - 'Now forward-compatible with future versions', + "Now forward-compatible with future versions", ], 3 => [ - 'Added Fixnum validation to the specification_version', + "Added Fixnum validation to the specification_version", ], 4 => [ - 'Added sandboxed freeform metadata to the specification version.', + "Added sandboxed freeform metadata to the specification version.", ], }.freeze MARSHAL_FIELDS = { # :nodoc: -1 => 16, - 1 => 16, - 2 => 16, - 3 => 17, - 4 => 18, + 1 => 16, + 2 => 16, + 3 => 17, + 4 => 18, }.freeze today = Time.now.utc TODAY = Time.utc(today.year, today.month, today.day) # :nodoc: - # rubocop:disable Style/MutableConstant - LOAD_CACHE = {} # :nodoc: - # rubocop:enable Style/MutableConstant - LOAD_CACHE_MUTEX = Mutex.new + @load_cache = {} # :nodoc: + @load_cache_mutex = Thread::Mutex.new - private_constant :LOAD_CACHE if defined? private_constant - - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: # :startdoc: @@ -128,39 +127,39 @@ 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 - INITIALIZE_CODE_FOR_DEFAULTS = { } # :nodoc: + INITIALIZE_CODE_FOR_DEFAULTS = {} # :nodoc: # rubocop:enable Style/MutableConstant @@default_value.each do |k,v| @@ -169,26 +168,30 @@ class Gem::Specification < Gem::BasicSpecification v.inspect when String v.dump - when Numeric - "default_value(:#{k})" else "default_value(:#{k}).dup" - end + 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.reject {|_k,v| v != [] }.keys @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k| @@default_value[k].nil? end - @@stubs = nil - @@stubs_by_name = {} + 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: - @@spec_with_requirable_file = {} - @@active_stub_with_requirable_file = {} # Tracking removed method calls to warn users during build time. REMOVED_METHODS = [:rubyforge_project=].freeze # :nodoc: @@ -222,7 +225,7 @@ class Gem::Specification < Gem::BasicSpecification attr_reader :version ## - # A short summary of this gem's description. Displayed in `gem list -d`. + # A short summary of this gem's description. Displayed in <tt>gem list -d</tt>. # # The #description should be more detailed than the summary. # @@ -260,15 +263,14 @@ class Gem::Specification < Gem::BasicSpecification @test_files, add_bindir(@executables), @extra_rdoc_files, - @extensions, - ].flatten.compact.uniq.sort + @extensions].flatten.compact.uniq.sort end ## # A list of authors for this gem. # # Alternatively, a single author can be specified by assigning a string to - # `spec.author` + # +spec.author+ # # Usage: # @@ -282,6 +284,15 @@ class Gem::Specification < Gem::BasicSpecification # :section: Recommended gemspec attributes ## + # The version of Ruby required by this gem + # + # Usage: + # + # spec.required_ruby_version = '>= 2.7.0' + + attr_reader :required_ruby_version + + ## # A long description of this gem # # The description should be more detailed than the summary but not @@ -290,7 +301,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 @@ -324,17 +335,21 @@ class Gem::Specification < Gem::BasicSpecification # This should just be the name of your license. The full text of the license # should be inside of the gem (at the top level) when you build it. # - # The simplest way, is to specify the standard SPDX ID + # 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. + # Ideally, you should pick one that is OSI (Open Source Initiative) + # 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 https://choosealicense.com/. # - # The most commonly used OSI approved licenses are MIT and Apache-2.0. - # GitHub also provides a license picker at http://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 + # the license text. # # You should specify a license for your gem so that people know how they are - # permitted to use it, and any restrictions you're placing on it. Not - # specifying a license means all rights are reserved; others have no rights + # permitted to use it and any restrictions you're placing on it. Not + # specifying a license means all rights are reserved; others have no right # to use the code for any purpose. # # You can set multiple licenses with #licenses= @@ -411,11 +426,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 @@ -458,7 +473,7 @@ class Gem::Specification < Gem::BasicSpecification # spec.platform = Gem::Platform.local def platform=(platform) - if @original_platform.nil? or + if @original_platform.nil? || @original_platform == Gem::Platform::RUBY @original_platform = platform end @@ -474,12 +489,12 @@ class Gem::Specification < Gem::BasicSpecification # legacy constants when nil, Gem::Platform::RUBY then @new_platform = Gem::Platform::RUBY - when 'mswin32' then # was Gem::Platform::WIN32 - @new_platform = Gem::Platform.new 'x86-mswin32' - when 'i586-linux' then # was Gem::Platform::LINUX_586 - @new_platform = Gem::Platform.new 'x86-linux' - when 'powerpc-darwin' then # was Gem::Platform::DARWIN - @new_platform = Gem::Platform.new 'ppc-darwin' + when "mswin32" then # was Gem::Platform::WIN32 + @new_platform = Gem::Platform.new "x86-mswin32" + when "i586-linux" then # was Gem::Platform::LINUX_586 + @new_platform = Gem::Platform.new "x86-linux" + when "powerpc-darwin" then # was Gem::Platform::DARWIN + @new_platform = Gem::Platform.new "ppc-darwin" else @new_platform = Gem::Platform.new platform end @@ -487,8 +502,6 @@ class Gem::Specification < Gem::BasicSpecification @platform = @new_platform.to_s invalidate_memoized_attributes - - @new_platform end ## @@ -513,23 +526,11 @@ class Gem::Specification < Gem::BasicSpecification end ## - # The version of Ruby required by this gem - - attr_reader :required_ruby_version - - ## # The RubyGems version required by this gem 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 @@ -567,7 +568,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. @@ -588,7 +589,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: # @@ -646,6 +647,8 @@ class Gem::Specification < Gem::BasicSpecification @rdoc_options ||= [] end + LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1") + ## # The version of Ruby required by this gem. The ruby version can be # specified to the patch-level: @@ -672,6 +675,14 @@ class Gem::Specification < Gem::BasicSpecification def required_ruby_version=(req) @required_ruby_version = Gem::Requirement.create req + + @required_ruby_version.requirements.map! do |op, v| + if v >= LATEST_RUBY_WITHOUT_PATCH_VERSIONS && v.release.segments.size == 4 + [op == "~>" ? "=" : op, Gem::Version.new(v.segments.tap {|s| s.delete_at(3) }.join("."))] + else + [op, v] + end + end end ## @@ -707,6 +718,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 ## @@ -714,7 +740,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,28 +770,20 @@ class Gem::Specification < Gem::BasicSpecification attr_accessor :specification_version def self._all # :nodoc: - unless defined?(@@all) && @@all - @@all = stubs.map(&:to_spec) - - # After a reset, make sure already loaded specs - # are still marked as activated. - specs = {} - Gem.loaded_specs.each_value{|s| specs[s] = true } - @@all.each{|s| s.activated = true if specs[s] } - end - @@all + @@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec) end - def self._clear_load_cache # :nodoc: - LOAD_CACHE_MUTEX.synchronize do - LOAD_CACHE.clear + def self.clear_load_cache # :nodoc: + @load_cache_mutex.synchronize do + @load_cache.clear end end + private_class_method :clear_load_cache 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 @@ -844,7 +862,7 @@ class Gem::Specification < Gem::BasicSpecification 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 } + stubs = stubs.uniq(&:full_name) _resort!(stubs) stubs end @@ -855,7 +873,7 @@ class Gem::Specification < Gem::BasicSpecification next names if names.nonzero? versions = b.version <=> a.version next versions if versions.nonzero? - b.platform == Gem::Platform::RUBY ? -1 : 1 + Gem::Platform.sort_priority(b.platform) end end @@ -871,6 +889,30 @@ class Gem::Specification < Gem::BasicSpecification end ## + # Adds +spec+ to the known specifications, keeping the collection + # 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) + 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 + end + + ## # Returns all specifications. This method is discouraged from use. # You probably want to use one of the Enumerable methods instead. @@ -901,7 +943,7 @@ class Gem::Specification < Gem::BasicSpecification # Return full names of all specs in sorted order. def self.all_names - self._all.map(&:full_name) + _all.map(&:full_name) end ## @@ -927,7 +969,7 @@ class Gem::Specification < Gem::BasicSpecification def self.dirs @@dirs ||= Gem.path.collect do |dir| - File.join dir.dup.tap(&Gem::UNTAINT), "specifications" + File.join dir, "specifications" end end @@ -936,7 +978,7 @@ 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" } end @@ -950,7 +992,7 @@ class Gem::Specification < Gem::BasicSpecification def self.each return enum_for(:each) unless block_given? - self._all.each do |x| + _all.each do |x| yield x end end @@ -961,8 +1003,6 @@ class Gem::Specification < Gem::BasicSpecification 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 end @@ -980,20 +1020,24 @@ 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| - next unless Gem::BundlerVersionFinder.compatible?(s) + spec = @@spec_with_requirable_file[path] ||= stubs.find do |s| s.contains_requirable_file? path - end || NOT_FOUND) + end || NOT_FOUND + spec.to_spec end @@ -1004,16 +1048,16 @@ class Gem::Specification < Gem::BasicSpecification def self.find_inactive_by_path(path) stub = stubs.find do |s| next if s.activated? - next unless Gem::BundlerVersionFinder.compatible?(s) s.contains_requirable_file? path end - stub && stub.to_spec + stub&.to_spec end def self.find_active_stub_by_path(path) - stub = @@active_stub_with_requirable_file[path] ||= (stubs.find do |s| - s.activated? and s.contains_requirable_file? path - end || NOT_FOUND) + stub = @@active_stub_with_requirable_file[path] ||= stubs.find do |s| + s.activated? && s.contains_requirable_file?(path) + end || NOT_FOUND + stub.this end @@ -1030,7 +1074,7 @@ 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| + spec.traverse do |_from_spec, _dep, to_spec, trail| if to_spec.has_conflicts? || to_spec.conficts_when_loaded_with?(trail) :next else @@ -1043,7 +1087,7 @@ class Gem::Specification < Gem::BasicSpecification end def self.unresolved_specs - unresolved_deps.values.map {|dep| dep.to_specs }.flatten + unresolved_deps.values.map(&:to_specs).flatten end private_class_method :unresolved_specs @@ -1071,6 +1115,7 @@ class Gem::Specification < Gem::BasicSpecification spec.specification_version ||= NONEXISTENT_SPECIFICATION_VERSION spec.reset_nil_attributes_to_default + spec.flatten_require_paths spec end @@ -1080,7 +1125,7 @@ class Gem::Specification < Gem::BasicSpecification # +prerelease+ is true. def self.latest_specs(prerelease = false) - _latest_specs Gem::Specification._all, prerelease + _latest_specs Gem::Specification.stubs, prerelease end ## @@ -1094,12 +1139,14 @@ class Gem::Specification < Gem::BasicSpecification 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.map(&:last).flatten.sort_by(&:name) end ## @@ -1108,36 +1155,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 = File.read file, :mode => 'r:UTF-8:-' - - code.tap(&Gem::UNTAINT) + code = Gem.open_file(file, "r:UTF-8:-", &:read) begin - _spec = eval code, binding, file + spec = eval code, binding, file - if Gem::Specification === _spec - _spec.loaded_from = File.expand_path file.to_s - LOAD_CACHE_MUTEX.synchronize do - prev = LOAD_CACHE[file] + 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 @@ -1199,7 +1243,7 @@ class Gem::Specification < Gem::BasicSpecification latest_remote = remotes.sort.last yield [local_spec, latest_remote] if - latest_remote and local_spec.version < latest_remote + latest_remote && local_spec.version < latest_remote end nil @@ -1225,17 +1269,12 @@ class Gem::Specification < Gem::BasicSpecification def self.reset @@dirs = nil - Gem.pre_reset_hooks.each {|hook| hook.call } - @@all = nil - @@stubs = nil - @@stubs_by_name = {} - @@spec_with_requirable_file = {} - @@active_stub_with_requirable_file = {} - _clear_load_cache + Gem.pre_reset_hooks.each(&:call) + clear_specs + clear_load_cache unresolved = unresolved_deps unless unresolved.empty? - w = "W" + "ARN" - warn "#{w}: Unresolved or ambiguous specs during Gem::Specification.reset:" + warn "WARN: Unresolved or ambiguous specs during Gem::Specification.reset:" unresolved.values.each do |dep| warn " #{dep}" @@ -1245,11 +1284,11 @@ class Gem::Specification < Gem::BasicSpecification versions.each {|s| warn " - #{s.version}" } end end - warn "#{w}: Clearing out unresolved specs. Try 'gem cleanup <gem>'" + warn "WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'" warn "Please report a bug if this causes problems." unresolved.clear end - Gem.post_reset_hooks.each {|hook| hook.call } + Gem.post_reset_hooks.each(&:call) end # DOC: This method needs documented or nodoc'd @@ -1262,8 +1301,50 @@ class Gem::Specification < Gem::BasicSpecification def self._load(str) Gem.load_yaml + Gem.load_safe_marshal + + yaml_set = false + retry_count = 0 + + array = begin + 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 + # that generated them. Workaround the issue by defining the necessary + # constants and retrying. + # + message = e.message + raise unless message.include?("YAML::") + + unless Object.const_defined?(:YAML) + Object.const_set "YAML", Psych + yaml_set = true + end + + if message.include?("YAML::Syck::") + YAML.const_set "Syck", YAML unless YAML.const_defined?(:Syck) - array = Marshal.load str + 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 + end + + retry_count += 1 + retry + ensure + Object.__send__(:remove_const, "YAML") if yaml_set + end spec = Gem::Specification.new spec.instance_variable_set :@specification_version, array[1] @@ -1271,22 +1352,17 @@ class Gem::Specification < Gem::BasicSpecification current_version = CURRENT_SPECIFICATION_VERSION field_count = if spec.specification_version > current_version - spec.instance_variable_set :@specification_version, - current_version - MARSHAL_FIELDS[current_version] - else - MARSHAL_FIELDS[spec.specification_version] - end + spec.instance_variable_set :@specification_version, + current_version + MARSHAL_FIELDS[current_version] + else + MARSHAL_FIELDS[spec.specification_version] + end if array.size < field_count raise TypeError, "invalid Gem::Specification format #{array.inspect}" end - # Cleanup any YAML::PrivateType. They only show up for an old bug - # where nil => null, so just convert them to nil based on the type. - - array.map! {|e| e.kind_of?(YAML::PrivateType) ? nil : e } - spec.instance_variable_set :@rubygems_version, array[0] # spec version spec.instance_variable_set :@name, array[2] @@ -1342,7 +1418,7 @@ class Gem::Specification < Gem::BasicSpecification @required_rubygems_version, @original_platform, @dependencies, - '', # rubyforge_project + "", # rubyforge_project @email, @authors, @description, @@ -1361,7 +1437,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 @@ -1372,11 +1448,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 ## @@ -1387,7 +1463,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 @@ -1401,7 +1477,7 @@ class Gem::Specification < Gem::BasicSpecification begin specs = spec_dep.to_specs rescue Gem::MissingSpecError => e - raise Gem::MissingSpecError.new(e.name, e.requirement, "at: #{self.spec_file}") + raise Gem::MissingSpecError.new(e.name, e.requirement, "at: #{spec_file}") end if specs.size == 1 @@ -1447,7 +1523,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 @@ -1465,8 +1541,8 @@ class Gem::Specification < Gem::BasicSpecification else executables end - rescue - return nil + rescue StandardError + nil end ## @@ -1476,10 +1552,10 @@ class Gem::Specification < Gem::BasicSpecification def add_dependency_with_type(dependency, type, requirements) requirements = if requirements.empty? - Gem::Requirement.default - else - requirements.flatten - end + Gem::Requirement.default + else + requirements.flatten + end unless dependency.respond_to?(:name) && dependency.respond_to?(:requirement) @@ -1491,7 +1567,7 @@ class Gem::Specification < Gem::BasicSpecification private :add_dependency_with_type - alias add_dependency add_runtime_dependency + alias_method :add_dependency, :add_runtime_dependency ## # Adds this spec's require paths to LOAD_PATH, in the proper location. @@ -1508,7 +1584,7 @@ class Gem::Specification < Gem::BasicSpecification # Singular reader for #authors. Returns the first author in the list def author - val = authors and val.first + (val = authors) && val.first end ## @@ -1543,7 +1619,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 @@ -1556,11 +1632,13 @@ class Gem::Specification < Gem::BasicSpecification # the gem.build_complete file is missing. def build_extensions # :nodoc: - return if default_gem? 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 @@ -1568,9 +1646,9 @@ class Gem::Specification < Gem::BasicSpecification unresolved_deps = Gem::Specification.unresolved_deps.dup Gem::Specification.unresolved_deps.clear - require 'rubygems/config_file' - require 'rubygems/ext' - require 'rubygems/user_interaction' + require_relative "config_file" + require_relative "ext" + require_relative "user_interaction" ui = Gem::SilentUI.new Gem::DefaultUserInteraction.use_ui ui do @@ -1578,7 +1656,7 @@ class Gem::Specification < Gem::BasicSpecification builder.build_extensions end ensure - ui.close if ui + ui&.close Gem::Specification.unresolved_deps.replace unresolved_deps end end @@ -1618,9 +1696,9 @@ 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 and not spec.satisfies_requirement? dep + if spec && !spec.satisfies_requirement?(dep) (conflicts[spec] ||= []) << dep end end @@ -1634,7 +1712,7 @@ class Gem::Specification < Gem::BasicSpecification def conficts_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 @@ -1644,14 +1722,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 and not 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. @@ -1668,14 +1744,14 @@ class Gem::Specification < Gem::BasicSpecification DateLike = Object.new # :nodoc: def DateLike.===(obj) # :nodoc: - defined?(::Date) and Date === obj + defined?(::Date) && Date === obj end DateTimeFormat = # :nodoc: /\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 @@ -1690,12 +1766,6 @@ class Gem::Specification < Gem::BasicSpecification when String then if DateTimeFormat =~ date Time.utc($1.to_i, $2.to_i, $3.to_i) - - # Workaround for where the date format output from psych isn't - # parsed as a Time object by syck and thus comes through as a - # string. - elsif /\A(\d{4})-(\d{2})-(\d{2}) \d{2}:\d{2}:\d{2}\.\d+?Z\z/ =~ date - Time.utc($1.to_i, $2.to_i, $3.to_i) else raise(Gem::InvalidSpecificationException, "invalid date format in specification: #{date.inspect}") @@ -1704,7 +1774,7 @@ class Gem::Specification < Gem::BasicSpecification Time.utc(date.year, date.month, date.day) else TODAY - end + end end ## @@ -1714,9 +1784,9 @@ class Gem::Specification < Gem::BasicSpecification # executable now. See Gem.bin_path. def default_executable # :nodoc: - if defined?(@default_executable) and @default_executable + if defined?(@default_executable) && @default_executable result = @default_executable - elsif @executables and @executables.size == 1 + elsif @executables && @executables.size == 1 result = Array(@executables).first else result = nil @@ -1753,13 +1823,12 @@ class Gem::Specification < Gem::BasicSpecification 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 @@ -1769,7 +1838,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.map(&:to_specs).flatten end ## @@ -1795,7 +1864,7 @@ class Gem::Specification < Gem::BasicSpecification # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri" def doc_dir(type = nil) - @doc_dir ||= File.join base_dir, 'doc', full_name + @doc_dir ||= File.join base_dir, "doc", full_name if type File.join @doc_dir, type @@ -1807,21 +1876,22 @@ class Gem::Specification < Gem::BasicSpecification def encode_with(coder) # :nodoc: mark_version - coder.add 'name', @name - coder.add 'version', @version + coder.add "name", @name + coder.add "version", @version platform = case @original_platform - when nil, '' then - 'ruby' + when nil, "" then + "ruby" when String then @original_platform else @original_platform.to_s - end - coder.add 'platform', platform + end + coder.add "platform", platform 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 @@ -1833,7 +1903,7 @@ class Gem::Specification < Gem::BasicSpecification # Singular accessor for #executables def executable - val = executables and val.first + (val = executables) && val.first end ## @@ -1938,18 +2008,18 @@ class Gem::Specification < Gem::BasicSpecification end rubygems_deprecate :has_rdoc= - alias :has_rdoc? :has_rdoc # :nodoc: + alias_method :has_rdoc?, :has_rdoc # :nodoc: rubygems_deprecate :has_rdoc? ## # True if this gem has files in test_files def has_unit_tests? # :nodoc: - not test_files.empty? + !test_files.empty? end # :stopdoc: - alias has_test_suite? has_unit_tests? + alias_method :has_test_suite?, :has_unit_tests? # :startdoc: def hash # :nodoc: @@ -1998,7 +2068,7 @@ class Gem::Specification < Gem::BasicSpecification self.name = name if name self.version = version if version - if platform = Gem.platforms.last and platform != Gem::Platform::RUBY and platform != Gem::Platform.local + if (platform = Gem.platforms.last) && platform != Gem::Platform::RUBY && platform != Gem::Platform.local self.platform = platform end @@ -2006,7 +2076,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| @@ -2028,15 +2099,18 @@ 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 return Gem.dir unless loaded_from @base_dir ||= if default_gem? - File.dirname File.dirname File.dirname loaded_from - else - File.dirname File.dirname loaded_from - end + File.dirname File.dirname File.dirname loaded_from + else + File.dirname File.dirname loaded_from + end end ## @@ -2113,8 +2187,8 @@ class Gem::Specification < Gem::BasicSpecification return end - if @specification_version > CURRENT_SPECIFICATION_VERSION and - sym.to_s.end_with?("=") + if @specification_version > CURRENT_SPECIFICATION_VERSION && + sym.to_s.end_with?("=") warn "ignoring #{sym} loading #{full_name}" if $DEBUG else super @@ -2126,8 +2200,8 @@ class Gem::Specification < Gem::BasicSpecification # probably want to build_extensions def missing_extensions? - return false if default_gem? return false if extensions.empty? + return false if default_gem? return false if File.exist? gem_build_complete_path true @@ -2140,7 +2214,7 @@ class Gem::Specification < Gem::BasicSpecification # file list. def normalize - if defined?(@extra_rdoc_files) and @extra_rdoc_files + if defined?(@extra_rdoc_files) && @extra_rdoc_files @extra_rdoc_files.uniq! @files ||= [] @files.concat(@extra_rdoc_files) @@ -2165,7 +2239,7 @@ class Gem::Specification < Gem::BasicSpecification # platform. For use with legacy gems. def original_name # :nodoc: - if platform == Gem::Platform::RUBY or platform.nil? + if platform == Gem::Platform::RUBY || platform.nil? "#{@name}-#{@version}" else "#{@name}-#{@version}-#{@original_platform}" @@ -2183,11 +2257,11 @@ 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: - q.group 2, 'Gem::Specification.new do |s|', 'end' do + q.group 2, "Gem::Specification.new do |s|", "end" do q.breakable attributes = @@attributes - [:name, :version] @@ -2196,23 +2270,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) or - 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 @@ -2222,7 +2295,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. @@ -2230,7 +2303,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 @@ -2257,7 +2330,7 @@ class Gem::Specification < Gem::BasicSpecification # Singular accessor for #require_paths def require_path - val = require_paths and val.first + (val = require_paths) && val.first end ## @@ -2282,7 +2355,7 @@ class Gem::Specification < Gem::BasicSpecification # Returns the full path to this spec's ri directory. def ri_dir - @ri_dir ||= File.join base_dir, 'ri', full_name + @ri_dir ||= File.join base_dir, "ri", full_name end ## @@ -2292,16 +2365,16 @@ class Gem::Specification < Gem::BasicSpecification def ruby_code(obj) case obj when String then obj.dump + ".freeze" - when Array then '[' + obj.map {|x| ruby_code x }.join(", ") + ']' + 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 - when DateLike then obj.strftime('%Y-%m-%d').dump - when Time then obj.strftime('%Y-%m-%d').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)})" @@ -2322,7 +2395,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? @@ -2331,7 +2404,7 @@ class Gem::Specification < Gem::BasicSpecification # Checks if this specification meets the requirement of +dependency+. def satisfies_requirement?(dependency) - return @name == dependency.name && + @name == dependency.name && dependency.requirement.satisfied_by?(@version) end @@ -2339,7 +2412,7 @@ class Gem::Specification < Gem::BasicSpecification # Returns an object you can use to sort specifications in #sort_by. def sort_obj - [@name, @version, @new_platform == Gem::Platform::RUBY ? -1 : 1] + [@name, @version, Gem::Platform.sort_priority(@new_platform)] end ## @@ -2386,7 +2459,7 @@ class Gem::Specification < Gem::BasicSpecification # Singular accessor for #test_files def test_file # :nodoc: - val = test_files and val.first + (val = test_files) && val.first end ## @@ -2408,7 +2481,7 @@ class Gem::Specification < Gem::BasicSpecification @test_files = [@test_suite_file].flatten @test_suite_file = nil end - if defined?(@test_files) and @test_files + if defined?(@test_files) && @test_files @test_files else @test_files = [] @@ -2421,7 +2494,6 @@ class Gem::Specification < Gem::BasicSpecification # still have their default values are omitted. def to_ruby - require_relative 'openssl' mark_version result = [] result << "# -*- encoding: utf-8 -*-" @@ -2433,13 +2505,13 @@ class Gem::Specification < Gem::BasicSpecification result << " s.name = #{ruby_code name}" result << " s.version = #{ruby_code version}" - unless platform.nil? or platform == Gem::Platform::RUBY + unless platform.nil? || platform == Gem::Platform::RUBY result << " s.platform = #{ruby_code original_platform}" end result << "" result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version=" - if metadata and !metadata.empty? + if metadata && !metadata.empty? result << " s.metadata = #{ruby_code metadata} if s.respond_to? :metadata=" end result << " s.require_paths = #{ruby_code raw_require_paths}" @@ -2455,42 +2527,35 @@ class Gem::Specification < Gem::BasicSpecification :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}" unless defined?(OpenSSL::PKey::RSA) && current_value.is_a?(OpenSSL::PKey::RSA) + result << " s.#{attr_name} = #{ruby_code current_value}" end end + if String === signing_key + 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} if s.respond_to? :installed_by_version" end unless dependencies.empty? result << nil - result << " if s.respond_to? :specification_version then" - result << " s.specification_version = #{specification_version}" - result << " end" + result << " s.specification_version = #{specification_version}" result << nil - result << " if s.respond_to? :add_runtime_dependency then" - 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})" - end - - result << " else" - dependencies.each do |dep| - version_reqs_param = dep.requirements_list.inspect - result << " s.add_dependency(%q<#{dep.name}>.freeze, #{version_reqs_param})" + result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})" end - result << " end" end result << "end" @@ -2527,14 +2592,14 @@ class Gem::Specification < Gem::BasicSpecification # 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 'rubygems/psych_tree' + require_relative "psych_tree" end builder = Gem::NoAliasYAMLTree.create builder << self ast = builder.tree - require 'stringio' + require "stringio" io = StringIO.new io.set_encoding Encoding::UTF_8 @@ -2550,10 +2615,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 @@ -2561,11 +2625,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 @@ -2612,20 +2675,13 @@ class Gem::Specification < Gem::BasicSpecification 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) - # 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 if @version.nil? - return @version + invalidate_memoized_attributes end def stubbed? @@ -2637,9 +2693,9 @@ 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 else - instance_variable_set "@#{ivar}", val.tap(&Gem::UNTAINT) + instance_variable_set "@#{ivar}", val end end @@ -2656,17 +2712,26 @@ 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 - end + end instance_variable_set "@#{attribute}", value end @installed_by_version ||= nil + + nil + end + + def flatten_require_paths # :nodoc: + return unless raw_require_paths.first.is_a?(Array) + + warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this" + raw_require_paths.flatten! end def raw_require_paths # :nodoc: diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index 2b8b05635e..516c26f53c 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -1,22 +1,25 @@ -require 'rubygems/user_interaction' +# 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: @@ -100,10 +103,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" @@ -120,30 +127,30 @@ class Gem::SpecificationPolicy metadata = @specification.metadata unless Hash === metadata - error 'metadata must be a hash' + error "metadata must be a hash" end metadata.each do |key, value| - if !key.kind_of?(String) + entry = "metadata['#{key}']" + unless key.is_a?(String) error "metadata keys must be a String" end if key.size > 128 - error "metadata key too large (#{key.size} > 128)" + error "metadata key is too large (#{key.size} > 128)" end - if !value.kind_of?(String) - error "metadata values must be a String" + unless value.is_a?(String) + error "#{entry} value must be a String" end if value.size > 1024 - error "metadata value too large (#{value.size} > 1024)" + error "#{entry} value is too large (#{value.size} > 1024)" end - if METADATA_LINK_KEYS.include? key - if value !~ VALID_URI_PATTERN - error "metadata['#{key}'] 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 @@ -153,14 +160,14 @@ class Gem::SpecificationPolicy def validate_duplicate_dependencies # :nodoc: # NOTE: see REFACTOR note in Gem::Dependency about types - this might be brittle - seen = Gem::Dependency::TYPES.inject({}) {|types, type| types.merge({ type => {}}) } + seen = Gem::Dependency::TYPES.inject({}) {|types, type| types.merge({ type => {} }) } error_messages = [] @specification.dependencies.each do |dep| 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 @@ -172,6 +179,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: end ## + # Checks that the gem does not depend on itself. # Checks that dependencies use requirements as we recommend. Warnings are # issued when dependencies are open-ended or overly strict for semantic # versioning. @@ -179,6 +187,10 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_dependencies # :nodoc: warning_messages = [] @specification.dependencies.each do |dep| + if dep.name == @specification.name # warn on self reference + warning_messages << "Self referencing dependency is unnecessary and strongly discouraged." + end + prerelease_dep = dep.requirements_list.any? do |req| Gem::Requirement.new(req).prerelease? end @@ -187,37 +199,42 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: prerelease_dep && !@specification.version.prerelease? open_ended = dep.requirement.requirements.all? do |op, version| - not version.prerelease? and (op == '>' or op == '>=') + !version.prerelease? && [">", ">="].include?(op) end - if open_ended - op, dep_version = dep.requirement.requirements.first - - segments = dep_version.segments + next unless open_ended + op, dep_version = dep.requirement.requirements.first - base = segments.first 2 + segments = dep_version.segments - recommendation = if (op == '>' || op == '>=') && segments == [0] - " use a bounded requirement, such as '~> x.y'" - else - bugfix = if op == '>' - ", '> #{dep_version}'" - elsif op == '>=' and base != segments - ", '>= #{dep_version}'" - end + base = segments.first 2 - " if #{dep.name} is semantically versioned, use:\n" \ - " add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}" - end + recommendation = if [">", ">="].include?(op) && segments == [0] + " use a bounded requirement, such as \"~> x.y\"" + else + bugfix = if op == ">" + ", \"> #{dep_version}\"" + elsif op == ">=" && base != segments + ", \">= #{dep_version}\"" + end - warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" + " if #{dep.name} is semantically versioned, use:\n" \ + " add_#{dep.type}_dependency \"#{dep.name}\", \"~> #{base.join "."}\"#{bugfix}" end + + warning_messages << ["open-ended dependency on #{dep} is not recommended", recommendation].join("\n") + "\n" end if warning_messages.any? warning_messages.each {|warning_message| warning warning_message } end end + 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 + ## # Issues a warning for each file to be packaged which is world-readable. # @@ -228,7 +245,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 @@ -247,7 +264,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 @@ -273,11 +290,11 @@ 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 + elsif SPECIAL_CHARACTERS.match?(name) error "invalid value for attribute name: #{name.dump} can not begin with a period, dash, or underscore" end end @@ -285,7 +302,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_require_paths return unless @specification.raw_require_paths.empty? - error 'specification must have at least one require_path' + error "specification must have at least one require_path" end def validate_non_files @@ -309,7 +326,7 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_specification_version return if @specification.specification_version.is_a?(Integer) - error 'specification_version must be an Integer (did you mean version?)' + error "specification_version must be an Integer (did you mean version?)" end def validate_platform @@ -335,9 +352,9 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: Gem::Dependency else String - end + end - unless Array === val and 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 @@ -352,6 +369,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 @@ -362,26 +381,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 = /FI XME|TO DO/x.freeze - HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze + LAZY = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, "") + 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? @@ -392,25 +423,25 @@ 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 homepage = @specification.homepage # Make sure a homepage is valid HTTP/HTTPS URI - if homepage and not homepage.empty? - require 'uri' + if homepage && !homepage.empty? + 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 @@ -444,7 +475,7 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li def validate_shebang_line_in(executable) executable_path = File.join(@specification.bindir, executable) - return if File.read(executable_path, 2) == '#!' + return if File.read(executable_path, 2) == "#!" warning "#{executable_path} is missing #! line" end @@ -456,17 +487,47 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li end def validate_extensions # :nodoc: - require_relative 'ext' + require_relative "ext" builder = Gem::Ext::Builder.new(@specification) + validate_rake_extensions(builder) + validate_rust_extensions(builder) + 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.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. + ERROR + end + + 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_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/stub_specification.rb b/lib/rubygems/stub_specification.rb index 4246f9de86..58748df5d6 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 @@ -6,10 +7,10 @@ class Gem::StubSpecification < Gem::BasicSpecification # :nodoc: - PREFIX = "# stub: ".freeze + PREFIX = "# stub: " # :nodoc: - OPEN_MODE = 'r:UTF-8:-'.freeze + OPEN_MODE = "r:UTF-8:-" class StubLine # :nodoc: all attr_reader :name, :version, :platform, :require_paths, :extensions, @@ -19,9 +20,9 @@ class Gem::StubSpecification < Gem::BasicSpecification # These are common require paths. REQUIRE_PATHS = { # :nodoc: - 'lib' => 'lib'.freeze, - 'test' => 'test'.freeze, - 'ext' => 'ext'.freeze, + "lib" => "lib", + "test" => "test", + "ext" => "ext", }.freeze # These are common require path lists. This hash is used to optimize @@ -29,28 +30,28 @@ class Gem::StubSpecification < Gem::BasicSpecification # in their require paths, so lets take advantage of that by pre-allocating # a require path list for that case. REQUIRE_PATH_LIST = { # :nodoc: - 'lib' => ['lib'].freeze, + "lib" => ["lib"].freeze, }.freeze def initialize(data, extensions) - parts = data[PREFIX.length..-1].split(" ".freeze, 4) - @name = parts[0].freeze + parts = data[PREFIX.length..-1].split(" ", 4) + @name = -parts[0] @version = if Gem::Version.correct?(parts[1]) - Gem::Version.new(parts[1]) - else - Gem::Version.new(0) - end + Gem::Version.new(parts[1]) + else + Gem::Version.new(0) + end @platform = Gem::Platform.new parts[2] @extensions = extensions @full_name = if platform == Gem::Platform::RUBY - "#{name}-#{version}" - else - "#{name}-#{version}-#{platform}" - end + "#{name}-#{version}" + else + "#{name}-#{version}-#{platform}" + end path_list = parts.last - @require_paths = REQUIRE_PATH_LIST[path_list] || path_list.split("\0".freeze).map! do |x| + @require_paths = REQUIRE_PATH_LIST[path_list] || path_list.split("\0").map! do |x| REQUIRE_PATHS[x] || x end end @@ -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 @@ -84,10 +84,10 @@ class Gem::StubSpecification < Gem::BasicSpecification def activated? @activated ||= - begin - loaded = Gem.loaded_specs[name] - loaded && loaded.version == version - end + begin + loaded = Gem.loaded_specs[name] + loaded && loaded.version == version + end end def default_gem? @@ -110,21 +110,24 @@ class Gem::StubSpecification < Gem::BasicSpecification begin saved_lineno = $. - File.open 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" - else - StubLine::NO_EXTENSIONS - end - - @data = StubLine.new stubline, extensions - end - rescue EOFError + Gem.open_file loaded_from, OPEN_MODE do |file| + 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 + + stubline.chomp! # readline(chomp: true) allocates 3x as much as .readline.chomp! + @data = StubLine.new stubline, extensions end + rescue EOFError end ensure $. = saved_lineno @@ -183,17 +186,15 @@ class Gem::StubSpecification < Gem::BasicSpecification ## # The full Gem::Specification for this gem, loaded from evalling its gemspec - def to_spec + def spec @spec ||= if @data - loaded = Gem.loaded_specs[name] - loaded if loaded && loaded.version == version - end + loaded = Gem.loaded_specs[name] + loaded if loaded && loaded.version == version + end @spec ||= Gem::Specification.load(loaded_from) - @spec.ignored = @ignored if @spec - - @spec end + alias_method :to_spec, :spec ## # Is this StubSpecification valid? i.e. have we found a stub line, OR does diff --git a/lib/rubygems/syck_hack.rb b/lib/rubygems/syck_hack.rb deleted file mode 100644 index 051483eac8..0000000000 --- a/lib/rubygems/syck_hack.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true -# :stopdoc: - -# Hack to handle syck's DefaultKey bug -# -# This file is always loaded AFTER either syck or psych are already -# loaded. It then looks at what constants are available and creates -# a consistent view on all rubys. -# -# All this is so that there is always a YAML::Syck::DefaultKey -# class no matter if the full yaml library has loaded or not. -# - -module YAML # :nodoc: - # In newer 1.9.2, there is a Syck toplevel constant instead of it - # being underneath YAML. If so, reference it back under YAML as - # well. - if defined? ::Syck - # for tests that change YAML::ENGINE - # 1.8 does not support the second argument to const_defined? - remove_const :Syck rescue nil - - Syck = ::Syck - - # JRuby's "Syck" is called "Yecht" - elsif defined? YAML::Yecht - Syck = YAML::Yecht - - # Otherwise, if there is no YAML::Syck, then we've got just psych - # loaded, so lets define a stub for DefaultKey. - elsif !defined? YAML::Syck - module Syck - class DefaultKey # :nodoc: - end - end - end - - # Now that we've got something that is always here, define #to_s - # so when code tries to use this, it at least just shows up like it - # should. - module Syck - class DefaultKey - remove_method :to_s rescue nil - - def to_s - '=' - end - end - end - - SyntaxError = Error unless defined? SyntaxError -end - -# Sometime in the 1.9 dev cycle, the Syck constant was moved from under YAML -# to be a toplevel constant. So gemspecs created under these versions of Syck -# will have references to Syck::DefaultKey. -# -# So we need to be sure that we reference Syck at the toplevel too so that -# we can always load these kind of gemspecs. -# -if !defined?(Syck) - Syck = YAML::Syck -end - -# Now that we've got Syck setup in all the right places, store -# a reference to the DefaultKey class inside Gem. We do this so that -# if later on YAML, etc are redefined, we've still got a consistent -# place to find the DefaultKey class for comparison. - -module Gem - # for tests that change YAML::ENGINE - remove_const :SyckDefaultKey if const_defined? :SyckDefaultKey - - SyckDefaultKey = YAML::Syck::DefaultKey -end - -# :startdoc: diff --git a/lib/rubygems/test_case.rb b/lib/rubygems/test_case.rb deleted file mode 100644 index 8dde20452e..0000000000 --- a/lib/rubygems/test_case.rb +++ /dev/null @@ -1,1584 +0,0 @@ -# frozen_string_literal: true - -require 'rubygems' - -# If bundler gemspec exists, add to stubs -bundler_gemspec = File.expand_path("../../../bundler/bundler.gemspec", __FILE__) -if File.exist?(bundler_gemspec) - Gem::Specification.dirs.unshift File.dirname(bundler_gemspec) - Gem::Specification.class_variable_set :@@stubs, nil - Gem::Specification.stubs - Gem::Specification.dirs.shift -end - -begin - gem 'test-unit', '~> 3.0' -rescue Gem::LoadError -end - -begin - require 'simplecov' - SimpleCov.start do - add_filter "/test/" - add_filter "/bundler/" - add_filter "/lib/rubygems/resolver/molinillo" - end -rescue LoadError -end - -if File.exist?(bundler_gemspec) - require_relative '../../bundler/lib/bundler' -else - require 'bundler' -end - -require 'test/unit' - -ENV["JARS_SKIP"] = "true" if Gem.java_platform? # avoid unnecessary and noisy `jar-dependencies` post install hook - -require 'rubygems/deprecate' - -require 'fileutils' -require 'pathname' -require 'pp' -require 'rubygems/package' -require 'shellwords' -require 'tmpdir' -require 'uri' -require 'zlib' -require 'benchmark' # stdlib -require 'rubygems/mock_gem_ui' - -module Gem - - ## - # Allows setting the gem path searcher. This method is available when - # requiring 'rubygems/test_case' - - def self.searcher=(searcher) - @searcher = searcher - end - - ## - # Allows toggling Windows behavior. This method is available when requiring - # 'rubygems/test_case' - - def self.win_platform=(val) - @@win_platform = val - end - - ## - # Allows setting path to Ruby. This method is available when requiring - # 'rubygems/test_case' - - def self.ruby=(ruby) - @ruby = ruby - end - - ## - # When rubygems/test_case is required the default user interaction is a - # MockGemUi. - - module DefaultUserInteraction - @ui = Gem::MockGemUi.new - end -end - -require "rubygems/command" - -class Gem::Command - ## - # Allows resetting the hash of specific args per command. This method is - # available when requiring 'rubygems/test_case' - - def self.specific_extra_args_hash=(value) - @specific_extra_args_hash = value - end -end - -## -# RubyGemTestCase provides a variety of methods for testing rubygems and -# gem-related behavior in a sandbox. Through RubyGemTestCase you can install -# and uninstall gems, fetch remote gems through a stub fetcher and be assured -# your normal set of gems is not affected. - -class Gem::TestCase < Test::Unit::TestCase - extend Gem::Deprecate - - attr_accessor :fetcher # :nodoc: - - attr_accessor :gem_repo # :nodoc: - - attr_accessor :uri # :nodoc: - - TEST_PATH = ENV.fetch('RUBYGEMS_TEST_PATH', File.expand_path('../../../test/rubygems', __FILE__)) - - def assert_activate(expected, *specs) - specs.each do |spec| - case spec - when String then - Gem::Specification.find_by_name(spec).activate - when Gem::Specification then - spec.activate - else - flunk spec.inspect - end - end - - loaded = Gem.loaded_specs.values.map(&:full_name) - - assert_equal expected.sort, loaded.sort if expected - end - - def assert_directory_exists(path, msg = nil) - msg = build_message(msg, "Expected path '#{path}' to be a directory") - assert_path_exist path - assert File.directory?(path), msg - end - - # https://github.com/seattlerb/minitest/blob/21d9e804b63c619f602f3f4ece6c71b48974707a/lib/minitest/assertions.rb#L188 - def _synchronize - yield - end - - # https://github.com/seattlerb/minitest/blob/21d9e804b63c619f602f3f4ece6c71b48974707a/lib/minitest/assertions.rb#L546 - def capture_subprocess_io - _synchronize do - begin - require "tempfile" - - captured_stdout, captured_stderr = Tempfile.new("out"), Tempfile.new("err") - - orig_stdout, orig_stderr = $stdout.dup, $stderr.dup - $stdout.reopen captured_stdout - $stderr.reopen captured_stderr - - yield - - $stdout.rewind - $stderr.rewind - - return captured_stdout.read, captured_stderr.read - ensure - captured_stdout.unlink - captured_stderr.unlink - $stdout.reopen orig_stdout - $stderr.reopen orig_stderr - - orig_stdout.close - orig_stderr.close - captured_stdout.close - captured_stderr.close - end - end - end - - ## - # Sets the ENABLE_SHARED entry in RbConfig::CONFIG to +value+ and restores - # the original value when the block ends - - def enable_shared(value) - enable_shared = RbConfig::CONFIG['ENABLE_SHARED'] - RbConfig::CONFIG['ENABLE_SHARED'] = value - - yield - ensure - if enable_shared - RbConfig::CONFIG['enable_shared'] = enable_shared - else - RbConfig::CONFIG.delete 'enable_shared' - end - end - - ## - # Sets the vendordir entry in RbConfig::CONFIG to +value+ and restores the - # original value when the block ends - # - def vendordir(value) - vendordir = RbConfig::CONFIG['vendordir'] - - if value - RbConfig::CONFIG['vendordir'] = value - else - RbConfig::CONFIG.delete 'vendordir' - end - - yield - ensure - if vendordir - RbConfig::CONFIG['vendordir'] = vendordir - else - RbConfig::CONFIG.delete 'vendordir' - end - end - - ## - # Sets the bindir entry in RbConfig::CONFIG to +value+ and restores the - # original value when the block ends - # - def bindir(value) - with_clean_path_to_ruby do - bindir = RbConfig::CONFIG['bindir'] - - if value - RbConfig::CONFIG['bindir'] = value - else - RbConfig::CONFIG.delete 'bindir' - end - - begin - yield - ensure - if bindir - RbConfig::CONFIG['bindir'] = bindir - else - RbConfig::CONFIG.delete 'bindir' - end - end - end - end - - ## - # Sets the EXEEXT entry in RbConfig::CONFIG to +value+ and restores the - # original value when the block ends - # - def exeext(value) - exeext = RbConfig::CONFIG['EXEEXT'] - - if value - RbConfig::CONFIG['EXEEXT'] = value - else - RbConfig::CONFIG.delete 'EXEEXT' - end - - yield - ensure - if exeext - RbConfig::CONFIG['EXEEXT'] = exeext - else - RbConfig::CONFIG.delete 'EXEEXT' - end - end - - def scan_make_command_lines(output) - output.scan(/^#{Regexp.escape make_command}(?:[[:blank:]].*)?$/) - end - - def parse_make_command_line(line) - command, *args = line.shellsplit - - targets = [] - macros = {} - - args.each do |arg| - case arg - when /\A(\w+)=/ - macros[$1] = $' - else - targets << arg - end - end - - targets << '' if targets.empty? - - { - :command => command, - :targets => targets, - :macros => macros, - } - end - - def assert_contains_make_command(target, output, msg = nil) - if output.match(/\n/) - msg = build_message(msg, - "Expected output containing make command \"%s\", but was \n\nBEGIN_OF_OUTPUT\n%sEND_OF_OUTPUT" % [ - ('%s %s' % [make_command, target]).rstrip, - output, - ] - ) - else - msg = build_message(msg, - 'Expected make command "%s": %s' % [ - ('%s %s' % [make_command, target]).rstrip, - output, - ] - ) - end - - assert scan_make_command_lines(output).any? {|line| - make = parse_make_command_line(line) - - if make[:targets].include?(target) - yield make, line if block_given? - true - else - false - end - }, msg - end - - include Gem::DefaultUserInteraction - - ## - # #setup prepares a sandboxed location to install gems. All installs are - # directed to a temporary directory. All install plugins are removed. - # - # If the +RUBY+ environment variable is set the given path is used for - # Gem::ruby. The local platform is set to <tt>i386-mswin32</tt> for Windows - # or <tt>i686-darwin8.10.1</tt> otherwise. - - def setup - @orig_env = ENV.to_hash - @tmp = File.expand_path("tmp") - - FileUtils.mkdir_p @tmp - - ENV['GEM_VENDOR'] = nil - ENV['GEMRC'] = nil - ENV['XDG_CACHE_HOME'] = nil - ENV['XDG_CONFIG_HOME'] = nil - ENV['XDG_DATA_HOME'] = nil - ENV['SOURCE_DATE_EPOCH'] = nil - ENV['BUNDLER_VERSION'] = nil - - @current_dir = Dir.pwd - @fetcher = nil - - @back_ui = Gem::DefaultUserInteraction.ui - @ui = Gem::MockGemUi.new - # This needs to be a new instance since we call use_ui(@ui) when we want to - # capture output - Gem::DefaultUserInteraction.ui = Gem::MockGemUi.new - - @tempdir = Dir.mktmpdir("test_rubygems_", @tmp) - @tempdir.tap(&Gem::UNTAINT) - - ENV["TMPDIR"] = @tempdir - - @orig_SYSTEM_WIDE_CONFIG_FILE = Gem::ConfigFile::SYSTEM_WIDE_CONFIG_FILE - Gem::ConfigFile.send :remove_const, :SYSTEM_WIDE_CONFIG_FILE - Gem::ConfigFile.send :const_set, :SYSTEM_WIDE_CONFIG_FILE, - File.join(@tempdir, 'system-gemrc') - - @gemhome = File.join @tempdir, 'gemhome' - @userhome = File.join @tempdir, 'userhome' - ENV["GEM_SPEC_CACHE"] = File.join @tempdir, 'spec_cache' - - @orig_ruby = if ENV['RUBY'] - ruby = Gem.ruby - Gem.ruby = ENV['RUBY'] - ruby - end - - @git = ENV['GIT'] || (win_platform? ? 'git.exe' : 'git') - - Gem.ensure_gem_subdirectories @gemhome - Gem.ensure_default_gem_subdirectories @gemhome - - @orig_LOAD_PATH = $LOAD_PATH.dup - $LOAD_PATH.map! do |s| - expand_path = File.realpath(s) rescue File.expand_path(s) - if expand_path != s - expand_path.tap(&Gem::UNTAINT) - if s.instance_variable_defined?(:@gem_prelude_index) - expand_path.instance_variable_set(:@gem_prelude_index, expand_path) - end - expand_path.freeze if s.frozen? - s = expand_path - end - s - end - - Dir.chdir @tempdir - - ENV['HOME'] = @userhome - Gem.instance_variable_set :@config_file, nil - Gem.instance_variable_set :@user_home, nil - Gem.instance_variable_set :@config_home, nil - Gem.instance_variable_set :@data_home, nil - Gem.instance_variable_set :@gemdeps, nil - Gem.instance_variable_set :@env_requirements_by_name, nil - Gem.send :remove_instance_variable, :@ruby_version if - Gem.instance_variables.include? :@ruby_version - - FileUtils.mkdir_p @userhome - - ENV['GEM_PRIVATE_KEY_PASSPHRASE'] = PRIVATE_KEY_PASSPHRASE - - if Gem.java_platform? - @orig_default_gem_home = RbConfig::CONFIG['default_gem_home'] - RbConfig::CONFIG['default_gem_home'] = @gemhome - else - Gem.instance_variable_set(:@default_dir, @gemhome) - end - - @orig_bindir = RbConfig::CONFIG["bindir"] - RbConfig::CONFIG["bindir"] = File.join @gemhome, "bin" - - Gem::Specification.unresolved_deps.clear - Gem.use_paths(@gemhome) - - Gem.loaded_specs.clear - Gem.instance_variable_set(:@activated_gem_paths, 0) - Gem.clear_default_specs - Bundler.reset! - - Gem.configuration.verbose = true - Gem.configuration.update_sources = true - - Gem::RemoteFetcher.fetcher = Gem::FakeFetcher.new - - @gem_repo = "http://gems.example.com/" - @uri = URI.parse @gem_repo - Gem.sources.replace [@gem_repo] - - Gem.searcher = nil - Gem::SpecFetcher.fetcher = nil - - @orig_arch = RbConfig::CONFIG['arch'] - - if win_platform? - util_set_arch 'i386-mswin32' - else - util_set_arch 'i686-darwin8.10.1' - end - - @orig_hooks = {} - %w[post_install_hooks done_installing_hooks post_uninstall_hooks pre_uninstall_hooks pre_install_hooks pre_reset_hooks post_reset_hooks post_build_hooks].each do |name| - @orig_hooks[name] = Gem.send(name).dup - end - - @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" - @orig_LOADED_FEATURES = $LOADED_FEATURES.dup - end - - ## - # #teardown restores the process to its original state and removes the - # tempdir - - def teardown - $LOAD_PATH.replace @orig_LOAD_PATH if @orig_LOAD_PATH - if @orig_LOADED_FEATURES - if @orig_LOAD_PATH - ($LOADED_FEATURES - @orig_LOADED_FEATURES).each do |feat| - $LOADED_FEATURES.delete(feat) if feat.start_with?(@tmp) - end - else - $LOADED_FEATURES.replace @orig_LOADED_FEATURES - end - end - - RbConfig::CONFIG['arch'] = @orig_arch - - if defined? Gem::RemoteFetcher - Gem::RemoteFetcher.fetcher = nil - end - - Dir.chdir @current_dir - - FileUtils.rm_rf @tempdir - - ENV.replace(@orig_env) - - Gem::ConfigFile.send :remove_const, :SYSTEM_WIDE_CONFIG_FILE - Gem::ConfigFile.send :const_set, :SYSTEM_WIDE_CONFIG_FILE, - @orig_SYSTEM_WIDE_CONFIG_FILE - - Gem.ruby = @orig_ruby if @orig_ruby - - RbConfig::CONFIG['bindir'] = @orig_bindir - - if Gem.java_platform? - RbConfig::CONFIG['default_gem_home'] = @orig_default_gem_home - else - Gem.instance_variable_set :@default_dir, nil - end - - Gem::Specification._clear_load_cache - Gem::Specification.unresolved_deps.clear - Gem::refresh - - @orig_hooks.each do |name, hooks| - Gem.send(name).replace hooks - end - - @back_ui.close - end - - def credential_setup - @temp_cred = File.join(@userhome, '.gem', 'credentials') - FileUtils.mkdir_p File.dirname(@temp_cred) - File.write @temp_cred, ':rubygems_api_key: 701229f217cdf23b1344c7b4b54ca97' - File.chmod 0600, @temp_cred - end - - def credential_teardown - FileUtils.rm_rf @temp_cred - end - - def common_installer_setup - common_installer_teardown - - Gem.post_build do |installer| - @post_build_hook_arg = installer - true - end - - Gem.post_install do |installer| - @post_install_hook_arg = installer - end - - Gem.post_uninstall do |uninstaller| - @post_uninstall_hook_arg = uninstaller - end - - Gem.pre_install do |installer| - @pre_install_hook_arg = installer - true - end - - Gem.pre_uninstall do |uninstaller| - @pre_uninstall_hook_arg = uninstaller - end - end - - def common_installer_teardown - Gem.post_build_hooks.clear - Gem.post_install_hooks.clear - Gem.done_installing_hooks.clear - Gem.post_reset_hooks.clear - Gem.post_uninstall_hooks.clear - Gem.pre_install_hooks.clear - Gem.pre_reset_hooks.clear - Gem.pre_uninstall_hooks.clear - end - - ## - # A git_gem is used with a gem dependencies file. The gem created here - # has no files, just a gem specification for the given +name+ and +version+. - # - # Yields the +specification+ to the block, if given - - def git_gem(name = 'a', version = 1) - have_git? - - directory = File.join 'git', name - directory = File.expand_path directory - - git_spec = Gem::Specification.new name, version do |specification| - yield specification if block_given? - end - - FileUtils.mkdir_p directory - - gemspec = "#{name}.gemspec" - - File.open File.join(directory, gemspec), 'w' do |io| - io.write git_spec.to_ruby - end - - head = nil - - Dir.chdir directory do - unless File.exist? '.git' - system @git, 'init', '--quiet' - system @git, 'config', 'user.name', 'RubyGems Tests' - system @git, 'config', 'user.email', 'rubygems@example' - end - - system @git, 'add', gemspec - system @git, 'commit', '-a', '-m', 'a non-empty commit message', '--quiet' - head = Gem::Util.popen(@git, 'rev-parse', 'master').strip - end - - return name, git_spec.version, directory, head - end - - ## - # Skips this test unless you have a git executable - - def have_git? - return if in_path? @git - - skip 'cannot find git executable, use GIT environment variable to set' - end - - def in_path?(executable) # :nodoc: - return true if %r{\A([A-Z]:|/)} =~ executable and File.exist? executable - - ENV['PATH'].split(File::PATH_SEPARATOR).any? do |directory| - File.exist? File.join directory, executable - end - end - - ## - # Builds and installs the Gem::Specification +spec+ - - def install_gem(spec, options = {}) - require 'rubygems/installer' - - gem = spec.cache_file - - unless File.exist? gem - use_ui Gem::MockGemUi.new do - Dir.chdir @tempdir do - Gem::Package.build spec - end - end - - gem = File.join(@tempdir, File.basename(gem)).tap(&Gem::UNTAINT) - end - - Gem::Installer.at(gem, options.merge({:wrappers => true})).install - end - - ## - # Builds and installs the Gem::Specification +spec+ into the user dir - - def install_gem_user(spec) - install_gem spec, :user_install => true - end - - ## - # Uninstalls the Gem::Specification +spec+ - def uninstall_gem(spec) - require 'rubygems/uninstaller' - - Class.new(Gem::Uninstaller) do - def ask_if_ok(spec) - true - end - end.new(spec.name, :executables => true, :user_install => true).uninstall - end - - ## - # Enables pretty-print for all tests - - def mu_pp(obj) - s = String.new - s = PP.pp obj, s - s = s.force_encoding(Encoding.default_external) - s.chomp - end - - ## - # Reads a Marshal file at +path+ - - def read_cache(path) - File.open path.dup.tap(&Gem::UNTAINT), 'rb' do |io| - Marshal.load io.read - end - end - - ## - # Reads a binary file at +path+ - - def read_binary(path) - Gem.read_binary path - end - - ## - # Writes a binary file to +path+ which is relative to +@gemhome+ - - def write_file(path) - path = File.join @gemhome, path unless Pathname.new(path).absolute? - dir = File.dirname path - FileUtils.mkdir_p dir unless File.directory? dir - - File.open path, 'wb' do |io| - yield io if block_given? - end - - path - end - - def all_spec_names - Gem::Specification.map(&:full_name) - end - - ## - # Creates a Gem::Specification with a minimum of extra work. +name+ and - # +version+ are the gem's name and version, platform, author, email, - # homepage, summary and description are defaulted. The specification is - # yielded for customization. - # - # The gem is added to the installed gems in +@gemhome+ and the runtime. - # - # Use this with #write_file to build an installed gem. - - def quick_gem(name, version='2') - require 'rubygems/specification' - - spec = Gem::Specification.new do |s| - s.platform = Gem::Platform::RUBY - s.name = name - s.version = version - s.author = 'A User' - s.email = 'example@example.com' - s.homepage = 'http://example.com' - s.summary = "this is a summary" - s.description = "This is a test description" - - yield(s) if block_given? - end - - written_path = write_file spec.spec_file do |io| - io.write spec.to_ruby_for_cache - end - - spec.loaded_from = written_path - - Gem::Specification.reset - - return spec - end - - ## - # Builds a gem from +spec+ and places it in <tt>File.join @gemhome, - # 'cache'</tt>. Automatically creates files based on +spec.files+ - - def util_build_gem(spec) - dir = spec.gem_dir - FileUtils.mkdir_p dir - - Dir.chdir dir do - spec.files.each do |file| - next if File.exist? file - FileUtils.mkdir_p File.dirname(file) - - File.open file, 'w' do |fp| - fp.puts "# #{file}" - end - end - - use_ui Gem::MockGemUi.new do - Gem::Package.build spec - end - - cache = spec.cache_file - FileUtils.mv File.basename(cache), cache - end - end - - def util_remove_gem(spec) - FileUtils.rm_rf spec.cache_file - FileUtils.rm_rf spec.spec_file - end - - ## - # Removes all installed gems from +@gemhome+. - - def util_clear_gems - FileUtils.rm_rf File.join(@gemhome, "gems") - FileUtils.mkdir File.join(@gemhome, "gems") - FileUtils.rm_rf File.join(@gemhome, "specifications") - FileUtils.mkdir File.join(@gemhome, "specifications") - Gem::Specification.reset - end - - ## - # Install the provided specs - - def install_specs(*specs) - specs.each do |spec| - Gem::Installer.for_spec(spec, :force => true).install - end - - Gem.searcher = nil - end - - ## - # Installs the provided default specs including writing the spec file - - def install_default_gems(*specs) - specs.each do |spec| - installer = Gem::Installer.for_spec(spec, :install_as_default => true) - installer.install - Gem.register_default_spec(spec) - end - end - - def loaded_spec_names - Gem.loaded_specs.values.map(&:full_name).sort - end - - def unresolved_names - Gem::Specification.unresolved_deps.values.map(&:to_s).sort - end - - def save_loaded_features - old_loaded_features = $LOADED_FEATURES.dup - yield - ensure - prefix = File.dirname(__FILE__) + "/" - new_features = ($LOADED_FEATURES - old_loaded_features) - old_loaded_features.concat(new_features.select {|f| f.rindex(prefix, 0) }) - $LOADED_FEATURES.replace old_loaded_features - end - - def new_default_spec(name, version, deps = nil, *files) - spec = util_spec name, version, deps - - spec.loaded_from = File.join(@gemhome, "specifications", "default", spec.spec_name) - spec.files = files - - lib_dir = File.join(@tempdir, "default_gems", "lib") - lib_dir.instance_variable_set(:@gem_prelude_index, lib_dir) - Gem.instance_variable_set(:@default_gem_load_paths, [*Gem.send(:default_gem_load_paths), lib_dir]) - $LOAD_PATH.unshift(lib_dir) - files.each do |file| - rb_path = File.join(lib_dir, file) - FileUtils.mkdir_p(File.dirname(rb_path)) - File.open(rb_path, "w") do |rb| - rb << "# #{file}" - end - end - - spec - end - - ## - # Creates a spec with +name+, +version+. +deps+ can specify the dependency - # or a +block+ can be given for full customization of the specification. - - def util_spec(name, version = 2, deps = nil, *files) # :yields: specification - raise "deps or block, not both" if deps and block_given? - - spec = Gem::Specification.new do |s| - s.platform = Gem::Platform::RUBY - s.name = name - s.version = version - s.author = 'A User' - s.email = 'example@example.com' - s.homepage = 'http://example.com' - s.summary = "this is a summary" - s.description = "This is a test description" - - s.files.push(*files) unless files.empty? - - yield s if block_given? - end - - if deps - deps.keys.each do |n| - spec.add_dependency n, (deps[n] || '>= 0') - end - end - - unless files.empty? - write_file spec.spec_file do |io| - io.write spec.to_ruby_for_cache - end - - util_build_gem spec - - FileUtils.rm spec.spec_file - end - - return spec - end - - ## - # Creates a gem with +name+, +version+ and +deps+. The specification will - # be yielded before gem creation for customization. The gem will be placed - # in <tt>File.join @tempdir, 'gems'</tt>. The specification and .gem file - # location are returned. - - def util_gem(name, version, deps = nil, &block) - if deps - block = proc do |s| - deps.keys.each do |n| - s.add_dependency n, (deps[n] || '>= 0') - end - end - end - - spec = quick_gem(name, version, &block) - - util_build_gem spec - - cache_file = File.join @tempdir, 'gems', "#{spec.original_name}.gem" - FileUtils.mkdir_p File.dirname cache_file - FileUtils.mv spec.cache_file, cache_file - FileUtils.rm spec.spec_file - - spec.loaded_from = nil - - [spec, cache_file] - end - - ## - # Gzips +data+. - - def util_gzip(data) - out = StringIO.new - - Zlib::GzipWriter.wrap out do |io| - io.write data - end - - out.string - end - - ## - # Creates several default gems which all have a lib/code.rb file. The gems - # are not installed but are available in the cache dir. - # - # +@a1+:: gem a version 1, this is the best-described gem. - # +@a2+:: gem a version 2 - # +@a3a:: gem a version 3.a - # +@a_evil9+:: gem a_evil version 9, use this to ensure similarly-named gems - # don't collide with a. - # +@b2+:: gem b version 2 - # +@c1_2+:: gem c version 1.2 - # +@pl1+:: gem pl version 1, this gem has a legacy platform of i386-linux. - # - # Additional +prerelease+ gems may also be created: - # - # +@a2_pre+:: gem a version 2.a - # TODO: nuke this and fix tests. this should speed up a lot - - def util_make_gems(prerelease = false) - @a1 = quick_gem 'a', '1' do |s| - s.files = %w[lib/code.rb] - s.require_paths = %w[lib] - s.date = Gem::Specification::TODAY - 86400 - s.homepage = 'http://a.example.com' - s.email = %w[example@example.com example2@example.com] - s.authors = %w[Example Example2] - s.description = <<-DESC -This line is really, really long. So long, in fact, that it is more than eighty characters long! The purpose of this line is for testing wrapping behavior because sometimes people don't wrap their text to eighty characters. Without the wrapping, the text might not look good in the RSS feed. - -Also, a list: - * An entry that\'s actually kind of sort - * an entry that\'s really long, which will probably get wrapped funny. That's ok, somebody wasn't thinking straight when they made it more than eighty characters. - DESC - end - - init = proc do |s| - s.files = %w[lib/code.rb] - s.require_paths = %w[lib] - end - - @a2 = quick_gem('a', '2', &init) - @a3a = quick_gem('a', '3.a', &init) - @a_evil9 = quick_gem('a_evil', '9', &init) - @b2 = quick_gem('b', '2', &init) - @c1_2 = quick_gem('c', '1.2', &init) - @x = quick_gem('x', '1', &init) - @dep_x = quick_gem('dep_x', '1') do |s| - s.files = %w[lib/code.rb] - s.require_paths = %w[lib] - s.add_dependency 'x', '>= 1' - end - - @pl1 = quick_gem 'pl', '1' do |s| # l for legacy - s.files = %w[lib/code.rb] - s.require_paths = %w[lib] - s.platform = Gem::Platform.new 'i386-linux' - s.instance_variable_set :@original_platform, 'i386-linux' - end - - if prerelease - @a2_pre = quick_gem('a', '2.a', &init) - write_file File.join(*%W[gems #{@a2_pre.original_name} lib code.rb]) - util_build_gem @a2_pre - end - - write_file File.join(*%W[gems #{@a1.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@a2.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@a3a.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@a_evil9.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@b2.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@c1_2.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@pl1.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@x.original_name} lib code.rb]) - write_file File.join(*%W[gems #{@dep_x.original_name} lib code.rb]) - - [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2, @pl1, @x, @dep_x].each do |spec| - util_build_gem spec - end - - FileUtils.rm_r File.join(@gemhome, "gems", @pl1.original_name) - end - - ## - # Set the platform to +arch+ - - def util_set_arch(arch) - RbConfig::CONFIG['arch'] = arch - platform = Gem::Platform.new arch - - Gem.instance_variable_set :@platforms, nil - Gem::Platform.instance_variable_set :@local, nil - - yield if block_given? - - platform - end - - ## - # Add +spec+ to +@fetcher+ serving the data in the file +path+. - # +repo+ indicates which repo to make +spec+ appear to be in. - - def add_to_fetcher(spec, path=nil, repo=@gem_repo) - path ||= spec.cache_file - @fetcher.data["#{@gem_repo}gems/#{spec.file_name}"] = read_binary(path) - end - - ## - # Sets up Gem::SpecFetcher to return information from the gems in +specs+. - - def util_setup_spec_fetcher(*specs) - all_specs = Gem::Specification.to_a + specs - Gem::Specification._resort! all_specs - - spec_fetcher = Gem::SpecFetcher.fetcher - - prerelease, all = all_specs.partition {|spec| spec.version.prerelease? } - latest = Gem::Specification._latest_specs all_specs - - spec_fetcher.specs[@uri] = [] - all.each do |spec| - spec_fetcher.specs[@uri] << spec.name_tuple - end - - spec_fetcher.latest_specs[@uri] = [] - latest.each do |spec| - spec_fetcher.latest_specs[@uri] << spec.name_tuple - end - - spec_fetcher.prerelease_specs[@uri] = [] - prerelease.each do |spec| - spec_fetcher.prerelease_specs[@uri] << spec.name_tuple - end - - # HACK for test_download_to_cache - unless Gem::RemoteFetcher === @fetcher - v = Gem.marshal_version - - specs = all.map {|spec| spec.name_tuple } - s_zip = util_gzip Marshal.dump Gem::NameTuple.to_basic specs - - latest_specs = latest.map do |spec| - spec.name_tuple - end - - l_zip = util_gzip Marshal.dump Gem::NameTuple.to_basic latest_specs - - prerelease_specs = prerelease.map {|spec| spec.name_tuple } - p_zip = util_gzip Marshal.dump Gem::NameTuple.to_basic prerelease_specs - - @fetcher.data["#{@gem_repo}specs.#{v}.gz"] = s_zip - @fetcher.data["#{@gem_repo}latest_specs.#{v}.gz"] = l_zip - @fetcher.data["#{@gem_repo}prerelease_specs.#{v}.gz"] = p_zip - - v = Gem.marshal_version - - all_specs.each do |spec| - path = "#{@gem_repo}quick/Marshal.#{v}/#{spec.original_name}.gemspec.rz" - data = Marshal.dump spec - data_deflate = Zlib::Deflate.deflate data - @fetcher.data[path] = data_deflate - end - end - - nil # force errors - end - - ## - # Deflates +data+ - - def util_zip(data) - Zlib::Deflate.deflate data - end - - def util_set_RUBY_VERSION(version, patchlevel = nil, revision = nil, description = nil, engine = "ruby", engine_version = nil) - if Gem.instance_variables.include? :@ruby_version - Gem.send :remove_instance_variable, :@ruby_version - end - - @RUBY_VERSION = RUBY_VERSION - @RUBY_PATCHLEVEL = RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) - @RUBY_REVISION = RUBY_REVISION if defined?(RUBY_REVISION) - @RUBY_DESCRIPTION = RUBY_DESCRIPTION if defined?(RUBY_DESCRIPTION) - @RUBY_ENGINE = RUBY_ENGINE - @RUBY_ENGINE_VERSION = RUBY_ENGINE_VERSION if defined?(RUBY_ENGINE_VERSION) - - util_clear_RUBY_VERSION - - Object.const_set :RUBY_VERSION, version - Object.const_set :RUBY_PATCHLEVEL, patchlevel if patchlevel - Object.const_set :RUBY_REVISION, revision if revision - Object.const_set :RUBY_DESCRIPTION, description if description - Object.const_set :RUBY_ENGINE, engine - Object.const_set :RUBY_ENGINE_VERSION, engine_version if engine_version - end - - def util_restore_RUBY_VERSION - util_clear_RUBY_VERSION - - Object.const_set :RUBY_VERSION, @RUBY_VERSION - Object.const_set :RUBY_PATCHLEVEL, @RUBY_PATCHLEVEL if - defined?(@RUBY_PATCHLEVEL) - Object.const_set :RUBY_REVISION, @RUBY_REVISION if - defined?(@RUBY_REVISION) - Object.const_set :RUBY_DESCRIPTION, @RUBY_DESCRIPTION if - defined?(@RUBY_DESCRIPTION) - Object.const_set :RUBY_ENGINE, @RUBY_ENGINE - Object.const_set :RUBY_ENGINE_VERSION, @RUBY_ENGINE_VERSION if - defined?(@RUBY_ENGINE_VERSION) - end - - def util_clear_RUBY_VERSION - Object.send :remove_const, :RUBY_VERSION - Object.send :remove_const, :RUBY_PATCHLEVEL if defined?(RUBY_PATCHLEVEL) - Object.send :remove_const, :RUBY_REVISION if defined?(RUBY_REVISION) - Object.send :remove_const, :RUBY_DESCRIPTION if defined?(RUBY_DESCRIPTION) - Object.send :remove_const, :RUBY_ENGINE - Object.send :remove_const, :RUBY_ENGINE_VERSION if defined?(RUBY_ENGINE_VERSION) - end - - ## - # Is this test being run on a Windows platform? - - def self.win_platform? - Gem.win_platform? - end - - ## - # Is this test being run on a Windows platform? - - def win_platform? - Gem.win_platform? - end - - ## - # Is this test being run on a Java platform? - - def self.java_platform? - Gem.java_platform? - end - - ## - # Is this test being run on a Java platform? - - def java_platform? - Gem.java_platform? - end - - ## - # Returns whether or not we're on a version of Ruby built with VC++ (or - # Borland) versus Cygwin, Mingw, etc. - - def self.vc_windows? - RUBY_PLATFORM.match('mswin') - end - - ## - # Returns whether or not we're on a version of Ruby built with VC++ (or - # Borland) versus Cygwin, Mingw, etc. - - def vc_windows? - RUBY_PLATFORM.match('mswin') - end - - ## - # Returns the make command for the current platform. For versions of Ruby - # built on MS Windows with VC++ or Borland it will return 'nmake'. On all - # other platforms, including Cygwin, it will return 'make'. - - def self.make_command - ENV["make"] || ENV["MAKE"] || (vc_windows? ? 'nmake' : 'make') - end - - ## - # Returns the make command for the current platform. For versions of Ruby - # built on MS Windows with VC++ or Borland it will return 'nmake'. On all - # other platforms, including Cygwin, it will return 'make'. - - def make_command - ENV["make"] || ENV["MAKE"] || (vc_windows? ? 'nmake' : 'make') - end - - ## - # Returns whether or not the nmake command could be found. - - def nmake_found? - system('nmake /? 1>NUL 2>&1') - end - - # In case we're building docs in a background process, this method waits for - # that process to exit (or if it's already been reaped, or never happened, - # swallows the Errno::ECHILD error). - def wait_for_child_process_to_exit - Process.wait if Process.respond_to?(:fork) - rescue Errno::ECHILD - end - - ## - # Allows tests to use a random (but controlled) port number instead of - # a hardcoded one. This helps CI tools when running parallels builds on - # the same builder slave. - - def self.process_based_port - @@process_based_port ||= 8000 + $$ % 1000 - end - - ## - # See ::process_based_port - - def process_based_port - self.class.process_based_port - end - - ## - # Allows the proper version of +rake+ to be used for the test. - - def build_rake_in(good=true) - gem_ruby = Gem.ruby - Gem.ruby = self.class.rubybin - env_rake = ENV["rake"] - rake = (good ? @@good_rake : @@bad_rake) - ENV["rake"] = rake - yield rake - ensure - Gem.ruby = gem_ruby - if env_rake - ENV["rake"] = env_rake - else - ENV.delete("rake") - end - end - - ## - # Finds the path to the Ruby executable - - def self.rubybin - ruby = ENV["RUBY"] - return ruby if ruby - ruby = "ruby" - rubyexe = "#{ruby}.exe" - - 3.times do - if File.exist? ruby and File.executable? ruby and !File.directory? ruby - return File.expand_path(ruby) - end - if File.exist? rubyexe and File.executable? rubyexe - return File.expand_path(rubyexe) - end - ruby = File.join("..", ruby) - end - - begin - Gem.ruby - rescue LoadError - "ruby" - end - end - - def ruby_with_rubygems_in_load_path - [Gem.ruby, "-I", File.expand_path("..", __dir__)] - end - - def with_clean_path_to_ruby - orig_ruby = Gem.ruby - - Gem.instance_variable_set :@ruby, nil - - yield - ensure - Gem.instance_variable_set :@ruby, orig_ruby - end - - class << self - # :nodoc: - ## - # Return the join path, with escaping backticks, dollars, and - # double-quotes. Unlike `shellescape`, equal-sign is not escaped. - - private - - def escape_path(*path) - path = File.join(*path) - if %r{\A[-+:/=@,.\w]+\z} =~ path - path - else - "\"#{path.gsub(/[`$"]/, '\\&')}\"" - end - end - end - - @@good_rake = "#{rubybin} #{escape_path(TEST_PATH, 'good_rake.rb')}" - @@bad_rake = "#{rubybin} #{escape_path(TEST_PATH, 'bad_rake.rb')}" - - ## - # Construct a new Gem::Dependency. - - def dep(name, *requirements) - Gem::Dependency.new name, *requirements - end - - ## - # Constructs a Gem::Resolver::DependencyRequest from a - # Gem::Dependency +dep+, a +from_name+ and +from_version+ requesting the - # dependency and a +parent+ DependencyRequest - - def dependency_request(dep, from_name, from_version, parent = nil) - remote = Gem::Source.new @uri - - unless parent - parent_dep = dep from_name, from_version - parent = Gem::Resolver::DependencyRequest.new parent_dep, nil - end - - spec = Gem::Resolver::IndexSpecification.new \ - nil, from_name, from_version, remote, Gem::Platform::RUBY - activation = Gem::Resolver::ActivationRequest.new spec, parent - - Gem::Resolver::DependencyRequest.new dep, activation - end - - ## - # Constructs a new Gem::Requirement. - - def req(*requirements) - return requirements.first if Gem::Requirement === requirements.first - Gem::Requirement.create requirements - end - - ## - # Constructs a new Gem::Specification. - - def spec(name, version, &block) - Gem::Specification.new name, v(version), &block - end - - ## - # Creates a SpecFetcher pre-filled with the gems or specs defined in the - # block. - # - # Yields a +fetcher+ object that responds to +spec+ and +gem+. +spec+ adds - # a specification to the SpecFetcher while +gem+ adds both a specification - # and the gem data to the RemoteFetcher so the built gem can be downloaded. - # - # If only the a-3 gem is supposed to be downloaded you can save setup - # time by creating only specs for the other versions: - # - # spec_fetcher do |fetcher| - # fetcher.spec 'a', 1 - # fetcher.spec 'a', 2, 'b' => 3 # dependency on b = 3 - # fetcher.gem 'a', 3 do |spec| - # # spec is a Gem::Specification - # # ... - # end - # end - - def spec_fetcher(repository = @gem_repo) - Gem::TestCase::SpecFetcherSetup.declare self, repository do |spec_fetcher_setup| - yield spec_fetcher_setup if block_given? - end - end - - ## - # Construct a new Gem::Version. - - def v(string) - Gem::Version.create string - end - - ## - # A vendor_gem is used with a gem dependencies file. The gem created here - # has no files, just a gem specification for the given +name+ and +version+. - # - # Yields the +specification+ to the block, if given - - def vendor_gem(name = 'a', version = 1) - directory = File.join 'vendor', name - - FileUtils.mkdir_p directory - - save_gemspec name, version, directory - end - - ## - # create_gemspec creates gem specification in given +directory+ or '.' - # for the given +name+ and +version+. - # - # Yields the +specification+ to the block, if given - - def save_gemspec(name = 'a', version = 1, directory = '.') - vendor_spec = Gem::Specification.new name, version do |specification| - yield specification if block_given? - end - - File.open File.join(directory, "#{name}.gemspec"), 'w' do |io| - io.write vendor_spec.to_ruby - end - - return name, vendor_spec.version, directory - end - - ## - # The StaticSet is a static set of gem specifications used for testing only. - # It is available by requiring Gem::TestCase. - - class StaticSet < Gem::Resolver::Set - ## - # A StaticSet ignores remote because it has a fixed set of gems. - - attr_accessor :remote - - ## - # Creates a new StaticSet for the given +specs+ - - def initialize(specs) - super() - - @specs = specs - - @remote = true - end - - ## - # Adds +spec+ to this set. - - def add(spec) - @specs << spec - end - - ## - # Finds +dep+ in this set. - - def find_spec(dep) - @specs.reverse_each do |s| - return s if dep.matches_spec? s - end - end - - ## - # Finds all gems matching +dep+ in this set. - - def find_all(dep) - @specs.find_all {|s| dep.match? s, @prerelease } - end - - ## - # Loads a Gem::Specification from this set which has the given +name+, - # version +ver+, +platform+. The +source+ is ignored. - - def load_spec(name, ver, platform, source) - dep = Gem::Dependency.new name, ver - spec = find_spec dep - - Gem::Specification.new spec.name, spec.version do |s| - s.platform = spec.platform - end - end - - def prefetch(reqs) # :nodoc: - end - end - - ## - # Loads certificate named +cert_name+ from <tt>test/rubygems/</tt>. - - def self.load_cert(cert_name) - cert_file = cert_path cert_name - - cert = File.read cert_file - - OpenSSL::X509::Certificate.new cert - end - - ## - # Returns the path to the certificate named +cert_name+ from - # <tt>test/rubygems/</tt>. - - def self.cert_path(cert_name) - if 32 == (Time.at(2**32) rescue 32) - cert_file = "#{TEST_PATH}/#{cert_name}_cert_32.pem" - - return cert_file if File.exist? cert_file - end - - "#{TEST_PATH}/#{cert_name}_cert.pem" - end - - ## - # Loads an RSA private key named +key_name+ with +passphrase+ in <tt>test/rubygems/</tt> - - def self.load_key(key_name, passphrase = nil) - key_file = key_path key_name - - key = File.read key_file - - OpenSSL::PKey::RSA.new key, passphrase - end - - ## - # Returns the path to the key named +key_name+ from <tt>test/rubygems</tt> - - def self.key_path(key_name) - "#{TEST_PATH}/#{key_name}_key.pem" - end - - # :stopdoc: - # only available in RubyGems tests - - PRIVATE_KEY_PASSPHRASE = 'Foo bar'.freeze - - begin - PRIVATE_KEY = load_key 'private' - PRIVATE_KEY_PATH = key_path 'private' - - # ENCRYPTED_PRIVATE_KEY is PRIVATE_KEY encrypted with PRIVATE_KEY_PASSPHRASE - ENCRYPTED_PRIVATE_KEY = load_key 'encrypted_private', PRIVATE_KEY_PASSPHRASE - ENCRYPTED_PRIVATE_KEY_PATH = key_path 'encrypted_private' - - PUBLIC_KEY = PRIVATE_KEY.public_key - - PUBLIC_CERT = load_cert 'public' - PUBLIC_CERT_PATH = cert_path 'public' - rescue Errno::ENOENT - PRIVATE_KEY = nil - PUBLIC_KEY = nil - PUBLIC_CERT = nil - end if Gem::HAVE_OPENSSL -end - -# https://github.com/seattlerb/minitest/blob/13c48a03d84a2a87855a4de0c959f96800100357/lib/minitest/mock.rb#L192 -class Object - def stub(name, val_or_callable, *block_args) - new_name = "__minitest_stub__#{name}" - - metaclass = class << self; self; end - - if respond_to? name and not methods.map(&:to_s).include? name.to_s - metaclass.send :define_method, name do |*args| - super(*args) - end - end - - metaclass.send :alias_method, new_name, name - - metaclass.send :define_method, name do |*args, &blk| - if val_or_callable.respond_to? :call - val_or_callable.call(*args, &blk) - else - blk.call(*block_args) if blk - val_or_callable - end - end - - metaclass.send(:ruby2_keywords, name) if metaclass.respond_to?(:ruby2_keywords, true) - - yield self - ensure - metaclass.send :undef_method, name - metaclass.send :alias_method, name, new_name - metaclass.send :undef_method, new_name - end -end - -require 'rubygems/test_utilities' diff --git a/lib/rubygems/test_utilities.rb b/lib/rubygems/test_utilities.rb deleted file mode 100644 index 08faef6578..0000000000 --- a/lib/rubygems/test_utilities.rb +++ /dev/null @@ -1,373 +0,0 @@ -# frozen_string_literal: true -require 'tempfile' -require 'rubygems' -require 'rubygems/remote_fetcher' - -## -# A fake Gem::RemoteFetcher for use in tests or to avoid real live HTTP -# requests when testing code that uses RubyGems. -# -# Example: -# -# @fetcher = Gem::FakeFetcher.new -# @fetcher.data['http://gems.example.com/yaml'] = source_index.to_yaml -# Gem::RemoteFetcher.fetcher = @fetcher -# -# use nested array if multiple response is needed -# -# @fetcher.data['http://gems.example.com/sequence'] = [['Success', 200, 'OK'], ['Failed', 401, 'Unauthorized']] -# -# @fetcher.fetch_path('http://gems.example.com/sequence') # => ['Success', 200, 'OK'] -# @fetcher.fetch_path('http://gems.example.com/sequence') # => ['Failed', 401, 'Unauthorized'] -# -# # invoke RubyGems code -# -# paths = @fetcher.paths -# assert_equal 'http://gems.example.com/yaml', paths.shift -# assert paths.empty?, paths.join(', ') -# -# See RubyGems' tests for more examples of FakeFetcher. - -class Gem::FakeFetcher - attr_reader :data - attr_reader :last_request - attr_accessor :paths - - def initialize - @data = {} - @paths = [] - end - - def find_data(path) - return Gem.read_binary path.path if URI === path and 'file' == path.scheme - - if URI === path and "URI::#{path.scheme.upcase}" != path.class.name - raise ArgumentError, - "mismatch for scheme #{path.scheme} and class #{path.class}" - end - - path = path.to_s - @paths << path - raise ArgumentError, 'need full URI' unless path.start_with?("https://", "http://") - - unless @data.key? path - raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path) - end - - if @data[path].kind_of?(Array) && @data[path].first.kind_of?(Array) - @data[path].shift - else - @data[path] - end - end - - def fetch_path(path, mtime = nil, head = false) - data = find_data(path) - - if data.respond_to?(:call) - data.call - else - if path.to_s.end_with?(".gz") and not data.nil? and not data.empty? - data = Gem::Util.gunzip data - end - data - end - end - - def cache_update_path(uri, path = nil, update = true) - if data = fetch_path(uri) - File.open(path, 'wb') {|io| io.write data } if path and update - data - else - Gem.read_binary(path) if path - end - end - - # Thanks, FakeWeb! - def open_uri_or_path(path) - data = find_data(path) - body, code, msg = data - - response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg) - response.instance_variable_set(:@body, body) - response.instance_variable_set(:@read, true) - response - end - - def request(uri, request_class, last_modified = nil) - data = find_data(uri) - body, code, msg = (data.respond_to?(:call) ? data.call : data) - - @last_request = request_class.new uri.request_uri - yield @last_request if block_given? - - response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg) - response.instance_variable_set(:@body, body) - response.instance_variable_set(:@read, true) - response - end - - def pretty_print(q) # :nodoc: - q.group 2, '[FakeFetcher', ']' do - q.breakable - q.text 'URIs:' - - q.breakable - q.pp @data.keys - end - end - - def fetch_size(path) - path = path.to_s - @paths << path - - raise ArgumentError, 'need full URI' unless path =~ %r{^http://} - - unless @data.key? path - raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path) - end - - data = @data[path] - - data.respond_to?(:call) ? data.call : data.length - end - - def download(spec, source_uri, install_dir = Gem.dir) - name = File.basename spec.cache_file - path = if Dir.pwd == install_dir # see fetch_command - install_dir - else - File.join install_dir, "cache" - end - - path = File.join path, name - - if source_uri =~ /^http/ - File.open(path, "wb") do |f| - f.write fetch_path(File.join(source_uri, "gems", name)) - end - else - FileUtils.cp source_uri, path - end - - path - end - - def download_to_cache(dependency) - found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency - - return if found.empty? - - spec, source = found.first - - download spec, source.uri.to_s - end -end - -# :stopdoc: -class Gem::RemoteFetcher - def self.fetcher=(fetcher) - @fetcher = fetcher - end -end -# :startdoc: - -## -# The SpecFetcherSetup allows easy setup of a remote source in RubyGems tests: -# -# spec_fetcher do |f| -# f.gem 'a', 1 -# f.spec 'a', 2 -# f.gem 'b', 1' 'a' => '~> 1.0' -# end -# -# The above declaration creates two gems, a-1 and b-1, with a dependency from -# b to a. The declaration creates an additional spec a-2, but no gem for it -# (so it cannot be installed). -# -# After the gems are created they are removed from Gem.dir. - -class Gem::TestCase::SpecFetcherSetup - ## - # Executes a SpecFetcher setup block. Yields an instance then creates the - # gems and specifications defined in the instance. - - def self.declare(test, repository) - setup = new test, repository - - yield setup - - setup.execute - end - - def initialize(test, repository) # :nodoc: - @test = test - @repository = repository - - @gems = {} - @downloaded = [] - @installed = [] - @operations = [] - end - - ## - # Returns a Hash of created Specification full names and the corresponding - # Specification. - - def created_specs - created = {} - - @gems.keys.each do |spec| - created[spec.full_name] = spec - end - - created - end - - ## - # Creates any defined gems or specifications - - def execute # :nodoc: - execute_operations - - setup_fetcher - - created_specs - end - - def execute_operations # :nodoc: - @operations.each do |operation, *arguments| - block = arguments.pop - case operation - when :gem then - spec, gem = @test.util_gem(*arguments, &block) - - write_spec spec - - @gems[spec] = gem - @installed << spec - when :download then - spec, gem = @test.util_gem(*arguments, &block) - - @gems[spec] = gem - @downloaded << spec - when :spec then - spec = @test.util_spec(*arguments, &block) - - write_spec spec - - @gems[spec] = nil - @installed << spec - end - end - end - - ## - # Creates a gem with +name+, +version+ and +deps+. The created gem can be - # downloaded and installed. - # - # The specification will be yielded before gem creation for customization, - # but only the block or the dependencies may be set, not both. - - def gem(name, version, dependencies = nil, &block) - @operations << [:gem, name, version, dependencies, block] - end - - ## - # Creates a gem with +name+, +version+ and +deps+. The created gem is - # downloaded in to the cache directory but is not installed - # - # The specification will be yielded before gem creation for customization, - # but only the block or the dependencies may be set, not both. - - def download(name, version, dependencies = nil, &block) - @operations << [:download, name, version, dependencies, block] - end - - ## - # Creates a legacy platform spec with the name 'pl' and version 1 - - def legacy_platform - spec 'pl', 1 do |s| - s.platform = Gem::Platform.new 'i386-linux' - s.instance_variable_set :@original_platform, 'i386-linux' - end - end - - def setup_fetcher # :nodoc: - require 'zlib' - require 'socket' - require 'rubygems/remote_fetcher' - - unless @test.fetcher - @test.fetcher = Gem::FakeFetcher.new - Gem::RemoteFetcher.fetcher = @test.fetcher - end - - Gem::Specification.reset - - begin - gem_repo, @test.gem_repo = @test.gem_repo, @repository - @test.uri = URI @repository - - @test.util_setup_spec_fetcher(*@downloaded) - ensure - @test.gem_repo = gem_repo - @test.uri = URI gem_repo - end - - @gems.each do |spec, gem| - next unless gem - - @test.fetcher.data["#{@repository}gems/#{spec.file_name}"] = - Gem.read_binary(gem) - - FileUtils.cp gem, spec.cache_file - end - end - - ## - # Creates a spec with +name+, +version+ and +deps+. The created gem can be - # downloaded and installed. - # - # The specification will be yielded before creation for customization, - # but only the block or the dependencies may be set, not both. - - def spec(name, version, dependencies = nil, &block) - @operations << [:spec, name, version, dependencies, block] - end - - def write_spec(spec) # :nodoc: - File.open spec.spec_file, 'w' do |io| - io.write spec.to_ruby_for_cache - end - end -end - -## -# A StringIO duck-typed class that uses Tempfile instead of String as the -# backing store. -# -# This is available when rubygems/test_utilities is required. -#-- -# This class was added to flush out problems in Rubinius' IO implementation. - -class TempIO < Tempfile - ## - # Creates a new TempIO that will be initialized to contain +string+. - - def initialize(string = '') - super "TempIO" - binmode - write string - rewind - end - - ## - # The content of the TempIO as a String. - - def string - flush - Gem.read_binary path - end -end diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index acf25a0bcd..da0795b771 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -4,12 +4,11 @@ # 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]/, ".".freeze) + text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".") end def truncate_text(text, description, max_length = 100_000) @@ -51,7 +50,7 @@ module Gem::Text # Returns a value representing the "cost" of transforming str1 into str2 # Vendored version of DidYouMean::Levenshtein.distance from the ruby/did_you_mean gem @ 1.4.0 - # https://git.io/JJgZI + # https://github.com/ruby/did_you_mean/blob/2ddf39b874808685965dbc47d344cf6c7651807c/lib/did_you_mean/levenshtein.rb#L7-L37 def levenshtein_distance(str1, str2) n = str1.length m = str2.length @@ -67,7 +66,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/uninstaller.rb b/lib/rubygems/uninstaller.rb index 51ac3494f3..c96df2a085 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -1,16 +1,17 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'fileutils' -require 'rubygems' -require 'rubygems/installer_uninstaller_utils' -require 'rubygems/dependency_list' -require 'rubygems/rdoc' -require 'rubygems/user_interaction' +require "fileutils" +require_relative "../rubygems" +require_relative "installer_uninstaller_utils" +require_relative "dependency_list" +require_relative "rdoc" +require_relative "user_interaction" ## # An Uninstaller. @@ -45,7 +46,7 @@ class Gem::Uninstaller # Constructs an uninstaller that will uninstall +gem+ def initialize(gem, options = {}) - # TODO document the valid options + # TODO: document the valid options @gem = gem @version = options[:version] || Gem::Requirement.default @gem_home = File.realpath(options[:install_dir] || Gem.dir) @@ -70,6 +71,9 @@ 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] + + # Optimization: populated during #uninstall + @default_specs_matching_uninstall_params = [] end ## @@ -95,17 +99,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.each do |default_spec| - say "Gem #{default_spec.full_name} cannot be uninstalled because it is a 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 list, other_repo_specs = list.partition do |spec| - @gem_home == spec.base_dir or - (@user_install and spec.base_dir == Gem.user_dir) + @gem_home == spec.base_dir || + (@user_install && spec.base_dir == Gem.user_dir) end list.sort! @@ -113,7 +113,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| @@ -125,7 +125,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) gem_names << "All versions" say @@ -133,7 +133,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}]" @@ -199,13 +199,13 @@ 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" + - "in addition to the gem?", - true) - else - @force_executables - end + ask_yes_no("Remove executables:\n" \ + "\t#{executables.join ", "}\n\n" \ + "in addition to the gem?", + true) + else + @force_executables + end if remove bin_dir = @bin_dir || Gem.bindir(spec.base_dir) @@ -238,10 +238,10 @@ class Gem::Uninstaller # spec:: the spec of the gem to be uninstalled def remove(spec) - unless path_ok?(@gem_home, spec) or - (@user_install and path_ok?(Gem.user_dir, spec)) + unless path_ok?(@gem_home, spec) || + (@user_install && path_ok?(Gem.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 @@ -261,7 +261,10 @@ class Gem::Uninstaller safe_delete { FileUtils.rm_r gem } - Gem::RDoc.new(spec).remove + begin + Gem::RDoc.new(spec).remove + rescue NameError + end gemspec = spec.spec_file @@ -270,7 +273,7 @@ class Gem::Uninstaller end safe_delete { FileUtils.rm_r gemspec } - say "Successfully uninstalled #{spec.full_name}" + announce_deletion_of(spec) Gem::Specification.reset end @@ -298,8 +301,8 @@ class Gem::Uninstaller # Is +spec+ in +gem_dir+? def path_ok?(gem_dir, spec) - full_path = File.join gem_dir, 'gems', spec.full_name - original_path = File.join gem_dir, 'gems', spec.original_name + full_path = File.join gem_dir, "gems", spec.full_name + original_path = File.join gem_dir, "gems", spec.original_name full_path == spec.full_gem_path || original_path == spec.full_gem_path end @@ -328,24 +331,24 @@ class Gem::Uninstaller # Asks if it is OK to remove +spec+. Returns true if it is OK. def ask_if_ok(spec) # :nodoc: - msg = [''] - msg << 'You have requested to uninstall the gem:' + msg = [""] + msg << "You have requested to uninstall the gem:" msg << "\t#{spec.full_name}" - msg << '' + msg << "" siblings = Gem::Specification.select do |s| 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 end - msg << 'If you remove this gem, these dependencies will not be met.' - msg << 'Continue with Uninstall?' - return ask_yes_no(msg.join("\n"), false) + msg << "If you remove this gem, these dependencies will not be met." + msg << "Continue with Uninstall?" + ask_yes_no(msg.join("\n"), false) end ## @@ -356,7 +359,7 @@ class Gem::Uninstaller # of what it did for us to find rather than trying to recreate # it again. if @format_executable - require 'rubygems/installer' + require_relative "installer" Gem::Installer.exec_format % File.basename(filename) else filename @@ -373,4 +376,34 @@ class Gem::Uninstaller raise e end + + private + + def announce_deletion_of(spec) + name = spec.full_name + say "Successfully uninstalled #{name}" + if default_spec_matches?(spec) + say( + "There was both a regular copy and a default copy of #{name}. The " \ + "regular copy was successfully uninstalled, but the default copy " \ + "was left around because default gems can't be removed." + ) + end + end + + # @return true if the specs of any default gems are `==` to the given `spec`. + def default_spec_matches?(spec) + !default_specs_that_match(spec).empty? + end + + # @return [Array] specs of default gems that are `==` to the given `spec`. + def default_specs_that_match(spec) + @default_specs_matching_uninstall_params.select {|default_spec| spec == default_spec } + end + + def warn_cannot_uninstall_default_gems(specs) + specs.each do |spec| + say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem" + end + end end diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb new file mode 100644 index 0000000000..6f3ec5f493 --- /dev/null +++ b/lib/rubygems/update_suggestion.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +## +# Mixin methods for Gem::Command to promote available RubyGems update + +module Gem::UpdateSuggestion + ONE_WEEK = 7 * 24 * 60 * 60 + + ## + # Message to promote available RubyGems update with related gem update command. + + def update_suggestion + <<-MESSAGE + +A new release of RubyGems is available: #{Gem.rubygems_version} → #{Gem.latest_rubygems_version}! +Run `gem update --system #{Gem.latest_rubygems_version}` to update your installation. + + MESSAGE + end + + ## + # Determines if current environment is eligible for update suggestion. + + def eligible_for_update? + # explicit opt-out + return false if Gem.configuration[:prevent_update_suggestion] + return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] + + # focus only on human usage of final RubyGems releases + return false unless Gem.ui.tty? + return false if Gem.rubygems_version.prerelease? + return false if Gem.disable_system_update_message + 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 + # on each command call + return unless Gem.configuration.state_file_writable? + + # load time of last check, ensure the difference is enough to repeat the suggestion + check_time = Time.now.to_i + last_update_check = Gem.configuration.last_update_check + return false if (check_time - last_update_check) < ONE_WEEK + + # 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 |eligible| + # store the time of last successful check into state file + Gem.configuration.last_update_check = check_time + + return eligible + end + rescue StandardError # don't block install command on any problem + false + end +end diff --git a/lib/rubygems/uri.rb b/lib/rubygems/uri.rb new file mode 100644 index 0000000000..a44aaceba5 --- /dev/null +++ b/lib/rubygems/uri.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +## +# The Uri handles rubygems source URIs. +# + +class Gem::Uri + ## + # Parses and redacts uri + + def self.redact(uri) + new(uri).redacted + end + + ## + # Parses uri, raising if it's invalid + + def self.parse!(uri) + require_relative "vendor/uri/lib/uri" + + raise Gem::URI::InvalidURIError unless uri + + return uri unless uri.is_a?(String) + + # Always escape URI's to deal with potential spaces and such + # It should also be considered that source_uri may already be + # a valid URI with escaped characters. e.g. "{DESede}" is encoded + # as "%7BDESede%7D". If this is escaped again the percentage + # symbols will be escaped. + begin + Gem::URI.parse(uri) + rescue Gem::URI::InvalidURIError + Gem::URI.parse(Gem::URI::DEFAULT_PARSER.escape(uri)) + end + end + + ## + # Parses uri, returning the original uri if it's invalid + + def self.parse(uri) + parse!(uri) + rescue Gem::URI::InvalidURIError + uri + end + + def initialize(source_uri) + @parsed_uri = parse(source_uri) + end + + def redacted + return self unless valid_uri? + + if token? || oauth_basic? + with_redacted_user + elsif password? + with_redacted_password + else + self + end + end + + def to_s + @parsed_uri.to_s + end + + def redact_credentials_from(text) + return text unless valid_uri? && password? && text.include?(to_s) + + text.sub(password, "REDACTED") + end + + def method_missing(method_name, *args, &blk) + if @parsed_uri.respond_to?(method_name) + @parsed_uri.send(method_name, *args, &blk) + else + super + end + end + + def respond_to_missing?(method_name, include_private = false) + @parsed_uri.respond_to?(method_name, include_private) || super + end + + protected + + # Add a protected reader for the cloned instance to access the original object's parsed uri + attr_reader :parsed_uri + + private + + def parse!(uri) + self.class.parse!(uri) + end + + def parse(uri) + self.class.parse(uri) + end + + def with_redacted_user + clone.tap {|uri| uri.user = "REDACTED" } + end + + def with_redacted_password + clone.tap {|uri| uri.password = "REDACTED" } + end + + def valid_uri? + !@parsed_uri.is_a?(String) + end + + def password? + !!password + end + + def oauth_basic? + password == "x-oauth-basic" + end + + def token? + !user.nil? && password.nil? + end + + def initialize_copy(original) + @parsed_uri = original.parsed_uri.clone + end +end diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index 3bda896875..517ce33637 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -17,7 +17,7 @@ class Gem::UriFormatter # Creates a new URI formatter for +uri+. def initialize(uri) - require 'cgi' + require "cgi" @uri = uri end @@ -34,7 +34,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/uri_parser.rb b/lib/rubygems/uri_parser.rb deleted file mode 100644 index f350edec8c..0000000000 --- a/lib/rubygems/uri_parser.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -## -# The UriParser handles parsing URIs. -# - -class Gem::UriParser - ## - # Parses the #uri, raising if it's invalid - - def parse!(uri) - raise URI::InvalidURIError unless uri - - # Always escape URI's to deal with potential spaces and such - # It should also be considered that source_uri may already be - # a valid URI with escaped characters. e.g. "{DESede}" is encoded - # as "%7BDESede%7D". If this is escaped again the percentage - # symbols will be escaped. - begin - URI.parse(uri) - rescue URI::InvalidURIError - URI.parse(URI::DEFAULT_PARSER.escape(uri)) - end - end - - ## - # Parses the #uri, returning the original uri if it's invalid - - def parse(uri) - parse!(uri) - rescue URI::InvalidURIError - uri - end -end diff --git a/lib/rubygems/uri_parsing.rb b/lib/rubygems/uri_parsing.rb deleted file mode 100644 index 941d7e023a..0000000000 --- a/lib/rubygems/uri_parsing.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -require "rubygems/uri_parser" - -module Gem::UriParsing - - def parse_uri(source_uri) - return source_uri unless source_uri.is_a?(String) - - uri_parser.parse(source_uri) - end - - private :parse_uri - - def uri_parser - require "uri" - - Gem::UriParser.new - end - - private :uri_parser - -end diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb index 27a9957117..0172c4ee89 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -1,19 +1,19 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/deprecate' -require 'rubygems/text' +require_relative "deprecate" +require_relative "text" ## # Module that defines the default UserInteraction. Any class including this # module will have access to the +ui+ method that returns the default UI. module Gem::DefaultUserInteraction - include Gem::Text ## @@ -68,7 +68,6 @@ module Gem::DefaultUserInteraction def use_ui(new_ui, &block) Gem::DefaultUserInteraction.use_ui(new_ui, &block) end - end ## @@ -91,7 +90,6 @@ end # end module Gem::UserInteraction - include Gem::DefaultUserInteraction ## @@ -148,7 +146,7 @@ module Gem::UserInteraction ## # Displays the given +statement+ on the standard output (or equivalent). - def say(statement = '') + def say(statement = "") ui.say statement end @@ -195,7 +193,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 +237,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 ## @@ -259,12 +258,12 @@ class Gem::StreamUI default_answer = case default when nil - 'yn' + "yn" when true - 'Yn' + "Yn" else - 'yN' - end + "yN" + end result = nil @@ -273,24 +272,23 @@ class Gem::StreamUI when /^y/i then true when /^n/i then false when /^$/ then default - else nil - end + 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 not tty? + return nil unless tty? @outs.print(question + " ") @outs.flush result = @ins.gets - result.chomp! if result + result&.chomp! result end @@ -298,21 +296,21 @@ class Gem::StreamUI # Ask for a password. Does not echo response to terminal. def ask_for_password(question) - return nil if not tty? + return nil unless tty? @outs.print(question, " ") @outs.flush password = _gets_noecho @outs.puts - password.chomp! if password + password&.chomp! password end def require_io_console @require_io_console ||= begin begin - require 'io/console' + require "io/console" rescue LoadError end true @@ -428,8 +426,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 +468,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 @@ -543,7 +539,7 @@ class Gem::StreamUI # A progress reporter that behaves nicely with threaded downloading. class ThreadedDownloadReporter - MUTEX = Mutex.new + MUTEX = Thread::Mutex.new ## # The current file name being displayed @@ -595,8 +591,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 +600,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 @@ -616,18 +612,11 @@ class Gem::SilentUI < Gem::StreamUI # The SilentUI has no arguments as it does not use any stream. def initialize - reader, writer = nil, nil - - reader = File.open(IO::NULL, 'r') - writer = File.open(IO::NULL, 'w') - - super reader, writer, writer, false + io = NullIO.new + super io, io, io, false end def close - super - @ins.close - @outs.close end def download_reporter(*args) # :nodoc: @@ -637,4 +626,25 @@ class Gem::SilentUI < Gem::StreamUI def progress_reporter(*args) # :nodoc: SilentProgressReporter.new(@outs, *args) end + + ## + # An absolutely silent IO. + + class NullIO + def puts(*args) + end + + def print(*args) + end + + def flush + end + + def gets(*args) + end + + def tty? + false + end + end end diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 2a55305172..51f9c2029f 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -1,18 +1,18 @@ # frozen_string_literal: true -require 'rubygems/deprecate' + +require_relative "deprecate" ## # This module contains various utility methods as module methods. module Gem::Util - ## # Zlib::GzipReader wrapper that unzips +data+. def self.gunzip(data) - require 'zlib' - require 'stringio' - data = StringIO.new(data, 'r') + require "zlib" + require "stringio" + data = StringIO.new(data, "r") gzip_reader = begin Zlib::GzipReader.new(data) @@ -29,9 +29,9 @@ module Gem::Util # Zlib::GzipWriter wrapper that zips +data+. def self.gzip(data) - require 'zlib' - require 'stringio' - zipped = StringIO.new(String.new, 'w') + require "zlib" + require "stringio" + zipped = StringIO.new(String.new, "w") zipped.set_encoding Encoding::BINARY Zlib::GzipWriter.wrap zipped do |io| @@ -45,7 +45,7 @@ module Gem::Util # A Zlib::Inflate#inflate wrapper def self.inflate(data) - require 'zlib' + require "zlib" Zlib::Inflate.inflate data end @@ -60,7 +60,7 @@ module Gem::Util # Invokes system, but silences all output. def self.silent_system(*command) - opt = {:out => IO::NULL, :err => [:child, :out]} + opt = { out: IO::NULL, err: [:child, :out] } if Hash === command.last opt.update(command.last) cmds = command[0...-1] @@ -84,9 +84,13 @@ 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) + new_here = File.expand_path("..", here) return if new_here == here # toplevel here = new_here end @@ -97,23 +101,18 @@ module Gem::Util # returning absolute paths to the matching files. def self.glob_files_in_dir(glob, base_path) - if RUBY_VERSION >= "2.5" - Dir.glob(glob, base: base_path).map! {|f| File.expand_path(f, base_path) } - else - Dir.glob(File.expand_path(glob, base_path)) - end + Dir.glob(glob, base: base_path).map! {|f| File.expand_path(f, base_path) } 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/licenses.rb b/lib/rubygems/util/licenses.rb index 29bf310ea0..f3c7201639 100644 --- a/lib/rubygems/util/licenses.rb +++ b/lib/rubygems/util/licenses.rb @@ -1,10 +1,15 @@ # frozen_string_literal: true -require 'rubygems/text' + +# This is generated by generate_spdx_license_list.rb, any edits to this +# file will be discarded. + +require_relative "../text" class Gem::Licenses extend Gem::Text - NONSTANDARD = 'Nonstandard'.freeze + NONSTANDARD = "Nonstandard" + LICENSE_REF = "LicenseRef-.+" # Software Package Data Exchange (SPDX) standard open-source software # license identifiers @@ -17,90 +22,150 @@ class Gem::Licenses AFL-2.0 AFL-2.1 AFL-3.0 - AGPL-1.0 - AGPL-3.0 + AGPL-1.0-only + AGPL-1.0-or-later AGPL-3.0-only AGPL-3.0-or-later AMDPLPA AML + AML-glslang AMPAS ANTLR-PD + ANTLR-PD-fallback APAFML APL-1.0 APSL-1.0 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 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 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-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 BSD-3-Clause-No-Nuclear-License 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-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-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 Borceux + Brian-Gladman-2-Clause + Brian-Gladman-3-Clause + C-UDA-1.0 + CAL-1.0 + CAL-1.0-Combined-Work-Exception 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 + CC-BY-SA-2.0-UK + CC-BY-SA-2.1-JP 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 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 @@ -108,106 +173,160 @@ class Gem::Licenses CECILL-2.1 CECILL-B CECILL-C + CERN-OHL-1.1 + CERN-OHL-1.2 + 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 ClArtistic + Clips + Community-Spec-1.0 Condor-1.1 + Cornell-Lossless-JPEG + Cronyx Crossword 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 Dotseqn ECL-1.0 ECL-2.0 EFL-1.0 EFL-2.0 + EPICS EPL-1.0 EPL-2.0 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 + FSFULLRWD FTL Fair + Ferguson-Twofish Frameworx-1.0 + FreeBSD-DOC FreeImage - GFDL-1.1 + Furuseth + GCR-docs + GD + 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 + GFDL-1.3-no-invariants-or-later GFDL-1.3-only GFDL-1.3-or-later GL2PS - GPL-1.0 - GPL-1.0+ + GLWTPL 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 Giftware Glide Glulxe + Graphics-Gems + HP-1986 + HP-1989 HPND + HPND-DEC + HPND-Fenneberg-Livingston + HPND-INRIA-IMAG + HPND-Kevlin-Henney + HPND-MIT-disclaimer + HPND-Markus-Kuhn + HPND-Pbmplus + HPND-UC + HPND-doc + HPND-doc-sell + HPND-export-US + HPND-export-US-modify + HPND-sell-MIT-disclaimer-xserver + HPND-sell-regexpr + HPND-sell-variant + HPND-sell-variant-MIT-disclaimer + HTMLTIDY HaskellReport + Hippocratic-2.1 IBM-pibs ICU + IEC-Code-Components-EULA IJG + IJG-short IPA IPL-1.0 ISC + ISC-Veillard ImageMagick Imlib2 Info-ZIP + Inner-Net-2.0 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 @@ -215,35 +334,67 @@ 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 MIT + MIT-0 MIT-CMU + MIT-Festival + MIT-Modern-Variant + MIT-Wu MIT-advertising MIT-enna MIT-feh + MIT-open-group + MIT-testregex MITNFA + MMIXware + 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 + MulanPSL-2.0 Multics Mup + NAIST-2003 NASA-1.3 NBPL-1.0 + NCGL-UK-2.0 NCSA NGPL + NICTA-1.0 + NIST-PD + NIST-PD-fallback + NIST-Software NLOD-1.0 + NLOD-2.0 NLPL NOSL NPL-1.0 @@ -251,18 +402,31 @@ class Gem::Licenses NPOSL-3.0 NRL NTP + NTP-0 Naumen Net-SNMP NetCDF Newsletr Nokia Noweb - Nunit + O-UDA-1.0 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 OFL-1.1 + OFL-1.1-RFN + OFL-1.1-no-RFN + OGC-1.0 + OGDL-Taiwan-1.0 + OGL-Canada-2.0 + OGL-UK-1.0 + OGL-UK-2.0 + OGL-UK-3.0 OGTSL OLDAP-1.1 OLDAP-1.2 @@ -280,22 +444,37 @@ 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 OSET-PL-2.1 OSL-1.0 OSL-1.1 OSL-2.0 OSL-2.1 OSL-3.0 + OpenPBS-2.3 OpenSSL + OpenSSL-standalone + OpenVision + PADL PDDL-1.0 PHP-3.0 PHP-3.01 + PSF-2.0 + 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 @@ -306,35 +485,65 @@ class Gem::Licenses Rdisc Ruby SAX-PD + SAX-PD-2.0 SCEA SGI-B-1.0 SGI-B-1.1 SGI-B-2.0 + SGI-OpenGL + SGP4 + SHL-0.5 + SHL-0.51 SISSL SISSL-1.2 + SL SMLNJ SMPPL SNIA SPL-1.0 + SSH-OpenSSH + SSH-short + SSLeay-standalone + SSPL-1.0 SWL Saxpath + SchemeReport Sendmail + Sendmail-8.23 SimPL-2.0 Sleepycat + Soundex Spencer-86 Spencer-94 Spencer-99 - StandardML-NJ SugarCRM-1.1.3 + Sun-PPP + 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 + TermReadKey + UCAR + UCL-1.0 + UMich-Merit UPL-1.0 + URT-RLE + Unicode-3.0 Unicode-DFS-2015 Unicode-DFS-2016 Unicode-TOU + UnixCrypt Unlicense VOSTROM VSL-1.0 @@ -344,11 +553,15 @@ class Gem::Licenses W3C-20150513 WTFPL Watcom-1.0 + Widget-Workshop Wsuipa X11 + X11-distribute-modifications-variant XFree86-1.1 XSkat + Xdebug-1.03 Xerox + Xfig Xnet YPL-1.0 YPL-1.1 @@ -356,35 +569,103 @@ class Gem::Licenses ZPL-2.0 ZPL-2.1 Zed + Zeeff Zend-2.0 Zimbra-1.3 Zimbra-1.4 Zlib - bzip2-1.0.5 + bcrypt-Solar-Designer + blessing bzip2-1.0.6 + check-cvs + checkmk + copyleft-next-0.3.0 + copyleft-next-0.3.1 curl diffmark + dtoa dvipdfm - eCos-2.0 eGenix + etalab-2.0 + fwlw gSOAP-1.3b gnuplot + gtkbook + hdparm iMatix + libpng-2.0 + libselinux-1.0 libtiff + libutil-David-Nugent + lsof + magaz + mailprio + metamail + mpi-permissive mpich2 + mplus + pnmstitch psfrag psutils - wxWindows + python-ldap + radvd + snprintf + softSurfer + ssh-keyscan + swrule + ulem + w3m xinetd + xkeyboard-config-Zinoviev + xlock xpp 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+ + Nunit + StandardML-NJ + bzip2-1.0.5 + eCos-2.0 + wxWindows + ].freeze + # exception identifiers EXCEPTION_IDENTIFIERS = %w[ 389-exception + Asterisk-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 CLISP-exception-2.0 @@ -394,36 +675,99 @@ class Gem::Licenses 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-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 + 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 + 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 + SANE-exception + SHL-2.0 + SHL-2.1 + SWI-exception + Swift-exception + Texinfo-exception + UBDL-exception + Universal-FOSS-exception-1.0 WxWindows-exception-3.1 + cryptsetup-OpenSSL-exception eCos-exception-2.0 + fmt-exception freertos-exception-2.0 gnu-javamail-exception i2p-gpl-java-exception + libpri-OpenH323-exception mif-exception openvpn-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)} \+? - (\s WITH \s #{Regexp.union(EXCEPTION_IDENTIFIERS)})? + (?:\s WITH \s #{Regexp.union(EXCEPTION_IDENTIFIERS)})? | #{NONSTANDARD} + | #{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 index 33c40af4bb..2899e8a2b9 100644 --- a/lib/rubygems/util/list.rb +++ b/lib/rubygems/util/list.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true + module Gem - class List + # The Gem::List class is currently unused and will be removed in the next major rubygems version + class List # :nodoc: include Enumerable attr_accessor :value, :tail @@ -34,4 +36,5 @@ module Gem List.new value, list end end + deprecate_constant :List end diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index 30cdd93b5c..57e0747eb4 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems/package' -require 'rubygems/installer' +require_relative "package" +require_relative "installer" ## # Validator performs various gem file and gem database validation @@ -15,7 +16,7 @@ class Gem::Validator include Gem::UserInteraction def initialize # :nodoc: - require 'find' + require "find" end private @@ -24,9 +25,9 @@ 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 =~ /CVS/ || fn.empty? || File.directory?(file_name) + fn.empty? || fn.include?("CVS") || File.directory?(file_name) end installed_files @@ -62,7 +63,9 @@ class Gem::Validator 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/vendor/molinillo/.document b/lib/rubygems/vendor/molinillo/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/molinillo/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo.rb b/lib/rubygems/vendor/molinillo/lib/molinillo.rb index f67badbde7..dd5600c9e3 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo.rb @@ -6,6 +6,6 @@ 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 +# Gem::Molinillo is a generic dependency resolution algorithm. +module Gem::Molinillo end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb new file mode 100644 index 0000000000..34842d46d5 --- /dev/null +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/resolution_state.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module Gem::Molinillo + # @!visibility private + module Delegates + # Delegates all {Gem::Molinillo::ResolutionState} methods to a `#state` property. + module ResolutionState + # (see Gem::Molinillo::ResolutionState#name) + def name + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.name + end + + # (see Gem::Molinillo::ResolutionState#requirements) + def requirements + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.requirements + end + + # (see Gem::Molinillo::ResolutionState#activated) + def activated + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.activated + end + + # (see Gem::Molinillo::ResolutionState#requirement) + def requirement + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.requirement + end + + # (see Gem::Molinillo::ResolutionState#possibilities) + def possibilities + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.possibilities + end + + # (see Gem::Molinillo::ResolutionState#depth) + def depth + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.depth + end + + # (see Gem::Molinillo::ResolutionState#conflicts) + def conflicts + current_state = state || Gem::Molinillo::ResolutionState.empty + current_state.conflicts + end + + # (see Gem::Molinillo::ResolutionState#unused_unwind_options) + def unused_unwind_options + current_state = state || Gem::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/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb index b765226fb0..8417721537 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/delegates/specification_provider.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/delegates/specification_provider.rb @@ -1,67 +1,67 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::Molinillo module Delegates - # Delegates all {Gem::Resolver::Molinillo::SpecificationProvider} methods to a + # Delegates all {Gem::Molinillo::SpecificationProvider} methods to a # `#specification_provider` property. module SpecificationProvider - # (see Gem::Resolver::Molinillo::SpecificationProvider#search_for) + # (see Gem::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) + # (see Gem::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?) + # (see Gem::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?) + # (see Gem::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) + # (see Gem::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) + # (see Gem::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) + # (see Gem::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) + # (see Gem::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?) + # (see Gem::Molinillo::SpecificationProvider#allow_missing?) def allow_missing?(dependency) with_no_such_dependency_error_handling do specification_provider.allow_missing?(dependency) diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb index 16430a79f5..2dbbc589dc 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -require 'tsort' +require_relative '../../../../vendored_tsort' require_relative 'dependency_graph/log' require_relative 'dependency_graph/vertex' -module Gem::Resolver::Molinillo +module Gem::Molinillo # A directed acyclic graph that is tuned to hold named dependencies class DependencyGraph include Enumerable @@ -17,7 +17,7 @@ module Gem::Resolver::Molinillo vertices.values.each { |v| yield v } end - include TSort + include Gem::TSort # @!visibility private alias tsort_each_node each @@ -32,7 +32,7 @@ module Gem::Resolver::Molinillo # all belong to the same graph. # @return [Array<Vertex>] The sorted vertices. def self.tsort(vertices) - TSort.tsort( + Gem::TSort.tsort( lambda { |b| vertices.each(&b) }, lambda { |v, &b| (v.successors & vertices).each(&b) } ) diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/action.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb index cc140031b3..8707ec451d 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/action.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/action.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # An action that modifies a {DependencyGraph} that is reversible. # @abstract diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb index 5570483253..aa9815c5ae 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_edge_no_circular.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # (see DependencyGraph#add_edge_no_circular) diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb index f1411d5efa..9c7066a669 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/add_vertex.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/add_vertex.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # (see DependencyGraph#add_vertex) diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/delete_edge.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb index 3b48d77a50..1e62c0a0b6 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/delete_edge.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/delete_edge.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # (see DependencyGraph#delete_edge) diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb index 92f60d5be8..6132f969b9 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/detach_vertex_named.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # @see DependencyGraph#detach_vertex_named diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/log.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb index 7aeb8847ec..6954c4b1f8 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/log.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/log.rb @@ -7,7 +7,7 @@ require_relative 'detach_vertex_named' require_relative 'set_payload' require_relative 'tag' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # A log for dependency graph actions class Log diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/set_payload.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb index 726292a2c3..9bcaaae0f9 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/set_payload.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/set_payload.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # @see DependencyGraph#set_payload diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/tag.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb index bfe6fd31f8..62f243a2af 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/tag.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/tag.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'action' -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # @!visibility private # @see DependencyGraph#tag diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/vertex.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb index 77114951b2..074de369be 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/dependency_graph/vertex.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/dependency_graph/vertex.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::Molinillo class DependencyGraph # A vertex in a {DependencyGraph} that encapsulates a {#name} and a # {#payload} diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/errors.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb index ada03a901c..07ea5fdf37 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/errors.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/errors.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::Molinillo # An error that occurred during the resolution process class ResolverError < StandardError; end @@ -107,36 +107,42 @@ module Gem::Resolver::Molinillo end end - conflicts.sort.reduce(''.dup) do |o, (name, conflict)| - o << "\n" << 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) + 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 << %( depends on) + t << %(\n) + depth += 1 end - t << %(\n) - depth += 1 - end - t - end.join("\n") + t + end.join("\n") - additional_message_for_conflict.call(o, name, conflict) + additional_message_for_conflict.call(o, name, conflict) - o + o + end + end + + conflicts.sort.reduce(''.dup) do |o, (name, conflict)| + o << full_message_for_conflict.call(name, conflict) end.strip end end diff --git a/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb new file mode 100644 index 0000000000..8ed3a920a2 --- /dev/null +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/gem_metadata.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module Gem::Molinillo + # The version of Gem::Molinillo. + VERSION = '0.8.0'.freeze +end diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb index 1067bf7439..85860902fc 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/specification_provider.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/specification_provider.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::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, + # This module contains the methods that users of Gem::Molinillo must to implement, # using knowledge of their own model classes. module SpecificationProvider # Search for the specifications that match the given dependency. diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/ui.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb index a810fd519c..464722902e 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/modules/ui.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/modules/ui.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::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. diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/resolution.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb index 8b40e59e42..84ec6cb095 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/resolution.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/resolution.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::Molinillo class Resolver # A specific resolution from a given {Resolver} class Resolution @@ -103,7 +103,7 @@ module Gem::Resolver::Molinillo # @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 + # impossible for the state we're unwinding to be a parent of # any of the other conflicting requirements (or we would have # circularity) def unwinding_to_primary_requirement? @@ -244,8 +244,8 @@ module Gem::Resolver::Molinillo require_relative 'delegates/resolution_state' require_relative 'delegates/specification_provider' - include Gem::Resolver::Molinillo::Delegates::ResolutionState - include Gem::Resolver::Molinillo::Delegates::SpecificationProvider + include Gem::Molinillo::Delegates::ResolutionState + include Gem::Molinillo::Delegates::SpecificationProvider # Processes the topmost available {RequirementState} on the stack # @return [void] diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/resolver.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb index d43121f8ca..86229c3fa1 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/resolver.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/resolver.rb @@ -2,7 +2,7 @@ require_relative 'dependency_graph' -module Gem::Resolver::Molinillo +module Gem::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} diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/state.rb b/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb index 6e7c715fce..c48ec6af9c 100644 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/state.rb +++ b/lib/rubygems/vendor/molinillo/lib/molinillo/state.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -module Gem::Resolver::Molinillo +module Gem::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 diff --git a/lib/rubygems/vendor/net-http/.document b/lib/rubygems/vendor/net-http/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/net-http/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented 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..7b15c3cf54 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http.rb @@ -0,0 +1,2496 @@ +# 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}[rdoc-ref:OpenURI]. + # - 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) + # + # - 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 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 {URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax]. + # + # A Ruby {Gem::URI::Generic}[rdoc-ref:Gem::URI::Generic] object + # represents an internet 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 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 {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 URI + # object that has an \HTTPS URL. \Gem::Net::HTTP automatically turns on TLS + # verification if the URI object has a 'https' :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 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 + # + # 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?] + # (aliased as {#active?}[rdoc-ref:Gem::Net::HTTP#active?]): + # 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. + # - {#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] + # (aliased as {#get2}[rdoc-ref:Gem::Net::HTTP#get2]): + # 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] + # (aliased as {#head2}[rdoc-ref:Gem::Net::HTTP#head2]): + # 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] + # (aliased as {#post2}[rdoc-ref:Gem::Net::HTTP#post2]): + # 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] + # (aliased as {#proxyaddr}[rdoc-ref:Gem::Net::HTTP#proxyaddr]): + # 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 {::is_version_1_2?}[rdoc-ref:Gem::Net::HTTP.is_version_1_2?] + # and {::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.4.0" + 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 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 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 + + # + # \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) + 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 + 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 + end + + http + 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: + @address = address + @port = (port || HTTP.default_port) + @ipaddr = nil + @local_host = nil + @local_port = nil + @curr_http_version = HTTPVersion + @keep_alive_timeout = 2 + @last_communicated = nil + @close_on_empty_response = false + @socket = nil + @started = 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 + + @proxy_from_env = false + @proxy_uri = nil + @proxy_address = nil + @proxy_port = nil + @proxy_user = nil + @proxy_pass = 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}[rdoc-ref:Encoding]. + # + # 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 + + # 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_IVNAMES = [ + :@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, + ] # :nodoc: + 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, + ] # :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=}[rdoc-ref:OpenSSL::SSL::SSLContext#ciphers-3D]. + attr_accessor :ciphers + + # Sets or returns the extra X509 certificates to be added to the certificate chain. + # See {OpenSSL::SSL::SSLContext#add_certificate}[rdoc-ref:OpenSSL::SSL::SSLContext#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=}[rdoc-ref:OpenSSL::SSL::SSLContext#ssl_version-3D]. + attr_accessor :ssl_version + + # Sets or returns the minimum SSL version. + # See {OpenSSL::SSL::SSLContext#min_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#min_version-3D]. + attr_accessor :min_version + + # Sets or returns the maximum SSL version. + # See {OpenSSL::SSL::SSLContext#max_version=}[rdoc-ref:OpenSSL::SSL::SSLContext#max_version-3D]. + 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=}[rdoc-ref:OpenSSL::SSL::SSLContext#attribute-i-verify_mode]. + 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 + + 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}..." + s = Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) { + begin + TCPSocket.open(conn_addr, conn_port, @local_host, @local_port) + rescue => e + 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? + plain_sock = BufferedIO.new(s, 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" + plain_sock.write(buf) + HTTPResponse.read_new(plain_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 + + def on_connect + end + private :on_connect + + # 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 + + 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 + + # 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) #: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 + } + end + + 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 + 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 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 + + def unescape(value) + require 'cgi/util' + 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 + + # + # 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> + # + 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 + + IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/ # :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?) { + yield res if block_given? + } + 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 + +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' + +require_relative 'http/backward' diff --git a/lib/rubygems/vendor/net-http/lib/net/http/backward.rb b/lib/rubygems/vendor/net-http/lib/net/http/backward.rb new file mode 100644 index 0000000000..10dbc16224 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/backward.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true +# for backward compatibility + +# :enddoc: + +class Gem::Net::HTTP + ProxyMod = ProxyDelta + deprecate_constant :ProxyMod +end + +module Gem::Net::NetPrivate + HTTPRequest = ::Gem::Net::HTTPRequest + deprecate_constant :HTTPRequest +end + +module Gem::Net + HTTPSession = HTTP + + HTTPInformationCode = HTTPInformation + HTTPSuccessCode = HTTPSuccess + HTTPRedirectionCode = HTTPRedirection + HTTPRetriableCode = HTTPRedirection + HTTPClientErrorCode = HTTPClientError + HTTPFatalErrorCode = HTTPClientError + HTTPServerErrorCode = HTTPServerError + HTTPResponseReceiver = HTTPResponse + + HTTPResponceReceiver = HTTPResponse # Typo since 2001 + + deprecate_constant :HTTPSession, + :HTTPInformationCode, + :HTTPSuccessCode, + :HTTPRedirectionCode, + :HTTPRetriableCode, + :HTTPClientErrorCode, + :HTTPFatalErrorCode, + :HTTPServerErrorCode, + :HTTPResponseReceiver, + :HTTPResponceReceiver +end 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..c629c0113b --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb @@ -0,0 +1,34 @@ +# 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 + def initialize(msg, res) #:nodoc: + super msg + @response = res + end + attr_reader :response + alias data response #:nodoc: obsolete + end + + 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..5cfe75a7cd --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb @@ -0,0 +1,414 @@ +# 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.hostname + raise ArgumentError, "no host component for Gem::URI" unless (hostname && hostname.length > 0) + @uri = uri_or_path.dup + host = @uri.hostname.dup + host << ":" << @uri.port.to_s if @uri.port != @uri.default_port + @path = uri_or_path.request_uri + raise ArgumentError, "no HTTP request path given" unless @path + else + @uri = nil + host = 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'] ||= host if host + @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 + + ## + # 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.sub!(/:.*/m, '') + 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 + + 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' + supply_default_content_type + 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 + supply_default_content_type + 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 + + def supply_default_content_type + return if content_type() + warn 'net/http: Content-Type did not set; using application/x-www-form-urlencoded', uplevel: 1 if $VERBOSE + set_content_type 'application/x-www-form-urlencoded' + 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..1488e60068 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/header.rb @@ -0,0 +1,981 @@ +# 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 + MAX_KEY_LENGTH = 1024 + 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 + + 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 + + # 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) + name.to_s.split(/-/).map {|s| s.capitalize }.join('-') + 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) + '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..1a57ddc7c2 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb @@ -0,0 +1,425 @@ +# 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 + 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 + 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 + 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. +# +class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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..cbbd191d87 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/response.rb @@ -0,0 +1,738 @@ +# 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 + + 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 + "#<#{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..0f26ae6c26 --- /dev/null +++ b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb @@ -0,0 +1,1174 @@ +# frozen_string_literal: true +#-- +# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + +module Gem::Net + + class HTTPUnknownResponse < HTTPResponse + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + HAS_BODY = true + end + +end + +class Gem::Net::HTTPResponse + CODE_CLASS_TO_OBJ = { + '1' => Gem::Net::HTTPInformation, + '2' => Gem::Net::HTTPSuccess, + '3' => Gem::Net::HTTPRedirection, + '4' => Gem::Net::HTTPClientError, + '5' => Gem::Net::HTTPServerError + } + 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, + } +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..d2784f0be0 --- /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 'rubygems/vendor/net-http/lib/net/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/.document b/lib/rubygems/vendor/net-protocol/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/net-protocol/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented 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/vendor/optparse/.document b/lib/rubygems/vendor/optparse/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/optparse/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/optparse/lib/optionparser.rb b/lib/rubygems/vendor/optparse/lib/optionparser.rb new file mode 100644 index 0000000000..4b9b40d82a --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optionparser.rb @@ -0,0 +1,2 @@ +# frozen_string_literal: false +require_relative 'optparse' diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb new file mode 100644 index 0000000000..5937431720 --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -0,0 +1,2330 @@ +# frozen_string_literal: true +# +# optparse.rb - command-line option analysis with the Gem::OptionParser class. +# +# Author:: Nobu Nakada +# Documentation:: Nobu Nakada and Gavin Sinclair. +# +# See Gem::OptionParser for documentation. +# + + +#-- +# == Developer Documentation (not for RDoc output) +# +# === Class tree +# +# - Gem::OptionParser:: front end +# - Gem::OptionParser::Switch:: each switches +# - Gem::OptionParser::List:: options list +# - Gem::OptionParser::ParseError:: errors on parsing +# - Gem::OptionParser::AmbiguousOption +# - Gem::OptionParser::NeedlessArgument +# - Gem::OptionParser::MissingArgument +# - Gem::OptionParser::InvalidOption +# - Gem::OptionParser::InvalidArgument +# - Gem::OptionParser::AmbiguousArgument +# +# === Object relationship diagram +# +# +--------------+ +# | Gem::OptionParser |<>-----+ +# +--------------+ | +--------+ +# | ,-| Switch | +# on_head -------->+---------------+ / +--------+ +# accept/reject -->| List |<|>- +# | |<|>- +----------+ +# on ------------->+---------------+ `-| argument | +# : : | class | +# +---------------+ |==========| +# on_tail -------->| | |pattern | +# +---------------+ |----------| +# Gem::OptionParser.accept ->| DefaultList | |converter | +# reject |(shared between| +----------+ +# | all instances)| +# +---------------+ +# +#++ +# +# == Gem::OptionParser +# +# === New to +Gem::OptionParser+? +# +# See the {Tutorial}[optparse/tutorial.rdoc]. +# +# === Introduction +# +# Gem::OptionParser is a class for command-line option analysis. It is much more +# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented +# solution. +# +# === Features +# +# 1. The argument specification and the code to handle it are written in the +# same place. +# 2. It can output an option summary; you don't need to maintain this string +# separately. +# 3. Optional and mandatory arguments are specified very gracefully. +# 4. Arguments can be automatically converted to a specified class. +# 5. Arguments can be restricted to a certain set. +# +# All of these features are demonstrated in the examples below. See +# #make_switch for full documentation. +# +# === Minimal example +# +# require 'rubygems/vendor/optparse/lib/optparse' +# +# options = {} +# Gem::OptionParser.new do |parser| +# parser.banner = "Usage: example.rb [options]" +# +# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# options[:verbose] = v +# end +# end.parse! +# +# p options +# p ARGV +# +# === Generating Help +# +# Gem::OptionParser can be used to automatically generate help for the commands you +# write: +# +# require 'rubygems/vendor/optparse/lib/optparse' +# +# Options = Struct.new(:name) +# +# class Parser +# def self.parse(options) +# args = Options.new("world") +# +# opt_parser = Gem::OptionParser.new do |parser| +# parser.banner = "Usage: example.rb [options]" +# +# parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n| +# args.name = n +# end +# +# parser.on("-h", "--help", "Prints this help") do +# puts parser +# exit +# end +# end +# +# opt_parser.parse!(options) +# return args +# end +# end +# options = Parser.parse %w[--help] +# +# #=> +# # Usage: example.rb [options] +# # -n, --name=NAME Name to say hello to +# # -h, --help Prints this help +# +# === Required Arguments +# +# For options that require an argument, option specification strings may include an +# option name in all caps. If an option is used without the required argument, +# an exception will be raised. +# +# require 'rubygems/vendor/optparse/lib/optparse' +# +# options = {} +# Gem::OptionParser.new do |parser| +# parser.on("-r", "--require LIBRARY", +# "Require the LIBRARY before executing your script") do |lib| +# puts "You required #{lib}!" +# end +# end.parse! +# +# Used: +# +# $ ruby optparse-test.rb -r +# optparse-test.rb:9:in `<main>': missing argument: -r (Gem::OptionParser::MissingArgument) +# $ ruby optparse-test.rb -r my-library +# You required my-library! +# +# === Type Coercion +# +# 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 +# coercion. They are: +# +# - 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) +# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3) +# - DecimalInteger -- Like +Integer+, but no octal format. +# - OctalInteger -- Like +Integer+, but no decimal format. +# - DecimalNumeric -- Decimal integer or float. +# - TrueClass -- Accepts '+, yes, true, -, no, false' and +# defaults as +true+ +# - FalseClass -- Same as +TrueClass+, but defaults to +false+ +# - Array -- Strings separated by ',' (e.g. 1,2,3) +# - Regexp -- Regular expressions. Also includes options. +# +# We can also add our own coercions, which we will cover below. +# +# ==== Using Built-in Conversions +# +# As an example, the built-in +Time+ conversion is used. The other built-in +# conversions behave in the same way. +# Gem::OptionParser will attempt to parse the argument +# as a +Time+. If it succeeds, that time will be passed to the +# handler block. Otherwise, an exception will be raised. +# +# 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 +# end +# end.parse! +# +# Used: +# +# $ ruby optparse-test.rb -t nonsense +# ... invalid argument: -t nonsense (Gem::OptionParser::InvalidArgument) +# $ ruby optparse-test.rb -t 10-11-12 +# 2010-11-12 00:00:00 -0500 +# $ ruby optparse-test.rb -t 9:30 +# 2014-08-13 09:30:00 -0400 +# +# ==== Creating Custom Conversions +# +# The +accept+ method on Gem::OptionParser may be used to create converters. +# 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/vendor/optparse/lib/optparse' +# +# User = Struct.new(:id, :name) +# +# def find_user id +# not_found = ->{ raise "No User Found for id #{id}" } +# [ User.new(1, "Sam"), +# User.new(2, "Gandalf") ].find(not_found) do |u| +# u.id == id +# end +# end +# +# op = Gem::OptionParser.new +# op.accept(User) do |user_id| +# find_user user_id.to_i +# end +# +# op.on("--user ID", User) do |user| +# puts user +# end +# +# op.parse! +# +# Used: +# +# $ ruby optparse-test.rb --user 1 +# #<struct User id=1, name="Sam"> +# $ 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) +# +# === Store options to a Hash +# +# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash. +# +# require 'rubygems/vendor/optparse/lib/optparse' +# +# options = {} +# Gem::OptionParser.new do |parser| +# parser.on('-a') +# parser.on('-b NUM', Integer) +# parser.on('-v', '--verbose') +# end.parse!(into: options) +# +# p options +# +# Used: +# +# $ ruby optparse-test.rb -a +# {:a=>true} +# $ ruby optparse-test.rb -a -v +# {:a=>true, :verbose=>true} +# $ ruby optparse-test.rb -a -b 100 +# {:a=>true, :b=>100} +# +# === Complete example +# +# The following example is a complete Ruby program. You can run it and see the +# effect of specifying various options. This is probably the best way to learn +# the features of +optparse+. +# +# require 'rubygems/vendor/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse/time' +# require 'ostruct' +# require 'pp' +# +# class OptparseExample +# Version = '1.0.0' +# +# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary] +# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" } +# +# class ScriptOptions +# attr_accessor :library, :inplace, :encoding, :transfer_type, +# :verbose, :extension, :delay, :time, :record_separator, +# :list +# +# def initialize +# self.library = [] +# self.inplace = false +# self.encoding = "utf8" +# self.transfer_type = :auto +# self.verbose = false +# end +# +# def define_options(parser) +# parser.banner = "Usage: example.rb [options]" +# parser.separator "" +# parser.separator "Specific options:" +# +# # add additional options +# perform_inplace_option(parser) +# delay_execution_option(parser) +# execute_at_time_option(parser) +# specify_record_separator_option(parser) +# list_example_option(parser) +# specify_encoding_option(parser) +# optional_option_argument_with_keyword_completion_option(parser) +# boolean_verbose_option(parser) +# +# parser.separator "" +# parser.separator "Common options:" +# # No argument, shows at tail. This will print an options summary. +# # Try it and see! +# parser.on_tail("-h", "--help", "Show this message") do +# puts parser +# exit +# end +# # Another typical switch to print the version. +# parser.on_tail("--version", "Show version") do +# puts Version +# exit +# end +# end +# +# def perform_inplace_option(parser) +# # Specifies an optional option argument +# parser.on("-i", "--inplace [EXTENSION]", +# "Edit ARGV files in place", +# "(make backup if EXTENSION supplied)") do |ext| +# self.inplace = true +# self.extension = ext || '' +# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot. +# end +# end +# +# def delay_execution_option(parser) +# # Cast 'delay' argument to a Float. +# parser.on("--delay N", Float, "Delay N seconds before executing") do |n| +# self.delay = n +# end +# end +# +# def execute_at_time_option(parser) +# # Cast 'time' argument to a Time object. +# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| +# self.time = time +# end +# end +# +# def specify_record_separator_option(parser) +# # Cast to octal integer. +# parser.on("-F", "--irs [OCTAL]", Gem::OptionParser::OctalInteger, +# "Specify record separator (default \\0)") do |rs| +# self.record_separator = rs +# end +# end +# +# def list_example_option(parser) +# # List of arguments. +# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list| +# self.list = list +# end +# end +# +# def specify_encoding_option(parser) +# # Keyword completion. We are specifying a specific set of arguments (CODES +# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide +# # the shortest unambiguous text. +# code_list = (CODE_ALIASES.keys + CODES).join(', ') +# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding", +# "(#{code_list})") do |encoding| +# self.encoding = encoding +# end +# end +# +# def optional_option_argument_with_keyword_completion_option(parser) +# # Optional '--type' option argument with keyword completion. +# parser.on("--type [TYPE]", [:text, :binary, :auto], +# "Select transfer type (text, binary, auto)") do |t| +# self.transfer_type = t +# end +# end +# +# def boolean_verbose_option(parser) +# # Boolean switch. +# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v| +# self.verbose = v +# end +# end +# end +# +# # +# # Return a structure describing the options. +# # +# def parse(args) +# # The options specified on the command line will be collected in +# # *options*. +# +# @options = ScriptOptions.new +# @args = Gem::OptionParser.new do |parser| +# @options.define_options(parser) +# parser.parse!(args) +# end +# @options +# end +# +# attr_reader :parser, :options +# end # class OptparseExample +# +# example = OptparseExample.new +# options = example.parse(ARGV) +# pp options # example.options +# pp ARGV +# +# === Shell Completion +# +# For modern shells (e.g. bash, zsh, etc.), you can use shell +# completion for command line options. +# +# === Further documentation +# +# The above examples, along with the accompanying +# {Tutorial}[optparse/tutorial.rdoc], +# should be enough to learn how to use this class. +# If you have any questions, file a ticket at http://bugs.ruby-lang.org. +# +class Gem::OptionParser + Gem::OptionParser::Version = "0.4.0" + + # :stopdoc: + NoArgument = [NO_ARGUMENT = :NONE, nil].freeze + RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze + OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze + # :startdoc: + + # + # Keyword completion module. This allows partial arguments to be specified + # and resolved against a list of acceptable values. + # + module Completion + def self.regexp(key, icase) + Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase) + end + + def self.candidate(key, icase = false, pat = nil, &block) + pat ||= Completion.regexp(key, icase) + candidates = [] + block.call do |k, *v| + (if Regexp === k + kn = "" + k === key + else + kn = defined?(k.id2name) ? k.id2name : k + pat === kn + end) or next + v << k if v.empty? + candidates << [k, v, kn] + end + candidates + end + + def candidate(key, icase = false, pat = nil) + Completion.candidate(key, icase, pat, &method(:each)) + end + + public + def complete(key, icase = false, pat = nil) + candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size} + if candidates.size == 1 + canon, sw, * = candidates[0] + elsif candidates.size > 1 + canon, sw, cn = candidates.shift + candidates.each do |k, v, kn| + next if sw == v + if String === cn and String === kn + if cn.rindex(kn, 0) + canon, sw, cn = k, v, kn + next + elsif kn.rindex(cn, 0) + next + end + end + throw :ambiguous, key + end + end + if canon + block_given? or return key, *sw + yield(key, *sw) + end + end + + def convert(opt = nil, val = nil, *) + val + end + end + + + # + # Map from option/keyword string to object with completion. + # + class OptionMap < Hash + include Completion + end + + + # + # Individual switch class. Not important to the user. + # + # Defined within Switch are several Switch-derived classes: NoArgument, + # RequiredArgument, etc. + # + class Switch + attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block + + # + # Guesses argument style from +arg+. Returns corresponding + # Gem::OptionParser::Switch class (OptionalArgument, etc.). + # + def self.guess(arg) + case arg + when "" + t = self + when /\A=?\[/ + t = Switch::OptionalArgument + when /\A\s+\[/ + t = Switch::PlacedArgument + else + t = Switch::RequiredArgument + end + self >= t or incompatible_argument_styles(arg, t) + t + end + + def self.incompatible_argument_styles(arg, t) + raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}", + ParseError.filter_backtrace(caller(2))) + end + + def self.pattern + NilClass + end + + def initialize(pattern = nil, conv = nil, + short = nil, long = nil, arg = nil, + desc = ([] if short or long), block = nil, &_block) + raise if Array === pattern + block ||= _block + @pattern, @conv, @short, @long, @arg, @desc, @block = + pattern, conv, short, long, arg, desc, block + end + + # + # Parses +arg+ and returns rest of +arg+ and matched portion to the + # argument pattern. Yields when the pattern doesn't match substring. + # + def parse_arg(arg) # :nodoc: + pattern or return nil, [arg] + unless m = pattern.match(arg) + yield(InvalidArgument, arg) + return arg, [] + end + if String === m + m = [s = m] + else + m = m.to_a + s = m[0] + return nil, m unless String === s + end + raise InvalidArgument, arg unless arg.rindex(s, 0) + return nil, m if s.length == arg.length + yield(InvalidArgument, arg) # didn't match whole arg + return arg[s.length..-1], m + end + private :parse_arg + + # + # Parses argument, converts and returns +arg+, +block+ and result of + # conversion. Yields at semi-error condition instead of raising an + # exception. + # + def conv_arg(arg, val = []) # :nodoc: + if conv + val = conv.call(*val) + else + val = proc {|v| v}.call(*val) + end + return arg, block, val + end + private :conv_arg + + # + # Produces the summary text. Each line of the summary is yielded to the + # block (without newline). + # + # +sdone+:: Already summarized short style options keyed hash. + # +ldone+:: Already summarized long style options keyed hash. + # +width+:: Width of left side (option part). In other words, the right + # side (description part) starts after +width+ columns. + # +max+:: Maximum width of left side -> the options are filled within + # +max+ columns. + # +indent+:: Prefix string indents all summarized lines. + # + def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "") + sopts, lopts = [], [], nil + @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short + @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long + return if sopts.empty? and lopts.empty? # completely hidden + + left = [sopts.join(', ')] + right = desc.dup + + while s = lopts.shift + l = left[-1].length + s.length + l += arg.length if left.size == 1 && arg + l < max or sopts.empty? or left << +'' + left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s + end + + if arg + left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg) + end + mlen = left.collect {|ss| ss.length}.max.to_i + while mlen > width and l = left.shift + mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen + if l.length < width and (r = right[0]) and !r.empty? + l = l.to_s.ljust(width) + ' ' + r + right.shift + end + yield(indent + l) + end + + while begin l = left.shift; r = right.shift; l or r end + l = l.to_s.ljust(width) + ' ' + r if r and !r.empty? + yield(indent + l) + end + + self + end + + def add_banner(to) # :nodoc: + unless @short or @long + s = desc.join + to << " [" + s + "]..." unless s.empty? + end + to + end + + def match_nonswitch?(str) # :nodoc: + @pattern =~ str unless @short or @long + end + + # + # Main name of the switch. + # + def switch_name + (long.first || short.first).sub(/\A-+(?:\[no-\])?/, '') + end + + def compsys(sdone, ldone) # :nodoc: + sopts, lopts = [], [] + @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short + @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long + return if sopts.empty? and lopts.empty? # completely hidden + + (sopts+lopts).each do |opt| + # "(-x -c -r)-l[left justify]" + if /^--\[no-\](.+)$/ =~ opt + o = $1 + yield("--#{o}", desc.join("")) + yield("--no-#{o}", desc.join("")) + else + yield("#{opt}", desc.join("")) + end + end + end + + def pretty_print_contents(q) # :nodoc: + if @block + q.text ":" + @block.source_location.join(":") + ":" + first = false + else + first = true + end + [@short, @long].each do |list| + list.each do |opt| + if first + q.text ":" + first = false + end + q.breakable + q.text opt + end + end + end + + def pretty_print(q) # :nodoc: + q.object_group(self) {pretty_print_contents(q)} + end + + # + # Switch that takes no arguments. + # + class NoArgument < self + + # + # Raises an exception if any arguments given. + # + def parse(arg, argv) + yield(NeedlessArgument, arg) if arg + conv_arg(arg) + end + + def self.incompatible_argument_styles(*) + end + + def self.pattern + Object + end + + def pretty_head # :nodoc: + "NoArgument" + end + end + + # + # Switch that takes an argument. + # + class RequiredArgument < self + + # + # Raises an exception if argument is not present. + # + def parse(arg, argv) + unless arg + raise MissingArgument if argv.empty? + arg = argv.shift + end + conv_arg(*parse_arg(arg, &method(:raise))) + end + + def pretty_head # :nodoc: + "Required" + end + end + + # + # Switch that can omit argument. + # + class OptionalArgument < self + + # + # Parses argument if given, or uses default value. + # + def parse(arg, argv, &error) + if arg + conv_arg(*parse_arg(arg, &error)) + else + conv_arg(arg) + end + end + + def pretty_head # :nodoc: + "Optional" + end + end + + # + # Switch that takes an argument, which does not begin with '-' or is '-'. + # + class PlacedArgument < self + + # + # Returns nil if argument is not present or begins with '-' and is not '-'. + # + def parse(arg, argv, &error) + if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0])) + return nil, block, nil + end + opt = (val = parse_arg(val, &error))[1] + val = conv_arg(*val) + if opt and !arg + argv.shift + else + val[0] = nil + end + val + end + + def pretty_head # :nodoc: + "Placed" + end + end + end + + # + # Simple option list providing mapping from short and/or long option + # string to Gem::OptionParser::Switch and mapping from acceptable argument to + # matching pattern and converter pair. Also provides summary feature. + # + class List + # Map from acceptable argument types to pattern and converter pairs. + attr_reader :atype + + # Map from short style option switches to actual switch objects. + attr_reader :short + + # Map from long style option switches to actual switch objects. + attr_reader :long + + # List of all switches and summary string. + attr_reader :list + + # + # Just initializes all instance variables. + # + def initialize + @atype = {} + @short = OptionMap.new + @long = OptionMap.new + @list = [] + end + + def pretty_print(q) # :nodoc: + q.group(1, "(", ")") do + @list.each do |sw| + next unless Switch === sw + q.group(1, "(" + sw.pretty_head, ")") do + sw.pretty_print_contents(q) + end + end + end + end + + # + # See Gem::OptionParser.accept. + # + def accept(t, pat = /.*/m, &block) + if pat + pat.respond_to?(:match) or + raise TypeError, "has no `match'", ParseError.filter_backtrace(caller(2)) + else + pat = t if t.respond_to?(:match) + end + unless block + block = pat.method(:convert).to_proc if pat.respond_to?(:convert) + end + @atype[t] = [pat, block] + end + + # + # See Gem::OptionParser.reject. + # + def reject(t) + @atype.delete(t) + end + + # + # Adds +sw+ according to +sopts+, +lopts+ and +nlopts+. + # + # +sw+:: Gem::OptionParser::Switch instance to be added. + # +sopts+:: Short style option list. + # +lopts+:: Long style option list. + # +nlopts+:: Negated long style options list. + # + def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc: + sopts.each {|o| @short[o] = sw} if sopts + lopts.each {|o| @long[o] = sw} if lopts + nlopts.each {|o| @long[o] = nsw} if nsw and nlopts + used = @short.invert.update(@long.invert) + @list.delete_if {|o| Switch === o and !used[o]} + end + private :update + + # + # Inserts +switch+ at the head of the list, and associates short, long + # and negated long options. Arguments are: + # + # +switch+:: Gem::OptionParser::Switch instance to be inserted. + # +short_opts+:: List of short style options. + # +long_opts+:: List of long style options. + # +nolong_opts+:: List of long style options with "no-" prefix. + # + # prepend(switch, short_opts, long_opts, nolong_opts) + # + def prepend(*args) + update(*args) + @list.unshift(args[0]) + end + + # + # Appends +switch+ at the tail of the list, and associates short, long + # and negated long options. Arguments are: + # + # +switch+:: Gem::OptionParser::Switch instance to be inserted. + # +short_opts+:: List of short style options. + # +long_opts+:: List of long style options. + # +nolong_opts+:: List of long style options with "no-" prefix. + # + # append(switch, short_opts, long_opts, nolong_opts) + # + def append(*args) + update(*args) + @list.push(args[0]) + end + + # + # Searches +key+ in +id+ list. The result is returned or yielded if a + # block is given. If it isn't found, nil is returned. + # + def search(id, key) + if list = __send__(id) + val = list.fetch(key) {return nil} + block_given? ? yield(val) : val + end + end + + # + # Searches list +id+ for +opt+ and the optional patterns for completion + # +pat+. If +icase+ is true, the search is case insensitive. The result + # is returned or yielded if a block is given. If it isn't found, nil is + # returned. + # + def complete(id, opt, icase = false, *pat, &block) + __send__(id).complete(opt, icase, *pat, &block) + end + + def get_candidates(id) + yield __send__(id).keys + end + + # + # Iterates over each option, passing the option to the +block+. + # + def each_option(&block) + list.each(&block) + end + + # + # Creates the summary table, passing each line to the +block+ (without + # newline). The arguments +args+ are passed along to the summarize + # method which is called on every option. + # + def summarize(*args, &block) + sum = [] + list.reverse_each do |opt| + if opt.respond_to?(:summarize) # perhaps Gem::OptionParser::Switch + s = [] + opt.summarize(*args) {|l| s << l} + sum.concat(s.reverse) + elsif !opt or opt.empty? + sum << "" + elsif opt.respond_to?(:each_line) + sum.concat([*opt.each_line].reverse) + else + sum.concat([*opt.each].reverse) + end + end + sum.reverse_each(&block) + end + + def add_banner(to) # :nodoc: + list.each do |opt| + if opt.respond_to?(:add_banner) + opt.add_banner(to) + end + end + to + end + + def compsys(*args, &block) # :nodoc: + list.each do |opt| + if opt.respond_to?(:compsys) + opt.compsys(*args, &block) + end + end + end + end + + # + # Hash with completion search feature. See Gem::OptionParser::Completion. + # + class CompletingHash < Hash + include Completion + + # + # Completion for hash key. + # + def match(key) + *values = fetch(key) { + raise AmbiguousArgument, catch(:ambiguous) {return complete(key)} + } + return key, *values + end + end + + # :stopdoc: + + # + # Enumeration of acceptable argument styles. Possible values are: + # + # NO_ARGUMENT:: The switch takes no arguments. (:NONE) + # REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED) + # OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL) + # + # Use like --switch=argument (long style) or -Xargument (short style). For + # short style, only portion matched to argument pattern is treated as + # argument. + # + ArgumentStyle = {} + NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument} + RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument} + OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument} + ArgumentStyle.freeze + + # + # Switches common used such as '--', and also provides default + # argument classes + # + DefaultList = List.new + DefaultList.short['-'] = Switch::NoArgument.new {} + DefaultList.long[''] = Switch::NoArgument.new {throw :terminate} + + + COMPSYS_HEADER = <<'XXX' # :nodoc: + +typeset -A opt_args +local context state line + +_arguments -s -S \ +XXX + + def compsys(to, name = File.basename($0)) # :nodoc: + to << "#compdef #{name}\n" + to << COMPSYS_HEADER + visit(:compsys, {}, {}) {|o, d| + to << %Q[ "#{o}[#{d.gsub(/[\"\[\]]/, '\\\\\&')}]" \\\n] + } + to << " '*:file:_files' && return 0\n" + end + + # + # Default options for ARGV, which never appear in option summary. + # + Officious = {} + + # + # --help + # Shows option summary. + # + Officious['help'] = proc do |parser| + Switch::NoArgument.new do |arg| + puts parser.help + exit + end + end + + # + # --*-completion-bash=WORD + # Shows candidates for command line completion. + # + Officious['*-completion-bash'] = proc do |parser| + Switch::RequiredArgument.new do |arg| + puts parser.candidate(arg) + exit + end + end + + # + # --*-completion-zsh[=NAME:FILE] + # Creates zsh completion file. + # + Officious['*-completion-zsh'] = proc do |parser| + Switch::OptionalArgument.new do |arg| + parser.compsys(STDOUT, arg) + exit + end + end + + # + # --version + # Shows version string if Version is defined. + # + Officious['version'] = proc do |parser| + Switch::OptionalArgument.new do |pkg| + if pkg + begin + require 'rubygems/vendor/optparse/lib/optparse/version' + rescue LoadError + else + show_version(*pkg.split(/,/)) or + abort("#{parser.program_name}: no version found in package #{pkg}") + exit + end + end + v = parser.ver or abort("#{parser.program_name}: version unknown") + puts v + exit + end + end + + # :startdoc: + + # + # Class methods + # + + # + # Initializes a new instance and evaluates the optional block in context + # of the instance. Arguments +args+ are passed to #new, see there for + # description of parameters. + # + # This method is *deprecated*, its behavior corresponds to the older #new + # method. + # + def self.with(*args, &block) + opts = new(*args) + opts.instance_eval(&block) + opts + end + + # + # Returns an incremented value of +default+ according to +arg+. + # + def self.inc(arg, default = nil) + case arg + when Integer + arg.nonzero? + when nil + default.to_i + 1 + end + end + def inc(*args) + self.class.inc(*args) + end + + # + # Initializes the instance and yields itself if called with a block. + # + # +banner+:: Banner message. + # +width+:: Summary width. + # +indent+:: Summary indent. + # + def initialize(banner = nil, width = 32, indent = ' ' * 4) + @stack = [DefaultList, List.new, List.new] + @program_name = nil + @banner = banner + @summary_width = width + @summary_indent = indent + @default_argv = ARGV + @require_exact = false + @raise_unknown = true + add_officious + yield self if block_given? + end + + def add_officious # :nodoc: + list = base() + Officious.each do |opt, block| + list.long[opt] ||= block.call(self) + end + end + + # + # Terminates option parsing. Optional parameter +arg+ is a string pushed + # back to be the first non-option argument. + # + def terminate(arg = nil) + self.class.terminate(arg) + end + def self.terminate(arg = nil) + throw :terminate, arg + end + + @stack = [DefaultList] + def self.top() DefaultList end + + # + # Directs to accept specified class +t+. The argument string is passed to + # the block in which it should be converted to the desired class. + # + # +t+:: Argument class specifier, any object including Class. + # +pat+:: Pattern for argument, defaults to +t+ if it responds to match. + # + # accept(t, pat, &block) + # + def accept(*args, &blk) top.accept(*args, &blk) end + # + # See #accept. + # + def self.accept(*args, &blk) top.accept(*args, &blk) end + + # + # Directs to reject specified class argument. + # + # +t+:: Argument class specifier, any object including Class. + # + # reject(t) + # + def reject(*args, &blk) top.reject(*args, &blk) end + # + # See #reject. + # + def self.reject(*args, &blk) top.reject(*args, &blk) end + + # + # Instance methods + # + + # Heading banner preceding summary. + attr_writer :banner + + # Program name to be emitted in error message and default banner, + # defaults to $0. + attr_writer :program_name + + # Width for option list portion of summary. Must be Numeric. + attr_accessor :summary_width + + # Indentation for summary. Must be String (or have + String method). + attr_accessor :summary_indent + + # Strings to be parsed in default. + attr_accessor :default_argv + + # Whether to require that options match exactly (disallows providing + # abbreviated long option as short option). + attr_accessor :require_exact + + # Whether to raise at unknown option. + attr_accessor :raise_unknown + + # + # Heading banner preceding summary. + # + def banner + unless @banner + @banner = +"Usage: #{program_name} [options]" + visit(:add_banner, @banner) + end + @banner + end + + # + # Program name to be emitted in error message and default banner, defaults + # to $0. + # + def program_name + @program_name || File.basename($0, '.*') + end + + # for experimental cascading :-) + alias set_banner banner= + alias set_program_name program_name= + alias set_summary_width summary_width= + alias set_summary_indent summary_indent= + + # Version + attr_writer :version + # Release code + attr_writer :release + + # + # Version + # + def version + (defined?(@version) && @version) || (defined?(::Version) && ::Version) + end + + # + # Release code + # + def release + (defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE) + end + + # + # Returns version string from program_name, version and release. + # + def ver + if v = version + str = +"#{program_name} #{[v].join('.')}" + str << " (#{v})" if v = release + str + end + end + + def warn(mesg = $!) + super("#{program_name}: #{mesg}") + end + + def abort(mesg = $!) + super("#{program_name}: #{mesg}") + end + + # + # Subject of #on / #on_head, #accept / #reject + # + def top + @stack[-1] + end + + # + # Subject of #on_tail. + # + def base + @stack[1] + end + + # + # Pushes a new List. + # + def new + @stack.push(List.new) + if block_given? + yield self + else + self + end + end + + # + # Removes the last List. + # + def remove + @stack.pop + end + + # + # Puts option summary into +to+ and returns +to+. Yields each line if + # a block is given. + # + # +to+:: Output destination, which must have method <<. Defaults to []. + # +width+:: Width of left side, defaults to @summary_width. + # +max+:: Maximum length allowed for left side, defaults to +width+ - 1. + # +indent+:: Indentation, defaults to @summary_indent. + # + def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk) + nl = "\n" + blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)} + visit(:summarize, {}, {}, width, max, indent, &blk) + to + end + + # + # Returns option summary string. + # + def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end + alias to_s help + + def pretty_print(q) # :nodoc: + q.object_group(self) do + first = true + if @stack.size > 2 + @stack.each_with_index do |s, i| + next if i < 2 + next if s.list.empty? + if first + first = false + q.text ":" + end + q.breakable + s.pretty_print(q) + end + end + end + end + + def inspect # :nodoc: + require 'pp' + pretty_print_inspect + end + + # + # Returns option summary list. + # + def to_a; summarize("#{banner}".split(/^/)) end + + # + # Checks if an argument is given twice, in which case an ArgumentError is + # raised. Called from Gem::OptionParser#switch only. + # + # +obj+:: New argument. + # +prv+:: Previously specified argument. + # +msg+:: Exception message. + # + def notwice(obj, prv, msg) # :nodoc: + unless !prv or prv == obj + raise(ArgumentError, "argument #{msg} given twice: #{obj}", + ParseError.filter_backtrace(caller(2))) + end + obj + end + private :notwice + + SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc: + + # :call-seq: + # make_switch(params, block = nil) + # + # :include: ../doc/optparse/creates_option.rdoc + # + def make_switch(opts, block = nil) + short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], [] + ldesc, sdesc, desc, arg = [], [], [] + default_style = Switch::NoArgument + default_pattern = nil + klass = nil + q, a = nil + has_arg = false + + opts.each do |o| + # argument class + next if search(:atype, o) do |pat, c| + klass = notwice(o, klass, 'type') + if not_style and not_style != Switch::NoArgument + not_pattern, not_conv = pat, c + else + default_pattern, conv = pat, c + end + end + + # directly specified pattern(any object possible to match) + if (!(String === o || Symbol === o)) and o.respond_to?(:match) + pattern = notwice(o, pattern, 'pattern') + if pattern.respond_to?(:convert) + conv = pattern.method(:convert).to_proc + else + conv = SPLAT_PROC + end + next + end + + # anything others + case o + when Proc, Method + block = notwice(o, block, 'block') + when Array, Hash + case pattern + when CompletingHash + when nil + pattern = CompletingHash.new + conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert) + else + raise ArgumentError, "argument pattern given twice" + end + o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}} + 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]*)(.+)?/ + q, a = $1, $2 + o = notwice(a ? Object : TrueClass, klass, 'type') + not_pattern, not_conv = search(:atype, o) unless not_style + not_style = (not_style || default_style).guess(arg = a) if a + default_style = Switch::NoArgument + default_pattern, conv = search(:atype, FalseClass) unless default_pattern + ldesc << "--no-#{q}" + (q = q.downcase).tr!('_', '-') + long << "no-#{q}" + nolong << q + when /^--\[no-\]([^\[\]=\s]*)(.+)?/ + q, a = $1, $2 + o = notwice(a ? Object : TrueClass, klass, 'type') + if a + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + ldesc << "--[no-]#{q}" + (o = q.downcase).tr!('_', '-') + long << o + not_pattern, not_conv = search(:atype, FalseClass) unless not_style + not_style = Switch::NoArgument + nolong << "no-#{o}" + when /^--([^\[\]=\s]*)(.+)?/ + q, a = $1, $2 + if a + o = notwice(NilClass, klass, 'type') + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + ldesc << "--#{q}" + (o = q.downcase).tr!('_', '-') + long << o + when /^-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/ + q, a = $1, $2 + o = notwice(Object, klass, 'type') + if a + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + else + has_arg = true + end + sdesc << "-#{q}" + short << Regexp.new(q) + when /^-(.)(.+)?/ + q, a = $1, $2 + if a + o = notwice(NilClass, klass, 'type') + default_style = default_style.guess(arg = a) + default_pattern, conv = search(:atype, o) unless default_pattern + end + sdesc << "-#{q}" + short << q + when /^=/ + style = notwice(default_style.guess(arg = o), style, 'style') + default_pattern, conv = search(:atype, Object) unless default_pattern + else + desc.push(o) if o && !o.empty? + end + end + + default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern + 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) + elsif !block + if style or pattern + raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller) + end + s = desc + else + short << pattern + s = (style || default_style).new(pattern, + conv, nil, nil, arg, desc, block) + end + return s, short, long, + (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style), + nolong + end + + # :call-seq: + # define(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + def define(*opts, &block) + top.append(*(sw = make_switch(opts, block))) + sw[0] + end + + # :call-seq: + # on(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + def on(*opts, &block) + define(*opts, &block) + self + end + alias def_option define + + # :call-seq: + # define_head(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + def define_head(*opts, &block) + top.prepend(*(sw = make_switch(opts, block))) + sw[0] + end + + # :call-seq: + # on_head(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + # The new option is added at the head of the summary. + # + def on_head(*opts, &block) + define_head(*opts, &block) + self + end + alias def_head_option define_head + + # :call-seq: + # define_tail(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + def define_tail(*opts, &block) + base.append(*(sw = make_switch(opts, block))) + sw[0] + end + + # + # :call-seq: + # on_tail(*params, &block) + # + # :include: ../doc/optparse/creates_option.rdoc + # + # The new option is added at the tail of the summary. + # + def on_tail(*opts, &block) + define_tail(*opts, &block) + self + end + alias def_tail_option define_tail + + # + # Add separator in summary. + # + def separator(string) + top.append(string, nil, nil) + end + + # + # Parses command line arguments +argv+ in order. When a block is given, + # each non-option argument is yielded. When optional +into+ keyword + # argument is provided, the parsed option values are stored there via + # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other + # similar object). + # + # Returns the rest of +argv+ left unparsed. + # + def order(*argv, into: nil, &nonopt) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + order!(argv, into: into, &nonopt) + end + + # + # Same as #order, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def order!(argv = default_argv, into: nil, &nonopt) + setter = ->(name, val) {into[name.to_sym] = val} if into + parse_in_order(argv, setter, &nonopt) + end + + def parse_in_order(argv = default_argv, setter = nil, &nonopt) # :nodoc: + opt, arg, val, rest = nil + nonopt ||= proc {|a| throw :terminate, a} + argv.unshift(arg) if arg = catch(:terminate) { + while arg = argv.shift + case arg + # long option + when /\A--([^=]*)(?:=(.*))?/m + 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 + end + rescue ParseError + throw :terminate, arg unless raise_unknown + raise $!.set_option(arg, true) + 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 + rescue ParseError + raise $!.set_option(arg, rest) + end + + # short option + when /\A-(.)((=).*|.+)?/m + eq, rest, opt = $3, $2, $1 + has_arg, val = eq, rest + begin + sw, = search(:short, opt) + unless sw + begin + sw, = complete(:short, opt) + # short option matched. + val = arg.delete_prefix('-') + has_arg = true + rescue InvalidOption + raise if require_exact + # if no short options match, try completion with long + # options. + sw, = complete(:long, opt) + eq ||= !rest + end + end + rescue ParseError + throw :terminate, arg unless raise_unknown + raise $!.set_option(arg, true) + end + begin + opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq} + rescue ParseError + raise $!.set_option(arg, arg.length > 2) + else + raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}" + 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 + rescue ParseError + raise $!.set_option(arg, arg.length > 2) + end + + # non-option argument + else + catch(:prune) do + visit(:each_option) do |sw0| + sw = sw0 + sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg) + end + nonopt.call(arg) + end + end + end + + nil + } + + visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern} + + argv + end + private :parse_in_order + + # + # Parses command line arguments +argv+ in permutation mode and returns + # list of non-option arguments. When optional +into+ keyword + # argument is provided, the parsed option values are stored there via + # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other + # similar object). + # + def permute(*argv, into: nil) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + permute!(argv, into: into) + end + + # + # Same as #permute, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def permute!(argv = default_argv, into: nil) + nonopts = [] + order!(argv, into: into, &nonopts.method(:<<)) + argv[0, 0] = nonopts + argv + end + + # + # Parses command line arguments +argv+ in order when environment variable + # POSIXLY_CORRECT is set, and in permutation mode otherwise. + # When optional +into+ keyword argument is provided, the parsed option + # values are stored there via <code>[]=</code> method (so it can be Hash, + # or OpenStruct, or other similar object). + # + def parse(*argv, into: nil) + argv = argv[0].dup if argv.size == 1 and Array === argv[0] + parse!(argv, into: into) + end + + # + # Same as #parse, but removes switches destructively. + # Non-option arguments remain in +argv+. + # + def parse!(argv = default_argv, into: nil) + if ENV.include?('POSIXLY_CORRECT') + order!(argv, into: into) + else + permute!(argv, into: into) + end + end + + # + # Wrapper method for getopts.rb. + # + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option") + # # params["a"] = true # -a + # # params["b"] = "1" # -b1 + # # params["foo"] = "1" # --foo + # # params["bar"] = "x" # --bar x + # # params["zot"] = "z" # --zot Z + # + # 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) + argv = Array === args.first ? args.shift : default_argv + single_options, *long_options = *args + + result = {} + + single_options.scan(/(.)(:)?/) do |opt, val| + if val + result[opt] = nil + define("-#{opt} VAL") + else + result[opt] = false + define("-#{opt}") + end + end if single_options + + long_options.each do |arg| + arg, desc = arg.split(';', 2) + opt, val = arg.split(':', 2) + if val + result[opt] = val.empty? ? nil : val + define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact) + else + result[opt] = false + define("--#{opt}", *[desc].compact) + end + end + + parse_in_order(argv, result.method(:[]=)) + symbolize_names ? result.transform_keys(&:to_sym) : result + end + + # + # See #getopts. + # + def self.getopts(*args, symbolize_names: false) + new.getopts(*args, symbolize_names: symbolize_names) + end + + # + # Traverses @stack, sending each element method +id+ with +args+ and + # +block+. + # + def visit(id, *args, &block) # :nodoc: + @stack.reverse_each do |el| + el.__send__(id, *args, &block) + end + nil + end + private :visit + + # + # Searches +key+ in @stack for +id+ hash and returns or yields the result. + # + def search(id, key) # :nodoc: + block_given = block_given? + visit(:search, id, key) do |k| + return block_given ? yield(k) : k + end + end + private :search + + # + # Completes shortened long style option switch and returns pair of + # canonical switch and switch descriptor Gem::OptionParser::Switch. + # + # +typ+:: Searching table. + # +opt+:: Searching key. + # +icase+:: Search case insensitive if true. + # +pat+:: Optional pattern for completion. + # + def complete(typ, opt, icase = false, *pat) # :nodoc: + if pat.empty? + search(typ, opt) {|sw| return [sw, opt]} # exact match or... + end + ambiguous = catch(:ambiguous) { + 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]) + end + private :complete + + # + # Returns additional info. + # + def additional_message(typ, opt) + return unless typ and opt and defined?(DidYouMean::SpellChecker) + all_candidates = [] + visit(:get_candidates, typ) do |candidates| + all_candidates.concat(candidates) + end + all_candidates.select! {|cand| cand.is_a?(String) } + checker = DidYouMean::SpellChecker.new(dictionary: all_candidates) + DidYouMean.formatter.message_for(all_candidates & checker.correct(opt)) + end + + def candidate(word) + list = [] + case word + when '-' + long = short = true + when /\A--/ + word, arg = word.split(/=/, 2) + argpat = Completion.regexp(arg, false) if arg and !arg.empty? + long = true + when /\A-/ + short = true + end + pat = Completion.regexp(word, long) + visit(:each_option) do |opt| + next unless Switch === opt + opts = (long ? opt.long : []) + (short ? opt.short : []) + opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat + if /\A=/ =~ opt.arg + opts.map! {|sw| sw + "="} + if arg and CompletingHash === opt.pattern + if opts = opt.pattern.candidate(arg, false, argpat) + opts.map!(&:last) + end + end + end + list.concat(opts) + end + list + end + + # + # Loads options from file names as +filename+. Does nothing when the file + # is not present. Returns whether successfully loaded. + # + # +filename+ defaults to basename of the program without suffix in a + # directory ~/.options, then the basename with '.options' suffix + # under XDG and Haiku standard places. + # + # The optional +into+ keyword argument works exactly like that accepted in + # method #parse. + # + def load(filename = nil, into: nil) + unless filename + basename = File.basename($0, '.*') + return true if load(File.expand_path(basename, '~/.options'), into: into) rescue nil + basename << ".options" + return [ + # XDG + ENV['XDG_CONFIG_HOME'], + '~/.config', + *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR), + + # Haiku + '~/config/settings', + ].any? {|dir| + next if !dir or dir.empty? + load(File.expand_path(basename, dir), into: into) rescue nil + } + end + begin + parse(*File.readlines(filename, chomp: true), into: into) + true + rescue Errno::ENOENT, Errno::ENOTDIR + false + end + end + + # + # Parses environment variable +env+ or its uppercase with splitting like a + # shell. + # + # +env+ defaults to the basename of the program. + # + def environment(env = File.basename($0, '.*')) + env = ENV[env] || ENV[env.upcase] or return + require 'shellwords' + parse(*Shellwords.shellwords(env)) + end + + # + # Acceptable argument classes + # + + # + # Any string and no conversion. This is fall-back. + # + accept(Object) {|s,|s or s.nil?} + + accept(NilClass) {|s,|s} + + # + # Any non-empty string, and no conversion. + # + accept(String, /.+/m) {|s,*|s} + + # + # Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal + # for 0x, and decimal for others; with optional sign prefix. Converts to + # Integer. + # + decimal = '\d+(?:_\d+)*' + binary = 'b[01]+(?:_[01]+)*' + hex = 'x[\da-f]+(?:_[\da-f]+)*' + octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?" + integer = "#{octal}|#{decimal}" + + accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,| + begin + Integer(s) + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end if s + } + + # + # Float number format, and converts to Float. + # + float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?" + floatpat = %r"\A[-+]?#{float}\z"io + accept(Float, floatpat) {|s,| s.to_f if s} + + # + # Generic numeric format, converts to Integer for integer format, Float + # for float format, and Rational for rational format. + # + real = "[-+]?(?:#{octal}|#{float})" + accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,| + if n + Rational(d, n) + elsif f + Float(s) + else + Integer(s) + end + } + + # + # Decimal integer format, to be converted to Integer. + # + DecimalInteger = /\A[-+]?#{decimal}\z/io + accept(DecimalInteger, DecimalInteger) {|s,| + begin + Integer(s, 10) + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end if s + } + + # + # Ruby/C like octal/hexadecimal/binary integer format, to be converted to + # Integer. + # + OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io + accept(OctalInteger, OctalInteger) {|s,| + begin + Integer(s, 8) + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end if s + } + + # + # Decimal integer/float number format, to be converted to Integer for + # integer format, Float for float format. + # + DecimalNumeric = floatpat # decimal integer is allowed as float also. + accept(DecimalNumeric, floatpat) {|s, f| + begin + if f + Float(s) + else + Integer(s) + end + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end if s + } + + # + # Boolean switch, which means whether it is present or not, whether it is + # absent or not with prefix no-, or it takes an argument + # yes/no/true/false/+/-. + # + yesno = CompletingHash.new + %w[- no false].each {|el| yesno[el] = false} + %w[+ yes true].each {|el| yesno[el] = true} + yesno['nil'] = false # should be nil? + accept(TrueClass, yesno) {|arg, val| val == nil or val} + # + # Similar to TrueClass, but defaults to false. + # + accept(FalseClass, yesno) {|arg, val| val != nil and val} + + # + # List of strings separated by ",". + # + accept(Array) do |s, | + if s + s = s.split(',').collect {|ss| ss unless ss.empty?} + end + s + end + + # + # Regular expression with options. + # + accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o| + f = 0 + if o + f |= Regexp::IGNORECASE if /i/ =~ o + f |= Regexp::MULTILINE if /m/ =~ o + f |= Regexp::EXTENDED if /x/ =~ o + 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, f) + end + + # + # Exceptions + # + + # + # Base class of exceptions from Gem::OptionParser. + # + class ParseError < RuntimeError + # Reason which caused the error. + Reason = 'parse error' + + def initialize(*args, additional: nil) + @additional = additional + @arg0, = args + @args = args + @reason = nil + end + + attr_reader :args + attr_writer :reason + attr_accessor :additional + + # + # Pushes back erred argument(s) to +argv+. + # + def recover(argv) + argv[0, 0] = @args + argv + end + + def self.filter_backtrace(array) + unless $DEBUG + array.delete_if(&%r"\A#{Regexp.quote(__FILE__)}:"o.method(:=~)) + end + array + end + + def set_backtrace(array) + super(self.class.filter_backtrace(array)) + end + + def set_option(opt, eq) + if eq + @args[0] = opt + else + @args.unshift(opt) + end + self + end + + # + # Returns error reason. Override this for I18N. + # + def reason + @reason || self.class::Reason + end + + def inspect + "#<#{self.class}: #{args.join(' ')}>" + end + + # + # Default stringizing method to emit standard error message. + # + def message + "#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}" + end + + alias to_s message + end + + # + # Raises when ambiguously completable string is encountered. + # + class AmbiguousOption < ParseError + const_set(:Reason, 'ambiguous option') + end + + # + # Raises when there is an argument for a switch which takes no argument. + # + class NeedlessArgument < ParseError + const_set(:Reason, 'needless argument') + end + + # + # Raises when a switch with mandatory argument has no argument. + # + class MissingArgument < ParseError + const_set(:Reason, 'missing argument') + end + + # + # Raises when switch is undefined. + # + class InvalidOption < ParseError + const_set(:Reason, 'invalid option') + end + + # + # Raises when the given argument does not match required format. + # + class InvalidArgument < ParseError + const_set(:Reason, 'invalid argument') + end + + # + # Raises when the given argument word can't be completed uniquely. + # + class AmbiguousArgument < InvalidArgument + const_set(:Reason, 'ambiguous argument') + end + + # + # Miscellaneous + # + + # + # Extends command line arguments array (ARGV) to parse itself. + # + module Arguable + + # + # Sets Gem::OptionParser object, when +opt+ is +false+ or +nil+, methods + # Gem::OptionParser::Arguable#options and Gem::OptionParser::Arguable#options= are + # undefined. Thus, there is no ways to access the Gem::OptionParser object + # via the receiver object. + # + def options=(opt) + unless @optparse = opt + class << self + undef_method(:options) + undef_method(:options=) + end + end + end + + # + # Actual Gem::OptionParser object, automatically created if nonexistent. + # + # If called with a block, yields the Gem::OptionParser object and returns the + # result of the block. If an Gem::OptionParser::ParseError exception occurs + # in the block, it is rescued, a error message printed to STDERR and + # +nil+ returned. + # + def options + @optparse ||= Gem::OptionParser.new + @optparse.default_argv = self + block_given? or return @optparse + begin + yield @optparse + rescue ParseError + @optparse.warn $! + nil + end + end + + # + # Parses +self+ destructively in order and returns +self+ containing the + # rest arguments left unparsed. + # + def order!(&blk) options.order!(self, &blk) end + + # + # Parses +self+ destructively in permutation mode and returns +self+ + # containing the rest arguments left unparsed. + # + def permute!() options.permute!(self) end + + # + # Parses +self+ destructively and returns +self+ containing the + # rest arguments left unparsed. + # + def parse!() options.parse!(self) end + + # + # Substitution of getopts is possible as follows. Also see + # Gem::OptionParser#getopts. + # + # def getopts(*args) + # ($OPT = ARGV.getopts(*args)).each do |opt, val| + # eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val" + # end + # rescue Gem::OptionParser::ParseError + # end + # + def getopts(*args, symbolize_names: false) + options.getopts(self, *args, symbolize_names: symbolize_names) + end + + # + # Initializes instance variable. + # + def self.extend_object(obj) + super + obj.instance_eval {@optparse = nil} + end + def initialize(*args) + super + @optparse = nil + end + end + + # + # Acceptable argument classes. Now contains DecimalInteger, OctalInteger + # and DecimalNumeric. See Acceptable argument classes (in source code). + # + module Acceptables + const_set(:DecimalInteger, Gem::OptionParser::DecimalInteger) + const_set(:OctalInteger, Gem::OptionParser::OctalInteger) + const_set(:DecimalNumeric, Gem::OptionParser::DecimalNumeric) + end +end + +# ARGV is arguable by Gem::OptionParser +ARGV.extend(Gem::OptionParser::Arguable) diff --git a/lib/rubygems/vendor/optparse/lib/optparse/ac.rb b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb new file mode 100644 index 0000000000..e84d01bf91 --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: false +require_relative '../optparse' + +class Gem::OptionParser::AC < Gem::OptionParser + private + + def _check_ac_args(name, block) + unless /\A\w[-\w]*\z/ =~ name + raise ArgumentError, name + end + unless block + raise ArgumentError, "no block given", ParseError.filter_backtrace(caller) + end + end + + ARG_CONV = proc {|val| val.nil? ? true : val} + + def _ac_arg_enable(prefix, name, help_string, block) + _check_ac_args(name, block) + + sdesc = [] + ldesc = ["--#{prefix}-#{name}"] + desc = [help_string] + q = name.downcase + ac_block = proc {|val| block.call(ARG_CONV.call(val))} + enable = Switch::PlacedArgument.new(nil, ARG_CONV, sdesc, ldesc, nil, desc, ac_block) + disable = Switch::NoArgument.new(nil, proc {false}, sdesc, ldesc, nil, desc, ac_block) + top.append(enable, [], ["enable-" + q], disable, ['disable-' + q]) + enable + end + + public + + def ac_arg_enable(name, help_string, &block) + _ac_arg_enable("enable", name, help_string, block) + end + + def ac_arg_disable(name, help_string, &block) + _ac_arg_enable("disable", name, help_string, block) + end + + def ac_arg_with(name, help_string, &block) + _check_ac_args(name, block) + + sdesc = [] + ldesc = ["--with-#{name}"] + desc = [help_string] + q = name.downcase + with = Switch::PlacedArgument.new(*search(:atype, String), sdesc, ldesc, nil, desc, block) + without = Switch::NoArgument.new(nil, proc {}, sdesc, ldesc, nil, desc, block) + top.append(with, [], ["with-" + q], without, ['without-' + q]) + with + end +end diff --git a/lib/rubygems/vendor/optparse/lib/optparse/date.rb b/lib/rubygems/vendor/optparse/lib/optparse/date.rb new file mode 100644 index 0000000000..d9a9f4f48a --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/date.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: false +require_relative '../optparse' +require 'date' + +Gem::OptionParser.accept(DateTime) do |s,| + begin + DateTime.parse(s) if s + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end +end +Gem::OptionParser.accept(Date) do |s,| + begin + Date.parse(s) if s + rescue ArgumentError + raise Gem::OptionParser::InvalidArgument, s + end +end diff --git a/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb new file mode 100644 index 0000000000..6987a5ed62 --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true +require_relative '../optparse' + +class Gem::OptionParser + # :call-seq: + # define_by_keywords(options, method, **params) + # + # :include: ../../doc/optparse/creates_option.rdoc + # + def define_by_keywords(options, meth, **opts) + meth.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| + options[name] = o + end + end + end + options + end +end diff --git a/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb new file mode 100644 index 0000000000..d47ad60255 --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# -*- ruby -*- + +require 'shellwords' +require_relative '../optparse' + +Gem::OptionParser.accept(Shellwords) {|s,| Shellwords.shellwords(s)} diff --git a/lib/rubygems/vendor/optparse/lib/optparse/time.rb b/lib/rubygems/vendor/optparse/lib/optparse/time.rb new file mode 100644 index 0000000000..c59e1e4ced --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/time.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: false +require_relative '../optparse' +require 'time' + +Gem::OptionParser.accept(Time) do |s,| + begin + (Time.httpdate(s) rescue Time.parse(s)) if s + rescue + raise Gem::OptionParser::InvalidArgument, s + end +end 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/vendor/optparse/lib/optparse/version.rb b/lib/rubygems/vendor/optparse/lib/optparse/version.rb new file mode 100644 index 0000000000..5d79e9db44 --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/version.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: false +# Gem::OptionParser internal utility + +class << Gem::OptionParser + def show_version(*pkgs) + progname = ARGV.options.program_name + result = false + show = proc do |klass, cname, version| + str = "#{progname}" + unless klass == ::Object and cname == :VERSION + version = version.join(".") if Array === version + str << ": #{klass}" unless klass == Object + str << " version #{version}" + end + [:Release, :RELEASE].find do |rel| + if klass.const_defined?(rel) + str << " (#{klass.const_get(rel)})" + end + end + puts str + result = true + end + if pkgs.size == 1 and pkgs[0] == "all" + self.search_const(::Object, /\AV(?:ERSION|ersion)\z/) do |klass, cname, version| + unless cname[1] == ?e and klass.const_defined?(:Version) + show.call(klass, cname.intern, version) + end + end + else + pkgs.each do |pkg| + begin + pkg = pkg.split(/::|\//).inject(::Object) {|m, c| m.const_get(c)} + v = case + when pkg.const_defined?(:Version) + pkg.const_get(n = :Version) + when pkg.const_defined?(:VERSION) + pkg.const_get(n = :VERSION) + else + n = nil + "unknown" + end + show.call(pkg, n, v) + rescue NameError + end + end + end + result + end + + def each_const(path, base = ::Object) + path.split(/::|\//).inject(base) do |klass, name| + raise NameError, path unless Module === klass + klass.constants.grep(/#{name}/i) do |c| + klass.const_defined?(c) or next + klass.const_get(c) + end + end + end + + def search_const(klass, name) + klasses = [klass] + while klass = klasses.shift + klass.constants.each do |cname| + klass.const_defined?(cname) or next + const = klass.const_get(cname) + yield klass, cname, const if name === cname + klasses << const if Module === const and const != ::Object + end + end + end +end diff --git a/lib/rubygems/vendor/resolv/.document b/lib/rubygems/vendor/resolv/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/resolv/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb new file mode 100644 index 0000000000..ac0ba0b313 --- /dev/null +++ b/lib/rubygems/vendor/resolv/lib/resolv.rb @@ -0,0 +1,3442 @@ +# frozen_string_literal: true + +require 'socket' +require_relative '../../timeout/lib/timeout' +require 'io/wait' + +begin + require 'securerandom' +rescue LoadError +end + +# 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 + + VERSION = "0.4.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+. + + def initialize(resolvers=nil, use_ipv6: nil) + @resolvers = resolvers || [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(use_ipv6: use_ipv6))] + 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|mingw|cygwin/ =~ RUBY_PLATFORM and + begin + require 'win32/resolv' + DefaultFileName = Win32::Resolv.get_hosts_path || IO::NULL + rescue LoadError + end + end + DefaultFileName ||= '/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) + each_resource(name, Resource::IN::A) {|resource| yield resource.address} + if use_ipv6? + each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} + end + end + + def use_ipv6? # :nodoc: + 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 + + def fetch_resource(name, typeclass) + lazy_initialize + begin + requester = make_udp_requester + rescue Errno::EACCES + # fall back to TCP + end + senders = {} + begin + @config.resolv(name) {|candidate, tout, nameserver, port| + requester ||= make_tcp_requester(nameserver, port) + msg = Message.new + msg.rd = 1 + msg.add_question(candidate, typeclass) + unless sender = senders[[candidate, nameserver, port]] + sender = requester.sender(msg, candidate, nameserver, port) + next if !sender + senders[[candidate, 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 + requester.close + # Retry via TCP: + requester = make_tcp_requester(nameserver, port) + senders = {} + # This will use TCP for all remaining candidates (assuming the + # current candidate does not already respond successfully via + # TCP). This makes sense because we already know the full + # response will not fit in an untruncated UDP packet. + 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 + } + ensure + 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) + 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 + + if defined? SecureRandom + def self.random(arg) # :nodoc: + begin + SecureRandom.random_number(arg) + rescue NotImplementedError + rand(arg) + end + end + else + def self.random(arg) # :nodoc: + 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 + + def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: + begin + 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) + when 'domain' + next if args.empty? + search = [args[0]] + when 'search' + next if args.empty? + search = args + when 'options' + args.each {|arg| + case arg + when /\Andots:(\d+)\z/ + ndots = $1.to_i + end + } + end + } + } + return { :nameserver => nameserver, :search => search, :ndots => ndots } + end + + def Config.default_config_hash(filename="/etc/resolv.conf") + if File.exist? filename + config_hash = Config.parse_resolv_conf(filename) + else + if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM + require 'win32/resolv' + search, nameserver = Win32::Resolv.get_resolv_info + config_hash = {} + config_hash[:nameserver] = nameserver if nameserver + config_hash[:search] = [search].flatten if search + end + end + config_hash || {} + 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 = [] + 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 + d << self.get_label + 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 = {} # :nodoc: + + 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: + return ClassHash[[type_value, class_value]] || + 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 proprty: + # - 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 + + ## + # Regular expression IPv4 addresses must match. + + Regex256 = /0 + |1(?:[0-9][0-9]?)? + |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? + |[3-9][0-9]?/x + Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ + + 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 + + ## + # A Gem::Resolv::LOC::Size + + class Size + + 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 + + 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 + + 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 + + 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 + + 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 + + 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/timeout/.document b/lib/rubygems/vendor/timeout/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/timeout/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb new file mode 100644 index 0000000000..df97d64ca0 --- /dev/null +++ b/lib/rubygems/vendor/timeout/lib/timeout.rb @@ -0,0 +1,199 @@ +# 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. +# +# Previous versions didn't use a module for namespacing, however +# #timeout is provided for backwards compatibility. You +# should prefer Gem::Timeout.timeout instead. +# +# == Copyright +# +# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc. +# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan + +module Gem::Timeout + VERSION = "0.4.1" + + # Internal error raised to when a timeout is triggered. + class ExitException < Exception + def exception(*) + self + end + end + + # Raised by Gem::Timeout.timeout when the block times out. + class Error < RuntimeError + def self.handle_timeout(message) + 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? + 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 number + # may be used, including Floats to specify fractional seconds. A + # value of 0 or +nil+ will execute the block without any timeout. + # +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? + + 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/vendor/tsort/.document b/lib/rubygems/vendor/tsort/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/tsort/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/tsort/lib/tsort.rb b/lib/rubygems/vendor/tsort/lib/tsort.rb new file mode 100644 index 0000000000..9dd7c09521 --- /dev/null +++ b/lib/rubygems/vendor/tsort/lib/tsort.rb @@ -0,0 +1,455 @@ +# frozen_string_literal: true + +#-- +# tsort.rb - provides a module for topological sorting and strongly connected components. +#++ +# + +# +# Gem::TSort implements topological sorting using Tarjan's algorithm for +# strongly connected components. +# +# Gem::TSort is designed to be able to be used with any object which can be +# interpreted as a directed graph. +# +# Gem::TSort requires two methods to interpret an object as a graph, +# tsort_each_node and tsort_each_child. +# +# * tsort_each_node is used to iterate for all nodes over a graph. +# * tsort_each_child is used to iterate for child nodes of a given node. +# +# The equality of nodes are defined by eql? and hash since +# Gem::TSort uses Hash internally. +# +# == A Simple Example +# +# The following example demonstrates how to mix the Gem::TSort module into an +# existing class (in this case, Hash). Here, we're treating each key in +# the hash as a node in the graph, and so we simply alias the required +# #tsort_each_node method to Hash's #each_key method. For each key in the +# hash, the associated value is an array of the node's child nodes. This +# choice in turn leads to our implementation of the required #tsort_each_child +# method, which fetches the array of child nodes and then iterates over that +# array using the user-supplied block. +# +# require 'rubygems/vendor/tsort/lib/tsort' +# +# class Hash +# include Gem::TSort +# alias tsort_each_node each_key +# def tsort_each_child(node, &block) +# fetch(node).each(&block) +# end +# end +# +# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort +# #=> [3, 2, 1, 4] +# +# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components +# #=> [[4], [2, 3], [1]] +# +# == A More Realistic Example +# +# A very simple `make' like tool can be implemented as follows: +# +# require 'rubygems/vendor/tsort/lib/tsort' +# +# class Make +# def initialize +# @dep = {} +# @dep.default = [] +# end +# +# def rule(outputs, inputs=[], &block) +# triple = [outputs, inputs, block] +# outputs.each {|f| @dep[f] = [triple]} +# @dep[triple] = inputs +# end +# +# def build(target) +# each_strongly_connected_component_from(target) {|ns| +# if ns.length != 1 +# fs = ns.delete_if {|n| Array === n} +# raise Gem::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}") +# end +# n = ns.first +# if Array === n +# outputs, inputs, block = n +# inputs_time = inputs.map {|f| File.mtime f}.max +# begin +# outputs_time = outputs.map {|f| File.mtime f}.min +# rescue Errno::ENOENT +# outputs_time = nil +# end +# if outputs_time == nil || +# inputs_time != nil && outputs_time <= inputs_time +# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i +# block.call +# end +# end +# } +# end +# +# def tsort_each_child(node, &block) +# @dep[node].each(&block) +# end +# include Gem::TSort +# end +# +# def command(arg) +# print arg, "\n" +# system arg +# end +# +# m = Make.new +# m.rule(%w[t1]) { command 'date > t1' } +# m.rule(%w[t2]) { command 'date > t2' } +# m.rule(%w[t3]) { command 'date > t3' } +# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' } +# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' } +# m.build('t5') +# +# == Bugs +# +# * 'tsort.rb' is wrong name because this library uses +# Tarjan's algorithm for strongly connected components. +# Although 'strongly_connected_components.rb' is correct but too long. +# +# == References +# +# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms", +# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972. +# + +module Gem::TSort + + VERSION = "0.2.0" + + class Cyclic < StandardError + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # If there is a cycle, Gem::TSort::Cyclic is raised. + # + # class G + # include Gem::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.tsort #=> [4, 2, 3, 1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.tsort # raises Gem::TSort::Cyclic + # + def tsort + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Gem::TSort.tsort(each_node, each_child) + end + + # Returns a topologically sorted array of nodes. + # The array is sorted from children to parents, i.e. + # the first element has no child and the last node has no parent. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # If there is a cycle, Gem::TSort::Cyclic is raised. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Gem::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Gem::TSort.tsort(each_node, each_child) # raises Gem::TSort::Cyclic + # + def self.tsort(each_node, each_child) + tsort_each(each_node, each_child).to_a + end + + # The iterator version of the #tsort method. + # <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #tsort_each returns +nil+. + # If there is a cycle, Gem::TSort::Cyclic is raised. + # + # class G + # include Gem::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.tsort_each {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def tsort_each(&block) # :yields: node + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Gem::TSort.tsort_each(each_node, each_child, &block) + end + + # The iterator version of the Gem::TSort.tsort method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Gem::TSort.tsort_each(each_node, each_child) {|n| p n } + # #=> 4 + # # 2 + # # 3 + # # 1 + # + def self.tsort_each(each_node, each_child) # :yields: node + return to_enum(__method__, each_node, each_child) unless block_given? + + each_strongly_connected_component(each_node, each_child) {|component| + if component.size == 1 + yield component.first + else + raise Cyclic.new("topological sort failed: #{component.inspect}") + end + } + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # class G + # include Gem::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2], [3], [1]] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # p graph.strongly_connected_components #=> [[4], [2, 3], [1]] + # + def strongly_connected_components + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Gem::TSort.strongly_connected_components(each_node, each_child) + end + + # Returns strongly connected components as an array of arrays of nodes. + # The array is sorted from children to parents. + # Each elements of the array represents a strongly connected component. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Gem::TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2], [3], [1]] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # p Gem::TSort.strongly_connected_components(each_node, each_child) + # #=> [[4], [2, 3], [1]] + # + def self.strongly_connected_components(each_node, each_child) + each_strongly_connected_component(each_node, each_child).to_a + end + + # The iterator version of the #strongly_connected_components method. + # <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to + # <tt><em>obj</em>.strongly_connected_components.each</tt>, but + # modification of _obj_ during the iteration may lead to unexpected results. + # + # #each_strongly_connected_component returns +nil+. + # + # class G + # include Gem::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def each_strongly_connected_component(&block) # :yields: nodes + each_node = method(:tsort_each_node) + each_child = method(:tsort_each_child) + Gem::TSort.each_strongly_connected_component(each_node, each_child, &block) + end + + # The iterator version of the Gem::TSort.strongly_connected_components method. + # + # The graph is represented by _each_node_ and _each_child_. + # _each_node_ should have +call+ method which yields for each node in the graph. + # _each_child_ should have +call+ method which takes a node argument and yields for each child node. + # + # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2] + # # [3] + # # [1] + # + # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_node = lambda {|&b| g.each_key(&b) } + # each_child = lambda {|n, &b| g[n].each(&b) } + # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc } + # #=> [4] + # # [2, 3] + # # [1] + # + def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes + return to_enum(__method__, each_node, each_child) unless block_given? + + id_map = {} + stack = [] + each_node.call {|node| + unless id_map.include? node + each_strongly_connected_component_from(node, each_child, id_map, stack) {|c| + yield c + } + end + } + nil + end + + # Iterates over strongly connected component in the subgraph reachable from + # _node_. + # + # Return value is unspecified. + # + # #each_strongly_connected_component_from doesn't call #tsort_each_node. + # + # class G + # include Gem::TSort + # def initialize(g) + # @g = g + # end + # def tsort_each_child(n, &b) @g[n].each(&b) end + # def tsort_each_node(&b) @g.each_key(&b) end + # end + # + # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2] + # + # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}) + # graph.each_strongly_connected_component_from(2) {|scc| p scc } + # #=> [4] + # # [2, 3] + # + def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes + Gem::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block) + end + + # Iterates over strongly connected components in a graph. + # The graph is represented by _node_ and _each_child_. + # + # _node_ is the first node. + # _each_child_ should have +call+ method which takes a node argument + # and yields for each child node. + # + # Return value is unspecified. + # + # #Gem::TSort.each_strongly_connected_component_from is a class method and + # it doesn't need a class to represent a graph which includes Gem::TSort. + # + # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]} + # each_child = lambda {|n, &b| graph[n].each(&b) } + # Gem::TSort.each_strongly_connected_component_from(1, each_child) {|scc| + # p scc + # } + # #=> [4] + # # [2, 3] + # # [1] + # + def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes + return to_enum(__method__, node, each_child, id_map, stack) unless block_given? + + minimum_id = node_id = id_map[node] = id_map.size + stack_length = stack.length + stack << node + + each_child.call(node) {|child| + if id_map.include? child + child_id = id_map[child] + minimum_id = child_id if child_id && child_id < minimum_id + else + sub_minimum_id = + each_strongly_connected_component_from(child, each_child, id_map, stack) {|c| + yield c + } + minimum_id = sub_minimum_id if sub_minimum_id < minimum_id + end + } + + if node_id == minimum_id + component = stack.slice!(stack_length .. -1) + component.each {|n| id_map[n] = nil} + yield component + end + + minimum_id + end + + # Should be implemented by a extended class. + # + # #tsort_each_node is used to iterate for all nodes over a graph. + # + def tsort_each_node # :yields: node + raise NotImplementedError.new + end + + # Should be implemented by a extended class. + # + # #tsort_each_child is used to iterate for child nodes of _node_. + # + def tsort_each_child(node) # :yields: child + raise NotImplementedError.new + end +end diff --git a/lib/rubygems/vendor/uri/.document b/lib/rubygems/vendor/uri/.document new file mode 100644 index 0000000000..0c43bbd6b3 --- /dev/null +++ b/lib/rubygems/vendor/uri/.document @@ -0,0 +1 @@ +# Vendored files do not need to be documented diff --git a/lib/rubygems/vendor/uri/lib/uri.rb b/lib/rubygems/vendor/uri/lib/uri.rb new file mode 100644 index 0000000000..f1ccc167cc --- /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[http://tools.ietf.org/html/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[http://tools.ietf.org/html/rfc822] +# - RFC1738[http://tools.ietf.org/html/rfc1738] +# - RFC2255[http://tools.ietf.org/html/rfc2255] +# - RFC2368[http://tools.ietf.org/html/rfc2368] +# - RFC2373[http://tools.ietf.org/html/rfc2373] +# - RFC2396[http://tools.ietf.org/html/rfc2396] +# - RFC2732[http://tools.ietf.org/html/rfc2732] +# - RFC3986[http://tools.ietf.org/html/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..921fb9dd28 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/common.rb @@ -0,0 +1,853 @@ +# 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 + include RFC2396_REGEXP + + REGEXP = RFC2396_REGEXP + Parser = RFC2396_Parser + RFC3986_PARSER = RFC3986_Parser.new + Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor) + + # Gem::URI::Parser.new + DEFAULT_PARSER = Parser.new + DEFAULT_PARSER.pattern.each_pair do |sym, str| + unless REGEXP::PATTERN.const_defined?(sym) + REGEXP::PATTERN.const_set(sym, str) + end + end + DEFAULT_PARSER.regexp.each_pair do |sym, str| + const_set(sym, str) + end + Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor) + + 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 + 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.const_set(scheme.to_s.upcase, 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.constants.map { |name| + [name.to_s.upcase, Schemes.const_get(name)] + }.to_h + end + + INITIAL_SCHEMES = scheme_list + private_constant :INITIAL_SCHEMES + Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor) + + # 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 = scheme.to_s.upcase + + uri_class = INITIAL_SCHEMES[const_name] + uri_class ||= if /\A[A-Z]\w*\z/.match?(const_name) && Schemes.const_defined?(const_name, false) + Schemes.const_get(const_name, false) + end + 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) + RFC3986_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 ::escape string +uri+ + # if it may contain invalid Gem::URI characters. + # + def self.parse(uri) + RFC3986_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) + RFC3986_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 + DEFAULT_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 + DEFAULT_PARSER.make_regexp(schemes) + end + + TBLENCWWWCOMP_ = {} # :nodoc: + 256.times do |i| + TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i) + end + TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze + 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 + + 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 + + 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}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-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}[https://docs.ruby-lang.org/en/master/implicit_conversion_rdoc.html#label-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}[https://docs.ruby-lang.org/en/master/String.html#method-i-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: + # + # # 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> + # + 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..d419b26055 --- /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::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, "can not set userinfo for file Gem::URI" + end + + # raise InvalidURIError + def check_user(user) + raise Gem::URI::InvalidURIError, "can not set user for file Gem::URI" + end + + # raise InvalidURIError + def check_password(user) + raise Gem::URI::InvalidURIError, "can not 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..100498ffb2 --- /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. + # http://tools.ietf.org/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..72c52aa8ee --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/generic.rb @@ -0,0 +1,1588 @@ +# 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::Escape.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) + DEFAULT_PARSER.escape(x) + else + x + end + }) + elsif args.kind_of?(Hash) + tmp = {} + args.each do |key, value| + tmp[key] = if value + DEFAULT_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.class.component rescue ::Gem::URI::Generic::COMPONENT + raise ArgumentError, + "expected Array of or Hash of components of #{self.class} (#{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.userinfo = userinfo + self.hostname = host + self.port = port + self.path = path + self.query = query + self.opaque = opaque + self.fragment = fragment + else + self.set_scheme(scheme) + self.set_userinfo(userinfo) + self.set_host(host) + self.set_port(port) + 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 a Gem::URI::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 Gem::URI::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 Gem::URI::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, + "can not 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 Gem::URI::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, + "can not 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 if 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, @password) + 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 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 Gem::URI::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, + "can not 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 + + # + # == 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) + 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 Gem::URI::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, + "can not 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) + port + end + + def check_registry(v) # :nodoc: + raise InvalidURIError, "can not set registry" + end + private :check_registry + + def set_registry(v) #:nodoc: + raise InvalidURIError, "can not set registry" + end + protected :set_registry + + def registry=(v) + raise InvalidURIError, "can not set registry" + end + + # + # Checks the path +v+ component for RFC2396 compliance + # and against the Gem::URI::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 Gem::URI::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, + "can not 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 Gem::URI::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://tools.ietf.org/html/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.userinfo || rel.host || rel.port + + # 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_path(merge_path(base.path, rel.path)) if base.path && rel.path + else + # RFC2396, Section 5.2, 4) + base.set_path(rel.path) if rel.path + end + + # RFC2396, Section 5.2, 7) + base.set_userinfo(rel.userinfo) if rel.userinfo + base.set_host(rel.host) if rel.host + base.set_port(rel.port) if rel.port + 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 can not `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 can not `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 + 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 + + def hash + self.component_ary.hash + end + + def eql?(oth) + self.class == oth.class && + parser == oth.parser && + self.component_ary.eql?(oth.component_ary) + end + +=begin + +--- Gem::URI::Generic#===(oth) + +=end +# def ===(oth) +# raise NotImplementedError +# end + +=begin +=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 + "#<#{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. Use http_proxy.', 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..bef43490a3 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/http.rb @@ -0,0 +1,125 @@ +# 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 + + # + # == 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://datatracker.ietf.org/doc/html/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://datatracker.ietf.org/doc/html/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..735a269f2c --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb @@ -0,0 +1,539 @@ +# 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::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::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 Gem::URI::Parser.initialize_pattern. + attr_reader :pattern + + # The Hash of Regexp. + # + # See also Gem::URI::Parser.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 + # + # p = Gem::URI::Parser.new + # p.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 Gem::URI::Parser.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 + /(?=#{Regexp.union(*schemes)}:)#{@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) + 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 + + # 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 + + 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/rfc3986_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb new file mode 100644 index 0000000000..728bb55674 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb @@ -0,0 +1,183 @@ +# 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 + + @@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..3c80c334d4 --- /dev/null +++ b/lib/rubygems/vendor/uri/lib/uri/version.rb @@ -0,0 +1,6 @@ +module Gem::URI + # :stopdoc: + VERSION_CODE = '001300'.freeze + VERSION = VERSION_CODE.scan(/../).collect{|n| n.to_i}.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_molinillo.rb b/lib/rubygems/vendored_molinillo.rb new file mode 100644 index 0000000000..45906c0e5c --- /dev/null +++ b/lib/rubygems/vendored_molinillo.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "vendor/molinillo/lib/molinillo" 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_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 20bbff4fdd..e174d8ad95 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true + +require_relative "deprecate" + ## # The Version class processes string versions into comparable # values. A version string should normally be a series of numbers @@ -128,7 +131,7 @@ # # == Preventing Version Catastrophe: # -# From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html +# From: https://www.zenspider.com/ruby/2008/10/rubygems-how-to-preventing-catastrophe.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 @@ -150,31 +153,27 @@ # a zero to give a sensible result. class Gem::Version - autoload :Requirement, File.expand_path('requirement', __dir__) - include Comparable - VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?'.freeze # :nodoc: - ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze # :nodoc: + 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/ # :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) - unless Gem::Deprecate.skip - warn "nil versions are discouraged and will be deprecated in Rubygems 4" if version.nil? - end + nil_versions_are_discouraged! if version.nil? - !!(version.to_s =~ ANCHORED_VERSION_PATTERN) + ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -189,6 +188,8 @@ class Gem::Version if self === input # check yourself before you wreck yourself input elsif input.nil? + nil_versions_are_discouraged! + nil else new input @@ -200,11 +201,19 @@ 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. @@ -215,9 +224,17 @@ 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.is_a?(String) && /\A\s*\Z/.match?(version) - @version = version.to_s.strip.gsub("-",".pre.") + @version = version.to_s + + # 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 end @@ -243,7 +260,7 @@ class Gem::Version # same precision. Version "1.0" is not the same as version "1". def eql?(other) - self.class === other and @version == other._version + self.class === other && @version == other.version end def hash # :nodoc: @@ -263,7 +280,7 @@ class Gem::Version # string for backwards (RubyGems 1.3.5 and earlier) compatibility. def marshal_dump - [version] + [@version] end ## @@ -275,7 +292,7 @@ class Gem::Version end def yaml_initialize(tag, map) # :nodoc: - @version = map['version'] + @version = -map["version"] @segments = nil @hash = nil end @@ -285,7 +302,7 @@ class Gem::Version end def encode_with(coder) # :nodoc: - coder.add 'version', @version + coder.add "version", @version end ## @@ -293,7 +310,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 @@ -308,12 +325,12 @@ class Gem::Version def release @@release[self] ||= if prerelease? - segments = self.segments - segments.pop while segments.any? {|s| String === s } - self.class.new segments.join('.') - else - self - end + segments = self.segments + segments.pop while segments.any? {|s| String === s } + self.class.new segments.join(".") + else + self + end end def segments # :nodoc: @@ -339,11 +356,13 @@ 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> return +nil+. + # <tt>Gem::Version</tt> or a valid version String return +nil+. 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 + return 0 if @version == other.version || canonical_segments == other.canonical_segments lhsegments = canonical_segments rhsegments = other.canonical_segments @@ -355,7 +374,8 @@ class Gem::Version i = 0 while i <= limit - lhs, rhs = lhsegments[i] || 0, rhsegments[i] || 0 + lhs = lhsegments[i] || 0 + rhs = rhsegments[i] || 0 i += 1 next if lhs == rhs @@ -365,42 +385,40 @@ class Gem::Version return lhs <=> rhs end - return 0 + 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 - 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 be71ef409b..7910fd3d1b 100644 --- a/lib/rubygems/version_option.rb +++ b/lib/rubygems/version_option.rb @@ -1,22 +1,22 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require 'rubygems' +require_relative "../rubygems" ## # Mixin methods for --version and --platform Gem::Command options. module Gem::VersionOption - ## # Add the --platform option to the option parser. def add_platform_option(task = command, *wrap) - OptionParser.accept Gem::Platform do |value| + Gem::OptionParser.accept Gem::Platform do |value| if value == Gem::Platform::RUBY value else @@ -24,9 +24,8 @@ module Gem::VersionOption end end - add_option('--platform PLATFORM', Gem::Platform, - "Specify the platform of gem to #{task}", *wrap) do - |value, options| + add_option("--platform PLATFORM", Gem::Platform, + "Specify the platform of gem to #{task}", *wrap) do |value, options| unless options[:added_platform] Gem.platforms = [Gem::Platform::RUBY] options[:added_platform] = true @@ -51,13 +50,12 @@ module Gem::VersionOption # Add the --version option to the option parser. def add_version_option(task = command, *wrap) - OptionParser.accept Gem::Requirement do |value| + Gem::OptionParser.accept Gem::Requirement do |value| Gem::Requirement.new(*value.split(/\s*,\s*/)) end - add_option('-v', '--version VERSION', Gem::Requirement, - "Specify version of gem to #{task}", *wrap) do - |value, options| + add_option("-v", "--version VERSION", Gem::Requirement, + "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/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb new file mode 100644 index 0000000000..128becc1ce --- /dev/null +++ b/lib/rubygems/yaml_serializer.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module Gem + # A stub yaml serializer that can handle only hashes and strings (as of now). + module YAMLSerializer + module_function + + def dump(hash) + yaml = String.new("---") + yaml << dump_hash(hash) + end + + def dump_hash(hash) + yaml = String.new("\n") + hash.each do |k, v| + yaml << k << ":" + if v.is_a?(Hash) + yaml << dump_hash(v).gsub(/^(?!$)/, " ") # indent all non-empty lines + elsif v.is_a?(Array) # Expected to be array of strings + if v.empty? + yaml << " []\n" + else + yaml << "\n- " << v.map {|s| s.to_s.gsub(/\s+/, " ").inspect }.join("\n- ") << "\n" + end + else + yaml << " " << v.to_s.gsub(/\s+/, " ").inspect << "\n" + end + end + yaml + end + + ARRAY_REGEX = / + ^ + (?:[ ]*-[ ]) # '- ' before array items + (['"]?) # optional opening quote + (.*) # value + \1 # matching closing quote + $ + /xo + + HASH_REGEX = / + ^ + ([ ]*) # indentations + (.+) # key + (?::(?=(?:\s|$))) # : (without the lookahead the #key includes this when : is present in value) + [ ]? + (['"]?) # optional opening quote + (.*) # value + \3 # matching closing quote + $ + /xo + + def load(str) + res = {} + stack = [res] + last_hash = nil + last_empty_key = nil + str.split(/\r?\n/) do |line| + if match = HASH_REGEX.match(line) + indent, key, quote, val = match.captures + val = strip_comment(val) + + convert_to_backward_compatible_key!(key) + depth = indent.size / 2 + if quote.empty? && val.empty? + new_hash = {} + stack[depth][key] = new_hash + stack[depth + 1] = new_hash + last_empty_key = key + last_hash = stack[depth] + else + val = [] if val == "[]" # empty array + stack[depth][key] = val + end + elsif match = ARRAY_REGEX.match(line) + _, val = match.captures + val = strip_comment(val) + + last_hash[last_empty_key] = [] unless last_hash[last_empty_key].is_a?(Array) + + last_hash[last_empty_key].push(val) + end + end + res + end + + def strip_comment(val) + if val.include?("#") && !val.start_with?("#") + val.split("#", 2).first.strip + else + val + end + end + + # for settings' keys + def convert_to_backward_compatible_key!(key) + key << "/" if /https?:/i.match?(key) && !%r{/\Z}.match?(key) + key.gsub!(".", "__") + end + + class << self + private :dump_hash, :convert_to_backward_compatible_key! + end + end +end |