diff options
Diffstat (limited to 'lib/rubygems')
252 files changed, 20204 insertions, 2814 deletions
diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb index 58b601f6b0..0af80cc3db 100644 --- a/lib/rubygems/available_set.rb +++ b/lib/rubygems/available_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::AvailableSet include Enumerable @@ -26,7 +27,7 @@ class Gem::AvailableSet s = o.set when Array s = o.map do |sp,so| - if !sp.kind_of?(Gem::Specification) || !so.kind_of?(Gem::Source) + if !sp.is_a?(Gem::Specification) || !so.is_a?(Gem::Source) raise TypeError, "Array must be in [[spec, source], ...] form" end @@ -69,7 +70,7 @@ class Gem::AvailableSet end def all_specs - @set.map {|t| t.spec } + @set.map(&:spec) end def match_platform! @@ -104,14 +105,14 @@ class Gem::AvailableSet def to_request_set(development = :none) request_set = Gem::RequestSet.new - request_set.development = :all == development + request_set.development = development == :all each_spec do |spec| request_set.always_install << spec request_set.gem spec.name, spec.version request_set.import spec.development_dependencies if - :shallow == development + development == :shallow end request_set @@ -146,7 +147,7 @@ class Gem::AvailableSet end def remove_installed!(dep) - @set.reject! do |t| + @set.reject! do |_t| # already locally installed Gem::Specification.any? do |installed_spec| dep.name == installed_spec.name && diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb index dcc64e6409..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. @@ -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. " + + 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 ## @@ -109,9 +116,7 @@ class Gem::BasicSpecification 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 @@ -132,9 +137,9 @@ class Gem::BasicSpecification def full_name if platform == Gem::Platform::RUBY || platform.nil? - "#{name}-#{version}".dup.tap(&Gem::UNTAINT) + "#{name}-#{version}" else - "#{name}-#{version}-#{platform}".dup.tap(&Gem::UNTAINT) + "#{name}-#{version}-#{platform}" end end @@ -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(',')}}" + dirs = if raw_require_paths + if raw_require_paths.size > 1 + "{#{raw_require_paths.join(",")}}" else - self.raw_require_paths.first + raw_require_paths.first end else "lib" # default value for require_paths for bundler/inline end - "#{self.full_gem_path}/#{dirs}".dup.tap(&Gem::UNTAINT) + "#{full_gem_path}/#{dirs}" end ## @@ -323,15 +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 5b34227d3a..dd2fd77418 100644 --- a/lib/rubygems/bundler_version_finder.rb +++ b/lib/rubygems/bundler_version_finder.rb @@ -52,7 +52,7 @@ module Gem::BundlerVersionFinder 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 @@ -65,9 +65,9 @@ module Gem::BundlerVersionFinder return unless gemfile lockfile = case gemfile - when "gems.rb" then "gems.locked" - else "#{gemfile}.lock" - end.dup.tap(&Gem::UNTAINT) + when "gems.rb" then "gems.locked" + else "#{gemfile}.lock" + end return unless File.file?(lockfile) diff --git a/lib/rubygems/ci_detector.rb b/lib/rubygems/ci_detector.rb new file mode 100644 index 0000000000..7a2d4ee29a --- /dev/null +++ b/lib/rubygems/ci_detector.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Gem + module CIDetector + # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and + # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates). + # TODO: Drop that duplication once bundler drops support for RubyGems 3.4 + # + # ## Recognized CI providers, their signifiers, and the relevant docs ## + # + # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables + # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables + # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables + # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/ + # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/ + # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables + # dsari - CI, DSARI https://github.com/rfinnie/dsari#running + # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables + # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters + # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments + # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars + # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/ + # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables + # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html + # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables + # + # ### Some "standard" ENVs that multiple providers may set ### + # + # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard. + # * CI_NAME - Not as frequently used, but some providers set this to specify their own name + + # Any of these being set is a reasonably reliable indicator that we are + # executing in a CI environment. + ENV_INDICATORS = [ + "CI", + "CI_NAME", + "CONTINUOUS_INTEGRATION", + "BUILD_NUMBER", + "CI_APP_ID", + "CI_BUILD_ID", + "CI_BUILD_NUMBER", + "RUN_ID", + "TASKCLUSTER_ROOT_URL", + ].freeze + + # For each CI, this env suffices to indicate that we're on _that_ CI's + # containers. (A few of them only supply a CI_NAME variable, which is also + # nice). And if they set "CI" but we can't tell which one they are, we also + # want to know that - a bare "ci" without another token tells us as much. + ENV_DESCRIPTORS = { + "TRAVIS" => "travis", + "CIRCLECI" => "circle", + "CIRRUS_CI" => "cirrus", + "DSARI" => "dsari", + "SEMAPHORE" => "semaphore", + "JENKINS_URL" => "jenkins", + "BUILDKITE" => "buildkite", + "GO_SERVER_URL" => "go", + "GITLAB_CI" => "gitlab", + "GITHUB_ACTIONS" => "github", + "TASKCLUSTER_ROOT_URL" => "taskcluster", + "CI" => "ci", + }.freeze + + def self.ci? + ENV_INDICATORS.any? {|var| ENV.include?(var) } + end + + def self.ci_strings + matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values + matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"] + matching_names.reject(&:empty?).sort.uniq + end + end +end diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb index 59bfc5a118..ec498a8b94 100644 --- a/lib/rubygems/command.rb +++ b/lib/rubygems/command.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require_relative "optparse" +require_relative "vendored_optparse" require_relative "requirement" require_relative "user_interaction" @@ -19,9 +20,7 @@ require_relative "user_interaction" class Gem::Command include Gem::UserInteraction - Gem::OptionParser.accept Symbol do |value| - value.to_sym - end + Gem::OptionParser.accept Symbol, &:to_sym ## # The name of the command. @@ -93,7 +92,7 @@ class Gem::Command # array or a string to be split on white space. def self.add_specific_extra_args(cmd,args) - args = args.split(/\s+/) if args.kind_of? String + args = args.split(/\s+/) if args.is_a? String specific_extra_args_hash[cmd] = args end @@ -191,7 +190,7 @@ class Gem::Command "Please specify at least one gem name (e.g. gem build GEMNAME)" end - args.select {|arg| arg !~ /^-/ } + args.reject {|arg| arg.start_with?("-") } end ## @@ -201,11 +200,15 @@ class Gem::Command # respectively. def get_all_gem_names_and_versions get_all_gem_names.map do |name| - if /\A(.*):(#{Gem::Requirement::PATTERN_RAW})\z/ =~ name - [$1, $2] - else - [name] - end + extract_gem_name_and_version(name) + end + end + + def extract_gem_name_and_version(name) # :nodoc: + if /\A(.*):(#{Gem::Requirement::PATTERN_RAW})\z/ =~ name + [$1, $2] + else + [name] end end @@ -223,7 +226,7 @@ class Gem::Command if args.size > 1 raise Gem::CommandLineError, - "Too many gem names (#{args.join(', ')}); please specify only one" + "Too many gem names (#{args.join(", ")}); please specify only one" end args.first @@ -311,7 +314,7 @@ class Gem::Command options[:build_args] = build_args if options[:silent] - old_ui = self.ui + old_ui = ui self.ui = ui = Gem::SilentUI.new end @@ -393,22 +396,21 @@ class Gem::Command def check_deprecated_options(options) options.each do |option| - if option_is_deprecated?(option) - deprecation = @deprecated_options[command][option] - version_to_expire = deprecation["rg_version_to_expire"] + next unless option_is_deprecated?(option) + deprecation = @deprecated_options[command][option] + version_to_expire = deprecation["rg_version_to_expire"] - deprecate_option_msg = if version_to_expire - "The \"#{option}\" option has been deprecated and will be removed in Rubygems #{version_to_expire}." - else - "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems." - end + deprecate_option_msg = if version_to_expire + "The \"#{option}\" option has been deprecated and will be removed in Rubygems #{version_to_expire}." + else + "The \"#{option}\" option has been deprecated and will be removed in future versions of Rubygems." + end - extra_msg = deprecation["extra_msg"] + extra_msg = deprecation["extra_msg"] - deprecate_option_msg += " #{extra_msg}" if extra_msg + deprecate_option_msg += " #{extra_msg}" if extra_msg - alert_warning(deprecate_option_msg) - end + alert_warning(deprecate_option_msg) end end @@ -425,12 +427,10 @@ class Gem::Command # True if the command handles the given argument list. def handles?(args) - begin - parser.parse!(args.dup) - return true - rescue - return false - end + parser.parse!(args.dup) + true + rescue StandardError + false end ## @@ -457,7 +457,7 @@ class Gem::Command until extra.empty? do ex = [] ex << extra.shift - ex << extra.shift if extra.first.to_s =~ /^[^-]/ # rubocop:disable Performance/StartWith + ex << extra.shift if /^[^-]/.match?(extra.first.to_s) result << ex if handles?(ex) end @@ -473,7 +473,7 @@ class Gem::Command private def option_is_deprecated?(option) - @deprecated_options[command].has_key?(option) + @deprecated_options[command].key?(option) end def add_parser_description # :nodoc: @@ -485,7 +485,7 @@ class Gem::Command @parser.separator nil @parser.separator " Description:" - formatted.split("\n").each do |line| + formatted.each_line do |line| @parser.separator " #{line.rstrip}" end end @@ -512,8 +512,8 @@ class Gem::Command @parser.separator nil @parser.separator " #{title}:" - content.split(/\n/).each do |line| - @parser.separator " #{line}" + content.each_line do |line| + @parser.separator " #{line.rstrip}" end end @@ -522,7 +522,7 @@ class Gem::Command @parser.separator nil @parser.separator " Summary:" - wrap(@summary, 80 - 4).split("\n").each do |line| + wrap(@summary, 80 - 4).each_line do |line| @parser.separator " #{line.strip}" end end @@ -579,12 +579,12 @@ class Gem::Command # Add the options common to all commands. add_common_option("-h", "--help", - "Get help on this command") do |value, options| + "Get help on this command") do |_value, options| options[:help] = true end add_common_option("-V", "--[no-]verbose", - "Set the verbose level of output") do |value, options| + "Set the verbose level of output") do |value, _options| # Set us to "really verbose" so the progress meter works if Gem.configuration.verbose && value Gem.configuration.verbose = 1 @@ -593,12 +593,12 @@ class Gem::Command end end - add_common_option("-q", "--quiet", "Silence command progress meter") do |value, options| + add_common_option("-q", "--quiet", "Silence command progress meter") do |_value, _options| Gem.configuration.verbose = false end add_common_option("--silent", - "Silence RubyGems output") do |value, options| + "Silence RubyGems output") do |_value, options| options[:silent] = true end diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb index 1bdbd50530..8e578dc196 100644 --- a/lib/rubygems/command_manager.rb +++ b/lib/rubygems/command_manager.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -43,6 +44,7 @@ class Gem::CommandManager :contents, :dependency, :environment, + :exec, :fetch, :generate_index, :help, @@ -58,6 +60,7 @@ class Gem::CommandManager :push, :query, :rdoc, + :rebuild, :search, :server, :signin, @@ -82,7 +85,7 @@ class Gem::CommandManager # Return the authoritative instance of the command manager. def self.instance - @command_manager ||= new + @instance ||= new end ## @@ -97,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| @@ -139,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 ## @@ -147,7 +150,7 @@ class Gem::CommandManager def run(args, build_args=nil) process_args(args, build_args) - rescue StandardError, Timeout::Error => ex + rescue StandardError, Gem::Timeout::Error => ex if ex.respond_to?(:detailed_message) msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 } else @@ -199,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 @@ -236,7 +239,7 @@ 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}") @@ -247,6 +250,7 @@ class Gem::CommandManager def invoke_command(args, build_args) cmd_name = args.shift.downcase cmd = find_command cmd_name + terminate_interaction 1 unless cmd cmd.deprecation_warning if cmd.deprecated? cmd.invoke_with_build_args args, build_args end diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb index 5d6152d3b9..2ec8324141 100644 --- a/lib/rubygems/commands/build_command.rb +++ b/lib/rubygems/commands/build_command.rb @@ -1,21 +1,24 @@ # frozen_string_literal: true + require_relative "../command" +require_relative "../gemspec_helpers" require_relative "../package" require_relative "../version_option" class Gem::Commands::BuildCommand < Gem::Command include Gem::VersionOption + include Gem::GemspecHelpers def initialize super "build", "Build a gem from a gemspec" add_platform_option - add_option "--force", "skip validation of the spec" do |value, options| + add_option "--force", "skip validation of the spec" do |_value, options| options[:force] = true end - add_option "--strict", "consider warnings as errors when validating the spec" do |value, options| + add_option "--strict", "consider warnings as errors when validating the spec" do |_value, options| options[:strict] = true end @@ -74,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 17b1d11b19..72dcf1dd17 100644 --- a/lib/rubygems/commands/cert_command.rb +++ b/lib/rubygems/commands/cert_command.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../security" class Gem::Commands::CertCommand < Gem::Command def initialize super "cert", "Manage RubyGems certificates and signing settings", - :add => [], :remove => [], :list => [], :build => [], :sign => [] + add: [], remove: [], list: [], build: [], sign: [] add_option("-a", "--add CERT", "Add a trusted certificate.") do |cert_file, options| @@ -135,7 +136,7 @@ class Gem::Commands::CertCommand < Gem::Command end def build(email) - if !valid_email?(email) + unless valid_email?(email) raise Gem::CommandLineError, "Invalid email address #{email}" end @@ -177,9 +178,9 @@ class Gem::Commands::CertCommand < Gem::Command algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM key = Gem::Security.create_key(algorithm) - key_path = Gem::Security.write key, "gem-private_key.pem", 0600, passphrase + key_path = Gem::Security.write key, "gem-private_key.pem", 0o600, passphrase - return key, key_path + [key, key_path] end def certificates_matching(filter) @@ -262,7 +263,6 @@ For further reading on signing gems see `ri Gem::Security`. key = File.read key_file passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"] options[:key] = OpenSSL::PKey.read key, passphrase - rescue Errno::ENOENT alert_error \ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist" @@ -291,7 +291,7 @@ For further reading on signing gems see `ri Gem::Security`. cert = File.read cert_file cert = OpenSSL::X509::Certificate.new cert - permissions = File.stat(cert_file).mode & 0777 + permissions = File.stat(cert_file).mode & 0o777 issuer_cert = options[:issuer_cert] issuer_key = options[:key] diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb index 4d1f8782b1..fb23dd9cb4 100644 --- a/lib/rubygems/commands/check_command.rb +++ b/lib/rubygems/commands/check_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../validator" @@ -9,7 +10,7 @@ class Gem::Commands::CheckCommand < Gem::Command def initialize super "check", "Check a gem repository for added or missing files", - :alien => true, :doctor => false, :dry_run => false, :gems => true + alien: true, doctor: false, dry_run: false, gems: true add_option("-a", "--[no-]alien", 'Report "unmanaged" or rogue files in the', @@ -40,17 +41,21 @@ class Gem::Commands::CheckCommand < Gem::Command def check_gems say "Checking gems..." say - gems = get_all_gem_names rescue [] + gems = begin + get_all_gem_names + rescue StandardError + [] + end Gem::Validator.new.alien(gems).sort.each do |key, val| - unless val.empty? + if val.empty? + say "#{key} is error-free" if Gem.configuration.verbose + else say "#{key} has #{val.size} problems" val.each do |error_entry| say " #{error_entry.path}:" say " #{error_entry.problem}" end - else - say "#{key} is error-free" if Gem.configuration.verbose end say end diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb index 1ae84924c1..08fb598cea 100644 --- a/lib/rubygems/commands/cleanup_command.rb +++ b/lib/rubygems/commands/cleanup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../dependency_list" require_relative "../uninstaller" @@ -7,16 +8,16 @@ class Gem::Commands::CleanupCommand < Gem::Command def initialize super "cleanup", "Clean up old versions of installed gems", - :force => false, :install_dir => Gem.dir, - :check_dev => true + force: false, install_dir: Gem.dir, + check_dev: true add_option("-n", "-d", "--dry-run", - "Do not uninstall gems") do |value, options| + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end add_option(:Deprecated, "--dryrun", - "Do not uninstall gems") do |value, options| + "Do not uninstall gems") do |_value, options| options[:dryrun] = true end deprecate_option("--dryrun", extra_msg: "Use --dry-run instead") @@ -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,12 +117,12 @@ If no gems are named all gems in GEM_HOME are cleaned. end def get_candidate_gems - @candidate_gems = unless options[:args].empty? + @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 - else - Gem::Specification.to_a end end @@ -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 @@ -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 c5fdfca31e..807158d9c9 100644 --- a/lib/rubygems/commands/contents_command.rb +++ b/lib/rubygems/commands/contents_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -7,8 +8,8 @@ class Gem::Commands::ContentsCommand < Gem::Command def initialize super "contents", "Display the contents of the installed gems", - :specdirs => [], :lib_only => false, :prefix => true, - :show_install_dir => false + specdirs: [], lib_only: false, prefix: true, + show_install_dir: false add_version_option @@ -91,9 +92,9 @@ prefix or only the files that are requireable. def files_in_gem(spec) gem_path = spec.full_gem_path - extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only] + extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only] glob = "#{gem_path}#{extra}/**/*" - prefix_re = /#{Regexp.escape(gem_path)}\// + prefix_re = %r{#{Regexp.escape(gem_path)}/} Dir[glob].map do |file| [gem_path, file.sub(prefix_re, "")] @@ -103,7 +104,7 @@ 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"], $'] when /\.so\z/ @@ -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 3f69a95e83..9aaefae999 100644 --- a/lib/rubygems/commands/dependency_command.rb +++ b/lib/rubygems/commands/dependency_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -10,15 +11,14 @@ class Gem::Commands::DependencyCommand < Gem::Command def initialize super "dependency", "Show the dependencies of an installed gem", - :version => Gem::Requirement.default, :domain => :local + version: Gem::Requirement.default, domain: :local add_version_option add_platform_option add_prerelease_option add_option("-R", "--[no-]reverse-dependencies", - "Include reverse dependencies in the output") do - |value, options| + "Include reverse dependencies in the output") do |value, options| options[:reverse_dependencies] = value end @@ -90,10 +90,9 @@ use with other commands. def display_pipe(specs) # :nodoc: specs.each do |spec| - unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| - say "#{dep.name} --version '#{dep.requirement}'" - end + next if spec.dependencies.empty? + spec.dependencies.sort_by(&:name).each do |dep| + say "#{dep.name} --version '#{dep.requirement}'" end end end @@ -153,7 +152,7 @@ use with other commands. response = String.new response << " " * level + "Gem #{spec.full_name}\n" unless spec.dependencies.empty? - spec.dependencies.sort_by {|dep| dep.name }.each do |dep| + spec.dependencies.sort_by(&:name).each do |dep| response << " " * level + " #{dep}\n" end end diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb index d95e1d0dbb..8ed0996069 100644 --- a/lib/rubygems/commands/environment_command.rb +++ b/lib/rubygems/commands/environment_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::EnvironmentCommand < Gem::Command @@ -16,7 +17,7 @@ class Gem::Commands::EnvironmentCommand < Gem::Command platform display the supported gem platforms <omitted> display everything EOF - return args.gsub(/^\s+/, "") + args.gsub(/^\s+/, "") end def description # :nodoc: @@ -107,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" @@ -172,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 5eb45d259c..f7f5b62306 100644 --- a/lib/rubygems/commands/fetch_command.rb +++ b/lib/rubygems/commands/fetch_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -9,8 +10,8 @@ class Gem::Commands::FetchCommand < Gem::Command def initialize defaults = { - :suggest_alternate => true, - :version => Gem::Requirement.default, + suggest_alternate: true, + version: Gem::Requirement.default, } super "fetch", "Download a gem and place it in the current directory", defaults diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb index bc71e60ff0..13be92593b 100644 --- a/lib/rubygems/commands/generate_index_command.rb +++ b/lib/rubygems/commands/generate_index_command.rb @@ -1,85 +1,51 @@ # frozen_string_literal: true + require_relative "../command" -require_relative "../indexer" -## -# Generates a index files for use as a gem server. -# -# See `gem help generate_index` +unless defined? Gem::Commands::GenerateIndexCommand + class Gem::Commands::GenerateIndexCommand < Gem::Command + module RubygemsTrampoline + def description # :nodoc: + <<~EOF + The generate_index command has been moved to the rubygems-generate_index gem. + EOF + end -class Gem::Commands::GenerateIndexCommand < Gem::Command - def initialize - super "generate_index", - "Generates the index files for a gem server directory", - :directory => ".", :build_modern => true + def execute + alert_error "Install the rubygems-generate_index gem for the generate_index command" + end - add_option "-d", "--directory=DIRNAME", - "repository base dir containing gems subdir" do |dir, options| - options[:directory] = File.expand_path dir + def invoke_with_build_args(args, build_args) + name = "rubygems-generate_index" + spec = begin + Gem::Specification.find_by_name(name) + rescue Gem::LoadError + require "rubygems/dependency_installer" + Gem.install(name, Gem::Requirement.default, Gem::DependencyInstaller::DEFAULT_OPTIONS).find {|s| s.name == name } + end + + # remove the methods defined in this file so that the methods defined in the gem are used instead, + # and without a method redefinition warning + %w[description execute invoke_with_build_args].each do |method| + RubygemsTrampoline.remove_method(method) + end + self.class.singleton_class.remove_method(:new) + + spec.activate + Gem.load_plugin_files spec.matches_for_glob("rubygems_plugin#{Gem.suffix_pattern}") + + self.class.new.invoke_with_build_args(args, build_args) + end end + private_constant :RubygemsTrampoline - add_option "--[no-]modern", - "Generate indexes for RubyGems", - "(always true)" do |value, options| - options[:build_modern] = value + # remove_method(:initialize) warns, but removing new does not warn + def self.new + command = allocate + command.send(:initialize, "generate_index", "Generates the index files for a gem server directory (requires rubygems-generate_index)") + command end - deprecate_option("--modern", version: "4.0", extra_msg: "Modern indexes (specs, latest_specs, and prerelease_specs) are always generated, so this option is not needed.") - deprecate_option("--no-modern", version: "4.0", extra_msg: "The `--no-modern` option is currently ignored. Modern indexes (specs, latest_specs, and prerelease_specs) are always generated.") - - add_option "--update", - "Update modern indexes with gems added", - "since the last update" do |value, options| - options[:update] = value - end - end - - def defaults_str # :nodoc: - "--directory . --modern" - end - - def description # :nodoc: - <<-EOF -The generate_index command creates a set of indexes for serving gems -statically. The command expects a 'gems' directory under the path given to -the --directory option. The given directory will be the directory you serve -as the gem repository. - -For `gem generate_index --directory /path/to/repo`, expose /path/to/repo via -your HTTP server configuration (not /path/to/repo/gems). - -When done, it will generate a set of files like this: - - gems/*.gem # .gem files you want to - # index - - specs.<version>.gz # specs index - latest_specs.<version>.gz # latest specs index - prerelease_specs.<version>.gz # prerelease specs index - quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file - -The .rz extension files are compressed with the inflate algorithm. -The Marshal version number comes from ruby's Marshal::MAJOR_VERSION and -Marshal::MINOR_VERSION constants. It is used to ensure compatibility. - EOF - end - - def execute - # This is always true because it's the only way now. - options[:build_modern] = true - - if !File.exist?(options[:directory]) || - !File.directory?(options[:directory]) - alert_error "unknown directory name #{options[:directory]}." - terminate_interaction 1 - else - indexer = Gem::Indexer.new options.delete(:directory), options - - if options[:update] - indexer.update_index - else - indexer.generate_index - end - end + prepend(RubygemsTrampoline) end end diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb index bf4ffefbb7..1619b152e7 100644 --- a/lib/rubygems/commands/help_command.rb +++ b/lib/rubygems/commands/help_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::HelpCommand < Gem::Command @@ -58,7 +59,7 @@ multiple environments. The RubyGems implementation is designed to be compatible with Bundler's Gemfile format. You can see additional documentation on the format at: - http://bundler.io + https://bundler.io RubyGems automatically looks for these gem dependencies files: @@ -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 ================================== @@ -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], @@ -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" + 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 ced7751ff5..f65c639662 100644 --- a/lib/rubygems/commands/info_command.rb +++ b/lib/rubygems/commands/info_command.rb @@ -8,8 +8,8 @@ class Gem::Commands::InfoCommand < Gem::Command def initialize super "info", "Show information for the given gem", - :name => //, :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + name: //, domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb index c04c01f258..2091634a29 100644 --- a/lib/rubygems/commands/install_command.rb +++ b/lib/rubygems/commands/install_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../install_update_options" require_relative "../dependency_installer" @@ -22,11 +23,11 @@ class Gem::Commands::InstallCommand < Gem::Command def initialize defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({ - :format_executable => false, - :lock => true, - :suggest_alternate => true, - :version => Gem::Requirement.default, - :without_groups => [], + format_executable: false, + lock: true, + suggest_alternate: true, + version: Gem::Requirement.default, + without_groups: [], }) defaults.merge!(install_update_options) @@ -47,7 +48,7 @@ class Gem::Commands::InstallCommand < Gem::Command end def defaults_str # :nodoc: - "--both --version '#{Gem::Requirement.default}' --no-force\n" + + "--both --version '#{Gem::Requirement.default}' --no-force\n" \ "--install-dir #{Gem.dir} --lock\n" + install_update_defaults_str end @@ -135,13 +136,6 @@ You can use `i` command instead of `install`. "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags" end - def check_install_dir # :nodoc: - if options[:install_dir] && options[:user_install] - alert_error "Use --install-dir or --user-install but not both" - terminate_interaction 1 - end - end - def check_version # :nodoc: if options[:version] != Gem::Requirement.default && get_all_gem_names.size > 1 @@ -161,7 +155,6 @@ You can use `i` command instead of `install`. ENV.delete "GEM_PATH" if options[:install_dir].nil? - check_install_dir check_version load_hooks @@ -170,7 +163,7 @@ You can use `i` command instead of `install`. show_installed - say update_suggestion if eglible_for_update? + say update_suggestion if eligible_for_update? terminate_interaction exit_code end @@ -262,7 +255,7 @@ You can use `i` command instead of `install`. return unless errors errors.each do |x| - return unless Gem::SourceFetchProblem === x + next unless Gem::SourceFetchProblem === x require_relative "../uri" msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}" diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb index 011873b99c..fab4b73814 100644 --- a/lib/rubygems/commands/list_command.rb +++ b/lib/rubygems/commands/list_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -10,8 +11,8 @@ class Gem::Commands::ListCommand < Gem::Command def initialize super "list", "Display local gems whose name matches REGEXP", - :domain => :local, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :local, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb index da636492c9..f7fd5ada16 100644 --- a/lib/rubygems/commands/lock_command.rb +++ b/lib/rubygems/commands/lock_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::LockCommand < Gem::Command def initialize super "lock", "Generate a lockdown list of gems", - :strict => false + strict: false add_option "-s", "--[no-]strict", "fail if unable to satisfy a dependency" do |strict, options| diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb index b633cd3d81..b91a8db12d 100644 --- a/lib/rubygems/commands/mirror_command.rb +++ b/lib/rubygems/commands/mirror_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::MirrorCommand diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb index d5283f72dd..0fe90dc8b8 100644 --- a/lib/rubygems/commands/open_command.rb +++ b/lib/rubygems/commands/open_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" @@ -69,9 +70,7 @@ class Gem::Commands::OpenCommand < Gem::Command end def open_editor(path) - Dir.chdir(path) do - system(*@editor.split(/\s+/) + [path]) - end + system(*@editor.split(/\s+/) + [path], { chdir: path }) end def spec_for(name) diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb index 1785194389..08a9221a26 100644 --- a/lib/rubygems/commands/outdated_command.rb +++ b/lib/rubygems/commands/outdated_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../spec_fetcher" diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb index 959a6186dc..12bfe3a834 100644 --- a/lib/rubygems/commands/owner_command.rb +++ b/lib/rubygems/commands/owner_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -38,7 +39,7 @@ permission to. add_proxy_option add_key_option add_otp_option - defaults.merge! :add => [], :remove => [] + defaults.merge! add: [], remove: [] add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options| options[:add] << value @@ -78,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 @@ -93,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 diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb index 72db53ef37..456d897df2 100644 --- a/lib/rubygems/commands/pristine_command.rb +++ b/lib/rubygems/commands/pristine_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../package" require_relative "../installer" @@ -10,10 +11,10 @@ class Gem::Commands::PristineCommand < Gem::Command def initialize super "pristine", "Restores installed gems to pristine condition from files located in the gem cache", - :version => Gem::Requirement.default, - :extensions => true, - :extensions_set => false, - :all => false + version: Gem::Requirement.default, + extensions: true, + extensions_set: false, + all: false add_option("--all", "Restore all installed gems to pristine", @@ -34,6 +35,11 @@ class Gem::Commands::PristineCommand < Gem::Command options[:extensions] = value end + add_option("--only-missing-extensions", + "Only restore gems with missing extensions") do |value, options| + options[:only_missing_extensions] = value + end + add_option("--only-executables", "Only restore executables") do |value, options| options[:only_executables] = value @@ -107,13 +113,15 @@ extensions will be restored. 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| RUBY_ENGINE == spec.platform || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } + specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY } if specs.to_a.empty? raise Gem::Exception, @@ -128,7 +136,7 @@ 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 @@ -171,12 +179,12 @@ extensions will be restored. install_dir = options[:install_dir] if options[:install_dir] installer_options = { - :wrappers => true, - :force => true, - :install_dir => install_dir || spec.base_dir, - :env_shebang => env_shebang, - :build_args => spec.build_args, - :bin_dir => bin_dir, + wrappers: true, + force: true, + install_dir: install_dir || spec.base_dir, + env_shebang: env_shebang, + build_args: spec.build_args, + bin_dir: bin_dir, } if options[:only_executables] diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb index 46b65f4e15..591ddc3a80 100644 --- a/lib/rubygems/commands/push_command.rb +++ b/lib/rubygems/commands/push_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../gemcutter_utilities" @@ -29,7 +30,7 @@ The push command will use ~/.gem/credentials to authenticate to a server, but yo end def initialize - super "push", "Push a gem up to the gem server", :host => self.host + super "push", "Push a gem up to the gem server", host: host @user_defined_host = false @@ -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 c6315acf8c..3b527974a3 100644 --- a/lib/rubygems/commands/query_command.rb +++ b/lib/rubygems/commands/query_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" require_relative "../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,11 +18,10 @@ 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, - :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", diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb index a998a9704c..977c90b8c4 100644 --- a/lib/rubygems/commands/rdoc_command.rb +++ b/lib/rubygems/commands/rdoc_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../rdoc" @@ -9,8 +10,8 @@ class Gem::Commands::RdocCommand < Gem::Command def initialize super "rdoc", "Generates RDoc for pre-installed gems", - :version => Gem::Requirement.default, - :include_rdoc => false, :include_ri => true, :overwrite => false + version: Gem::Requirement.default, + include_rdoc: false, include_ri: true, overwrite: false add_option("--all", "Generate RDoc/RI documentation for all", @@ -83,14 +84,7 @@ Use --overwrite to force rebuilding of documentation. FileUtils.rm_rf File.join(spec.doc_dir, "rdoc") end - begin - doc.generate - rescue Errno::ENOENT => e - match = / - /.match(e.message) - alert_error "Unable to document #{spec.full_name}, " \ - " #{match.post_match} is missing, skipping" - terminate_interaction 1 if specs.length == 1 - end + doc.generate end end end diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb new file mode 100644 index 0000000000..97f05ef79c --- /dev/null +++ b/lib/rubygems/commands/rebuild_command.rb @@ -0,0 +1,264 @@ +# 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 + + DATE_FORMAT = "%Y-%m-%d %H:%M:%S.%N Z" + + def initialize + super "rebuild", "Attempt to reproduce a build of a gem." + + add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options| + options[:diff] = true + end + + add_option "--force", "Skip validation of the spec." do |_value, options| + options[:force] = true + end + + add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options| + options[:strict] = true + end + + add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options| + options[:source] = value + end + + add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options| + options[:original_gem_file] = value + end + + add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options| + options[:gemspec_file] = value + end + + add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options| + options[:build_path] = value + end + end + + def arguments # :nodoc: + "GEM_NAME gem name on gem server\n" \ + "GEM_VERSION gem version you are attempting to rebuild" + end + + def description # :nodoc: + <<-EOF +The rebuild command allows you to (attempt to) reproduce a build of a gem +from a ruby gemspec. + +This command assumes the gemspec can be built with the `gem build` command. +If you use any of `gem build`, `rake build`, or`rake release` in the +build/release process for a gem, it is a potential candidate. + +You will need to match the RubyGems version used, since this is included in +the Gem metadata. + +If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will +require more effort to reproduce a build. For example, it might require +more precisely matched versions of Ruby and/or Bundler to be used. + EOF + end + + def usage # :nodoc: + "#{program_name} GEM_NAME GEM_VERSION" + end + + def execute + gem_name, gem_version = get_gem_name_and_version + + old_dir, new_dir = prep_dirs + + gem_filename = "#{gem_name}-#{gem_version}.gem" + old_file = File.join(old_dir, gem_filename) + new_file = File.join(new_dir, gem_filename) + + if options[:original_gem_file] + FileUtils.copy_file(options[:original_gem_file], old_file) + else + download_gem(gem_name, gem_version, old_file) + end + + rg_version = rubygems_version(old_file) + unless rg_version == Gem::VERSION + alert_error <<-EOF +You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with. + +#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}. +Gem files include the version of RubyGems used to build them. +This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}. + +You're using RubyGems v#{Gem::VERSION}. + +Please install RubyGems v#{rg_version} and try again. + EOF + terminate_interaction 1 + end + + source_date_epoch = get_timestamp(old_file).to_s + + if build_path = options[:build_path] + Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) } + else + build_gem(gem_name, source_date_epoch, new_file) + end + + compare(source_date_epoch, old_file, new_file) + end + + private + + def sha256(file) + Digest::SHA256.hexdigest(Gem.read_binary(file)) + end + + def get_timestamp(file) + mtime = nil + File.open(file, Gem.binary_mode) do |f| + Gem::Package::TarReader.new(f) do |tar| + mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime } + end + end + + mtime + end + + def compare(source_date_epoch, old_file, new_file) + date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z") + + old_hash = sha256(old_file) + new_hash = sha256(new_file) + + say + say "Built at: #{date} (#{source_date_epoch})" + say "Original build saved to: #{old_file}" + say "Reproduced build saved to: #{new_file}" + say "Working directory: #{options[:build_path] || Dir.pwd}" + say + say "Hash comparison:" + say " #{old_hash}\t#{old_file}" + say " #{new_hash}\t#{new_file}" + say + + if old_hash == new_hash + say "SUCCESS - original and rebuild hashes matched" + else + say "FAILURE - original and rebuild hashes did not match" + say + + if options[:diff] + if system("diffoscope", old_file, new_file).nil? + alert_error "error: could not find `diffoscope` executable" + end + else + say "Pass --diff for more details (requires diffoscope to be installed)." + end + + terminate_interaction 1 + end + end + + def prep_dirs + rebuild_dir = Dir.mktmpdir("gem_rebuild") + old_dir = File.join(rebuild_dir, "old") + new_dir = File.join(rebuild_dir, "new") + + FileUtils.mkdir_p(old_dir) + FileUtils.mkdir_p(new_dir) + + [old_dir, new_dir] + end + + def get_gem_name_and_version + args = options[:args] || [] + if args.length == 2 + gem_name, gem_version = args + elsif args.length > 2 + raise Gem::CommandLineError, "Too many arguments" + else + raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)" + end + + [gem_name, gem_version] + end + + def build_gem(gem_name, source_date_epoch, output_file) + gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec") + + if gemspec + build_package(gemspec, source_date_epoch, output_file) + else + alert_error error_message(gem_name) + terminate_interaction(1) + end + end + + def build_package(gemspec, source_date_epoch, output_file) + with_source_date_epoch(source_date_epoch) do + spec = Gem::Specification.load(gemspec) + if spec + Gem::Package.build( + spec, + options[:force], + options[:strict], + output_file + ) + else + alert_error "Error loading gemspec. Aborting." + terminate_interaction 1 + end + end + end + + def with_source_date_epoch(source_date_epoch) + old_sde = ENV["SOURCE_DATE_EPOCH"] + ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s + + yield + ensure + ENV["SOURCE_DATE_EPOCH"] = old_sde + end + + def error_message(gem_name) + if gem_name + "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}" + else + "Couldn't find a gemspec file in #{Dir.pwd}" + end + end + + def download_gem(gem_name, gem_version, old_file) + # This code was based loosely off the `gem fetch` command. + version = "= #{gem_version}" + dep = Gem::Dependency.new gem_name, version + + specs_and_sources, errors = + Gem::SpecFetcher.fetcher.spec_for_dependency dep + + # There should never be more than one item in specs_and_sources, + # since we search for an exact version. + spec, source = specs_and_sources[0] + + if spec.nil? + show_lookup_failure gem_name, version, errors, options[:domain] + terminate_interaction 1 + end + + download_path = source.download spec + + FileUtils.move(download_path, old_file) + + say "Downloaded #{gem_name} version #{gem_version} as #{old_file}." + end + + def rubygems_version(gem_file) + Gem::Package.new(gem_file).spec.rubygems_version + end +end diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb index 3f8f7e13f2..50e161ac9b 100644 --- a/lib/rubygems/commands/search_command.rb +++ b/lib/rubygems/commands/search_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../query_utils" @@ -7,8 +8,8 @@ class Gem::Commands::SearchCommand < Gem::Command def initialize super "search", "Display remote gems whose name matches REGEXP", - :domain => :remote, :details => false, :versions => true, - :installed => nil, :version => Gem::Requirement.default + domain: :remote, details: false, versions: true, + installed: nil, version: Gem::Requirement.default add_query_options end diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb index 56be07c79d..f1dde4aa02 100644 --- a/lib/rubygems/commands/server_command.rb +++ b/lib/rubygems/commands/server_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" unless defined? Gem::Commands::ServerCommand diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb index c779b7c244..3f38074280 100644 --- a/lib/rubygems/commands/setup_command.rb +++ b/lib/rubygems/commands/setup_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" ## @@ -6,19 +7,19 @@ require_relative "../command" # RubyGems checkout or tarball. class Gem::Commands::SetupCommand < Gem::Command - HISTORY_HEADER = /^#\s*[\d.a-zA-Z]+\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze - VERSION_MATCHER = /^#\s*([\d.a-zA-Z]+)\s*\/\s*\d{4}-\d{2}-\d{2}\s*$/.freeze + HISTORY_HEADER = %r{^#\s*[\d.a-zA-Z]+\s*/\s*\d{4}-\d{2}-\d{2}\s*$} + VERSION_MATCHER = %r{^#\s*([\d.a-zA-Z]+)\s*/\s*\d{4}-\d{2}-\d{2}\s*$} ENV_PATHS = %w[/usr/bin/env /bin/env].freeze def initialize super "setup", "Install RubyGems", - :format_executable => false, :document => %w[ri], - :force => true, - :site_or_vendor => "sitelibdir", - :destdir => "", :prefix => "", :previous_version => "", - :regenerate_binstubs => true, - :regenerate_plugins => true + format_executable: false, document: %w[ri], + force: true, + site_or_vendor: "sitelibdir", + destdir: "", prefix: "", previous_version: "", + regenerate_binstubs: true, + regenerate_plugins: true add_option "--previous-version=VERSION", "Previous version of RubyGems", @@ -54,9 +55,9 @@ class Gem::Commands::SetupCommand < Gem::Command "List the documentation types you wish to", "generate. For example: rdoc,ri" do |value, options| options[:document] = case value - when nil then %w[rdoc ri] - when false then [] - else value + when nil then %w[rdoc ri] + when false then [] + else value end end @@ -133,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 @@ -242,9 +243,9 @@ By default, this RubyGems will install gem as: end def install_executables(bin_dir) - prog_mode = options[:prog_mode] || 0755 + prog_mode = options[:prog_mode] || 0o755 - executables = { "gem" => "bin" } + executables = { "gem" => "exe" } executables.each do |tool, path| say "Installing #{tool} executable" if @verbose @@ -264,7 +265,7 @@ By default, this RubyGems will install gem as: fp.puts bin.join end - install bin_tmp_file, dest_file, :mode => prog_mode + install bin_tmp_file, dest_file, mode: prog_mode bin_file_names << dest_file ensure rm bin_tmp_file @@ -286,7 +287,7 @@ By default, this RubyGems will install gem as: TEXT end - install bin_cmd_file, "#{dest_file}.bat", :mode => prog_mode + install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode ensure rm bin_cmd_file end @@ -356,7 +357,7 @@ 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) @@ -368,18 +369,21 @@ By default, this RubyGems will install gem as: File.dirname(loaded_from) else target_specs_dir = File.join(default_dir, "specifications", "default") - mkdir_p target_specs_dir, :mode => 0755 + mkdir_p target_specs_dir, mode: 0o755 target_specs_dir end - bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } - default_spec_path = File.join(specs_dir, "#{bundler_spec.full_name}.gemspec") - Gem.write_binary(default_spec_path, bundler_spec.to_ruby) + new_bundler_spec = Dir.chdir("bundler") { Gem::Specification.load("bundler.gemspec") } + full_name = new_bundler_spec.full_name + gemspec_path = "#{full_name}.gemspec" + + default_spec_path = File.join(specs_dir, gemspec_path) + Gem.write_binary(default_spec_path, new_bundler_spec.to_ruby) bundler_spec = Gem::Specification.load(default_spec_path) # Remove gemspec that was same version of vendored bundler. - normal_gemspec = File.join(default_dir, "specifications", "bundler-#{bundler_spec.version}.gemspec") + normal_gemspec = File.join(default_dir, "specifications", gemspec_path) if File.file? normal_gemspec File.delete normal_gemspec end @@ -387,20 +391,14 @@ By default, this RubyGems will install gem as: # Remove gem files that were same version of vendored bundler. if File.directory? bundler_spec.gems_dir Dir.entries(bundler_spec.gems_dir). - select {|default_gem| File.basename(default_gem) == "bundler-#{bundler_spec.version}" }. + select {|default_gem| File.basename(default_gem) == full_name }. each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) } end - bundler_bin_dir = bundler_spec.bin_dir - mkdir_p bundler_bin_dir, :mode => 0755 - bundler_spec.executables.each do |e| - cp File.join("bundler", bundler_spec.bindir, e), File.join(bundler_bin_dir, e) - end - require_relative "../installer" Dir.chdir("bundler") do - built_gem = Gem::Package.build(bundler_spec) + built_gem = Gem::Package.build(new_bundler_spec) begin Gem::Installer.at( built_gem, @@ -417,9 +415,9 @@ By default, this RubyGems will install gem as: end end - bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } + new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) } - say "Bundler #{bundler_spec.version} installed" + say "Bundler #{new_bundler_spec.version} installed" end def make_destination_dirs @@ -429,10 +427,10 @@ By default, this RubyGems will install gem as: lib_dir, bin_dir = generate_default_dirs end - mkdir_p lib_dir, :mode => 0755 - mkdir_p bin_dir, :mode => 0755 + mkdir_p lib_dir, mode: 0o755 + mkdir_p bin_dir, mode: 0o755 - return lib_dir, bin_dir + [lib_dir, bin_dir] end def generate_default_man_dir @@ -575,8 +573,8 @@ abort "#{deprecation_message}" def uninstall_old_gemcutter require_relative "../uninstaller" - ui = Gem::Uninstaller.new("gemcutter", :all => true, :ignore => true, - :version => "< 0.4") + ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true, + version: "< 0.4") ui.uninstall rescue Gem::InstallError end @@ -638,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) diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb index 2660eee4f3..0f77908c5b 100644 --- a/lib/rubygems/commands/signin_command.rb +++ b/lib/rubygems/commands/signin_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../gemcutter_utilities" diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb index fa688ea3f8..bdd01e4393 100644 --- a/lib/rubygems/commands/signout_command.rb +++ b/lib/rubygems/commands/signout_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::SignoutCommand < Gem::Command diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb index 5a8f5af9c3..976f4a4ea2 100644 --- a/lib/rubygems/commands/sources_command.rb +++ b/lib/rubygems/commands/sources_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../remote_fetcher" require_relative "../spec_fetcher" @@ -58,7 +59,7 @@ 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 @@ -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 && uri.scheme.downcase == "http" && - uri.host.downcase == "rubygems.org" + if uri.scheme && uri.scheme.casecmp("http").zero? && + uri.host.casecmp("rubygems.org").zero? question = <<-QUESTION.chomp https://rubygems.org is recommended for security over #{uri} @@ -98,16 +99,16 @@ Do you want to add this insecure source? path = Gem.spec_cache_dir FileUtils.rm_rf path - unless File.exist? path - say "*** Removed specs cache ***" - else - unless File.writable? path - say "*** Unable to remove source cache (write protected) ***" - else + if File.exist? path + if File.writable? path say "*** Unable to remove source cache ***" + else + say "*** Unable to remove source cache (write protected) ***" end terminate_interaction 1 + else + say "*** Removed specs cache ***" end end @@ -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 diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb index 12004a6d56..a21ed35be3 100644 --- a/lib/rubygems/commands/specification_command.rb +++ b/lib/rubygems/commands/specification_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -12,27 +13,27 @@ class Gem::Commands::SpecificationCommand < Gem::Command Gem.load_yaml super "specification", "Display gem specification (in yaml)", - :domain => :local, :version => Gem::Requirement.default, - :format => :yaml + domain: :local, version: Gem::Requirement.default, + format: :yaml add_version_option("examine") add_platform_option add_prerelease_option add_option("--all", "Output specifications for all versions of", - "the gem") do |value, options| + "the gem") do |_value, options| options[:all] = true end - add_option("--ruby", "Output ruby format") do |value, options| + add_option("--ruby", "Output ruby format") do |_value, options| options[:format] = :ruby end - add_option("--yaml", "Output YAML format") do |value, options| + add_option("--yaml", "Output YAML format") do |_value, options| options[:format] = :yaml end - add_option("--marshal", "Output Marshal format") do |value, options| + add_option("--marshal", "Output Marshal format") do |_value, options| options[:format] = :marshal end @@ -106,7 +107,11 @@ Specific fields in the specification can be extracted in YAML format: if local? if File.exist? gem - specs << Gem::Package.new(gem).spec rescue nil + begin + specs << Gem::Package.new(gem).spec + rescue StandardError + nil + end end if specs.empty? @@ -133,16 +138,16 @@ Specific fields in the specification can be extracted in YAML format: end unless options[:all] - specs = [specs.max_by {|s| s.version }] + specs = [specs.max_by(&:version)] end specs.each do |s| s = s.send field if field say case options[:format] - when :ruby then s.to_ruby - when :marshal then Marshal.dump s - else s.to_yaml + when :ruby then s.to_ruby + when :marshal then Marshal.dump s + else s.to_yaml end say "\n" diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb index 0246f42e3e..0be2b85159 100644 --- a/lib/rubygems/commands/stale_command.rb +++ b/lib/rubygems/commands/stale_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::StaleCommand < Gem::Command @@ -17,7 +18,7 @@ longer using. end def usage # :nodoc: - "#{program_name}" + program_name.to_s end def execute @@ -33,7 +34,7 @@ longer using. end gem_to_atime.sort_by {|_, atime| atime }.each do |name, atime| - say "#{name} at #{atime.strftime '%c'}" + say "#{name} at #{atime.strftime "%c"}" end end end diff --git a/lib/rubygems/commands/uninstall_command.rb b/lib/rubygems/commands/uninstall_command.rb index 3c520826e5..2a77ec72cf 100644 --- a/lib/rubygems/commands/uninstall_command.rb +++ b/lib/rubygems/commands/uninstall_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../uninstaller" @@ -14,12 +15,11 @@ class Gem::Commands::UninstallCommand < Gem::Command def initialize super "uninstall", "Uninstall gems from the local repository", - :version => Gem::Requirement.default, :user_install => true, - :check_dev => false, :vendor => false + version: Gem::Requirement.default, user_install: true, + check_dev: false, vendor: false add_option("-a", "--[no-]all", - "Uninstall all matching versions" - ) do |value, options| + "Uninstall all matching versions") do |value, options| options[:all] = value end @@ -79,7 +79,7 @@ class Gem::Commands::UninstallCommand < Gem::Command add_option("--vendor", "Uninstall gem from the vendor directory.", - "Only for use by gem repackagers.") do |value, options| + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end @@ -95,7 +95,7 @@ class Gem::Commands::UninstallCommand < Gem::Command end def defaults_str # :nodoc: - "--version '#{Gem::Requirement.default}' --no-force " + + "--version '#{Gem::Requirement.default}' --no-force " \ "--user-install" end @@ -125,6 +125,9 @@ that is a dependency of an existing gem. You can use the def execute check_version + # Consider only gem specifications installed at `--install-dir` + Gem::Specification.dirs = options[:install_dir] if options[:install_dir] + if options[:all] && !options[:args].empty? uninstall_specific elsif options[:all] @@ -135,7 +138,7 @@ that is a dependency of an existing gem. You can use the end def uninstall_all - specs = Gem::Specification.reject {|spec| spec.default_gem? } + specs = Gem::Specification.reject(&:default_gem?) specs.each do |spec| options[:version] = spec.version @@ -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 b1f939b0bc..c2fc720297 100644 --- a/lib/rubygems/commands/unpack_command.rb +++ b/lib/rubygems/commands/unpack_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../version_option" require_relative "../security_option" @@ -20,15 +21,15 @@ class Gem::Commands::UnpackCommand < Gem::Command require "fileutils" super "unpack", "Unpack an installed gem to the current directory", - :version => Gem::Requirement.default, - :target => Dir.pwd + version: Gem::Requirement.default, + target: Dir.pwd add_option("--target=DIR", "target directory for unpacking") do |value, options| options[:target] = value end - add_option("--spec", "unpack the gem specification") do |value, options| + add_option("--spec", "unpack the gem specification") do |_value, options| options[:spec] = true end @@ -95,12 +96,10 @@ command help for an example. FileUtils.mkdir_p @options[:target] if @options[:target] - destination = begin - if @options[:target] - File.join @options[:target], spec_file - else - spec_file - end + destination = if @options[:target] + File.join @options[:target], spec_file + else + spec_file end File.open destination, "w" do |io| @@ -131,7 +130,7 @@ command help for an example. return this_path if File.exist? this_path end - return nil + nil end ## @@ -144,24 +143,18 @@ command help for an example. # get_path 'rake', '< 0.1' # nil # get_path 'rak' # nil (exact name required) #-- - # TODO: This should be refactored so that it's a general service. I don't - # think any of our existing classes are the right place though. Just maybe - # 'Cache'? - # - # TODO: It just uses Gem.dir for now. What's an easy way to get the list of - # source directories? def get_path(dependency) - return dependency.name if dependency.name =~ /\.gem$/i + return dependency.name if /\.gem$/i.match?(dependency.name) specs = dependency.matching_specs - selected = specs.max_by {|s| s.version } + selected = specs.max_by(&:version) return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless selected - return unless dependency.name =~ /^#{selected.name}$/i + return unless /^#{selected.name}$/i.match?(dependency.name) # We expect to find (basename).gem in the 'cache' directory. Furthermore, # the name match must be exact (ignoring case). diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb index 5c90981645..3d6fecaa40 100644 --- a/lib/rubygems/commands/update_command.rb +++ b/lib/rubygems/commands/update_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../command_manager" require_relative "../dependency_installer" @@ -20,7 +21,7 @@ class Gem::Commands::UpdateCommand < Gem::Command def initialize options = { - :force => false, + force: false, } options.merge!(install_update_options) @@ -36,10 +37,10 @@ class Gem::Commands::UpdateCommand < Gem::Command end add_option("--system [VERSION]", Gem::Version, - "Update the RubyGems system software") do |value, options| - value = true unless value + "Update the RubyGems system software") do |value, opts| + value ||= true - options[:system] = value + opts[:system] = value end add_local_remote_options @@ -119,7 +120,7 @@ command to remove old versions. updated = update_gems gems_to_update installed_names = highest_installed_gems.keys - updated_names = updated.map {|spec| spec.name } + updated_names = updated.map(&:name) not_updated_names = options[:args].uniq - updated_names not_installed_names = not_updated_names - installed_names up_to_date_names = not_updated_names - not_installed_names @@ -127,10 +128,10 @@ command to remove old versions. if updated.empty? say "Nothing to update" else - say "Gems updated: #{updated_names.join(' ')}" + say "Gems updated: #{updated_names.join(" ")}" end - say "Gems already up-to-date: #{up_to_date_names.join(' ')}" unless up_to_date_names.empty? - say "Gems not currently installed: #{not_installed_names.join(' ')}" unless not_installed_names.empty? + say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty? + say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty? end def fetch_remote_gems(spec) # :nodoc: @@ -185,7 +186,9 @@ command to remove old versions. system Gem.ruby, "--disable-gems", "setup.rb", *args end - say "RubyGems system software updated" if installed unless options[:silent] + unless options[:silent] + say "RubyGems system software updated" if installed + end end end @@ -230,7 +233,7 @@ command to remove old versions. highest_remote_tup = highest_remote_name_tuple(rubygems_update) target = highest_remote_tup ? highest_remote_tup.version : version - return target, requirement + [target, requirement] end def update_gem(name, version = Gem::Requirement.default) @@ -241,7 +244,7 @@ command to remove old versions. @installer = Gem::DependencyInstaller.new update_options - say "Updating #{name}" unless options[:system] && options[:silent] + say "Updating #{name}" unless options[:system] begin @installer.install name, Gem::Requirement.new(version) rescue Gem::InstallError, Gem::DependencyError => e @@ -279,7 +282,7 @@ command to remove old versions. check_oldest_rubygems version installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement - installed_gems = update_gem("rubygems-update", version) if installed_gems.empty? || installed_gems.first.version != version + installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version return if installed_gems.empty? install_rubygems installed_gems.first @@ -291,16 +294,14 @@ command to remove old versions. args << "--prefix" << Gem.prefix if Gem.prefix args << "--no-document" unless options[:document].include?("rdoc") || options[:document].include?("ri") args << "--no-format-executable" if options[:no_format_executable] - args << "--previous-version" << Gem::VERSION if - options[:system] == true || - Gem::Version.new(options[:system]) >= Gem::Version.new(2) + args << "--previous-version" << Gem::VERSION args end def which_to_update(highest_installed_gems, gem_names) result = [] - highest_installed_gems.each do |l_name, l_spec| + highest_installed_gems.each do |_l_name, l_spec| next if !gem_names.empty? && gem_names.none? {|name| name == l_spec.name } @@ -325,12 +326,8 @@ command to remove old versions. @oldest_supported_version ||= if Gem.ruby_version > Gem::Version.new("3.1.a") Gem::Version.new("3.3.3") - elsif Gem.ruby_version > Gem::Version.new("3.0.a") - Gem::Version.new("3.2.3") - elsif Gem.ruby_version > Gem::Version.new("2.7.a") - Gem::Version.new("3.1.2") else - Gem::Version.new("3.0.1") + 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 5b9a79b734..5ed4d9d142 100644 --- a/lib/rubygems/commands/which_command.rb +++ b/lib/rubygems/commands/which_command.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true + require_relative "../command" class Gem::Commands::WhichCommand < Gem::Command def initialize super "which", "Find the location of a library file you can require", - :search_gems_first => false, :show_all => false + search_gems_first: false, show_all: false add_option "-a", "--[no-]all", "show all matching files" do |show_all, options| options[:show_all] = show_all diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb index 1499f72f5d..fbdc262549 100644 --- a/lib/rubygems/commands/yank_command.rb +++ b/lib/rubygems/commands/yank_command.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../command" require_relative "../local_remote_options" require_relative "../version_option" @@ -61,7 +62,7 @@ data you will need to change them immediately and yank your gem. end def yank_gem(version, platform) - say "Yanking gem from #{self.host}..." + say "Yanking gem from #{host}..." args = [:delete, version, platform, "api/v1/gems/yank"] response = yank_api_request(*args) @@ -88,7 +89,7 @@ data you will need to change them immediately and yank your gem. def get_version_from_requirements(requirements) requirements.requirements.first[1].version - rescue + rescue StandardError nil end diff --git a/lib/rubygems/compatibility.rb b/lib/rubygems/compatibility.rb index b4c1ef16fa..0d9df56f8a 100644 --- a/lib/rubygems/compatibility.rb +++ b/lib/rubygems/compatibility.rb @@ -26,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 4aa8b4d33a..6f83fe2c79 100644 --- a/lib/rubygems/config_file.rb +++ b/lib/rubygems/config_file.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -46,6 +47,8 @@ class Gem::ConfigFile 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 @@ -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,15 +190,16 @@ class Gem::ConfigFile @update_sources = DEFAULT_UPDATE_SOURCES @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS + @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS) platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS) system_config = load_file SYSTEM_WIDE_CONFIG_FILE - user_config = load_file config_file_name.dup.tap(&Gem::UNTAINT) + user_config = load_file config_file_name - environment_config = (ENV["GEMRC"] || "") - .split(File::PATH_SEPARATOR).inject({}) do |result, file| + environment_config = (ENV["GEMRC"] || ""). + split(File::PATH_SEPARATOR).inject({}) do |result, file| result.merge load_file file end @@ -201,7 +210,7 @@ class Gem::ConfigFile @hash = @hash.merge environment_config end - # HACK these override command-line args, which is bad + # 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 @@ -211,6 +220,7 @@ class Gem::ConfigFile @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server @sources = @hash[:sources] if @hash.key? :sources @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 @@ -240,9 +250,9 @@ class Gem::ConfigFile return if Gem.win_platform? # windows doesn't write 0600 as 0600 return unless File.exist? credentials_path - existing_permissions = File.stat(credentials_path).mode & 0777 + existing_permissions = File.stat(credentials_path).mode & 0o777 - return if existing_permissions == 0600 + return if existing_permissions == 0o600 alert_error <<-ERROR Your gem push credentials file located at: @@ -323,11 +333,9 @@ if you believe they were disclosed to a third party. require "fileutils" FileUtils.mkdir_p(dirname) - Gem.load_yaml - - permissions = 0600 & (~File.umask) + permissions = 0o600 & (~File.umask) File.open(credentials_path, "w", permissions) do |f| - f.write config.to_yaml + f.write self.class.dump_with_rubygems_yaml(config) end load_api_keys # reload @@ -343,20 +351,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 @@ -467,6 +473,9 @@ if you believe they were disclosed to a third party. yaml_hash[:concurrent_downloads] = @hash.fetch(:concurrent_downloads, DEFAULT_CONCURRENT_DOWNLOADS) + yaml_hash[:install_extension_in_lib] = + @hash.fetch(:install_extension_in_lib, DEFAULT_INSTALL_EXTENSION_IN_LIB) + yaml_hash[:ssl_verify_mode] = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode @@ -476,17 +485,17 @@ if you believe they were disclosed to a third party. yaml_hash[:ssl_client_cert] = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert - keys = yaml_hash.keys.map {|key| key.to_s } + keys = yaml_hash.keys.map(&:to_s) keys << "debug" re = Regexp.union(*keys) @hash.each do |key, value| key = key.to_s - next if key =~ re + next if key&.match?(re) yaml_hash[key.to_s] = value end - yaml_hash.to_yaml + self.class.dump_with_rubygems_yaml(yaml_hash) end # Writes out this config file, replacing its source. @@ -521,6 +530,57 @@ if you believe they were disclosed to a third party. attr_reader :hash protected :hash + def self.dump_with_rubygems_yaml(content) + content.transform_keys! do |k| + k.is_a?(Symbol) ? ":#{k}" : k + end + + require_relative "yaml_serializer" + Gem::YAMLSerializer.dump(content) + end + + def self.load_with_rubygems_config_hash(yaml) + require_relative "yaml_serializer" + + content = Gem::YAMLSerializer.load(yaml) + + 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) @@ -533,7 +593,7 @@ if you believe they were disclosed to a third party. need_config_file_name = false elsif arg =~ /^--config-file=(.*)/ @config_file_name = $1 - elsif arg =~ /^--config-file$/ + elsif /^--config-file$/.match?(arg) need_config_file_name = true end end diff --git a/lib/rubygems/core_ext/kernel_gem.rb b/lib/rubygems/core_ext/kernel_gem.rb index b2f97b9ed9..4e09b95c44 100644 --- a/lib/rubygems/core_ext/kernel_gem.rb +++ b/lib/rubygems/core_ext/kernel_gem.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true module Kernel - ## # Use Kernel#gem to activate a specific version of +gem_name+. # @@ -37,9 +36,9 @@ module Kernel skip_list = (ENV["GEM_SKIP"] || "").split(/:/) raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name - if gem_name.kind_of? Gem::Dependency + if gem_name.is_a? Gem::Dependency unless Gem::Deprecate.skip - warn "#{Gem.location_of_caller.join ':'}:Warning: Kernel.gem no longer "\ + warn "#{Gem.location_of_caller.join ":"}:Warning: Kernel.gem no longer "\ "accepts a Gem::Dependency object, please pass the name "\ "and requirements directly" end @@ -66,5 +65,4 @@ module Kernel end private :gem - end diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb index b92d6f9965..073966b696 100644 --- a/lib/rubygems/core_ext/kernel_require.rb +++ b/lib/rubygems/core_ext/kernel_require.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -8,13 +9,12 @@ require "monitor" module Kernel - RUBYGEMS_ACTIVATION_MONITOR = Monitor.new # :nodoc: # Make sure we have a reference to Ruby's original Kernel#require unless defined?(gem_original_require) # :stopdoc: - alias gem_original_require require + alias_method :gem_original_require, :require private :gem_original_require # :startdoc: end @@ -34,141 +34,116 @@ module Kernel # that file has already been loaded is preserved. def require(path) # :doc: - if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?) - monitor_owned = RUBYGEMS_ACTIVATION_MONITOR.mon_owned? - end - RUBYGEMS_ACTIVATION_MONITOR.enter - - path = path.to_path if path.respond_to? :to_path - - if spec = Gem.find_unresolved_default_spec(path) - # Ensure -I beats a default gem - resolved_path = begin - rp = nil - load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths - Gem.suffixes.each do |s| - $LOAD_PATH[0...load_path_check_index].each do |lp| - safe_lp = lp.dup.tap(&Gem::UNTAINT) - begin - if File.symlink? safe_lp # for backward compatibility + return gem_original_require(path) unless Gem.discover_gems_on_require + + RUBYGEMS_ACTIVATION_MONITOR.synchronize do + path = File.path(path) + + # If +path+ belongs to a default gem, we activate it and then go straight + # to normal require + + if spec = Gem.find_default_spec(path) + name = spec.name + + next if Gem.loaded_specs[name] + + # Ensure -I beats a default gem + resolved_path = begin + rp = nil + load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths + Gem.suffixes.find do |s| + $LOAD_PATH[0...load_path_check_index].find do |lp| + if File.symlink? lp # for backward compatibility next end - rescue SecurityError - RUBYGEMS_ACTIVATION_MONITOR.exit - raise - end - full_path = File.expand_path(File.join(safe_lp, "#{path}#{s}")) - if File.file?(full_path) - rp = full_path - break + full_path = File.expand_path(File.join(lp, "#{path}#{s}")) + rp = full_path if File.file?(full_path) end end - break if rp + rp end - rp - end - begin - Kernel.send(:gem, spec.name, Gem::Requirement.default_prerelease) - rescue Exception - RUBYGEMS_ACTIVATION_MONITOR.exit - raise - end unless resolved_path - end + 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 - if load_error.path == path - RUBYGEMS_ACTIVATION_MONITOR.enter + if names.size > 1 + raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ", "}" + end - begin - require_again = Gem.try_activate(path) - ensure - RUBYGEMS_ACTIVATION_MONITOR.exit - end + # Ok, now find a gem that has no conflicts, starting + # at the highest version. + valid = found_specs.find {|s| !s.has_conflicts? } - return gem_original_require(path) if require_again + unless valid + le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate" + le.name = names.first + raise le + end + + valid.activate + end end - raise load_error - ensure - if RUBYGEMS_ACTIVATION_MONITOR.respond_to?(:mon_owned?) - if monitor_owned != (ow = RUBYGEMS_ACTIVATION_MONITOR.mon_owned?) - STDERR.puts [$$, Thread.current, $!, $!.backtrace].inspect if $! - raise "CRITICAL: RUBYGEMS_ACTIVATION_MONITOR.owned?: before #{monitor_owned} -> after #{ow}" + begin + gem_original_require(path) + rescue LoadError => load_error + if load_error.path == path && + RUBYGEMS_ACTIVATION_MONITOR.synchronize { Gem.try_activate(path) } + + return gem_original_require(path) end + + raise load_error end end private :require - end diff --git a/lib/rubygems/core_ext/kernel_warn.rb b/lib/rubygems/core_ext/kernel_warn.rb index 1f4c77f04b..9dc9f2218c 100644 --- a/lib/rubygems/core_ext/kernel_warn.rb +++ b/lib/rubygems/core_ext/kernel_warn.rb @@ -35,11 +35,10 @@ module Kernel start += 1 - if path = loc.path - unless path.start_with?(rubygems_path) || path.start_with?("<internal:") - # Non-rubygems frames - uplevel -= 1 - end + next unless path = loc.path + unless path.start_with?(rubygems_path, "<internal:") + # Non-rubygems frames + uplevel -= 1 end end kw[:uplevel] = start diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb index c9e0a92953..018c49dbeb 100644 --- a/lib/rubygems/core_ext/tcpsocket_init.rb +++ b/lib/rubygems/core_ext/tcpsocket_init.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "socket" module CoreExtensions @@ -17,7 +19,7 @@ module CoreExtensions cond_var = Thread::ConditionVariable.new Addrinfo.foreach(host, serv, nil, :STREAM) do |addr| - Thread.report_on_exception = false if defined? Thread.report_on_exception = () + Thread.report_on_exception = false threads << Thread.new(addr) do # give head start to ipv6 addresses diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb index 4806ea6469..1bd208feb9 100644 --- a/lib/rubygems/defaults.rb +++ b/lib/rubygems/defaults.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Gem DEFAULT_HOST = "https://rubygems.org" @@ -23,7 +24,7 @@ module Gem default_spec_cache_dir = File.join Gem.user_home, ".gem", "specs" unless File.exist?(default_spec_cache_dir) - default_spec_cache_dir = File.join Gem.data_home, "gem", "specs" + default_spec_cache_dir = File.join Gem.cache_home, "gem", "specs" end default_spec_cache_dir @@ -79,7 +80,7 @@ module Gem def self.find_home Dir.home.dup - rescue + rescue StandardError if Gem.win_platform? File.expand_path File.join(ENV["HOMEDRIVE"] || ENV["SystemDrive"], "/") else @@ -93,7 +94,7 @@ module Gem # The home directory for the user. def self.user_home - @user_home ||= find_home.tap(&Gem::UNTAINT) + @user_home ||= find_home end ## @@ -111,7 +112,7 @@ module Gem # The path to standard location of the user's configuration directory. def self.config_home - @config_home ||= (ENV["XDG_CONFIG_HOME"] || File.join(Gem.user_home, ".config")) + @config_home ||= ENV["XDG_CONFIG_HOME"] || File.join(Gem.user_home, ".config") end ## @@ -130,35 +131,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").tap(&Gem::UNTAINT) + @state_file ||= File.join(Gem.state_home, "gem", "last_update_check") end ## # The path to standard location of the user's cache directory. def self.cache_home - @cache_home ||= (ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache")) + @cache_home ||= ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache") end ## # The path to standard location of the user's data directory. def self.data_home - @data_home ||= (ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share")) + @data_home ||= ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share") end ## # The path to standard location of the user's state directory. def self.state_home - @data_home ||= (ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state")) + @state_home ||= ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state") end ## @@ -183,7 +184,11 @@ module Gem # Deduce Ruby's --program-prefix and --program-suffix from its install name def self.default_exec_format - exec_format = RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s") rescue "%s" + exec_format = begin + RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s") + rescue StandardError + "%s" + end unless exec_format.include?("%s") raise Gem::Exception, @@ -231,10 +236,22 @@ module Gem end ## + # Enables automatic installation into user directory + + def self.default_user_install # :nodoc: + if !ENV.key?("GEM_HOME") && (File.exist?(Gem.dir) && !File.writable?(Gem.dir)) + Gem.ui.say "Defaulting to user installation because default installation directory (#{Gem.dir}) is not writable." + return true + end + + false + end + + ## # Install extensions into lib as well as into the extension directory. def self.install_extension_in_lib # :nodoc: - true + Gem.configuration.install_extension_in_lib end ## diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb index cd03e7e299..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 @@ -168,16 +167,16 @@ class Gem::Dependency def ==(other) # :nodoc: Gem::Dependency === other && - self.name == other.name && - self.type == other.type && - self.requirement == other.requirement + name == other.name && + type == other.type && + requirement == other.requirement end ## # Dependencies are ordered by name. def <=>(other) - self.name <=> other.name + name <=> other.name end ## @@ -204,7 +203,7 @@ class Gem::Dependency requirement.satisfied_by? version end - alias === =~ + alias_method :===, :=~ ## # :call-seq: @@ -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 @@ -323,15 +322,15 @@ class Gem::Dependency end def to_spec - matches = self.to_specs.compact + matches = to_specs.compact - active = matches.find {|spec| spec.activated? } + active = matches.find(&:activated?) return active if active unless prerelease? - # Move prereleases to the end of the list for >= 0 requirements + # Consider prereleases only as a fallback pre, matches = matches.partition {|spec| spec.version.prerelease? } - matches += pre if requirement == Gem::Requirement.default + matches = pre if matches.empty? end matches.first diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb index 1009376b90..b119dca1cf 100644 --- a/lib/rubygems/dependency_installer.rb +++ b/lib/rubygems/dependency_installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "dependency_list" require_relative "package" @@ -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] @@ -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 @@ -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 @@ -293,13 +291,11 @@ class Gem::DependencyInstaller src = Gem::Source::SpecificFile.new dep_or_name installer_set.add_local dep_or_name, src.spec, src version = src.spec.version if version == Gem::Requirement.default - elsif dep_or_name =~ /\.gem$/ + elsif dep_or_name =~ /\.gem$/ # rubocop:disable Performance/RegexpMatch Dir[dep_or_name].each do |name| - begin - src = Gem::Source::SpecificFile.new name - installer_set.add_local dep_or_name, src.spec, src - rescue Gem::Package::FormatError - end + src = Gem::Source::SpecificFile.new name + installer_set.add_local dep_or_name, src.spec, src + rescue Gem::Package::FormatError end # else This is a dependency. InstallerSet handles this case end diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb index eaf6702177..ad5e59e8c1 100644 --- a/lib/rubygems/dependency_list.rb +++ b/lib/rubygems/dependency_list.rb @@ -1,11 +1,12 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require_relative "tsort" +require_relative "vendored_tsort" require_relative "deprecate" ## @@ -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 ## diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb index 5fe0afb6b0..58a6c5b7dc 100644 --- a/lib/rubygems/deprecate.rb +++ b/lib/rubygems/deprecate.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Provides 3 methods for declaring when something is going away. # @@ -69,7 +70,6 @@ # end module Gem::Deprecate - def self.skip # :nodoc: @skip ||= false end @@ -82,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 @@ -103,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 @@ -128,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 @@ -143,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 96829227fc..56b7c081eb 100644 --- a/lib/rubygems/doctor.rb +++ b/lib/rubygems/doctor.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "user_interaction" @@ -32,7 +33,7 @@ class Gem::Doctor Gem::REPOSITORY_SUBDIRECTORIES.sort - REPOSITORY_EXTENSION_MAP.map {|(k,_)| k }.sort - raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ', '}" unless + raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ", "}" unless missing.empty? ## @@ -52,7 +53,7 @@ class Gem::Doctor # Specs installed in this gem repository def installed_specs # :nodoc: - @installed_specs ||= Gem::Specification.map {|s| s.full_name } + @installed_specs ||= Gem::Specification.map(&:full_name) end ## @@ -74,7 +75,7 @@ class Gem::Doctor Gem.use_paths @gem_repository.to_s unless gem_repository? - say "This directory does not appear to be a RubyGems repository, " + + say "This directory does not appear to be a RubyGems repository, " \ "skipping" say return @@ -103,16 +104,16 @@ class Gem::Doctor directory = File.join(@gem_repository, sub_directory) Dir.entries(directory).sort.each do |ent| - next if ent == "." || ent == ".." + next if [".", ".."].include?(ent) child = File.join(directory, ent) next unless File.exist?(child) basename = File.basename(child, extension) next if installed_specs.include? basename - next if /^rubygems-\d/ =~ basename - next if "specifications" == sub_directory && "default" == basename - next if "plugins" == sub_directory && Gem.plugin_suffix_regexp =~ (basename) + next if /^rubygems-\d/.match?(basename) + next if sub_directory == "specifications" && basename == "default" + next if sub_directory == "plugins" && Gem.plugin_suffix_regexp =~ (basename) type = File.directory?(child) ? "directory" : "file" diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb index ac82a551a5..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. @@ -60,7 +61,7 @@ module Gem def build_message names = specs.map(&:full_name) - "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ','}]\n" + "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ","}]\n" end end @@ -133,11 +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 @@ -174,6 +171,6 @@ module Gem ## # The "exception" alias allows you to call raise on a SourceFetchProblem. - alias exception error + alias_method :exception, :error end end diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb index ca4fbb20de..0308b4687f 100644 --- a/lib/rubygems/exceptions.rb +++ b/lib/rubygems/exceptions.rb @@ -172,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 @@ -214,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 @@ -221,7 +232,7 @@ class Gem::SystemExitException < SystemExit ## # The exit code for the process - alias exit_code status + alias_method :exit_code, :status ## # Creates a new SystemExitException with the given +exit_code+ @@ -254,7 +265,7 @@ class Gem::UnsatisfiableDependencyError < Gem::DependencyError def initialize(dep, platform_mismatch=nil) if platform_mismatch && !platform_mismatch.empty? plats = platform_mismatch.map {|x| x.platform.to_s }.sort.uniq - super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(', ')}" + super "Unable to resolve dependency: No match for '#{dep}' on this platform. Found: #{plats.join(", ")}" else if dep.explicit? super "Unable to resolve dependency: user requested '#{dep}'" @@ -281,9 +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: -Gem.deprecate_constant :UnsatisfiableDepedencyError diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb index d714985c21..b5ca126a08 100644 --- a/lib/rubygems/ext.rb +++ b/lib/rubygems/ext.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb index 727bc065c2..0329c1eec3 100644 --- a/lib/rubygems/ext/build_error.rb +++ b/lib/rubygems/ext/build_error.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Raised when there is an error while building extensions. diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb index 98d354183c..be1ba3031c 100644 --- a/lib/rubygems/ext/builder.rb +++ b/lib/rubygems/ext/builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -6,6 +7,7 @@ #++ require_relative "../user_interaction" +require_relative "../shellwords" class Gem::Ext::Builder include Gem::UserInteraction @@ -25,19 +27,17 @@ class Gem::Ext::Builder # try to find make program from Ruby configure arguments first RbConfig::CONFIG["configure_args"] =~ /with-make-prog\=(\w+)/ make_program_name = ENV["MAKE"] || ENV["make"] || $1 - unless make_program_name - make_program_name = (RUBY_PLATFORM.include?("mswin")) ? "nmake" : "make" - end + make_program_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"] != "") ? "DESTDIR=%s" % ENV["DESTDIR"] : "" + destdir = /\bnmake/i !~ make_program_name || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : "" env = [destdir] if sitedir - env << "sitearchdir=%s" % sitedir - env << "sitelibdir=%s" % sitedir + env << format("sitearchdir=%s", sitedir) + env << format("sitelibdir=%s", sitedir) end targets.each do |target| @@ -55,30 +55,53 @@ class Gem::Ext::Builder end end + 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}" - require "shellwords" - results << command.shelljoin + results << Shellwords.join(command) require "open3" # Set $SOURCE_DATE_EPOCH for the subprocess. build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env) output, status = begin - Open3.capture2e(build_env, *command, :chdir => dir) - rescue => error + Open3.popen2e(build_env, *command, chdir: dir) do |_stdin, stdouterr, wait_thread| + 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 @@ -131,8 +154,7 @@ class Gem::Ext::Builder when /CMakeLists.txt/ then Gem::Ext::CmakeBuilder when /Cargo.toml/ then - # We use the spec name here to ensure we invoke the correct init function later - Gem::Ext::CargoBuilder.new(@spec) + Gem::Ext::CargoBuilder.new else build_error("No builder for extension '#{extension}'") end @@ -174,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 @@ -190,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 diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb index 60ab5544fe..86a0e73f28 100644 --- a/lib/rubygems/ext/cargo_builder.rb +++ b/lib/rubygems/ext/cargo_builder.rb @@ -1,35 +1,66 @@ # 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(spec) + def initialize require_relative "../command" require_relative "cargo_builder/link_flag_converter" - @spec = spec @runner = self.class.method(:run) @profile = :release end - def build(_extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd) + require "tempfile" require "fileutils" - require "shellwords" - build_crate(dest_path, results, args, cargo_dir) - validate_cargo_build!(dest_path) - rename_cdylib_for_ruby_compatibility(dest_path) - finalize_directory(dest_path, lib_dir, cargo_dir) - results - end + # Where's the Cargo.toml of the crate we're building + cargo_toml = File.join(cargo_dir, "Cargo.toml") + # What's the crate's name + crate_name = cargo_crate_name(cargo_dir, cargo_toml, results) + + begin + # Create a tmp dir to do the build in + tmp_dest = Dir.mktmpdir(".gem.", cargo_dir) + + # Run the build + cmd = cargo_command(cargo_toml, tmp_dest, args, crate_name) + runner.call(cmd, results, "cargo", cargo_dir, build_env) + + # Where do we expect Cargo to write the compiled library + dylib_path = cargo_dylib_path(tmp_dest, crate_name) - def build_crate(dest_path, results, args, cargo_dir) - env = build_env - cmd = cargo_command(cargo_dir, dest_path, args) - runner.call cmd, results, "cargo", cargo_dir, env + # Helpful error if we didn't find the compiled library + raise DylibNotFoundError, tmp_dest unless File.exist?(dylib_path) + + # Cargo and Ruby differ on how the library should be named, rename from + # what Cargo outputs to what Ruby expects + dlext_name = "#{crate_name}.#{makefile_config("DLEXT")}" + dlext_path = File.join(File.dirname(dylib_path), dlext_name) + FileUtils.cp(dylib_path, dlext_path) + + nesting = extension_nesting(extension) + + if Gem.install_extension_in_lib && lib_dir + nested_lib_dir = File.join(lib_dir, nesting) + FileUtils.mkdir_p nested_lib_dir + FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true + end + + # move to final destination + nested_dest_path = File.join(dest_path, nesting) + FileUtils.mkdir_p nested_dest_path + FileUtils.cp_r dlext_path, nested_dest_path, remove_destination: true + ensure + # clean up intermediary build artifacts + FileUtils.rm_rf tmp_dest if tmp_dest + end results end @@ -42,39 +73,57 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder build_env end - def cargo_command(cargo_dir, dest_path, args = []) - manifest = File.join(cargo_dir, "Cargo.toml") - cargo = ENV.fetch("CARGO", "cargo") - + def cargo_command(cargo_toml, dest_path, args = [], crate_name = nil) cmd = [] cmd += [cargo, "rustc"] cmd += ["--crate-type", "cdylib"] cmd += ["--target", ENV["CARGO_BUILD_TARGET"]] if ENV["CARGO_BUILD_TARGET"] cmd += ["--target-dir", dest_path] - cmd += ["--manifest-path", manifest] + cmd += ["--manifest-path", cargo_toml] cmd += ["--lib"] cmd += ["--profile", profile.to_s] cmd += ["--locked"] cmd += Gem::Command.build_args cmd += args cmd += ["--"] - cmd += [*cargo_rustc_args(dest_path)] + cmd += [*cargo_rustc_args(dest_path, crate_name)] cmd end private + def cargo + ENV.fetch("CARGO", "cargo") + end + + # returns the directory nesting of the extension, ignoring the first part, so + # "ext/foo/bar/Cargo.toml" becomes "foo/bar" + def extension_nesting(extension) + parts = extension.to_s.split(Regexp.union([File::SEPARATOR, File::ALT_SEPARATOR].compact)) + + parts = parts.each_with_object([]) do |segment, final| + next if segment == "." + if segment == ".." + raise Gem::InstallError, "extension outside of gem root" if final.empty? + next final.pop + end + final << segment + end + + File.join(parts[1...-1]) + end + def rb_config_env result = {} RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v } result end - def cargo_rustc_args(dest_dir) + def cargo_rustc_args(dest_dir, crate_name) [ *linker_args, *mkmf_libpath, - *rustc_dynamic_linker_flags(dest_dir), + *rustc_dynamic_linker_flags(dest_dir, crate_name), *rustc_lib_flags(dest_dir), *platform_specific_rustc_args(dest_dir), ] @@ -134,44 +183,72 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder makefile_config("ENABLE_SHARED") == "no" end - # Ruby expects the dylib to follow a file name convention for loading - def rename_cdylib_for_ruby_compatibility(dest_path) - new_path = final_extension_path(dest_path) - FileUtils.cp(cargo_dylib_path(dest_path), new_path) - new_path + def cargo_dylib_path(dest_path, crate_name) + prefix = so_ext == "dll" ? "" : "lib" + path_parts = [dest_path] + path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"] + path_parts += ["release", "#{prefix}#{crate_name}.#{so_ext}"] + File.join(*path_parts) end - def validate_cargo_build!(dir) - dylib_path = cargo_dylib_path(dir) + def cargo_crate_name(cargo_dir, manifest_path, results) + require "open3" + Gem.load_yaml - raise DylibNotFoundError, dir unless File.exist?(dylib_path) + output, status = + begin + Open3.capture2e(cargo, "metadata", "--no-deps", "--format-version", "1", chdir: cargo_dir) + rescue StandardError => error + raise Gem::InstallError, "cargo metadata failed #{error.message}" + end - dylib_path - end + unless status.success? + if Gem.configuration.really_verbose + puts output + else + results << output + end - def final_extension_path(dest_path) - dylib_path = cargo_dylib_path(dest_path) - dlext_name = "#{spec.name}.#{makefile_config("DLEXT")}" - dylib_path.gsub(File.basename(dylib_path), dlext_name) - end + exit_reason = + if status.exited? + ", exit code #{status.exitstatus}" + elsif status.signaled? + ", uncaught signal #{status.termsig}" + end - def cargo_dylib_path(dest_path) - prefix = so_ext == "dll" ? "" : "lib" - path_parts = [dest_path] - path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"] - path_parts += ["release", "#{prefix}#{cargo_crate_name}.#{so_ext}"] - File.join(*path_parts) + raise Gem::InstallError, "cargo metadata failed#{exit_reason}" + end + + # cargo metadata output is specified as json, 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 cargo_crate_name - spec.metadata.fetch("cargo_crate_name", spec.name).tr("-", "_") + def normalize_path(path) + return path unless File::ALT_SEPARATOR + + path.tr(File::ALT_SEPARATOR, File::SEPARATOR) end - def rustc_dynamic_linker_flags(dest_dir) - split_flags("DLDFLAGS") - .map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir) } - .compact - .flat_map {|arg| ldflag_to_link_modifier(arg) } + def rustc_dynamic_linker_flags(dest_dir, crate_name) + split_flags("DLDFLAGS"). + 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) @@ -204,7 +281,7 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder end # Interpolate substitution vars in the arg (i.e. $(DEFFILE)) - def maybe_resolve_ldflag_variable(input_arg, dest_dir) + def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name) var_matches = input_arg.match(/\$\((\w+)\)/) return input_arg unless var_matches @@ -215,27 +292,27 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder case var_name # On windows, it is assumed that mkmf has setup an exports file for the - # extension, so we have to to create one ourselves. + # extension, so we have to create one ourselves. when "DEFFILE" - write_deffile(dest_dir) + write_deffile(dest_dir, crate_name) else RbConfig::CONFIG[var_name] end end - def write_deffile(dest_dir) - deffile_path = File.join(dest_dir, "#{spec.name}-#{RbConfig::CONFIG["arch"]}.def") + def write_deffile(dest_dir, crate_name) + deffile_path = File.join(dest_dir, "#{crate_name}-#{RbConfig::CONFIG["arch"]}.def") export_prefix = makefile_config("EXPORT_PREFIX") || "" File.open(deffile_path, "w") do |f| f.puts "EXPORTS" - f.puts "#{export_prefix.strip}Init_#{spec.name}" + f.puts "#{export_prefix.strip}Init_#{crate_name}" end deffile_path end - # We have to basically reimplement RbConfig::CONFIG['SOEXT'] here to support + # 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 @@ -264,44 +341,6 @@ class Gem::Ext::CargoBuilder < Gem::Ext::Builder RbConfig.expand(val.dup) end - # Copied from ExtConfBuilder - def finalize_directory(dest_path, lib_dir, extension_dir) - require "fileutils" - require "tempfile" - - ext_path = final_extension_path(dest_path) - - begin - tmp_dest = Dir.mktmpdir(".gem.", extension_dir) - - # Some versions of `mktmpdir` return absolute paths, which will break make - # if the paths contain spaces. - # - # As such, we convert to a relative path. - tmp_dest_relative = get_relative_path(tmp_dest.clone, extension_dir) - - full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - - # TODO: remove in RubyGems 4 - if Gem.install_extension_in_lib && lib_dir - FileUtils.mkdir_p lib_dir - FileUtils.cp_r ext_path, lib_dir, remove_destination: true - end - - FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| - destent = ent.class.new(dest_path, ent.rel) - destent.exist? || FileUtils.mv(ent.path, destent.path) - end - ensure - FileUtils.rm_rf tmp_dest if tmp_dest - end - end - - def get_relative_path(path, base) - path[0..base.length - 1] = "." if path.start_with?(base) - path - end - # Error raised when no cdylib artifact was created class DylibNotFoundError < StandardError def initialize(dir) diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb index 51106c6370..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. diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb index 27ebd8c62b..fb68a7a8cc 100644 --- a/lib/rubygems/ext/ext_conf_builder.rb +++ b/lib/rubygems/ext/ext_conf_builder.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -21,8 +22,7 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder destdir = ENV["DESTDIR"] begin - require "shellwords" - cmd = Gem.ruby.shellsplit << "-I" << File.expand_path("../..", __dir__) << File.basename(extension) + cmd = ruby << File.basename(extension) cmd.push(*args) run(cmd, results, class_name, extension_dir) do |s, r| @@ -43,12 +43,11 @@ class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder full_tmp_dest = File.join(extension_dir, tmp_dest_relative) - # TODO remove in RubyGems 4 if Gem.install_extension_in_lib && lib_dir FileUtils.mkdir_p lib_dir entries = Dir.entries(full_tmp_dest) - %w[. ..] entries = entries.map {|entry| File.join full_tmp_dest, entry } - FileUtils.cp_r entries, lib_dir, :remove_destination => true + FileUtils.cp_r entries, lib_dir, remove_destination: true end FileUtils::Entry_.new(full_tmp_dest).traverse do |ent| @@ -66,8 +65,6 @@ 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 diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb index 9f2e099d40..0171807b39 100644 --- a/lib/rubygems/ext/rake_builder.rb +++ b/lib/rubygems/ext/rake_builder.rb @@ -1,4 +1,7 @@ # frozen_string_literal: true + +require_relative "../shellwords" + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -7,18 +10,17 @@ 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"] if rake - require "shellwords" - 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"] end diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb index 31890a60d7..8335a0ad03 100644 --- a/lib/rubygems/gem_runner.rb +++ b/lib/rubygems/gem_runner.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -32,7 +33,11 @@ class Gem::GemRunner do_configuration args - Gem.load_env_plugins rescue nil + begin + Gem.load_env_plugins + rescue StandardError + nil + end Gem.load_plugins cmd = @command_manager_class.instance @@ -40,10 +45,10 @@ class Gem::GemRunner cmd.command_names.each do |command_name| config_args = Gem.configuration[command_name] config_args = case config_args - when String - config_args.split " " - else - Array(config_args) + when String + config_args.split " " + else + Array(config_args) end Gem::Command.add_specific_extra_args command_name, config_args end diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb index d4078aaf5b..a8361b7ff1 100644 --- a/lib/rubygems/gemcutter_utilities.rb +++ b/lib/rubygems/gemcutter_utilities.rb @@ -1,14 +1,17 @@ # frozen_string_literal: true + require_relative "remote_fetcher" require_relative "text" +require_relative "gemcutter_utilities/webauthn_listener" +require_relative "gemcutter_utilities/webauthn_poller" ## # Utility methods for using the RubyGems API. module Gem::GemcutterUtilities - ERROR_CODE = 1 - API_SCOPES = %i[index_rubygems push_rubygem yank_rubygem add_owner remove_owner access_webhooks show_dashboard].freeze + API_SCOPES = [:index_rubygems, :push_rubygem, :yank_rubygem, :add_owner, :remove_owner, :access_webhooks].freeze + EXCLUSIVELY_API_SCOPES = [:show_dashboard].freeze include Gem::Text @@ -81,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 @@ -91,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}" @@ -100,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 @@ -117,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.basic_auth identifier, password request["OTP"] = otp if otp - request.body = URI.encode_www_form({ :api_key => api_key }.merge(update_scope_params)) + request.body = Gem::URI.encode_www_form({ api_key: api_key }.merge(update_scope_params)) end - with_response response do |resp| + with_response response do |_resp| say "Added #{scope} scope to the existing API key" end end @@ -147,33 +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(email, password) + profile = get_user_profile(identifier, password) mfa_params = get_mfa_params(profile) all_params = scope_params.merge(mfa_params) warning = profile["warning"] + credentials = { identifier: identifier, password: password } say "#{warning}\n" if warning response = rubygems_api_request(:post, "api/v1/api_key", - sign_in_host, scope: scope) do |request| - request.basic_auth email, password + sign_in_host, credentials: credentials, scope: scope) do |request| + request.basic_auth identifier, password request["OTP"] = otp if otp - request.body = URI.encode_www_form({ name: key_name }.merge(all_params)) + request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params)) end with_response response do |resp| @@ -205,14 +209,14 @@ module Gem::GemcutterUtilities def with_response(response, error_prefix = nil) case response - when Net::HTTPSuccess then + when Gem::Net::HTTPSuccess then if block_given? yield response else say clean_text(response.body) end - when Net::HTTPPermanentRedirect, Net::HTTPRedirection then - message = "The request has redirected permanently to #{response['location']}. Please check your defined push host URL." + when Gem::Net::HTTPPermanentRedirect, Gem::Net::HTTPRedirection then + message = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL." message = "#{error_prefix}: #{message}" if error_prefix say clean_text(message) @@ -241,7 +245,7 @@ module Gem::GemcutterUtilities private def request_with_otp(method, uri, &block) - request_method = Net::HTTP.const_get method.to_s.capitalize + request_method = Gem::Net::HTTP.const_get method.to_s.capitalize Gem::RemoteFetcher.fetcher.request(uri, request_method) do |req| req["OTP"] = otp if otp @@ -249,9 +253,52 @@ module Gem::GemcutterUtilities 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) @@ -263,15 +310,31 @@ module Gem::GemcutterUtilities 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_yes_no("#{scope}", false) - scope_params[scope] = true if selected + say "The default access scope is:" + scope_params.each do |k, _v| + say " #{k}: y" + end + say "\n" + customise = ask_yes_no("Do you want to customise scopes?", false) + if customise + EXCLUSIVELY_API_SCOPES.each do |excl_scope| + selected = ask_yes_no("#{excl_scope} (exclusive scope, answering yes will not prompt for other scopes)", false) + next unless selected + + return { excl_scope => true } + end + + scope_params = {} + + API_SCOPES.each do |s| + selected = ask_yes_no(s.to_s, false) + scope_params[s] = true if selected + end end say "\n" end @@ -280,25 +343,25 @@ module Gem::GemcutterUtilities end def default_host? - self.host == Gem::DEFAULT_HOST + host == Gem::DEFAULT_HOST end - def get_user_profile(email, password) + def get_user_profile(identifier, password) return {} unless default_host? response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request| - request.basic_auth email, password + request.basic_auth identifier, password end with_response response do |resp| - Gem::SafeYAML.load clean_text(resp.body) + Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body)) end end def get_mfa_params(profile) mfa_level = profile["mfa"] params = {} - if mfa_level == "ui_only" || mfa_level == "ui_and_gem_signin" + if ["ui_only", "ui_and_gem_signin"].include?(mfa_level) selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)") params["mfa"] = true if selected end @@ -320,6 +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 d0061ff82e..0000000000 --- a/lib/rubygems/indexer.rb +++ /dev/null @@ -1,427 +0,0 @@ -# frozen_string_literal: true -require_relative "../rubygems" -require_relative "package" -require "tmpdir" - -## -# Top level class for building the gem repository index. - -class Gem::Indexer - include Gem::UserInteraction - - ## - # Build indexes for RubyGems 1.2.0 and newer when true - - attr_accessor :build_modern - - ## - # Index install location - - attr_reader :dest_directory - - ## - # Specs index install location - - attr_reader :dest_specs_index - - ## - # Latest specs index install location - - attr_reader :dest_latest_specs_index - - ## - # Prerelease specs index install location - - attr_reader :dest_prerelease_specs_index - - ## - # Index build directory - - attr_reader :directory - - ## - # Create an indexer that will index the gems in +directory+. - - def initialize(directory, options = {}) - require "fileutils" - require "tmpdir" - require "zlib" - - options = { :build_modern => true }.merge options - - @build_modern = options[:build_modern] - - @dest_directory = directory - @directory = Dir.mktmpdir "gem_generate_index" - - marshal_name = "Marshal.#{Gem.marshal_version}" - - @master_index = File.join @directory, "yaml" - @marshal_index = File.join @directory, marshal_name - - @quick_dir = File.join @directory, "quick" - @quick_marshal_dir = File.join @quick_dir, marshal_name - @quick_marshal_dir_base = File.join "quick", marshal_name # FIX: UGH - - @quick_index = File.join @quick_dir, "index" - @latest_index = File.join @quick_dir, "latest_index" - - @specs_index = File.join @directory, "specs.#{Gem.marshal_version}" - @latest_specs_index = - File.join(@directory, "latest_specs.#{Gem.marshal_version}") - @prerelease_specs_index = - File.join(@directory, "prerelease_specs.#{Gem.marshal_version}") - @dest_specs_index = - File.join(@dest_directory, "specs.#{Gem.marshal_version}") - @dest_latest_specs_index = - File.join(@dest_directory, "latest_specs.#{Gem.marshal_version}") - @dest_prerelease_specs_index = - File.join(@dest_directory, "prerelease_specs.#{Gem.marshal_version}") - - @files = [] - end - - ## - # Build various indices - - def build_indices - specs = map_gems_to_specs gem_file_list - Gem::Specification._resort! specs - build_marshal_gemspecs specs - build_modern_indices specs if @build_modern - - compress_indices - end - - ## - # Builds Marshal quick index gemspecs. - - def build_marshal_gemspecs(specs) - count = specs.count - progress = ui.progress_reporter count, - "Generating Marshal quick index gemspecs for #{count} gems", - "Complete" - - files = [] - - Gem.time "Generated Marshal quick index gemspecs" do - specs.each do |spec| - next if spec.default_gem? - spec_file_name = "#{spec.original_name}.gemspec.rz" - marshal_name = File.join @quick_marshal_dir, spec_file_name - - marshal_zipped = Gem.deflate Marshal.dump(spec) - - File.open marshal_name, "wb" do |io| - io.write marshal_zipped - end - - files << marshal_name - - progress.updated spec.original_name - end - - progress.done - end - - @files << @quick_marshal_dir - - files - end - - ## - # Build a single index for RubyGems 1.2 and newer - - def build_modern_index(index, file, name) - say "Generating #{name} index" - - Gem.time "Generated #{name} index" do - File.open(file, "wb") do |io| - specs = index.map do |*spec| - # We have to splat here because latest_specs is an array, while the - # others are hashes. - spec = spec.flatten.last - platform = spec.original_platform - - # win32-api-1.0.4-x86-mswin32-60 - unless String === platform - alert_warning "Skipping invalid platform in gem: #{spec.full_name}" - next - end - - platform = Gem::Platform::RUBY if platform.nil? || platform.empty? - [spec.name, spec.version, platform] - end - - specs = compact_specs(specs) - Marshal.dump(specs, io) - end - end - end - - ## - # Builds indices for RubyGems 1.2 and newer. Handles full, latest, prerelease - - def build_modern_indices(specs) - prerelease, released = specs.partition do |s| - s.version.prerelease? - end - latest_specs = - Gem::Specification._latest_specs specs - - build_modern_index(released.sort, @specs_index, "specs") - build_modern_index(latest_specs.sort, @latest_specs_index, "latest specs") - build_modern_index(prerelease.sort, @prerelease_specs_index, - "prerelease specs") - - @files += [@specs_index, - "#{@specs_index}.gz", - @latest_specs_index, - "#{@latest_specs_index}.gz", - @prerelease_specs_index, - "#{@prerelease_specs_index}.gz"] - end - - def map_gems_to_specs(gems) - gems.map do |gemfile| - if File.size(gemfile) == 0 - alert_warning "Skipping zero-length gem: #{gemfile}" - next - end - - begin - spec = Gem::Package.new(gemfile).spec - spec.loaded_from = gemfile - - spec.abbreviate - spec.sanitize - - spec - rescue SignalException - alert_error "Received signal, exiting" - raise - rescue Exception => e - msg = ["Unable to process #{gemfile}", - "#{e.message} (#{e.class})", - "\t#{e.backtrace.join "\n\t"}"].join("\n") - alert_error msg - end - end.compact - end - - ## - # Compresses indices on disk - #-- - # All future files should be compressed using gzip, not deflate - - def compress_indices - say "Compressing indices" - - Gem.time "Compressed indices" do - if @build_modern - gzip @specs_index - gzip @latest_specs_index - gzip @prerelease_specs_index - end - end - end - - ## - # Compacts Marshal output for the specs index data source by using identical - # objects as much as possible. - - def compact_specs(specs) - names = {} - versions = {} - platforms = {} - - specs.map do |(name, version, platform)| - names[name] = name unless names.include? name - versions[version] = version unless versions.include? version - platforms[platform] = platform unless platforms.include? platform - - [names[name], versions[version], platforms[platform]] - end - end - - ## - # Compress +filename+ with +extension+. - - def compress(filename, extension) - data = Gem.read_binary filename - - zipped = Gem.deflate data - - File.open "#{filename}.#{extension}", "wb" do |io| - io.write zipped - end - end - - ## - # List of gem file names to index. - - def gem_file_list - Gem::Util.glob_files_in_dir("*.gem", File.join(@dest_directory, "gems")) - end - - ## - # Builds and installs indices. - - def generate_index - make_temp_directories - build_indices - install_indices - rescue SignalException - ensure - FileUtils.rm_rf @directory - end - - ## - # Zlib::GzipWriter wrapper that gzips +filename+ on disk. - - def gzip(filename) - Zlib::GzipWriter.open "#{filename}.gz" do |io| - io.write Gem.read_binary(filename) - end - end - - ## - # Install generated indices into the destination directory. - - def install_indices - verbose = Gem.configuration.really_verbose - - say "Moving index into production dir #{@dest_directory}" if verbose - - files = @files - files.delete @quick_marshal_dir if files.include? @quick_dir - - if files.include?(@quick_marshal_dir) && !files.include?(@quick_dir) - files.delete @quick_marshal_dir - - dst_name = File.join(@dest_directory, @quick_marshal_dir_base) - - FileUtils.mkdir_p File.dirname(dst_name), :verbose => verbose - FileUtils.rm_rf dst_name, :verbose => verbose - FileUtils.mv(@quick_marshal_dir, dst_name, - :verbose => verbose, :force => true) - end - - files = files.map do |path| - path.sub(/^#{Regexp.escape @directory}\/?/, "") # HACK? - end - - files.each do |file| - src_name = File.join @directory, file - dst_name = File.join @dest_directory, file - - FileUtils.rm_rf dst_name, :verbose => verbose - FileUtils.mv(src_name, @dest_directory, - :verbose => verbose, :force => true) - end - end - - ## - # Make directories for index generation - - def make_temp_directories - FileUtils.rm_rf @directory - FileUtils.mkdir_p @directory, :mode => 0700 - FileUtils.mkdir_p @quick_marshal_dir - end - - ## - # Ensure +path+ and path with +extension+ are identical. - - def paranoid(path, extension) - data = Gem.read_binary path - compressed_data = Gem.read_binary "#{path}.#{extension}" - - unless data == Gem::Util.inflate(compressed_data) - raise "Compressed file #{compressed_path} does not match uncompressed file #{path}" - end - end - - ## - # Perform an in-place update of the repository from newly added gems. - - def update_index - make_temp_directories - - specs_mtime = File.stat(@dest_specs_index).mtime - newest_mtime = Time.at 0 - - updated_gems = gem_file_list.select do |gem| - gem_mtime = File.stat(gem).mtime - newest_mtime = gem_mtime if gem_mtime > newest_mtime - gem_mtime >= specs_mtime - end - - if updated_gems.empty? - say "No new gems" - terminate_interaction 0 - end - - specs = map_gems_to_specs updated_gems - prerelease, released = specs.partition {|s| s.version.prerelease? } - - files = build_marshal_gemspecs specs - - Gem.time "Updated indexes" do - update_specs_index released, @dest_specs_index, @specs_index - update_specs_index released, @dest_latest_specs_index, @latest_specs_index - update_specs_index(prerelease, - @dest_prerelease_specs_index, - @prerelease_specs_index) - end - - compress_indices - - verbose = Gem.configuration.really_verbose - - say "Updating production dir #{@dest_directory}" if verbose - - files << @specs_index - files << "#{@specs_index}.gz" - files << @latest_specs_index - files << "#{@latest_specs_index}.gz" - files << @prerelease_specs_index - files << "#{@prerelease_specs_index}.gz" - - files = files.map do |path| - path.sub(/^#{Regexp.escape @directory}\/?/, "") # HACK? - end - - files.each do |file| - src_name = File.join @directory, file - dst_name = File.join @dest_directory, file # REFACTOR: duped above - - FileUtils.mv src_name, dst_name, :verbose => verbose, - :force => true - - File.utime newest_mtime, newest_mtime, dst_name - end - ensure - FileUtils.rm_rf @directory - end - - ## - # Combines specs in +index+ and +source+ then writes out a new copy to - # +dest+. For a latest index, does not ensure the new file is minimal. - - def update_specs_index(index, source, dest) - specs_index = Marshal.load Gem.read_binary(source) - - index.each do |spec| - platform = spec.original_platform - platform = Gem::Platform::RUBY if platform.nil? || platform.empty? - specs_index << [spec.name, spec.version, platform] - end - - specs_index = compact_specs specs_index.uniq.sort - - File.open dest, "wb" do |io| - Marshal.dump specs_index, io - end - end -end diff --git a/lib/rubygems/install_default_message.rb b/lib/rubygems/install_default_message.rb index 0d112a15df..0640eaaf08 100644 --- a/lib/rubygems/install_default_message.rb +++ b/lib/rubygems/install_default_message.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "user_interaction" diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb index 2565f36261..a24e26b918 100644 --- a/lib/rubygems/install_message.rb +++ b/lib/rubygems/install_message.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "user_interaction" diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb index 79effcf21f..aad207a718 100644 --- a/lib/rubygems/install_update_options.rb +++ b/lib/rubygems/install_update_options.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -35,9 +36,9 @@ module Gem::InstallUpdateOptions "List the documentation types you wish to", "generate. For example: rdoc,ri") do |value, options| options[:document] = case value - when nil then %w[ri] - when false then [] - else value + when nil then %w[ri] + when false then [] + else value end end @@ -49,7 +50,7 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--vendor", "Install gem into the vendor directory.", - "Only for use by gem repackagers.") do |value, options| + "Only for use by gem repackagers.") do |_value, options| unless Gem.vendor_dir raise Gem::OptionParser::InvalidOption.new "your platform is not supported" end @@ -59,7 +60,7 @@ module Gem::InstallUpdateOptions end add_option(:"Install/Update", "-N", "--no-document", - "Disable documentation generation") do |value, options| + "Disable documentation generation") do |_value, options| options[:document] = [] end @@ -103,21 +104,21 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--development", "Install additional development", - "dependencies") do |value, options| + "dependencies") do |_value, options| options[:development] = true options[:dev_shallow] = true end add_option(:"Install/Update", "--development-all", "Install development dependencies for all", - "gems (including dev deps themselves)") do |value, options| + "gems (including dev deps themselves)") do |_value, options| options[:development] = true options[:dev_shallow] = false end add_option(:"Install/Update", "--conservative", "Don't attempt to upgrade gems already", - "meeting version requirement") do |value, options| + "meeting version requirement") do |_value, options| options[:conservative] = true options[:minimal_deps] = true end @@ -135,13 +136,13 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "-g", "--file [FILE]", "Read from a gem dependencies API file and", - "install the listed gems") do |v,o| - v = Gem::GEM_DEP_FILES.find do |file| + "install the listed gems") do |v,_o| + v ||= Gem::GEM_DEP_FILES.find do |file| File.exist? file - end unless v + end unless v - message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ', '})" + message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ", "})" raise Gem::OptionParser::InvalidArgument, "cannot find gem dependencies file #{message}" @@ -153,29 +154,29 @@ module Gem::InstallUpdateOptions add_option(:"Install/Update", "--without GROUPS", Array, "Omit the named groups (comma separated)", "when installing from a gem dependencies", - "file") do |v,o| - options[:without_groups].concat v.map {|without| without.intern } + "file") do |v,_o| + options[:without_groups].concat v.map(&:intern) end add_option(:"Install/Update", "--default", "Add the gem's full specification to", - "specifications/default and extract only its bin") do |v,o| + "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| + "be installed") do |v,_o| options[:explain] = v end add_option(:"Install/Update", "--[no-]lock", - "Create a lock file (when used with -g/--file)") do |v,o| + "Create a lock file (when used with -g/--file)") do |v,_o| options[:lock] = v end add_option(:"Install/Update", "--[no-]suggestions", - "Suggest alternates when gems are not found") do |v,o| + "Suggest alternates when gems are not found") do |v,_o| options[:suggest_alternate] = v end end @@ -185,7 +186,7 @@ module Gem::InstallUpdateOptions def install_update_options { - :document => %w[ri], + document: %w[ri], } end @@ -195,5 +196,4 @@ module Gem::InstallUpdateOptions def install_update_defaults_str "--document=ri" end - end diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb index 9dfd7fae71..8f6f9a5aa8 100644 --- a/lib/rubygems/installer.rb +++ b/lib/rubygems/installer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -188,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 @@ -221,31 +224,32 @@ class Gem::Installer File.open generated_bin, "rb" do |io| line = io.gets - shebang = /^#!.*ruby/ + shebang = /^#!.*ruby/o - if load_relative_enabled? - until line.nil? || line =~ shebang do + # TruffleRuby uses a bash prelude in default launchers + if load_relative_enabled? || RUBY_ENGINE == "truffleruby" + until line.nil? || shebang.match?(line) do line = io.gets end end - next unless line =~ shebang + next unless line&.match?(shebang) io.gets # blankline - # TODO detect a specially formatted comment instead of trying + # TODO: detect a specially formatted comment instead of trying # to find a string inside Ruby code. - next unless io.gets.to_s.include?("This file was generated by RubyGems") + next unless io.gets&.include?("This file was generated by RubyGems") ruby_executable = true - existing = io.read.slice(%r{ + existing = io.read.slice(/ ^\s*( gem \s | load \s Gem\.bin_path\( | load \s Gem\.activate_bin_path\( ) (['"])(.*?)(\2), - }x, 3) + /x, 3) end return if spec.name == existing @@ -315,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 @@ -342,37 +346,35 @@ class Gem::Installer 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 @@ -388,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 @@ -462,6 +464,9 @@ class Gem::Installer ## # Writes the full .gemspec specification (in Ruby) to the gem home's # specifications/default directory. + # + # In contrast to #write_spec, this keeps file lists, so the `gem contents` + # command works. def write_default_spec Gem.write_binary(default_spec_file, spec.to_ruby) @@ -488,12 +493,11 @@ class Gem::Installer ensure_writable_dir @bin_dir spec.executables.each do |filename| - filename.tap(&Gem::UNTAINT) bin_path = File.join gem_dir, spec.bindir, filename next unless File.exist? bin_path mode = File.stat(bin_path).mode - dir_mode = options[:prog_mode] || (mode | 0111) + dir_mode = options[:prog_mode] || (mode | 0o111) unless dir_mode == mode require "fileutils" @@ -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 ## @@ -536,9 +542,9 @@ class Gem::Installer 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 @@ -563,7 +569,7 @@ class Gem::Installer File.unlink dst end - FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose + FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose rescue NotImplementedError, SystemCallError alert_warning "Unable to use symlinks, installing wrapper" generate_bin_script filename, bindir @@ -586,7 +592,7 @@ class Gem::Installer def shebang(bin_file_name) path = File.join gem_dir, spec.bindir, bin_file_name - first_line = File.open(path, "rb") {|file| file.gets } || "" + first_line = File.open(path, "rb", &:gets) || "" if first_line.start_with?("#!") # Preserve extra words on shebang line, like "-w". Thanks RPA. @@ -628,7 +634,6 @@ class Gem::Installer def ensure_loadable_spec ruby = spec.to_ruby_for_cache - ruby.tap(&Gem::UNTAINT) begin eval ruby @@ -649,32 +654,37 @@ class Gem::Installer def process_options # :nodoc: @options = { - :bin_dir => nil, - :env_shebang => false, - :force => false, - :only_install_dir => false, - :post_install_message => true, + bin_dir: nil, + env_shebang: false, + force: false, + only_install_dir: false, + post_install_message: true, }.merge options @env_shebang = options[:env_shebang] @force = options[:force] @install_dir = options[:install_dir] - @gem_home = options[:install_dir] || Gem.dir - @plugins_dir = Gem.plugindir(@gem_home) + @user_install = options[:user_install] @ignore_dependencies = options[:ignore_dependencies] @format_executable = options[:format_executable] @wrappers = options[:wrappers] @only_install_dir = options[:only_install_dir] - # If the user has asked for the gem to be installed in a directory that is - # the system gem directory, then use the system bin directory, else create - # (or use) a new bin dir under the gem_home. - @bin_dir = options[:bin_dir] || Gem.bindir(gem_home) + @bin_dir = options[:bin_dir] @development = options[:development] @build_root = options[:build_root] @build_args = options[:build_args] + @gem_home = @install_dir || user_install_dir || Gem.dir + + # If the user has asked for the gem to be installed in a directory that is + # the system gem directory, then use the system bin directory, else create + # (or use) a new bin dir under the gem_home. + @bin_dir ||= Gem.bindir(@gem_home) + + @plugins_dir = Gem.plugindir(@gem_home) + unless @build_root.nil? @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, "")) @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, "")) @@ -708,12 +718,11 @@ 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 @@ -725,11 +734,11 @@ class Gem::Installer raise Gem::InstallError, "#{spec} has an invalid extensions" end - if spec.platform.to_s =~ /\R/ + if /\R/.match?(spec.platform.to_s) raise Gem::InstallError, "#{spec.platform} is an invalid platform" end - unless spec.specification_version.to_s =~ /\A\d+\z/ + unless /\A\d+\z/.match?(spec.specification_version.to_s) raise Gem::InstallError, "#{spec} has an invalid specification_version" end @@ -746,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. @@ -805,10 +814,10 @@ TEXT rb_topdir = RbConfig::TOPDIR || File.dirname(rb_config["bindir"]) # get ruby executable file name from RbConfig - ruby_exe = "#{rb_config['RUBY_INSTALL_NAME']}#{rb_config['EXEEXT']}" + ruby_exe = "#{rb_config["RUBY_INSTALL_NAME"]}#{rb_config["EXEEXT"]}" ruby_exe = "ruby.exe" if ruby_exe.empty? - if File.exist?(File.join bindir, ruby_exe) + if File.exist?(File.join(bindir, ruby_exe)) # stub & ruby.exe within same folder. Portable <<-TEXT @ECHO OFF @@ -930,7 +939,7 @@ TEXT build_info_dir = File.join gem_home, "build_info" dir_mode = options[:dir_mode] - FileUtils.mkdir_p build_info_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p build_info_dir, mode: dir_mode && 0o755 build_info_file = File.join build_info_dir, "#{spec.full_name}.info" @@ -953,7 +962,7 @@ TEXT 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 @@ -963,6 +972,19 @@ TEXT 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" @@ -988,7 +1010,7 @@ TEXT bindir="${0%/*}" EOS - script << %Q(exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n) + script << %(exec "$bindir/#{ruby_install_name}" "-x" "$0" "$@"\n) <<~EOS #!/bin/sh @@ -1002,4 +1024,17 @@ TEXT "" 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_uninstaller_utils.rb b/lib/rubygems/installer_uninstaller_utils.rb index d97b4e29b1..c5c2a52bab 100644 --- a/lib/rubygems/installer_uninstaller_utils.rb +++ b/lib/rubygems/installer_uninstaller_utils.rb @@ -4,7 +4,6 @@ # Helper methods for both Gem::Installer and Gem::Uninstaller module Gem::InstallerUninstallerUtils - def regenerate_plugins_for(spec, plugins_dir) plugins = spec.plugins return if plugins.empty? @@ -25,5 +24,4 @@ module Gem::InstallerUninstallerUtils def remove_plugins_for(spec, plugins_dir) FileUtils.rm_f Gem::Util.glob_files_in_dir("#{spec.name}#{Gem.plugin_suffix_pattern}", plugins_dir) end - end diff --git a/lib/rubygems/local_remote_options.rb b/lib/rubygems/local_remote_options.rb index b2c2dea905..51a61213a5 100644 --- a/lib/rubygems/local_remote_options.rb +++ b/lib/rubygems/local_remote_options.rb @@ -1,26 +1,26 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. # See LICENSE.txt for permissions. #++ -require "uri" +require_relative "vendor/uri/lib/uri" require_relative "../rubygems" ## # Mixin methods for local and remote Gem::Command options. module Gem::LocalRemoteOptions - ## # Allows Gem::OptionParser to handle HTTP URIs. def accept_uri_http - Gem::OptionParser.accept URI::HTTP do |value| + Gem::OptionParser.accept Gem::URI::HTTP do |value| begin - uri = URI.parse value - rescue URI::InvalidURIError + uri = Gem::URI.parse value + rescue Gem::URI::InvalidURIError raise Gem::OptionParser::InvalidArgument, value end @@ -39,17 +39,17 @@ module Gem::LocalRemoteOptions def add_local_remote_options add_option(:"Local/Remote", "-l", "--local", - "Restrict operations to the LOCAL domain") do |value, options| + "Restrict operations to the LOCAL domain") do |_value, options| options[:domain] = :local end add_option(:"Local/Remote", "-r", "--remote", - "Restrict operations to the REMOTE domain") do |value, options| + "Restrict operations to the REMOTE domain") do |_value, options| options[:domain] = :remote end add_option(:"Local/Remote", "-b", "--both", - "Allow LOCAL and REMOTE operations") do |value, options| + "Allow LOCAL and REMOTE operations") do |_value, options| options[:domain] = :both end @@ -66,8 +66,7 @@ module Gem::LocalRemoteOptions def add_bulk_threshold_option add_option(:"Local/Remote", "-B", "--bulk-threshold COUNT", "Threshold for switching to bulk", - "synchronization (default #{Gem.configuration.bulk_threshold})") do - |value, options| + "synchronization (default #{Gem.configuration.bulk_threshold})") do |value, _options| Gem.configuration.bulk_threshold = value.to_i end end @@ -77,7 +76,7 @@ module Gem::LocalRemoteOptions def add_clear_sources_option add_option(:"Local/Remote", "--clear-sources", - "Clear the gem sources") do |value, options| + "Clear the gem sources") do |_value, options| Gem.sources = nil options[:sources_cleared] = true end @@ -89,9 +88,9 @@ module Gem::LocalRemoteOptions def add_proxy_option accept_uri_http - add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", URI::HTTP, + add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", Gem::URI::HTTP, "Use HTTP proxy for remote operations") do |value, options| - options[:http_proxy] = (value == false) ? :no_proxy : value + options[:http_proxy] = value == false ? :no_proxy : value Gem.configuration[:http_proxy] = options[:http_proxy] end end @@ -102,9 +101,9 @@ module Gem::LocalRemoteOptions def add_source_option accept_uri_http - add_option(:"Local/Remote", "-s", "--source URL", URI::HTTP, + add_option(:"Local/Remote", "-s", "--source URL", Gem::URI::HTTP, "Append URL to list of remote gem sources") do |source, options| - source << "/" if source !~ /\/\z/ + source << "/" unless source.end_with?("/") if options.delete :sources_cleared Gem.sources = [source] @@ -119,7 +118,7 @@ module Gem::LocalRemoteOptions def add_update_sources_option add_option(:Deprecated, "-u", "--[no-]update-sources", - "Update local source cache") do |value, options| + "Update local source cache") do |value, _options| Gem.configuration.update_sources = value end end @@ -144,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 5cc67ad099..0000000000 --- a/lib/rubygems/mock_gem_ui.rb +++ /dev/null @@ -1,85 +0,0 @@ -# frozen_string_literal: true -require_relative "user_interaction" - -## -# This Gem::StreamUI subclass records input and output to StringIO for -# retrieval during tests. - -class Gem::MockGemUi < Gem::StreamUI - ## - # Raised when you haven't provided enough input to your MockGemUi - - class InputEOFError < RuntimeError - def initialize(question) - super "Out of input for MockGemUi on #{question.inspect}" - end - end - - class TermError < RuntimeError - attr_reader :exit_code - - def initialize(exit_code) - super - @exit_code = exit_code - end - end - class SystemExitException < RuntimeError; end - - module TTY - - attr_accessor :tty - - def tty?() - @tty = true unless defined?(@tty) - @tty - end - - def noecho - yield self - end - end - - def initialize(input = "") - require "stringio" - ins = StringIO.new input - outs = StringIO.new - errs = StringIO.new - - ins.extend TTY - outs.extend TTY - errs.extend TTY - - super ins, outs, errs, true - - @terminated = false - end - - def ask(question) - raise InputEOFError, question if @ins.eof? - - super - end - - def input - @ins.string - end - - def output - @outs.string - end - - def error - @errs.string - end - - def terminated? - @terminated - end - - def terminate_interaction(status=0) - @terminated = true - - raise TermError, status if status != 0 - raise SystemExitException - end -end diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb index 767dc1fb45..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 || 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,7 +85,7 @@ class Gem::NameTuple "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>" end - alias to_s inspect # :nodoc: + alias_method :to_s, :inspect # :nodoc: def <=>(other) [@name, @version, Gem::Platform.sort_priority(@platform)] <=> diff --git a/lib/rubygems/optparse.rb b/lib/rubygems/optparse.rb deleted file mode 100644 index 6ed718423c..0000000000 --- a/lib/rubygems/optparse.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "optparse/lib/optparse" diff --git a/lib/rubygems/optparse/lib/optparse/uri.rb b/lib/rubygems/optparse/lib/optparse/uri.rb deleted file mode 100644 index 664d7f2af4..0000000000 --- a/lib/rubygems/optparse/lib/optparse/uri.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: false -# -*- ruby -*- - -require_relative '../optparse' -require 'uri' - -Gem::OptionParser.accept(URI) {|s,| URI.parse(s) if s} diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb index 050ffbfe77..1d5d764237 100644 --- a/lib/rubygems/package.rb +++ b/lib/rubygems/package.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 require_relative "../rubygems" require_relative "security" @@ -56,9 +59,9 @@ class Gem::Package def initialize(message, source = nil) if source - @path = source.path + @path = source.is_a?(String) ? source : source.path - message = message + " in #{path}" if path + message += " in #{path}" if path end super message @@ -67,15 +70,13 @@ class Gem::Package class PathError < Error def initialize(destination, destination_dir) - super "installing into parent path %s of %s is not allowed" % - [destination, destination_dir] + super format("installing into parent path %s of %s is not allowed", destination, destination_dir) end end class SymlinkError < Error def initialize(name, destination, destination_dir) - super "installing symlink '%s' pointing to parent path %s of %s is not allowed" % - [name, destination, destination_dir] + super format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir) end end @@ -154,7 +155,7 @@ class Gem::Package Gem::Package::FileSource.new gem end - return super unless Gem::Package == self + return super unless self == Gem::Package return super unless gem.present? return super unless gem.start @@ -186,7 +187,7 @@ class Gem::Package end end - return spec, metadata + [spec, metadata] end ## @@ -229,7 +230,7 @@ class Gem::Package end end - tar.add_file_signed "checksums.yaml.gz", 0444, @signer do |io| + tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io| gzip_to io do |gz_io| Psych.dump checksums_by_algorithm, gz_io end @@ -241,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 @@ -267,7 +268,7 @@ class Gem::Package tar.add_file_simple file, stat.mode, stat.size do |dst_io| File.open file, "rb" do |src_io| - dst_io.write src_io.read 16384 until src_io.eof? + copy_stream(src_io, dst_io) end end end @@ -277,7 +278,7 @@ class Gem::Package # Adds the package's Gem::Specification to the +tar+ file def add_metadata(tar) # :nodoc: - digests = tar.add_file_signed "metadata.gz", 0444, @signer do |io| + digests = tar.add_file_signed "metadata.gz", 0o444, @signer do |io| gzip_to io do |gz_io| gz_io.write @spec.to_yaml end @@ -346,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 ## @@ -354,18 +357,21 @@ EOM def digest(entry) # :nodoc: algorithms = if @checksums - @checksums.keys - else - [Gem::Security::DIGEST_NAME].compact + @checksums.to_h {|algorithm, _| [algorithm, Gem::Security.create_digest(algorithm)] } + elsif Gem::Security::DIGEST_NAME + { Gem::Security::DIGEST_NAME => Gem::Security.create_digest(Gem::Security::DIGEST_NAME) } end - algorithms.each do |algorithm| - digester = Gem::Security.create_digest(algorithm) - - digester << entry.read(16384) until entry.eof? + return @digests if algorithms.nil? || algorithms.empty? - entry.rewind + buf = String.new(capacity: 16_384, encoding: Encoding::BINARY) + until entry.eof? + entry.readpartial(16_384, buf) + algorithms.each_value {|digester| digester << buf } + end + entry.rewind + algorithms.each do |algorithm, digester| @digests[algorithm][entry.full_name] = digester end @@ -381,7 +387,7 @@ EOM def extract_files(destination_dir, pattern = "*") verify unless @spec - FileUtils.mkdir_p destination_dir, :mode => dir_mode && 0755 + FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755 @gem.with_read_io do |io| reader = Gem::Package::TarReader.new io @@ -391,9 +397,11 @@ EOM extract_tar_gz entry, destination_dir, pattern - return # ignore further entries + break # ignore further entries end end + rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e + raise Gem::Package::FormatError.new e.message, @gem end ## @@ -408,6 +416,8 @@ EOM # extracted. def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc: + destination_dir = File.realpath(destination_dir) + directories = [] symlinks = [] @@ -430,8 +440,6 @@ EOM FileUtils.rm_rf destination - mkdir_options = {} - mkdir_options[:mode] = dir_mode ? 0755 : (entry.header.mode if entry.directory?) mkdir = if entry.directory? destination @@ -440,13 +448,13 @@ EOM end unless directories.include?(mkdir) - FileUtils.mkdir_p mkdir, **mkdir_options + FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?) directories << mkdir end if entry.file? - File.open(destination, "wb") {|out| out.write entry.read } - FileUtils.chmod file_mode(entry.header.mode), destination + File.open(destination, "wb") {|out| copy_stream(entry, out) } + FileUtils.chmod file_mode(entry.header.mode) & ~File.umask, destination end verbose destination @@ -467,7 +475,7 @@ EOM end def file_mode(mode) # :nodoc: - ((mode & 0111).zero? ? data_mode : prog_mode) || + ((mode & 0o111).zero? ? data_mode : prog_mode) || # If we're not using one of the default modes, then we're going to fall # back to the mode from the tarball. In this case we need to mask it down # to fit into 2^16 bits (the maximum value for a mode in CRuby since it @@ -505,7 +513,6 @@ EOM raise Gem::Package::PathError.new(destination, destination_dir) unless normalize_path(destination).start_with? normalize_path(destination_dir + "/") - destination.tap(&Gem::UNTAINT) destination end @@ -571,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 @@ -625,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 @@ -669,7 +676,7 @@ EOM when "data.tar.gz" then verify_gz entry end - rescue + rescue StandardError warn "Exception while verifying #{@gem.path}" raise end @@ -688,11 +695,11 @@ EOM unless @files.include? "data.tar.gz" raise Gem::Package::FormatError.new \ - "package content (data.tar.gz) is missing", @gem + "package content (data.tar.gz) is missing", @gem end - if (duplicates = @files.group_by {|f| f }.select {|k,v| v.size > 1 }.map(&:first)) && duplicates.any? - raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(', ')})" + if (duplicates = @files.group_by {|f| f }.select {|_k,v| v.size > 1 }.map(&:first)) && duplicates.any? + raise Gem::Security::Exception, "duplicate files in the package: (#{duplicates.map(&:inspect).join(", ")})" end end @@ -701,11 +708,22 @@ 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_relative "package/digest_io" diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb index 4736f76d93..f04ab97462 100644 --- a/lib/rubygems/package/digest_io.rb +++ b/lib/rubygems/package/digest_io.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # IO wrapper that creates digests of contents written to the IO it wraps. @@ -35,7 +36,7 @@ class Gem::Package::DigestIO yield digest_io - return digests + digests end ## diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb index 14c7a9f6d2..d9717e0f2a 100644 --- a/lib/rubygems/package/file_source.rb +++ b/lib/rubygems/package/file_source.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The primary source of gems is a file on disk, including all usages # internal to rubygems. diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb index 03d7714524..227835dfce 100644 --- a/lib/rubygems/package/io_source.rb +++ b/lib/rubygems/package/io_source.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Supports reading and writing gems from/to a generic IO object. This is # useful for other applications built on top of rubygems, such as diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb index 09a02d3ecd..1a13ac3e29 100644 --- a/lib/rubygems/package/old.rb +++ b/lib/rubygems/package/old.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -69,7 +70,7 @@ class Gem::Package::Old < Gem::Package file_data << line end - file_data = file_data.strip.unpack("m")[0] + file_data = file_data.strip.unpack1("m") file_data = Zlib::Inflate.inflate file_data raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if @@ -77,7 +78,7 @@ class Gem::Package::Old < Gem::Package FileUtils.rm_rf destination - FileUtils.mkdir_p File.dirname(destination), :mode => dir_mode && 0755 + FileUtils.mkdir_p File.dirname(destination), mode: dir_mode && 0o755 File.open destination, "wb", file_mode(entry["mode"]) do |out| out.write file_data diff --git a/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb index 69701e55e9..8c44f8c305 100644 --- a/lib/rubygems/package/source.rb +++ b/lib/rubygems/package/source.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true + class Gem::Package::Source # :nodoc: end diff --git a/lib/rubygems/package/tar_header.rb b/lib/rubygems/package/tar_header.rb index 590a2f0315..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 ## #-- @@ -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 @@ -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 cdc3fdc015..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,7 +92,7 @@ class Gem::Package::TarReader return unless found - return yield found + yield found ensure rewind end diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb index 8634381c18..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 ## @@ -77,9 +102,7 @@ class Gem::Package::TarReader::Entry # Read one byte from the tar entry def getc - check_closed - - return nil if @read >= @header.size + return nil if eof? ret = @io.getc @read += 1 if ret @@ -117,36 +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_writer.rb b/lib/rubygems/package/tar_writer.rb index db5242c5e4..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 @@ -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 @@ -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 8432bc5806..d26411684d 100644 --- a/lib/rubygems/package_task.rb +++ b/lib/rubygems/package_task.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # Copyright (c) 2003, 2004 Jim Weirich, 2009 Eric Hodel # # Permission is hereby granted, free of charge, to any person obtaining @@ -96,13 +97,13 @@ class Gem::PackageTask < Rake::PackageTask gem_path = File.join package_dir, gem_file gem_dir = File.join package_dir, gem_spec.full_name - task :package => [:gem] + task package: [:gem] directory package_dir directory gem_dir desc "Build the gem file #{gem_file}" - task :gem => [gem_path] + task gem: [gem_path] trace = Rake.application.options.trace Gem.configuration.verbose = trace diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb index d601e653c9..13091e29ba 100644 --- a/lib/rubygems/path_support.rb +++ b/lib/rubygems/path_support.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # # Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings @@ -23,23 +24,22 @@ class Gem::PathSupport # hashtable, or defaults to ENV, the system environment. # def initialize(env) - @home = env["GEM_HOME"] || Gem.default_dir - - if File::ALT_SEPARATOR - @home = @home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) - end - - @home = expand(@home) - + @home = normalize_home_dir(env["GEM_HOME"] || Gem.default_dir) @path = split_gem_path env["GEM_PATH"], @home @spec_cache_dir = env["GEM_SPEC_CACHE"] || Gem.default_spec_cache_dir - - @spec_cache_dir = @spec_cache_dir.dup.tap(&Gem::UNTAINT) end private + def normalize_home_dir(home) + if File::ALT_SEPARATOR + home = home.gsub(File::ALT_SEPARATOR, File::SEPARATOR) + end + + expand(home) + end + ## # Split the Gem search path (as reported by Gem.path). @@ -52,7 +52,7 @@ class Gem::PathSupport gem_path = gpaths.split(Gem.path_separator) # Handle the path_separator being set to a regexp, which will cause # end_with? to error - if gpaths =~ /#{Gem.path_separator}\z/ + if /#{Gem.path_separator}\z/.match?(gpaths) gem_path += default_path end diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb index f4983c1153..48b7344aee 100644 --- a/lib/rubygems/platform.rb +++ b/lib/rubygems/platform.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "deprecate" ## @@ -12,15 +13,22 @@ 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| @@ -35,10 +43,20 @@ class Gem::Platform match_gem?(spec.platform, spec.name) end - def self.match_gem?(platform, gem_name) - # Note: this method might be redefined by Ruby implementations to - # customize behavior per RUBY_ENGINE, gem_name or other criteria. - match_platforms?(platform, Gem.platforms) + if RUBY_ENGINE == "truffleruby" + def self.match_gem?(platform, gem_name) + raise "Not a string: #{gem_name.inspect}" unless String === gem_name + + if REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(gem_name) + match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform.local]) + else + match_platforms?(platform, Gem.platforms) + end + end + else + def self.match_gem?(platform, gem_name) + match_platforms?(platform, Gem.platforms) + end end def self.sort_priority(platform) @@ -71,7 +89,7 @@ class Gem::Platform when String then arch = arch.split "-" - if arch.length > 2 && arch.last !~ /\d+(\.\d+)?$/ # reassemble x86-linux-{libc} + if arch.length > 2 && !arch.last.match?(/\d+(\.\d+)?$/) # reassemble x86-linux-{libc} extra = arch.pop arch.last << "-#{extra}" end @@ -79,42 +97,46 @@ class Gem::Platform cpu = arch.shift @cpu = case cpu - when /i\d86/ then "x86" - else cpu + when /i\d86/ then "x86" + else cpu end - if arch.length == 2 && 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 /^java$/, /^jruby$/ then [ "java", nil ] - when /^java([\d.]*)/ then [ "java", $1 ] - when /^dalvik(\d+)?$/ then [ "dalvik", $1 ] - when /^dotnet$/ then [ "dotnet", nil ] - when /^dotnet([\d.]*)/ then [ "dotnet", $1 ] - when /linux-?(\w+)?/ then [ "linux", $1 ] - when /mingw32/ then [ "mingw32", nil ] - when /mingw-?(\w+)?/ then [ "mingw", $1 ] - when /(mswin\d+)(\_(\d+))?/ then - os, version = $1, $3 - @cpu = "x86" if @cpu.nil? && os =~ /32$/ - [os, version] - when /netbsdelf/ then [ "netbsdelf", nil ] - when /openbsd(\d+\.\d+)?/ then [ "openbsd", $1 ] - when /solaris(\d+\.\d+)?/ then [ "solaris", $1 ] - # test - when /^(\w+_platform)(\d+)?/ then [ $1, $2 ] - else [ "unknown", nil ] + when /aix(\d+)?/ then ["aix", $1] + when /cygwin/ then ["cygwin", nil] + when /darwin(\d+)?/ then ["darwin", $1] + when /^macruby$/ then ["macruby", nil] + when /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 = $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 /solaris(\d+\.\d+)?/ then ["solaris", $1] + # test + when /^(\w+_platform)(\d+)?/ then [$1, $2] + else ["unknown", nil] end when Gem::Platform then @cpu = arch.cpu @@ -141,7 +163,7 @@ class Gem::Platform self.class === other && to_a == other.to_a end - alias :eql? :== + alias_method :eql?, :== def hash # :nodoc: to_a.hash @@ -209,18 +231,18 @@ class Gem::Platform when String then # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007 other = case other - when /^i686-darwin(\d)/ then ["x86", "darwin", $1 ] - when /^i\d86-linux/ then ["x86", "linux", nil ] - when "java", "jruby" then [nil, "java", nil ] - when /^dalvik(\d+)?$/ then [nil, "dalvik", $1 ] - when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2 ] - when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2 ] - when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2 ] - when "powerpc-darwin" then ["powerpc", "darwin", nil ] - when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1 ] - when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8" ] - when /universal-darwin(\d)/ then ["universal", "darwin", $1 ] - else other + when /^i686-darwin(\d)/ then ["x86", "darwin", $1] + when /^i\d86-linux/ then ["x86", "linux", nil] + when "java", "jruby" then [nil, "java", nil] + when /^dalvik(\d+)?$/ then [nil, "dalvik", $1] + when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2] + when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2] + when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2] + when "powerpc-darwin" then ["powerpc", "darwin", nil] + when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1] + when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"] + when /universal-darwin(\d)/ then ["universal", "darwin", $1] + else other end other = Gem::Platform.new other diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb index b90f9f7d1d..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 @@ -13,6 +14,10 @@ module Gem @emitter.scalar str, nil, nil, false, true, quote end + def visit_Hash(o) + super(o.compact) + end + # Noop this out so there are no anchors def register(target, obj) end diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb index c72955f83b..a95a759401 100644 --- a/lib/rubygems/query_utils.rb +++ b/lib/rubygems/query_utils.rb @@ -6,7 +6,6 @@ require_relative "version_option" require_relative "text" module Gem::QueryUtils - include Gem::Text include Gem::LocalRemoteOptions include Gem::VersionOption @@ -17,7 +16,7 @@ module Gem::QueryUtils options[:installed] = value end - add_option("-I", "Equivalent to --no-installed") do |value, options| + add_option("-I", "Equivalent to --no-installed") do |_value, options| options[:installed] = false end @@ -85,7 +84,7 @@ module Gem::QueryUtils installed = !installed unless options[:installed] say(installed) - exit_code = 1 if !installed + exit_code = 1 unless installed end exit_code @@ -119,7 +118,7 @@ module Gem::QueryUtils end end - #Guts of original execute + # Guts of original execute def show_gems(name) show_local_gems(name) if local? show_remote_gems(name) if remote? @@ -197,7 +196,7 @@ module Gem::QueryUtils end def output_versions(output, versions) - versions.each do |gem_name, matching_tuples| + versions.each do |_gem_name, matching_tuples| matching_tuples = matching_tuples.sort_by {|n,_| n.version }.reverse platforms = Hash.new {|h,version| h[version] = [] } @@ -243,7 +242,7 @@ module Gem::QueryUtils list = if platforms.empty? || options[:details] - name_tuples.map {|n| n.version }.uniq + name_tuples.map(&:version).uniq else platforms.sort.reverse.map do |version, pls| out = version.to_s @@ -264,7 +263,7 @@ module Gem::QueryUtils end end - entry << " (#{list.join ', '})" + entry << " (#{list.join ", "})" end def make_entry(entry_tuples, platforms) @@ -283,7 +282,7 @@ module Gem::QueryUtils end def spec_authors(entry, spec) - authors = "Author#{spec.authors.length > 1 ? 's' : ''}: ".dup + authors = "Author#{spec.authors.length > 1 ? "s" : ""}: ".dup authors << spec.authors.join(", ") entry << format_text(authors, 68, 4) end @@ -297,7 +296,7 @@ module Gem::QueryUtils def spec_license(entry, spec) return if spec.license.nil? || spec.license.empty? - licenses = "License#{spec.licenses.length > 1 ? 's' : ''}: ".dup + licenses = "License#{spec.licenses.length > 1 ? "s" : ""}: ".dup licenses << spec.licenses.join(", ") entry << "\n" << format_text(licenses, 68, 4) end @@ -312,8 +311,8 @@ module Gem::QueryUtils label = "Installed at" specs.each do |s| version = s.version.to_s - version << ", default" if s.default_gem? - entry << "\n" << " #{label} (#{version}): #{s.base_dir}" + default = ", default" if s.default_gem? + entry << "\n" << " #{label} (#{version}#{default}): #{s.base_dir}" label = " " * label.length end end @@ -328,11 +327,11 @@ module Gem::QueryUtils if platforms.length == 1 title = platforms.values.length == 1 ? "Platform" : "Platforms" - entry << " #{title}: #{platforms.values.sort.join(', ')}\n" + entry << " #{title}: #{platforms.values.sort.join(", ")}\n" else entry << " Platforms:\n" - sorted_platforms = platforms.sort_by {|version,| version } + sorted_platforms = platforms.sort sorted_platforms.each do |version, pls| label = " #{version}: " @@ -347,5 +346,4 @@ module Gem::QueryUtils summary = truncate_text(spec.summary, "the summary for #{spec.full_name}") entry << "\n\n" << format_text(summary, 68, 4) end - end diff --git a/lib/rubygems/rdoc.rb b/lib/rubygems/rdoc.rb index 769ec61d1e..907dcd9431 100644 --- a/lib/rubygems/rdoc.rb +++ b/lib/rubygems/rdoc.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" begin diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb index 0ac6eaa130..c3a41592f6 100644 --- a/lib/rubygems/remote_fetcher.rb +++ b/lib/rubygems/remote_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../rubygems" require_relative "request" require_relative "request/connection_pools" @@ -52,7 +53,7 @@ class Gem::RemoteFetcher # Cached RemoteFetcher instance. def self.fetcher - @fetcher ||= self.new Gem.configuration[:http_proxy] + @fetcher ||= new Gem.configuration[:http_proxy] end attr_accessor :headers @@ -73,9 +74,9 @@ class Gem::RemoteFetcher def initialize(proxy=nil, dns=nil, headers={}) require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled - require "net/http" + require_relative "vendored_net_http" require "stringio" - require "uri" + require_relative "vendor/uri/lib/uri" Socket.do_not_reverse_lookup = true @@ -114,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) && (!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" @@ -124,14 +125,18 @@ 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 = Gem::Uri.new(source_uri) scheme = source_uri.scheme - # URI.parse gets confused by MS Windows paths with forward slashes. - scheme = nil if scheme =~ /^[a-z]$/i + # Gem::URI.parse gets confused by MS Windows paths with forward slashes. + scheme = nil if /^[a-z]$/i.match?(scheme) # REFACTOR: split this up and dispatch on scheme (eg download_http) # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner @@ -143,7 +148,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{gem_file_name}" - self.cache_update_path remote_gem_path, local_gem_path + cache_update_path remote_gem_path, local_gem_path rescue FetchError raise if spec.original_platform == spec.platform @@ -153,7 +158,7 @@ class Gem::RemoteFetcher remote_gem_path = source_uri + "gems/#{alternate_name}" - self.cache_update_path remote_gem_path, local_gem_path + cache_update_path remote_gem_path, local_gem_path end end when "file" then @@ -169,7 +174,7 @@ 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}" @@ -205,17 +210,17 @@ class Gem::RemoteFetcher # HTTP Fetcher. Dispatched by +fetch_path+. Use it instead. def fetch_http(uri, last_modified = nil, head = false, depth = 0) - fetch_type = head ? Net::HTTP::Head : Net::HTTP::Get + fetch_type = head ? Gem::Net::HTTP::Head : Gem::Net::HTTP::Get response = request uri, fetch_type, last_modified do |req| headers.each {|k,v| req.add_field(k,v) } end case response - when Net::HTTPOK, Net::HTTPNotModified then + when Gem::Net::HTTPOK, Gem::Net::HTTPNotModified then response.uri = uri head ? response : response.body - when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther, - Net::HTTPTemporaryRedirect then + when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther, + Gem::Net::HTTPTemporaryRedirect then raise FetchError.new("too many redirects", uri) if depth > 10 unless location = response["Location"] @@ -233,7 +238,7 @@ class Gem::RemoteFetcher end end - alias :fetch_https :fetch_http + alias_method :fetch_https, :fetch_http ## # Downloads +uri+ and returns it as a String. @@ -256,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 @@ -280,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) @@ -296,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) @@ -312,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 c3ea46e0eb..9116785231 100644 --- a/lib/rubygems/request.rb +++ b/lib/rubygems/request.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require "net/http" + +require_relative "vendored_net_http" require_relative "user_interaction" class Gem::Request @@ -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,14 +30,19 @@ class Gem::Request @uri = uri @request_class = request_class @last_modified = last_modified - @requests = Hash.new 0 + @requests = Hash.new(0).compare_by_identity @user_agent = user_agent @connection_pool = pool end - def proxy_uri; @connection_pool.proxy_uri; end - def cert_files; @connection_pool.cert_files; end + def proxy_uri + @connection_pool.proxy_uri + end + + def cert_files + @connection_pool.cert_files + end def self.get_cert_files pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__) @@ -159,23 +165,22 @@ class Gem::Request # environment variables. def self.get_proxy_from_env(scheme = "http") - _scheme = scheme.downcase - _SCHEME = scheme.upcase - env_proxy = ENV["#{_scheme}_proxy"] || ENV["#{_SCHEME}_PROXY"] + downcase_scheme = scheme.downcase + upcase_scheme = scheme.upcase + env_proxy = ENV["#{downcase_scheme}_proxy"] || ENV["#{upcase_scheme}_PROXY"] no_env_proxy = env_proxy.nil? || env_proxy.empty? if no_env_proxy - return (_scheme == "https" || _scheme == "http") ? - :no_proxy : get_proxy_from_env("http") + return ["https", "http"].include?(downcase_scheme) ? :no_proxy : get_proxy_from_env("http") end require "uri" - uri = URI(Gem::UriFormatter.new(env_proxy).normalize) + uri = Gem::URI(Gem::UriFormatter.new(env_proxy).normalize) if uri && uri.user.nil? && uri.password.nil? - user = ENV["#{_scheme}_proxy_user"] || ENV["#{_SCHEME}_PROXY_USER"] - password = ENV["#{_scheme}_proxy_pass"] || ENV["#{_SCHEME}_PROXY_PASS"] + user = ENV["#{downcase_scheme}_proxy_user"] || ENV["#{upcase_scheme}_PROXY_USER"] + password = ENV["#{downcase_scheme}_proxy_pass"] || ENV["#{upcase_scheme}_PROXY_PASS"] uri.user = Gem::UriFormatter.new(user).escape uri.password = Gem::UriFormatter.new(password).escape @@ -191,7 +196,7 @@ class Gem::Request bad_response = false begin - @requests[connection.object_id] += 1 + @requests[connection] += 1 verbose "#{request.method} #{Gem::Uri.redact(@uri)}" @@ -200,7 +205,7 @@ class Gem::Request if request.response_body_permitted? && file_name =~ /\.gem$/ reporter = ui.download_reporter response = connection.request(request) do |incomplete_response| - if Net::HTTPOK === incomplete_response + if Gem::Net::HTTPOK === incomplete_response reporter.fetch(file_name, incomplete_response.content_length) downloaded = 0 data = String.new @@ -223,8 +228,7 @@ class Gem::Request end verbose "#{response.code} #{response.message}" - - rescue Net::HTTPBadResponse + rescue Gem::Net::HTTPBadResponse verbose "bad response" reset connection @@ -233,17 +237,17 @@ class Gem::Request bad_response = true retry - rescue Net::HTTPFatalError + rescue Gem::Net::HTTPFatalError verbose "fatal error" raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri) - # HACK work around EOFError bug in Net::HTTP + # HACK: work around EOFError bug in Gem::Net::HTTP # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible # to install gems. - rescue EOFError, Timeout::Error, + rescue EOFError, Gem::Timeout::Error, Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE - requests = @requests[connection.object_id] + requests = @requests[connection] verbose "connection reset after #{requests} requests, retrying" raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried @@ -263,7 +267,7 @@ class Gem::Request # Resets HTTP connection +connection+. def reset(connection) - @requests.delete connection.object_id + @requests.delete connection connection.finish connection.start @@ -278,7 +282,7 @@ class Gem::Request ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}" if RUBY_PATCHLEVEL >= 0 ua << " patchlevel #{RUBY_PATCHLEVEL}" - elsif defined?(RUBY_REVISION) + else ua << " revision #{RUBY_REVISION}" end ua << ")" diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb index 44280489fb..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 @@ -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 @@ -45,7 +45,7 @@ class Gem::Request::ConnectionPools # :nodoc: end def https?(uri) - uri.scheme.downcase == "https" + uri.scheme.casecmp("https").zero? end def no_proxy?(host, env_no_proxy) diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb index 7b309eedd3..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 diff --git a/lib/rubygems/request/https_pool.rb b/lib/rubygems/request/https_pool.rb index 50f42d9e0d..cb1d4b59b6 100644 --- a/lib/rubygems/request/https_pool.rb +++ b/lib/rubygems/request/https_pool.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + class Gem::Request::HTTPSPool < Gem::Request::HTTPPool # :nodoc: private diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb index 64701a8214..875df7e019 100644 --- a/lib/rubygems/request_set.rb +++ b/lib/rubygems/request_set.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true -require_relative "tsort" + +require_relative "vendored_tsort" ## # A RequestSet groups a request to activate a set of dependencies. @@ -107,7 +108,7 @@ class Gem::RequestSet @requests = [] @sets = [] @soft_missing = false - @sorted = nil + @sorted_requests = nil @specs = nil @vendor_set = nil @source_set = nil @@ -159,7 +160,7 @@ class Gem::RequestSet end # Create N threads in a pool, have them download all the gems - threads = Gem.configuration.concurrent_downloads.times.map do + threads = Array.new(Gem.configuration.concurrent_downloads) do # When a thread pops this item, it knows to stop running. The symbol # is queued here so that there will be one symbol per thread. download_queue << :stop @@ -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 } @@ -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, [] @@ -374,7 +376,7 @@ class Gem::RequestSet q.text "sets:" q.breakable - q.pp @sets.map {|set| set.class } + q.pp @sets.map(&:class) end end @@ -424,11 +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) @@ -446,7 +448,7 @@ class Gem::RequestSet next if dep.type == :development && !@development match = @requests.find do |r| - dep.match? r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease + dep.match?(r.spec.name, r.spec.version, r.spec.is_a?(Gem::Resolver::InstalledSpecification) || @prerelease) end unless match diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb index ad6e45005b..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,22 +33,22 @@ class Gem::RequestSet::GemDependencyAPI ENGINE_MAP = { # :nodoc: - :jruby => %w[jruby], - :jruby_18 => %w[jruby], - :jruby_19 => %w[jruby], - :maglev => %w[maglev], - :mri => %w[ruby], - :mri_18 => %w[ruby], - :mri_19 => %w[ruby], - :mri_20 => %w[ruby], - :mri_21 => %w[ruby], - :rbx => %w[rbx], - :truffleruby => %w[truffleruby], - :ruby => %w[ruby rbx maglev truffleruby], - :ruby_18 => %w[ruby rbx maglev truffleruby], - :ruby_19 => %w[ruby rbx maglev truffleruby], - :ruby_20 => %w[ruby rbx maglev truffleruby], - :ruby_21 => %w[ruby rbx maglev truffleruby], + jruby: %w[jruby], + jruby_18: %w[jruby], + jruby_19: %w[jruby], + maglev: %w[maglev], + mri: %w[ruby], + mri_18: %w[ruby], + mri_19: %w[ruby], + mri_20: %w[ruby], + mri_21: %w[ruby], + rbx: %w[rbx], + truffleruby: %w[truffleruby], + ruby: %w[ruby rbx maglev truffleruby], + ruby_18: %w[ruby rbx maglev truffleruby], + ruby_19: %w[ruby rbx maglev truffleruby], + ruby_20: %w[ruby rbx maglev truffleruby], + ruby_21: %w[ruby rbx maglev truffleruby], }.freeze mswin = Gem::Platform.new "x86-mswin32" @@ -56,39 +57,39 @@ class Gem::RequestSet::GemDependencyAPI x64_mingw = Gem::Platform.new "x64-mingw32" PLATFORM_MAP = { # :nodoc: - :jruby => Gem::Platform::RUBY, - :jruby_18 => Gem::Platform::RUBY, - :jruby_19 => Gem::Platform::RUBY, - :maglev => Gem::Platform::RUBY, - :mingw => x86_mingw, - :mingw_18 => x86_mingw, - :mingw_19 => x86_mingw, - :mingw_20 => x86_mingw, - :mingw_21 => x86_mingw, - :mri => Gem::Platform::RUBY, - :mri_18 => Gem::Platform::RUBY, - :mri_19 => Gem::Platform::RUBY, - :mri_20 => Gem::Platform::RUBY, - :mri_21 => Gem::Platform::RUBY, - :mswin => mswin, - :mswin_18 => mswin, - :mswin_19 => mswin, - :mswin_20 => mswin, - :mswin_21 => mswin, - :mswin64 => mswin64, - :mswin64_19 => mswin64, - :mswin64_20 => mswin64, - :mswin64_21 => mswin64, - :rbx => Gem::Platform::RUBY, - :ruby => Gem::Platform::RUBY, - :ruby_18 => Gem::Platform::RUBY, - :ruby_19 => Gem::Platform::RUBY, - :ruby_20 => Gem::Platform::RUBY, - :ruby_21 => Gem::Platform::RUBY, - :truffleruby => Gem::Platform::RUBY, - :x64_mingw => x64_mingw, - :x64_mingw_20 => x64_mingw, - :x64_mingw_21 => x64_mingw, + jruby: Gem::Platform::RUBY, + jruby_18: Gem::Platform::RUBY, + jruby_19: Gem::Platform::RUBY, + maglev: Gem::Platform::RUBY, + mingw: x86_mingw, + mingw_18: x86_mingw, + mingw_19: x86_mingw, + mingw_20: x86_mingw, + mingw_21: x86_mingw, + mri: Gem::Platform::RUBY, + mri_18: Gem::Platform::RUBY, + mri_19: Gem::Platform::RUBY, + mri_20: Gem::Platform::RUBY, + mri_21: Gem::Platform::RUBY, + mswin: mswin, + mswin_18: mswin, + mswin_19: mswin, + mswin_20: mswin, + mswin_21: mswin, + mswin64: mswin64, + mswin64_19: mswin64, + mswin64_20: mswin64, + mswin64_21: mswin64, + rbx: Gem::Platform::RUBY, + ruby: Gem::Platform::RUBY, + ruby_18: Gem::Platform::RUBY, + ruby_19: Gem::Platform::RUBY, + ruby_20: Gem::Platform::RUBY, + ruby_21: Gem::Platform::RUBY, + truffleruby: Gem::Platform::RUBY, + x64_mingw: x64_mingw, + x64_mingw_20: x64_mingw, + x64_mingw_21: x64_mingw, }.freeze gt_eq_0 = Gem::Requirement.new ">= 0" @@ -98,70 +99,70 @@ class Gem::RequestSet::GemDependencyAPI tilde_gt_2_1_0 = Gem::Requirement.new "~> 2.1.0" VERSION_MAP = { # :nodoc: - :jruby => gt_eq_0, - :jruby_18 => tilde_gt_1_8_0, - :jruby_19 => tilde_gt_1_9_0, - :maglev => gt_eq_0, - :mingw => gt_eq_0, - :mingw_18 => tilde_gt_1_8_0, - :mingw_19 => tilde_gt_1_9_0, - :mingw_20 => tilde_gt_2_0_0, - :mingw_21 => tilde_gt_2_1_0, - :mri => gt_eq_0, - :mri_18 => tilde_gt_1_8_0, - :mri_19 => tilde_gt_1_9_0, - :mri_20 => tilde_gt_2_0_0, - :mri_21 => tilde_gt_2_1_0, - :mswin => gt_eq_0, - :mswin_18 => tilde_gt_1_8_0, - :mswin_19 => tilde_gt_1_9_0, - :mswin_20 => tilde_gt_2_0_0, - :mswin_21 => tilde_gt_2_1_0, - :mswin64 => gt_eq_0, - :mswin64_19 => tilde_gt_1_9_0, - :mswin64_20 => tilde_gt_2_0_0, - :mswin64_21 => tilde_gt_2_1_0, - :rbx => gt_eq_0, - :ruby => gt_eq_0, - :ruby_18 => tilde_gt_1_8_0, - :ruby_19 => tilde_gt_1_9_0, - :ruby_20 => tilde_gt_2_0_0, - :ruby_21 => tilde_gt_2_1_0, - :truffleruby => gt_eq_0, - :x64_mingw => gt_eq_0, - :x64_mingw_20 => tilde_gt_2_0_0, - :x64_mingw_21 => tilde_gt_2_1_0, + jruby: gt_eq_0, + jruby_18: tilde_gt_1_8_0, + jruby_19: tilde_gt_1_9_0, + maglev: gt_eq_0, + mingw: gt_eq_0, + mingw_18: tilde_gt_1_8_0, + mingw_19: tilde_gt_1_9_0, + mingw_20: tilde_gt_2_0_0, + mingw_21: tilde_gt_2_1_0, + mri: gt_eq_0, + mri_18: tilde_gt_1_8_0, + mri_19: tilde_gt_1_9_0, + mri_20: tilde_gt_2_0_0, + mri_21: tilde_gt_2_1_0, + mswin: gt_eq_0, + mswin_18: tilde_gt_1_8_0, + mswin_19: tilde_gt_1_9_0, + mswin_20: tilde_gt_2_0_0, + mswin_21: tilde_gt_2_1_0, + mswin64: gt_eq_0, + mswin64_19: tilde_gt_1_9_0, + mswin64_20: tilde_gt_2_0_0, + mswin64_21: tilde_gt_2_1_0, + rbx: gt_eq_0, + ruby: gt_eq_0, + ruby_18: tilde_gt_1_8_0, + ruby_19: tilde_gt_1_9_0, + ruby_20: tilde_gt_2_0_0, + ruby_21: tilde_gt_2_1_0, + truffleruby: gt_eq_0, + x64_mingw: gt_eq_0, + x64_mingw_20: tilde_gt_2_0_0, + x64_mingw_21: tilde_gt_2_1_0, }.freeze WINDOWS = { # :nodoc: - :mingw => :only, - :mingw_18 => :only, - :mingw_19 => :only, - :mingw_20 => :only, - :mingw_21 => :only, - :mri => :never, - :mri_18 => :never, - :mri_19 => :never, - :mri_20 => :never, - :mri_21 => :never, - :mswin => :only, - :mswin_18 => :only, - :mswin_19 => :only, - :mswin_20 => :only, - :mswin_21 => :only, - :mswin64 => :only, - :mswin64_19 => :only, - :mswin64_20 => :only, - :mswin64_21 => :only, - :rbx => :never, - :ruby => :never, - :ruby_18 => :never, - :ruby_19 => :never, - :ruby_20 => :never, - :ruby_21 => :never, - :x64_mingw => :only, - :x64_mingw_20 => :only, - :x64_mingw_21 => :only, + mingw: :only, + mingw_18: :only, + mingw_19: :only, + mingw_20: :only, + mingw_21: :only, + mri: :never, + mri_18: :never, + mri_19: :never, + mri_20: :never, + mri_21: :never, + mswin: :only, + mswin_18: :only, + mswin_19: :only, + mswin_20: :only, + mswin_21: :only, + mswin64: :only, + mswin64_19: :only, + mswin64_20: :only, + mswin64_21: :only, + rbx: :never, + ruby: :never, + ruby_18: :never, + ruby_19: :never, + ruby_20: :never, + ruby_21: :never, + x64_mingw: :only, + x64_mingw_20: :only, + x64_mingw_21: :only, }.freeze ## @@ -261,7 +262,7 @@ class Gem::RequestSet::GemDependencyAPI raise ArgumentError, "no gemspecs found at #{Dir.pwd}" else raise ArgumentError, - "found multiple gemspecs at #{Dir.pwd}, " + + "found multiple gemspecs at #{Dir.pwd}, " \ "use the name: option to specify the one you want" end end @@ -279,7 +280,7 @@ class Gem::RequestSet::GemDependencyAPI # Loads the gem dependency file and returns self. def load - instance_eval File.read(@path).tap(&Gem::UNTAINT), @path, 1 + instance_eval File.read(@path), @path, 1 self end @@ -356,7 +357,7 @@ class Gem::RequestSet::GemDependencyAPI # Use the given tag for git:, gist: and github: dependencies. def gem(name, *requirements) - options = requirements.pop if requirements.last.kind_of?(Hash) + options = requirements.pop if requirements.last.is_a?(Hash) options ||= {} options[:git] = @current_repository if @current_repository @@ -435,7 +436,6 @@ Gem dependencies file #{@path} requires #{name} more than once. reference ||= ref reference ||= branch reference ||= tag - reference ||= "master" if ref && branch warn <<-WARNING @@ -533,8 +533,8 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta # platform matches the current platform. def gem_platforms(name, options) # :nodoc: - platform_names = Array(options.delete :platform) - platform_names.concat Array(options.delete :platforms) + platform_names = Array(options.delete(:platform)) + platform_names.concat Array(options.delete(:platforms)) platform_names.concat @current_platforms if @current_platforms return true if platform_names.empty? @@ -593,7 +593,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_repository = repository yield - ensure @current_repository = nil end @@ -685,7 +684,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_groups = groups yield - ensure @current_groups = nil end @@ -760,7 +758,6 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta @current_platforms = platforms yield - ensure @current_platforms = nil end @@ -771,7 +768,7 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta # Block form for restricting gems to a particular set of platforms. See # #platform. - alias :platforms :platform + alias_method :platforms, :platform ## # :category: Gem Dependencies DSL @@ -793,15 +790,15 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta return true if @installing - unless RUBY_VERSION == version - message = "Your Ruby version is #{RUBY_VERSION}, " + + unless version == RUBY_VERSION + message = "Your Ruby version is #{RUBY_VERSION}, " \ "but your #{gem_deps_file} requires #{version}" raise Gem::RubyVersionMismatch, message end if engine && engine != Gem.ruby_engine - message = "Your Ruby engine is #{Gem.ruby_engine}, " + + message = "Your Ruby engine is #{Gem.ruby_engine}, " \ "but your #{gem_deps_file} requires #{engine}" raise Gem::RubyVersionMismatch, message @@ -810,14 +807,14 @@ Gem dependencies file #{@path} includes git reference for both ref/branch and ta if engine_version if engine_version != RUBY_ENGINE_VERSION message = - "Your Ruby engine version is #{Gem.ruby_engine} #{RUBY_ENGINE_VERSION}, " + + "Your Ruby engine version is #{Gem.ruby_engine} #{RUBY_ENGINE_VERSION}, " \ "but your #{gem_deps_file} requires #{engine} #{engine_version}" raise Gem::RubyVersionMismatch, message end end - return true + true end ## diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb index 3ba202f661..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 @@ -75,18 +76,13 @@ class Gem::RequestSet::Lockfile @dependencies = dependencies @gem_deps_file = File.expand_path(gem_deps_file) @gem_deps_dir = File.dirname(@gem_deps_file) - - if RUBY_VERSION < "2.7" - @gem_deps_file.untaint unless gem_deps_file.tainted? - end - @platforms = [] end def add_DEPENDENCIES(out) # :nodoc: out << "DEPENDENCIES" - out.concat @dependencies.sort_by {|name,| name }.map {|name, requirement| + out.concat @dependencies.sort.map {|name, requirement| " #{name}#{requirement.for_lockfile}" } @@ -105,10 +101,10 @@ class Gem::RequestSet::Lockfile out << " remote: #{group}" out << " specs:" - requests.sort_by {|request| request.name }.each do |request| + requests.sort_by(&:name).each do |request| next if request.spec.name == "bundler" platform = "-#{request.spec.platform}" unless - Gem::Platform::RUBY == request.spec.platform + request.spec.platform == Gem::Platform::RUBY out << " #{request.name} (#{request.version}#{platform})" @@ -137,10 +133,10 @@ class Gem::RequestSet::Lockfile out << " revision: #{revision}" out << " specs:" - requests.sort_by {|request| request.name }.each do |request| + requests.sort_by(&:name).each do |request| out << " #{request.name} (#{request.version})" - dependencies = request.spec.dependencies.sort_by {|dep| dep.name } + dependencies = request.spec.dependencies.sort_by(&:name) dependencies.each do |dep| out << " #{dep.name}#{dep.requirement.for_lockfile}" end @@ -184,7 +180,7 @@ class Gem::RequestSet::Lockfile platforms = requests.map {|request| request.spec.platform }.uniq - platforms = platforms.sort_by {|platform| platform.to_s } + platforms = platforms.sort_by(&:to_s) platforms.each do |platform| out << " #{platform}" diff --git a/lib/rubygems/request_set/lockfile/parser.rb b/lib/rubygems/request_set/lockfile/parser.rb index 2d98c9520b..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 @@ -47,7 +48,7 @@ class Gem::RequestSet::Lockfile::Parser 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 @@ -56,8 +57,8 @@ class Gem::RequestSet::Lockfile::Parser 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 !@tokens.empty? && :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text requirements = [] @@ -110,7 +111,7 @@ class Gem::RequestSet::Lockfile::Parser def parse_GEM # :nodoc: sources = [] - while [:entry, "remote"] == peek.first(2) do + while peek.first(2) == [:entry, "remote"] do get :entry, "remote" data = get(:text).value skip :newline @@ -127,7 +128,7 @@ class Gem::RequestSet::Lockfile::Parser set = Gem::Resolver::LockSet.new sources last_specs = nil - while !@tokens.empty? && :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text name = token.value column = token.column @@ -199,7 +200,7 @@ class Gem::RequestSet::Lockfile::Parser last_spec = nil - while !@tokens.empty? && :text == peek.type do + while !@tokens.empty? && peek.type == :text do token = get :text name = token.value column = token.column @@ -246,7 +247,7 @@ class Gem::RequestSet::Lockfile::Parser set = Gem::Resolver::VendorSet.new last_spec = nil - while !@tokens.empty? && :text == peek.first do + while !@tokens.empty? && peek.first == :text do token = get :text name = token.value column = token.column @@ -281,7 +282,7 @@ class Gem::RequestSet::Lockfile::Parser end def parse_PLATFORMS # :nodoc: - while !@tokens.empty? && :text == peek.first do + while !@tokens.empty? && peek.first == :text do name = get(:text).value @platforms << name diff --git a/lib/rubygems/request_set/lockfile/tokenizer.rb b/lib/rubygems/request_set/lockfile/tokenizer.rb index 4476a041c4..65cef3baa0 100644 --- a/lib/rubygems/request_set/lockfile/tokenizer.rb +++ b/lib/rubygems/request_set/lockfile/tokenizer.rb @@ -1,4 +1,6 @@ -#) frozen_string_literal: true +# frozen_string_literal: true + +# ) frozen_string_literal: true require_relative "parser" class Gem::RequestSet::Lockfile::Tokenizer @@ -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 @@ -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 bc2fd9af55..02543cb14a 100644 --- a/lib/rubygems/requirement.rb +++ b/lib/rubygems/requirement.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "version" ## @@ -9,7 +10,7 @@ require_relative "version" # together in RubyGems. class Gem::Requirement - OPS = { #:nodoc: + OPS = { # :nodoc: "=" => lambda {|v, r| v == r }, "!=" => lambda {|v, r| v != r }, ">" => lambda {|v, r| v > r }, @@ -22,12 +23,12 @@ class Gem::Requirement SOURCE_SET_REQUIREMENT = Struct.new(:for_lockfile).new "!" # :nodoc: quoted = OPS.keys.map {|k| Regexp.quote k }.join "|" - PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*" # :nodoc: + PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*".freeze # :nodoc: ## # A regular expression that matches a requirement - PATTERN = /\A#{PATTERN_RAW}\z/.freeze + PATTERN = /\A#{PATTERN_RAW}\z/ ## # The default requirement matches any non-prerelease version @@ -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 ## @@ -244,8 +245,8 @@ class Gem::Requirement 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. @@ -283,6 +284,11 @@ class Gem::Requirement def _tilde_requirements @_tilde_requirements ||= _sorted_requirements.select {|r| r.first == "~>" } end + + def initialize_copy(other) # :nodoc: + @requirements = other.requirements.dup + super + end end class Gem::Version diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb index 76d1e9d0cc..115c716b6b 100644 --- a/lib/rubygems/resolver.rb +++ b/lib/rubygems/resolver.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "dependency" require_relative "exceptions" require_relative "util/list" @@ -10,7 +11,7 @@ require_relative "util/list" # all the requirements. class Gem::Resolver - require_relative "resolver/molinillo" + require_relative "vendored_molinillo" ## # If the DEBUG_RESOLVER environment variable is set then debugging mode is @@ -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 @@ -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 @@ -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: @@ -170,7 +167,7 @@ class Gem::Resolver reqs end - include Molinillo::UI + include Gem::Molinillo::UI def output @output ||= debug? ? $stdout : File.open(IO::NULL, "w") @@ -180,15 +177,14 @@ class Gem::Resolver 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 @@ -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, spec.platform =~ Gem::Platform.local ? 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 diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb index 27877e0f4b..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 ## diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb index f2bef54a9c..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. @@ -29,7 +30,7 @@ class Gem::Resolver::APISet < Gem::Resolver::Set def initialize(dep_uri = "https://index.rubygems.org/info/") super() - dep_uri = URI dep_uri unless URI === dep_uri + dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri @dep_uri = dep_uri @uri = dep_uri + ".." @@ -75,7 +76,8 @@ class Gem::Resolver::APISet < Gem::Resolver::Set end def prefetch_now # :nodoc: - needed, @to_fetch = @to_fetch, [] + needed = @to_fetch + @to_fetch = [] needed.sort.each do |name| versions(name) 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 1e65d5e5a9..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) diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb index 075ee1ef5c..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. # @@ -59,8 +60,8 @@ 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 && set.dep_uri == uri 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 aba6d73ea7..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. @@ -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 ## @@ -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 diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb index c3aa3a2c37..370e445089 100644 --- a/lib/rubygems/resolver/current_set.rb +++ b/lib/rubygems/resolver/current_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A set which represents the installed gems. Respects # all the normal settings that control where to look diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb index 70a61cbc25..60b338277f 100644 --- a/lib/rubygems/resolver/dependency_request.rb +++ b/lib/rubygems/resolver/dependency_request.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Used Internally. Wraps a Dependency object to also track which spec # contained the Dependency. diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb index f010273a8f..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. # diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb index 6a178ea82e..e587c17d2a 100644 --- a/lib/rubygems/resolver/git_specification.rb +++ b/lib/rubygems/resolver/git_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A GitSpecification represents a gem that is sourced from a git repository # and is being loaded through a gem dependencies file through the +git:+ diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb index 2344178314..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,10 +44,10 @@ class Gem::Resolver::IndexSet < Gem::Resolver::Set name = req.dependency.name @all[name].each do |uri, n| - if req.match? n, @prerelease - res << Gem::Resolver::IndexSpecification.new( - self, n.name, n.version, uri, n.platform) - end + next unless req.match? n, @prerelease + res << Gem::Resolver::IndexSpecification.new( + self, n.name, n.version, uri, n.platform + ) end res diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb index 0fc758dfd3..7b95608071 100644 --- a/lib/rubygems/resolver/index_specification.rb +++ b/lib/rubygems/resolver/index_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Represents a possible Specification object returned from IndexSet. Used to # delay needed to download full Specification objects when only the +name+ @@ -67,7 +68,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification end def inspect # :nodoc: - "#<%s %s source %s>" % [self.class, full_name, @source] + format("#<%s %s source %s>", self.class, full_name, @source) end def pretty_print(q) # :nodoc: @@ -75,7 +76,7 @@ class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification q.breakable q.text full_name - unless Gem::Platform::RUBY == @platform + unless @platform == Gem::Platform::RUBY q.breakable q.text @platform.to_s end diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb index 8932e068be..8280ae4672 100644 --- a/lib/rubygems/resolver/installed_specification.rb +++ b/lib/rubygems/resolver/installed_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # An InstalledSpecification represents a gem that is already installed # locally. @@ -24,7 +25,7 @@ class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification def installable_platform? # BACKCOMPAT If the file is coming out of a specified file, then we # ignore the platform. This code can be removed in RG 3.0. - return true if @source.kind_of? Gem::Source::SpecificFile + return true if @source.is_a? Gem::Source::SpecificFile super end diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb index 5e18be50ef..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 @@ -147,6 +148,8 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set res << Gem::Resolver::InstalledSpecification.new(self, gemspec) end unless @ignore_installed + matching_local = [] + if consider_local? matching_local = @local.values.select do |spec, _| req.match? spec @@ -160,14 +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.concat @remote_set.find_all req if consider_remote? + res.concat @remote_set.find_all req if consider_remote? && matching_local.empty? res end @@ -183,11 +187,9 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set end def inspect # :nodoc: - always_install = @always_install.map {|s| s.full_name } + always_install = @always_install.map(&:full_name) - "#<%s domain: %s specs: %p always install: %p>" % [ - self.class, @domain, @specs.keys, always_install - ] + format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install) end ## @@ -261,7 +263,7 @@ class Gem::Resolver::InstallerSet < Gem::Resolver::Set unless rrgv.satisfied_by? Gem.rubygems_version rg_version = Gem::VERSION raise Gem::RuntimeRequirementNotMetError, - "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " + + "#{spec.full_name} requires RubyGems version #{rrgv}. The current RubyGems version is #{rg_version}. " \ "Try 'gem update --system' to update RubyGems itself." end end diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb index c27bab0f5a..b57d40e795 100644 --- a/lib/rubygems/resolver/local_specification.rb +++ b/lib/rubygems/resolver/local_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A LocalSpecification comes from a .gem file on the local filesystem. @@ -7,7 +8,7 @@ class Gem::Resolver::LocalSpecification < Gem::Resolver::SpecSpecification # Returns +true+ if this gem is installable for the current platform. def installable_platform? - return true if @source.kind_of? Gem::Source::SpecificFile + return true if @source.is_a? Gem::Source::SpecificFile super end diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb index b1a5433cb5..e5ee32a9a6 100644 --- a/lib/rubygems/resolver/lock_set.rb +++ b/lib/rubygems/resolver/lock_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A set of gems from a gem dependencies lockfile. @@ -74,7 +75,7 @@ class Gem::Resolver::LockSet < Gem::Resolver::Set q.text "specs:" q.breakable - q.pp @specs.map {|spec| spec.full_name } + q.pp @specs.map(&:full_name) end end end diff --git a/lib/rubygems/resolver/lock_specification.rb b/lib/rubygems/resolver/lock_specification.rb index 7de2a14658..06f912dd85 100644 --- a/lib/rubygems/resolver/lock_specification.rb +++ b/lib/rubygems/resolver/lock_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile). # diff --git a/lib/rubygems/resolver/molinillo.rb b/lib/rubygems/resolver/molinillo.rb deleted file mode 100644 index e154342571..0000000000 --- a/lib/rubygems/resolver/molinillo.rb +++ /dev/null @@ -1,2 +0,0 @@ -# frozen_string_literal: true -require_relative "molinillo/lib/molinillo" diff --git a/lib/rubygems/resolver/molinillo/lib/molinillo/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 86c249c404..0000000000 --- a/lib/rubygems/resolver/molinillo/lib/molinillo/gem_metadata.rb +++ /dev/null @@ -1,6 +0,0 @@ -# frozen_string_literal: true - -module Gem::Resolver::Molinillo - # The version of Gem::Resolver::Molinillo. - VERSION = '0.8.0'.freeze -end diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb index 5b51493c9a..6f86f0f412 100644 --- a/lib/rubygems/resolver/requirement_list.rb +++ b/lib/rubygems/resolver/requirement_list.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The RequirementList is used to hold the requirements being considered # while resolving a set of gems. diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb index 5d8dd51eaa..243fee5fd5 100644 --- a/lib/rubygems/resolver/set.rb +++ b/lib/rubygems/resolver/set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Resolver sets are used to look up specifications (and their # dependencies) used in resolution. This set is abstract. diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb index bf8c23184e..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 3da803cab5..d2098ef0e2 100644 --- a/lib/rubygems/resolver/specification.rb +++ b/lib/rubygems/resolver/specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A Resolver::Specification contains a subset of the information # contained in a Gem::Specification. Only the information necessary for diff --git a/lib/rubygems/resolver/stats.rb b/lib/rubygems/resolver/stats.rb index 3b95efebf7..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 diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb index 6c0ef2a1a1..293a1e3331 100644 --- a/lib/rubygems/resolver/vendor_set.rb +++ b/lib/rubygems/resolver/vendor_set.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A VendorSet represents gems that have been unpacked into a specific # directory that contains a gemspec. diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb index 600a98a2bf..ac78f54558 100644 --- a/lib/rubygems/resolver/vendor_specification.rb +++ b/lib/rubygems/resolver/vendor_specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A VendorSpecification represents a gem that has been unpacked into a project # and is being loaded through a gem dependencies file through the +path:+ diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb index 5522753af5..7c95a9d4f5 100644 --- a/lib/rubygems/s3_uri_signer.rb +++ b/lib/rubygems/s3_uri_signer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "openssl" ## @@ -10,7 +12,7 @@ class Gem::S3URISigner end def to_s # :nodoc: - "#{super}" + super.to_s end end @@ -20,7 +22,7 @@ class Gem::S3URISigner end def to_s # :nodoc: - "#{super}" + super.to_s end end @@ -32,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 @@ -47,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 @@ -134,11 +136,11 @@ class Gem::S3URISigner end def base64_uri_escape(str) - str.gsub(/[\+\/=\n]/, BASE64_URI_TRANSLATE) + str.gsub(%r{[\+/=\n]}, BASE64_URI_TRANSLATE) end def ec2_metadata_credentials_json - require "net/http" + require_relative "vendored_net_http" require_relative "request" require_relative "request/connection_pools" require "json" @@ -150,13 +152,13 @@ class Gem::S3URISigner 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}") @@ -170,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 5a98505598..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 @@ -24,34 +25,21 @@ module Gem runtime ].freeze - if ::Psych.respond_to? :safe_load - def self.safe_load(input) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1.0.pre1") - ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, aliases: true) - else - ::Psych.safe_load(input, PERMITTED_CLASSES, PERMITTED_SYMBOLS, true) - end - end + @aliases_enabled = true + def self.aliases_enabled=(value) # :nodoc: + @aliases_enabled = !!value + end - def self.load(input) - if Gem::Version.new(Psych::VERSION) >= Gem::Version.new("3.1.0.pre1") - ::Psych.safe_load(input, permitted_classes: [::Symbol]) - else - ::Psych.safe_load(input, [::Symbol]) - end - end - else - unless Gem::Deprecate.skip - warn "Psych safe loading is not available. Please upgrade psych to a version that supports safe loading (>= 2.0)." - end + def self.aliases_enabled? # :nodoc: + @aliases_enabled + end - def self.safe_load(input, *args) - ::Psych.load input - 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) - ::Psych.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 3ba8c6957c..69ba87b07f 100644 --- a/lib/rubygems/security.rb +++ b/lib/rubygems/security.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -322,10 +323,9 @@ require_relative "openssl" # == Original author # # Paul Duncan <pabs@pablotron.org> -# http://pablotron.org/ +# https://pablotron.org/ module Gem::Security - ## # Gem::Security default exception type @@ -360,7 +360,7 @@ module Gem::Security ## # One day in seconds - ONE_DAY = 86400 + ONE_DAY = 86_400 ## # One year in seconds @@ -398,8 +398,7 @@ module Gem::Security # # The +extensions+ restrict the key to the indicated uses. - def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, - serial = 1) + def self.create_cert(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) cert = OpenSSL::X509::Certificate.new cert.public_key = get_public_key(key) @@ -450,8 +449,7 @@ module Gem::Security # Creates a self-signed certificate with an issuer and subject of +subject+ # and the given +extensions+ for the +key+. - def self.create_cert_self_signed(subject, key, age = ONE_YEAR, - extensions = EXTENSIONS, serial = 1) + def self.create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) certificate = create_cert subject, key, age, extensions sign certificate, key, certificate, age, extensions, serial @@ -461,16 +459,8 @@ module Gem::Security # Creates a new digest instance using the specified +algorithm+. The default # is SHA256. - if defined?(OpenSSL::Digest) - def self.create_digest(algorithm = DIGEST_NAME) - OpenSSL::Digest.new(algorithm) - end - else - require "digest" - - def self.create_digest(algorithm = DIGEST_NAME) - Digest.const_get(algorithm).new - end + def self.create_digest(algorithm = DIGEST_NAME) + OpenSSL::Digest.new(algorithm) end ## @@ -515,11 +505,10 @@ module Gem::Security #-- # TODO increment serial - def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, - extensions = EXTENSIONS) + def self.re_sign(expired_certificate, private_key, age = ONE_YEAR, extensions = EXTENSIONS) raise Gem::Security::Exception, "incorrect signing key for re-signing " + - "#{expired_certificate.subject}" unless + expired_certificate.subject.to_s unless expired_certificate.check_private_key(private_key) unless expired_certificate.subject.to_s == @@ -528,7 +517,7 @@ module Gem::Security issuer = alt_name_or_x509_entry expired_certificate, :issuer raise Gem::Security::Exception, - "#{subject} is not self-signed, contact #{issuer} " + + "#{subject} is not self-signed, contact #{issuer} " \ "to obtain a valid certificate" end @@ -552,8 +541,7 @@ module Gem::Security # # Returns the newly signed certificate. - def self.sign(certificate, signing_key, signing_cert, - age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) + def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1) signee_subject = certificate.subject signee_key = certificate.public_key @@ -601,7 +589,7 @@ module Gem::Security # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be # passed to +to_pem+. - def self.write(pemmable, path, permissions = 0600, passphrase = nil, cipher = KEY_CIPHER) + def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER) path = File.expand_path path File.open path, "wb", permissions do |io| @@ -616,7 +604,6 @@ module Gem::Security end reset - end if Gem::HAVE_OPENSSL diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb index d28005223e..41f66043ad 100644 --- a/lib/rubygems/security/policies.rb +++ b/lib/rubygems/security/policies.rb @@ -1,17 +1,17 @@ # frozen_string_literal: true -module Gem::Security +module Gem::Security ## # No security policy: all package signature checks are disabled. NoSecurity = Policy.new( "No Security", - :verify_data => false, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: false, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -24,12 +24,12 @@ module Gem::Security AlmostNoSecurity = Policy.new( "Almost No Security", - :verify_data => true, - :verify_signer => false, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: true, + verify_signer: false, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -41,12 +41,12 @@ module Gem::Security LowSecurity = Policy.new( "Low Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => false, - :verify_root => false, - :only_trusted => false, - :only_signed => false + verify_data: true, + verify_signer: true, + verify_chain: false, + verify_root: false, + only_trusted: false, + only_signed: false ) ## @@ -60,12 +60,12 @@ module Gem::Security MediumSecurity = Policy.new( "Medium Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => false + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: false ) ## @@ -79,12 +79,12 @@ module Gem::Security HighSecurity = Policy.new( "High Security", - :verify_data => true, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => true, - :only_signed => true + verify_data: true, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: true, + only_signed: true ) ## @@ -92,12 +92,12 @@ module Gem::Security SigningPolicy = Policy.new( "Signing Policy", - :verify_data => false, - :verify_signer => true, - :verify_chain => true, - :verify_root => true, - :only_trusted => false, - :only_signed => false + verify_data: false, + verify_signer: true, + verify_chain: true, + verify_root: true, + only_trusted: false, + only_signed: false ) ## @@ -111,5 +111,4 @@ module Gem::Security "HighSecurity" => HighSecurity, # SigningPolicy is not intended for use by `gem -P` so do not list it }.freeze - end diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb index 959880ddc1..7b86ac5763 100644 --- a/lib/rubygems/security/policy.rb +++ b/lib/rubygems/security/policy.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "../user_interaction" ## @@ -134,7 +135,7 @@ class Gem::Security::Policy raise Gem::Security::Exception, "missing root certificate" unless root raise Gem::Security::Exception, - "root certificate #{root.subject} is not self-signed " + + "root certificate #{root.subject} is not self-signed " \ "(issuer #{root.issuer})" if root.issuer != root.subject @@ -170,7 +171,7 @@ class Gem::Security::Policy cert_dgst = digester.digest pkey_str raise Gem::Security::Exception, - "trusted root certificate #{root.subject} checksum " + + "trusted root certificate #{root.subject} checksum " \ "does not match signing root certificate checksum" unless save_dgst == cert_dgst @@ -191,11 +192,8 @@ class Gem::Security::Policy end def inspect # :nodoc: - ("[Policy: %s - data: %p signer: %p chain: %p root: %p " + - "signed-only: %p trusted-only: %p]") % [ - @name, @verify_chain, @verify_data, @verify_root, @verify_signer, - @only_signed, @only_trusted - ] + format("[Policy: %s - data: %p signer: %p chain: %p root: %p " \ + "signed-only: %p trusted-only: %p]", @name, @verify_chain, @verify_data, @verify_root, @verify_signer, @only_signed, @only_trusted) end ## @@ -205,8 +203,7 @@ class Gem::Security::Policy # # If +key+ is given it is used to validate the signing certificate. - def verify(chain, key = nil, digests = {}, signatures = {}, - full_name = "(unknown)") + def verify(chain, key = nil, digests = {}, signatures = {}, full_name = "(unknown)") if signatures.empty? if @only_signed raise Gem::Security::Exception, @@ -225,7 +222,7 @@ class Gem::Security::Policy trust_dir = opt[:trust_dir] time = Time.now - _, signer_digests = digests.find do |algorithm, file_digests| + _, signer_digests = digests.find do |_algorithm, file_digests| file_digests.values.first.name == Gem::Security::DIGEST_NAME end @@ -287,5 +284,5 @@ class Gem::Security::Policy true end - alias to_s name # :nodoc: + alias_method :to_s, :name # :nodoc: end diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb index cca82f1cf8..5732fb57fd 100644 --- a/lib/rubygems/security/signer.rb +++ b/lib/rubygems/security/signer.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # Basic OpenSSL-based package signing class. @@ -105,7 +106,7 @@ class Gem::Security::Signer # this value is preferred, otherwise the subject is used. def extract_name(cert) # :nodoc: - subject_alt_name = cert.extensions.find {|e| "subjectAltName" == e.oid } + subject_alt_name = cert.extensions.find {|e| e.oid == "subjectAltName" } if subject_alt_name /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith @@ -174,10 +175,18 @@ class Gem::Security::Signer old_cert = @cert_chain.last disk_cert_path = File.join(Gem.default_cert_path) - disk_cert = File.read(disk_cert_path) rescue nil + disk_cert = begin + File.read(disk_cert_path) + rescue StandardError + nil + end disk_key_path = File.join(Gem.default_key_path) - disk_key = OpenSSL::PKey.read(File.read(disk_key_path), @passphrase) rescue nil + disk_key = begin + OpenSSL::PKey.read(File.read(disk_key_path), @passphrase) + rescue StandardError + nil + end return unless disk_key diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb index df59680d84..d23d161cfe 100644 --- a/lib/rubygems/security/trust_dir.rb +++ b/lib/rubygems/security/trust_dir.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # The TrustDir manages the trusted certificates for gem signature # verification. @@ -8,8 +9,8 @@ class Gem::Security::TrustDir # Default permissions for the trust directory and its contents DEFAULT_PERMISSIONS = { - :trust_dir => 0700, - :trusted_cert => 0600, + trust_dir: 0o700, + trusted_cert: 0o600, }.freeze ## @@ -44,13 +45,11 @@ class Gem::Security::TrustDir glob = File.join @dir, "*.pem" Dir[glob].each do |certificate_file| - begin - certificate = load_certificate certificate_file + certificate = load_certificate certificate_file - yield certificate, certificate_file - rescue OpenSSL::X509::CertificateError - next # HACK warn - end + yield certificate, certificate_file + rescue OpenSSL::X509::CertificateError + next # HACK: warn end end @@ -92,7 +91,7 @@ class Gem::Security::TrustDir destination = cert_path certificate - File.open destination, "wb", 0600 do |io| + File.open destination, "wb", 0o600 do |io| io.write certificate.to_pem io.chmod(@permissions[:trusted_cert]) end @@ -110,9 +109,9 @@ class Gem::Security::TrustDir "trust directory #{@dir} is not a directory" unless File.directory? @dir - FileUtils.chmod 0700, @dir + FileUtils.chmod 0o700, @dir else - FileUtils.mkdir_p @dir, :mode => @permissions[:trust_dir] + FileUtils.mkdir_p @dir, mode: @permissions[:trust_dir] end end end diff --git a/lib/rubygems/security_option.rb b/lib/rubygems/security_option.rb index ab3898bf11..3a101fe9db 100644 --- a/lib/rubygems/security_option.rb +++ b/lib/rubygems/security_option.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -28,7 +29,7 @@ module Gem::SecurityOption policy = Gem::Security::Policies[value] unless policy valid = Gem::Security::Policies.keys.sort - raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ', '} are valid)" + raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ", "} are valid)" end policy end diff --git a/lib/rubygems/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 aa0cbc1641..d90e311b65 100644 --- a/lib/rubygems/source.rb +++ b/lib/rubygems/source.rb @@ -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 ## @@ -44,20 +44,18 @@ class Gem::Source Gem::Source::Vendor then -1 when Gem::Source then - if !@uri + unless @uri return 0 unless other.uri return 1 end - return -1 if !other.uri + return -1 unless other.uri # Returning 1 here ensures that when sorting a list of sources, the # original ordering of sources supplied by the user is preserved. return 1 unless @uri.to_s == other.uri.to_s 0 - else - nil end end @@ -71,7 +69,7 @@ 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 @@ -102,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 @@ -137,7 +134,12 @@ class Gem::Source if File.exist? local_spec spec = Gem.read_binary local_spec - spec = Marshal.load(spec) rescue nil + Gem.load_safe_marshal + spec = begin + Gem::SafeMarshal.safe_load(spec) + rescue StandardError + nil + end return spec if spec end @@ -155,8 +157,9 @@ class Gem::Source end end + Gem.load_safe_marshal # TODO: Investigate setting Gem::Specification#loaded_from to a URI - Marshal.load spec + Gem::SafeMarshal.safe_load spec end ## @@ -186,8 +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 @@ -229,7 +233,7 @@ class Gem::Source private def enforce_trailing_slash(uri) - uri.merge(uri.path.gsub(/\/+$/, "") + "/") + uri.merge(uri.path.gsub(%r{/+$}, "") + "/") end end diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb index 2609a309e8..bda63c6844 100644 --- a/lib/rubygems/source/git.rb +++ b/lib/rubygems/source/git.rb @@ -53,7 +53,7 @@ class Gem::Source::Git < Gem::Source @uri = Gem::Uri.parse(repository) @name = name @repository = repository - @reference = reference + @reference = reference || "HEAD" @need_submodules = submodules @remote = true @@ -70,8 +70,6 @@ class Gem::Source::Git < Gem::Source -1 when Gem::Source then 1 - else - nil end end @@ -223,14 +221,14 @@ 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_relative "../openssl" normalized = - if @repository =~ %r{^\w+://(\w+@)?} - uri = URI(@repository).normalize.to_s.sub %r{/$},"" + if @repository.match?(%r{^\w+://(\w+@)?}) + uri = Gem::URI(@repository).normalize.to_s.sub %r{/$},"" uri.sub(/\A(\w+)/) { $1.downcase } else @repository diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb index 786faab3e3..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 diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb index ec1a594238..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] + 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 diff --git a/lib/rubygems/source/lock.rb b/lib/rubygems/source/lock.rb index 49f097467b..70849210bd 100644 --- a/lib/rubygems/source/lock.rb +++ b/lib/rubygems/source/lock.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # A Lock source wraps an installed gem's source and sorts before other sources # during dependency resolution. This allows RubyGems to prefer gems from @@ -24,13 +25,11 @@ class Gem::Source::Lock < Gem::Source @wrapped <=> other.wrapped when Gem::Source then 1 - else - nil end end def ==(other) # :nodoc: - 0 == (self <=> other) + (self <=> other) == 0 end def hash # :nodoc: diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb index 552aeba50f..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: diff --git a/lib/rubygems/source/vendor.rb b/lib/rubygems/source/vendor.rb index 543acf1388..44ef614441 100644 --- a/lib/rubygems/source/vendor.rb +++ b/lib/rubygems/source/vendor.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + ## # This represents a vendored source that is similar to an installed gem. @@ -18,8 +19,6 @@ class Gem::Source::Vendor < Gem::Source::Installed 0 when Gem::Source then 1 - else - nil end end end diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb index 7abe796409..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,15 +44,15 @@ class Gem::SourceList end ## - # Appends +obj+ to the source list which may be a Gem::Source, URI or URI + # Appends +obj+ to the source list which may be a Gem::Source, Gem::URI or URI # String. def <<(obj) src = case obj - when Gem::Source - obj - else - Gem::Source.new(obj) + when Gem::Source + obj + else + Gem::Source.new(obj) end @sources << src unless @sources.include?(src) @@ -126,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 } @@ -137,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 0d06d1f144..610edf25c9 100644 --- a/lib/rubygems/spec_fetcher.rb +++ b/lib/rubygems/spec_fetcher.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + require_relative "remote_fetcher" require_relative "user_interaction" require_relative "errors" @@ -68,9 +69,9 @@ class Gem::SpecFetcher @prerelease_specs = {} @caches = { - :latest => @latest_specs, - :prerelease => @prerelease_specs, - :released => @specs, + latest: @latest_specs, + prerelease: @prerelease_specs, + released: @specs, } @fetcher = Gem::RemoteFetcher.fetcher @@ -91,9 +92,9 @@ class Gem::SpecFetcher list.each do |source, specs| if dependency.name.is_a?(String) && specs.respond_to?(:bsearch) - start_index = (0 ... specs.length).bsearch {|i| specs[i].name >= dependency.name } - end_index = (0 ... specs.length).bsearch {|i| specs[i].name > dependency.name } - specs = specs[start_index ... end_index] if start_index && end_index + start_index = (0...specs.length).bsearch {|i| specs[i].name >= dependency.name } + end_index = (0...specs.length).bsearch {|i| specs[i].name > dependency.name } + specs = specs[start_index...end_index] if start_index && end_index end found[source] = specs.select do |tup| @@ -123,7 +124,7 @@ class Gem::SpecFetcher 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 ## @@ -193,10 +192,10 @@ class Gem::SpecFetcher matches = if matches.empty? && type != :prerelease suggest_gems_from_name gem_name, :prerelease else - matches.uniq.sort_by {|name, dist| dist } + matches.uniq.sort_by {|_name, dist| dist } end - matches.map {|name, dist| name }.uniq.first(num_results) + matches.map {|name, _dist| name }.uniq.first(num_results) end ## @@ -214,34 +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 31b8ef9546..29139cf725 100644 --- a/lib/rubygems/specification.rb +++ b/lib/rubygems/specification.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + # #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. @@ -12,6 +13,8 @@ require_relative "stub_specification" require_relative "platform" require_relative "util/list" +require "rbconfig" + ## # The Specification class contains the information for a gem. Typically # defined in a .gemspec file or a Rakefile, and looks like this: @@ -105,7 +108,7 @@ class Gem::Specification < Gem::BasicSpecification @load_cache = {} # :nodoc: @load_cache_mutex = Thread::Mutex.new - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: # :startdoc: @@ -124,35 +127,35 @@ class Gem::Specification < Gem::BasicSpecification # Map of attribute names to default values. @@default_value = { - :authors => [], - :autorequire => nil, - :bindir => "bin", - :cert_chain => [], - :date => nil, - :dependencies => [], - :description => nil, - :email => nil, - :executables => [], - :extensions => [], - :extra_rdoc_files => [], - :files => [], - :homepage => nil, - :licenses => [], - :metadata => {}, - :name => nil, - :platform => Gem::Platform::RUBY, - :post_install_message => nil, - :rdoc_options => [], - :require_paths => ["lib"], - :required_ruby_version => Gem::Requirement.default, - :required_rubygems_version => Gem::Requirement.default, - :requirements => [], - :rubygems_version => Gem::VERSION, - :signing_key => nil, - :specification_version => CURRENT_SPECIFICATION_VERSION, - :summary => nil, - :test_files => [], - :version => nil, + authors: [], + autorequire: nil, + bindir: "bin", + cert_chain: [], + date: nil, + dependencies: [], + description: nil, + email: nil, + executables: [], + extensions: [], + extra_rdoc_files: [], + files: [], + homepage: nil, + licenses: [], + metadata: {}, + name: nil, + platform: Gem::Platform::RUBY, + post_install_message: nil, + rdoc_options: [], + require_paths: ["lib"], + required_ruby_version: Gem::Requirement.default, + required_rubygems_version: Gem::Requirement.default, + requirements: [], + rubygems_version: Gem::VERSION, + signing_key: nil, + specification_version: CURRENT_SPECIFICATION_VERSION, + summary: nil, + test_files: [], + version: nil, }.freeze # rubocop:disable Style/MutableConstant @@ -161,19 +164,17 @@ class Gem::Specification < Gem::BasicSpecification @@default_value.each do |k,v| INITIALIZE_CODE_FOR_DEFAULTS[k] = case v - when [], {}, true, false, nil, Numeric, Symbol - v.inspect - when String - v.dump - when Numeric - "default_value(:#{k})" - else - "default_value(:#{k}).dup" + when [], {}, true, false, nil, Numeric, Symbol + v.inspect + when String + v.dump + else + "default_value(:#{k}).dup" end end - @@attributes = @@default_value.keys.sort_by {|s| s.to_s } - @@array_attributes = @@default_value.reject {|k,v| v != [] }.keys + @@attributes = @@default_value.keys.sort_by(&:to_s) + @@array_attributes = @@default_value.reject {|_k,v| v != [] }.keys @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k| @@default_value[k].nil? end @@ -262,8 +263,7 @@ class Gem::Specification < Gem::BasicSpecification @test_files, add_bindir(@executables), @extra_rdoc_files, - @extensions, - ].flatten.compact.uniq.sort + @extensions].flatten.compact.uniq.sort end ## @@ -301,7 +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 @@ -338,10 +338,10 @@ class Gem::Specification < Gem::BasicSpecification # The simplest way is to specify the standard SPDX ID # https://spdx.org/licenses/ for the license. # Ideally, you should pick one that is OSI (Open Source Initiative) - # http://opensource.org/licenses/alphabetical approved. + # https://opensource.org/licenses/ approved. # # The most commonly used OSI-approved licenses are MIT and Apache-2.0. - # GitHub also provides a license picker at http://choosealicense.com/. + # GitHub also provides a license picker at https://choosealicense.com/. # # You can also use a custom license file along with your gemspec and specify # a LicenseRef-<idstring>, where idstring is the name of the file containing @@ -426,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 @@ -502,8 +502,6 @@ class Gem::Specification < Gem::BasicSpecification @platform = @new_platform.to_s invalidate_memoized_attributes - - @new_platform end ## @@ -533,13 +531,6 @@ class Gem::Specification < Gem::BasicSpecification attr_reader :required_rubygems_version ## - # The version of RubyGems used to create this gem. - # - # Do not set this, it is set automatically when the gem is packaged. - - attr_accessor :rubygems_version - - ## # The key used to sign this gem. See Gem::Security for details. attr_accessor :signing_key @@ -577,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. @@ -598,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: # @@ -727,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 ## @@ -734,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. @@ -777,7 +783,7 @@ class Gem::Specification < Gem::BasicSpecification 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 @@ -856,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 @@ -937,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 ## @@ -963,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 @@ -972,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 @@ -986,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 @@ -997,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 @@ -1016,19 +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| + 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 @@ -1045,9 +1054,10 @@ class Gem::Specification < Gem::BasicSpecification end def self.find_active_stub_by_path(path) - stub = @@active_stub_with_requirable_file[path] ||= (stubs.find do |s| + stub = @@active_stub_with_requirable_file[path] ||= stubs.find do |s| s.activated? && s.contains_requirable_file?(path) - end || NOT_FOUND) + end || NOT_FOUND + stub.this end @@ -1064,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 @@ -1077,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 @@ -1129,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 ## @@ -1143,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 = Gem.open_file(file, "r:UTF-8:-", &:read) - code.tap(&Gem::UNTAINT) - begin - _spec = eval code, binding, file + spec = eval code, binding, file - if Gem::Specification === _spec - _spec.loaded_from = File.expand_path file.to_s + if Gem::Specification === spec + spec.loaded_from = File.expand_path file.to_s @load_cache_mutex.synchronize do prev = @load_cache[file] if prev - _spec = prev + spec = prev else - @load_cache[file] = _spec + @load_cache[file] = spec end end - return _spec + return spec end - warn "[#{file}] isn't a Gem::Specification (#{_spec.class} instead)." + warn "[#{file}] isn't a Gem::Specification (#{spec.class} instead)." rescue SignalException, SystemExit raise - rescue SyntaxError, Exception => e + rescue SyntaxError, StandardError => e warn "Invalid gemspec in [#{file}]: #{e}" end @@ -1260,7 +1269,7 @@ class Gem::Specification < Gem::BasicSpecification def self.reset @@dirs = nil - Gem.pre_reset_hooks.each {|hook| hook.call } + Gem.pre_reset_hooks.each(&:call) clear_specs clear_load_cache unresolved = unresolved_deps @@ -1279,7 +1288,7 @@ class Gem::Specification < Gem::BasicSpecification 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 @@ -1292,10 +1301,23 @@ class Gem::Specification < Gem::BasicSpecification def self._load(str) Gem.load_yaml + Gem.load_safe_marshal + + yaml_set = false + retry_count = 0 array = begin - Marshal.load str + Gem::SafeMarshal.safe_load str rescue ArgumentError => e + # Avoid an infinite retry loop when the argument error has nothing to do + # with the classes not being defined. + # 1 retry each allowed in case all 3 of + # - YAML + # - YAML::Syck::DefaultKey + # - YAML::PrivateType + # need to be defined + raise if retry_count >= 3 + # # Some very old marshaled specs included references to `YAML::PrivateType` # and `YAML::Syck::DefaultKey` constants due to bugs in the old emitter @@ -1305,17 +1327,23 @@ class Gem::Specification < Gem::BasicSpecification message = e.message raise unless message.include?("YAML::") - Object.const_set "YAML", Psych unless Object.const_defined?(:YAML) + unless Object.const_defined?(:YAML) + Object.const_set "YAML", Psych + yaml_set = true + end if message.include?("YAML::Syck::") YAML.const_set "Syck", YAML unless YAML.const_defined?(:Syck) - YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") - elsif message.include?("YAML::PrivateType") + YAML::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 @@ -1409,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 @@ -1420,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 ## @@ -1435,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 @@ -1449,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 @@ -1495,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 @@ -1513,8 +1541,8 @@ class Gem::Specification < Gem::BasicSpecification else executables end - rescue - return nil + rescue StandardError + nil end ## @@ -1539,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. @@ -1591,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 @@ -1606,9 +1634,11 @@ class Gem::Specification < Gem::BasicSpecification def build_extensions # :nodoc: return if extensions.empty? return if default_gem? + # we need to fresh build when same name and version of default gems + return if self.class.find_by_full_name(full_name)&.default_gem? return if File.exist? gem_build_complete_path - return if !File.writable?(base_dir) - return if !File.exist?(File.join(base_dir, "extensions")) + return unless File.writable?(base_dir) + return unless File.exist?(File.join(base_dir, "extensions")) begin # We need to require things in $LOAD_PATH without looking for the @@ -1666,7 +1696,7 @@ class Gem::Specification < Gem::BasicSpecification def conflicts conflicts = {} - self.runtime_dependencies.each do |dep| + runtime_dependencies.each do |dep| spec = Gem.loaded_specs[dep.name] if spec && !spec.satisfies_requirement?(dep) (conflicts[spec] ||= []) << dep @@ -1682,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 @@ -1692,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 && !spec.satisfies_requirement?(dep) - else - false - end + runtime_dependencies.any? do |dep| + spec = Gem.loaded_specs[dep.name] + spec && !spec.satisfies_requirement?(dep) end + rescue ArgumentError => e + raise e, "#{name} #{version}: #{e.message}" end # The date this gem was created. @@ -1723,7 +1751,7 @@ class Gem::Specification < Gem::BasicSpecification /\A (\d{4})-(\d{2})-(\d{2}) (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )? - \Z/x.freeze + \Z/x ## # The date this gem was created @@ -1735,17 +1763,17 @@ class Gem::Specification < Gem::BasicSpecification # This is the cleanest, most-readable, faster-than-using-Date # way to do it. @date = case date - when String then - if DateTimeFormat =~ date - Time.utc($1.to_i, $2.to_i, $3.to_i) - else - raise(Gem::InvalidSpecificationException, - "invalid date format in specification: #{date.inspect}") - end - when Time, DateLike then - Time.utc(date.year, date.month, date.day) - else - TODAY + when String then + if DateTimeFormat =~ date + Time.utc($1.to_i, $2.to_i, $3.to_i) + else + raise(Gem::InvalidSpecificationException, + "invalid date format in specification: #{date.inspect}") + end + when Time, DateLike then + Time.utc(date.year, date.month, date.day) + else + TODAY end end @@ -1795,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 @@ -1811,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 ## @@ -1852,18 +1879,19 @@ class Gem::Specification < Gem::BasicSpecification coder.add "name", @name coder.add "version", @version platform = case @original_platform - when nil, "" then - "ruby" - when String then - @original_platform - else - @original_platform.to_s + when nil, "" then + "ruby" + when String then + @original_platform + else + @original_platform.to_s 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 @@ -1980,7 +2008,7 @@ 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? ## @@ -1991,7 +2019,7 @@ class Gem::Specification < Gem::BasicSpecification end # :stopdoc: - alias has_test_suite? has_unit_tests? + alias_method :has_test_suite?, :has_unit_tests? # :startdoc: def hash # :nodoc: @@ -2048,7 +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| @@ -2070,6 +2099,9 @@ class Gem::Specification < Gem::BasicSpecification raise e end end + + @required_ruby_version = other_spec.required_ruby_version.dup + @required_rubygems_version = other_spec.required_rubygems_version.dup end def base_dir @@ -2225,7 +2257,7 @@ class Gem::Specification < Gem::BasicSpecification # The platform this gem runs on. See Gem::Platform for details. def platform - @new_platform ||= Gem::Platform::RUBY + @new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName end def pretty_print(q) # :nodoc: @@ -2238,23 +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) || - self.class.required_attribute?(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) - q.text "s.#{attr_name} = " + q.text "s.#{attr_name} = " - if attr_name == :date - current_value = current_value.utc + if attr_name == :date + current_value = current_value.utc - q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})" - else - q.pp current_value - end - - q.breakable + q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})" + else + q.pp current_value end + + q.breakable end end end @@ -2264,7 +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. @@ -2272,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 @@ -2337,13 +2368,13 @@ class Gem::Specification < Gem::BasicSpecification when Array then "[" + obj.map {|x| ruby_code x }.join(", ") + "]" when Hash then seg = obj.keys.sort.map {|k| "#{k.to_s.dump} => #{obj[k].to_s.dump}" } - "{ #{seg.join(', ')} }" - when Gem::Version then obj.to_s.dump + "{ #{seg.join(", ")} }" + when Gem::Version then ruby_code(obj.to_s) when DateLike then obj.strftime("%Y-%m-%d").dump when Time then obj.strftime("%Y-%m-%d").dump when Numeric then obj.inspect when true, false, nil then obj.inspect - when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})" + when Gem::Platform then "Gem::Platform.new(#{ruby_code obj.to_a})" when Gem::Requirement then list = obj.as_list "Gem::Requirement.new(#{ruby_code(list.size == 1 ? obj.to_s : list)})" @@ -2364,7 +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? @@ -2373,8 +2404,8 @@ class Gem::Specification < Gem::BasicSpecification # Checks if this specification meets the requirement of +dependency+. def satisfies_requirement?(dependency) - return @name == dependency.name && - dependency.requirement.satisfied_by?(@version) + @name == dependency.name && + dependency.requirement.satisfied_by?(@version) end ## @@ -2501,19 +2532,19 @@ class Gem::Specification < Gem::BasicSpecification @@attributes.each do |attr_name| next if handled.include? attr_name - current_value = self.send(attr_name) + current_value = send(attr_name) if current_value != default_value(attr_name) || self.class.required_attribute?(attr_name) result << " s.#{attr_name} = #{ruby_code current_value}" end end if String === signing_key - result << " s.signing_key = #{signing_key.dump}.freeze" + result << " s.signing_key = #{ruby_code signing_key}" end if @installed_by_version result << nil - result << " s.installed_by_version = \"#{Gem::VERSION}\" if s.respond_to? :installed_by_version" + result << " s.installed_by_version = #{ruby_code Gem::VERSION} if s.respond_to? :installed_by_version" end unless dependencies.empty? @@ -2522,9 +2553,8 @@ class Gem::Specification < Gem::BasicSpecification result << nil dependencies.each do |dep| - req = dep.requirements_list.inspect dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK - result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{req})" + result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})" end end @@ -2585,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 @@ -2596,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 @@ -2647,22 +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) return if @version.nil? - # skip to set required_ruby_version when pre-released rubygems. - # It caused to raise CircularDependencyError - if @version.prerelease? && (@name.nil? || @name.strip != "rubygems") - self.required_rubygems_version = "> 1.3.1" - end invalidate_memoized_attributes - - return @version end def stubbed? @@ -2674,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 @@ -2693,17 +2712,19 @@ class Gem::Specification < Gem::BasicSpecification end nil_attributes.each do |attribute| - default = self.default_value attribute + default = default_value attribute value = case default - when Time, Numeric, Symbol, true, false, nil then default - else default.dup + when Time, Numeric, Symbol, true, false, nil then default + else default.dup end instance_variable_set "@#{attribute}", value end @installed_by_version ||= nil + + nil end def flatten_require_paths # :nodoc: diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb index f01a6cd743..516c26f53c 100644 --- a/lib/rubygems/specification_policy.rb +++ b/lib/rubygems/specification_policy.rb @@ -1,22 +1,25 @@ +# frozen_string_literal: true + require_relative "user_interaction" class Gem::SpecificationPolicy include Gem::UserInteraction - VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/.freeze # :nodoc: + VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc: - SPECIAL_CHARACTERS = /\A[#{Regexp.escape('.-_')}]+/.freeze # :nodoc: + SPECIAL_CHARACTERS = /\A[#{Regexp.escape(".-_")}]+/ # :nodoc: - VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}.freeze # :nodoc: + VALID_URI_PATTERN = %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z} # :nodoc: METADATA_LINK_KEYS = %w[ - bug_tracker_uri - changelog_uri - documentation_uri homepage_uri - mailing_list_uri + changelog_uri source_code_uri + documentation_uri wiki_uri + mailing_list_uri + bug_tracker_uri + download_uri funding_uri ].freeze # :nodoc: @@ -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" @@ -125,7 +132,7 @@ class Gem::SpecificationPolicy metadata.each do |key, value| entry = "metadata['#{key}']" - if !key.kind_of?(String) + unless key.is_a?(String) error "metadata keys must be a String" end @@ -133,7 +140,7 @@ class Gem::SpecificationPolicy error "metadata key is too large (#{key.size} > 128)" end - if !value.kind_of?(String) + unless value.is_a?(String) error "#{entry} value must be a String" end @@ -141,10 +148,9 @@ class Gem::SpecificationPolicy error "#{entry} value is too large (#{value.size} > 1024)" end - if METADATA_LINK_KEYS.include? key - if value !~ VALID_URI_PATTERN - error "#{entry} has invalid link: #{value.inspect}" - end + next unless METADATA_LINK_KEYS.include? key + unless VALID_URI_PATTERN.match?(value) + error "#{entry} has invalid link: #{value.inspect}" end end end @@ -161,7 +167,7 @@ class Gem::SpecificationPolicy if prev = seen[dep.type][dep.name] error_messages << <<-MESSAGE duplicate dependency on #{dep}, (#{prev.requirement}) use: - add_#{dep.type}_dependency '#{dep.name}', '#{dep.requirement}', '#{prev.requirement}' + add_#{dep.type}_dependency \"#{dep.name}\", \"#{dep.requirement}\", \"#{prev.requirement}\" MESSAGE end @@ -173,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. @@ -180,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 @@ -188,37 +199,42 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: prerelease_dep && !@specification.version.prerelease? open_ended = dep.requirement.requirements.all? do |op, version| - !version.prerelease? && (op == ">" || op == ">=") + !version.prerelease? && [">", ">="].include?(op) end - if open_ended - op, dep_version = dep.requirement.requirements.first + next unless open_ended + op, dep_version = dep.requirement.requirements.first - segments = dep_version.segments + segments = dep_version.segments - base = segments.first 2 + base = segments.first 2 - recommendation = if (op == ">" || op == ">=") && segments == [0] - " use a bounded requirement, such as '~> x.y'" - else - bugfix = if op == ">" - ", '> #{dep_version}'" - elsif op == ">=" && base != segments - ", '>= #{dep_version}'" - end - - " if #{dep.name} is semantically versioned, use:\n" \ - " add_#{dep.type}_dependency '#{dep.name}', '~> #{base.join '.'}'#{bugfix}" + 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. # @@ -229,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 @@ -248,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 @@ -274,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 @@ -332,13 +348,13 @@ duplicate dependency on #{dep}, (#{prev.requirement}) use: def validate_array_attribute(field) val = @specification.send(field) klass = case field - when :dependencies then - Gem::Dependency - else - String + when :dependencies then + Gem::Dependency + else + String end - unless Array === val && val.all? {|x| x.kind_of?(klass) } + unless Array === val && val.all? {|x| x.is_a?(klass) || (field == :licenses && x.nil?) } error "#{field} must be an Array of #{klass}" end end @@ -353,6 +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 @@ -363,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 = /\AFI XME|\ATO DO/x.freeze - HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i.freeze + LAZY_PATTERN = /\AFI XME|\ATO DO/x + HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i def validate_lazy_metadata unless @specification.authors.grep(LAZY_PATTERN).empty? @@ -393,11 +423,11 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li error "#{LAZY} is not an email" end - if @specification.description =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.description) error "#{LAZY} is not a description" end - if @specification.summary =~ LAZY_PATTERN + if LAZY_PATTERN.match?(@specification.summary) error "#{LAZY} is not a summary" end @@ -405,13 +435,13 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li # Make sure a homepage is valid HTTP/HTTPS URI if homepage && !homepage.empty? - require "uri" + require_relative "vendor/uri/lib/uri" begin - homepage_uri = URI.parse(homepage) - unless [URI::HTTP, URI::HTTPS].member? homepage_uri.class + homepage_uri = Gem::URI.parse(homepage) + unless [Gem::URI::HTTP, Gem::URI::HTTPS].member? homepage_uri.class error "\"#{homepage}\" is not a valid HTTP URI" end - rescue URI::InvalidURIError + rescue Gem::URI::InvalidURIError error "\"#{homepage}\" is not a valid HTTP URI" end end @@ -466,7 +496,7 @@ http://spdx.org/licenses or '#{Gem::Licenses::NONSTANDARD}' for a nonstandard li def validate_rust_extensions(builder) # :nodoc: rust_extension = @specification.extensions.any? {|s| builder.builder_for(s).is_a? Gem::Ext::CargoBuilder } - missing_cargo_lock = !@specification.files.include?("Cargo.lock") + missing_cargo_lock = !@specification.files.any? {|f| f.end_with?("Cargo.lock") } error <<-ERROR if rust_extension && missing_cargo_lock You have specified rust based extension, but Cargo.lock is not part of the gem files. Please run `cargo generate-lockfile` or any other command to generate Cargo.lock and ensure it is added to your gem files section in gemspec. @@ -475,13 +505,29 @@ You have specified rust based extension, but Cargo.lock is not part of the gem f def validate_rake_extensions(builder) # :nodoc: rake_extension = @specification.extensions.any? {|s| builder.builder_for(s) == Gem::Ext::RakeBuilder } - rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" } + rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" && d.type == :runtime } warning <<-WARNING if rake_extension && !rake_dependency -You have specified rake based extension, but rake is not added as dependency. It is recommended to add rake as a dependency in gemspec since there's no guarantee rake will be already installed. +You have specified rake based extension, but rake is not added as runtime dependency. It is recommended to add rake as a runtime dependency in gemspec since there's no guarantee rake will be already installed. WARNING end + def validate_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 d87abdd993..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 @@ -34,7 +35,7 @@ class Gem::StubSpecification < Gem::BasicSpecification def initialize(data, extensions) parts = data[PREFIX.length..-1].split(" ", 4) - @name = parts[0].freeze + @name = -parts[0] @version = if Gem::Version.correct?(parts[1]) Gem::Version.new(parts[1]) else @@ -68,7 +69,6 @@ class Gem::StubSpecification < Gem::BasicSpecification def initialize(filename, base_dir, gems_dir, default_gem) super() - filename.tap(&Gem::UNTAINT) self.loaded_from = filename @data = nil @@ -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? @@ -111,20 +111,23 @@ class Gem::StubSpecification < Gem::BasicSpecification saved_lineno = $. Gem.open_file loaded_from, OPEN_MODE do |file| - begin - file.readline # discard encoding line - stubline = file.readline.chomp - if stubline.start_with?(PREFIX) - extensions = if /\A#{PREFIX}/ =~ file.readline.chomp - $'.split "\0" + file.readline # discard encoding line + stubline = file.readline + if stubline.start_with?(PREFIX) + extline = file.readline + + extensions = + if extline.delete_prefix!(PREFIX) + extline.chomp! + extline.split "\0" else StubLine::NO_EXTENSIONS end - @data = StubLine.new stubline, extensions - end - rescue EOFError + stubline.chomp! # readline(chomp: true) allocates 3x as much as .readline.chomp! + @data = StubLine.new stubline, extensions end + rescue EOFError end ensure $. = saved_lineno @@ -183,7 +186,7 @@ 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 @@ -191,6 +194,7 @@ class Gem::StubSpecification < Gem::BasicSpecification @spec ||= Gem::Specification.load(loaded_from) end + alias_method :to_spec, :spec ## # Is this StubSpecification valid? i.e. have we found a stub line, OR does diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb index be811525f2..da0795b771 100644 --- a/lib/rubygems/text.rb +++ b/lib/rubygems/text.rb @@ -4,7 +4,6 @@ # A collection of text-wrangling methods module Gem::Text - ## # Remove any non-printable characters and make the text suitable for # printing. @@ -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/tsort.rb b/lib/rubygems/tsort.rb deleted file mode 100644 index 60ebe22e81..0000000000 --- a/lib/rubygems/tsort.rb +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -require_relative "tsort/lib/tsort" diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb index 5883ed1c41..c96df2a085 100644 --- a/lib/rubygems/uninstaller.rb +++ b/lib/rubygems/uninstaller.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -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) @@ -98,9 +99,7 @@ class Gem::Uninstaller raise Gem::InstallError, "gem #{@gem.inspect} is not installed" end - default_specs, list = list.partition do |spec| - spec.default_gem? - end + default_specs, list = list.partition(&:default_gem?) warn_cannot_uninstall_default_gems(default_specs - list) @default_specs_matching_uninstall_params = default_specs @@ -114,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| @@ -126,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 @@ -134,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}]" @@ -200,8 +199,8 @@ class Gem::Uninstaller executables = executables.map {|exec| formatted_program_filename exec } remove = if @force_executables.nil? - ask_yes_no("Remove executables:\n" + - "\t#{executables.join ', '}\n\n" + + ask_yes_no("Remove executables:\n" \ + "\t#{executables.join ", "}\n\n" \ "in addition to the gem?", true) else @@ -242,7 +241,7 @@ class Gem::Uninstaller 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 @@ -341,7 +340,7 @@ class Gem::Uninstaller s.name == spec.name && s.full_name != spec.full_name end - spec.dependent_gems(@check_dev).each do |dep_spec, dep, satlist| + spec.dependent_gems(@check_dev).each do |dep_spec, dep, _satlist| unless siblings.any? {|s| s.satisfies_requirement? dep } msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}" end @@ -349,7 +348,7 @@ class Gem::Uninstaller msg << "If you remove this gem, these dependencies will not be met." msg << "Continue with Uninstall?" - return ask_yes_no(msg.join("\n"), false) + ask_yes_no(msg.join("\n"), false) end ## diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb index c2e81b2374..6f3ec5f493 100644 --- a/lib/rubygems/update_suggestion.rb +++ b/lib/rubygems/update_suggestion.rb @@ -4,15 +4,6 @@ # Mixin methods for Gem::Command to promote available RubyGems update module Gem::UpdateSuggestion - # list taken from https://github.com/watson/ci-info/blob/7a3c30d/index.js#L56-L66 - CI_ENV_VARS = [ - "CI", # Travis CI, CircleCI, Cirrus CI, Gitlab CI, Appveyor, CodeShip, dsari - "CONTINUOUS_INTEGRATION", # Travis CI, Cirrus CI - "BUILD_NUMBER", # Jenkins, TeamCity - "CI_APP_ID", "CI_BUILD_ID", "CI_BUILD_NUMBER", # Applfow - "RUN_ID" # TaskCluster, dsari - ].freeze - ONE_WEEK = 7 * 24 * 60 * 60 ## @@ -28,9 +19,9 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa end ## - # Determines if current environment is eglible for update suggestion. + # Determines if current environment is eligible for update suggestion. - def eglible_for_update? + def eligible_for_update? # explicit opt-out return false if Gem.configuration[:prevent_update_suggestion] return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"] @@ -39,7 +30,7 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa return false unless Gem.ui.tty? return false if Gem.rubygems_version.prerelease? return false if Gem.disable_system_update_message - return false if ci? + return false if Gem::CIDetector.ci? # check makes sense only when we can store timestamp of last try # otherwise we will not be able to prevent "annoying" update message @@ -53,17 +44,13 @@ Run `gem update --system #{Gem.latest_rubygems_version}` to update your installa # compare current and latest version, this is the part where # latest rubygems spec is fetched from remote - (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eglible| + (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eligible| # store the time of last successful check into state file Gem.configuration.last_update_check = check_time - return eglible + return eligible end - rescue # don't block install command on any problem + rescue StandardError # don't block install command on any problem false end - - def ci? - CI_ENV_VARS.any? {|var| ENV.include?(var) } - end end diff --git a/lib/rubygems/uri.rb b/lib/rubygems/uri.rb index 4b5d035aa0..a44aaceba5 100644 --- a/lib/rubygems/uri.rb +++ b/lib/rubygems/uri.rb @@ -16,9 +16,9 @@ class Gem::Uri # Parses uri, raising if it's invalid def self.parse!(uri) - require "uri" + require_relative "vendor/uri/lib/uri" - raise URI::InvalidURIError unless uri + raise Gem::URI::InvalidURIError unless uri return uri unless uri.is_a?(String) @@ -28,9 +28,9 @@ class Gem::Uri # as "%7BDESede%7D". If this is escaped again the percentage # symbols will be escaped. begin - URI.parse(uri) - rescue URI::InvalidURIError - URI.parse(URI::DEFAULT_PARSER.escape(uri)) + Gem::URI.parse(uri) + rescue Gem::URI::InvalidURIError + Gem::URI.parse(Gem::URI::DEFAULT_PARSER.escape(uri)) end end @@ -39,7 +39,7 @@ class Gem::Uri def self.parse(uri) parse!(uri) - rescue URI::InvalidURIError + rescue Gem::URI::InvalidURIError uri end diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb index 3f1d02d774..517ce33637 100644 --- a/lib/rubygems/uri_formatter.rb +++ b/lib/rubygems/uri_formatter.rb @@ -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/user_interaction.rb b/lib/rubygems/user_interaction.rb index 69de05fa24..0172c4ee89 100644 --- a/lib/rubygems/user_interaction.rb +++ b/lib/rubygems/user_interaction.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -13,7 +14,6 @@ require_relative "text" # module will have access to the +ui+ method that returns the default UI. module Gem::DefaultUserInteraction - include Gem::Text ## @@ -68,7 +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 ## @@ -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 ## @@ -258,33 +257,32 @@ class Gem::StreamUI end default_answer = case default - when nil - "yn" - when true - "Yn" - else - "yN" + when nil + "yn" + when true + "Yn" + else + "yN" end result = nil while result.nil? do result = case ask "#{question} [#{default_answer}]" - when /^y/i then true - when /^n/i then false - when /^$/ then default - else nil + when /^y/i then true + when /^n/i then false + when /^$/ then default end end - return result + result end ## # Ask a question. Returns an answer if connected to a tty, nil otherwise. def ask(question) - return nil if !tty? + return nil unless tty? @outs.print(question + " ") @outs.flush @@ -298,7 +296,7 @@ class Gem::StreamUI # Ask for a password. Does not echo response to terminal. def ask_for_password(question) - return nil if !tty? + return nil unless tty? @outs.print(question, " ") @outs.flush @@ -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 @@ -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 diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb index 05e5788932..51f9c2029f 100644 --- a/lib/rubygems/util.rb +++ b/lib/rubygems/util.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true + require_relative "deprecate" ## # This module contains various utility methods as module methods. module Gem::Util - ## # Zlib::GzipReader wrapper that unzips +data+. @@ -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,7 +84,11 @@ module Gem::Util here = File.expand_path directory loop do - Dir.chdir here, &block rescue Errno::EACCES + begin + Dir.chdir here, &block + rescue StandardError + Errno::EACCES + end new_here = File.expand_path("..", here) return if new_here == here # toplevel @@ -101,15 +105,14 @@ module Gem::Util end ## - # Corrects +path+ (usually returned by `URI.parse().path` on Windows), that + # Corrects +path+ (usually returned by `Gem::URI.parse().path` on Windows), that # comes with a leading slash. def self.correct_for_windows_path(path) - if path[0].chr == "/" && path[1].chr =~ /[a-z]/i && path[2].chr == ":" + if path[0].chr == "/" && path[1].chr.match?(/[a-z]/i) && path[2].chr == ":" path[1..-1] else path end end - end diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb index a26e964728..f3c7201639 100644 --- a/lib/rubygems/util/licenses.rb +++ b/lib/rubygems/util/licenses.rb @@ -22,14 +22,13 @@ class Gem::Licenses AFL-2.0 AFL-2.1 AFL-3.0 - AGPL-1.0 AGPL-1.0-only AGPL-1.0-or-later - AGPL-3.0 AGPL-3.0-only AGPL-3.0-or-later AMDPLPA AML + AML-glslang AMPAS ANTLR-PD ANTLR-PD-fallback @@ -39,9 +38,14 @@ class Gem::Licenses APSL-1.1 APSL-1.2 APSL-2.0 + ASWF-Digital-Assets-1.0 + ASWF-Digital-Assets-1.1 Abstyles + AdaCore-doc Adobe-2006 + Adobe-Display-PostScript Adobe-Glyph + Adobe-Utopia Afmparse Aladdin Apache-1.0 @@ -55,13 +59,13 @@ class Gem::Licenses 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 @@ -69,11 +73,22 @@ class Gem::Licenses BSD-3-Clause-No-Nuclear-License-2014 BSD-3-Clause-No-Nuclear-Warranty BSD-3-Clause-Open-MPI + BSD-3-Clause-Sun + BSD-3-Clause-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 @@ -82,9 +97,13 @@ class Gem::Licenses 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 @@ -95,6 +114,7 @@ class Gem::Licenses 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 @@ -115,6 +135,7 @@ class Gem::Licenses 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 @@ -136,6 +157,7 @@ class Gem::Licenses 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 @@ -156,6 +178,9 @@ class Gem::Licenses CERN-OHL-P-2.0 CERN-OHL-S-2.0 CERN-OHL-W-2.0 + CFITSIO + CMU-Mach + CMU-Mach-nodoc CNRI-Jython CNRI-Python CNRI-Python-GPL-Compatible @@ -165,16 +190,23 @@ class Gem::Licenses 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 @@ -192,32 +224,34 @@ class Gem::Licenses 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 + Furuseth + GCR-docs GD - GFDL-1.1 GFDL-1.1-invariants-only GFDL-1.1-invariants-or-later GFDL-1.1-no-invariants-only GFDL-1.1-no-invariants-or-later GFDL-1.1-only GFDL-1.1-or-later - GFDL-1.2 GFDL-1.2-invariants-only GFDL-1.2-invariants-or-later GFDL-1.2-no-invariants-only GFDL-1.2-no-invariants-or-later GFDL-1.2-only GFDL-1.2-or-later - GFDL-1.3 GFDL-1.3-invariants-only GFDL-1.3-invariants-or-later GFDL-1.3-no-invariants-only @@ -226,65 +260,73 @@ class Gem::Licenses GFDL-1.3-or-later GL2PS GLWTPL - GPL-1.0 - GPL-1.0+ GPL-1.0-only GPL-1.0-or-later - GPL-2.0 - GPL-2.0+ GPL-2.0-only GPL-2.0-or-later - GPL-2.0-with-GCC-exception - GPL-2.0-with-autoconf-exception - GPL-2.0-with-bison-exception - GPL-2.0-with-classpath-exception - GPL-2.0-with-font-exception - GPL-3.0 - GPL-3.0+ GPL-3.0-only GPL-3.0-or-later - GPL-3.0-with-GCC-exception - GPL-3.0-with-autoconf-exception 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 @@ -295,22 +337,32 @@ class Gem::Licenses 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 @@ -319,7 +371,11 @@ class Gem::Licenses MS-PL MS-RL MTLL + Mackerras-3-Clause + Mackerras-3-Clause-acknowledgment MakeIndex + Martin-Birgmeier + McPhee-slideshow Minpack MirOS Motosoto @@ -336,6 +392,7 @@ class Gem::Licenses NICTA-1.0 NIST-PD NIST-PD-fallback + NIST-Software NLOD-1.0 NLOD-2.0 NLPL @@ -352,12 +409,12 @@ class Gem::Licenses 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 @@ -387,8 +444,10 @@ 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 @@ -396,13 +455,18 @@ class Gem::Licenses 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 @@ -410,6 +474,7 @@ class Gem::Licenses Python-2.0 Python-2.0.1 QPL-1.0 + QPL-1.0-INRIA-2004 Qhull RHeCos-1.1 RPL-1.1 @@ -420,20 +485,25 @@ 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 @@ -442,24 +512,38 @@ class Gem::Licenses 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 @@ -469,12 +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 @@ -482,45 +569,103 @@ class Gem::Licenses ZPL-2.0 ZPL-2.1 Zed + Zeeff Zend-2.0 Zimbra-1.3 Zimbra-1.4 Zlib + bcrypt-Solar-Designer blessing - bzip2-1.0.5 bzip2-1.0.6 + check-cvs checkmk copyleft-next-0.3.0 copyleft-next-0.3.1 curl 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 @@ -530,42 +675,62 @@ 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 - REGEXP = %r{ + DEPRECATED_EXCEPTION_IDENTIFIERS = %w[ + Nokia-Qt-exception-1.1 + ].freeze + + VALID_REGEXP = / \A (?: #{Regexp.union(LICENSE_IDENTIFIERS)} @@ -575,10 +740,34 @@ class Gem::Licenses | #{LICENSE_REF} ) \Z - }ox.freeze + /ox + + DEPRECATED_LICENSE_REGEXP = / + \A + #{Regexp.union(DEPRECATED_LICENSE_IDENTIFIERS)} + \+? + (?:\s WITH \s .+?)? + \Z + /ox + + DEPRECATED_EXCEPTION_REGEXP = / + \A + .+? + \+? + (?:\s WITH \s #{Regexp.union(DEPRECATED_EXCEPTION_IDENTIFIERS)}) + \Z + /ox def self.match?(license) - !REGEXP.match(license).nil? + VALID_REGEXP.match?(license) + end + + def self.deprecated_license_id?(license) + DEPRECATED_LICENSE_REGEXP.match?(license) + end + + def self.deprecated_exception_id?(license) + DEPRECATED_EXCEPTION_REGEXP.match?(license) end def self.suggestions(license) diff --git a/lib/rubygems/util/list.rb b/lib/rubygems/util/list.rb index fc2ab38c45..2899e8a2b9 100644 --- a/lib/rubygems/util/list.rb +++ b/lib/rubygems/util/list.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + module Gem # The Gem::List class is currently unused and will be removed in the next major rubygems version class List # :nodoc: diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb index 1609924607..57e0747eb4 100644 --- a/lib/rubygems/validator.rb +++ b/lib/rubygems/validator.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -24,7 +25,7 @@ class Gem::Validator installed_files = [] Find.find gem_directory do |file_name| - fn = file_name[gem_directory.size..file_name.size - 1].sub(/^\//, "") + fn = file_name[gem_directory.size..file_name.size - 1].sub(%r{^/}, "") installed_files << fn unless fn.empty? || fn.include?("CVS") || File.directory?(file_name) end @@ -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/optparse/.document b/lib/rubygems/vendor/molinillo/.document index 0c43bbd6b3..0c43bbd6b3 100644 --- a/lib/rubygems/optparse/.document +++ b/lib/rubygems/vendor/molinillo/.document 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 731a9e3e90..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_relative '../../../../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 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 4289902828..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 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/tsort/.document b/lib/rubygems/vendor/net-http/.document index 0c43bbd6b3..0c43bbd6b3 100644 --- a/lib/rubygems/tsort/.document +++ b/lib/rubygems/vendor/net-http/.document diff --git a/lib/rubygems/vendor/net-http/lib/net/http.rb b/lib/rubygems/vendor/net-http/lib/net/http.rb new file mode 100644 index 0000000000..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/optparse/lib/optionparser.rb b/lib/rubygems/vendor/optparse/lib/optionparser.rb index 4b9b40d82a..4b9b40d82a 100644 --- a/lib/rubygems/optparse/lib/optionparser.rb +++ b/lib/rubygems/vendor/optparse/lib/optionparser.rb diff --git a/lib/rubygems/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb index 1e50bda769..5937431720 100644 --- a/lib/rubygems/optparse/lib/optparse.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse.rb @@ -48,7 +48,7 @@ # # == Gem::OptionParser # -# === New to \Gem::OptionParser? +# === New to +Gem::OptionParser+? # # See the {Tutorial}[optparse/tutorial.rdoc]. # @@ -73,7 +73,7 @@ # # === Minimal example # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -92,7 +92,7 @@ # Gem::OptionParser can be used to automatically generate help for the commands you # write: # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # Options = Struct.new(:name) # @@ -130,7 +130,7 @@ # option name in all caps. If an option is used without the required argument, # an exception will be raised. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -152,14 +152,14 @@ # Gem::OptionParser supports the ability to coerce command line arguments # into objects for us. # -# Gem::OptionParser comes with a few ready-to-use kinds of type +# Gem::OptionParser comes with a few ready-to-use kinds of type # coercion. They are: # -# - Date -- Anything accepted by +Date.parse+ -# - DateTime -- Anything accepted by +DateTime.parse+ -# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ -# - URI -- Anything accepted by +URI.parse+ -# - Shellwords -- Anything accepted by +Shellwords.shellwords+ +# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+) +# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+) +# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+) +# - URI -- Anything accepted by +Gem::URI.parse+ (need to require +optparse/uri+) +# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+) # - String -- Any non-empty string # - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040) # - Float -- Any float. (e.g. 10, 3.14, -100E+13) @@ -183,8 +183,8 @@ # as a +Time+. If it succeeds, that time will be passed to the # handler block. Otherwise, an exception will be raised. # -# require 'rubygems/optparse/lib/optparse' -# require 'rubygems/optparse/lib/optparse/time' +# require 'rubygems/vendor/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse/time' # Gem::OptionParser.new do |parser| # parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time| # p time @@ -206,7 +206,7 @@ # It specifies which conversion block to call whenever a class is specified. # The example below uses it to fetch a +User+ object before the +on+ handler receives it. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # User = Struct.new(:id, :name) # @@ -242,7 +242,7 @@ # # The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash. # -# require 'rubygems/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse' # # options = {} # Gem::OptionParser.new do |parser| @@ -268,8 +268,8 @@ # effect of specifying various options. This is probably the best way to learn # the features of +optparse+. # -# require 'rubygems/optparse/lib/optparse' -# require 'rubygems/optparse/lib/optparse/time' +# require 'rubygems/vendor/optparse/lib/optparse' +# require 'rubygems/vendor/optparse/lib/optparse/time' # require 'ostruct' # require 'pp' # @@ -425,7 +425,7 @@ # If you have any questions, file a ticket at http://bugs.ruby-lang.org. # class Gem::OptionParser - Gem::OptionParser::Version = "0.3.0" + Gem::OptionParser::Version = "0.4.0" # :stopdoc: NoArgument = [NO_ARGUMENT = :NONE, nil].freeze @@ -1084,7 +1084,7 @@ XXX Switch::OptionalArgument.new do |pkg| if pkg begin - require 'rubygems/optparse/lib/optparse/version' + require 'rubygems/vendor/optparse/lib/optparse/version' rescue LoadError else show_version(*pkg.split(/,/)) or @@ -1775,7 +1775,16 @@ XXX # # params["bar"] = "x" # --bar x # # params["zot"] = "z" # --zot Z # - def getopts(*args) + # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings). + # + # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true) + # # params[:a] = true # -a + # # params[:b] = "1" # -b1 + # # params[:foo] = "1" # --foo + # # params[:bar] = "x" # --bar x + # # params[:zot] = "z" # --zot Z + # + def getopts(*args, symbolize_names: false) argv = Array === args.first ? args.shift : default_argv single_options, *long_options = *args @@ -1804,14 +1813,14 @@ XXX end parse_in_order(argv, result.method(:[]=)) - result + symbolize_names ? result.transform_keys(&:to_sym) : result end # # See #getopts. # - def self.getopts(*args) - new.getopts(*args) + def self.getopts(*args, symbolize_names: false) + new.getopts(*args, symbolize_names: symbolize_names) end # @@ -2084,10 +2093,23 @@ XXX f |= Regexp::IGNORECASE if /i/ =~ o f |= Regexp::MULTILINE if /m/ =~ o f |= Regexp::EXTENDED if /x/ =~ o - k = o.delete("imx") - k = nil if k.empty? + case o = o.delete("imx") + when "" + when "u" + s = s.encode(Encoding::UTF_8) + when "e" + s = s.encode(Encoding::EUC_JP) + when "s" + s = s.encode(Encoding::SJIS) + when "n" + f |= Regexp::NOENCODING + else + raise Gem::OptionParser::InvalidArgument, "unknown regexp option - #{o}" + end + else + s ||= all end - Regexp.new(s || all, f, k) + Regexp.new(s, f) end # @@ -2276,8 +2298,8 @@ XXX # rescue Gem::OptionParser::ParseError # end # - def getopts(*args) - options.getopts(self, *args) + def getopts(*args, symbolize_names: false) + options.getopts(self, *args, symbolize_names: symbolize_names) end # diff --git a/lib/rubygems/optparse/lib/optparse/ac.rb b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb index e84d01bf91..e84d01bf91 100644 --- a/lib/rubygems/optparse/lib/optparse/ac.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb diff --git a/lib/rubygems/optparse/lib/optparse/date.rb b/lib/rubygems/vendor/optparse/lib/optparse/date.rb index d9a9f4f48a..d9a9f4f48a 100644 --- a/lib/rubygems/optparse/lib/optparse/date.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/date.rb diff --git a/lib/rubygems/optparse/lib/optparse/kwargs.rb b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb index 6987a5ed62..6987a5ed62 100644 --- a/lib/rubygems/optparse/lib/optparse/kwargs.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb diff --git a/lib/rubygems/optparse/lib/optparse/shellwords.rb b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb index d47ad60255..d47ad60255 100644 --- a/lib/rubygems/optparse/lib/optparse/shellwords.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb diff --git a/lib/rubygems/optparse/lib/optparse/time.rb b/lib/rubygems/vendor/optparse/lib/optparse/time.rb index c59e1e4ced..c59e1e4ced 100644 --- a/lib/rubygems/optparse/lib/optparse/time.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/time.rb diff --git a/lib/rubygems/vendor/optparse/lib/optparse/uri.rb b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb new file mode 100644 index 0000000000..398127479a --- /dev/null +++ b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: false +# -*- ruby -*- + +require_relative '../optparse' +require_relative '../../../uri/lib/uri' + +Gem::OptionParser.accept(Gem::URI) {|s,| Gem::URI.parse(s) if s} diff --git a/lib/rubygems/optparse/lib/optparse/version.rb b/lib/rubygems/vendor/optparse/lib/optparse/version.rb index 5d79e9db44..5d79e9db44 100644 --- a/lib/rubygems/optparse/lib/optparse/version.rb +++ b/lib/rubygems/vendor/optparse/lib/optparse/version.rb 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/tsort/lib/tsort.rb b/lib/rubygems/vendor/tsort/lib/tsort.rb index f825f14257..9dd7c09521 100644 --- a/lib/rubygems/tsort/lib/tsort.rb +++ b/lib/rubygems/vendor/tsort/lib/tsort.rb @@ -32,7 +32,7 @@ # method, which fetches the array of child nodes and then iterates over that # array using the user-supplied block. # -# require 'rubygems/tsort/lib/tsort' +# require 'rubygems/vendor/tsort/lib/tsort' # # class Hash # include Gem::TSort @@ -52,7 +52,7 @@ # # A very simple `make' like tool can be implemented as follows: # -# require 'rubygems/tsort/lib/tsort' +# require 'rubygems/vendor/tsort/lib/tsort' # # class Make # def initialize @@ -122,6 +122,9 @@ # module Gem::TSort + + VERSION = "0.2.0" + class Cyclic < StandardError end diff --git a/lib/rubygems/vendor/uri/.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 903b7de99d..e174d8ad95 100644 --- a/lib/rubygems/version.rb +++ b/lib/rubygems/version.rb @@ -131,7 +131,7 @@ require_relative "deprecate" # # == 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 @@ -156,16 +156,16 @@ class Gem::Version include Comparable VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc: - ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/.freeze # :nodoc: + ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc: ## # 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. @@ -173,7 +173,7 @@ class Gem::Version def self.correct?(version) nil_versions_are_discouraged! if version.nil? - !!(version.to_s =~ ANCHORED_VERSION_PATTERN) + ANCHORED_VERSION_PATTERN.match?(version.to_s) end ## @@ -201,7 +201,7 @@ 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 @@ -224,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 - @version = version.to_s.strip.gsub("-",".pre.") + # optimization to avoid allocation when given an integer, since we know + # it's to_s won't have any spaces or dashes + unless version.is_a?(Integer) + @version = @version.strip + @version.gsub!("-",".pre.") + end + @version = -@version @segments = nil end @@ -252,7 +260,7 @@ class Gem::Version # same precision. Version "1.0" is not the same as version "1". def eql?(other) - self.class === other && @version == other._version + self.class === other && @version == other.version end def hash # :nodoc: @@ -272,7 +280,7 @@ class Gem::Version # string for backwards (RubyGems 1.3.5 and earlier) compatibility. def marshal_dump - [version] + [@version] end ## @@ -284,7 +292,7 @@ class Gem::Version end def yaml_initialize(tag, map) # :nodoc: - @version = map["version"] + @version = -map["version"] @segments = nil @hash = nil end @@ -302,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 @@ -354,7 +362,7 @@ class Gem::Version 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 @@ -366,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 @@ -376,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 a487a0bc24..7910fd3d1b 100644 --- a/lib/rubygems/version_option.rb +++ b/lib/rubygems/version_option.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true + #-- # Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others. # All rights reserved. @@ -11,7 +12,6 @@ require_relative "../rubygems" # Mixin methods for --version and --platform Gem::Command options. module Gem::VersionOption - ## # Add the --platform option to the option parser. @@ -25,8 +25,7 @@ module Gem::VersionOption end add_option("--platform PLATFORM", Gem::Platform, - "Specify the platform of gem to #{task}", *wrap) do - |value, options| + "Specify the platform of gem to #{task}", *wrap) do |value, options| unless options[:added_platform] Gem.platforms = [Gem::Platform::RUBY] options[:added_platform] = true @@ -56,8 +55,7 @@ module Gem::VersionOption end add_option("-v", "--version VERSION", Gem::Requirement, - "Specify version of gem to #{task}", *wrap) do - |value, options| + "Specify version of gem to #{task}", *wrap) do |value, options| # Allow handling for multiple --version operators if options[:version] && !options[:version].none? options[:version].concat([value]) diff --git a/lib/rubygems/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 |