summaryrefslogtreecommitdiff
path: root/lib/rubygems
diff options
context:
space:
mode:
Diffstat (limited to 'lib/rubygems')
-rw-r--r--lib/rubygems/available_set.rb165
-rw-r--r--lib/rubygems/basic_specification.rb384
-rw-r--r--lib/rubygems/bundler_version_finder.rb135
-rw-r--r--lib/rubygems/ci_detector.rb75
-rw-r--r--lib/rubygems/command.rb664
-rw-r--r--lib/rubygems/command_manager.rb254
-rw-r--r--lib/rubygems/commands/build_command.rb120
-rw-r--r--lib/rubygems/commands/cert_command.rb325
-rw-r--r--lib/rubygems/commands/check_command.rb97
-rw-r--r--lib/rubygems/commands/cleanup_command.rb178
-rw-r--r--lib/rubygems/commands/contents_command.rb196
-rw-r--r--lib/rubygems/commands/dependency_command.rb206
-rw-r--r--lib/rubygems/commands/environment_command.rb182
-rw-r--r--lib/rubygems/commands/exec_command.rb259
-rw-r--r--lib/rubygems/commands/fetch_command.rb109
-rw-r--r--lib/rubygems/commands/generate_index_command.rb51
-rw-r--r--lib/rubygems/commands/help_command.rb377
-rw-r--r--lib/rubygems/commands/info_command.rb38
-rw-r--r--lib/rubygems/commands/install_command.rb268
-rw-r--r--lib/rubygems/commands/list_command.rb42
-rw-r--r--lib/rubygems/commands/lock_command.rb109
-rw-r--r--lib/rubygems/commands/mirror_command.rb26
-rw-r--r--lib/rubygems/commands/open_command.rb83
-rw-r--r--lib/rubygems/commands/outdated_command.rb33
-rw-r--r--lib/rubygems/commands/owner_command.rb125
-rw-r--r--lib/rubygems/commands/pristine_command.rb223
-rw-r--r--lib/rubygems/commands/push_command.rb185
-rw-r--r--lib/rubygems/commands/rdoc_command.rb90
-rw-r--r--lib/rubygems/commands/rebuild_command.rb261
-rw-r--r--lib/rubygems/commands/search_command.rb41
-rw-r--r--lib/rubygems/commands/server_command.rb26
-rw-r--r--lib/rubygems/commands/setup_command.rb667
-rw-r--r--lib/rubygems/commands/signin_command.rb34
-rw-r--r--lib/rubygems/commands/signout_command.rb32
-rw-r--r--lib/rubygems/commands/sources_command.rb348
-rw-r--r--lib/rubygems/commands/specification_command.rb156
-rw-r--r--lib/rubygems/commands/stale_command.rb40
-rw-r--r--lib/rubygems/commands/uninstall_command.rb204
-rw-r--r--lib/rubygems/commands/unpack_command.rb168
-rw-r--r--lib/rubygems/commands/update_command.rb326
-rw-r--r--lib/rubygems/commands/which_command.rb88
-rw-r--r--lib/rubygems/commands/yank_command.rb99
-rw-r--r--lib/rubygems/config_file.rb652
-rw-r--r--lib/rubygems/core_ext/kernel_gem.rb68
-rw-r--r--lib/rubygems/core_ext/kernel_require.rb152
-rw-r--r--lib/rubygems/core_ext/kernel_warn.rb45
-rw-r--r--lib/rubygems/core_ext/tcpsocket_init.rb54
-rw-r--r--lib/rubygems/defaults.rb317
-rw-r--r--lib/rubygems/dependency.rb348
-rw-r--r--lib/rubygems/dependency_installer.rb264
-rw-r--r--lib/rubygems/dependency_list.rb242
-rw-r--r--lib/rubygems/deprecate.rb171
-rw-r--r--lib/rubygems/doctor.rb132
-rw-r--r--lib/rubygems/errors.rb177
-rw-r--r--lib/rubygems/exceptions.rb251
-rw-r--r--lib/rubygems/ext.rb20
-rw-r--r--lib/rubygems/ext/build_error.rb9
-rw-r--r--lib/rubygems/ext/builder.rb271
-rw-r--r--lib/rubygems/ext/cargo_builder.rb349
-rw-r--r--lib/rubygems/ext/cargo_builder/link_flag_converter.rb27
-rw-r--r--lib/rubygems/ext/cmake_builder.rb110
-rw-r--r--lib/rubygems/ext/configure_builder.rb26
-rw-r--r--lib/rubygems/ext/ext_conf_builder.rb81
-rw-r--r--lib/rubygems/ext/rake_builder.rb37
-rw-r--r--lib/rubygems/gem_runner.rb88
-rw-r--r--lib/rubygems/gemcutter_utilities.rb398
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_listener.rb112
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb163
-rw-r--r--lib/rubygems/gemcutter_utilities/webauthn_poller.rb80
-rw-r--r--lib/rubygems/gemspec_helpers.rb19
-rw-r--r--lib/rubygems/install_message.rb13
-rw-r--r--lib/rubygems/install_update_options.rb224
-rw-r--r--lib/rubygems/installer.rb1043
-rw-r--r--lib/rubygems/installer_uninstaller_utils.rb27
-rw-r--r--lib/rubygems/local_remote_options.rb146
-rw-r--r--lib/rubygems/name_tuple.rb125
-rw-r--r--lib/rubygems/openssl.rb7
-rw-r--r--lib/rubygems/package.rb769
-rw-r--r--lib/rubygems/package/digest_io.rb63
-rw-r--r--lib/rubygems/package/file_source.rb32
-rw-r--r--lib/rubygems/package/io_source.rb48
-rw-r--r--lib/rubygems/package/old.rb169
-rw-r--r--lib/rubygems/package/source.rb4
-rw-r--r--lib/rubygems/package/tar_header.rb277
-rw-r--r--lib/rubygems/package/tar_reader.rb103
-rw-r--r--lib/rubygems/package/tar_reader/entry.rb244
-rw-r--r--lib/rubygems/package/tar_writer.rb332
-rw-r--r--lib/rubygems/package_task.rb123
-rw-r--r--lib/rubygems/path_support.rb85
-rw-r--r--lib/rubygems/platform.rb392
-rw-r--r--lib/rubygems/psych_tree.rb37
-rw-r--r--lib/rubygems/query_utils.rb349
-rw-r--r--lib/rubygems/rdoc.rb26
-rw-r--r--lib/rubygems/remote_fetcher.rb350
-rw-r--r--lib/rubygems/request.rb299
-rw-r--r--lib/rubygems/request/connection_pools.rb96
-rw-r--r--lib/rubygems/request/http_pool.rb54
-rw-r--r--lib/rubygems/request/https_pool.rb10
-rw-r--r--lib/rubygems/request_set.rb514
-rw-r--r--lib/rubygems/request_set/gem_dependency_api.rb841
-rw-r--r--lib/rubygems/request_set/lockfile.rb233
-rw-r--r--lib/rubygems/requirement.rb298
-rw-r--r--lib/rubygems/resolver.rb565
-rw-r--r--lib/rubygems/resolver/activation_request.rb159
-rw-r--r--lib/rubygems/resolver/api_set.rb139
-rw-r--r--lib/rubygems/resolver/api_set/gem_parser.rb21
-rw-r--r--lib/rubygems/resolver/api_specification.rb105
-rw-r--r--lib/rubygems/resolver/best_set.rb49
-rw-r--r--lib/rubygems/resolver/composed_set.rb65
-rw-r--r--lib/rubygems/resolver/current_set.rb12
-rw-r--r--lib/rubygems/resolver/dependency_request.rb119
-rw-r--r--lib/rubygems/resolver/git_set.rb120
-rw-r--r--lib/rubygems/resolver/git_specification.rb57
-rw-r--r--lib/rubygems/resolver/incompatibility.rb10
-rw-r--r--lib/rubygems/resolver/index_set.rb79
-rw-r--r--lib/rubygems/resolver/index_specification.rb101
-rw-r--r--lib/rubygems/resolver/installed_specification.rb57
-rw-r--r--lib/rubygems/resolver/installer_set.rb271
-rw-r--r--lib/rubygems/resolver/local_specification.rb40
-rw-r--r--lib/rubygems/resolver/lock_set.rb81
-rw-r--r--lib/rubygems/resolver/lock_specification.rb86
-rw-r--r--lib/rubygems/resolver/requirement_list.rb82
-rw-r--r--lib/rubygems/resolver/set.rb55
-rw-r--r--lib/rubygems/resolver/source_set.rb47
-rw-r--r--lib/rubygems/resolver/spec_specification.rb76
-rw-r--r--lib/rubygems/resolver/specification.rb126
-rw-r--r--lib/rubygems/resolver/strategy.rb44
-rw-r--r--lib/rubygems/resolver/vendor_set.rb86
-rw-r--r--lib/rubygems/resolver/vendor_specification.rb23
-rw-r--r--lib/rubygems/s3_uri_signer.rb226
-rw-r--r--lib/rubygems/safe_marshal.rb75
-rw-r--r--lib/rubygems/safe_marshal/elements.rb146
-rw-r--r--lib/rubygems/safe_marshal/reader.rb325
-rw-r--r--lib/rubygems/safe_marshal/visitors/stream_printer.rb31
-rw-r--r--lib/rubygems/safe_marshal/visitors/to_ruby.rb428
-rw-r--r--lib/rubygems/safe_marshal/visitors/visitor.rb74
-rw-r--r--lib/rubygems/safe_yaml.rb55
-rw-r--r--lib/rubygems/security.rb615
-rw-r--r--lib/rubygems/security/policies.rb114
-rw-r--r--lib/rubygems/security/policy.rb288
-rw-r--r--lib/rubygems/security/signer.rb212
-rw-r--r--lib/rubygems/security/trust_dir.rb117
-rw-r--r--lib/rubygems/security_option.rb43
-rw-r--r--lib/rubygems/source.rb253
-rw-r--r--lib/rubygems/source/git.rb244
-rw-r--r--lib/rubygems/source/installed.rb39
-rw-r--r--lib/rubygems/source/local.rb135
-rw-r--r--lib/rubygems/source/lock.rb49
-rw-r--r--lib/rubygems/source/specific_file.rb73
-rw-r--r--lib/rubygems/source/vendor.rb24
-rw-r--r--lib/rubygems/source_list.rb182
-rw-r--r--lib/rubygems/spec_fetcher.rb290
-rw-r--r--lib/rubygems/specification.rb2604
-rw-r--r--lib/rubygems/specification_policy.rb557
-rw-r--r--lib/rubygems/specification_record.rb225
-rw-r--r--lib/rubygems/ssl_certs/.document1
-rw-r--r--lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem21
-rw-r--r--lib/rubygems/stub_specification.rb236
-rw-r--r--lib/rubygems/target_rbconfig.rb50
-rw-r--r--lib/rubygems/text.rb94
-rw-r--r--lib/rubygems/uninstaller.rb440
-rw-r--r--lib/rubygems/unknown_command_spell_checker.rb21
-rw-r--r--lib/rubygems/update_suggestion.rb56
-rw-r--r--lib/rubygems/uri.rb126
-rw-r--r--lib/rubygems/uri_formatter.rb48
-rw-r--r--lib/rubygems/user_interaction.rb647
-rw-r--r--lib/rubygems/util.rb96
-rw-r--r--lib/rubygems/util/atomic_file_writer.rb76
-rw-r--r--lib/rubygems/util/licenses.rb888
-rw-r--r--lib/rubygems/validator.rb144
-rw-r--r--lib/rubygems/vendor/.document1
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http.rb2608
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb35
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb429
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/header.rb985
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb17
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/request.rb88
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/requests.rb444
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/response.rb739
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/responses.rb1242
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/http/status.rb84
-rw-r--r--lib/rubygems/vendor/net-http/lib/net/https.rb23
-rw-r--r--lib/rubygems/vendor/net-protocol/lib/net/protocol.rb544
-rw-r--r--lib/rubygems/vendor/optparse/lib/optionparser.rb2
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse.rb2467
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/ac.rb70
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/date.rb18
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb27
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb7
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/time.rb11
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/uri.rb7
-rw-r--r--lib/rubygems/vendor/optparse/lib/optparse/version.rb80
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub.rb53
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb20
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb169
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb182
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb150
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb43
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb121
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb45
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb19
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb61
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb42
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb105
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb3
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb129
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb423
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb236
-rw-r--r--lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb178
-rw-r--r--lib/rubygems/vendor/resolv/lib/resolv.rb3499
-rw-r--r--lib/rubygems/vendor/securerandom/lib/securerandom.rb102
-rw-r--r--lib/rubygems/vendor/timeout/lib/timeout.rb201
-rw-r--r--lib/rubygems/vendor/tsort/lib/tsort.rb455
-rw-r--r--lib/rubygems/vendor/uri/lib/uri.rb104
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/common.rb922
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/file.rb100
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ftp.rb267
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/generic.rb1592
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/http.rb137
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/https.rb23
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ldap.rb261
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ldaps.rb22
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/mailto.rb293
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb547
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb206
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/version.rb6
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/ws.rb83
-rw-r--r--lib/rubygems/vendor/uri/lib/uri/wss.rb23
-rw-r--r--lib/rubygems/vendored_net_http.rb5
-rw-r--r--lib/rubygems/vendored_optparse.rb3
-rw-r--r--lib/rubygems/vendored_pub_grub.rb3
-rw-r--r--lib/rubygems/vendored_securerandom.rb3
-rw-r--r--lib/rubygems/vendored_timeout.rb5
-rw-r--r--lib/rubygems/vendored_tsort.rb3
-rw-r--r--lib/rubygems/version.rb472
-rw-r--r--lib/rubygems/version_option.rb80
-rw-r--r--lib/rubygems/win_platform.rb30
-rw-r--r--lib/rubygems/yaml_serializer.rb845
238 files changed, 54727 insertions, 0 deletions
diff --git a/lib/rubygems/available_set.rb b/lib/rubygems/available_set.rb
new file mode 100644
index 0000000000..0af80cc3db
--- /dev/null
+++ b/lib/rubygems/available_set.rb
@@ -0,0 +1,165 @@
+# frozen_string_literal: true
+
+class Gem::AvailableSet
+ include Enumerable
+
+ Tuple = Struct.new(:spec, :source)
+
+ attr_accessor :remote # :nodoc:
+
+ def initialize
+ @set = []
+ @sorted = nil
+ @remote = true
+ end
+
+ attr_reader :set
+
+ def add(spec, source)
+ @set << Tuple.new(spec, source)
+ @sorted = nil
+ self
+ end
+
+ def <<(o)
+ case o
+ when Gem::AvailableSet
+ s = o.set
+ when Array
+ s = o.map do |sp,so|
+ if !sp.is_a?(Gem::Specification) || !so.is_a?(Gem::Source)
+ raise TypeError, "Array must be in [[spec, source], ...] form"
+ end
+
+ Tuple.new(sp,so)
+ end
+ else
+ raise TypeError, "must be a Gem::AvailableSet"
+ end
+
+ @set += s
+ @sorted = nil
+
+ self
+ end
+
+ ##
+ # Yields each Tuple in this AvailableSet
+
+ def each
+ return enum_for __method__ unless block_given?
+
+ @set.each do |tuple|
+ yield tuple
+ end
+ end
+
+ ##
+ # Yields the Gem::Specification for each Tuple in this AvailableSet
+
+ def each_spec
+ return enum_for __method__ unless block_given?
+
+ each do |tuple|
+ yield tuple.spec
+ end
+ end
+
+ def empty?
+ @set.empty?
+ end
+
+ def all_specs
+ @set.map(&:spec)
+ end
+
+ def match_platform!
+ @set.reject! {|t| !Gem::Platform.match_spec?(t.spec) }
+ @sorted = nil
+ self
+ end
+
+ def sorted
+ @sorted ||= @set.sort do |a,b|
+ i = b.spec <=> a.spec
+ i != 0 ? i : (a.source <=> b.source)
+ end
+ end
+
+ def size
+ @set.size
+ end
+
+ def source_for(spec)
+ f = @set.find {|t| t.spec == spec }
+ f.source
+ end
+
+ ##
+ # Converts this AvailableSet into a RequestSet that can be used to install
+ # gems.
+ #
+ # If +development+ is :none then no development dependencies are installed.
+ # Other options are :shallow for only direct development dependencies of the
+ # gems in this set or :all for all development dependencies.
+
+ def to_request_set(development = :none)
+ request_set = Gem::RequestSet.new
+ 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
+ development == :shallow
+ end
+
+ request_set
+ end
+
+ ##
+ #
+ # Used by the Resolver, the protocol to use a AvailableSet as a
+ # search Set.
+
+ def find_all(req)
+ dep = req.dependency
+
+ match = @set.find_all do |t|
+ dep.match? t.spec
+ end
+
+ match.map do |t|
+ Gem::Resolver::LocalSpecification.new(self, t.spec, t.source)
+ end
+ end
+
+ def prefetch(reqs)
+ end
+
+ def pick_best!
+ return self if empty?
+
+ @set = [sorted.first]
+ @sorted = nil
+ self
+ end
+
+ def remove_installed!(dep)
+ @set.reject! do |_t|
+ # already locally installed
+ Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name &&
+ dep.requirement.satisfied_by?(installed_spec.version)
+ end
+ end
+
+ @sorted = nil
+ self
+ end
+
+ def inject_into_list(dep_list)
+ @set.each {|t| dep_list.add t.spec }
+ end
+end
diff --git a/lib/rubygems/basic_specification.rb b/lib/rubygems/basic_specification.rb
new file mode 100644
index 0000000000..0ed7fc60bb
--- /dev/null
+++ b/lib/rubygems/basic_specification.rb
@@ -0,0 +1,384 @@
+# frozen_string_literal: true
+
+##
+# BasicSpecification is an abstract class which implements some common code
+# used by both Specification and StubSpecification.
+
+class Gem::BasicSpecification
+ ##
+ # Allows installation of extensions for git: gems.
+
+ attr_writer :base_dir # :nodoc:
+
+ ##
+ # Sets the directory where extensions for this gem will be installed.
+
+ attr_writer :extension_dir # :nodoc:
+
+ ##
+ # Is this specification ignored for activation purposes?
+
+ attr_writer :ignored # :nodoc:
+
+ ##
+ # The path this gemspec was loaded from. This attribute is not persisted.
+
+ attr_accessor :loaded_from
+
+ ##
+ # Allows correct activation of git: and path: gems.
+
+ attr_writer :full_gem_path # :nodoc:
+
+ def initialize
+ internal_init
+ end
+
+ ##
+ # The path to the gem.build_complete file within the extension install
+ # directory.
+
+ def gem_build_complete_path # :nodoc:
+ File.join extension_dir, "gem.build_complete"
+ end
+
+ ##
+ # True when the gem has been activated
+
+ def activated?
+ raise NotImplementedError
+ end
+
+ ##
+ # Returns the full path to the base gem directory.
+ #
+ # eg: /usr/local/lib/ruby/gems/1.8
+
+ def base_dir
+ raise NotImplementedError
+ end
+
+ ##
+ # Return true if this spec can require +file+.
+
+ def contains_requirable_file?(file)
+ if ignored?
+ if platform == Gem::Platform::RUBY || Gem::Platform.local === platform
+ warn "Ignoring #{full_name} because its extensions are not built. " \
+ "Try: gem pristine #{name} --version #{version}"
+ end
+
+ return false
+ end
+
+ is_soext = file.end_with?(".so", ".o")
+
+ if is_soext
+ have_file? file.delete_suffix(File.extname(file)), Gem.dynamic_library_suffixes
+ else
+ have_file? file, Gem.suffixes
+ end
+ end
+
+ ##
+ # Return true if this spec should be ignored because it's missing extensions.
+
+ def ignored?
+ return @ignored unless @ignored.nil?
+
+ @ignored = missing_extensions?
+ end
+
+ def default_gem?
+ !loaded_from.nil? &&
+ File.dirname(loaded_from) == Gem.default_specifications_dir
+ end
+
+ ##
+ # Regular gems take precedence over default gems
+
+ def default_gem_priority
+ default_gem? ? 1 : -1
+ end
+
+ ##
+ # Gems higher up in +gem_path+ take precedence
+
+ def base_dir_priority(gem_path)
+ gem_path.index(base_dir) || gem_path.size
+ end
+
+ ##
+ # Returns full path to the directory where gem's extensions are installed.
+
+ def extension_dir
+ @extension_dir ||= File.expand_path(File.join(extensions_dir, full_name))
+ end
+
+ ##
+ # Returns path to the extensions directory.
+
+ def extensions_dir
+ Gem.default_ext_dir_for(base_dir) ||
+ File.join(base_dir, "extensions", Gem::Platform.local.to_s,
+ Gem.extension_api_version)
+ end
+
+ def find_full_gem_path # :nodoc:
+ File.expand_path File.join(gems_dir, full_name)
+ end
+
+ private :find_full_gem_path
+
+ ##
+ # The full path to the gem (install path + full name).
+ #
+ # TODO: This is duplicated with #gem_dir. Eventually either of them should be deprecated.
+
+ def full_gem_path
+ @full_gem_path ||= find_full_gem_path
+ end
+
+ ##
+ # Returns the full name (name-version) of this Gem. Platform information
+ # is included (name-version-platform) if it is specified and not the
+ # default Ruby platform.
+
+ def full_name
+ if platform == Gem::Platform::RUBY || platform.nil?
+ "#{name}-#{version}"
+ else
+ "#{name}-#{version}-#{platform}"
+ end
+ end
+
+ ##
+ # Returns the full name of this Gem (see `Gem::BasicSpecification#full_name`).
+ # Information about where the gem is installed is also included if not
+ # installed in the default GEM_HOME.
+
+ def full_name_with_location
+ if base_dir != Gem.dir
+ "#{full_name} in #{base_dir}"
+ else
+ full_name
+ end
+ end
+
+ ##
+ # Full paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+
+ def full_require_paths
+ @full_require_paths ||=
+ 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
+ end
+ end
+
+ ##
+ # The path to the data directory for this gem.
+
+ def datadir
+ # TODO: drop the extra ", gem_name" which is uselessly redundant
+ File.expand_path(File.join(gems_dir, full_name, "data", name))
+ end
+
+ extend Gem::Deprecate
+ rubygems_deprecate :datadir, :none, "4.1"
+
+ ##
+ # Full path of the target library file.
+ # If the file is not in this gem, return nil.
+
+ def to_fullpath(path)
+ if activated?
+ @paths_map ||= {}
+ 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
+ @paths_map[path]
+ end
+ end
+
+ ##
+ # Returns the full path to this spec's gem directory.
+ # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0
+ #
+ # TODO: This is duplicated with #full_gem_path. Eventually either of them should be deprecated.
+
+ def gem_dir
+ @gem_dir ||= find_full_gem_path
+ end
+
+ ##
+ # Returns the full path to the gems directory containing this spec's
+ # gem directory. eg: /usr/local/lib/ruby/1.8/gems
+
+ def gems_dir
+ raise NotImplementedError
+ end
+
+ def internal_init # :nodoc:
+ @extension_dir = nil
+ @full_gem_path = nil
+ @gem_dir = nil
+ @ignored = nil
+ end
+
+ ##
+ # Name of the gem
+
+ def name
+ raise NotImplementedError
+ end
+
+ ##
+ # Platform of the gem
+
+ def platform
+ raise NotImplementedError
+ end
+
+ def installable_on_platform?(target_platform) # :nodoc:
+ return true if [Gem::Platform::RUBY, nil, target_platform].include?(platform)
+ return true if Gem::Platform.new(platform) === target_platform
+
+ false
+ end
+
+ def raw_require_paths # :nodoc:
+ raise NotImplementedError
+ end
+
+ ##
+ # Paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+ #
+ # See also #require_paths=
+ #
+ # If you have an extension you do not need to add <code>"ext"</code> to the
+ # require path, the extension build process will copy the extension files
+ # into "lib" for you.
+ #
+ # The default value is <code>"lib"</code>
+ #
+ # Usage:
+ #
+ # # If all library files are in the root directory...
+ # spec.require_path = '.'
+
+ def require_paths
+ return raw_require_paths unless have_extensions?
+
+ [extension_dir].concat raw_require_paths
+ end
+
+ ##
+ # Returns the paths to the source files for use with analysis and
+ # documentation tools. These paths are relative to full_gem_path.
+
+ def source_paths
+ paths = raw_require_paths.dup
+
+ if have_extensions?
+ ext_dirs = extensions.map do |extension|
+ extension.split(File::SEPARATOR, 2).first
+ end.uniq
+
+ paths.concat ext_dirs
+ end
+
+ paths.uniq
+ end
+
+ ##
+ # Return all files in this gem that match for +glob+.
+
+ def matches_for_glob(glob) # TODO: rename?
+ glob = File.join(lib_dirs_glob, glob)
+
+ Dir[glob]
+ end
+
+ ##
+ # Returns the list of plugins in this spec.
+
+ def plugins
+ matches_for_glob("rubygems#{Gem.plugin_suffix_pattern}")
+ end
+
+ ##
+ # Returns a string usable in Dir.glob to match all requirable paths
+ # for this spec.
+
+ def lib_dirs_glob
+ dirs = if raw_require_paths
+ if raw_require_paths.size > 1
+ "{#{raw_require_paths.join(",")}}"
+ else
+ raw_require_paths.first
+ end
+ else
+ "lib" # default value for require_paths for bundler/inline
+ end
+
+ "#{full_gem_path}/#{dirs}"
+ end
+
+ ##
+ # Return a Gem::Specification from this gem
+
+ def to_spec
+ raise NotImplementedError
+ end
+
+ ##
+ # Version of the gem
+
+ def version
+ raise NotImplementedError
+ end
+
+ ##
+ # Whether this specification is stubbed - i.e. we have information
+ # about the gem from a stub line, without having to evaluate the
+ # entire gemspec file.
+ def stubbed?
+ raise NotImplementedError
+ end
+
+ def this
+ self
+ end
+
+ private
+
+ 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, file)
+ suffixes.any? {|suf| File.file? base + suf }
+ end
+
+ if have_extensions?
+ base = File.join extension_dir, file
+ suffixes.any? {|suf| File.file? base + suf }
+ else
+ false
+ end
+ end
+end
diff --git a/lib/rubygems/bundler_version_finder.rb b/lib/rubygems/bundler_version_finder.rb
new file mode 100644
index 0000000000..bbe7bf0ab5
--- /dev/null
+++ b/lib/rubygems/bundler_version_finder.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+module Gem::BundlerVersionFinder
+ def self.bundler_version
+ bcv = bundle_config_version
+ return if bcv == "system"
+
+ v = ENV["BUNDLER_VERSION"]
+ v = nil if v&.empty?
+
+ v ||= bundle_update_bundler_version
+ return if v == true
+
+ v ||= bcv unless bcv == "lockfile"
+
+ v ||= lockfile_version
+ return unless v
+
+ Gem::Version.new(v)
+ end
+
+ def self.prioritize!(specs)
+ exact_match_index = specs.find_index {|spec| spec.version == bundler_version }
+ return unless exact_match_index
+
+ specs.unshift(specs.delete_at(exact_match_index))
+ end
+
+ def self.bundle_update_bundler_version
+ return unless ["bundle", "bundler"].include? File.basename($0)
+ return unless "update".start_with?(ARGV.first || " ")
+ bundler_version = nil
+ update_index = nil
+ ARGV.each_with_index do |a, i|
+ if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
+ bundler_version = a
+ end
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
+ bundler_version = $1 || true
+ update_index = i
+ end
+ bundler_version
+ end
+ private_class_method :bundle_update_bundler_version
+
+ def self.lockfile_version
+ return unless contents = lockfile_contents
+ regexp = /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+ return unless contents =~ regexp
+ $1
+ end
+ private_class_method :lockfile_version
+
+ def self.lockfile_contents
+ gemfile = gemfile_path
+
+ return unless gemfile
+
+ lockfile = ENV["BUNDLE_LOCKFILE"]
+ lockfile = nil if lockfile&.empty?
+
+ lockfile ||= case gemfile
+ when "gems.rb" then "gems.locked"
+ else "#{gemfile}.lock"
+ end
+
+ return unless File.file?(lockfile)
+
+ File.read(lockfile)
+ end
+ private_class_method :lockfile_contents
+
+ def self.bundle_config_version
+ env_version = ENV["BUNDLE_VERSION"]
+ return env_version if env_version && !env_version.empty?
+
+ version = nil
+
+ [bundler_local_config_file, bundler_global_config_file].each do |config_file|
+ next unless config_file && File.file?(config_file)
+
+ contents = File.read(config_file)
+ contents =~ /^BUNDLE_VERSION:\s*["']?([^"'\s]+)["']?\s*$/
+
+ version = $1
+ break if version
+ end
+
+ version
+ end
+ private_class_method :bundle_config_version
+
+ def self.bundler_global_config_file
+ # see Bundler::Settings#global_config_file
+ if ENV["BUNDLE_CONFIG"] && !ENV["BUNDLE_CONFIG"].empty?
+ ENV["BUNDLE_CONFIG"]
+ elsif ENV["BUNDLE_USER_CONFIG"] && !ENV["BUNDLE_USER_CONFIG"].empty?
+ ENV["BUNDLE_USER_CONFIG"]
+ elsif ENV["BUNDLE_USER_HOME"] && !ENV["BUNDLE_USER_HOME"].empty?
+ ENV["BUNDLE_USER_HOME"] + "config"
+ elsif Gem.user_home && !Gem.user_home.empty?
+ Gem.user_home + ".bundle/config"
+ end
+ end
+ private_class_method :bundler_global_config_file
+
+ def self.bundler_local_config_file
+ gemfile = gemfile_path
+ return unless gemfile
+
+ File.join(File.dirname(gemfile), ".bundle", "config")
+ end
+ private_class_method :bundler_local_config_file
+
+ def self.gemfile_path
+ gemfile = ENV["BUNDLE_GEMFILE"]
+ gemfile = nil if gemfile&.empty?
+
+ unless gemfile
+ begin
+ Gem::Util.traverse_parents(Dir.pwd) do |directory|
+ next unless gemfile = Gem::GEM_DEP_FILES.find {|f| File.file?(f) }
+
+ gemfile = File.join directory, gemfile
+ break
+ end
+ rescue Errno::ENOENT
+ return
+ end
+ end
+
+ gemfile
+ end
+ private_class_method :gemfile_path
+end
diff --git a/lib/rubygems/ci_detector.rb b/lib/rubygems/ci_detector.rb
new file mode 100644
index 0000000000..7a2d4ee29a
--- /dev/null
+++ b/lib/rubygems/ci_detector.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gem
+ module CIDetector
+ # NOTE: Any changes made here will need to be made to both lib/rubygems/ci_detector.rb and
+ # bundler/lib/bundler/ci_detector.rb (which are enforced duplicates).
+ # TODO: Drop that duplication once bundler drops support for RubyGems 3.4
+ #
+ # ## Recognized CI providers, their signifiers, and the relevant docs ##
+ #
+ # Travis CI - CI, TRAVIS https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
+ # Cirrus CI - CI, CIRRUS_CI https://cirrus-ci.org/guide/writing-tasks/#environment-variables
+ # Circle CI - CI, CIRCLECI https://circleci.com/docs/variables/#built-in-environment-variables
+ # Gitlab CI - CI, GITLAB_CI https://docs.gitlab.com/ee/ci/variables/
+ # AppVeyor - CI, APPVEYOR https://www.appveyor.com/docs/environment-variables/
+ # CodeShip - CI_NAME https://docs.cloudbees.com/docs/cloudbees-codeship/latest/pro-builds-and-configuration/environment-variables#_default_environment_variables
+ # dsari - CI, DSARI https://github.com/rfinnie/dsari#running
+ # Jenkins - BUILD_NUMBER https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables
+ # TeamCity - TEAMCITY_VERSION https://www.jetbrains.com/help/teamcity/predefined-build-parameters.html#Predefined+Server+Build+Parameters
+ # Appflow - CI_BUILD_ID https://ionic.io/docs/appflow/automation/environments#predefined-environments
+ # TaskCluster - TASKCLUSTER_ROOT_URL https://docs.taskcluster.net/docs/manual/design/env-vars
+ # Semaphore - CI, SEMAPHORE https://docs.semaphoreci.com/ci-cd-environment/environment-variables/
+ # BuildKite - CI, BUILDKITE https://buildkite.com/docs/pipelines/environment-variables
+ # GoCD - GO_SERVER_URL https://docs.gocd.org/current/faq/dev_use_current_revision_in_build.html
+ # GH Actions - CI, GITHUB_ACTIONS https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
+ #
+ # ### Some "standard" ENVs that multiple providers may set ###
+ #
+ # * CI - this is set by _most_ (but not all) CI providers now; it's approaching a standard.
+ # * CI_NAME - Not as frequently used, but some providers set this to specify their own name
+
+ # Any of these being set is a reasonably reliable indicator that we are
+ # executing in a CI environment.
+ ENV_INDICATORS = [
+ "CI",
+ "CI_NAME",
+ "CONTINUOUS_INTEGRATION",
+ "BUILD_NUMBER",
+ "CI_APP_ID",
+ "CI_BUILD_ID",
+ "CI_BUILD_NUMBER",
+ "RUN_ID",
+ "TASKCLUSTER_ROOT_URL",
+ ].freeze
+
+ # For each CI, this env suffices to indicate that we're on _that_ CI's
+ # containers. (A few of them only supply a CI_NAME variable, which is also
+ # nice). And if they set "CI" but we can't tell which one they are, we also
+ # want to know that - a bare "ci" without another token tells us as much.
+ ENV_DESCRIPTORS = {
+ "TRAVIS" => "travis",
+ "CIRCLECI" => "circle",
+ "CIRRUS_CI" => "cirrus",
+ "DSARI" => "dsari",
+ "SEMAPHORE" => "semaphore",
+ "JENKINS_URL" => "jenkins",
+ "BUILDKITE" => "buildkite",
+ "GO_SERVER_URL" => "go",
+ "GITLAB_CI" => "gitlab",
+ "GITHUB_ACTIONS" => "github",
+ "TASKCLUSTER_ROOT_URL" => "taskcluster",
+ "CI" => "ci",
+ }.freeze
+
+ def self.ci?
+ ENV_INDICATORS.any? {|var| ENV.include?(var) }
+ end
+
+ def self.ci_strings
+ matching_names = ENV_DESCRIPTORS.select {|env, _| ENV[env] }.values
+ matching_names << ENV["CI_NAME"].downcase if ENV["CI_NAME"]
+ matching_names.reject(&:empty?).sort.uniq
+ end
+ end
+end
diff --git a/lib/rubygems/command.rb b/lib/rubygems/command.rb
new file mode 100644
index 0000000000..d38363f293
--- /dev/null
+++ b/lib/rubygems/command.rb
@@ -0,0 +1,664 @@
+# 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 "vendored_optparse"
+require_relative "requirement"
+require_relative "user_interaction"
+
+##
+# Base class for all Gem commands. When creating a new gem command, define
+# #initialize, #execute, #arguments, #defaults_str, #description and #usage
+# (as appropriate). See the above mentioned methods for details.
+#
+# A very good example to look at is Gem::Commands::ContentsCommand
+
+class Gem::Command
+ include Gem::UserInteraction
+
+ Gem::OptionParser.accept Symbol, &:to_sym
+
+ ##
+ # The name of the command.
+
+ attr_reader :command
+
+ ##
+ # The options for the command.
+
+ attr_reader :options
+
+ ##
+ # The default options for the command.
+
+ attr_accessor :defaults
+
+ ##
+ # The name of the command for command-line invocation.
+
+ attr_accessor :program_name
+
+ ##
+ # A short description of the command.
+
+ attr_accessor :summary
+
+ ##
+ # Arguments used when building gems
+
+ def self.build_args
+ @build_args ||= []
+ end
+
+ def self.build_args=(value)
+ @build_args = value
+ end
+
+ def self.common_options
+ @common_options ||= []
+ end
+
+ def self.add_common_option(*args, &handler)
+ Gem::Command.common_options << [args, handler]
+ end
+
+ def self.extra_args
+ @extra_args ||= []
+ end
+
+ def self.extra_args=(value)
+ case value
+ when Array
+ @extra_args = value
+ when String
+ @extra_args = value.split(" ")
+ end
+ end
+
+ ##
+ # Return an array of extra arguments for the command. The extra arguments
+ # come from the gem configuration file read at program startup.
+
+ def self.specific_extra_args(cmd)
+ specific_extra_args_hash[cmd]
+ end
+
+ ##
+ # Add a list of extra arguments for the given command. +args+ may be an
+ # array or a string to be split on white space.
+
+ def self.add_specific_extra_args(cmd,args)
+ args = args.split(/\s+/) if args.is_a? String
+ specific_extra_args_hash[cmd] = args
+ end
+
+ ##
+ # Accessor for the specific extra args hash (self initializing).
+
+ def self.specific_extra_args_hash
+ @specific_extra_args_hash ||= Hash.new do |h,k|
+ h[k] = Array.new
+ end
+ end
+
+ ##
+ # Initializes a generic gem command named +command+. +summary+ is a short
+ # description displayed in `gem help commands`. +defaults+ are the default
+ # options. Defaults should be mirrored in #defaults_str, unless there are
+ # none.
+ #
+ # When defining a new command subclass, use add_option to add command-line
+ # switches.
+ #
+ # Unhandled arguments (gem names, files, etc.) are left in
+ # <tt>options[:args]</tt>.
+
+ def initialize(command, summary = nil, defaults = {})
+ @command = command
+ @summary = summary
+ @program_name = "gem #{command}"
+ @defaults = defaults
+ @options = defaults.dup
+ @option_groups = Hash.new {|h,k| h[k] = [] }
+ @deprecated_options = { command => {} }
+ @parser = nil
+ @when_invoked = nil
+ end
+
+ ##
+ # True if +long+ begins with the characters from +short+.
+
+ def begins?(long, short)
+ return false if short.nil?
+ long[0, short.length] == short
+ end
+
+ ##
+ # Override to provide command handling.
+ #
+ # #options will be filled in with your parsed options, unparsed options will
+ # be left in <tt>options[:args]</tt>.
+ #
+ # See also: #get_all_gem_names, #get_one_gem_name,
+ # #get_one_optional_argument
+
+ def execute
+ raise Gem::Exception, "generic command has no actions"
+ end
+
+ ##
+ # Display to the user that a gem couldn't be found and reasons why
+ #--
+
+ def show_lookup_failure(gem_name, version, errors, suppress_suggestions = false, required_by = nil)
+ gem = "'#{gem_name}' (#{version})"
+ msg = String.new "Could not find a valid gem #{gem}"
+
+ if errors && !errors.empty?
+ msg << ", here is why:\n"
+ errors.each {|x| msg << " #{x.wordy}\n" }
+ else
+ if required_by && gem != required_by
+ msg << " (required by #{required_by}) in any repository"
+ else
+ msg << " in any repository"
+ end
+ end
+
+ alert_error msg
+
+ unless suppress_suggestions
+ suggestions = Gem::SpecFetcher.fetcher.suggest_gems_from_name(gem_name, :latest, 10)
+ unless suggestions.empty?
+ alert_error "Possible alternatives: #{suggestions.join(", ")}"
+ end
+ end
+ end
+
+ ##
+ # Get all gem names from the command line.
+
+ def get_all_gem_names
+ args = options[:args]
+
+ if args.nil? || args.empty?
+ raise Gem::CommandLineError,
+ "Please specify at least one gem name (e.g. gem build GEMNAME)"
+ end
+
+ args.reject {|arg| arg.start_with?("-") }
+ end
+
+ ##
+ # Get all [gem, version] from the command line.
+ #
+ # An argument in the form gem:ver is pull apart into the gen name and version,
+ # respectively.
+ def get_all_gem_names_and_versions
+ get_all_gem_names.map do |name|
+ 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
+
+ ##
+ # Get a single gem name from the command line. Fail if there is no gem name
+ # or if there is more than one gem name given.
+
+ def get_one_gem_name
+ args = options[:args]
+
+ if args.nil? || args.empty?
+ raise Gem::CommandLineError,
+ "Please specify a gem name on the command line (e.g. gem build GEMNAME)"
+ end
+
+ if args.size > 1
+ raise Gem::CommandLineError,
+ "Too many gem names (#{args.join(", ")}); please specify only one"
+ end
+
+ args.first
+ end
+
+ ##
+ # Get a single optional argument from the command line. If more than one
+ # argument is given, return only the first. Return nil if none are given.
+
+ def get_one_optional_argument
+ args = options[:args] || []
+ args.first
+ end
+
+ ##
+ # Override to provide details of the arguments a command takes. It should
+ # return a left-justified string, one argument per line.
+ #
+ # For example:
+ #
+ # def usage
+ # "#{program_name} FILE [FILE ...]"
+ # end
+ #
+ # def arguments
+ # "FILE name of file to find"
+ # end
+
+ def arguments
+ ""
+ end
+
+ ##
+ # Override to display the default values of the command options. (similar to
+ # +arguments+, but displays the default values).
+ #
+ # For example:
+ #
+ # def defaults_str
+ # --no-gems-first --no-all
+ # end
+
+ def defaults_str
+ ""
+ end
+
+ ##
+ # Override to display a longer description of what this command does.
+
+ def description
+ nil
+ end
+
+ ##
+ # Override to display the usage for an individual gem command.
+ #
+ # The text "[options]" is automatically appended to the usage text.
+
+ def usage
+ program_name
+ end
+
+ ##
+ # Display the help message for the command.
+
+ def show_help
+ parser.program_name = usage
+ say parser
+ end
+
+ ##
+ # Invoke the command with the given list of arguments.
+
+ def invoke(*args)
+ invoke_with_build_args args, nil
+ end
+
+ ##
+ # Invoke the command with the given list of normal arguments
+ # and additional build arguments.
+
+ def invoke_with_build_args(args, build_args)
+ handle_options args
+
+ options[:build_args] = build_args
+
+ if options[:silent]
+ old_ui = ui
+ self.ui = ui = Gem::SilentUI.new
+ end
+
+ if options[:help]
+ show_help
+ elsif @when_invoked
+ @when_invoked.call options
+ else
+ execute
+ end
+ ensure
+ if ui
+ self.ui = old_ui
+ ui.close
+ end
+ end
+
+ ##
+ # Call the given block when invoked.
+ #
+ # Normal command invocations just executes the +execute+ method of the
+ # command. Specifying an invocation block allows the test methods to
+ # override the normal action of a command to determine that it has been
+ # invoked correctly.
+
+ def when_invoked(&block)
+ @when_invoked = block
+ end
+
+ ##
+ # Add a command-line option and handler to the command.
+ #
+ # See Gem::OptionParser#make_switch for an explanation of +opts+.
+ #
+ # +handler+ will be called with two values, the value of the argument and
+ # the options hash.
+ #
+ # If the first argument of add_option is a Symbol, it's used to group
+ # options in output. See `gem help list` for an example.
+
+ def add_option(*opts, &handler) # :yields: value, options
+ group_name = Symbol === opts.first ? opts.shift : :options
+
+ raise "Do not pass an empty string in opts" if opts.include?("")
+
+ @option_groups[group_name] << [opts, handler]
+ end
+
+ ##
+ # Remove previously defined command-line argument +name+.
+
+ def remove_option(name)
+ @option_groups.each do |_, option_list|
+ option_list.reject! {|args, _| args.any? {|x| x.is_a?(String) && x =~ /^#{name}/ } }
+ end
+ end
+
+ ##
+ # Mark a command-line option as deprecated, and optionally specify a
+ # deprecation horizon.
+ #
+ # Note that with the current implementation, every version of the option needs
+ # to be explicitly deprecated, so to deprecate an option defined as
+ #
+ # add_option('-t', '--[no-]test', 'Set test mode') do |value, options|
+ # # ... stuff ...
+ # end
+ #
+ # you would need to explicitly add a call to `deprecate_option` for every
+ # version of the option you want to deprecate, like
+ #
+ # deprecate_option('-t')
+ # deprecate_option('--test')
+ # deprecate_option('--no-test')
+
+ def deprecate_option(name, version: nil, extra_msg: nil)
+ @deprecated_options[command].merge!({ name => { "rg_version_to_expire" => version, "extra_msg" => extra_msg } })
+ end
+
+ def check_deprecated_options(options)
+ options.each do |option|
+ 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
+
+ extra_msg = deprecation["extra_msg"]
+
+ deprecate_option_msg += " #{extra_msg}" if extra_msg
+
+ alert_warning(deprecate_option_msg)
+ end
+ end
+
+ ##
+ # Merge a set of command options with the set of default options (without
+ # modifying the default option hash).
+
+ def merge_options(new_options)
+ @options = @defaults.clone
+ new_options.each {|k,v| @options[k] = v }
+ end
+
+ ##
+ # True if the command handles the given argument list.
+
+ def handles?(args)
+ parser.parse!(args.dup)
+ true
+ rescue StandardError
+ false
+ end
+
+ ##
+ # Handle the given list of arguments by parsing them and recording the
+ # results.
+
+ def handle_options(args)
+ args = add_extra_args(args)
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
+ parser.parse!(args)
+ @options[:args] = args
+ end
+
+ ##
+ # Adds extra args from ~/.gemrc
+
+ def add_extra_args(args)
+ result = []
+
+ s_extra = Gem::Command.specific_extra_args(@command)
+ extra = Gem::Command.extra_args + s_extra
+
+ until extra.empty? do
+ ex = []
+ ex << extra.shift
+ ex << extra.shift if /^[^-]/.match?(extra.first.to_s)
+ result << ex if handles?(ex)
+ end
+
+ result.flatten!
+ result.concat(args)
+ result
+ end
+
+ def deprecated?
+ false
+ end
+
+ private
+
+ def option_is_deprecated?(option)
+ @deprecated_options[command].key?(option)
+ end
+
+ def add_parser_description # :nodoc:
+ return unless description
+
+ formatted = description.split("\n\n").map do |chunk|
+ wrap chunk, 80 - 4
+ end.join "\n"
+
+ @parser.separator nil
+ @parser.separator " Description:"
+ formatted.each_line do |line|
+ @parser.separator " #{line.rstrip}"
+ end
+ end
+
+ def add_parser_options # :nodoc:
+ @parser.separator nil
+
+ regular_options = @option_groups.delete :options
+
+ configure_options "", regular_options
+
+ @option_groups.sort_by {|n,_| n.to_s }.each do |group_name, option_list|
+ @parser.separator nil
+ configure_options group_name, option_list
+ end
+ end
+
+ ##
+ # Adds a section with +title+ and +content+ to the parser help view. Used
+ # for adding command arguments and default arguments.
+
+ def add_parser_run_info(title, content)
+ return if content.empty?
+
+ @parser.separator nil
+ @parser.separator " #{title}:"
+ content.each_line do |line|
+ @parser.separator " #{line.rstrip}"
+ end
+ end
+
+ def add_parser_summary # :nodoc:
+ return unless @summary
+
+ @parser.separator nil
+ @parser.separator " Summary:"
+ wrap(@summary, 80 - 4).each_line do |line|
+ @parser.separator " #{line.strip}"
+ end
+ end
+
+ ##
+ # Create on demand parser.
+
+ def parser
+ create_option_parser if @parser.nil?
+ @parser
+ end
+
+ ##
+ # Creates an option parser and fills it in with the help info for the
+ # command.
+
+ def create_option_parser
+ @parser = Gem::OptionParser.new
+
+ add_parser_options
+
+ @parser.separator nil
+ configure_options "Common", Gem::Command.common_options
+
+ add_parser_run_info "Arguments", arguments
+ add_parser_summary
+ add_parser_description
+ add_parser_run_info "Defaults", defaults_str
+ end
+
+ def configure_options(header, option_list)
+ return if option_list.nil? || option_list.empty?
+
+ header = header.to_s.empty? ? "" : "#{header} "
+ @parser.separator " #{header}Options:"
+
+ option_list.each do |args, handler|
+ @parser.on(*args) do |value|
+ handler.call(value, @options)
+ end
+ end
+
+ @parser.separator ""
+ end
+
+ ##
+ # Wraps +text+ to +width+
+
+ def wrap(text, width) # :doc:
+ text.gsub(/(.{1,#{width}})( +|$\n?)|(.{1,#{width}})/, "\\1\\3\n")
+ end
+
+ # ----------------------------------------------------------------
+ # Add the options common to all commands.
+
+ add_common_option("-h", "--help",
+ "Get help on this command") do |_value, options|
+ options[:help] = true
+ end
+
+ add_common_option("-V", "--[no-]verbose",
+ "Set the verbose level of output") do |value, _options|
+ # Set us to "really verbose" so the progress meter works
+ if Gem.configuration.verbose && value
+ Gem.configuration.verbose = 1
+ else
+ Gem.configuration.verbose = value
+ end
+ end
+
+ add_common_option("-q", "--quiet", "Silence command progress meter") do |_value, _options|
+ Gem.configuration.verbose = false
+ end
+
+ add_common_option("--silent",
+ "Silence RubyGems output") do |_value, options|
+ options[:silent] = true
+ end
+
+ # Backtrace and config-file are added so they show up in the help
+ # commands. Both options are actually handled before the other
+ # options get parsed.
+
+ add_common_option("--config-file FILE",
+ "Use this config file instead of default") do
+ end
+
+ add_common_option("--backtrace",
+ "Show stack backtrace on errors") do
+ end
+
+ add_common_option("--debug",
+ "Turn on Ruby debugging") do
+ end
+
+ add_common_option("--norc",
+ "Avoid loading any .gemrc file") do
+ end
+
+ # :stopdoc:
+
+ HELP = <<-HELP
+RubyGems is a package manager for Ruby.
+
+ Usage:
+ gem -h/--help
+ gem -v/--version
+ gem [global options...] command [arguments...] [options...]
+
+ Global options:
+ -C PATH run as if gem was started in <PATH>
+ instead of the current working directory
+
+ Examples:
+ gem install rake
+ gem list --local
+ gem build package.gemspec
+ gem push package-0.0.1.gem
+ gem help install
+
+ Further help:
+ gem help commands list all 'gem' commands
+ gem help examples show some examples of usage
+ gem help gem_dependencies gem dependencies file guide
+ gem help platforms gem platforms guide
+ gem help <COMMAND> show help on COMMAND
+ (e.g. 'gem help install')
+ Further information:
+ https://guides.rubygems.org
+ HELP
+
+ # :startdoc:
+end
+
+##
+# \Commands will be placed in this namespace
+
+module Gem::Commands
+end
diff --git a/lib/rubygems/command_manager.rb b/lib/rubygems/command_manager.rb
new file mode 100644
index 0000000000..76b2fba835
--- /dev/null
+++ b/lib/rubygems/command_manager.rb
@@ -0,0 +1,254 @@
+# 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 "command"
+require_relative "user_interaction"
+require_relative "text"
+
+##
+# The command manager registers and installs all the individual sub-commands
+# supported by the gem command.
+#
+# Extra commands can be provided by writing a rubygems_plugin.rb
+# file in an installed gem. You should register your command against the
+# Gem::CommandManager instance, like this:
+#
+# # file rubygems_plugin.rb
+# require 'rubygems/command_manager'
+#
+# Gem::CommandManager.instance.register_command :edit
+#
+# You should put the implementation of your command in rubygems/commands.
+#
+# # file rubygems/commands/edit_command.rb
+# class Gem::Commands::EditCommand < Gem::Command
+# # ...
+# end
+#
+# See Gem::Command for instructions on writing gem commands.
+
+class Gem::CommandManager
+ include Gem::Text
+ include Gem::UserInteraction
+
+ BUILTIN_COMMANDS = [ # :nodoc:
+ :build,
+ :cert,
+ :check,
+ :cleanup,
+ :contents,
+ :dependency,
+ :environment,
+ :exec,
+ :fetch,
+ :generate_index,
+ :help,
+ :info,
+ :install,
+ :list,
+ :lock,
+ :mirror,
+ :open,
+ :outdated,
+ :owner,
+ :pristine,
+ :push,
+ :rdoc,
+ :rebuild,
+ :search,
+ :server,
+ :signin,
+ :signout,
+ :sources,
+ :specification,
+ :stale,
+ :uninstall,
+ :unpack,
+ :update,
+ :which,
+ :yank,
+ ].freeze
+
+ ALIAS_COMMANDS = {
+ "i" => "install",
+ "login" => "signin",
+ "logout" => "signout",
+ }.freeze
+
+ ##
+ # Return the authoritative instance of the command manager.
+
+ def self.instance
+ @instance ||= new
+ end
+
+ ##
+ # Returns self. Allows a CommandManager instance to stand
+ # in for the class itself.
+
+ def instance
+ self
+ end
+
+ ##
+ # Reset the authoritative instance of the command manager.
+
+ def self.reset
+ @instance = nil
+ end
+
+ ##
+ # Register all the subcommands supported by the gem command.
+
+ def initialize
+ require_relative "vendored_timeout"
+ @commands = {}
+
+ BUILTIN_COMMANDS.each do |name|
+ register_command name
+ end
+ end
+
+ ##
+ # Register the Symbol +command+ as a gem command.
+
+ def register_command(command, obj = false)
+ @commands[command] = obj
+ end
+
+ ##
+ # Unregister the Symbol +command+ as a gem command.
+
+ def unregister_command(command)
+ @commands.delete command
+ end
+
+ ##
+ # Returns a Command instance for +command_name+
+
+ def [](command_name)
+ command_name = command_name.intern
+ return nil if @commands[command_name].nil?
+ @commands[command_name] ||= load_and_instantiate(command_name)
+ end
+
+ ##
+ # Return a sorted list of all command names as strings.
+
+ def command_names
+ @commands.keys.collect(&:to_s).sort
+ end
+
+ ##
+ # Run the command specified by +args+.
+
+ def run(args, build_args = nil)
+ process_args(args, build_args)
+ rescue StandardError, Gem::Timeout::Error => ex
+ if ex.respond_to?(:detailed_message)
+ msg = ex.detailed_message(highlight: false).sub(/\A(.*?)(?: \(.+?\))/) { $1 }
+ else
+ msg = ex.message
+ end
+ alert_error clean_text("While executing gem ... (#{ex.class})\n #{msg}")
+ ui.backtrace ex
+
+ terminate_interaction(1)
+ rescue Interrupt
+ alert_error clean_text("Interrupted")
+ terminate_interaction(1)
+ end
+
+ def process_args(args, build_args = nil)
+ if args.empty?
+ say Gem::Command::HELP
+ terminate_interaction 1
+ end
+
+ case args.first
+ when "-h", "--help" then
+ say Gem::Command::HELP
+ terminate_interaction 0
+ when "-v", "--version" then
+ say Gem::VERSION
+ terminate_interaction 0
+ when "-C" then
+ args.shift
+ start_point = args.shift
+ if Dir.exist?(start_point)
+ Dir.chdir(start_point) { invoke_command(args, build_args) }
+ else
+ alert_error clean_text("#{start_point} isn't a directory.")
+ terminate_interaction 1
+ end
+ when /^-/ then
+ alert_error clean_text("Invalid option: #{args.first}. See 'gem --help'.")
+ terminate_interaction 1
+ else
+ invoke_command(args, build_args)
+ end
+ end
+
+ def find_command(cmd_name)
+ cmd_name = find_alias_command cmd_name
+
+ possibilities = find_command_possibilities cmd_name
+
+ if possibilities.size > 1
+ raise Gem::CommandLineError,
+ "Ambiguous command #{cmd_name} matches [#{possibilities.join(", ")}]"
+ elsif possibilities.empty?
+ raise Gem::UnknownCommandError.new(cmd_name)
+ end
+
+ self[possibilities.first]
+ end
+
+ def find_alias_command(cmd_name)
+ alias_name = ALIAS_COMMANDS[cmd_name]
+ alias_name ? alias_name : cmd_name
+ end
+
+ def find_command_possibilities(cmd_name)
+ len = cmd_name.length
+
+ found = command_names.select {|name| cmd_name == name[0, len] }
+
+ exact = found.find {|name| name == cmd_name }
+
+ exact ? [exact] : found
+ end
+
+ private
+
+ def load_and_instantiate(command_name)
+ command_name = command_name.to_s
+ const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } << "Command"
+
+ begin
+ begin
+ require "rubygems/commands/#{command_name}_command"
+ rescue LoadError
+ # it may have been defined from a rubygems_plugin.rb file
+ end
+
+ Gem::Commands.const_get(const_name).new
+ rescue StandardError => e
+ alert_error clean_text("Loading command: #{command_name} (#{e.class})\n\t#{e}")
+ ui.backtrace e
+ end
+ end
+
+ def invoke_command(args, build_args)
+ cmd_name = args.shift.downcase
+ cmd = find_command cmd_name
+ terminate_interaction 1 unless cmd
+ cmd.deprecation_warning if cmd.deprecated?
+ cmd.invoke_with_build_args args, build_args
+ end
+end
diff --git a/lib/rubygems/commands/build_command.rb b/lib/rubygems/commands/build_command.rb
new file mode 100644
index 0000000000..cfe1f8ec3c
--- /dev/null
+++ b/lib/rubygems/commands/build_command.rb
@@ -0,0 +1,120 @@
+# 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|
+ options[:force] = true
+ end
+
+ add_option "--strict", "consider warnings as errors when validating the spec" do |_value, options|
+ options[:strict] = true
+ end
+
+ add_option "-o", "--output FILE", "output gem with the given filename" do |value, options|
+ options[:output] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMSPEC_FILE gemspec file name to build a gem for"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The build command allows you to create a gem from a ruby gemspec.
+
+The best way to build a gem is to use a Rakefile and the Gem::PackageTask
+which ships with RubyGems.
+
+The gemspec can either be created by hand or extracted from an existing gem
+with gem spec:
+
+ $ gem unpack my_gem-1.0.gem
+ Unpacked gem: '.../my_gem-1.0'
+ $ gem spec my_gem-1.0.gem --ruby > my_gem-1.0/my_gem-1.0.gemspec
+ $ cd my_gem-1.0
+ [edit gem contents]
+ $ gem build my_gem-1.0.gemspec
+
+Gems can be saved to a specified filename with the output option:
+
+ $ gem build my_gem-1.0.gemspec --output=release.gem
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMSPEC_FILE"
+ end
+
+ def execute
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem }
+ return
+ end
+
+ build_gem
+ end
+
+ private
+
+ def build_gem
+ gemspec = resolve_gem_name
+
+ if gemspec
+ build_package(gemspec)
+ else
+ alert_error error_message
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec)
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ options[:output]
+ )
+ else
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+
+ def resolve_gem_name
+ return find_gemspec unless gem_name
+
+ if File.exist?(gem_name)
+ gem_name
+ else
+ find_gemspec("#{gem_name}.gemspec") || find_gemspec(gem_name)
+ end
+ end
+
+ def error_message
+ 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 gem_name
+ get_one_optional_argument
+ end
+end
diff --git a/lib/rubygems/commands/cert_command.rb b/lib/rubygems/commands/cert_command.rb
new file mode 100644
index 0000000000..fe03841ddb
--- /dev/null
+++ b/lib/rubygems/commands/cert_command.rb
@@ -0,0 +1,325 @@
+# 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_option("-a", "--add CERT",
+ "Add a trusted certificate.") do |cert_file, options|
+ options[:add] << open_cert(cert_file)
+ end
+
+ add_option("-l", "--list [FILTER]",
+ "List trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ filter ||= ""
+
+ options[:list] << filter
+ end
+
+ add_option("-r", "--remove FILTER",
+ "Remove trusted certificates where the",
+ "subject contains FILTER") do |filter, options|
+ options[:remove] << filter
+ end
+
+ add_option("-b", "--build EMAIL_ADDR",
+ "Build private key and self-signed",
+ "certificate for EMAIL_ADDR") do |email_address, options|
+ options[:build] << email_address
+ end
+
+ add_option("-C", "--certificate CERT",
+ "Signing certificate for --sign") do |cert_file, options|
+ options[:issuer_cert] = open_cert(cert_file)
+ options[:issuer_cert_file] = cert_file
+ end
+
+ add_option("-K", "--private-key KEY",
+ "Key for --sign or --build") do |key_file, options|
+ options[:key] = open_private_key(key_file)
+ end
+
+ add_option("-A", "--key-algorithm ALGORITHM",
+ "Select which key algorithm to use for --build") do |algorithm, options|
+ options[:key_algorithm] = algorithm
+ end
+
+ add_option("-s", "--sign CERT",
+ "Signs CERT with the key from -K",
+ "and the certificate from -C") do |cert_file, options|
+ raise Gem::OptionParser::InvalidArgument, "#{cert_file}: does not exist" unless
+ File.file? cert_file
+
+ options[:sign] << cert_file
+ end
+
+ add_option("-d", "--days NUMBER_OF_DAYS",
+ "Days before the certificate expires") do |days, options|
+ options[:expiration_length_days] = days.to_i
+ end
+
+ add_option("-R", "--re-sign",
+ "Re-signs the certificate from -C with the key from -K") do |resign, options|
+ options[:resign] = resign
+ end
+ end
+
+ def add_certificate(certificate) # :nodoc:
+ Gem::Security.trust_dir.trust_cert certificate
+
+ say "Added '#{certificate.subject}'"
+ end
+
+ def check_openssl
+ return if Gem::HAVE_OPENSSL
+
+ alert_error "OpenSSL library is required for the cert command"
+ terminate_interaction 1
+ end
+
+ def open_cert(certificate_file)
+ check_openssl
+ OpenSSL::X509::Certificate.new File.read certificate_file
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{certificate_file}: does not exist"
+ rescue OpenSSL::X509::CertificateError
+ raise Gem::OptionParser::InvalidArgument,
+ "#{certificate_file}: invalid X509 certificate"
+ end
+
+ def open_private_key(key_file)
+ check_openssl
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ key = OpenSSL::PKey.read File.read(key_file), passphrase
+ raise Gem::OptionParser::InvalidArgument,
+ "#{key_file}: private key not found" unless key.private?
+ key
+ rescue Errno::ENOENT
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: does not exist"
+ rescue OpenSSL::PKey::PKeyError, ArgumentError
+ raise Gem::OptionParser::InvalidArgument, "#{key_file}: invalid RSA, DSA, or EC key"
+ end
+
+ def execute
+ check_openssl
+
+ options[:add].each do |certificate|
+ add_certificate certificate
+ end
+
+ options[:remove].each do |filter|
+ remove_certificates_matching filter
+ end
+
+ options[:list].each do |filter|
+ list_certificates_matching filter
+ end
+
+ options[:build].each do |email|
+ build email
+ end
+
+ if options[:resign]
+ re_sign_cert(
+ options[:issuer_cert],
+ options[:issuer_cert_file],
+ options[:key]
+ )
+ end
+
+ sign_certificates unless options[:sign].empty?
+ end
+
+ def build(email)
+ unless valid_email?(email)
+ raise Gem::CommandLineError, "Invalid email address #{email}"
+ end
+
+ key, key_path = build_key
+ cert_path = build_cert email, key
+
+ say "Certificate: #{cert_path}"
+
+ if key_path
+ say "Private Key: #{key_path}"
+ say "Don't forget to move the key file to somewhere private!"
+ end
+ end
+
+ def build_cert(email, key) # :nodoc:
+ expiration_length_days = options[:expiration_length_days] ||
+ Gem.configuration.cert_expiration_length_days
+
+ cert = Gem::Security.create_cert_email(
+ email,
+ key,
+ Gem::Security::ONE_DAY * expiration_length_days
+ )
+
+ Gem::Security.write cert, "gem-public_cert.pem"
+ end
+
+ def build_key # :nodoc:
+ return options[:key] if options[:key]
+
+ passphrase = ask_for_password "Passphrase for your Private Key:"
+ say "\n"
+
+ passphrase_confirmation = ask_for_password "Please repeat the passphrase for your Private Key:"
+ say "\n"
+
+ raise Gem::CommandLineError,
+ "Passphrase and passphrase confirmation don't match" unless passphrase == passphrase_confirmation
+
+ algorithm = options[:key_algorithm] || Gem::Security::DEFAULT_KEY_ALGORITHM
+ key = Gem::Security.create_key(algorithm)
+ key_path = Gem::Security.write key, "gem-private_key.pem", 0o600, passphrase
+
+ [key, key_path]
+ end
+
+ def certificates_matching(filter)
+ return enum_for __method__, filter unless block_given?
+
+ Gem::Security.trusted_certificates.select do |certificate, _|
+ subject = certificate.subject.to_s
+ subject.downcase.index filter
+ end.sort_by do |certificate, _|
+ certificate.subject.to_a.map {|name, data,| [name, data] }
+ end.each do |certificate, path|
+ yield certificate, path
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cert command manages signing keys and certificates for creating signed
+gems. Your signing certificate and private key are typically stored in
+~/.gem/gem-public_cert.pem and ~/.gem/gem-private_key.pem respectively.
+
+To build a certificate for signing gems:
+
+ gem cert --build you@example
+
+If you already have an RSA key, or are creating a new certificate for an
+existing key:
+
+ gem cert --build you@example --private-key /path/to/key.pem
+
+If you wish to trust a certificate you can add it to the trust list with:
+
+ gem cert --add /path/to/cert.pem
+
+You can list trusted certificates with:
+
+ gem cert --list
+
+or:
+
+ gem cert --list cert_subject_substring
+
+If you wish to remove a previously trusted certificate:
+
+ gem cert --remove cert_subject_substring
+
+To sign another gem author's certificate:
+
+ gem cert --sign /path/to/other_cert.pem
+
+For further reading on signing gems see `ri Gem::Security`.
+ EOF
+ end
+
+ def list_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, _|
+ # this could probably be formatted more gracefully
+ say certificate.subject.to_s
+ end
+ end
+
+ def load_default_cert
+ cert_file = File.join Gem.default_cert_path
+ cert = File.read cert_file
+ options[:issuer_cert] = OpenSSL::X509::Certificate.new cert
+ rescue Errno::ENOENT
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::X509::CertificateError
+ alert_error \
+ "--certificate not specified and ~/.gem/gem-public_cert.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_default_key
+ key_file = File.join Gem.default_key_path
+ key = File.read key_file
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ options[:key] = OpenSSL::PKey.read key, passphrase
+ rescue Errno::ENOENT
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem does not exist"
+
+ terminate_interaction 1
+ rescue OpenSSL::PKey::PKeyError
+ alert_error \
+ "--private-key not specified and ~/.gem/gem-private_key.pem is not valid"
+
+ terminate_interaction 1
+ end
+
+ def load_defaults # :nodoc:
+ load_default_cert unless options[:issuer_cert]
+ load_default_key unless options[:key]
+ end
+
+ def remove_certificates_matching(filter) # :nodoc:
+ certificates_matching filter do |certificate, path|
+ FileUtils.rm path
+ say "Removed '#{certificate.subject}'"
+ end
+ end
+
+ def sign(cert_file)
+ cert = File.read cert_file
+ cert = OpenSSL::X509::Certificate.new cert
+
+ permissions = File.stat(cert_file).mode & 0o777
+
+ issuer_cert = options[:issuer_cert]
+ issuer_key = options[:key]
+
+ cert = Gem::Security.sign cert, issuer_key, issuer_cert
+
+ Gem::Security.write cert, cert_file, permissions
+ end
+
+ def sign_certificates # :nodoc:
+ load_defaults unless options[:sign].empty?
+
+ options[:sign].each do |cert_file|
+ sign cert_file
+ end
+ end
+
+ def re_sign_cert(cert, cert_path, private_key)
+ Gem::Security::Signer.re_sign_cert(cert, cert_path, private_key) do |expired_cert_path, new_expired_cert_path|
+ alert("Your certificate #{expired_cert_path} has been re-signed")
+ alert("Your expired certificate will be located at: #{new_expired_cert_path}")
+ end
+ end
+
+ private
+
+ def valid_email?(email)
+ # It's simple, but is all we need
+ email =~ /\A.+@.+\z/
+ end
+end
diff --git a/lib/rubygems/commands/check_command.rb b/lib/rubygems/commands/check_command.rb
new file mode 100644
index 0000000000..fb23dd9cb4
--- /dev/null
+++ b/lib/rubygems/commands/check_command.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../validator"
+require_relative "../doctor"
+
+class Gem::Commands::CheckCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "check", "Check a gem repository for added or missing files",
+ alien: true, doctor: false, dry_run: false, gems: true
+
+ add_option("-a", "--[no-]alien",
+ 'Report "unmanaged" or rogue files in the',
+ "gem repository") do |value, options|
+ options[:alien] = value
+ end
+
+ add_option("--[no-]doctor",
+ "Clean up uninstalled gems and broken",
+ "specifications") do |value, options|
+ options[:doctor] = value
+ end
+
+ add_option("--[no-]dry-run",
+ "Do not remove files, only report what",
+ "would be removed") do |value, options|
+ options[:dry_run] = value
+ end
+
+ add_option("--[no-]gems",
+ "Check installed gems for problems") do |value, options|
+ options[:gems] = value
+ end
+
+ add_version_option "check"
+ end
+
+ def check_gems
+ say "Checking gems..."
+ say
+ gems = begin
+ get_all_gem_names
+ rescue StandardError
+ []
+ end
+
+ Gem::Validator.new.alien(gems).sort.each do |key, val|
+ 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
+ end
+ say
+ end
+ end
+
+ def doctor
+ say "Checking for files from uninstalled gems..."
+ say
+
+ Gem.path.each do |gem_repo|
+ doctor = Gem::Doctor.new gem_repo, options[:dry_run]
+ doctor.doctor
+ end
+ end
+
+ def execute
+ check_gems if options[:gems]
+ doctor if options[:doctor]
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to check"
+ end
+
+ def defaults_str # :nodoc:
+ "--gems --alien"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The check command can list and repair problems with installed gems and
+specifications and will clean up gems that have been partially uninstalled.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [OPTIONS] [GEMNAME ...]"
+ end
+end
diff --git a/lib/rubygems/commands/cleanup_command.rb b/lib/rubygems/commands/cleanup_command.rb
new file mode 100644
index 0000000000..c89a24eee9
--- /dev/null
+++ b/lib/rubygems/commands/cleanup_command.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../dependency_list"
+require_relative "../uninstaller"
+
+class Gem::Commands::CleanupCommand < Gem::Command
+ def initialize
+ super "cleanup",
+ "Clean up old versions of installed gems",
+ force: false, install_dir: Gem.dir,
+ check_dev: true
+
+ add_option("-n", "-d", "--dry-run",
+ "Do not uninstall gems") do |_value, options|
+ options[:dryrun] = true
+ end
+
+ add_option(:Deprecated, "--dryrun",
+ "Do not uninstall gems") do |_value, options|
+ options[:dryrun] = true
+ end
+ deprecate_option("--dryrun", extra_msg: "Use --dry-run instead")
+
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: true)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("--[no-]user-install",
+ "Cleanup in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ @candidate_gems = nil
+ @default_gems = []
+ @full = nil
+ @gems_to_cleanup = nil
+ @primary_gems = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to cleanup"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-dry-run"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The cleanup command removes old versions of gems from GEM_HOME that are not
+required to meet a dependency. If a gem is installed elsewhere in GEM_PATH
+the cleanup command won't delete it.
+
+If no gems are named all gems in GEM_HOME are cleaned.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEMNAME ...]"
+ end
+
+ def execute
+ say "Cleaning up installed gems..."
+
+ if options[:args].empty?
+ done = false
+ last_set = nil
+
+ until done do
+ clean_gems
+
+ this_set = @gems_to_cleanup.map(&:full_name).sort
+
+ done = this_set.empty? || last_set == this_set
+
+ last_set = this_set
+ end
+ else
+ clean_gems
+ end
+
+ say "Clean up complete"
+
+ verbose do
+ skipped = @default_gems.map(&:full_name)
+
+ "Skipped default gems: #{skipped.join ", "}"
+ end
+ end
+
+ def clean_gems
+ get_primary_gems
+ get_candidate_gems
+ get_gems_to_cleanup
+
+ @full = Gem::DependencyList.from_specs
+
+ deplist = Gem::DependencyList.new
+ @gems_to_cleanup.each {|spec| deplist.add spec }
+
+ deps = deplist.strongly_connected_components.flatten
+
+ deps.reverse_each do |spec|
+ uninstall_dep spec
+ end
+ end
+
+ def get_candidate_gems
+ @candidate_gems = if options[:args].empty?
+ Gem::Specification.to_a
+ else
+ options[:args].flat_map do |gem_name|
+ Gem::Specification.find_all_by_name gem_name
+ end
+ end
+ end
+
+ def get_gems_to_cleanup
+ gems_to_cleanup = @candidate_gems.select do |spec|
+ @primary_gems[spec.name].version != spec.version
+ end
+
+ default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?)
+
+ uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir
+
+ gems_to_cleanup = gems_to_cleanup.select do |spec|
+ spec.base_dir == uninstall_from
+ end
+
+ @default_gems += default_gems
+ @default_gems.uniq!
+ @gems_to_cleanup = gems_to_cleanup.uniq
+ end
+
+ def get_primary_gems
+ @primary_gems = {}
+
+ Gem::Specification.each do |spec|
+ if @primary_gems[spec.name].nil? ||
+ @primary_gems[spec.name].version < spec.version
+ @primary_gems[spec.name] = spec
+ end
+ end
+ end
+
+ def uninstall_dep(spec)
+ return unless @full.ok_to_remove?(spec.full_name, options[:check_dev])
+
+ if options[:dryrun]
+ say "Dry Run Mode: Would uninstall #{spec.full_name}"
+ return
+ end
+
+ say "Attempting to uninstall #{spec.full_name}"
+
+ uninstall_options = {
+ executables: false,
+ version: "= #{spec.version}",
+ }
+
+ uninstall_options[:user_install] = Gem.user_dir == spec.base_dir
+
+ uninstaller = Gem::Uninstaller.new spec.name, uninstall_options
+
+ begin
+ uninstaller.uninstall
+ rescue Gem::DependencyRemovalException, Gem::InstallError,
+ Gem::GemNotInHomeException, Gem::FilePermissionError => e
+ say "Unable to uninstall #{spec.full_name}:"
+ say "\t#{e.class}: #{e.message}"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/contents_command.rb b/lib/rubygems/commands/contents_command.rb
new file mode 100644
index 0000000000..d4f9871868
--- /dev/null
+++ b/lib/rubygems/commands/contents_command.rb
@@ -0,0 +1,196 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+
+class Gem::Commands::ContentsCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "contents", "Display the contents of the installed gems",
+ specdirs: [], lib_only: false, prefix: true,
+ show_install_dir: false
+
+ add_version_option
+
+ add_option("--all",
+ "Contents for all gems") do |all, options|
+ options[:all] = all
+ end
+
+ add_option("-s", "--spec-dir a,b,c", Array,
+ "Search for gems under specific paths") do |spec_dirs, options|
+ options[:specdirs] = spec_dirs
+ end
+
+ add_option("-l", "--[no-]lib-only",
+ "Only return files in the Gem's lib_dirs") do |lib_only, options|
+ options[:lib_only] = lib_only
+ end
+
+ add_option("--[no-]prefix",
+ "Don't include installed path prefix") do |prefix, options|
+ options[:prefix] = prefix
+ end
+
+ add_option("--[no-]show-install-dir",
+ "Show only the gem install dir") do |show, options|
+ options[:show_install_dir] = show
+ end
+
+ @path_kind = nil
+ @spec_dirs = nil
+ @version = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to list contents for"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-lib-only --prefix"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The contents command lists the files in an installed gem. The listing can
+be given as full file names, file names without the installed directory
+prefix or only the files that are requireable.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def execute
+ @version = options[:version] || Gem::Requirement.default
+ @spec_dirs = specification_directories
+ @path_kind = path_description @spec_dirs
+
+ names = gem_names
+
+ names.each do |name|
+ found =
+ if options[:show_install_dir]
+ gem_install_dir name
+ else
+ gem_contents name
+ end
+
+ terminate_interaction 1 unless found || names.length > 1
+ end
+ end
+
+ def files_in(spec)
+ if spec.default_gem?
+ files_in_default_gem spec
+ else
+ files_in_gem spec
+ end
+ end
+
+ def files_in_gem(spec)
+ gem_path = spec.full_gem_path
+ extra = "/{#{spec.require_paths.join ","}}" if options[:lib_only]
+ glob = "#{gem_path}#{extra}/**/*"
+ prefix_re = %r{#{Regexp.escape(gem_path)}/}
+
+ Dir[glob].map do |file|
+ [gem_path, file.sub(prefix_re, "")]
+ end
+ end
+
+ def files_in_default_gem(spec)
+ spec.files.filter_map do |file|
+ if file.start_with?("#{spec.bindir}/")
+ [RbConfig::CONFIG["bindir"], file.delete_prefix("#{spec.bindir}/")]
+ else
+ gem spec.name, spec.version
+
+ require_path = spec.require_paths.find do |path|
+ file.start_with?("#{path}/")
+ end
+
+ requirable_part = file.delete_prefix("#{require_path}/")
+
+ resolve = $LOAD_PATH.resolve_feature_path(requirable_part)&.last
+ next unless resolve
+
+ [resolve.delete_suffix(requirable_part), requirable_part]
+ end
+ end
+ end
+
+ def gem_contents(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ files = files_in spec
+
+ show_files files
+
+ true
+ end
+
+ def gem_install_dir(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ say spec.gem_dir
+
+ true
+ end
+
+ def gem_names # :nodoc:
+ if options[:all]
+ Gem::Specification.map(&:name)
+ else
+ get_all_gem_names
+ end
+ end
+
+ def path_description(spec_dirs) # :nodoc:
+ if spec_dirs.empty?
+ "default gem paths"
+ else
+ "specified path"
+ end
+ end
+
+ def show_files(files)
+ files.sort.each do |prefix, basename|
+ absolute_path = File.join(prefix, basename)
+ next if File.directory? absolute_path
+
+ if options[:prefix]
+ say absolute_path
+ else
+ say basename
+ end
+ end
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}' in #{@path_kind}"
+
+ if Gem.configuration.verbose
+ say "\nDirectories searched:"
+ @spec_dirs.sort.each {|dir| say dir }
+ end
+
+ nil
+ end
+
+ def specification_directories # :nodoc:
+ options[:specdirs].flat_map do |i|
+ [i, File.join(i, "specifications")]
+ end
+ end
+end
diff --git a/lib/rubygems/commands/dependency_command.rb b/lib/rubygems/commands/dependency_command.rb
new file mode 100644
index 0000000000..9aaefae999
--- /dev/null
+++ b/lib/rubygems/commands/dependency_command.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+
+class Gem::Commands::DependencyCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ super "dependency",
+ "Show the dependencies of an installed gem",
+ version: Gem::Requirement.default, domain: :local
+
+ add_version_option
+ add_platform_option
+ add_prerelease_option
+
+ add_option("-R", "--[no-]reverse-dependencies",
+ "Include reverse dependencies in the output") do |value, options|
+ options[:reverse_dependencies] = value
+ end
+
+ add_option("-p", "--pipe",
+ "Pipe Format (name --version ver)") do |value, options|
+ options[:pipe_format] = value
+ end
+
+ add_local_remote_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP show dependencies for gems whose names start with REGEXP"
+ end
+
+ def defaults_str # :nodoc:
+ "--local --version '#{Gem::Requirement.default}' --no-reverse-dependencies"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The dependency commands lists which other gems a given gem depends on. For
+local gems only the reverse dependencies can be shown (which gems depend on
+the named gem).
+
+The dependency list can be displayed in a format suitable for piping for
+use with other commands.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} REGEXP"
+ end
+
+ def fetch_remote_specs(name, requirement, prerelease) # :nodoc:
+ fetcher = Gem::SpecFetcher.fetcher
+
+ specs_type = prerelease ? :complete : :released
+
+ ss = if name.nil?
+ fetcher.detect(specs_type) { true }
+ else
+ fetcher.detect(specs_type) do |name_tuple|
+ name === name_tuple.name && requirement.satisfied_by?(name_tuple.version)
+ end
+ end
+
+ ss.map {|tuple, source| source.fetch_spec(tuple) }
+ end
+
+ def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
+ specs = []
+
+ if local?
+ specs.concat Gem::Specification.stubs.find_all {|spec|
+ name_matches = name_pattern ? name_pattern =~ spec.name : true
+ version_matches = requirement.satisfied_by?(spec.version)
+
+ name_matches && version_matches
+ }.map(&:to_spec)
+ end
+
+ specs.concat fetch_remote_specs name_pattern, requirement, prerelease if remote?
+
+ ensure_specs specs
+
+ specs.uniq.sort
+ end
+
+ def display_pipe(specs) # :nodoc:
+ specs.each do |spec|
+ next if spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ say "#{dep.name} --version '#{dep.requirement}'"
+ end
+ end
+ end
+
+ def display_readable(specs, reverse) # :nodoc:
+ response = String.new
+
+ specs.each do |spec|
+ response << print_dependencies(spec)
+ unless reverse[spec.full_name].empty?
+ response << " Used by\n"
+ reverse[spec.full_name].each do |sp, dep|
+ response << " #{sp} (#{dep})\n"
+ end
+ end
+ response << "\n"
+ end
+
+ say response
+ end
+
+ def execute
+ ensure_local_only_reverse_dependencies
+
+ pattern = name_pattern options[:args]
+ requirement = Gem::Requirement.new options[:version]
+
+ specs = fetch_specs pattern, requirement, options[:prerelease]
+
+ reverse = reverse_dependencies specs
+
+ if options[:pipe_format]
+ display_pipe specs
+ else
+ display_readable specs, reverse
+ end
+ end
+
+ def ensure_local_only_reverse_dependencies # :nodoc:
+ if options[:reverse_dependencies] && remote? && !local?
+ alert_error "Only reverse dependencies for local gems are supported."
+ terminate_interaction 1
+ end
+ end
+
+ def ensure_specs(specs) # :nodoc:
+ return unless specs.empty?
+
+ patterns = options[:args].join ","
+ say "No gems found matching #{patterns} (#{options[:version]})" if
+ Gem.configuration.verbose
+
+ terminate_interaction 1
+ end
+
+ def print_dependencies(spec, level = 0) # :nodoc:
+ response = String.new
+ response << " " * level + "Gem #{spec.full_name}\n"
+ unless spec.dependencies.empty?
+ spec.dependencies.sort_by(&:name).each do |dep|
+ response << " " * level + " #{dep}\n"
+ end
+ end
+ response
+ end
+
+ def reverse_dependencies(specs) # :nodoc:
+ reverse = Hash.new {|h, k| h[k] = [] }
+
+ return reverse unless options[:reverse_dependencies]
+
+ specs.each do |spec|
+ reverse[spec.full_name] = find_reverse_dependencies spec
+ end
+
+ reverse
+ end
+
+ ##
+ # Returns an Array of [specification, dep] that are satisfied by +spec+.
+
+ def find_reverse_dependencies(spec) # :nodoc:
+ result = []
+
+ Gem::Specification.each do |sp|
+ sp.dependencies.each do |dep|
+ dep = Gem::Dependency.new(*dep) unless Gem::Dependency === dep
+
+ if spec.name == dep.name &&
+ dep.requirement.satisfied_by?(spec.version)
+ result << [sp.full_name, dep]
+ end
+ end
+ end
+
+ result
+ end
+
+ private
+
+ def name_pattern(args)
+ return if args.empty?
+
+ if args.length == 1 && args.first =~ /\A(.*)(i)?\z/m
+ flags = $2 ? Regexp::IGNORECASE : nil
+ Regexp.new $1, flags
+ else
+ /\A#{Regexp.union(*args)}/
+ end
+ end
+end
diff --git a/lib/rubygems/commands/environment_command.rb b/lib/rubygems/commands/environment_command.rb
new file mode 100644
index 0000000000..a5eb521a53
--- /dev/null
+++ b/lib/rubygems/commands/environment_command.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::EnvironmentCommand < Gem::Command
+ def initialize
+ super "environment", "Display information about the RubyGems environment"
+ end
+
+ def arguments # :nodoc:
+ args = <<-EOF
+ home display the path where gems are installed. Aliases: gemhome, gemdir, GEM_HOME
+ path display path used to search for gems. Aliases: gempath, GEM_PATH
+ user_gemhome display the path where gems are installed when `--user-install` is given. Aliases: user_gemdir
+ version display the gem format version
+ remotesources display the remote gem servers
+ platform display the supported gem platforms
+ credentials display the path where credentials are stored
+ <omitted> display everything
+ EOF
+ args.gsub(/^\s+/, "")
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The environment command lets you query rubygems for its configuration for
+use in shell scripts or as a debugging aid.
+
+The RubyGems environment can be controlled through command line arguments,
+gemrc files, environment variables and built-in defaults.
+
+Command line argument defaults and some RubyGems defaults can be set in a
+~/.gemrc file for individual users and a gemrc in the SYSTEM CONFIGURATION
+DIRECTORY for all users. These files are YAML files with the following YAML
+keys:
+
+ :sources: A YAML array of remote gem repositories to install gems from
+ :verbose: Verbosity of the gem command. false, true, and :really are the
+ levels
+ :update_sources: Enable/disable automatic updating of repository metadata
+ :concurrent_downloads: The number of gem downloads to perform concurrently
+ :backtrace: Print backtrace when RubyGems encounters an error
+ :gempath: The paths in which to look for gems
+ :disable_default_gem_server: Force specification of gem server host on push
+ <gem_command>: A string containing arguments for the specified gem command
+
+Example:
+
+ :verbose: false
+ install: --no-wrappers
+ update: --no-wrappers
+ :disable_default_gem_server: true
+
+RubyGems' default local repository can be overridden with the GEM_PATH and
+GEM_HOME environment variables. GEM_HOME sets the default repository to
+install into. GEM_PATH allows multiple local repositories to be searched for
+gems.
+
+If you are behind a proxy server, RubyGems uses the HTTP_PROXY,
+HTTP_PROXY_USER and HTTP_PROXY_PASS environment variables to discover the
+proxy server.
+
+If you would like to push gems to a private gem server the RUBYGEMS_HOST
+environment variable can be set to the URI for that server.
+
+If you are packaging RubyGems all of RubyGems' defaults are in
+lib/rubygems/defaults.rb. You may override these in
+lib/rubygems/defaults/operating_system.rb
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [arg]"
+ end
+
+ def execute
+ out = String.new
+ arg = options[:args][0]
+ out <<
+ case arg
+ when /^version/ then
+ Gem::VERSION
+ when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then
+ Gem.dir
+ when /^gempath/, /^path/, /^GEM_PATH/ then
+ Gem.path.join(File::PATH_SEPARATOR)
+ when /^user_gemdir/, /^user_gemhome/ then
+ Gem.user_dir
+ when /^remotesources/ then
+ Gem.sources.to_a.join("\n")
+ when /^platform/ then
+ Gem.platforms.join(File::PATH_SEPARATOR)
+ when /^credentials/, /^creds/ then
+ Gem.configuration.credentials_path
+ when nil then
+ show_environment
+ else
+ raise Gem::CommandLineError, "Unknown environment option [#{arg}]"
+ end
+ say out
+ true
+ end
+
+ def add_path(out, path)
+ path.each do |component|
+ out << " - #{component}\n"
+ end
+ end
+
+ def show_environment # :nodoc:
+ out = "RubyGems Environment:\n".dup
+
+ out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n"
+
+ out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE} patchlevel #{RUBY_PATCHLEVEL}) [#{RUBY_PLATFORM}]\n"
+
+ out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n"
+
+ out << " - USER INSTALLATION DIRECTORY: #{Gem.user_dir}\n"
+
+ out << " - CREDENTIALS FILE: #{Gem.configuration.credentials_path}\n"
+
+ out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil?
+
+ out << " - RUBY EXECUTABLE: #{Gem.ruby}\n"
+
+ out << " - GIT EXECUTABLE: #{git_path}\n"
+
+ out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n"
+
+ out << " - SPEC CACHE DIRECTORY: #{Gem.spec_cache_dir}\n"
+
+ out << " - SYSTEM CONFIGURATION DIRECTORY: #{Gem::ConfigFile::SYSTEM_CONFIG_PATH}\n"
+
+ out << " - RUBYGEMS PLATFORMS:\n"
+ Gem.platforms.each do |platform|
+ out << " - #{platform}\n"
+ end
+
+ out << " - GEM PATHS:\n"
+ out << " - #{Gem.dir}\n"
+
+ gem_path = Gem.path.dup
+ gem_path.delete Gem.dir
+ add_path out, gem_path
+
+ out << " - GEM CONFIGURATION:\n"
+ Gem.configuration.each do |name, value|
+ value = value.gsub(/./, "*") if name == "gemcutter_key"
+ out << " - #{name.inspect} => #{value.inspect}\n"
+ end
+
+ out << " - REMOTE SOURCES:\n"
+ Gem.sources.each do |s|
+ out << " - #{s}\n"
+ end
+
+ out << " - SHELL PATH:\n"
+
+ shell_path = ENV["PATH"].split(File::PATH_SEPARATOR)
+ add_path out, shell_path
+
+ out
+ end
+
+ private
+
+ ##
+ # Git binary path
+
+ def git_path
+ exts = ENV["PATHEXT"] ? ENV["PATHEXT"].split(";") : [""]
+ ENV["PATH"].split(File::PATH_SEPARATOR).each do |path|
+ exts.each do |ext|
+ exe = File.join(path, "git#{ext}")
+ return exe if File.executable?(exe) && !File.directory?(exe)
+ end
+ end
+
+ nil
+ end
+end
diff --git a/lib/rubygems/commands/exec_command.rb b/lib/rubygems/commands/exec_command.rb
new file mode 100644
index 0000000000..1feafbdd35
--- /dev/null
+++ b/lib/rubygems/commands/exec_command.rb
@@ -0,0 +1,259 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../dependency_installer"
+require_relative "../gem_runner"
+require_relative "../package"
+require_relative "../version_option"
+
+class Gem::Commands::ExecCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "exec", "Run a command from a gem", {
+ version: Gem::Requirement.default,
+ }
+
+ add_version_option
+ add_prerelease_option "to be installed"
+
+ add_option "-g", "--gem GEM", "run the executable from the given gem" do |value, options|
+ options[:gem_name] = value
+ end
+
+ add_option(:"Install/Update", "--conservative",
+ "Prefer the most recent installed version, ",
+ "rather than the latest version overall") do |_value, options|
+ options[:conservative] = true
+ end
+ end
+
+ def arguments # :nodoc:
+ "COMMAND the executable command to run"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The exec command handles installing (if necessary) and running an executable
+from a gem, regardless of whether that gem is currently installed.
+
+The exec command can be thought of as a shortcut to running `gem install` and
+then the executable from the installed gem.
+
+For example, `gem exec rails new .` will run `rails new .` in the current
+directory, without having to manually run `gem install rails`.
+Additionally, the exec command ensures the most recent version of the gem
+is used (unless run with `--conservative`), and that the gem is not installed
+to the same gem path as user-installed gems.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [options --] COMMAND [args]"
+ end
+
+ def execute
+ check_executable
+
+ print_command
+ if options[:gem_name] == "gem" && options[:executable] == "gem"
+ set_gem_exec_install_paths
+ Gem::GemRunner.new.run options[:args]
+ return
+ elsif options[:conservative]
+ install_if_needed
+ else
+ install
+ activate!
+ end
+
+ load!
+ end
+
+ private
+
+ def handle_options(args)
+ args = add_extra_args(args)
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
+ parser.order!(args) do |v|
+ # put the non-option back at the front of the list of arguments
+ args.unshift(v)
+
+ # stop parsing once we hit the first non-option,
+ # so you can call `gem exec rails --version` and it prints the rails
+ # version rather than rubygem's
+ break
+ end
+ @options[:args] = args
+
+ options[:executable], gem_version = extract_gem_name_and_version(options[:args].shift)
+ options[:gem_name] ||= options[:executable]
+
+ if gem_version
+ if options[:version].none?
+ options[:version] = Gem::Requirement.new(gem_version)
+ else
+ options[:version].concat [gem_version]
+ end
+ end
+
+ if options[:prerelease] && !options[:version].prerelease?
+ if options[:version].none?
+ options[:version] = Gem::Requirement.default_prerelease
+ else
+ options[:version].concat [Gem::Requirement.default_prerelease]
+ end
+ end
+ end
+
+ def check_executable
+ if options[:executable].nil?
+ raise Gem::CommandLineError,
+ "Please specify an executable to run (e.g. #{program_name} COMMAND)"
+ end
+ end
+
+ def print_command
+ verbose "running #{program_name} with:\n"
+ opts = options.reject {|_, v| v.nil? || Array(v).empty? }
+ max_length = opts.map {|k, _| k.size }.max
+ opts.each do |k, v|
+ next if v.nil?
+ verbose "\t#{k.to_s.rjust(max_length)}: #{v}"
+ end
+ verbose ""
+ end
+
+ def install_if_needed
+ activate!
+ rescue Gem::MissingSpecError
+ verbose "#{Gem::Dependency.new(options[:gem_name], options[:version])} not available locally, installing from remote"
+ install
+ activate!
+ end
+
+ def set_gem_exec_install_paths
+ home = Gem.dir
+
+ ENV["GEM_PATH"] = ([home] + Gem.path).join(File::PATH_SEPARATOR)
+ ENV["GEM_HOME"] = home
+ Gem.clear_paths
+ end
+
+ def install
+ set_gem_exec_install_paths
+
+ gem_name = options[:gem_name]
+ gem_version = options[:version]
+
+ install_options = options.merge(
+ minimal_deps: false,
+ wrappers: true
+ )
+
+ suppress_always_install do
+ dep_installer = Gem::DependencyInstaller.new install_options
+
+ request_set = dep_installer.resolve_dependencies gem_name, gem_version
+
+ verbose "Gems to install:"
+ request_set.sorted_requests.each do |activation_request|
+ verbose "\t#{activation_request.full_name}"
+ end
+
+ request_set.install install_options
+ end
+
+ Gem::Specification.reset
+ rescue Gem::InstallError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ terminate_interaction 1
+ rescue Gem::DependencyResolutionError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ terminate_interaction 2
+ rescue Gem::GemNotFoundException => e
+ show_lookup_failure e.name, e.version, e.errors, false
+
+ terminate_interaction 2
+ rescue Gem::UnsatisfiableDependencyError => e
+ show_lookup_failure e.name, e.version, e.errors, false,
+ "'#{gem_name}' (#{gem_version})"
+
+ terminate_interaction 2
+ end
+
+ def activate!
+ gem(options[:gem_name], options[:version])
+ Gem.finish_resolve
+
+ verbose "activated #{options[:gem_name]} (#{Gem.loaded_specs[options[:gem_name]].version})"
+ end
+
+ def load!
+ argv = ARGV.clone
+ ARGV.replace options[:args]
+
+ executable = options[:executable]
+
+ contains_executable = Gem.loaded_specs.values.select do |spec|
+ spec.executables.include?(executable)
+ end
+
+ if contains_executable.any? {|s| s.name == executable }
+ contains_executable.select! {|s| s.name == executable }
+ end
+
+ if contains_executable.empty?
+ spec = Gem.loaded_specs[executable]
+
+ if spec.nil? || spec.executables.empty?
+ alert_error "Failed to load executable `#{executable}`," \
+ " are you sure the gem `#{options[:gem_name]}` contains it?"
+ terminate_interaction 1
+ end
+
+ if spec.executables.size > 1
+ alert_error "Ambiguous which executable from gem `#{executable}` should be run: " \
+ "the options are #{spec.executables.sort}, specify one via COMMAND, and use `-g` and `-v` to specify gem and version"
+ terminate_interaction 1
+ end
+
+ contains_executable << spec
+ executable = spec.executable
+ end
+
+ if contains_executable.size > 1
+ alert_error "Ambiguous which gem `#{executable}` should come from: " \
+ "the options are #{contains_executable.map(&:name)}, " \
+ "specify one via `-g`"
+ terminate_interaction 1
+ end
+
+ old_exe = $0
+ $0 = executable
+ load Gem.activate_bin_path(contains_executable.first.name, executable, ">= 0.a")
+ ensure
+ $0 = old_exe if old_exe
+ ARGV.replace argv
+ end
+
+ def suppress_always_install
+ name = :always_install
+ cls = ::Gem::Resolver::InstallerSet
+ method = cls.instance_method(name)
+ cls.remove_method(name)
+ cls.define_method(name) { [] }
+
+ begin
+ yield
+ ensure
+ cls.remove_method(name)
+ cls.define_method(name, method)
+ end
+ end
+end
diff --git a/lib/rubygems/commands/fetch_command.rb b/lib/rubygems/commands/fetch_command.rb
new file mode 100644
index 0000000000..8e64a18cee
--- /dev/null
+++ b/lib/rubygems/commands/fetch_command.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+
+class Gem::Commands::FetchCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ defaults = {
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ }
+
+ super "fetch", "Download a gem and place it in the current directory", defaults
+
+ add_bulk_threshold_option
+ add_proxy_option
+ add_source_option
+ add_clear_sources_option
+
+ add_version_option
+ add_platform_option
+ add_prerelease_option
+
+ add_option "--[no-]suggestions", "Suggest alternates when gems are not found" do |value, options|
+ options[:suggest_alternate] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to download"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The fetch command fetches gem files that can be stored for later use or
+unpacked to examine their contents.
+
+See the build command help for an example of unpacking a gem, modifying it,
+then repackaging it.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem fetch 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ check_version
+
+ exit_code = fetch_gems
+
+ terminate_interaction exit_code
+ end
+
+ private
+
+ def fetch_gems
+ exit_code = 0
+
+ version = options[:version]
+
+ platform = Gem.platforms.last
+ gem_names = get_all_gem_names_and_versions
+
+ gem_names.each do |gem_name, gem_version|
+ gem_version ||= version
+ dep = Gem::Dependency.new gem_name, gem_version
+ dep.prerelease = options[:prerelease]
+ suppress_suggestions = !options[:suggest_alternate]
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ if platform
+ filtered = specs_and_sources.select {|s,| s.platform == platform }
+ specs_and_sources = filtered unless filtered.empty?
+ end
+
+ spec, source = specs_and_sources.max_by {|s,| s }
+
+ if spec.nil?
+ show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain]
+ exit_code |= 2
+ next
+ end
+ source.download spec
+ say "Downloaded #{spec.full_name}"
+ end
+
+ exit_code
+ end
+end
diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb
new file mode 100644
index 0000000000..13be92593b
--- /dev/null
+++ b/lib/rubygems/commands/generate_index_command.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+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
+
+ def execute
+ alert_error "Install the rubygems-generate_index gem for the generate_index command"
+ end
+
+ 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
+
+ # 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
+
+ prepend(RubygemsTrampoline)
+ end
+end
diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb
new file mode 100644
index 0000000000..664f400561
--- /dev/null
+++ b/lib/rubygems/commands/help_command.rb
@@ -0,0 +1,377 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::HelpCommand < Gem::Command
+ # :stopdoc:
+ EXAMPLES = <<-EOF
+Some examples of 'gem' usage.
+
+* Install 'rake', either from local directory or remote server:
+
+ gem install rake
+
+* Install 'rake', only from remote server:
+
+ gem install rake --remote
+
+* Install 'rake', but only version 0.3.1, even if dependencies
+ are not met, and into a user-specific directory:
+
+ gem install rake --version 0.3.1 --force --user-install
+
+* List local gems whose name begins with 'D':
+
+ gem list D
+
+* List local and remote gems whose name contains 'log':
+
+ gem search log --both
+
+* List only remote gems whose name contains 'log':
+
+ gem search log --remote
+
+* Uninstall 'rake':
+
+ gem uninstall rake
+
+* Create a gem:
+
+ See https://guides.rubygems.org/make-your-own-gem/
+
+* See information about RubyGems:
+
+ gem environment
+
+* Update all gems on your system:
+
+ gem update
+
+* Update your local version of RubyGems
+
+ gem update --system
+ EOF
+
+ GEM_DEPENDENCIES = <<-EOF
+A gem dependencies file allows installation of a consistent set of gems across
+multiple environments. The RubyGems implementation is designed to be
+compatible with Bundler's Gemfile format. You can see additional
+documentation on the format at:
+
+ https://bundler.io
+
+RubyGems automatically looks for these gem dependencies files:
+
+* gem.deps.rb
+* Gemfile
+* Isolate
+
+These files are looked up automatically using `gem install -g`, or you can
+specify a custom file.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+You can also activate gem dependencies at program startup using
+Gem.use_gemdeps.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to execution
+of arbitrary code when used from directories outside your control.
+
+Gem Dependencies
+================
+
+Use #gem to declare which gems you directly depend upon:
+
+ gem 'rake'
+
+To depend on a specific set of versions:
+
+ gem 'rake', '>= 10.3.2'
+ # or for multiple version restrictions
+ gem 'rake', '>= 10.3.2', "< 13"
+
+RubyGems will require the gem name when activating the gem using
+the RUBYGEMS_GEMDEPS environment variable or Gem::use_gemdeps. Use the
+require: option to override this behavior if the gem does not have a file of
+that name or you don't want to require those files:
+
+ gem 'my_gem', require: 'other_file'
+
+To prevent RubyGems from requiring any files use:
+
+ gem 'my_gem', require: false
+
+To load dependencies from a .gemspec file:
+
+ gemspec
+
+RubyGems looks for the first .gemspec file in the current directory. To
+override this use the name: option:
+
+ gemspec name: 'specific_gem'
+
+To look in a different directory use the path: option:
+
+ gemspec name: 'specific_gem', path: 'gemspecs'
+
+To depend on a gem unpacked into a local directory:
+
+ gem 'modified_gem', path: 'vendor/modified_gem'
+
+To depend on a gem from git:
+
+ gem 'private_gem', git: 'git@my.company.example:private_gem.git'
+
+To depend on a gem from github:
+
+ gem 'private_gem', github: 'my_company/private_gem'
+
+To depend on a gem from a github gist:
+
+ gem 'bang', gist: '1232884'
+
+Git, github and gist support the ref:, branch: and tag: options to specify a
+commit reference or hash, branch or tag respectively to use for the gem.
+
+Setting the submodules: option to true for git, github and gist dependencies
+causes fetching of submodules when fetching the repository.
+
+You can depend on multiple gems from a single repository with the git method:
+
+ git 'https://github.com/rails/rails.git' do
+ gem 'activesupport'
+ gem 'activerecord'
+ end
+
+Gem Sources
+===========
+
+RubyGems uses the default sources for regular `gem install` for gem
+dependencies files. Unlike bundler, you do need to specify a source.
+
+You can override the sources used for downloading gems with:
+
+ source 'https://gem_server.example'
+
+You may specify multiple sources. Unlike bundler the prepend: option is not
+supported. Sources are used in-order, to prepend a source place it at the
+front of the list.
+
+Gem Platform
+============
+
+You can restrict gem dependencies to specific platforms with the #platform
+and #platforms methods:
+
+ platform :ruby_21 do
+ gem 'debugger'
+ end
+
+See the bundler Gemfile manual page for a list of platforms supported in a gem
+dependencies file.:
+
+ https://bundler.io/v2.5/man/gemfile.5.html
+
+Ruby Version and Engine Dependency
+==================================
+
+You can specify the version, engine and engine version of ruby to use with
+your gem dependencies file. If you are not running the specified version
+RubyGems will raise an exception.
+
+To depend on a specific version of ruby:
+
+ ruby '2.1.2'
+
+To depend on a specific ruby engine:
+
+ ruby '1.9.3', engine: 'jruby'
+
+To depend on a specific ruby engine version:
+
+ ruby '1.9.3', engine: 'jruby', engine_version: '1.7.11'
+
+Grouping Dependencies
+=====================
+
+Gem dependencies may be placed in groups that can be excluded from install.
+Dependencies required for development or testing of your code may be excluded
+when installed in a production environment.
+
+A #gem dependency may be placed in a group using the group: option:
+
+ gem 'minitest', group: :test
+
+To install dependencies from a gemfile without specific groups use the
+`--without` option for `gem install -g`:
+
+ $ gem install -g --without test
+
+The group: option also accepts multiple groups if the gem fits in multiple
+categories.
+
+Multiple groups may be excluded during install by comma-separating the groups for `--without` or by specifying `--without` multiple times.
+
+The #group method can also be used to place gems in groups:
+
+ group :test do
+ gem 'minitest'
+ gem 'minitest-emoji'
+ end
+
+The #group method allows multiple groups.
+
+The #gemspec development dependencies are placed in the :development group by
+default. This may be overridden with the :development_group option:
+
+ gemspec development_group: :other
+
+ EOF
+
+ PLATFORMS = <<-'EOF'
+RubyGems platforms are composed of three parts, a CPU, an OS, and a
+version. These values are taken from values in rbconfig.rb. You can view
+your current platform by running `gem environment`.
+
+RubyGems matches platforms as follows:
+
+ * The CPU must match exactly unless one of the platforms has
+ "universal" as the CPU or the local CPU starts with "arm" and the gem's
+ CPU is exactly "arm" (for gems that support generic ARM architecture).
+ * The OS must match exactly.
+ * The versions must match exactly unless one of the versions is nil.
+
+For commands that install, uninstall and list gems, you can override what
+RubyGems thinks your platform is with the --platform option. The platform
+you pass must match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{version}". On mswin
+platforms, the version is the compiler version, not the OS version. (Ruby
+compiled with VC6 uses "60" as the compiler version, VC8 uses "80".)
+
+For the ARM architecture, gems with a platform of "arm-linux" should run on a
+reasonable set of ARM CPUs and not depend on instructions present on a limited
+subset of the architecture. For example, the binary should run on platforms
+armv5, armv6hf, armv6l, armv7, etc. If you use the "arm-linux" platform
+please test your gem on a variety of ARM hardware before release to ensure it
+functions correctly.
+
+Example platforms:
+
+ x86-freebsd # Any FreeBSD version on an x86 CPU
+ universal-darwin-8 # Darwin 8 only gems that run on any CPU
+ x86-mswin32-80 # Windows gems compiled with VC8
+ armv7-linux # Gem complied for an ARMv7 CPU running linux
+ arm-linux # Gem compiled for any ARM CPU running linux
+
+When building platform gems, set the platform in the gem specification to
+Gem::Platform::CURRENT. This will correctly mark the gem with your ruby's
+platform.
+ EOF
+
+ # NOTE: when updating also update Gem::Command::HELP
+
+ SUBCOMMANDS = [
+ ["commands", :show_commands],
+ ["options", Gem::Command::HELP],
+ ["examples", EXAMPLES],
+ ["gem_dependencies", GEM_DEPENDENCIES],
+ ["platforms", PLATFORMS],
+ ].freeze
+ # :startdoc:
+
+ def initialize
+ super "help", "Provide help on the 'gem' command"
+
+ @command_manager = Gem::CommandManager.instance
+ end
+
+ def usage # :nodoc:
+ "#{program_name} ARGUMENT"
+ end
+
+ def execute
+ arg = options[:args][0]
+
+ _, help = SUBCOMMANDS.find do |command,|
+ begins? command, arg
+ end
+
+ if help
+ if Symbol === help
+ send help
+ else
+ say help
+ end
+ return
+ end
+
+ if options[:help]
+ show_help
+
+ elsif arg
+ show_command_help arg
+
+ else
+ say Gem::Command::HELP
+ end
+ end
+
+ def show_commands # :nodoc:
+ out = []
+ out << "GEM commands are:"
+ out << nil
+
+ margin_width = 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"
+
+ @command_manager.command_names.each do |cmd_name|
+ command = @command_manager[cmd_name]
+
+ next if command&.deprecated?
+
+ summary =
+ if command
+ command.summary
+ else
+ "[No command found for #{cmd_name}]"
+ end
+
+ summary = wrap(summary, summary_width).split "\n"
+ out << format(format, cmd_name, summary.shift)
+ until summary.empty? do
+ out << "#{wrap_indent}#{summary.shift}"
+ end
+ end
+
+ out << nil
+ out << "For help on a particular command, use 'gem help COMMAND'."
+ out << nil
+ out << "Commands may be abbreviated, so long as they are unambiguous."
+ out << "e.g. 'gem i rake' is short for 'gem install rake'."
+
+ say out.join("\n")
+ end
+
+ def show_command_help(command_name) # :nodoc:
+ command_name = command_name.downcase
+
+ possibilities = @command_manager.find_command_possibilities command_name
+
+ if possibilities.size == 1
+ command = @command_manager[possibilities.first]
+ command.invoke("--help")
+ elsif possibilities.size > 1
+ alert_warning "Ambiguous command #{command_name} (#{possibilities.join(", ")})"
+ else
+ alert_warning "Unknown command #{command_name}. Try: gem help commands"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/info_command.rb b/lib/rubygems/commands/info_command.rb
new file mode 100644
index 0000000000..f65c639662
--- /dev/null
+++ b/lib/rubygems/commands/info_command.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::InfoCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "info", "Show information for the given gem",
+ name: //, domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+
+ remove_option("-d")
+
+ defaults[:details] = true
+ defaults[:exact] = true
+ end
+
+ def description # :nodoc:
+ "Info prints information about the gem such as name,"\
+ " description, website, license and installed paths"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME"
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of the gem to print information about"
+ end
+
+ def defaults_str
+ "--local"
+ end
+end
diff --git a/lib/rubygems/commands/install_command.rb b/lib/rubygems/commands/install_command.rb
new file mode 100644
index 0000000000..6d3beec0b4
--- /dev/null
+++ b/lib/rubygems/commands/install_command.rb
@@ -0,0 +1,268 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../install_update_options"
+require_relative "../dependency_installer"
+require_relative "../local_remote_options"
+require_relative "../validator"
+require_relative "../version_option"
+require_relative "../update_suggestion"
+
+##
+# Gem installer command line tool
+#
+# See `gem help install`
+
+class Gem::Commands::InstallCommand < Gem::Command
+ attr_reader :installed_specs # :nodoc:
+
+ include Gem::VersionOption
+ include Gem::LocalRemoteOptions
+ include Gem::InstallUpdateOptions
+ include Gem::UpdateSuggestion
+
+ def initialize
+ defaults = Gem::DependencyInstaller::DEFAULT_OPTIONS.merge({
+ format_executable: false,
+ lock: true,
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ without_groups: [],
+ })
+
+ defaults.merge!(install_update_options)
+
+ super "install", "Install a gem into the local repository", defaults
+
+ add_install_update_options
+ add_local_remote_options
+ add_platform_option
+ add_version_option
+ add_prerelease_option "to be installed. (Only for listed gems)"
+
+ @installed_specs = []
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to install"
+ end
+
+ def defaults_str # :nodoc:
+ "--both --version '#{Gem::Requirement.default}' --no-force\n" \
+ "--install-dir #{Gem.dir} --lock\n" +
+ install_update_defaults_str
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The install command installs local or remote gem into a gem repository.
+
+For gems with executables ruby installs a wrapper file into the executable
+directory by default. This can be overridden with the --no-wrappers option.
+The wrapper allows you to choose among alternate gem versions using _version_.
+
+For example `rake _0.7.3_ --version` will run rake version 0.7.3 if a newer
+version is also installed.
+
+Gem Dependency Files
+====================
+
+RubyGems can install a consistent set of gems across multiple environments
+using `gem install -g` when a gem dependencies file (gem.deps.rb, Gemfile or
+Isolate) is present. If no explicit file is given RubyGems attempts to find
+one in the current directory.
+
+When the RUBYGEMS_GEMDEPS environment variable is set to a gem dependencies
+file the gems from that file will be activated at startup time. Set it to a
+specific filename or to "-" to have RubyGems automatically discover the gem
+dependencies file by walking up from the current directory.
+
+NOTE: Enabling automatic discovery on multiuser systems can lead to
+execution of arbitrary code when used from directories outside your control.
+
+Extension Install Failures
+==========================
+
+If an extension fails to compile during gem installation the gem
+specification is not written out, but the gem remains unpacked in the
+repository. You may need to specify the path to the library's headers and
+libraries to continue. You can do this by adding a -- between RubyGems'
+options and the extension's build options:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ gem install some_extension_gem -- --with-extension-lib=/path/to/lib
+ [build succeeds]
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+If you correct the compilation errors by editing the gem files you will need
+to write the specification by hand. For example:
+
+ $ gem install some_extension_gem
+ [build fails]
+ Gem files will remain installed in \\
+ /path/to/gems/some_extension_gem-1.0 for inspection.
+ Results logged to /path/to/gems/some_extension_gem-1.0/gem_make.out
+ $ [cd /path/to/gems/some_extension_gem-1.0]
+ $ [edit files or what-have-you and run make]
+ $ gem spec ../../cache/some_extension_gem-1.0.gem --ruby > \\
+ ../../specifications/some_extension_gem-1.0.gemspec
+ $ gem list some_extension_gem
+
+ *** LOCAL GEMS ***
+
+ some_extension_gem (1.0)
+ $
+
+Command Alias
+==========================
+
+You can use `i` command instead of `install`.
+
+ $ gem i GEMNAME
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [options] GEMNAME [GEMNAME ...] -- --build-flags"
+ end
+
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem install 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ if options.include? :gemdeps
+ install_from_gemdeps
+ return # not reached
+ end
+
+ @installed_specs = []
+
+ ENV.delete "GEM_PATH" if options[:install_dir].nil?
+
+ check_version
+
+ load_hooks
+
+ exit_code = install_gems
+
+ show_installed
+
+ say update_suggestion if eligible_for_update?
+
+ terminate_interaction exit_code
+ end
+
+ def install_from_gemdeps # :nodoc:
+ require_relative "../request_set"
+ rs = Gem::RequestSet.new
+
+ specs = rs.install_from_gemdeps options do |req, inst|
+ s = req.full_spec
+
+ if inst
+ say "Installing #{s.name} (#{s.version})"
+ else
+ say "Using #{s.name} (#{s.version})"
+ end
+ end
+
+ @installed_specs = specs
+
+ terminate_interaction
+ end
+
+ def install_gem(name, version) # :nodoc:
+ return if options[:conservative] &&
+ !Gem::Dependency.new(name, version).matching_specs.empty?
+
+ req = Gem::Requirement.create(version)
+
+ dinst = Gem::DependencyInstaller.new options
+
+ request_set = dinst.resolve_dependencies name, req
+
+ if options[:explain]
+ say "Gems to install:"
+
+ request_set.sorted_requests.each do |activation_request|
+ say " #{activation_request.full_name}"
+ end
+ else
+ @installed_specs.concat request_set.install options
+ end
+
+ show_install_errors dinst.errors
+ end
+
+ def install_gems # :nodoc:
+ exit_code = 0
+
+ get_all_gem_names_and_versions.each do |gem_name, gem_version|
+ gem_version ||= options[:version]
+ domain = options[:domain]
+ domain = :local unless options[:suggest_alternate]
+ suppress_suggestions = (domain == :local)
+
+ begin
+ install_gem gem_name, gem_version
+ rescue Gem::InstallError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ exit_code |= 1
+ rescue Gem::DependencyResolutionError => e
+ alert_error "Error installing #{gem_name}:\n\t#{e.message}"
+ exit_code |= 2
+ rescue Gem::UnsatisfiableDependencyError => e
+ show_lookup_failure e.name, e.version, e.errors, suppress_suggestions,
+ "'#{gem_name}' (#{gem_version})"
+
+ exit_code |= 2
+ end
+ end
+
+ exit_code
+ end
+
+ ##
+ # Loads post-install hooks
+
+ def load_hooks # :nodoc:
+ require_relative "../install_message"
+ require_relative "../rdoc"
+ end
+
+ def show_install_errors(errors) # :nodoc:
+ return unless errors
+
+ errors.each do |x|
+ next unless Gem::SourceFetchProblem === x
+
+ require_relative "../uri"
+ msg = "Unable to pull data from '#{Gem::Uri.redact(x.source.uri)}': #{x.error.message}"
+
+ alert_warning msg
+ end
+ end
+
+ def show_installed # :nodoc:
+ return if @installed_specs.empty?
+
+ gems = @installed_specs.length == 1 ? "gem" : "gems"
+ say "#{@installed_specs.length} #{gems} installed"
+ end
+end
diff --git a/lib/rubygems/commands/list_command.rb b/lib/rubygems/commands/list_command.rb
new file mode 100644
index 0000000000..fab4b73814
--- /dev/null
+++ b/lib/rubygems/commands/list_command.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+##
+# Searches for gems starting with the supplied argument.
+
+class Gem::Commands::ListCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "list", "Display local gems whose name matches REGEXP",
+ domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP regexp to look for in gem name"
+ end
+
+ def defaults_str # :nodoc:
+ "--local --no-details"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The list command is used to view the gems you have installed locally.
+
+The --details option displays additional details including the summary, the
+homepage, the author, the locations of different versions of the gem.
+
+To search for remote gems use the search command.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [REGEXP ...]"
+ end
+end
diff --git a/lib/rubygems/commands/lock_command.rb b/lib/rubygems/commands/lock_command.rb
new file mode 100644
index 0000000000..f7fd5ada16
--- /dev/null
+++ b/lib/rubygems/commands/lock_command.rb
@@ -0,0 +1,109 @@
+# 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
+
+ add_option "-s", "--[no-]strict",
+ "fail if unable to satisfy a dependency" do |strict, options|
+ options[:strict] = strict
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to lock\nVERSION version of gem to lock"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-strict"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The lock command will generate a list of +gem+ statements that will lock down
+the versions for the gem given in the command line. It will specify exact
+versions in the requirements list to ensure that the gems loaded will always
+be consistent. A full recursive search of all effected gems will be
+generated.
+
+Example:
+
+ gem lock rails-1.0.0 > lockdown.rb
+
+will produce in lockdown.rb:
+
+ require "rubygems"
+ gem 'rails', '= 1.0.0'
+ gem 'rake', '= 0.7.0.1'
+ gem 'activesupport', '= 1.2.5'
+ gem 'activerecord', '= 1.13.2'
+ gem 'actionpack', '= 1.11.2'
+ gem 'actionmailer', '= 1.1.5'
+ gem 'actionwebservice', '= 1.0.0'
+
+Just load lockdown.rb from your application to ensure that the current
+versions are loaded. Make sure that lockdown.rb is loaded *before* any
+other require statements.
+
+Notice that rails 1.0.0 only requires that rake 0.6.2 or better be used.
+Rake-0.7.0.1 is the most recent version installed that satisfies that, so we
+lock it down to the exact version.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME-VERSION [GEMNAME-VERSION ...]"
+ end
+
+ def complain(message)
+ if options[:strict]
+ raise Gem::Exception, message
+ else
+ say "# #{message}"
+ end
+ end
+
+ def execute
+ say "require 'rubygems'"
+
+ locked = {}
+
+ pending = options[:args]
+
+ until pending.empty? do
+ full_name = pending.shift
+
+ spec = Gem::Specification.load spec_path(full_name)
+
+ if spec.nil?
+ complain "Could not find gem #{full_name}, try using the full name"
+ next
+ end
+
+ say "gem '#{spec.name}', '= #{spec.version}'" unless locked[spec.name]
+ locked[spec.name] = true
+
+ spec.runtime_dependencies.each do |dep|
+ next if locked[dep.name]
+ candidates = dep.matching_specs
+
+ if candidates.empty?
+ complain "Unable to satisfy '#{dep}' from currently installed gems"
+ else
+ pending << candidates.last.full_name
+ end
+ end
+ end
+ end
+
+ def spec_path(gem_full_name)
+ gemspecs = Gem.path.map do |path|
+ File.join path, "specifications", "#{gem_full_name}.gemspec"
+ end
+
+ gemspecs.find {|path| File.exist? path }
+ end
+end
diff --git a/lib/rubygems/commands/mirror_command.rb b/lib/rubygems/commands/mirror_command.rb
new file mode 100644
index 0000000000..b91a8db12d
--- /dev/null
+++ b/lib/rubygems/commands/mirror_command.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::MirrorCommand
+ class Gem::Commands::MirrorCommand < Gem::Command
+ def initialize
+ super("mirror", "Mirror all gem files (requires rubygems-mirror)")
+ begin
+ Gem::Specification.find_by_name("rubygems-mirror").activate
+ rescue Gem::LoadError
+ # no-op
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The mirror command has been moved to the rubygems-mirror gem.
+ EOF
+ end
+
+ def execute
+ alert_error "Install the rubygems-mirror gem for the mirror command"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/open_command.rb b/lib/rubygems/commands/open_command.rb
new file mode 100644
index 0000000000..0fe90dc8b8
--- /dev/null
+++ b/lib/rubygems/commands/open_command.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+
+class Gem::Commands::OpenCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "open", "Open gem sources in editor"
+
+ add_option("-e", "--editor COMMAND", String,
+ "Prepends COMMAND to gem path. Could be used to specify editor.") do |command, options|
+ options[:editor] = command || get_env_editor
+ end
+ add_option("-v", "--version VERSION", String,
+ "Opens specific gem version") do |version|
+ options[:version] = version
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to open in editor"
+ end
+
+ def defaults_str # :nodoc:
+ "-e #{get_env_editor}"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+ The open command opens gem in editor and changes current path
+ to gem's source directory.
+ Editor command can be specified with -e option, otherwise rubygems
+ will look for editor in $EDITOR, $VISUAL and $GEM_EDITOR variables.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [-e COMMAND] GEMNAME"
+ end
+
+ def get_env_editor
+ ENV["GEM_EDITOR"] ||
+ ENV["VISUAL"] ||
+ ENV["EDITOR"] ||
+ "vi"
+ end
+
+ def execute
+ @version = options[:version] || Gem::Requirement.default
+ @editor = options[:editor] || get_env_editor
+
+ found = open_gem(get_one_gem_name)
+
+ terminate_interaction 1 unless found
+ end
+
+ def open_gem(name)
+ spec = spec_for name
+
+ return false unless spec
+
+ if spec.default_gem?
+ say "'#{name}' is a default gem and can't be opened."
+ return false
+ end
+
+ open_editor(spec.full_gem_path)
+ end
+
+ def open_editor(path)
+ system(*@editor.split(/\s+/) + [path], { chdir: path })
+ end
+
+ def spec_for(name)
+ spec = Gem::Specification.find_all_by_name(name, @version).first
+
+ return spec if spec
+
+ say "Unable to find gem '#{name}'"
+ end
+end
diff --git a/lib/rubygems/commands/outdated_command.rb b/lib/rubygems/commands/outdated_command.rb
new file mode 100644
index 0000000000..08a9221a26
--- /dev/null
+++ b/lib/rubygems/commands/outdated_command.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+
+class Gem::Commands::OutdatedCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ super "outdated", "Display all gems that need updates"
+
+ add_local_remote_options
+ add_platform_option
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The outdated command lists gems you may wish to upgrade to a newer version.
+
+You can check for dependency mismatches using the dependency command and
+update the gems with the update or install commands.
+ EOF
+ end
+
+ def execute
+ Gem::Specification.outdated_and_latest_version.each do |spec, remote_version|
+ say "#{spec.name} (#{spec.version} < #{remote_version})"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/owner_command.rb b/lib/rubygems/commands/owner_command.rb
new file mode 100644
index 0000000000..675e866734
--- /dev/null
+++ b/lib/rubygems/commands/owner_command.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../text"
+
+class Gem::Commands::OwnerCommand < Gem::Command
+ include Gem::Text
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The owner command lets you add and remove owners of a gem on a push
+server (the default is https://rubygems.org). Multiple owners can be
+added or removed at the same time, if the flag is given multiple times.
+
+The supported user identifiers are dependent on the push server.
+For rubygems.org, both e-mail and handle are supported, even though the
+user identifier field is called "email".
+
+The owner of a gem has the permission to push new versions, yank existing
+versions or edit the HTML page of the gem. Be careful of who you give push
+permission to.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM gem to manage owners for"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "owner", "Manage gem owners of a gem on the push server"
+ add_proxy_option
+ add_key_option
+ add_otp_option
+ defaults.merge! add: [], remove: []
+
+ add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options|
+ options[:add] << value
+ end
+
+ add_option "-r", "--remove OLD_OWNER", "Remove an owner by user identifier" do |value, options|
+ options[:remove] << value
+ end
+
+ add_option "-h", "--host HOST",
+ "Use another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)" do |value, options|
+ options[:host] = value
+ end
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in(scope: get_owner_scope)
+ name = get_one_gem_name
+
+ add_owners name, options[:add]
+ remove_owners name, options[:remove]
+ show_owners name
+ end
+
+ def show_owners(name)
+ Gem.load_yaml
+
+ response = rubygems_api_request :get, "api/v1/gems/#{name}/owners.yaml" do |request|
+ request.add_field "Authorization", api_key
+ end
+
+ with_response response do |resp|
+ owners = Gem::SafeYAML.safe_load clean_text(resp.body)
+
+ say "Owners for gem: #{name}"
+ owners.each do |owner|
+ identifier = owner["email"] || owner["handle"] || owner["id"]
+ say "- #{identifier} (#{owner["role"]})"
+ end
+ end
+ end
+
+ def add_owners(name, owners)
+ manage_owners :post, name, owners
+ end
+
+ def remove_owners(name, owners)
+ manage_owners :delete, name, owners
+ end
+
+ def manage_owners(method, name, owners)
+ owners.each do |owner|
+ 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
+
+ private
+
+ def send_owner_request(method, name, owner)
+ rubygems_api_request method, "api/v1/gems/#{name}/owners", scope: get_owner_scope(method: method) do |request|
+ request.set_form_data "email" => owner
+ request.add_field "Authorization", api_key
+ end
+ end
+
+ def get_owner_scope(method: nil)
+ if method == :post || options.any? && options[:add].any?
+ :add_owner
+ elsif method == :delete || options.any? && options[:remove].any?
+ :remove_owner
+ end
+ end
+end
diff --git a/lib/rubygems/commands/pristine_command.rb b/lib/rubygems/commands/pristine_command.rb
new file mode 100644
index 0000000000..10978c2af7
--- /dev/null
+++ b/lib/rubygems/commands/pristine_command.rb
@@ -0,0 +1,223 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../package"
+require_relative "../installer"
+require_relative "../version_option"
+
+class Gem::Commands::PristineCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "pristine",
+ "Restores installed gems to pristine condition from files located in the gem cache",
+ version: Gem::Requirement.default,
+ extensions: true,
+ extensions_set: false,
+ all: false
+
+ add_option("--all",
+ "Restore all installed gems to pristine",
+ "condition") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("--skip=gem_name",
+ "used on --all, skip if name == gem_name") do |value, options|
+ options[:skip] ||= []
+ options[:skip] << value
+ end
+
+ add_option("--[no-]extensions",
+ "Restore gems with extensions",
+ "in addition to regular gems") do |value, options|
+ options[:extensions_set] = true
+ 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
+ end
+
+ add_option("--only-plugins",
+ "Only restore plugins") do |value, options|
+ options[:only_plugins] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ add_option("-i", "--install-dir DIR",
+ "Gem repository to get gems restored") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option("-n", "--bindir DIR",
+ "Directory where executables are",
+ "located") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_version_option("restore to", "pristine condition")
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME gem to restore to pristine condition (unless --all)"
+ end
+
+ def defaults_str # :nodoc:
+ "--extensions"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The pristine command compares an installed gem with the contents of its
+cached .gem file and restores any files that don't match the cached .gem's
+copy.
+
+If you have made modifications to an installed gem, the pristine command
+will revert them. All extensions are rebuilt and all bin stubs for the gem
+are regenerated after checking for modifications.
+
+Rebuilding extensions also refreshes C-extension gems against updated system
+libraries (for example after OS or package upgrades) to avoid mismatches like
+outdated library version warnings.
+
+If the cached gem cannot be found it will be downloaded.
+
+If --no-extensions is provided pristine will not attempt to restore a gem
+with an extension.
+
+If --extensions is given (but not --all or gem names) only gems with
+extensions will be restored.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEMNAME ...]"
+ end
+
+ def execute
+ install_dir = options[:install_dir]
+
+ specification_record = install_dir ? Gem::SpecificationRecord.from_path(install_dir) : Gem::Specification.specification_record
+
+ specs = if options[:all]
+ specification_record.map
+
+ # `--extensions` must be explicitly given to pristine only gems
+ # with extensions.
+ elsif options[:extensions_set] &&
+ options[:extensions] && options[:args].empty?
+ specification_record.select do |spec|
+ spec.extensions && !spec.extensions.empty?
+ end
+ elsif options[:only_missing_extensions]
+ specification_record.select(&:missing_extensions?)
+ else
+ get_all_gem_names.sort.flat_map do |gem_name|
+ specification_record.find_all_by_name(gem_name, options[:version]).reverse
+ end
+ end
+
+ specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY }
+
+ if specs.to_a.empty?
+ if options[:only_missing_extensions]
+ say "No gems with missing extensions to restore"
+ return
+ end
+
+ raise Gem::Exception,
+ "Failed to find gems #{options[:args]} #{options[:version]}"
+ end
+
+ say "Restoring gems to pristine condition..."
+
+ specs.group_by(&:full_name_with_location).values.each do |grouped_specs|
+ spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first
+
+ only_executables = options[:only_executables]
+ only_plugins = options[:only_plugins]
+
+ unless only_executables || only_plugins
+ # Default gemspecs include changes provided by ruby-core installer that
+ # can't currently be pristined (inclusion of compiled extension targets in
+ # the file list). So stick to resetting executables if it's a default gem.
+ only_executables = true if spec.default_gem?
+ end
+
+ if options.key? :skip
+ if options[:skip].include? spec.name
+ say "Skipped #{spec.full_name}, it was given through options"
+ next
+ end
+ end
+
+ unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins
+ say "Skipped #{spec.full_name_with_location}, it needs to compile an extension"
+ next
+ end
+
+ gem = spec.cache_file
+
+ unless File.exist?(gem) || only_executables || only_plugins
+ require_relative "../remote_fetcher"
+
+ say "Cached gem for #{spec.full_name_with_location} not found, attempting to fetch..."
+
+ dep = Gem::Dependency.new spec.name, spec.version
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ if found.empty?
+ say "Skipped #{spec.full_name}, it was not found from cache and remote sources"
+ next
+ end
+
+ spec_candidate, source = found.first
+ Gem::RemoteFetcher.fetcher.download spec_candidate, source.uri.to_s, spec.base_dir
+ end
+
+ env_shebang =
+ if options.include? :env_shebang
+ options[:env_shebang]
+ else
+ install_defaults = Gem::ConfigFile::PLATFORM_DEFAULTS["install"]
+ install_defaults.to_s["--env-shebang"]
+ end
+
+ bin_dir = options[:bin_dir] if options[:bin_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,
+ }
+
+ if only_executables
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_bin
+ elsif only_plugins
+ installer = Gem::Installer.for_spec(spec, installer_options)
+ installer.generate_plugins
+ else
+ installer = Gem::Installer.at(gem, installer_options)
+ installer.install
+ end
+
+ say "Restored #{spec.full_name_with_location}"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb
new file mode 100644
index 0000000000..02931b3025
--- /dev/null
+++ b/lib/rubygems/commands/push_command.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../gemcutter_utilities"
+require_relative "../package"
+
+class Gem::Commands::PushCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The push command uploads a gem to the push server (the default is
+https://rubygems.org) and adds it to the index.
+
+The gem can be removed from the index and deleted from the server using the yank
+command. For further discussion see the help for the yank command.
+
+The push command will use ~/.gem/credentials to authenticate to a server, but you can use the RubyGems environment variable GEM_HOST_API_KEY to set the api key to authenticate.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM built gem to push up"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM"
+ end
+
+ def initialize
+ super "push", "Push a gem up to the gem server", host: host, attestations: []
+
+ @user_defined_host = false
+
+ add_proxy_option
+ add_key_option
+ add_otp_option
+
+ add_option("--host HOST",
+ "Push to another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ @user_defined_host = true
+ end
+
+ add_option("--attestation FILE",
+ "Push with sigstore attestations") do |value, options|
+ options[:attestations] << value
+ end
+
+ @host = nil
+ end
+
+ def execute
+ gem_name = get_one_gem_name
+ default_gem_server, push_host = get_hosts_for(gem_name)
+
+ @host = if @user_defined_host
+ options[:host]
+ elsif default_gem_server
+ default_gem_server
+ elsif push_host
+ push_host
+ else
+ options[:host]
+ end
+
+ sign_in @host, scope: get_push_scope
+
+ send_gem(gem_name)
+ end
+
+ def send_gem(name)
+ args = [:post, "api/v1/gems"]
+
+ _, push_host = get_hosts_for(name)
+
+ @host ||= push_host
+
+ # Always include @host, even if it's nil
+ args += [@host, push_host]
+
+ say "Pushing gem to #{@host || Gem.host}..."
+
+ response = send_push_request(name, args)
+
+ with_response response
+ end
+
+ private
+
+ def send_push_request(name, args)
+ # Always honor explicit --attestation option
+ # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby)
+ if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"])
+ send_push_request_with_attestation(name, args)
+ else
+ send_push_request_without_attestation(name, args)
+ end
+ end
+
+ def send_push_request_without_attestation(name, args)
+ scope = get_push_scope
+ rubygems_api_request(*args, scope: scope) do |request|
+ body = Gem.read_binary name
+ request.body = body
+ request.add_field "Content-Type", "application/octet-stream"
+ request.add_field "Content-Length", request.body.size
+ request.add_field "Authorization", api_key
+ end
+ end
+
+ def send_push_request_with_attestation(name, args)
+ attestations = if options[:attestations].any?
+ options[:attestations].map do |attestation|
+ Gem.read_binary(attestation)
+ end
+ else
+ bundle_path = attest!(name)
+ begin
+ [Gem.read_binary(bundle_path)]
+ ensure
+ File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path)
+ end
+ end
+ bundles = "[" + attestations.join(",") + "]"
+
+ rubygems_api_request(*args, scope: get_push_scope) do |request|
+ request.set_form([
+ ["gem", Gem.read_binary(name), { filename: name, content_type: "application/octet-stream" }],
+ ["attestations", bundles, { content_type: "application/json" }],
+ ], "multipart/form-data")
+ request.add_field "Authorization", api_key
+ end
+ rescue StandardError => e
+ message = "Failed to push with attestation, retrying without attestation.\n"
+ message += if Gem.configuration.really_verbose
+ e.full_message
+ else
+ e.message
+ end
+ alert_warning message
+ send_push_request_without_attestation(name, args)
+ end
+
+ def attest!(name)
+ require "open3"
+ require "tempfile"
+
+ tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"])
+ bundle = tempfile.path
+ tempfile.close(false)
+
+ env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h
+ out, st = Open3.capture2e(
+ env,
+ Gem.ruby, "-S", "gem", "exec", "--conservative",
+ "sigstore-cli", "sign", name, "--bundle", bundle,
+ unsetenv_others: true
+ )
+ raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success?
+
+ bundle
+ end
+
+ def get_hosts_for(name)
+ gem_metadata = Gem::Package.new(name).spec.metadata
+
+ [
+ gem_metadata["default_gem_server"],
+ gem_metadata["allowed_push_host"],
+ ]
+ end
+
+ def get_push_scope
+ :push_rubygem
+ end
+
+ def attestation_supported_host?
+ host = (@host || Gem.host).to_s.chomp("/")
+ host == Gem::DEFAULT_HOST
+ end
+end
diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb
new file mode 100644
index 0000000000..62c4bf8ce9
--- /dev/null
+++ b/lib/rubygems/commands/rdoc_command.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../rdoc"
+require "fileutils"
+
+class Gem::Commands::RdocCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "rdoc", "Generates RDoc for pre-installed gems",
+ version: Gem::Requirement.default,
+ include_rdoc: false, include_ri: true, overwrite: false
+
+ add_option("--all",
+ "Generate RDoc/RI documentation for all",
+ "installed gems") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("--[no-]rdoc",
+ "Generate RDoc HTML") do |value, options|
+ options[:include_rdoc] = value
+ end
+
+ add_option("--[no-]ri",
+ "Generate RI data") do |value, options|
+ options[:include_ri] = value
+ end
+
+ add_option("--[no-]overwrite",
+ "Overwrite installed documents") do |value, options|
+ options[:overwrite] = value
+ end
+
+ add_version_option
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME gem to generate documentation for (unless --all)"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --ri --no-overwrite"
+ end
+
+ def description # :nodoc:
+ <<-DESC
+The rdoc command builds documentation for installed gems. By default
+only documentation is built using rdoc, but additional types of
+documentation may be built through rubygems plugins and the
+Gem.post_installs hook.
+
+Use --overwrite to force rebuilding of documentation.
+ DESC
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [args]"
+ end
+
+ def execute
+ specs = if options[:all]
+ Gem::Specification.to_a
+ else
+ get_all_gem_names.flat_map do |name|
+ Gem::Specification.find_by_name name, options[:version]
+ end.uniq
+ end
+
+ if specs.empty?
+ alert_error "No matching gems found"
+ terminate_interaction 1
+ end
+
+ specs.each do |spec|
+ doc = Gem::RDoc.new spec, options[:include_rdoc], options[:include_ri]
+
+ doc.force = options[:overwrite]
+
+ if options[:overwrite]
+ FileUtils.rm_rf File.join(spec.doc_dir, "ri")
+ FileUtils.rm_rf File.join(spec.doc_dir, "rdoc")
+ end
+
+ doc.generate
+ end
+ end
+end
diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb
new file mode 100644
index 0000000000..23b9d7b3ba
--- /dev/null
+++ b/lib/rubygems/commands/rebuild_command.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "tmpdir"
+require_relative "../gemspec_helpers"
+require_relative "../package"
+
+class Gem::Commands::RebuildCommand < Gem::Command
+ include Gem::GemspecHelpers
+
+ def initialize
+ super "rebuild", "Attempt to reproduce a build of a gem."
+
+ add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options|
+ options[:diff] = true
+ end
+
+ add_option "--force", "Skip validation of the spec." do |_value, options|
+ options[:force] = true
+ end
+
+ add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options|
+ options[:strict] = true
+ end
+
+ add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options|
+ options[:source] = value
+ end
+
+ add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options|
+ options[:original_gem_file] = value
+ end
+
+ add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options|
+ options[:gemspec_file] = value
+ end
+
+ add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options|
+ options[:build_path] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEM_NAME gem name on gem server\n" \
+ "GEM_VERSION gem version you are attempting to rebuild"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The rebuild command allows you to (attempt to) reproduce a build of a gem
+from a ruby gemspec.
+
+This command assumes the gemspec can be built with the `gem build` command.
+If you use any of `gem build`, `rake build`, or`rake release` in the
+build/release process for a gem, it is a potential candidate.
+
+You will need to match the RubyGems version used, since this is included in
+the Gem metadata.
+
+If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will
+require more effort to reproduce a build. For example, it might require
+more precisely matched versions of Ruby and/or Bundler to be used.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM_NAME GEM_VERSION"
+ end
+
+ def execute
+ gem_name, gem_version = get_gem_name_and_version
+
+ old_dir, new_dir = prep_dirs
+
+ gem_filename = "#{gem_name}-#{gem_version}.gem"
+ old_file = File.join(old_dir, gem_filename)
+ new_file = File.join(new_dir, gem_filename)
+
+ if options[:original_gem_file]
+ FileUtils.copy_file(options[:original_gem_file], old_file)
+ else
+ download_gem(gem_name, gem_version, old_file)
+ end
+
+ rg_version = rubygems_version(old_file)
+ unless rg_version == Gem::VERSION
+ alert_error <<-EOF
+You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with.
+
+#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}.
+Gem files include the version of RubyGems used to build them.
+This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}.
+
+You're using RubyGems v#{Gem::VERSION}.
+
+Please install RubyGems v#{rg_version} and try again.
+ EOF
+ terminate_interaction 1
+ end
+
+ source_date_epoch = get_timestamp(old_file).to_s
+
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) }
+ else
+ build_gem(gem_name, source_date_epoch, new_file)
+ end
+
+ compare(source_date_epoch, old_file, new_file)
+ end
+
+ private
+
+ def sha256(file)
+ Digest::SHA256.hexdigest(Gem.read_binary(file))
+ end
+
+ def get_timestamp(file)
+ mtime = nil
+ File.open(file, Gem.binary_mode) do |f|
+ Gem::Package::TarReader.new(f) do |tar|
+ mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime }
+ end
+ end
+
+ mtime
+ end
+
+ def compare(source_date_epoch, old_file, new_file)
+ date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z")
+
+ old_hash = sha256(old_file)
+ new_hash = sha256(new_file)
+
+ say
+ say "Built at: #{date} (#{source_date_epoch})"
+ say "Original build saved to: #{old_file}"
+ say "Reproduced build saved to: #{new_file}"
+ say "Working directory: #{options[:build_path] || Dir.pwd}"
+ say
+ say "Hash comparison:"
+ say " #{old_hash}\t#{old_file}"
+ say " #{new_hash}\t#{new_file}"
+ say
+
+ if old_hash == new_hash
+ say "SUCCESS - original and rebuild hashes matched"
+ else
+ say "FAILURE - original and rebuild hashes did not match"
+ say
+
+ if options[:diff]
+ if system("diffoscope", old_file, new_file).nil?
+ alert_error "error: could not find `diffoscope` executable"
+ end
+ else
+ say "Pass --diff for more details (requires diffoscope to be installed)."
+ end
+
+ terminate_interaction 1
+ end
+ end
+
+ def prep_dirs
+ rebuild_dir = Dir.mktmpdir("gem_rebuild")
+ old_dir = File.join(rebuild_dir, "old")
+ new_dir = File.join(rebuild_dir, "new")
+
+ FileUtils.mkdir_p(old_dir)
+ FileUtils.mkdir_p(new_dir)
+
+ [old_dir, new_dir]
+ end
+
+ def get_gem_name_and_version
+ args = options[:args] || []
+ if args.length == 2
+ gem_name, gem_version = args
+ elsif args.length > 2
+ raise Gem::CommandLineError, "Too many arguments"
+ else
+ raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)"
+ end
+
+ [gem_name, gem_version]
+ end
+
+ def build_gem(gem_name, source_date_epoch, output_file)
+ gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec")
+
+ if gemspec
+ build_package(gemspec, source_date_epoch, output_file)
+ else
+ alert_error error_message(gem_name)
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec, source_date_epoch, output_file)
+ with_source_date_epoch(source_date_epoch) do
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ output_file
+ )
+ else
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+ end
+
+ def with_source_date_epoch(source_date_epoch)
+ old_sde = ENV["SOURCE_DATE_EPOCH"]
+ ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s
+
+ yield
+ ensure
+ ENV["SOURCE_DATE_EPOCH"] = old_sde
+ end
+
+ def error_message(gem_name)
+ if gem_name
+ "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
+ else
+ "Couldn't find a gemspec file in #{Dir.pwd}"
+ end
+ end
+
+ def download_gem(gem_name, gem_version, old_file)
+ # This code was based loosely off the `gem fetch` command.
+ version = "= #{gem_version}"
+ dep = Gem::Dependency.new gem_name, version
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ # There should never be more than one item in specs_and_sources,
+ # since we search for an exact version.
+ spec, source = specs_and_sources[0]
+
+ if spec.nil?
+ show_lookup_failure gem_name, version, errors, options[:domain]
+ terminate_interaction 1
+ end
+
+ download_path = source.download spec
+
+ FileUtils.move(download_path, old_file)
+
+ say "Downloaded #{gem_name} version #{gem_version} as #{old_file}."
+ end
+
+ def rubygems_version(gem_file)
+ Gem::Package.new(gem_file).spec.rubygems_version
+ end
+end
diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb
new file mode 100644
index 0000000000..50e161ac9b
--- /dev/null
+++ b/lib/rubygems/commands/search_command.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::SearchCommand < Gem::Command
+ include Gem::QueryUtils
+
+ def initialize
+ super "search", "Display remote gems whose name matches REGEXP",
+ domain: :remote, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
+
+ add_query_options
+ end
+
+ def arguments # :nodoc:
+ "REGEXP regexp to search for in gem name"
+ end
+
+ def defaults_str # :nodoc:
+ "--remote --no-details"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The search command displays remote gems whose name matches the given
+regexp.
+
+The --details option displays additional details from the gem but will
+take a little longer to complete as it must download the information
+individually from the index.
+
+To list local gems use the list command.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [REGEXP]"
+ end
+end
diff --git a/lib/rubygems/commands/server_command.rb b/lib/rubygems/commands/server_command.rb
new file mode 100644
index 0000000000..f1dde4aa02
--- /dev/null
+++ b/lib/rubygems/commands/server_command.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+unless defined? Gem::Commands::ServerCommand
+ class Gem::Commands::ServerCommand < Gem::Command
+ def initialize
+ super("server", "Starts up a web server that hosts the RDoc (requires rubygems-server)")
+ begin
+ Gem::Specification.find_by_name("rubygems-server").activate
+ rescue Gem::LoadError
+ # no-op
+ end
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The server command has been moved to the rubygems-server gem.
+ EOF
+ end
+
+ def execute
+ alert_error "Install the rubygems-server gem for the server command"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb
new file mode 100644
index 0000000000..175599967c
--- /dev/null
+++ b/lib/rubygems/commands/setup_command.rb
@@ -0,0 +1,667 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+##
+# Installs RubyGems itself. This command is ordinarily only available from a
+# RubyGems checkout or tarball.
+
+class Gem::Commands::SetupCommand < Gem::Command
+ HISTORY_HEADER = %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
+
+ add_option "--previous-version=VERSION",
+ "Previous version of RubyGems",
+ "Used for changelog processing" do |version, options|
+ options[:previous_version] = version
+ end
+
+ add_option "--prefix=PREFIX",
+ "Prefix path for installing RubyGems",
+ "Will not affect gem repository location" do |prefix, options|
+ options[:prefix] = File.expand_path prefix
+ end
+
+ add_option "--destdir=DESTDIR",
+ "Root directory to install RubyGems into",
+ "Mainly used for packaging RubyGems" do |destdir, options|
+ options[:destdir] = File.expand_path destdir
+ end
+
+ add_option "--[no-]vendor",
+ "Install into vendorlibdir not sitelibdir" do |vendor, options|
+ options[:site_or_vendor] = vendor ? "vendorlibdir" : "sitelibdir"
+ end
+
+ add_option "--[no-]format-executable",
+ "Makes `gem` match ruby",
+ "If Ruby is ruby18, gem will be gem18" do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option "--[no-]document [TYPES]", Array,
+ "Generate documentation for RubyGems",
+ "List the documentation types you wish to",
+ "generate. For example: rdoc,ri" do |value, options|
+ options[:document] = case value
+ when nil then %w[rdoc ri]
+ when false then []
+ else value
+ end
+ end
+
+ add_option "--[no-]rdoc",
+ "Generate RDoc documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "rdoc"
+ else
+ options[:document].delete "rdoc"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]ri",
+ "Generate RI documentation for RubyGems" do |value, options|
+ if value
+ options[:document] << "ri"
+ else
+ options[:document].delete "ri"
+ end
+
+ options[:document].uniq!
+ end
+
+ add_option "--[no-]regenerate-binstubs",
+ "Regenerate gem binstubs" do |value, options|
+ options[:regenerate_binstubs] = value
+ end
+
+ add_option "--[no-]regenerate-plugins",
+ "Regenerate gem plugins" do |value, options|
+ options[:regenerate_plugins] = value
+ end
+
+ add_option "-f", "--[no-]force",
+ "Forcefully overwrite binstubs" do |value, options|
+ options[:force] = value
+ end
+
+ add_option("-E", "--[no-]env-shebang",
+ "Rewrite executables with a shebang",
+ "of /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ @verbose = nil
+ end
+
+ def defaults_str # :nodoc:
+ "--format-executable --document ri --regenerate-binstubs"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+Installs RubyGems itself.
+
+RubyGems installs RDoc for itself in GEM_HOME. By default this is:
+ #{Gem.dir}
+
+If you prefer a different directory, set the GEM_HOME environment variable.
+
+RubyGems will install the gem command with a name matching ruby's
+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"}
+ EOF
+ end
+
+ module MakeDirs
+ def mkdir_p(path, **opts)
+ super
+ (@mkdirs ||= []) << path
+ end
+ end
+
+ def execute
+ @verbose = Gem.configuration.really_verbose
+
+ require "fileutils"
+ if Gem.configuration.really_verbose
+ extend FileUtils::Verbose
+ else
+ extend FileUtils
+ end
+ extend MakeDirs
+
+ lib_dir, bin_dir = make_destination_dirs
+ man_dir = generate_default_man_dir
+
+ install_lib lib_dir
+
+ install_executables bin_dir
+
+ remove_old_bin_files bin_dir
+
+ remove_old_lib_files lib_dir
+
+ # Can be removed one we drop support for bundler 2.2.3 (the last version installing man files to man_dir)
+ remove_old_man_files man_dir if man_dir && File.exist?(man_dir)
+
+ install_default_bundler_gem bin_dir
+
+ if mode = options[:dir_mode]
+ @mkdirs.uniq!
+ File.chmod(mode, @mkdirs)
+ end
+
+ say "RubyGems #{Gem::VERSION} installed"
+
+ regenerate_binstubs(bin_dir) if options[:regenerate_binstubs]
+ regenerate_plugins(bin_dir) if options[:regenerate_plugins]
+
+ uninstall_old_gemcutter
+
+ documentation_success = install_rdoc
+
+ say
+ if @verbose
+ say "-" * 78
+ say
+ end
+
+ if options[:previous_version].empty?
+ options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0")
+ end
+
+ options[:previous_version] = Gem::Version.new(options[:previous_version])
+
+ show_release_notes
+
+ say
+ say "-" * 78
+ say
+
+ say "RubyGems installed the following executables:"
+ say bin_file_names.map {|name| "\t#{name}\n" }
+ say
+
+ unless bin_file_names.grep(/#{File::SEPARATOR}gem$/)
+ say "If `gem` was installed by a previous RubyGems installation, you may need"
+ say "to remove it by hand."
+ say
+ end
+
+ if documentation_success
+ if options[:document].include? "rdoc"
+ say "Rdoc documentation was installed. You may now invoke:"
+ say " gem server"
+ say "and then peruse beautifully formatted documentation for your gems"
+ say "with your web browser."
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+
+ if options[:document].include? "ri"
+ say "Ruby Interactive (ri) documentation was installed. ri is kind of like man "
+ say "pages for Ruby libraries. You may access it like this:"
+ say " ri Classname"
+ say " ri Classname.class_method"
+ say " ri Classname#instance_method"
+ say "If you do not wish to install this documentation in the future, use the"
+ say "--no-document flag, or set it as the default in your ~/.gemrc file. See"
+ say "'gem help env' for details."
+ say
+ end
+ end
+ end
+
+ def install_executables(bin_dir)
+ prog_mode = options[:prog_mode] || 0o755
+
+ executables = { "gem" => "exe" }
+ executables.each do |tool, path|
+ say "Installing #{tool} executable" if @verbose
+
+ Dir.chdir path do
+ bin_file = "gem"
+
+ require "tmpdir"
+
+ dest_file = target_bin_path(bin_dir, bin_file)
+ bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}"
+
+ begin
+ bin = File.readlines bin_file
+ bin[0] = shebang
+
+ File.open bin_tmp_file, "w" do |fp|
+ fp.puts bin.join
+ end
+
+ install bin_tmp_file, dest_file, mode: prog_mode
+ bin_file_names << dest_file
+ ensure
+ rm bin_tmp_file
+ end
+
+ next unless Gem.win_platform?
+
+ begin
+ bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat"
+
+ File.open bin_cmd_file, "w" do |file|
+ file.puts <<-TEXT
+ @ECHO OFF
+ @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
+ TEXT
+ end
+
+ install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode
+ ensure
+ rm bin_cmd_file
+ end
+ end
+ end
+ end
+
+ def shebang
+ if options[:env_shebang]
+ ruby_name = RbConfig::CONFIG["ruby_install_name"]
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ "#!#{@env_path} #{ruby_name}\n"
+ else
+ "#!#{Gem.ruby}\n"
+ end
+ end
+
+ def install_lib(lib_dir)
+ libs = { "RubyGems" => "lib" }
+ libs["Bundler"] = "bundler/lib"
+ libs.each do |tool, path|
+ say "Installing #{tool}" if @verbose
+
+ lib_files = files_in path
+
+ Dir.chdir path do
+ install_file_list(lib_files, lib_dir)
+ end
+ end
+ end
+
+ def install_rdoc
+ gem_doc_dir = File.join Gem.dir, "doc"
+ rubygems_name = "rubygems-#{Gem::VERSION}"
+ rubygems_doc_dir = File.join gem_doc_dir, rubygems_name
+
+ begin
+ Gem.ensure_gem_subdirectories Gem.dir
+ rescue SystemCallError
+ # ignore
+ end
+
+ if File.writable?(gem_doc_dir) &&
+ (!File.exist?(rubygems_doc_dir) ||
+ File.writable?(rubygems_doc_dir))
+ say "Removing old RubyGems RDoc and ri" if @verbose
+ Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir|
+ rm_rf dir
+ end
+
+ require_relative "../rdoc"
+
+ return false unless defined?(Gem::RDoc)
+
+ fake_spec = Gem::Specification.new "rubygems", Gem::VERSION
+ def fake_spec.full_gem_path
+ File.expand_path "../../..", __dir__
+ end
+
+ generate_ri = options[:document].include? "ri"
+ generate_rdoc = options[:document].include? "rdoc"
+
+ rdoc = Gem::RDoc.new fake_spec, generate_rdoc, generate_ri
+ rdoc.generate
+
+ return true
+ elsif @verbose
+ say "Skipping RDoc generation, #{gem_doc_dir} not writable"
+ say "Set the GEM_HOME environment variable if you want RDoc generated"
+ end
+
+ false
+ end
+
+ def install_default_bundler_gem(bin_dir)
+ current_default_spec = Gem::Specification.default_stubs.find {|s| s.name == "bundler" }
+ specs_dir = if current_default_spec && default_dir == Gem.default_dir
+ all_specs_current_version = Gem::Specification.stubs.select {|s| s.full_name == current_default_spec.full_name }
+
+ Gem::Specification.remove_spec current_default_spec
+ loaded_from = current_default_spec.loaded_from
+ File.delete(loaded_from)
+
+ # Remove previous default gem executables if they were not shadowed by a regular gem
+ FileUtils.rm_rf current_default_spec.full_gem_path if all_specs_current_version.size == 1
+
+ File.dirname(loaded_from)
+ else
+ target_specs_dir = File.join(default_dir, "specifications", "default")
+ mkdir_p target_specs_dir, mode: 0o755
+ target_specs_dir
+ end
+
+ 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", gemspec_path)
+ if File.file? normal_gemspec
+ File.delete normal_gemspec
+ end
+
+ # 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) == full_name }.
+ each {|default_gem| rm_r File.join(bundler_spec.gems_dir, default_gem) }
+ end
+
+ require_relative "../installer"
+
+ Dir.chdir("bundler") do
+ built_gem = Gem::Package.build(new_bundler_spec)
+ begin
+ installer = Gem::Installer.at(
+ built_gem,
+ env_shebang: options[:env_shebang],
+ format_executable: options[:format_executable],
+ force: options[:force],
+ bin_dir: bin_dir,
+ install_dir: default_dir,
+ wrappers: true
+ )
+ # We need to install only executable and default spec files.
+ # lib/bundler.rb and lib/bundler/* are available under the site_ruby directory.
+ installer.extract_bin
+ installer.generate_bin
+ installer.write_default_spec
+ ensure
+ FileUtils.rm_f built_gem
+ end
+ end
+
+ new_bundler_spec.executables.each {|executable| bin_file_names << target_bin_path(bin_dir, executable) }
+
+ say "Bundler #{new_bundler_spec.version} installed"
+ end
+
+ def make_destination_dirs
+ lib_dir, bin_dir = Gem.default_rubygems_dirs
+
+ unless lib_dir
+ lib_dir, bin_dir = generate_default_dirs
+ end
+
+ mkdir_p lib_dir, mode: 0o755
+ mkdir_p bin_dir, mode: 0o755
+
+ [lib_dir, bin_dir]
+ end
+
+ def generate_default_man_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ man_dir = RbConfig::CONFIG["mandir"]
+ return unless man_dir
+ else
+ man_dir = File.join prefix, "man"
+ end
+
+ prepend_destdir_if_present(man_dir)
+ end
+
+ def generate_default_dirs
+ prefix = options[:prefix]
+ site_or_vendor = options[:site_or_vendor]
+
+ if prefix.empty?
+ lib_dir = RbConfig::CONFIG[site_or_vendor]
+ bin_dir = RbConfig::CONFIG["bindir"]
+ else
+ lib_dir = File.join prefix, "lib"
+ bin_dir = File.join prefix, "bin"
+ end
+
+ [prepend_destdir_if_present(lib_dir), prepend_destdir_if_present(bin_dir)]
+ end
+
+ def files_in(dir)
+ Dir.chdir dir do
+ Dir.glob(File.join("**", "*"), File::FNM_DOTMATCH).
+ select {|f| !File.directory?(f) }
+ end
+ end
+
+ def remove_old_bin_files(bin_dir)
+ old_bin_files = {
+ "gem_mirror" => "gem mirror",
+ "gem_server" => "gem server",
+ "gemlock" => "gem lock",
+ "gemri" => "ri",
+ "gemwhich" => "gem which",
+ "index_gem_repository.rb" => "gem generate_index",
+ }
+
+ old_bin_files.each do |old_bin_file, new_name|
+ old_bin_path = File.join bin_dir, old_bin_file
+ next unless File.exist? old_bin_path
+
+ deprecation_message = "`#{old_bin_file}` has been deprecated. Use `#{new_name}` instead."
+
+ File.open old_bin_path, "w" do |fp|
+ fp.write <<-EOF
+#!#{Gem.ruby}
+
+abort "#{deprecation_message}"
+ EOF
+ end
+
+ next unless Gem.win_platform?
+
+ File.open "#{old_bin_path}.bat", "w" do |fp|
+ fp.puts %(@ECHO.#{deprecation_message})
+ end
+ end
+ end
+
+ def remove_old_lib_files(lib_dir)
+ lib_dirs = { File.join(lib_dir, "rubygems") => "lib/rubygems" }
+ lib_dirs[File.join(lib_dir, "bundler")] = "bundler/lib/bundler"
+ lib_dirs.each do |old_lib_dir, new_lib_dir|
+ lib_files = files_in(new_lib_dir)
+
+ old_lib_files = files_in(old_lib_dir)
+
+ to_remove = old_lib_files - lib_files
+
+ gauntlet_rubygems = File.join(lib_dir, "gauntlet_rubygems.rb")
+ to_remove << gauntlet_rubygems if File.exist? gauntlet_rubygems
+
+ to_remove.delete_if do |file|
+ file.start_with? "defaults"
+ end
+
+ remove_file_list(to_remove, old_lib_dir)
+ end
+ end
+
+ def remove_old_man_files(old_man_dir)
+ old_man1_dir = "#{old_man_dir}/man1"
+
+ if File.exist?(old_man1_dir)
+ man1_to_remove = Dir.chdir(old_man1_dir) { Dir["bundle*.1{,.txt,.ronn}"] }
+
+ remove_file_list(man1_to_remove, old_man1_dir)
+ end
+
+ old_man5_dir = "#{old_man_dir}/man5"
+
+ if File.exist?(old_man5_dir)
+ man5_to_remove = Dir.chdir(old_man5_dir) { Dir["gemfile.5{,.txt,.ronn}"] }
+
+ remove_file_list(man5_to_remove, old_man5_dir)
+ end
+ end
+
+ def show_release_notes
+ release_notes = File.join Dir.pwd, "CHANGELOG.md"
+
+ release_notes =
+ if File.exist? release_notes
+ history = File.read release_notes
+
+ history.force_encoding Encoding::UTF_8
+
+ text = history.split(HISTORY_HEADER)
+ text.shift # correct an off-by-one generated by split
+ version_lines = history.scan(HISTORY_HEADER)
+ versions = history.scan(VERSION_MATCHER).flatten.map do |x|
+ Gem::Version.new(x)
+ end
+
+ history_string = ""
+
+ until versions.length == 0 ||
+ versions.shift <= options[:previous_version] do
+ history_string += version_lines.shift + text.shift
+ end
+
+ history_string
+ else
+ "Oh-no! Unable to find release notes!"
+ end
+
+ say release_notes
+ end
+
+ def uninstall_old_gemcutter
+ require_relative "../uninstaller"
+
+ ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true,
+ version: "< 0.4")
+ ui.uninstall
+ rescue Gem::InstallError
+ end
+
+ def regenerate_binstubs(bindir)
+ require_relative "pristine_command"
+ say "Regenerating binstubs"
+
+ args = %w[--all --only-executables --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ if options[:env_shebang]
+ args << "--env-shebang"
+ end
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ def regenerate_plugins(bindir)
+ require_relative "pristine_command"
+ say "Regenerating plugins"
+
+ args = %w[--all --only-plugins --silent]
+ args << "--bindir=#{bindir}"
+ args << "--install-dir=#{default_dir}"
+
+ command = Gem::Commands::PristineCommand.new
+ command.invoke(*args)
+ end
+
+ private
+
+ def default_dir
+ prefix = options[:prefix]
+
+ if prefix.empty?
+ dir = Gem.default_dir
+ else
+ dir = prefix
+ end
+
+ prepend_destdir_if_present(dir)
+ end
+
+ def prepend_destdir_if_present(path)
+ destdir = options[:destdir]
+ return path if destdir.empty?
+
+ File.join(options[:destdir], path.gsub(/^[a-zA-Z]:/, ""))
+ end
+
+ def install_file_list(files, dest_dir)
+ files.each do |file|
+ install_file file, dest_dir
+ end
+ end
+
+ def install_file(file, dest_dir)
+ dest_file = File.join dest_dir, file
+ dest_dir = File.dirname dest_file
+ unless File.directory? dest_dir
+ mkdir_p dest_dir, mode: 0o755
+ end
+
+ install file, dest_file, mode: options[:data_mode] || 0o644
+ end
+
+ def remove_file_list(files, dir)
+ Dir.chdir dir do
+ files.each do |file|
+ FileUtils.rm_f file
+
+ warn "unable to remove old file #{file} please remove it by hand" if
+ File.exist? file
+ end
+ end
+ end
+
+ def target_bin_path(bin_dir, bin_file)
+ bin_file_formatted = if options[:format_executable]
+ Gem.default_exec_format % bin_file
+ else
+ bin_file
+ end
+ File.join bin_dir, bin_file_formatted
+ end
+
+ def bin_file_names
+ @bin_file_names ||= []
+ end
+end
diff --git a/lib/rubygems/commands/signin_command.rb b/lib/rubygems/commands/signin_command.rb
new file mode 100644
index 0000000000..0f77908c5b
--- /dev/null
+++ b/lib/rubygems/commands/signin_command.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::SigninCommand < Gem::Command
+ include Gem::GemcutterUtilities
+
+ def initialize
+ super "signin", "Sign in to any gemcutter-compatible host. "\
+ "It defaults to https://rubygems.org"
+
+ add_option("--host HOST", "Push to another gemcutter-compatible host") do |value, options|
+ options[:host] = value
+ end
+
+ add_otp_option
+ end
+
+ def description # :nodoc:
+ "The signin command executes host sign in for a push server (the default is"\
+ " https://rubygems.org). The host can be provided with the host flag or can"\
+ " be inferred from the provided gem. Host resolution matches the resolution"\
+ " strategy for the push command."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ sign_in options[:host]
+ end
+end
diff --git a/lib/rubygems/commands/signout_command.rb b/lib/rubygems/commands/signout_command.rb
new file mode 100644
index 0000000000..bdd01e4393
--- /dev/null
+++ b/lib/rubygems/commands/signout_command.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::SignoutCommand < Gem::Command
+ def initialize
+ super "signout", "Sign out from all the current sessions."
+ end
+
+ def description # :nodoc:
+ "The `signout` command is used to sign out from all current sessions,"\
+ " allowing you to sign in using a different set of credentials."
+ end
+
+ def usage # :nodoc:
+ program_name
+ end
+
+ def execute
+ credentials_path = Gem.configuration.credentials_path
+
+ if !File.exist?(credentials_path)
+ alert_error "You are not currently signed in."
+ elsif !File.writable?(credentials_path)
+ alert_error "File '#{Gem.configuration.credentials_path}' is read-only."\
+ " Please make sure it is writable."
+ else
+ Gem.configuration.unset_api_key!
+ say "You have successfully signed out from all sessions."
+ end
+ end
+end
diff --git a/lib/rubygems/commands/sources_command.rb b/lib/rubygems/commands/sources_command.rb
new file mode 100644
index 0000000000..b399af2bd3
--- /dev/null
+++ b/lib/rubygems/commands/sources_command.rb
@@ -0,0 +1,348 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../remote_fetcher"
+require_relative "../spec_fetcher"
+require_relative "../local_remote_options"
+
+class Gem::Commands::SourcesCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+
+ def initialize
+ require "fileutils"
+
+ super "sources",
+ "Manage the sources and cache file RubyGems uses to search for gems"
+
+ add_option "-a", "--add SOURCE_URI", "Add source" do |value, options|
+ options[:add] = value
+ end
+
+ add_option "--append SOURCE_URI", "Append source (can be used multiple times)" do |value, options|
+ options[:append] = value
+ end
+
+ add_option "-p", "--prepend SOURCE_URI", "Prepend source (can be used multiple times)" do |value, options|
+ options[:prepend] = value
+ end
+
+ add_option "-l", "--list", "List sources" do |value, options|
+ options[:list] = value
+ end
+
+ add_option "-r", "--remove SOURCE_URI", "Remove source" do |value, options|
+ options[:remove] = value
+ end
+
+ add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options|
+ options[:clear_all] = value
+ end
+
+ add_option "-u", "--update", "Update source cache" do |value, options|
+ options[:update] = value
+ end
+
+ add_option "-f", "--[no-]force", "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options|
+ options[:force] = value
+ end
+
+ add_proxy_option
+ end
+
+ def add_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
+
+ begin
+ if Gem.sources.include? source
+ say "source #{source_uri} already present in the cache"
+ else
+ source.load_specs :released
+ Gem.sources << source
+ Gem.configuration.write
+
+ say "#{source_uri} added to sources"
+ end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
+ end
+ end
+
+ def append_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
+
+ begin
+ source.load_specs :released
+ was_present = Gem.sources.include?(source)
+ Gem.sources.append source
+ Gem.configuration.write
+
+ if was_present
+ say "#{source_uri} moved to end of sources"
+ else
+ say "#{source_uri} added to sources"
+ end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
+ end
+ end
+
+ def prepend_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
+
+ begin
+ source.load_specs :released
+ was_present = Gem.sources.include?(source)
+ Gem.sources.prepend source
+ Gem.configuration.write
+
+ if was_present
+ say "#{source_uri} moved to top of sources"
+ else
+ say "#{source_uri} added to sources"
+ end
+ rescue Gem::URI::Error, ArgumentError
+ say "#{source_uri} is not a URI"
+ terminate_interaction 1
+ rescue Gem::RemoteFetcher::FetchError => e
+ say "Error fetching #{Gem::Uri.redact(source.uri)}:\n\t#{e.message}"
+ terminate_interaction 1
+ end
+ end
+
+ def check_typo_squatting(source)
+ if source.typo_squatting?("rubygems.org")
+ question = <<-QUESTION.chomp
+#{source.uri} is too similar to https://rubygems.org
+
+Do you want to add this source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ end
+ end
+
+ def normalize_source_uri(source_uri) # :nodoc:
+ # Ensure the source URI has a trailing slash for proper RFC 2396 path merging
+ # Without a trailing slash, the last path segment is treated as a file and removed
+ # during relative path resolution (e.g., "/blish" + "gems/foo.gem" = "/gems/foo.gem")
+ # With a trailing slash, it's treated as a directory (e.g., "/blish/" + "gems/foo.gem" = "/blish/gems/foo.gem")
+ uri = Gem::URI.parse(source_uri)
+ uri.path = uri.path.gsub(%r{/+\z}, "") + "/" if uri.path && !uri.path.empty?
+ uri.to_s
+ rescue Gem::URI::Error
+ # If parsing fails, return the original URI and let later validation handle it
+ source_uri
+ end
+
+ def check_rubygems_https(source_uri) # :nodoc:
+ uri = Gem::URI source_uri
+
+ 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}
+
+Do you want to add this insecure source?
+ QUESTION
+
+ terminate_interaction 1 unless options[:force] || ask_yes_no(question)
+ end
+ end
+
+ def clear_all # :nodoc:
+ path = Gem.spec_cache_dir
+ FileUtils.rm_rf path
+
+ if File.exist? path
+ if File.writable? path
+ say "*** Unable to remove source cache ***"
+ else
+ say "*** Unable to remove source cache (write protected) ***"
+ end
+
+ terminate_interaction 1
+ else
+ say "*** Removed specs cache ***"
+ end
+ end
+
+ def defaults_str # :nodoc:
+ "--list"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+RubyGems fetches gems from the sources you have configured (stored in your
+~/.gemrc).
+
+The default source is https://rubygems.org, but you may have other sources
+configured. This guide will help you update your sources or configure
+yourself to use your own gem server.
+
+Without any arguments the sources lists your currently configured sources:
+
+ $ gem sources
+ *** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***
+
+ https://rubygems.org
+
+This may list multiple sources or non-rubygems sources. You probably
+configured them before or have an old `~/.gemrc`. If you have sources you
+do not recognize you should remove them.
+
+RubyGems has been configured to serve gems via the following URLs through
+its history:
+
+* http://gems.rubyforge.org (RubyGems 1.3.5 and earlier)
+* http://rubygems.org (RubyGems 1.3.6 through 1.8.30, and 2.0.0)
+* https://rubygems.org (RubyGems 2.0.1 and newer)
+
+Since all of these sources point to the same set of gems you only need one
+of them in your list. https://rubygems.org is recommended as it brings the
+protections of an SSL connection to gem downloads.
+
+To add a private gem source use the --prepend argument to insert it before
+the default source. This is usually the best place for private gem sources:
+
+ $ gem sources --prepend https://my.private.source
+ https://my.private.source added to sources
+
+RubyGems will check to see if gems can be installed from the source given
+before it is added.
+
+To add or move a source after all other sources, use --append:
+
+ $ gem sources --append https://rubygems.org
+ https://rubygems.org moved to end of sources
+
+To remove a source use the --remove argument:
+
+ $ gem sources --remove https://my.private.source/
+ https://my.private.source/ removed from sources
+
+ EOF
+ end
+
+ def list # :nodoc:
+ if configured_sources
+ header = "*** CURRENT SOURCES ***"
+ list = configured_sources
+ else
+ header = "*** NO CONFIGURED SOURCES, DEFAULT SOURCES LISTED BELOW ***"
+ list = Gem.sources
+ end
+
+ say header
+ say
+
+ list.each do |src|
+ say src
+ end
+ end
+
+ def list? # :nodoc:
+ !(options[:add] ||
+ options[:prepend] ||
+ options[:append] ||
+ options[:clear_all] ||
+ options[:remove] ||
+ options[:update])
+ end
+
+ def execute
+ clear_all if options[:clear_all]
+
+ add_source options[:add] if options[:add]
+
+ prepend_source options[:prepend] if options[:prepend]
+
+ append_source options[:append] if options[:append]
+
+ remove_source options[:remove] if options[:remove]
+
+ update if options[:update]
+
+ list if list?
+ end
+
+ def remove_source(source_uri) # :nodoc:
+ source = build_source(source_uri)
+ source_uri = source.uri.to_s
+
+ if configured_sources&.include? source
+ Gem.sources.delete source
+ Gem.configuration.write
+
+ if default_sources.include?(source) && configured_sources.one?
+ alert_warning "Removing a default source when it is the only source has no effect. Add a different source to #{config_file_name} if you want to stop using it as a source."
+ else
+ say "#{source_uri} removed from sources"
+ end
+ elsif configured_sources
+ say "source #{source_uri} cannot be removed because it's not present in #{config_file_name}"
+ else
+ say "source #{source_uri} cannot be removed because there are no configured sources in #{config_file_name}"
+ end
+ end
+
+ def update # :nodoc:
+ Gem.sources.each_source do |src|
+ src.load_specs :released
+ src.load_specs :latest
+ end
+
+ say "source cache successfully updated"
+ end
+
+ def remove_cache_file(desc, path) # :nodoc:
+ FileUtils.rm_rf path
+
+ if !File.exist?(path)
+ say "*** Removed #{desc} source cache ***"
+ elsif !File.writable?(path)
+ say "*** Unable to remove #{desc} source cache (write protected) ***"
+ else
+ say "*** Unable to remove #{desc} source cache ***"
+ end
+ end
+
+ private
+
+ def default_sources
+ Gem::SourceList.from(Gem.default_sources)
+ end
+
+ def configured_sources
+ return @configured_sources if defined?(@configured_sources)
+
+ configuration_sources = Gem.configuration.sources
+ @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources
+ end
+
+ def config_file_name
+ Gem.configuration.config_file_name
+ end
+
+ def build_source(source_uri)
+ source_uri = normalize_source_uri(source_uri)
+ Gem::Source.new(source_uri)
+ end
+
+ def build_new_source(source_uri)
+ source = build_source(source_uri)
+ check_rubygems_https(source.uri.to_s)
+ check_typo_squatting(source)
+ source
+ end
+end
diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb
new file mode 100644
index 0000000000..15e543f1a6
--- /dev/null
+++ b/lib/rubygems/commands/specification_command.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../package"
+
+class Gem::Commands::SpecificationCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def initialize
+ Gem.load_yaml
+
+ super "specification", "Display gem specification (in 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|
+ options[:all] = true
+ end
+
+ add_option("--ruby", "Output ruby format") do |_value, options|
+ options[:format] = :ruby
+ end
+
+ add_option("--yaml", "Output YAML format") do |_value, options|
+ options[:format] = :yaml
+ end
+
+ add_option("--marshal", "Output Marshal format") do |_value, options|
+ options[:format] = :marshal
+ end
+
+ add_local_remote_options
+ end
+
+ def arguments # :nodoc:
+ <<-ARGS
+GEM_OR_FILE gem name or a .gem file to show the gemspec for
+FIELD name of gemspec field to show
+ ARGS
+ end
+
+ def defaults_str # :nodoc:
+ "--local --version '#{Gem::Requirement.default}' --yaml"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The specification command allows you to extract the specification from
+a gem for examination.
+
+The specification can be output in YAML, ruby or Marshal formats.
+
+Specific fields in the specification can be extracted in YAML format:
+
+ $ gem spec rake summary
+ --- Ruby based make-like utility.
+ ...
+
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} [GEM_OR_FILE] [FIELD]"
+ end
+
+ def execute
+ specs = []
+ gem = options[:args].shift
+
+ unless gem
+ raise Gem::CommandLineError,
+ "Please specify a gem name or a .gem file on the command line"
+ end
+
+ case v = options[:version]
+ when String
+ req = Gem::Requirement.create v
+ when Gem::Requirement
+ req = v
+ else
+ raise Gem::CommandLineError, "Unsupported version type: '#{v}'"
+ end
+
+ if !req.none? && options[:all]
+ alert_error "Specify --all or -v, not both"
+ terminate_interaction 1
+ end
+
+ if options[:all]
+ dep = Gem::Dependency.new gem
+ else
+ dep = Gem::Dependency.new gem, req
+ end
+
+ field = get_one_optional_argument
+
+ raise Gem::CommandLineError, "--ruby and FIELD are mutually exclusive" if
+ field && options[:format] == :ruby
+
+ if local?
+ if File.exist? gem
+ begin
+ specs << Gem::Package.new(gem).spec
+ rescue StandardError
+ nil
+ end
+ end
+
+ if specs.empty?
+ specs.push(*dep.matching_specs)
+ end
+ end
+
+ if remote?
+ dep.prerelease = options[:prerelease]
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ specs.push(*found.map {|spec,| spec })
+ end
+
+ if specs.empty?
+ alert_error "No gem matching '#{dep}' found"
+ terminate_interaction 1
+ end
+
+ platform = get_platform_from_requirements(options)
+
+ if platform
+ specs = specs.select {|s| s.platform.to_s == platform }
+ end
+
+ unless options[:all]
+ 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 Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s)
+ end
+
+ say "\n"
+ end
+ end
+end
diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb
new file mode 100644
index 0000000000..0be2b85159
--- /dev/null
+++ b/lib/rubygems/commands/stale_command.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+
+class Gem::Commands::StaleCommand < Gem::Command
+ def initialize
+ super("stale", "List gems along with access times")
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The stale command lists the latest access time for all the files in your
+installed gems.
+
+You can use this command to discover gems and gem versions you are no
+longer using.
+ EOF
+ end
+
+ def usage # :nodoc:
+ program_name.to_s
+ end
+
+ def execute
+ gem_to_atime = {}
+ Gem::Specification.each do |spec|
+ name = spec.full_name
+ Dir["#{spec.full_gem_path}/**/*.*"].each do |file|
+ next if File.directory?(file)
+ stat = File.stat(file)
+ gem_to_atime[name] ||= stat.atime
+ gem_to_atime[name] = stat.atime if gem_to_atime[name] < stat.atime
+ end
+ end
+
+ gem_to_atime.sort_by {|_, atime| atime }.each do |name, atime|
+ 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
new file mode 100644
index 0000000000..3c26074f93
--- /dev/null
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -0,0 +1,204 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../uninstaller"
+require "fileutils"
+
+##
+# Gem uninstaller command line tool
+#
+# See `gem help uninstall`
+
+class Gem::Commands::UninstallCommand < Gem::Command
+ include Gem::VersionOption
+
+ def initialize
+ super "uninstall", "Uninstall gems from the local repository",
+ version: Gem::Requirement.default, user_install: true,
+ check_dev: false, vendor: false
+
+ add_option("-a", "--[no-]all",
+ "Uninstall all matching versions") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("-I", "--[no-]ignore-dependencies",
+ "Ignore dependency requirements while",
+ "uninstalling") do |value, options|
+ options[:ignore] = value
+ end
+
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: false)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("-x", "--[no-]executables",
+ "Uninstall applicable executables without",
+ "confirmation") do |value, options|
+ options[:executables] = value
+ end
+
+ add_option("-i", "--install-dir DIR",
+ "Directory to uninstall gem from") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option("-n", "--bindir DIR",
+ "Directory to remove executables from") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_option("--[no-]user-install",
+ "Uninstall from user's home directory",
+ "in addition to GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ add_option("--[no-]format-executable",
+ "Assume executable names match Ruby's prefix and suffix.") do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option("--[no-]force",
+ "Uninstall all versions of the named gems",
+ "ignoring dependencies") do |value, options|
+ options[:force] = value
+ end
+
+ add_option("--[no-]abort-on-dependent",
+ "Prevent uninstalling gems that are",
+ "depended on by other gems.") do |value, options|
+ options[:abort_on_dependent] = value
+ end
+
+ add_version_option
+ add_platform_option
+
+ add_option("--vendor",
+ "Uninstall gem from the vendor directory.",
+ "Only for use by gem repackagers.") do |_value, options|
+ unless Gem.vendor_dir
+ raise Gem::OptionParser::InvalidOption.new "your platform is not supported"
+ end
+
+ alert_warning "Use your OS package manager to uninstall vendor gems"
+ options[:vendor] = true
+ options[:install_dir] = Gem.vendor_dir
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to uninstall"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}' --no-force " \
+ "--user-install"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The uninstall command removes a previously installed gem.
+
+RubyGems will ask for confirmation if you are attempting to uninstall a gem
+that is a dependency of an existing gem. You can use the
+--ignore-dependencies option to skip this check.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def check_version # :nodoc:
+ if options[:version] != Gem::Requirement.default &&
+ get_all_gem_names.size > 1
+ alert_error "Can't use --version with multiple gems. You can specify multiple gems with" \
+ " version requirements using `gem uninstall 'my_gem:1.0.0' 'my_other_gem:>=2'`"
+ terminate_interaction 1
+ end
+ end
+
+ 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]
+ uninstall_all
+ else
+ uninstall_specific
+ end
+ end
+
+ def uninstall_all
+ specs = Gem::Specification.reject(&:default_gem?)
+
+ specs.each do |spec|
+ options[:version] = spec.version
+ uninstall_gem spec.name
+ end
+
+ alert "Uninstalled all gems in #{options[:install_dir] || Gem.dir}"
+ end
+
+ def uninstall_specific
+ deplist = Gem::DependencyList.new
+ original_gem_version = {}
+
+ get_all_gem_names_and_versions.each do |name, version|
+ original_gem_version[name] = version || options[:version]
+
+ gem_specs = Gem::Specification.find_all_by_name(name, original_gem_version[name])
+
+ if gem_specs.empty?
+ say("Gem '#{name}' is not installed")
+ else
+ gem_specs.reject!(&:default_gem?) if gem_specs.size > 1
+
+ gem_specs.each do |spec|
+ deplist.add spec
+ end
+ end
+ end
+
+ deps = deplist.strongly_connected_components.flatten.reverse
+
+ gems_to_uninstall = {}
+
+ deps.each do |dep|
+ if original_gem_version[dep.name] == Gem::Requirement.default
+ next if gems_to_uninstall[dep.name]
+ gems_to_uninstall[dep.name] = true
+ else
+ options[:version] = dep.version
+ end
+
+ uninstall_gem(dep.name)
+ end
+ end
+
+ def uninstall_gem(gem_name)
+ uninstall(gem_name)
+ rescue Gem::GemNotInHomeException => e
+ spec = e.spec
+ alert("In order to remove #{spec.name}, please execute:\n" \
+ "\tgem uninstall #{spec.name} --install-dir=#{spec.base_dir}")
+ rescue Gem::UninstallError => e
+ spec = e.spec
+ alert_error("Error: unable to successfully uninstall '#{spec.name}' which is " \
+ "located at '#{spec.full_gem_path}'. This is most likely because" \
+ "the current user does not have the appropriate permissions")
+ terminate_interaction 1
+ end
+
+ def uninstall(gem_name)
+ Gem::Uninstaller.new(gem_name, options).uninstall
+ end
+end
diff --git a/lib/rubygems/commands/unpack_command.rb b/lib/rubygems/commands/unpack_command.rb
new file mode 100644
index 0000000000..c2fc720297
--- /dev/null
+++ b/lib/rubygems/commands/unpack_command.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../security_option"
+require_relative "../remote_fetcher"
+require_relative "../package"
+
+# forward-declare
+
+module Gem::Security # :nodoc:
+ class Policy # :nodoc:
+ end
+end
+
+class Gem::Commands::UnpackCommand < Gem::Command
+ include Gem::VersionOption
+ include Gem::SecurityOption
+
+ def initialize
+ require "fileutils"
+
+ super "unpack", "Unpack an installed gem to the current directory",
+ version: Gem::Requirement.default,
+ target: Dir.pwd
+
+ add_option("--target=DIR",
+ "target directory for unpacking") do |value, options|
+ options[:target] = value
+ end
+
+ add_option("--spec", "unpack the gem specification") do |_value, options|
+ options[:spec] = true
+ end
+
+ add_security_option
+ add_version_option
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to unpack"
+ end
+
+ def defaults_str # :nodoc:
+ "--version '#{Gem::Requirement.default}'"
+ end
+
+ def description
+ <<-EOF
+The unpack command allows you to examine the contents of a gem or modify
+them to help diagnose a bug.
+
+You can add the contents of the unpacked gem to the load path using the
+RUBYLIB environment variable or -I:
+
+ $ gem unpack my_gem
+ Unpacked gem: '.../my_gem-1.0'
+ [edit my_gem-1.0/lib/my_gem.rb]
+ $ ruby -Imy_gem-1.0/lib -S other_program
+
+You can repackage an unpacked gem using the build command. See the build
+command help for an example.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME"
+ end
+
+ #--
+ # TODO: allow, e.g., 'gem unpack rake-0.3.1'. Find a general solution for
+ # this, so that it works for uninstall as well. (And check other commands
+ # at the same time.)
+
+ def execute
+ security_policy = options[:security_policy]
+
+ get_all_gem_names.each do |name|
+ dependency = Gem::Dependency.new name, options[:version]
+ path = get_path dependency
+
+ unless path
+ alert_error "Gem '#{name}' not installed nor fetchable."
+ next
+ end
+
+ if @options[:spec]
+ spec, metadata = Gem::Package.raw_spec(path, security_policy)
+
+ if metadata.nil?
+ alert_error "--spec is unsupported on '#{name}' (old format gem)"
+ next
+ end
+
+ spec_file = File.basename spec.spec_file
+
+ FileUtils.mkdir_p @options[:target] if @options[:target]
+
+ destination = if @options[:target]
+ File.join @options[:target], spec_file
+ else
+ spec_file
+ end
+
+ File.open destination, "w" do |io|
+ io.write metadata
+ end
+ else
+ basename = File.basename path, ".gem"
+ target_dir = File.expand_path basename, options[:target]
+
+ package = Gem::Package.new path, security_policy
+ package.extract_files target_dir
+
+ say "Unpacked gem: '#{target_dir}'"
+ end
+ end
+ end
+
+ ##
+ #
+ # Find cached filename in Gem.path. Returns nil if the file cannot be found.
+ #
+ #--
+ # TODO: see comments in get_path() about general service.
+
+ def find_in_cache(filename)
+ Gem.path.each do |path|
+ this_path = File.join(path, "cache", filename)
+ return this_path if File.exist? this_path
+ end
+
+ nil
+ end
+
+ ##
+ # Return the full path to the cached gem file matching the given
+ # name and version requirement. Returns 'nil' if no match.
+ #
+ # Example:
+ #
+ # get_path 'rake', '> 0.4' # "/usr/lib/ruby/gems/1.8/cache/rake-0.4.2.gem"
+ # get_path 'rake', '< 0.1' # nil
+ # get_path 'rak' # nil (exact name required)
+ #--
+
+ def get_path(dependency)
+ return dependency.name if /\.gem$/i.match?(dependency.name)
+
+ specs = dependency.matching_specs
+
+ selected = specs.max_by(&:version)
+
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless
+ selected
+
+ 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).
+
+ path = find_in_cache File.basename selected.cache_file
+
+ return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless path
+
+ path
+ end
+end
diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb
new file mode 100644
index 0000000000..d9740d814a
--- /dev/null
+++ b/lib/rubygems/commands/update_command.rb
@@ -0,0 +1,326 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../command_manager"
+require_relative "../dependency_installer"
+require_relative "../install_update_options"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+require_relative "../install_message" # must come before rdoc for messaging
+require_relative "../rdoc"
+
+class Gem::Commands::UpdateCommand < Gem::Command
+ include Gem::InstallUpdateOptions
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ attr_reader :installer # :nodoc:
+
+ attr_reader :updated # :nodoc:
+
+ def initialize
+ options = {
+ force: false,
+ }
+
+ options.merge!(install_update_options)
+
+ super "update", "Update installed gems to the latest version", options
+
+ add_install_update_options
+
+ Gem::OptionParser.accept Gem::Version do |value|
+ Gem::Version.new value
+
+ value
+ end
+
+ add_option("--system [VERSION]", Gem::Version,
+ "Update the RubyGems system software") do |value, opts|
+ value ||= true
+
+ opts[:system] = value
+ end
+
+ add_local_remote_options
+ add_platform_option
+ add_prerelease_option "as update targets"
+
+ @updated = []
+ @installer = nil
+ end
+
+ def arguments # :nodoc:
+ "GEMNAME name of gem to update"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-force --install-dir #{Gem.dir}\n" +
+ install_update_defaults_str
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The update command will update your gems to the latest version.
+
+The update command does not remove the previous version. Use the cleanup
+command to remove old versions.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEMNAME [GEMNAME ...]"
+ end
+
+ def check_latest_rubygems(version) # :nodoc:
+ if Gem.rubygems_version == version
+ say "Latest version already installed. Done."
+ terminate_interaction
+ end
+ end
+
+ def check_oldest_rubygems(version) # :nodoc:
+ if oldest_supported_version > version
+ alert_error "rubygems #{version} is not supported on #{RUBY_VERSION}. The oldest version supported by this ruby is #{oldest_supported_version}"
+ terminate_interaction 1
+ end
+ end
+
+ def check_update_arguments # :nodoc:
+ unless options[:args].empty?
+ alert_error "Gem names are not allowed with the --system option"
+ terminate_interaction 1
+ end
+ end
+
+ def execute
+ if options[:system]
+ update_rubygems
+ return
+ end
+
+ gems_to_update = which_to_update(
+ highest_installed_gems,
+ options[:args].uniq
+ )
+
+ if options[:explain]
+ say "Gems to update:"
+
+ gems_to_update.each do |name_tuple|
+ say " #{name_tuple.full_name}"
+ end
+
+ return
+ end
+
+ say "Updating installed gems"
+
+ updated = update_gems gems_to_update
+
+ installed_names = highest_installed_gems.keys
+ updated_names = updated.map(&:name)
+ not_updated_names = options[:args].uniq - updated_names
+ not_installed_names = not_updated_names - installed_names
+ up_to_date_names = not_updated_names - not_installed_names
+
+ if updated.empty?
+ say "Nothing to update"
+ else
+ say "Gems updated: #{updated_names.join(" ")}"
+ end
+ say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty?
+ say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty?
+ end
+
+ def fetch_remote_gems(spec) # :nodoc:
+ dependency = Gem::Dependency.new spec.name, "> #{spec.version}"
+ dependency.prerelease = options[:prerelease]
+
+ fetcher = Gem::SpecFetcher.fetcher
+
+ spec_tuples, errors = fetcher.search_for_dependency dependency
+
+ error = errors.find {|e| e.respond_to? :exception }
+
+ raise error if error
+
+ spec_tuples
+ end
+
+ def highest_installed_gems # :nodoc:
+ hig = {} # highest installed gems
+
+ # Get only gem specifications installed as --user-install
+ Gem::Specification.dirs = Gem.user_dir if options[:user_install]
+
+ Gem::Specification.each do |spec|
+ if hig[spec.name].nil? || hig[spec.name].version < spec.version
+ hig[spec.name] = spec
+ end
+ end
+
+ hig
+ end
+
+ def highest_remote_name_tuple(spec) # :nodoc:
+ spec_tuples = fetch_remote_gems spec
+
+ highest_remote_gem = spec_tuples.max
+ return unless highest_remote_gem
+
+ highest_remote_gem.first
+ end
+
+ def install_rubygems(spec) # :nodoc:
+ args = update_rubygems_arguments
+ version = spec.version
+
+ update_dir = File.join spec.base_dir, "gems", "rubygems-update-#{version}"
+
+ Dir.chdir update_dir do
+ say "Installing RubyGems #{version}" unless options[:silent]
+
+ installed = preparing_gem_layout_for(version) do
+ system Gem.ruby, "--disable-gems", "setup.rb", *args
+ end
+
+ unless options[:silent]
+ say "RubyGems system software updated" if installed
+ end
+ end
+ end
+
+ def preparing_gem_layout_for(version)
+ if Gem::Version.new(version) >= Gem::Version.new("3.2.a")
+ yield
+ else
+ require "tmpdir"
+ Dir.mktmpdir("gem_update") do |tmpdir|
+ FileUtils.mv Gem.plugindir, tmpdir
+
+ status = yield
+
+ unless status
+ FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir
+ end
+
+ status
+ end
+ end
+ end
+
+ def rubygems_target_version
+ version = options[:system]
+ update_latest = version == true
+
+ unless update_latest
+ version = Gem::Version.new version
+ requirement = Gem::Requirement.new version
+
+ return version, requirement
+ end
+
+ version = Gem::Version.new Gem::VERSION
+ requirement = Gem::Requirement.new ">= #{Gem::VERSION}"
+
+ rubygems_update = Gem::Specification.new
+ rubygems_update.name = "rubygems-update"
+ rubygems_update.version = version
+
+ highest_remote_tup = highest_remote_name_tuple(rubygems_update)
+ target = highest_remote_tup ? highest_remote_tup.version : version
+
+ [target, requirement]
+ end
+
+ def update_gem(name, version = Gem::Requirement.default)
+ return if @updated.any? {|spec| spec.name == name }
+
+ update_options = options.dup
+ update_options[:prerelease] = version.prerelease?
+
+ @installer = Gem::DependencyInstaller.new update_options
+
+ say "Updating #{name}" unless options[:system]
+ begin
+ @installer.install name, Gem::Requirement.new(version)
+ rescue Gem::InstallError, Gem::DependencyError => e
+ alert_error "Error installing #{name}:\n\t#{e.message}"
+ end
+
+ @installer.installed_gems.each do |spec|
+ @updated << spec
+ end
+ end
+
+ def update_gems(gems_to_update)
+ gems_to_update.uniq.sort.each do |name_tuple|
+ update_gem name_tuple.name, name_tuple.version
+ end
+
+ @updated
+ end
+
+ ##
+ # Update RubyGems software to the latest version.
+
+ def update_rubygems
+ if Gem.disable_system_update_message
+ alert_error Gem.disable_system_update_message
+ terminate_interaction 1
+ end
+
+ check_update_arguments
+
+ version, requirement = rubygems_target_version
+
+ check_latest_rubygems version
+
+ check_oldest_rubygems version
+
+ installed_gems = Gem::Specification.find_all_by_name "rubygems-update", requirement
+ installed_gems = update_gem("rubygems-update", requirement) if installed_gems.empty? || installed_gems.first.version != version
+ return if installed_gems.empty?
+
+ install_rubygems installed_gems.first
+ end
+
+ def update_rubygems_arguments # :nodoc:
+ args = []
+ args << "--silent" if options[:silent]
+ args << "--prefix" << Gem.prefix if Gem.prefix
+ args << "--no-document" unless options[:document].include?("rdoc") || options[:document].include?("ri")
+ args << "--no-format-executable" if options[:no_format_executable]
+ args << "--previous-version" << Gem::VERSION
+ args
+ end
+
+ def which_to_update(highest_installed_gems, gem_names)
+ result = []
+
+ highest_installed_gems.each do |_l_name, l_spec|
+ next if !gem_names.empty? &&
+ gem_names.none? {|name| name == l_spec.name }
+
+ highest_remote_tup = highest_remote_name_tuple l_spec
+ next unless highest_remote_tup
+
+ result << highest_remote_tup
+ end
+
+ result
+ end
+
+ private
+
+ #
+ # Oldest version we support downgrading to. This is the version that
+ # originally ships with the oldest supported patch version of ruby.
+ #
+ def oldest_supported_version
+ @oldest_supported_version ||=
+ Gem::Version.new("3.3.3")
+ end
+end
diff --git a/lib/rubygems/commands/which_command.rb b/lib/rubygems/commands/which_command.rb
new file mode 100644
index 0000000000..5ed4d9d142
--- /dev/null
+++ b/lib/rubygems/commands/which_command.rb
@@ -0,0 +1,88 @@
+# 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
+
+ add_option "-a", "--[no-]all", "show all matching files" do |show_all, options|
+ options[:show_all] = show_all
+ end
+
+ add_option "-g", "--[no-]gems-first",
+ "search gems before non-gems" do |gems_first, options|
+ options[:search_gems_first] = gems_first
+ end
+ end
+
+ def arguments # :nodoc:
+ "FILE name of file to find"
+ end
+
+ def defaults_str # :nodoc:
+ "--no-gems-first --no-all"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The which command is like the shell which command and shows you where
+the file you wish to require lives.
+
+You can use the which command to help determine why you are requiring a
+version you did not expect or to look at the content of a file you are
+requiring to see why it does not behave as you expect.
+ EOF
+ end
+
+ def execute
+ found = true
+
+ options[:args].each do |arg|
+ arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, "")
+ dirs = $LOAD_PATH
+
+ spec = Gem::Specification.find_by_path arg
+
+ if spec
+ if options[:search_gems_first]
+ dirs = spec.full_require_paths + $LOAD_PATH
+ else
+ dirs = $LOAD_PATH + spec.full_require_paths
+ end
+ end
+
+ paths = find_paths arg, dirs
+
+ if paths.empty?
+ alert_error "Can't find Ruby library file or shared library #{arg}"
+ found = false
+ else
+ say paths
+ end
+ end
+
+ terminate_interaction 1 unless found
+ end
+
+ def find_paths(package_name, dirs)
+ result = []
+
+ dirs.each do |dir|
+ Gem.suffixes.each do |ext|
+ full_path = File.join dir, "#{package_name}#{ext}"
+ if File.exist?(full_path) && !File.directory?(full_path)
+ result << full_path
+ return result unless options[:show_all]
+ end
+ end
+ end
+
+ result
+ end
+
+ def usage # :nodoc:
+ "#{program_name} FILE [FILE ...]"
+ end
+end
diff --git a/lib/rubygems/commands/yank_command.rb b/lib/rubygems/commands/yank_command.rb
new file mode 100644
index 0000000000..fbdc262549
--- /dev/null
+++ b/lib/rubygems/commands/yank_command.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+require_relative "../gemcutter_utilities"
+
+class Gem::Commands::YankCommand < Gem::Command
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+ include Gem::GemcutterUtilities
+
+ def description # :nodoc:
+ <<-EOF
+The yank command permanently removes a gem you pushed to a server.
+
+Once you have pushed a gem several downloads will happen automatically
+via the webhooks. If you accidentally pushed passwords or other sensitive
+data you will need to change them immediately and yank your gem.
+ EOF
+ end
+
+ def arguments # :nodoc:
+ "GEM name of gem"
+ end
+
+ def usage # :nodoc:
+ "#{program_name} -v VERSION [-p PLATFORM] [--key KEY_NAME] [--host HOST] GEM"
+ end
+
+ def initialize
+ super "yank", "Remove a pushed gem from the index"
+
+ add_version_option("remove")
+ add_platform_option("remove")
+ add_otp_option
+
+ add_option("--host HOST",
+ "Yank from another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
+ options[:host] = value
+ end
+
+ add_key_option
+ @host = nil
+ end
+
+ def execute
+ @host = options[:host]
+
+ sign_in @host, scope: get_yank_scope
+
+ version = get_version_from_requirements(options[:version])
+ platform = get_platform_from_requirements(options)
+
+ if version
+ yank_gem(version, platform)
+ else
+ say "A version argument is required: #{usage}"
+ terminate_interaction
+ end
+ end
+
+ def yank_gem(version, platform)
+ say "Yanking gem from #{host}..."
+ args = [:delete, version, platform, "api/v1/gems/yank"]
+ response = yank_api_request(*args)
+
+ say response.body
+ end
+
+ private
+
+ def yank_api_request(method, version, platform, api)
+ name = get_one_gem_name
+ response = rubygems_api_request(method, api, host, scope: get_yank_scope) do |request|
+ request.add_field("Authorization", api_key)
+
+ data = {
+ "gem_name" => name,
+ "version" => version,
+ }
+ data["platform"] = platform if platform
+
+ request.set_form_data data
+ end
+ response
+ end
+
+ def get_version_from_requirements(requirements)
+ requirements.requirements.first[1].version
+ rescue StandardError
+ nil
+ end
+
+ def get_yank_scope
+ :yank_rubygem
+ end
+end
diff --git a/lib/rubygems/config_file.rb b/lib/rubygems/config_file.rb
new file mode 100644
index 0000000000..d5e9eb4e33
--- /dev/null
+++ b/lib/rubygems/config_file.rb
@@ -0,0 +1,652 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require_relative "user_interaction"
+require "rbconfig"
+
+##
+# Gem::ConfigFile RubyGems options and gem command options from gemrc.
+#
+# gemrc is a YAML file that uses strings to match gem command arguments and
+# symbols to match RubyGems options.
+#
+# Gem command arguments use a String key that matches the command name and
+# allow you to specify default arguments:
+#
+# install: --no-rdoc --no-ri
+# update: --no-rdoc --no-ri
+#
+# You can use <tt>gem:</tt> to set default arguments for all commands.
+#
+# RubyGems options use symbol keys. Valid options are:
+#
+# +:backtrace+:: See #backtrace
+# +:bulk_threshold+:: See #bulk_threshold
+# +:verbose+:: See #verbose
+# +:update_sources+:: See #update_sources
+# +:concurrent_downloads+:: See #concurrent_downloads
+# +:cert_expiration_length_days+:: See #cert_expiration_length_days
+# +:install_extension_in_lib+:: See #install_extension_in_lib
+# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled
+# +:global_gem_cache+:: See #global_gem_cache
+# +:use_psych+:: See #use_psych
+# +:gemhome+:: See #home
+# +:gempath+:: See #path
+# +:sources+:: Sets Gem::sources
+# +:disable_default_gem_server+:: See #disable_default_gem_server
+# +:ssl_verify_mode+:: See #ssl_verify_mode
+# +:ssl_ca_cert+:: See #ssl_ca_cert
+# +:ssl_client_cert+:: See #ssl_client_cert
+#
+# gemrc files may exist in various locations and are read and merged in
+# the following order:
+#
+# - system wide (/etc/gemrc)
+# - per user (~/.gemrc)
+# - per environment (gemrc files listed in the GEMRC environment variable)
+
+class Gem::ConfigFile
+ include Gem::UserInteraction
+
+ DEFAULT_BACKTRACE = true
+ DEFAULT_BULK_THRESHOLD = 1000
+ DEFAULT_VERBOSITY = true
+ DEFAULT_UPDATE_SOURCES = true
+ DEFAULT_CONCURRENT_DOWNLOADS = 8
+ DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365
+ DEFAULT_IPV4_FALLBACK_ENABLED = false
+ DEFAULT_INSTALL_EXTENSION_IN_LIB = true
+ DEFAULT_GLOBAL_GEM_CACHE = false
+ DEFAULT_USE_PSYCH = false
+
+ ##
+ # For Ruby packagers to set configuration defaults. Set in
+ # rubygems/defaults/operating_system.rb
+
+ OPERATING_SYSTEM_DEFAULTS = Gem.operating_system_defaults
+
+ ##
+ # For Ruby implementers to set configuration defaults. Set in
+ # rubygems/defaults/#{RUBY_ENGINE}.rb
+
+ PLATFORM_DEFAULTS = Gem.platform_defaults
+
+ # :stopdoc:
+
+ SYSTEM_CONFIG_PATH =
+ begin
+ require "etc"
+ Etc.sysconfdir
+ rescue LoadError, NoMethodError
+ RbConfig::CONFIG["sysconfdir"] || "/etc"
+ end
+
+ # :startdoc:
+
+ SYSTEM_WIDE_CONFIG_FILE = File.join SYSTEM_CONFIG_PATH, "gemrc"
+
+ ##
+ # List of arguments supplied to the config file object.
+
+ attr_reader :args
+
+ ##
+ # Where to look for gems (deprecated)
+
+ attr_accessor :path
+
+ ##
+ # Where to install gems (deprecated)
+
+ attr_accessor :home
+
+ ##
+ # True if we print backtraces on errors.
+
+ attr_writer :backtrace
+
+ ##
+ # Bulk threshold value. If the number of missing gems are above this
+ # threshold value, then a bulk download technique is used. (deprecated)
+
+ attr_accessor :bulk_threshold
+
+ ##
+ # Verbose level of output:
+ # * false -- No output
+ # * true -- Normal output
+ # * :loud -- Extra output
+
+ attr_accessor :verbose
+
+ ##
+ # Number of gem downloads that should be performed concurrently.
+
+ attr_accessor :concurrent_downloads
+
+ ##
+ # True if we want to update the SourceInfoCache every time, false otherwise
+
+ attr_accessor :update_sources
+
+ ##
+ # True if we want to force specification of gem server when pushing a gem
+
+ attr_accessor :disable_default_gem_server
+
+ # openssl verify mode value, used for remote https connection
+
+ attr_reader :ssl_verify_mode
+
+ ##
+ # Path name of directory or file of openssl CA certificate, used for remote
+ # https connection
+
+ attr_accessor :ssl_ca_cert
+
+ ##
+ # sources to look for gems
+ attr_accessor :sources
+
+ ##
+ # Expiration length to sign a certificate
+
+ attr_accessor :cert_expiration_length_days
+
+ ##
+ # Install extensions into lib as well as into the extension directory.
+
+ attr_accessor :install_extension_in_lib
+
+ ##
+ # == Experimental ==
+ # Fallback to IPv4 when IPv6 is not reachable or slow (default: false)
+
+ attr_accessor :ipv4_fallback_enabled
+
+ ##
+ # Use a global cache for .gem files shared across all Ruby installations.
+ # When enabled, gems are cached to ~/.cache/gem/gems (or XDG_CACHE_HOME/gem/gems).
+
+ attr_accessor :global_gem_cache
+
+ ##
+ # Use Psych (C extension YAML parser) instead of the pure Ruby YAMLSerializer.
+
+ attr_accessor :use_psych
+
+ ##
+ # Path name of directory or file of openssl client certificate, used for remote https connection with client authentication
+
+ attr_reader :ssl_client_cert
+
+ ##
+ # Create the config file object. +args+ is the list of arguments
+ # from the command line.
+ #
+ # The following command line options are handled early here rather
+ # than later at the time most command options are processed.
+ #
+ # <tt>--config-file</tt>, <tt>--config-file==NAME</tt>::
+ # Obviously these need to be handled by the ConfigFile object to ensure we
+ # get the right config file.
+ #
+ # <tt>--backtrace</tt>::
+ # Backtrace needs to be turned on early so that errors before normal
+ # option parsing can be properly handled.
+ #
+ # <tt>--debug</tt>::
+ # Enable Ruby level debug messages. Handled early for the same reason as
+ # --backtrace.
+ #--
+ # TODO: parse options upstream, pass in options directly
+
+ def initialize(args)
+ set_config_file_name(args)
+
+ @backtrace = DEFAULT_BACKTRACE
+ @bulk_threshold = DEFAULT_BULK_THRESHOLD
+ @verbose = DEFAULT_VERBOSITY
+ @update_sources = DEFAULT_UPDATE_SOURCES
+ @concurrent_downloads = DEFAULT_CONCURRENT_DOWNLOADS
+ @cert_expiration_length_days = DEFAULT_CERT_EXPIRATION_LENGTH_DAYS
+ @install_extension_in_lib = DEFAULT_INSTALL_EXTENSION_IN_LIB
+ @ipv4_fallback_enabled = ENV["IPV4_FALLBACK_ENABLED"] == "true" || DEFAULT_IPV4_FALLBACK_ENABLED
+ @global_gem_cache = ENV["RUBYGEMS_GLOBAL_GEM_CACHE"] == "true" || DEFAULT_GLOBAL_GEM_CACHE
+ @use_psych = ENV["RUBYGEMS_USE_PSYCH"] == "true" || DEFAULT_USE_PSYCH
+
+ operating_system_config = Marshal.load Marshal.dump(OPERATING_SYSTEM_DEFAULTS)
+ platform_config = Marshal.load Marshal.dump(PLATFORM_DEFAULTS)
+ system_config = load_file SYSTEM_WIDE_CONFIG_FILE
+ user_config = load_file config_file_name
+
+ environment_config = (ENV["GEMRC"] || "").
+ split(File::PATH_SEPARATOR).inject({}) do |result, file|
+ result.merge load_file file
+ end
+
+ @hash = operating_system_config.merge platform_config
+ unless args.index "--norc"
+ @hash = @hash.merge system_config
+ @hash = @hash.merge user_config
+ @hash = @hash.merge environment_config
+ end
+
+ @hash.transform_keys! do |k|
+ # gemhome and gempath are not working with symbol keys
+ if %w[backtrace bulk_threshold verbose update_sources cert_expiration_length_days
+ concurrent_downloads install_extension_in_lib ipv4_fallback_enabled
+ global_gem_cache use_psych sources
+ disable_default_gem_server ssl_verify_mode ssl_ca_cert ssl_client_cert].include?(k)
+ k.to_sym
+ else
+ k
+ end
+ end
+
+ # HACK: these override command-line args, which is bad
+ @backtrace = @hash[:backtrace] if @hash.key? :backtrace
+ @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold
+ @verbose = @hash[:verbose] if @hash.key? :verbose
+ @update_sources = @hash[:update_sources] if @hash.key? :update_sources
+ @concurrent_downloads = @hash[:concurrent_downloads] if @hash.key? :concurrent_downloads
+ @cert_expiration_length_days = @hash[:cert_expiration_length_days] if @hash.key? :cert_expiration_length_days
+ @install_extension_in_lib = @hash[:install_extension_in_lib] if @hash.key? :install_extension_in_lib
+ @ipv4_fallback_enabled = @hash[:ipv4_fallback_enabled] if @hash.key? :ipv4_fallback_enabled
+ @global_gem_cache = @hash[:global_gem_cache] if @hash.key? :global_gem_cache
+ @use_psych = @hash[:use_psych] if @hash.key? :use_psych
+
+ @home = @hash[:gemhome] if @hash.key? :gemhome
+ @path = @hash[:gempath] if @hash.key? :gempath
+ @sources = @hash[:sources] if @hash.key? :sources
+ @disable_default_gem_server = @hash[:disable_default_gem_server] if @hash.key? :disable_default_gem_server
+ @ssl_verify_mode = @hash[:ssl_verify_mode] if @hash.key? :ssl_verify_mode
+ @ssl_ca_cert = @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert
+ @ssl_client_cert = @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert
+
+ @api_keys = nil
+ @rubygems_api_key = nil
+
+ handle_arguments args
+ end
+
+ ##
+ # Hash of RubyGems.org and alternate API keys
+
+ def api_keys
+ load_api_keys unless @api_keys
+
+ @api_keys
+ end
+
+ ##
+ # Checks the permissions of the credentials file. If they are not 0600 an
+ # error message is displayed and RubyGems aborts.
+
+ def check_credentials_permissions
+ 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 & 0o777
+
+ return if existing_permissions == 0o600
+
+ alert_error <<-ERROR
+Your gem push credentials file located at:
+
+\t#{credentials_path}
+
+has file permissions of 0#{existing_permissions.to_s 8} but 0600 is required.
+
+To fix this error run:
+
+\tchmod 0600 #{credentials_path}
+
+You should reset your credentials at:
+
+\thttps://rubygems.org/profile/edit
+
+if you believe they were disclosed to a third party.
+ ERROR
+
+ terminate_interaction 1
+ end
+
+ ##
+ # Location of RubyGems.org credentials
+
+ def credentials_path
+ credentials = File.join Gem.user_home, ".gem", "credentials"
+ if File.exist? credentials
+ credentials
+ else
+ File.join Gem.data_home, "gem", "credentials"
+ end
+ end
+
+ def load_api_keys
+ check_credentials_permissions
+
+ @api_keys = if File.exist? credentials_path
+ load_file(credentials_path)
+ else
+ @hash
+ end
+
+ if @api_keys.key? :rubygems_api_key
+ @rubygems_api_key = @api_keys[:rubygems_api_key]
+ @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless
+ @api_keys.key? :rubygems
+ end
+ end
+
+ ##
+ # Returns the RubyGems.org API key
+
+ def rubygems_api_key
+ load_api_keys unless @rubygems_api_key
+
+ @rubygems_api_key
+ end
+
+ ##
+ # Sets the RubyGems.org API key to +api_key+
+
+ def rubygems_api_key=(api_key)
+ set_api_key :rubygems_api_key, api_key
+
+ @rubygems_api_key = api_key
+ end
+
+ ##
+ # Set a specific host's API key to +api_key+
+
+ def set_api_key(host, api_key)
+ check_credentials_permissions
+
+ config = load_file(credentials_path).merge(host => api_key)
+
+ dirname = File.dirname credentials_path
+ require "fileutils"
+ FileUtils.mkdir_p(dirname)
+
+ permissions = 0o600 & ~File.umask
+ File.open(credentials_path, "w", permissions) do |f|
+ f.write self.class.dump_with_rubygems_yaml(config)
+ end
+
+ load_api_keys # reload
+ end
+
+ ##
+ # Remove the +~/.gem/credentials+ file to clear all the current sessions.
+
+ def unset_api_key!
+ return false unless File.exist?(credentials_path)
+
+ File.delete(credentials_path)
+ end
+
+ def load_file(filename)
+ yaml_errors = [ArgumentError]
+
+ return {} unless filename && !filename.empty? && File.exist?(filename)
+
+ begin
+ config = self.class.load_with_rubygems_config_hash(File.read(filename))
+ has_invalid_keys = config.keys.any? {|k| k.to_s.gsub(%r{https?:\/\/}, "").include?(": ") }
+ has_invalid_values = config.values.any? {|v| v.is_a?(String) && v.gsub(%r{https?:\/\/}, "").match?(/\A\S+: /) }
+ if has_invalid_keys || has_invalid_values
+ warn "Failed to load #{filename} because it doesn't contain valid YAML hash"
+ return {}
+ else
+ return config
+ end
+ rescue *yaml_errors => e
+ warn "Failed to load #{filename}, #{e}"
+ rescue Errno::EACCES
+ warn "Failed to load #{filename} due to permissions problem."
+ end
+
+ {}
+ end
+
+ # True if the backtrace option has been specified, or debug is on.
+ def backtrace
+ @backtrace || $DEBUG
+ end
+
+ # Check state file is writable. Creates empty file if not present to ensure we can write to it.
+ def state_file_writable?
+ if File.exist?(state_file_name)
+ File.writable?(state_file_name)
+ else
+ require "fileutils"
+ FileUtils.mkdir_p File.dirname(state_file_name)
+ File.open(state_file_name, "w") {}
+ true
+ end
+ rescue Errno::EACCES
+ false
+ end
+
+ # The name of the configuration file.
+ def config_file_name
+ @config_file_name || Gem.config_file
+ end
+
+ # The name of the state file.
+ def state_file_name
+ Gem.state_file
+ end
+
+ # Reads time of last update check from state file
+ def last_update_check
+ if File.readable?(state_file_name)
+ File.read(state_file_name).to_i
+ else
+ 0
+ end
+ end
+
+ # Writes time of last update check to state file
+ def last_update_check=(timestamp)
+ File.write(state_file_name, timestamp.to_s) if state_file_writable?
+ end
+
+ # Delegates to @hash
+ def each(&block)
+ hash = @hash.dup
+ hash.delete :update_sources
+ hash.delete :verbose
+ hash.delete :backtrace
+ hash.delete :bulk_threshold
+
+ yield :update_sources, @update_sources
+ yield :verbose, @verbose
+ yield :backtrace, @backtrace
+ yield :bulk_threshold, @bulk_threshold
+
+ yield "config_file_name", @config_file_name if @config_file_name
+
+ hash.each(&block)
+ end
+
+ # Handle the command arguments.
+ def handle_arguments(arg_list)
+ @args = []
+
+ arg_list.each do |arg|
+ case arg
+ when /^--(backtrace|traceback)$/ then
+ @backtrace = true
+ when /^--debug$/ then
+ $DEBUG = true
+
+ warn "NOTE: Debugging mode prints all exceptions even when rescued"
+ else
+ @args << arg
+ end
+ end
+ end
+
+ # Really verbose mode gives you extra output.
+ def really_verbose
+ case verbose
+ when true, false, nil then
+ false
+ else
+ true
+ end
+ end
+
+ # to_yaml only overwrites things you can't override on the command line.
+ def to_yaml # :nodoc:
+ yaml_hash = {}
+ yaml_hash[:backtrace] = @hash.fetch(:backtrace, DEFAULT_BACKTRACE)
+ yaml_hash[:bulk_threshold] = @hash.fetch(:bulk_threshold, DEFAULT_BULK_THRESHOLD)
+ yaml_hash[:sources] = Gem.sources.to_a
+ yaml_hash[:update_sources] = @hash.fetch(:update_sources, DEFAULT_UPDATE_SOURCES)
+ yaml_hash[:verbose] = @hash.fetch(:verbose, DEFAULT_VERBOSITY)
+
+ 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
+
+ yaml_hash[:ssl_ca_cert] =
+ @hash[:ssl_ca_cert] if @hash.key? :ssl_ca_cert
+
+ yaml_hash[:ssl_client_cert] =
+ @hash[:ssl_client_cert] if @hash.key? :ssl_client_cert
+
+ 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&.match?(re)
+ yaml_hash[key.to_s] = value
+ end
+
+ self.class.dump_with_rubygems_yaml(yaml_hash)
+ end
+
+ # Writes out this config file, replacing its source.
+ def write
+ require "fileutils"
+ FileUtils.mkdir_p File.dirname(config_file_name)
+
+ File.open config_file_name, "w" do |io|
+ io.write to_yaml
+ end
+ end
+
+ # Return the configuration information for +key+.
+ def [](key)
+ @hash[key] || @hash[key.to_s]
+ end
+
+ # Set configuration option +key+ to +value+.
+ def []=(key, value)
+ @hash[key] = value
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @backtrace == other.backtrace &&
+ @bulk_threshold == other.bulk_threshold &&
+ @verbose == other.verbose &&
+ @update_sources == other.update_sources &&
+ @hash == other.hash
+ end
+
+ attr_reader :hash
+ protected :hash
+
+ def self.dump_with_rubygems_yaml(content)
+ content.transform_keys! do |k|
+ k.is_a?(Symbol) ? ":#{k}" : k
+ end
+
+ require_relative "yaml_serializer"
+ Gem::YAMLSerializer.dump(content)
+ end
+
+ def self.load_with_rubygems_config_hash(yaml)
+ require_relative "yaml_serializer"
+
+ content = Gem::YAMLSerializer.load(yaml, permitted_classes: [])
+ return {} unless content.is_a?(Hash)
+
+ deep_transform_config_keys!(content)
+ end
+
+ private
+
+ def self.deep_transform_config_keys!(config)
+ config.transform_keys! do |k|
+ if k.match?(/\A:(.*)\Z/)
+ k[1..-1].to_sym
+ elsif k.include?("__") || k.match?(%r{/\Z})
+ if k.is_a?(Symbol)
+ k.to_s.gsub(/__/,".").gsub(%r{/\Z}, "").to_sym
+ else
+ k.dup.gsub(/__/,".").gsub(%r{/\Z}, "")
+ end
+ else
+ k
+ end
+ end
+
+ config.transform_values! do |v|
+ if v.is_a?(String)
+ if v.match?(/\A:(.*)\Z/)
+ v[1..-1].to_sym
+ elsif v.match?(/\A[+-]?\d+\Z/)
+ v.to_i
+ elsif v.match?(/\Atrue|false\Z/)
+ v == "true"
+ elsif v.empty?
+ nil
+ else
+ v
+ end
+ elsif v.respond_to?(:empty?) && v.empty?
+ nil
+ elsif v.is_a?(Hash)
+ deep_transform_config_keys!(v)
+ else
+ v
+ end
+ end
+
+ config
+ end
+
+ def set_config_file_name(args)
+ @config_file_name = ENV["GEMRC"]
+ need_config_file_name = false
+
+ args.each do |arg|
+ if need_config_file_name
+ @config_file_name = arg
+ need_config_file_name = false
+ elsif arg =~ /^--config-file=(.*)/
+ @config_file_name = $1
+ elsif /^--config-file$/.match?(arg)
+ need_config_file_name = true
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/core_ext/kernel_gem.rb b/lib/rubygems/core_ext/kernel_gem.rb
new file mode 100644
index 0000000000..4e09b95c44
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_gem.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Kernel
+ ##
+ # Use Kernel#gem to activate a specific version of +gem_name+.
+ #
+ # +requirements+ is a list of version requirements that the
+ # specified gem must match, most commonly "= example.version.number". See
+ # Gem::Requirement for how to specify a version requirement.
+ #
+ # If you will be activating the latest version of a gem, there is no need to
+ # call Kernel#gem, Kernel#require will do the right thing for you.
+ #
+ # Kernel#gem returns true if the gem was activated, otherwise false. If the
+ # gem could not be found, didn't match the version requirements, or a
+ # different version was already activated, an exception will be raised.
+ #
+ # Kernel#gem should be called *before* any require statements (otherwise
+ # RubyGems may load a conflicting library version).
+ #
+ # Kernel#gem only loads prerelease versions when prerelease +requirements+
+ # are given:
+ #
+ # gem 'rake', '>= 1.1.a', '< 2'
+ #
+ # In older RubyGems versions, the environment variable GEM_SKIP could be
+ # used to skip activation of specified gems, for example to test out changes
+ # that haven't been installed yet. Now RubyGems defers to -I and the
+ # RUBYLIB environment variable to skip activation of a gem.
+ #
+ # Example:
+ #
+ # GEM_SKIP=libA:libB ruby -I../libA -I../libB ./mycode.rb
+
+ def gem(gem_name, *requirements) # :doc:
+ skip_list = (ENV["GEM_SKIP"] || "").split(/:/)
+ raise Gem::LoadError, "skipping #{gem_name}" if skip_list.include? gem_name
+
+ if gem_name.is_a? Gem::Dependency
+ unless Gem::Deprecate.skip
+ warn "#{Gem.location_of_caller.join ":"}:Warning: Kernel.gem no longer "\
+ "accepts a Gem::Dependency object, please pass the name "\
+ "and requirements directly"
+ end
+
+ requirements = gem_name.requirement
+ gem_name = gem_name.name
+ end
+
+ dep = Gem::Dependency.new(gem_name, *requirements)
+
+ loaded = Gem.loaded_specs[gem_name]
+
+ return false if loaded && dep.matches_spec?(loaded)
+
+ spec = dep.to_spec
+
+ if spec
+ if Gem::LOADED_SPECS_MUTEX.owned?
+ spec.activate
+ else
+ Gem::LOADED_SPECS_MUTEX.synchronize { spec.activate }
+ end
+ end
+ end
+
+ private :gem
+end
diff --git a/lib/rubygems/core_ext/kernel_require.rb b/lib/rubygems/core_ext/kernel_require.rb
new file mode 100644
index 0000000000..3a9bdbdc9d
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_require.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require "monitor"
+
+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_method :gem_original_require, :require
+ private :gem_original_require
+ # :startdoc:
+ end
+
+ ##
+ # When RubyGems is required, Kernel#require is replaced with our own which
+ # is capable of loading gems on demand.
+ #
+ # When you call <tt>require 'x'</tt>, this is what happens:
+ # * If the file can be loaded from the existing Ruby loadpath, it
+ # is.
+ # * Otherwise, installed gems are searched for a file that matches.
+ # If it's found in gem 'y', that gem is activated (added to the
+ # loadpath).
+ #
+ # The normal <tt>require</tt> functionality of returning false if
+ # that file has already been loaded is preserved.
+
+ def require(path) # :doc:
+ return gem_original_require(path) unless Gem.discover_gems_on_require
+
+ RUBYGEMS_ACTIVATION_MONITOR.synchronize do
+ path = File.path(path)
+
+ # If +path+ belongs to a default gem, we activate it and then go straight
+ # to normal require
+
+ if spec = Gem.find_default_spec(path)
+ name = spec.name
+
+ next if Gem.loaded_specs[name]
+
+ # Ensure -I beats a default gem
+ resolved_path = begin
+ rp = nil
+ load_path_check_index = Gem.load_path_insert_index - Gem.activated_gem_paths
+ Gem.suffixes.find do |s|
+ $LOAD_PATH[0...load_path_check_index].find do |lp|
+ if File.symlink? lp # for backward compatibility
+ next
+ end
+
+ full_path = File.expand_path(File.join(lp, "#{path}#{s}"))
+ rp = full_path if File.file?(full_path)
+ end
+ end
+ rp
+ end
+
+ next if resolved_path
+
+ Kernel.send(:gem, name, Gem::Requirement.default_prerelease)
+
+ Gem.load_bundler_extensions(Gem.loaded_specs[name].version) if name == "bundler"
+
+ next
+ end
+
+ # If there are no unresolved deps, then we can use just try
+ # normal require handle loading a gem from the rescue below.
+
+ if Gem::Specification.unresolved_deps.empty?
+ next
+ 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
+
+ if Gem::Specification.find_active_stub_by_path(path)
+ next
+ end
+
+ # 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(&:activate)
+
+ # We found +path+ directly in an unresolved gem. Now we figure out, of
+ # the possible found specs, which one we should activate.
+ else
+
+ # Check that all the found specs are just different
+ # versions of the same gem
+ names = found_specs.map(&:name).uniq
+
+ if names.size > 1
+ raise Gem::LoadError, "#{path} found in multiple gems: #{names.join ", "}"
+ end
+
+ # Ok, now find a gem that has no conflicts, starting
+ # at the highest version.
+ valid = found_specs.find {|s| !s.has_conflicts? }
+
+ unless valid
+ le = Gem::LoadError.new "unable to find a version of '#{names.first}' to activate"
+ le.name = names.first
+ raise le
+ end
+
+ valid.activate
+ end
+ end
+
+ 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
new file mode 100644
index 0000000000..f806b77fab
--- /dev/null
+++ b/lib/rubygems/core_ext/kernel_warn.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Kernel
+ rubygems_path = "#{__dir__}/" # Frames to be skipped start with this path.
+
+ original_warn = instance_method(:warn)
+
+ remove_method :warn
+
+ class << self
+ remove_method :warn
+ end
+
+ module_function define_method(:warn) {|*messages, **kw|
+ unless uplevel = kw[:uplevel]
+ return original_warn.bind_call(self, *messages, **kw)
+ end
+
+ # Ensure `uplevel` fits a `long`
+ uplevel, = [uplevel].pack("l!").unpack("l!")
+
+ if uplevel >= 0
+ start = 0
+ while uplevel >= 0
+ loc, = caller_locations(start, 1)
+ unless loc
+ # No more backtrace
+ start += uplevel
+ break
+ end
+
+ start += 1
+
+ next unless path = loc.path
+ unless path.start_with?(rubygems_path, "<internal:")
+ # Non-rubygems frames
+ uplevel -= 1
+ end
+ end
+ kw[:uplevel] = start
+ end
+
+ original_warn.bind_call(self, *messages, **kw)
+ }
+end
diff --git a/lib/rubygems/core_ext/tcpsocket_init.rb b/lib/rubygems/core_ext/tcpsocket_init.rb
new file mode 100644
index 0000000000..018c49dbeb
--- /dev/null
+++ b/lib/rubygems/core_ext/tcpsocket_init.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require "socket"
+
+module CoreExtensions
+ module TCPSocketExt
+ def self.prepended(base)
+ base.prepend Initializer
+ end
+
+ module Initializer
+ CONNECTION_TIMEOUT = 5
+ IPV4_DELAY_SECONDS = 0.1
+
+ def initialize(host, serv, *rest)
+ mutex = Thread::Mutex.new
+ addrs = []
+ threads = []
+ cond_var = Thread::ConditionVariable.new
+
+ Addrinfo.foreach(host, serv, nil, :STREAM) do |addr|
+ Thread.report_on_exception = false
+
+ threads << Thread.new(addr) do
+ # give head start to ipv6 addresses
+ sleep IPV4_DELAY_SECONDS if addr.ipv4?
+
+ # raises Errno::ECONNREFUSED when ip:port is unreachable
+ Socket.tcp(addr.ip_address, serv, connect_timeout: CONNECTION_TIMEOUT).close
+ mutex.synchronize do
+ addrs << addr.ip_address
+ cond_var.signal
+ end
+ end
+ end
+
+ mutex.synchronize do
+ timeout_time = CONNECTION_TIMEOUT + Time.now.to_f
+ while addrs.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
+ cond_var.wait(mutex, remaining_time)
+ end
+
+ host = addrs.shift unless addrs.empty?
+ end
+
+ threads.each {|t| t.kill.join if t.alive? }
+
+ super(host, serv, *rest)
+ end
+ end
+ end
+end
+
+TCPSocket.prepend CoreExtensions::TCPSocketExt
diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb
new file mode 100644
index 0000000000..2247c49c81
--- /dev/null
+++ b/lib/rubygems/defaults.rb
@@ -0,0 +1,317 @@
+# frozen_string_literal: true
+
+module Gem
+ DEFAULT_HOST = "https://rubygems.org"
+
+ @post_install_hooks ||= []
+ @done_installing_hooks ||= []
+ @post_uninstall_hooks ||= []
+ @pre_uninstall_hooks ||= []
+ @pre_install_hooks ||= []
+
+ ##
+ # An Array of the default sources that come with RubyGems
+
+ def self.default_sources
+ @default_sources ||= %w[https://rubygems.org/]
+ end
+
+ ##
+ # Default spec directory path to be used if an alternate value is not
+ # specified in the environment
+
+ def self.default_spec_cache_dir
+ 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.cache_home, "gem", "specs"
+ end
+
+ default_spec_cache_dir
+ end
+
+ ##
+ # Default home directory path to be used if an alternate value is not
+ # specified in the environment
+
+ def self.default_dir
+ @default_dir ||= File.join(RbConfig::CONFIG["rubylibprefix"], "gems", RbConfig::CONFIG["ruby_version"])
+ end
+
+ ##
+ # Returns binary extensions dir for specified RubyGems base dir or nil
+ # if such directory cannot be determined.
+ #
+ # By default, the binary extensions are located side by side with their
+ # Ruby counterparts, therefore nil is returned
+
+ def self.default_ext_dir_for(base_dir)
+ nil
+ end
+
+ ##
+ # Paths where RubyGems' .rb files and bin files are installed
+
+ def self.default_rubygems_dirs
+ nil # default to standard layout
+ end
+
+ ##
+ # Path to specification files of default gems.
+
+ def self.default_specifications_dir
+ @default_specifications_dir ||= File.join(Gem.default_dir, "specifications", "default")
+ end
+
+ ##
+ # Finds the user's home directory.
+ #--
+ # Some comments from the ruby-talk list regarding finding the home
+ # directory:
+ #
+ # I have HOME, USERPROFILE and HOMEDRIVE + HOMEPATH. Ruby seems
+ # to be depending on HOME in those code samples. I propose that
+ # it should fallback to USERPROFILE and HOMEDRIVE + HOMEPATH (at
+ # least on Win32).
+ #++
+ #--
+ #
+ #++
+
+ def self.find_home
+ Dir.home.dup
+ rescue StandardError
+ if Gem.win_platform?
+ File.expand_path File.join(ENV["HOMEDRIVE"] || ENV["SystemDrive"], "/")
+ else
+ File.expand_path "/"
+ end
+ end
+
+ private_class_method :find_home
+
+ ##
+ # The home directory for the user.
+
+ def self.user_home
+ @user_home ||= find_home
+ end
+
+ ##
+ # Path for gems in the user's home directory
+
+ def self.user_dir
+ gem_dir = File.join(Gem.user_home, ".gem")
+ gem_dir = File.join(Gem.data_home, "gem") unless File.exist?(gem_dir)
+ parts = [gem_dir, ruby_engine]
+ parts << RbConfig::CONFIG["ruby_version"] unless RbConfig::CONFIG["ruby_version"].empty?
+ File.join parts
+ end
+
+ ##
+ # 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")
+ end
+
+ ##
+ # Finds the user's config file
+
+ def self.find_config_file
+ gemrc = File.join Gem.user_home, ".gemrc"
+ if File.exist? gemrc
+ gemrc
+ else
+ File.join Gem.config_home, "gem", "gemrc"
+ end
+ end
+
+ ##
+ # The path to standard location of the user's .gemrc file.
+
+ def self.config_file
+ @config_file ||= find_config_file
+ end
+
+ ##
+ # The path to standard location of the user's state file.
+
+ def self.state_file
+ @state_file ||= File.join(Gem.state_home, "gem", "last_update_check")
+ end
+
+ ##
+ # The path to standard location of the user's cache directory.
+
+ def self.cache_home
+ @cache_home ||= ENV["XDG_CACHE_HOME"] || File.join(Gem.user_home, ".cache")
+ end
+
+ ##
+ # The path to the global gem cache directory.
+ # This is used when global_gem_cache is enabled to share .gem files
+ # across all Ruby installations.
+
+ def self.global_gem_cache_path
+ File.join(cache_home, "gem", "gems")
+ end
+
+ ##
+ # The path to standard location of the user's data directory.
+
+ def self.data_home
+ @data_home ||= ENV["XDG_DATA_HOME"] || File.join(Gem.user_home, ".local", "share")
+ end
+
+ ##
+ # The path to standard location of the user's state directory.
+
+ def self.state_home
+ @state_home ||= ENV["XDG_STATE_HOME"] || File.join(Gem.user_home, ".local", "state")
+ end
+
+ ##
+ # How String Gem paths should be split. Overridable for esoteric platforms.
+
+ def self.path_separator
+ File::PATH_SEPARATOR
+ end
+
+ ##
+ # Default gem load path
+
+ def self.default_path
+ path = []
+ path << user_dir if user_home && File.exist?(user_home)
+ path << default_dir
+ path << vendor_dir if vendor_dir && File.directory?(vendor_dir)
+ path
+ end
+
+ ##
+ # Deduce Ruby's --program-prefix and --program-suffix from its install name
+
+ def self.default_exec_format
+ exec_format = begin
+ RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s")
+ rescue StandardError
+ "%s"
+ end
+
+ unless exec_format.include?("%s")
+ raise Gem::Exception,
+ "[BUG] invalid exec_format #{exec_format.inspect}, no %s"
+ end
+
+ exec_format
+ end
+
+ ##
+ # The default directory for binaries
+
+ def self.default_bindir
+ RbConfig::CONFIG["bindir"]
+ end
+
+ def self.ruby_engine
+ RUBY_ENGINE
+ end
+
+ ##
+ # The default signing key path
+
+ def self.default_key_path
+ default_key_path = File.join Gem.user_home, ".gem", "gem-private_key.pem"
+
+ unless File.exist?(default_key_path)
+ default_key_path = File.join Gem.data_home, "gem", "gem-private_key.pem"
+ end
+
+ default_key_path
+ end
+
+ ##
+ # The default signing certificate chain path
+
+ def self.default_cert_path
+ default_cert_path = File.join Gem.user_home, ".gem", "gem-public_cert.pem"
+
+ unless File.exist?(default_cert_path)
+ default_cert_path = File.join Gem.data_home, "gem", "gem-public_cert.pem"
+ end
+
+ default_cert_path
+ 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:
+ Gem.configuration.install_extension_in_lib
+ end
+
+ ##
+ # Directory where vendor gems are installed.
+
+ def self.vendor_dir # :nodoc:
+ if vendor_dir = ENV["GEM_VENDOR"]
+ return vendor_dir.dup
+ end
+
+ return nil unless RbConfig::CONFIG.key? "vendordir"
+
+ File.join RbConfig::CONFIG["vendordir"], "gems",
+ RbConfig::CONFIG["ruby_version"]
+ end
+
+ ##
+ # Default options for gem commands for Ruby packagers.
+ #
+ # The options here should be structured as an array of string "gem"
+ # command names as keys and a string of the default options as values.
+ #
+ # Example:
+ #
+ # def self.operating_system_defaults
+ # {
+ # 'install' => '--no-rdoc --no-ri --env-shebang',
+ # 'update' => '--no-rdoc --no-ri --env-shebang'
+ # }
+ # end
+
+ def self.operating_system_defaults
+ {}
+ end
+
+ ##
+ # Default options for gem commands for Ruby implementers.
+ #
+ # The options here should be structured as an array of string "gem"
+ # command names as keys and a string of the default options as values.
+ #
+ # Example:
+ #
+ # def self.platform_defaults
+ # {
+ # 'install' => '--no-rdoc --no-ri --env-shebang',
+ # 'update' => '--no-rdoc --no-ri --env-shebang'
+ # }
+ # end
+
+ def self.platform_defaults
+ {}
+ end
+end
diff --git a/lib/rubygems/dependency.rb b/lib/rubygems/dependency.rb
new file mode 100644
index 0000000000..1e91f493a6
--- /dev/null
+++ b/lib/rubygems/dependency.rb
@@ -0,0 +1,348 @@
+# frozen_string_literal: true
+
+##
+# The Dependency class holds a Gem name and a Gem::Requirement.
+
+class Gem::Dependency
+ ##
+ # Valid dependency types.
+ #--
+ # When this list is updated, be sure to change
+ # Gem::Specification::CURRENT_SPECIFICATION_VERSION as well.
+ #
+ # REFACTOR: This type of constant, TYPES, indicates we might want
+ # two classes, used via inheritance or duck typing.
+
+ TYPES = [
+ :development,
+ :runtime,
+ ].freeze
+
+ ##
+ # Dependency name or regular expression.
+
+ attr_accessor :name
+
+ ##
+ # Allows you to force this dependency to be a prerelease.
+
+ attr_writer :prerelease
+
+ ##
+ # Constructs a dependency with +name+ and +requirements+. The last
+ # argument can optionally be the dependency type, which defaults to
+ # <tt>:runtime</tt>.
+
+ def initialize(name, *requirements)
+ case name
+ when String then # ok
+ when Regexp then
+ msg = ["NOTE: Dependency.new w/ a regexp is deprecated.",
+ "Dependency.new called from #{Gem.location_of_caller.join(":")}"]
+ warn msg.join("\n") unless Gem::Deprecate.skip
+ else
+ raise ArgumentError,
+ "dependency name must be a String, was #{name.inspect}"
+ end
+
+ type = Symbol === requirements.last ? requirements.pop : :runtime
+ requirements = requirements.first if requirements.length == 1 # unpack
+
+ unless TYPES.include? type
+ raise ArgumentError, "Valid types are #{TYPES.inspect}, " \
+ "not #{type.inspect}"
+ end
+
+ @name = name
+ @requirement = Gem::Requirement.create requirements
+ @type = type
+ @prerelease = false
+
+ # This is for Marshal backwards compatibility. See the comments in
+ # +requirement+ for the dirty details.
+
+ @version_requirements = @requirement
+ end
+
+ ##
+ # A dependency's hash is the XOR of the hashes of +name+, +type+,
+ # and +requirement+.
+
+ def hash # :nodoc:
+ name.hash ^ type.hash ^ requirement.hash
+ end
+
+ def inspect # :nodoc:
+ if prerelease?
+ format("<%s type=%p name=%p requirements=%p prerelease=ok>", self.class, type, name, requirement.to_s)
+ else
+ format("<%s type=%p name=%p requirements=%p>", self.class, type, name, requirement.to_s)
+ end
+ end
+
+ ##
+ # Does this dependency require a prerelease?
+
+ def prerelease?
+ @prerelease || requirement.prerelease?
+ end
+
+ ##
+ # Is this dependency simply asking for the latest version
+ # of a gem?
+
+ def latest_version?
+ @requirement.none?
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 1, "Gem::Dependency.new(", ")" do
+ q.pp name
+ q.text ","
+ q.breakable
+
+ q.pp requirement
+
+ q.text ","
+ q.breakable
+
+ q.pp type
+ end
+ end
+
+ ##
+ # What does this dependency require?
+
+ def requirement
+ return @requirement if defined?(@requirement) && @requirement
+
+ # @version_requirements and @version_requirement are legacy ivar
+ # names, and supported here because older gems need to keep
+ # working and Dependency doesn't implement marshal_dump and
+ # marshal_load. In a happier world, this would be an
+ # attr_accessor. The horrifying instance_variable_get you see
+ # below is also the legacy of some old restructurings.
+ #
+ # Note also that because of backwards compatibility (loading new
+ # gems in an old RubyGems installation), we can't add explicit
+ # marshaling to this class until we want to make a big
+ # break. Maybe 2.0.
+ #
+ # Children, define explicit marshal and unmarshal behavior for
+ # public classes. Marshal formats are part of your public API.
+
+ # REFACTOR: See above
+
+ if defined?(@version_requirement) && @version_requirement
+ version = @version_requirement.instance_variable_get :@version
+ @version_requirement = nil
+ @version_requirements = Gem::Requirement.new version
+ end
+
+ @requirement = @version_requirements if defined?(@version_requirements)
+ end
+
+ def requirements_list
+ requirement.as_list
+ end
+
+ def to_s # :nodoc:
+ if type != :runtime
+ "#{name} (#{requirement}, #{type})"
+ else
+ "#{name} (#{requirement})"
+ end
+ end
+
+ ##
+ # Dependency type.
+
+ def type
+ @type ||= :runtime
+ end
+
+ def runtime?
+ @type == :runtime || !@type
+ end
+
+ def ==(other) # :nodoc:
+ Gem::Dependency === other &&
+ name == other.name &&
+ type == other.type &&
+ requirement == other.requirement
+ end
+
+ ##
+ # Dependencies are ordered by name.
+
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ##
+ # Uses this dependency as a pattern to compare to +other+. This
+ # dependency will match if the name matches the other's name, and
+ # other has only an equal version requirement that satisfies this
+ # dependency.
+
+ def =~(other)
+ unless Gem::Dependency === other
+ return unless other.respond_to?(:name) && other.respond_to?(:version)
+ other = Gem::Dependency.new other.name, other.version
+ end
+
+ return false unless name === other.name
+
+ reqs = other.requirement.requirements
+
+ return false unless reqs.length == 1
+ return false unless reqs.first.first == "="
+
+ version = reqs.first.last
+
+ requirement.satisfied_by? version
+ end
+
+ alias_method :===, :=~
+
+ ##
+ # :call-seq:
+ # dep.match? name => true or false
+ # dep.match? name, version => true or false
+ # dep.match? spec => true or false
+ #
+ # Does this dependency match the specification described by +name+ and
+ # +version+ or match +spec+?
+ #
+ # NOTE: Unlike #matches_spec? this method does not return true when the
+ # version is a prerelease version unless this is a prerelease dependency.
+
+ def match?(obj, version = nil, allow_prerelease = false)
+ if !version
+ name = obj.name
+ version = obj.version
+ else
+ name = obj
+ end
+
+ return false unless self.name === name
+
+ version = Gem::Version.new version
+
+ return true if requirement.none? && !version.prerelease?
+ return false if version.prerelease? &&
+ !allow_prerelease &&
+ !prerelease?
+
+ requirement.satisfied_by? version
+ end
+
+ ##
+ # Does this dependency match +spec+?
+ #
+ # NOTE: This is not a convenience method. Unlike #match? this method
+ # returns true when +spec+ is a prerelease version even if this dependency
+ # is not a prerelease dependency.
+
+ def matches_spec?(spec)
+ return false unless name === spec.name
+ return true if requirement.none?
+
+ requirement.satisfied_by?(spec.version)
+ end
+
+ ##
+ # Merges the requirements of +other+ into this dependency
+
+ def merge(other)
+ unless name == other.name
+ raise ArgumentError,
+ "#{self} and #{other} have different names"
+ end
+
+ default = Gem::Requirement.default
+ self_req = requirement
+ other_req = other.requirement
+
+ return self.class.new name, self_req if other_req == default
+ return self.class.new name, other_req if self_req == default
+
+ self.class.new name, self_req.as_list.concat(other_req.as_list)
+ end
+
+ def matching_specs(platform_only = false)
+ matches = Gem::Specification.find_all_by_name(name, requirement)
+
+ if platform_only
+ matches.reject! do |spec|
+ spec.nil? || !Gem::Platform.match_spec?(spec)
+ end
+ end
+
+ matches.reject(&:ignored?)
+ end
+
+ ##
+ # True if the dependency will not always match the latest version.
+
+ def specific?
+ @requirement.specific?
+ end
+
+ def to_specs
+ matches = matching_specs true
+
+ # TODO: check Gem.activated_spec[self.name] in case matches falls outside
+
+ if matches.empty?
+ specs = Gem::Specification.stubs_for name
+
+ if specs.empty?
+ raise Gem::MissingSpecError.new name, requirement
+ else
+ raise Gem::MissingSpecVersionError.new name, requirement, specs
+ end
+ end
+
+ # TODO: any other resolver validations should go here
+
+ matches
+ end
+
+ def to_spec
+ matches = to_specs.compact
+
+ active = matches.find(&:activated?)
+ return active if active
+
+ unless prerelease?
+ # Consider prereleases only as a fallback
+ pre, matches = matches.partition {|spec| spec.version.prerelease? }
+ matches = pre if matches.empty?
+ end
+
+ matches.first
+ end
+
+ def identity
+ if prerelease?
+ if specific?
+ :complete
+ else
+ :abs_latest
+ end
+ elsif latest_version?
+ :latest
+ else
+ :released
+ end
+ end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "requirement", @requirement
+ coder.add "type", @type
+ coder.add "prerelease", @prerelease
+ coder.add "version_requirements", @version_requirements
+ end
+end
diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb
new file mode 100644
index 0000000000..c842714d95
--- /dev/null
+++ b/lib/rubygems/dependency_installer.rb
@@ -0,0 +1,264 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "dependency_list"
+require_relative "package"
+require_relative "installer"
+require_relative "spec_fetcher"
+require_relative "user_interaction"
+require_relative "available_set"
+
+##
+# Installs a gem along with all its dependencies from local and remote gems.
+
+class Gem::DependencyInstaller
+ include Gem::UserInteraction
+
+ 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,
+ }.freeze
+
+ ##
+ # Documentation types. For use by the Gem.done_installing hook
+
+ attr_reader :document
+
+ ##
+ # Errors from SpecFetcher while searching for remote specifications
+
+ attr_reader :errors
+
+ ##
+ # List of gems installed by #install in alphabetic order
+
+ attr_reader :installed_gems
+
+ ##
+ # Creates a new installer instance.
+ #
+ # Options are:
+ # :cache_dir:: Alternate repository path to store .gem files in.
+ # :domain:: :local, :remote, or :both. :local only searches gems in the
+ # current directory. :remote searches only gems in Gem::sources.
+ # :both searches both.
+ # :env_shebang:: See Gem::Installer::new.
+ # :force:: See Gem::Installer#install.
+ # :format_executable:: See Gem::Installer#initialize.
+ # :ignore_dependencies:: Don't install any dependencies.
+ # :install_dir:: See Gem::Installer#install.
+ # :prerelease:: Allow prerelease versions. See #install.
+ # :security_policy:: See Gem::Installer::new and Gem::Security.
+ # :user_install:: See Gem::Installer.new
+ # :wrappers:: See Gem::Installer::new
+ # :build_args:: See Gem::Installer::new
+
+ def initialize(options = {})
+ @only_install_dir = !options[:install_dir].nil?
+ @install_dir = options[:install_dir] || Gem.dir
+ @build_root = options[:build_root]
+
+ options = DEFAULT_OPTIONS.merge options
+
+ @bin_dir = options[:bin_dir]
+ @dev_shallow = options[:dev_shallow]
+ @development = options[:development]
+ @document = options[:document]
+ @domain = options[:domain]
+ @env_shebang = options[:env_shebang]
+ @force = options[:force]
+ @format_executable = options[:format_executable]
+ @ignore_dependencies = options[:ignore_dependencies]
+ @prerelease = options[:prerelease]
+ @security_policy = options[:security_policy]
+ @user_install = options[:user_install]
+ @wrappers = options[:wrappers]
+ @build_args = options[:build_args]
+ @build_jobs = options[:build_jobs]
+ @build_docs_in_background = options[:build_docs_in_background]
+ @dir_mode = options[:dir_mode]
+ @data_mode = options[:data_mode]
+ @prog_mode = options[:prog_mode]
+ @build_extension = options[:build_extension]
+ @install_plugin = options[:install_plugin]
+
+ # Indicates that we should not try to update any deps unless
+ # we absolutely must.
+ @minimal_deps = options[:minimal_deps]
+
+ @available = nil
+ @installed_gems = []
+ @toplevel_specs = nil
+
+ @cache_dir = options[:cache_dir] || @install_dir
+
+ @errors = []
+ end
+
+ ##
+ # Indicated, based on the requested domain, if local
+ # gems should be considered.
+
+ def consider_local?
+ @domain == :both || @domain == :local
+ end
+
+ ##
+ # Indicated, based on the requested domain, if remote
+ # gems should be considered.
+
+ def consider_remote?
+ @domain == :both || @domain == :remote
+ end
+
+ def in_background(what) # :nodoc:
+ fork_happened = false
+ if @build_docs_in_background && Process.respond_to?(:fork)
+ begin
+ Process.fork do
+ yield
+ end
+ fork_happened = true
+ say "#{what} in a background process."
+ rescue NotImplementedError
+ end
+ end
+ yield unless fork_happened
+ end
+
+ ##
+ # Installs the gem +dep_or_name+ and all its dependencies. Returns an Array
+ # of installed gem specifications.
+ #
+ # If the +:prerelease+ option is set and there is a prerelease for
+ # +dep_or_name+ the prerelease version will be installed.
+ #
+ # Unless explicitly specified as a prerelease dependency, prerelease gems
+ # that +dep_or_name+ depend on will not be installed.
+ #
+ # If c-1.a depends on b-1 and a-1.a and there is a gem b-1.a available then
+ # c-1.a, b-1 and a-1.a will be installed. b-1.a will need to be installed
+ # separately.
+
+ def install(dep_or_name, version = Gem::Requirement.default)
+ request_set = resolve_dependencies dep_or_name, version
+
+ @installed_gems = []
+
+ options = {
+ bin_dir: @bin_dir,
+ build_args: @build_args,
+ build_jobs: @build_jobs,
+ document: @document,
+ env_shebang: @env_shebang,
+ force: @force,
+ format_executable: @format_executable,
+ ignore_dependencies: @ignore_dependencies,
+ prerelease: @prerelease,
+ security_policy: @security_policy,
+ user_install: @user_install,
+ wrappers: @wrappers,
+ build_root: @build_root,
+ dir_mode: @dir_mode,
+ data_mode: @data_mode,
+ prog_mode: @prog_mode,
+ build_extension: @build_extension,
+ install_plugin: @install_plugin,
+ }
+ options[:install_dir] = @install_dir if @only_install_dir
+
+ request_set.install options do |_, installer|
+ @installed_gems << installer.spec if installer
+ end
+
+ @installed_gems.sort!
+
+ # Since this is currently only called for docs, we can be lazy and just say
+ # it's documentation. Ideally the hook adder could decide whether to be in
+ # the background or not, and what to call it.
+ in_background "Installing documentation" do
+ Gem.done_installing_hooks.each do |hook|
+ hook.call self, @installed_gems
+ end
+ end unless Gem.done_installing_hooks.empty?
+
+ @installed_gems
+ end
+
+ def install_development_deps # :nodoc:
+ if @development && @dev_shallow
+ :shallow
+ elsif @development
+ :all
+ else
+ :none
+ end
+ end
+
+ def resolve_dependencies(dep_or_name, version) # :nodoc:
+ request_set = Gem::RequestSet.new
+ request_set.development = @development
+ request_set.development_shallow = @dev_shallow
+ request_set.soft_missing = @force
+ request_set.prerelease = @prerelease
+
+ installer_set = Gem::Resolver::InstallerSet.new @domain
+ installer_set.ignore_installed = (@minimal_deps == false) || @only_install_dir
+ installer_set.force = @force
+
+ if consider_local?
+ if dep_or_name =~ /\.gem$/ && File.file?(dep_or_name)
+ src = Gem::Source::SpecificFile.new dep_or_name
+ installer_set.add_local dep_or_name, src.spec, src
+ version = src.spec.version if version == Gem::Requirement.default
+ elsif dep_or_name =~ /\.gem$/ # rubocop:disable Performance/RegexpMatch
+ Dir[dep_or_name].each do |name|
+ 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
+ end
+
+ dependency =
+ if spec = installer_set.local?(dep_or_name)
+ installer_set.remote = nil if spec.dependencies.none?
+ Gem::Dependency.new spec.name, version
+ elsif String === dep_or_name
+ Gem::Dependency.new dep_or_name, version
+ else
+ dep_or_name
+ end
+
+ dependency.prerelease = @prerelease
+
+ request_set.import [dependency]
+
+ installer_set.add_always_install dependency
+
+ request_set.always_install = installer_set.always_install
+ request_set.remote = installer_set.consider_remote?
+
+ if @ignore_dependencies
+ installer_set.ignore_dependencies = true
+ request_set.ignore_dependencies = true
+ request_set.soft_missing = true
+ end
+
+ request_set.resolve installer_set
+
+ @errors.concat request_set.errors
+
+ request_set
+ end
+end
diff --git a/lib/rubygems/dependency_list.rb b/lib/rubygems/dependency_list.rb
new file mode 100644
index 0000000000..d50cfe2d54
--- /dev/null
+++ b/lib/rubygems/dependency_list.rb
@@ -0,0 +1,242 @@
+# 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 "vendored_tsort"
+
+##
+# Gem::DependencyList is used for installing and uninstalling gems in the
+# correct order to avoid conflicts.
+#--
+# TODO: It appears that all but topo-sort functionality is being duplicated
+# (or is planned to be duplicated) elsewhere in rubygems. Is the majority of
+# this class necessary anymore? Especially #ok?, #why_not_ok?
+
+class Gem::DependencyList
+ attr_reader :specs
+
+ include Enumerable
+ include Gem::TSort
+
+ ##
+ # Allows enabling/disabling use of development dependencies
+
+ attr_accessor :development
+
+ ##
+ # Creates a DependencyList from the current specs.
+
+ def self.from_specs
+ list = new
+ list.add(*Gem::Specification.to_a)
+ list
+ end
+
+ ##
+ # Creates a new DependencyList. If +development+ is true, development
+ # dependencies will be included.
+
+ def initialize(development = false)
+ @specs = []
+
+ @development = development
+ end
+
+ ##
+ # Adds +gemspecs+ to the dependency list.
+
+ def add(*gemspecs)
+ @specs.concat gemspecs
+ end
+
+ def clear
+ @specs.clear
+ end
+
+ ##
+ # Return a list of the gem specifications in the dependency list, sorted in
+ # order so that no gemspec in the list depends on a gemspec earlier in the
+ # list.
+ #
+ # This is useful when removing gems from a set of installed gems. By
+ # removing them in the returned order, you don't get into as many dependency
+ # issues.
+ #
+ # If there are circular dependencies (yuck!), then gems will be returned in
+ # order until only the circular dependents and anything they reference are
+ # left. Then arbitrary gemspecs will be returned until the circular
+ # dependency is broken, after which gems will be returned in dependency
+ # order again.
+
+ def dependency_order
+ sorted = strongly_connected_components.flatten
+
+ result = []
+ seen = {}
+
+ sorted.each do |spec|
+ if index = seen[spec.name]
+ if result[index].version < spec.version
+ result[index] = spec
+ end
+ else
+ seen[spec.name] = result.length
+ result << spec
+ end
+ end
+
+ result.reverse
+ end
+
+ ##
+ # Iterator over dependency_order
+
+ def each(&block)
+ dependency_order.each(&block)
+ end
+
+ def find_name(full_name)
+ @specs.find {|spec| spec.full_name == full_name }
+ end
+
+ def inspect # :nodoc:
+ format("%s %p>", super[0..-2], map(&:full_name))
+ end
+
+ ##
+ # Are all the dependencies in the list satisfied?
+
+ def ok?
+ why_not_ok?(:quick).empty?
+ end
+
+ def why_not_ok?(quick = false)
+ unsatisfied = Hash.new {|h,k| h[k] = [] }
+ each do |spec|
+ spec.runtime_dependencies.each do |dep|
+ inst = Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name &&
+ dep.requirement.satisfied_by?(installed_spec.version)
+ end
+
+ unless inst || @specs.find {|s| s.satisfies_requirement? dep }
+ unsatisfied[spec.name] << dep
+ return unsatisfied if quick
+ end
+ end
+ end
+
+ unsatisfied
+ end
+
+ ##
+ # It is ok to remove a gemspec from the dependency list?
+ #
+ # If removing the gemspec creates breaks a currently ok dependency, then it
+ # is NOT ok to remove the gemspec.
+
+ def ok_to_remove?(full_name, check_dev = true)
+ gem_to_remove = find_name full_name
+
+ # If the state is inconsistent, at least don't crash
+ return true unless gem_to_remove
+
+ siblings = @specs.find_all do |s|
+ s.name == gem_to_remove.name &&
+ s.full_name != gem_to_remove.full_name
+ end
+
+ deps = []
+
+ @specs.each do |spec|
+ check = check_dev ? spec.dependencies : spec.runtime_dependencies
+
+ check.each do |dep|
+ deps << dep if gem_to_remove.satisfies_requirement?(dep)
+ end
+ end
+
+ deps.all? do |dep|
+ siblings.any? do |s|
+ s.satisfies_requirement? dep
+ end
+ end
+ end
+
+ ##
+ # Remove everything in the DependencyList that matches but doesn't
+ # satisfy items in +dependencies+ (a hash of gem names to arrays of
+ # dependencies).
+
+ def remove_specs_unsatisfied_by(dependencies)
+ specs.reject! do |spec|
+ dep = dependencies[spec.name]
+ dep && !dep.requirement.satisfied_by?(spec.version)
+ end
+ end
+
+ ##
+ # Removes the gemspec matching +full_name+ from the dependency list
+
+ def remove_by_name(full_name)
+ @specs.delete_if {|spec| spec.full_name == full_name }
+ end
+
+ ##
+ # Return a hash of predecessors. <tt>result[spec]</tt> is an Array of
+ # gemspecs that have a dependency satisfied by the named gemspec.
+
+ def spec_predecessors
+ result = Hash.new {|h,k| h[k] = [] }
+
+ specs = @specs.sort.reverse
+
+ specs.each do |spec|
+ specs.each do |other|
+ next if spec == other
+
+ other.dependencies.each do |dep|
+ if spec.satisfies_requirement? dep
+ result[spec] << other
+ end
+ end
+ end
+ end
+
+ result
+ end
+
+ def tsort_each_node(&block)
+ @specs.each(&block)
+ end
+
+ def tsort_each_child(node)
+ specs = @specs.sort.reverse
+
+ dependencies = node.runtime_dependencies
+ dependencies.push(*node.development_dependencies) if @development
+
+ dependencies.each do |dep|
+ specs.each do |spec|
+ if spec.satisfies_requirement? dep
+ yield spec
+ break
+ end
+ end
+ end
+ end
+
+ private
+
+ ##
+ # Count the number of gemspecs in the list +specs+ that are not in
+ # +ignored+.
+
+ def active_count(specs, ignored)
+ specs.count {|spec| ignored[spec.full_name].nil? }
+ end
+end
diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb
new file mode 100644
index 0000000000..eb503bb269
--- /dev/null
+++ b/lib/rubygems/deprecate.rb
@@ -0,0 +1,171 @@
+# frozen_string_literal: true
+
+module Gem
+ ##
+ # Provides 3 methods for declaring when something is going away.
+ #
+ # <tt>deprecate(name, repl, year, month)</tt>:
+ # Indicate something may be removed on/after a certain date.
+ #
+ # <tt>rubygems_deprecate(name, replacement=:none)</tt>:
+ # Indicate something will be removed in the next major RubyGems version,
+ # and (optionally) a replacement for it.
+ #
+ # +rubygems_deprecate_command+:
+ # Indicate a RubyGems command (in +lib/rubygems/commands/*.rb+) will be
+ # removed in the next RubyGems version.
+ #
+ # Also provides +skip_during+ for temporarily turning off deprecation warnings.
+ # This is intended to be used in the test suite, so deprecation warnings
+ # don't cause test failures if you need to make sure stderr is otherwise empty.
+ #
+ #
+ # Example usage of +deprecate+ and +rubygems_deprecate+:
+ #
+ # class Legacy
+ # def self.some_class_method
+ # # ...
+ # end
+ #
+ # def some_instance_method
+ # # ...
+ # end
+ #
+ # def some_old_method
+ # # ...
+ # end
+ #
+ # extend Gem::Deprecate
+ # deprecate :some_instance_method, "X.z", 2011, 4
+ # rubygems_deprecate :some_old_method, "Modern#some_new_method"
+ #
+ # class << self
+ # extend Gem::Deprecate
+ # deprecate :some_class_method, :none, 2011, 4
+ # end
+ # end
+ #
+ #
+ # Example usage of +rubygems_deprecate_command+:
+ #
+ # class Gem::Commands::QueryCommand < Gem::Command
+ # extend Gem::Deprecate
+ # rubygems_deprecate_command
+ #
+ # # ...
+ # end
+ #
+ #
+ # Example usage of +skip_during+:
+ #
+ # class TestSomething < Gem::Testcase
+ # def test_some_thing_with_deprecations
+ # Gem::Deprecate.skip_during do
+ # actual_stdout, actual_stderr = capture_output do
+ # Gem.something_deprecated
+ # end
+ # assert_empty actual_stdout
+ # assert_equal(expected, actual_stderr)
+ # end
+ # end
+ # end
+
+ module Deprecate
+ def self.skip # :nodoc:
+ @skip ||= false
+ end
+
+ def self.skip=(v) # :nodoc:
+ @skip = v
+ end
+
+ ##
+ # Temporarily turn off warnings. Intended for tests only.
+
+ def skip_during
+ original = Gem::Deprecate.skip
+ Gem::Deprecate.skip = true
+ yield
+ ensure
+ Gem::Deprecate.skip = original
+ end
+
+ def self.next_rubygems_major_version # :nodoc:
+ Gem::Version.new(Gem.rubygems_version.segments.first).bump
+ end
+
+ ##
+ # Simple deprecation method that deprecates +name+ by wrapping it up
+ # in a dummy method. It warns on each call to the dummy method
+ # telling the user of +repl+ (unless +repl+ is :none) and the
+ # year/month that it is planned to go away.
+
+ def deprecate(name, repl, year, month)
+ class_eval do
+ old = "_deprecated_#{name}"
+ alias_method old, name
+ define_method name do |*args, &block|
+ klass = is_a? Module
+ target = klass ? "#{self}." : "#{self.class}#"
+ msg = [
+ "NOTE: #{target}#{name} is deprecated",
+ repl == :none ? " with no replacement" : "; use #{repl} instead",
+ format(". It will be removed on or after %4d-%02d.", year, month),
+ "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}",
+ ]
+ warn "#{msg.join}." unless Gem::Deprecate.skip
+ send old, *args, &block
+ end
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
+ end
+ end
+
+ ##
+ # Simple deprecation method that deprecates +name+ by wrapping it up
+ # in a dummy method. It warns on each call to the dummy method
+ # telling the user of +repl+ (unless +repl+ is :none) and the
+ # Rubygems version that it is planned to go away.
+
+ def rubygems_deprecate(name, replacement = :none, version = nil)
+ class_eval do
+ old = "_deprecated_#{name}"
+ alias_method old, name
+ define_method name do |*args, &block|
+ klass = is_a? Module
+ target = klass ? "#{self}." : "#{self.class}#"
+ version ||= Gem::Deprecate.next_rubygems_major_version
+ msg = [
+ "NOTE: #{target}#{name} is deprecated",
+ replacement == :none ? " with no replacement" : "; use #{replacement} instead",
+ ". It will be removed in Rubygems #{version}",
+ "\n#{target}#{name} called from #{Gem.location_of_caller.join(":")}",
+ ]
+ warn "#{msg.join}." unless Gem::Deprecate.skip
+ send old, *args, &block
+ end
+ ruby2_keywords name if respond_to?(:ruby2_keywords, true)
+ end
+ end
+
+ # Deprecation method to deprecate Rubygems commands
+ def rubygems_deprecate_command(version = nil)
+ class_eval do
+ define_method "deprecated?" do
+ true
+ end
+
+ define_method "deprecation_warning" do
+ version ||= Gem::Deprecate.next_rubygems_major_version
+ msg = [
+ "#{command} command is deprecated",
+ ". It will be removed in Rubygems #{version}.\n",
+ ]
+
+ alert_warning msg.join.to_s unless Gem::Deprecate.skip
+ end
+ end
+ end
+
+ module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during
+ end
+end
diff --git a/lib/rubygems/doctor.rb b/lib/rubygems/doctor.rb
new file mode 100644
index 0000000000..4f26260d83
--- /dev/null
+++ b/lib/rubygems/doctor.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "user_interaction"
+
+##
+# Cleans up after a partially-failed uninstall or for an invalid
+# Gem::Specification.
+#
+# If a specification was removed by hand this will remove any remaining files.
+#
+# If a corrupt specification was installed this will clean up warnings by
+# removing the bogus specification.
+
+class Gem::Doctor
+ include Gem::UserInteraction
+
+ ##
+ # Maps a gem subdirectory to the files that are expected to exist in the
+ # subdirectory.
+
+ REPOSITORY_EXTENSION_MAP = [ # :nodoc:
+ ["specifications", ".gemspec"],
+ ["build_info", ".info"],
+ ["cache", ".gem"],
+ ["doc", ""],
+ ["extensions", ""],
+ ["gems", ""],
+ ["plugins", ""],
+ ].freeze
+
+ missing =
+ Gem::REPOSITORY_SUBDIRECTORIES.sort -
+ REPOSITORY_EXTENSION_MAP.map {|(k,_)| k }.sort
+
+ raise "Update REPOSITORY_EXTENSION_MAP, missing: #{missing.join ", "}" unless
+ missing.empty?
+
+ ##
+ # Creates a new Gem::Doctor that will clean up +gem_repository+. Only one
+ # gem repository may be cleaned at a time.
+ #
+ # If +dry_run+ is true no files or directories will be removed.
+
+ def initialize(gem_repository, dry_run = false)
+ @gem_repository = gem_repository
+ @dry_run = dry_run
+
+ @installed_specs = nil
+ end
+
+ ##
+ # Specs installed in this gem repository
+
+ def installed_specs # :nodoc:
+ @installed_specs ||= Gem::Specification.map(&:full_name)
+ end
+
+ ##
+ # Are we doctoring a gem repository?
+
+ def gem_repository?
+ !installed_specs.empty?
+ end
+
+ ##
+ # Cleans up uninstalled files and invalid gem specifications
+
+ def doctor
+ @orig_home = Gem.dir
+ @orig_path = Gem.path
+
+ say "Checking #{@gem_repository}"
+
+ Gem.use_paths @gem_repository.to_s
+
+ unless gem_repository?
+ say "This directory does not appear to be a RubyGems repository, " \
+ "skipping"
+ say
+ return
+ end
+
+ doctor_children
+
+ say
+ ensure
+ Gem.use_paths @orig_home, *@orig_path
+ end
+
+ ##
+ # Cleans up children of this gem repository
+
+ def doctor_children # :nodoc:
+ REPOSITORY_EXTENSION_MAP.each do |sub_directory, extension|
+ doctor_child sub_directory, extension
+ end
+ end
+
+ ##
+ # Removes files in +sub_directory+ with +extension+
+
+ def doctor_child(sub_directory, extension) # :nodoc:
+ directory = File.join(@gem_repository, sub_directory)
+
+ Dir.entries(directory).sort.each do |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/.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"
+
+ action = if @dry_run
+ "Extra"
+ else
+ FileUtils.rm_r(child)
+ "Removed"
+ end
+
+ say "#{action} #{type} #{sub_directory}/#{File.basename(child)}"
+ end
+ rescue Errno::ENOENT
+ # ignore
+ end
+end
diff --git a/lib/rubygems/errors.rb b/lib/rubygems/errors.rb
new file mode 100644
index 0000000000..4bbc5217e0
--- /dev/null
+++ b/lib/rubygems/errors.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+#--
+# This file contains all the various exceptions and other errors that are used
+# inside of RubyGems.
+#
+# DOC: Confirm _all_
+#++
+
+module Gem
+ ##
+ # Raised when RubyGems is unable to load or activate a gem. Contains the
+ # name and version requirements of the gem that either conflicts with
+ # already activated gems or that RubyGems is otherwise unable to activate.
+
+ class LoadError < ::LoadError
+ # Name of gem
+ attr_accessor :name
+
+ # Version requirement of gem
+ attr_accessor :requirement
+ end
+
+ ##
+ # Raised when trying to activate a gem, and that gem does not exist on the
+ # system. Instead of rescuing from this class, make sure to rescue from the
+ # superclass Gem::LoadError to catch all types of load errors.
+ class MissingSpecError < Gem::LoadError
+ def initialize(name, requirement, extra_message = nil)
+ @name = name
+ @requirement = requirement
+ @extra_message = extra_message
+ super(message)
+ end
+
+ def message # :nodoc:
+ build_message +
+ "Checked in 'GEM_PATH=#{Gem.path.join(File::PATH_SEPARATOR)}' #{@extra_message}, execute `gem env` for more information"
+ end
+
+ private
+
+ def build_message
+ total = Gem::Specification.stubs.size
+ "Could not find '#{name}' (#{requirement}) among #{total} total gem(s)\n"
+ end
+ end
+
+ ##
+ # Raised when trying to activate a gem, and the gem exists on the system, but
+ # not the requested version. Instead of rescuing from this class, make sure to
+ # rescue from the superclass Gem::LoadError to catch all types of load errors.
+ class MissingSpecVersionError < MissingSpecError
+ attr_reader :specs
+
+ def initialize(name, requirement, specs)
+ @specs = specs
+ super(name, requirement)
+ end
+
+ private
+
+ def build_message
+ names = specs.map(&:full_name)
+ "Could not find '#{name}' (#{requirement}) - did find: [#{names.join ","}]\n"
+ end
+ end
+
+ # Raised when there are conflicting gem specs loaded
+
+ class ConflictError < LoadError
+ ##
+ # A Hash mapping conflicting specifications to the dependencies that
+ # caused the conflict
+
+ attr_reader :conflicts
+
+ ##
+ # The specification that had the conflict
+
+ attr_reader :target
+
+ def initialize(target, conflicts)
+ @target = target
+ @conflicts = conflicts
+ @name = target.name
+
+ reason = conflicts.map do |act, dependencies|
+ "#{act.full_name} conflicts with #{dependencies.join(", ")}"
+ end.join ", "
+
+ # TODO: improve message by saying who activated `con`
+
+ super("Unable to activate #{target.full_name}, because #{reason}")
+ end
+ end
+
+ class ErrorReason; end
+
+ # Generated when trying to lookup a gem to indicate that the gem
+ # was found, but that it isn't usable on the current platform.
+ #
+ # fetch and install read these and report them to the user to aid
+ # in figuring out why a gem couldn't be installed.
+ #
+ class PlatformMismatch < ErrorReason
+ ##
+ # the name of the gem
+ attr_reader :name
+
+ ##
+ # the version
+ attr_reader :version
+
+ ##
+ # The platforms that are mismatched
+ attr_reader :platforms
+
+ def initialize(name, version)
+ @name = name
+ @version = version
+ @platforms = []
+ end
+
+ ##
+ # append a platform to the list of mismatched platforms.
+ #
+ # Platforms are added via this instead of injected via the constructor
+ # so that we can loop over a list of mismatches and just add them rather
+ # than perform some kind of calculation mismatch summary before creation.
+ def add_platform(platform)
+ @platforms << platform
+ end
+
+ ##
+ # A wordy description of the error.
+ def wordy
+ format("Found %s (%s), but was for platform%s %s", @name, @version, @platforms.size == 1 ? "" : "s", @platforms.join(" ,"))
+ end
+ end
+
+ ##
+ # An error that indicates we weren't able to fetch some
+ # data from a source
+
+ class SourceFetchProblem < ErrorReason
+ ##
+ # Creates a new SourceFetchProblem for the given +source+ and +error+.
+
+ def initialize(source, error)
+ @source = source
+ @error = error
+ end
+
+ ##
+ # The source that had the fetch problem.
+
+ attr_reader :source
+
+ ##
+ # The fetch error which is an Exception subclass.
+
+ attr_reader :error
+
+ ##
+ # An English description of the error.
+
+ def wordy
+ "Unable to download data from #{Gem::Uri.redact(@source.uri)} - #{@error.message}"
+ end
+
+ ##
+ # The "exception" alias allows you to call raise on a SourceFetchProblem.
+
+ alias_method :exception, :error
+ end
+end
diff --git a/lib/rubygems/exceptions.rb b/lib/rubygems/exceptions.rb
new file mode 100644
index 0000000000..e00a70c662
--- /dev/null
+++ b/lib/rubygems/exceptions.rb
@@ -0,0 +1,251 @@
+# frozen_string_literal: true
+
+require_relative "unknown_command_spell_checker"
+
+##
+# Base exception class for RubyGems. All exception raised by RubyGems are a
+# subclass of this one.
+class Gem::Exception < RuntimeError; end
+
+class Gem::CommandLineError < Gem::Exception; end
+
+class Gem::UnknownCommandError < Gem::Exception
+ attr_reader :unknown_command
+
+ def initialize(unknown_command)
+ self.class.attach_correctable
+
+ @unknown_command = unknown_command
+ super("Unknown command #{unknown_command}")
+ end
+
+ def self.attach_correctable
+ return if method_defined?(:corrections)
+
+ if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
+ DidYouMean.correct_error(Gem::UnknownCommandError, Gem::UnknownCommandSpellChecker)
+ end
+ end
+end
+
+class Gem::DependencyError < Gem::Exception; end
+
+class Gem::DependencyRemovalException < Gem::Exception; end
+
+##
+# Raised by Gem::Resolver when dependency resolution fails.
+
+class Gem::DependencyResolutionError < Gem::DependencyError
+ def initialize(conflict)
+ @explanation = conflict.explanation
+ super @explanation
+ end
+
+ def explanation
+ @explanation
+ end
+
+ def conflict
+ nil
+ end
+
+ def conflicting_dependencies
+ []
+ end
+end
+
+##
+# Raised when attempting to uninstall a gem that isn't in GEM_HOME.
+
+class Gem::GemNotInHomeException < Gem::Exception
+ attr_accessor :spec
+end
+
+###
+# Raised when removing a gem with the uninstall command fails
+
+class Gem::UninstallError < Gem::Exception
+ attr_accessor :spec
+end
+
+class Gem::DocumentError < Gem::Exception; end
+
+##
+# Potentially raised when a specification is validated.
+class Gem::EndOfYAMLException < Gem::Exception; end
+
+##
+# Signals that a file permission error is preventing the user from
+# operating on the given directory.
+
+class Gem::FilePermissionError < Gem::Exception
+ attr_reader :directory
+
+ def initialize(directory)
+ @directory = directory
+
+ super "You don't have write permissions for the #{directory} directory."
+ end
+end
+
+##
+# Used to raise parsing and loading errors
+class Gem::FormatException < Gem::Exception
+ attr_accessor :file_path
+end
+
+class Gem::GemNotFoundException < Gem::Exception; end
+
+class Gem::SpecificGemNotFoundException < Gem::GemNotFoundException
+ ##
+ # Creates a new SpecificGemNotFoundException for a gem with the given +name+
+ # and +version+. Any +errors+ encountered when attempting to find the gem
+ # are also stored.
+
+ def initialize(name, version, errors = nil)
+ super "Could not find a valid gem '#{name}' (#{version}) locally or in a repository"
+
+ @name = name
+ @version = version
+ @errors = errors
+ end
+
+ ##
+ # The name of the gem that could not be found.
+
+ attr_reader :name
+
+ ##
+ # The version of the gem that could not be found.
+
+ attr_reader :version
+
+ ##
+ # Errors encountered attempting to find the gem.
+
+ attr_reader :errors
+end
+
+Gem.deprecate_constant :SpecificGemNotFoundException
+
+class Gem::InstallError < Gem::Exception; end
+
+class Gem::RuntimeRequirementNotMetError < Gem::InstallError
+ attr_accessor :suggestion
+ def message
+ [suggestion, super].compact.join("\n\t")
+ end
+end
+
+##
+# Potentially raised when a specification is validated.
+class Gem::InvalidSpecificationException < Gem::Exception; end
+
+class Gem::OperationNotSupportedError < Gem::Exception; end
+
+##
+# Signals that a remote operation cannot be conducted, probably due to not
+# being connected (or just not finding host).
+#--
+# TODO: create a method that tests connection to the preferred gems server.
+# All code dealing with remote operations will want this. Failure in that
+# method should raise this error.
+class Gem::RemoteError < Gem::Exception; end
+
+class Gem::RemoteInstallationCancelled < Gem::Exception; end
+
+class Gem::RemoteInstallationSkipped < Gem::Exception; end
+
+##
+# Represents an error communicating via HTTP.
+class Gem::RemoteSourceException < Gem::Exception; end
+
+##
+# Raised when a gem dependencies file specifies a ruby version that does not
+# match the current version.
+
+class Gem::RubyVersionMismatch < Gem::Exception; end
+
+##
+# Raised by Gem::Validator when something is not right in a gem.
+
+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
+
+class Gem::SystemExitException < SystemExit
+ ##
+ # The exit code for the process
+
+ alias_method :exit_code, :status
+
+ ##
+ # Creates a new SystemExitException with the given +exit_code+
+
+ def initialize(exit_code)
+ super exit_code, "Exiting RubyGems with exit_code #{exit_code}"
+ end
+end
+
+##
+# Raised by Resolver when a dependency requests a gem for which
+# there is no spec.
+
+class Gem::UnsatisfiableDependencyError < Gem::DependencyError
+ ##
+ # The unsatisfiable dependency. This is a
+ # Gem::Resolver::DependencyRequest, not a Gem::Dependency
+
+ attr_reader :dependency
+
+ ##
+ # Errors encountered which may have contributed to this exception
+
+ attr_accessor :errors
+
+ ##
+ # Creates a new UnsatisfiableDependencyError for the unsatisfiable
+ # Gem::Resolver::DependencyRequest +dep+
+
+ 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(", ")}"
+ else
+ if dep.explicit?
+ super "Unable to resolve dependency: user requested '#{dep}'"
+ else
+ super "Unable to resolve dependency: '#{dep.request_context}' requires '#{dep}'"
+ end
+ end
+
+ @dependency = dep
+ @errors = []
+ end
+
+ ##
+ # The name of the unresolved dependency
+
+ def name
+ @dependency.name
+ end
+
+ ##
+ # The Requirement of the unresolved dependency (not Version).
+
+ def version
+ @dependency.requirement
+ end
+end
diff --git a/lib/rubygems/ext.rb b/lib/rubygems/ext.rb
new file mode 100644
index 0000000000..b5ca126a08
--- /dev/null
+++ b/lib/rubygems/ext.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+##
+# Classes for building C extensions live here.
+
+module Gem::Ext; end
+
+require_relative "ext/build_error"
+require_relative "ext/builder"
+require_relative "ext/configure_builder"
+require_relative "ext/ext_conf_builder"
+require_relative "ext/rake_builder"
+require_relative "ext/cmake_builder"
+require_relative "ext/cargo_builder"
diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb
new file mode 100644
index 0000000000..0329c1eec3
--- /dev/null
+++ b/lib/rubygems/ext/build_error.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+##
+# Raised when there is an error while building extensions.
+
+require_relative "../exceptions"
+
+class Gem::Ext::BuildError < Gem::InstallError
+end
diff --git a/lib/rubygems/ext/builder.rb b/lib/rubygems/ext/builder.rb
new file mode 100644
index 0000000000..e00cf159da
--- /dev/null
+++ b/lib/rubygems/ext/builder.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require_relative "../user_interaction"
+
+class Gem::Ext::Builder
+ include Gem::UserInteraction
+
+ class NoMakefileError < Gem::InstallError
+ end
+
+ attr_accessor :build_args # :nodoc:
+
+ def self.class_name
+ name =~ /Ext::(.*)Builder/
+ $1.downcase
+ end
+
+ def self.make(dest_path, results, make_dir = Dir.pwd, sitedir = nil, targets = ["clean", "", "install"],
+ target_rbconfig: Gem.target_rbconfig, n_jobs: nil)
+ unless File.exist? File.join(make_dir, "Makefile")
+ # No makefile exists, nothing to do.
+ raise NoMakefileError, "No Makefile found in #{make_dir}"
+ end
+
+ # try to find make program from Ruby configure arguments first
+ target_rbconfig["configure_args"] =~ /with-make-prog\=(\w+)/
+ make_program_name = ENV["MAKE"] || ENV["make"] || $1
+ make_program_name ||= RUBY_PLATFORM.include?("mswin") ? "nmake" : "make"
+ make_program = shellsplit(make_program_name)
+
+ is_nmake = /\bnmake/i.match?(make_program_name)
+ # The installation of the bundled gems is failed when DESTDIR is empty in mswin platform.
+ destdir = !is_nmake || ENV["DESTDIR"] && ENV["DESTDIR"] != "" ? format("DESTDIR=%s", ENV["DESTDIR"]) : ""
+
+ # nmake doesn't support parallel build
+ unless is_nmake
+ have_make_arguments = make_program.size > 1
+
+ if !have_make_arguments && !ENV["MAKEFLAGS"] && n_jobs
+ make_program << "-j#{n_jobs}"
+ end
+ end
+
+ env = [destdir]
+
+ if sitedir
+ env << format("sitearchdir=%s", sitedir)
+ env << format("sitelibdir=%s", sitedir)
+ end
+
+ targets.each do |target|
+ # Pass DESTDIR via command line to override what's in MAKEFLAGS
+ cmd = [
+ *make_program,
+ *env,
+ target,
+ ].reject(&:empty?)
+ begin
+ run(cmd, results, "make #{target}".rstrip, make_dir)
+ rescue Gem::InstallError
+ raise unless target == "clean" # ignore clean failure
+ end
+ end
+ end
+
+ def self.ruby
+ # Gem.ruby is quoted if it contains whitespace
+ cmd = shellsplit(Gem.ruby)
+
+ # This load_path is only needed when running rubygems test without a proper installation.
+ # Prepending it in a normal installation will cause problem with order of $LOAD_PATH.
+ # Therefore only add load_path if it is not present in the default $LOAD_PATH.
+ load_path = File.expand_path("../..", __dir__)
+ case load_path
+ when RbConfig::CONFIG["sitelibdir"], RbConfig::CONFIG["vendorlibdir"], RbConfig::CONFIG["rubylibdir"]
+ cmd
+ else
+ cmd << "-I#{load_path}"
+ end
+ end
+
+ def self.run(command, results, command_name = nil, dir = Dir.pwd, env = {})
+ verbose = Gem.configuration.really_verbose
+
+ begin
+ rubygems_gemdeps = ENV["RUBYGEMS_GEMDEPS"]
+ ENV["RUBYGEMS_GEMDEPS"] = nil
+ if verbose
+ puts("current directory: #{dir}")
+ p(command)
+ end
+ results << "current directory: #{dir}"
+ results << shelljoin(command)
+
+ require "open3"
+ # Set $SOURCE_DATE_EPOCH for the subprocess.
+ build_env = { "SOURCE_DATE_EPOCH" => Gem.source_date_epoch_string }.merge(env)
+ output, status = begin
+ Open3.popen2e(build_env, *command, chdir: dir) do |stdin, stdouterr, wait_thread|
+ stdin.close
+ output = String.new
+ while line = stdouterr.gets
+ output << line
+ if verbose
+ print line
+ end
+ end
+ [output, wait_thread.value]
+ end
+ rescue StandardError => error
+ raise Gem::InstallError, "#{command_name || class_name} failed#{error.message}"
+ end
+ unless verbose
+ results << output
+ end
+ ensure
+ ENV["RUBYGEMS_GEMDEPS"] = rubygems_gemdeps
+ end
+
+ unless status.success?
+ results << "Building has failed. See above output for more information on the failure." if verbose
+ end
+
+ yield(status, results) if block_given?
+
+ unless status.success?
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
+
+ raise Gem::InstallError, "#{command_name || class_name} failed#{exit_reason}"
+ end
+ end
+
+ def self.shellsplit(command)
+ require "shellwords"
+
+ Shellwords.split(command)
+ end
+
+ def self.shelljoin(command)
+ require "shellwords"
+
+ Shellwords.join(command)
+ end
+
+ ##
+ # Creates a new extension builder for +spec+. If the +spec+ does not yet
+ # have build arguments, saved, set +build_args+ which is an ARGV-style
+ # array.
+
+ def initialize(spec, build_args = spec.build_args, target_rbconfig = Gem.target_rbconfig, build_jobs = nil)
+ @spec = spec
+ @build_args = build_args
+ @gem_dir = spec.full_gem_path
+ @target_rbconfig = target_rbconfig
+ @build_jobs = build_jobs
+ end
+
+ ##
+ # Chooses the extension builder class for +extension+
+
+ def builder_for(extension) # :nodoc:
+ case extension
+ when /extconf/ then
+ Gem::Ext::ExtConfBuilder
+ when /configure/ then
+ Gem::Ext::ConfigureBuilder
+ when /rakefile/i, /mkrf_conf/i then
+ Gem::Ext::RakeBuilder
+ when /CMakeLists.txt/ then
+ Gem::Ext::CmakeBuilder.new
+ when /Cargo.toml/ then
+ Gem::Ext::CargoBuilder.new
+ else
+ build_error("No builder for extension '#{extension}'")
+ end
+ end
+
+ ##
+ # Logs the build +output+, then raises Gem::Ext::BuildError.
+
+ def build_error(output, backtrace = nil) # :nodoc:
+ gem_make_out = write_gem_make_out output
+
+ message = <<-EOF
+ERROR: Failed to build gem native extension.
+
+ #{output}
+
+Gem files will remain installed in #{@gem_dir} for inspection.
+Results logged to #{gem_make_out}
+EOF
+
+ raise Gem::Ext::BuildError, message, backtrace
+ end
+
+ def build_extension(extension, dest_path) # :nodoc:
+ results = []
+
+ builder = builder_for(extension)
+
+ extension_dir =
+ File.expand_path File.join(@gem_dir, File.dirname(extension))
+ lib_dir = File.join @spec.full_gem_path, @spec.raw_require_paths.first
+
+ begin
+ FileUtils.mkdir_p dest_path
+
+ results = builder.build(extension, dest_path,
+ results, @build_args, lib_dir, extension_dir, @target_rbconfig, n_jobs: @build_jobs)
+
+ verbose { results.join("\n") }
+
+ write_gem_make_out results.join "\n"
+ rescue StandardError => e
+ results << e.message
+ build_error(results.join("\n"), $@)
+ end
+ end
+
+ ##
+ # Builds extensions. Valid types of extensions are extconf.rb files,
+ # configure scripts and rakefiles or mkrf_conf files.
+
+ def build_extensions
+ return if @spec.extensions.empty?
+
+ if @build_args.empty?
+ say "Building native extensions. This could take a while..."
+ else
+ say "Building native extensions with: '#{@build_args.join " "}'"
+ say "This could take a while..."
+ end
+
+ dest_path = @spec.extension_dir
+
+ require "fileutils"
+ FileUtils.rm_f @spec.gem_build_complete_path
+
+ @spec.extensions.each do |extension|
+ build_extension extension, dest_path
+ end
+
+ FileUtils.touch @spec.gem_build_complete_path
+ end
+
+ ##
+ # Writes +output+ to gem_make.out in the extension install directory.
+
+ def write_gem_make_out(output) # :nodoc:
+ destination = File.join @spec.extension_dir, "gem_make.out"
+
+ FileUtils.mkdir_p @spec.extension_dir
+
+ File.open destination, "wb" do |io|
+ io.puts output
+ end
+
+ destination
+ end
+end
diff --git a/lib/rubygems/ext/cargo_builder.rb b/lib/rubygems/ext/cargo_builder.rb
new file mode 100644
index 0000000000..516459dd60
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder.rb
@@ -0,0 +1,349 @@
+# frozen_string_literal: true
+
+# This class is used by rubygems to build Rust extensions. It is a thin-wrapper
+# over the `cargo rustc` command which takes care of building Rust code in a way
+# that Ruby can use.
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ attr_accessor :spec, :runner, :profile
+
+ def initialize
+ require_relative "../command"
+ require_relative "cargo_builder/link_flag_converter"
+
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
+
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cargo_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "tempfile"
+ require "fileutils"
+
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rust extensions. Ignoring"
+ end
+
+ # Where's the Cargo.toml of the crate we're building
+ cargo_toml = File.join(cargo_dir, "Cargo.toml")
+ # What's the crate's name
+ crate_name = cargo_crate_name(cargo_dir, cargo_toml, results)
+
+ begin
+ # Create a tmp dir to do the build in
+ tmp_dest = Dir.mktmpdir(".gem.", cargo_dir)
+
+ # Run the build
+ cmd = cargo_command(cargo_toml, tmp_dest, args, crate_name)
+ runner.call(cmd, results, "cargo", cargo_dir, build_env)
+
+ # Where do we expect Cargo to write the compiled library
+ dylib_path = cargo_dylib_path(tmp_dest, crate_name)
+
+ # Helpful error if we didn't find the compiled library
+ raise DylibNotFoundError, tmp_dest unless File.exist?(dylib_path)
+
+ # Cargo and Ruby differ on how the library should be named, rename from
+ # what Cargo outputs to what Ruby expects
+ dlext_name = "#{crate_name}.#{makefile_config("DLEXT")}"
+ dlext_path = File.join(File.dirname(dylib_path), dlext_name)
+ FileUtils.cp(dylib_path, dlext_path)
+
+ nesting = extension_nesting(extension)
+
+ if Gem.install_extension_in_lib && lib_dir
+ nested_lib_dir = File.join(lib_dir, nesting)
+ FileUtils.mkdir_p nested_lib_dir
+ FileUtils.cp_r dlext_path, nested_lib_dir, remove_destination: true
+ end
+
+ # move to final destination
+ nested_dest_path = File.join(dest_path, nesting)
+ FileUtils.mkdir_p nested_dest_path
+ FileUtils.cp_r dlext_path, nested_dest_path, remove_destination: true
+ ensure
+ # clean up intermediary build artifacts
+ FileUtils.rm_rf tmp_dest if tmp_dest
+ end
+
+ results
+ end
+
+ def build_env
+ build_env = rb_config_env
+ build_env["RUBY_STATIC"] = "true" if ruby_static? && ENV.key?("RUBY_STATIC")
+ cfg = "--cfg=rb_sys_gem --cfg=rubygems --cfg=rubygems_#{Gem::VERSION.tr(".", "_")}"
+ build_env["RUSTFLAGS"] = [ENV["RUSTFLAGS"], cfg].compact.join(" ")
+ build_env
+ end
+
+ def cargo_command(cargo_toml, dest_path, args = [], crate_name = nil)
+ cmd = []
+ cmd += [cargo, "rustc"]
+ cmd += ["--crate-type", "cdylib"]
+ cmd += ["--target", ENV["CARGO_BUILD_TARGET"]] if ENV["CARGO_BUILD_TARGET"]
+ cmd += ["--target-dir", dest_path]
+ cmd += ["--manifest-path", cargo_toml]
+ cmd += ["--lib"]
+ cmd += ["--profile", profile.to_s]
+ cmd += ["--locked"]
+ cmd += Gem::Command.build_args
+ cmd += args
+ cmd += ["--"]
+ cmd += [*cargo_rustc_args(dest_path, crate_name)]
+ cmd
+ end
+
+ private
+
+ def cargo
+ ENV.fetch("CARGO", "cargo")
+ end
+
+ # returns the directory nesting of the extension, ignoring the first part, so
+ # "ext/foo/bar/Cargo.toml" becomes "foo/bar"
+ def extension_nesting(extension)
+ parts = extension.to_s.split(Regexp.union([File::SEPARATOR, File::ALT_SEPARATOR].compact))
+
+ parts = parts.each_with_object([]) do |segment, final|
+ next if segment == "."
+ if segment == ".."
+ raise Gem::InstallError, "extension outside of gem root" if final.empty?
+ next final.pop
+ end
+ final << segment
+ end
+
+ File.join(parts[1...-1])
+ end
+
+ def rb_config_env
+ result = {}
+ RbConfig::CONFIG.each {|k, v| result["RBCONFIG_#{k}"] = v }
+ result
+ end
+
+ def cargo_rustc_args(dest_dir, crate_name)
+ [
+ *linker_args,
+ *mkmf_libpath,
+ *rustc_dynamic_linker_flags(dest_dir, crate_name),
+ *rustc_lib_flags(dest_dir),
+ *platform_specific_rustc_args(dest_dir),
+ ]
+ end
+
+ def platform_specific_rustc_args(dest_dir, flags = [])
+ if mingw_target?
+ # On mingw platforms, mkmf adds libruby to the linker flags
+ flags += libruby_args(dest_dir)
+
+ # Make sure ALSR is used on mingw
+ # see https://github.com/rust-lang/rust/pull/75406/files
+ flags += ["-C", "link-arg=-Wl,--dynamicbase"]
+ flags += ["-C", "link-arg=-Wl,--disable-auto-image-base"]
+
+ # If the gem is installed on a host with build tools installed, but is
+ # run on one that isn't the missing libraries will cause the extension
+ # to fail on start.
+ flags += ["-C", "link-arg=-static-libgcc"]
+ elsif darwin_target?
+ # Ventura does not always have this flag enabled
+ flags += ["-C", "link-arg=-Wl,-undefined,dynamic_lookup"]
+ end
+
+ flags
+ end
+
+ # We want to use the same linker that Ruby uses, so that the linker flags from
+ # mkmf work properly.
+ def linker_args
+ cc_flag = self.class.shellsplit(makefile_config("CC"))
+ # Avoid to ccache like tool from Rust build
+ # see https://github.com/ruby/rubygems/pull/8521#issuecomment-2689854359
+ # ex. CC="ccache gcc" or CC="sccache clang --any --args"
+ cc_flag.shift if cc_flag.size >= 2 && !cc_flag[1].start_with?("-")
+ linker = cc_flag.shift
+ link_args = cc_flag.flat_map {|a| ["-C", "link-arg=#{a}"] }
+
+ return mswin_link_args if linker == "cl"
+
+ ["-C", "linker=#{linker}", *link_args]
+ end
+
+ def mswin_link_args
+ args = []
+ args += ["-l", makefile_config("LIBRUBYARG_SHARED").chomp(".lib")]
+ args += split_flags("LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args += split_flags("LOCAL_LIBS").flat_map {|lib| ["-l", lib.chomp(".lib")] }
+ args
+ end
+
+ def libruby_args(dest_dir)
+ libs = makefile_config(ruby_static? ? "LIBRUBYARG_STATIC" : "LIBRUBYARG_SHARED")
+ raw_libs = self.class.shellsplit(libs)
+ raw_libs.flat_map {|l| ldflag_to_link_modifier(l) }
+ end
+
+ def ruby_static?
+ return true if %w[1 true].include?(ENV["RUBY_STATIC"])
+
+ makefile_config("ENABLE_SHARED") == "no"
+ end
+
+ def cargo_dylib_path(dest_path, crate_name)
+ so_ext = RbConfig::CONFIG["SOEXT"]
+ prefix = so_ext == "dll" ? "" : "lib"
+ path_parts = [dest_path]
+ path_parts << ENV["CARGO_BUILD_TARGET"] if ENV["CARGO_BUILD_TARGET"]
+ path_parts += ["release", "#{prefix}#{crate_name}.#{so_ext}"]
+ File.join(*path_parts)
+ end
+
+ def cargo_crate_name(cargo_dir, manifest_path, results)
+ require "open3"
+ Gem.load_yaml
+
+ output, status =
+ begin
+ Open3.capture2e(cargo, "metadata", "--no-deps", "--format-version", "1", chdir: cargo_dir)
+ rescue StandardError => error
+ raise Gem::InstallError, "cargo metadata failed #{error.message}"
+ end
+
+ unless status.success?
+ if Gem.configuration.really_verbose
+ puts output
+ else
+ results << output
+ end
+
+ exit_reason =
+ if status.exited?
+ ", exit code #{status.exitstatus}"
+ elsif status.signaled?
+ ", uncaught signal #{status.termsig}"
+ end
+
+ raise Gem::InstallError, "cargo metadata failed#{exit_reason}"
+ end
+
+ # cargo metadata output is specified as json
+ require "json"
+ metadata = JSON.parse(output)
+ package = metadata["packages"].find {|pkg| normalize_path(pkg["manifest_path"]) == manifest_path }
+ unless package
+ found = metadata["packages"].map {|md| "#{md["name"]} at #{md["manifest_path"]}" }
+ raise Gem::InstallError, <<-EOF
+failed to determine cargo package name
+
+looking for: #{manifest_path}
+
+found:
+#{found.join("\n")}
+EOF
+ end
+ package["name"].tr("-", "_")
+ end
+
+ def normalize_path(path)
+ return path unless File::ALT_SEPARATOR
+
+ path.tr(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
+
+ def rustc_dynamic_linker_flags(dest_dir, crate_name)
+ split_flags("DLDFLAGS").
+ filter_map {|arg| maybe_resolve_ldflag_variable(arg, dest_dir, crate_name) }.
+ flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def rustc_lib_flags(dest_dir)
+ split_flags("LIBS").flat_map {|arg| ldflag_to_link_modifier(arg) }
+ end
+
+ def split_flags(var)
+ self.class.shellsplit(RbConfig::CONFIG.fetch(var, ""))
+ end
+
+ def ldflag_to_link_modifier(arg)
+ LinkFlagConverter.convert(arg)
+ end
+
+ def msvc_target?
+ makefile_config("target_os").include?("msvc")
+ end
+
+ def darwin_target?
+ makefile_config("target_os").include?("darwin")
+ end
+
+ def mingw_target?
+ makefile_config("target_os").include?("mingw")
+ end
+
+ def win_target?
+ target_platform = RbConfig::CONFIG["target_os"]
+ !!Gem::WIN_PATTERNS.find {|r| target_platform =~ r }
+ end
+
+ # Interpolate substitution vars in the arg (i.e. $(DEFFILE))
+ def maybe_resolve_ldflag_variable(input_arg, dest_dir, crate_name)
+ var_matches = input_arg.match(/\$\((\w+)\)/)
+
+ return input_arg unless var_matches
+
+ var_name = var_matches[1]
+
+ return input_arg if var_name.nil? || var_name.chomp.empty?
+
+ case var_name
+ # On windows, it is assumed that mkmf has setup an exports file for the
+ # extension, so we have to create one ourselves.
+ when "DEFFILE"
+ write_deffile(dest_dir, crate_name)
+ else
+ RbConfig::CONFIG[var_name]
+ end
+ end
+
+ def write_deffile(dest_dir, crate_name)
+ deffile_path = File.join(dest_dir, "#{crate_name}-#{RbConfig::CONFIG["arch"]}.def")
+ export_prefix = makefile_config("EXPORT_PREFIX") || ""
+
+ File.open(deffile_path, "w") do |f|
+ f.puts "EXPORTS"
+ f.puts "#{export_prefix.strip}Init_#{crate_name}"
+ end
+
+ deffile_path
+ end
+
+ # Corresponds to $(LIBPATH) in mkmf
+ def mkmf_libpath
+ ["-L", "native=#{makefile_config("libdir")}"]
+ end
+
+ def makefile_config(var_name)
+ val = RbConfig::MAKEFILE_CONFIG[var_name]
+
+ return unless val
+
+ RbConfig.expand(val.dup)
+ end
+
+ # Error raised when no cdylib artifact was created
+ class DylibNotFoundError < StandardError
+ def initialize(dir)
+ files = Dir.glob(File.join(dir, "**", "*")).map {|f| "- #{f}" }.join "\n"
+
+ super <<~MSG
+ Dynamic library not found for Rust extension (in #{dir})
+
+ Make sure you set "crate-type" in Cargo.toml to "cdylib"
+
+ Found files:
+ #{files}
+ MSG
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cargo_builder/link_flag_converter.rb b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
new file mode 100644
index 0000000000..e4d196cb10
--- /dev/null
+++ b/lib/rubygems/ext/cargo_builder/link_flag_converter.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Gem::Ext::CargoBuilder < Gem::Ext::Builder
+ # Converts Ruby link flags into something cargo understands
+ class LinkFlagConverter
+ FILTERED_PATTERNS = [
+ /compress-debug-sections/, # Not supported by all linkers, and not required for Rust
+ ].freeze
+
+ def self.convert(arg)
+ return [] if FILTERED_PATTERNS.any? {|p| p.match?(arg) }
+
+ case arg.chomp
+ when /^-L\s*(.+)$/
+ ["-L", "native=#{$1}"]
+ when /^--library=(\w+\S+)$/, /^-l\s*(\w+\S+)$/
+ ["-l", $1]
+ when /^-l\s*([^:\s])+/ # -lfoo, but not -l:libfoo.a
+ ["-l", $1]
+ when /^-F\s*(.*)$/
+ ["-l", "framework=#{$1}"]
+ else
+ ["-C", "link-args=#{arg}"]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/ext/cmake_builder.rb b/lib/rubygems/ext/cmake_builder.rb
new file mode 100644
index 0000000000..e660ed558b
--- /dev/null
+++ b/lib/rubygems/ext/cmake_builder.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+# This builder creates extensions defined using CMake. Its is invoked if a Gem's spec file
+# sets the `extension` property to a string that contains `CMakeLists.txt`.
+#
+# In general, CMake projects are built in two steps:
+#
+# * configure
+# * build
+#
+# The builder follow this convention. First it runs a configuration step and then it runs a build step.
+#
+# CMake projects can be quite configurable - it is likely you will want to specify options when
+# installing a gem. To pass options to CMake specify them after `--` in the gem install command. For example:
+#
+# gem install <gem_name> -- --preset <preset_name>
+#
+# Note that options are ONLY sent to the configure step - it is not currently possible to specify
+# options for the build step. If this becomes and issue then the CMake builder can be updated to
+# support build options.
+#
+# Useful options to know are:
+#
+# -G to specify a generator (-G Ninja is recommended)
+# -D<CMAKE_VARIABLE> to set a CMake variable (for example -DCMAKE_BUILD_TYPE=Release)
+# --preset <preset_name> to use a preset
+#
+# If the Gem author provides presets, via CMakePresets.json file, you will likely want to use one of them.
+# If not, you may wish to specify a generator. Ninja is recommended because it can build projects in parallel
+# and thus much faster than building them serially like Make does.
+
+class Gem::Ext::CmakeBuilder < Gem::Ext::Builder
+ attr_accessor :runner, :profile
+ def initialize
+ @runner = self.class.method(:run)
+ @profile = :release
+ end
+
+ def build(extension, dest_path, results, args = [], lib_dir = nil, cmake_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for CMake extensions. Ignoring"
+ end
+
+ # Figure the build dir
+ build_dir = File.join(cmake_dir, "build")
+
+ # Check if the gem defined presets
+ check_presets(cmake_dir, args, results)
+
+ # Configure
+ configure(cmake_dir, build_dir, dest_path, args, results)
+
+ # Compile
+ compile(cmake_dir, build_dir, args, results)
+
+ results
+ end
+
+ def configure(cmake_dir, build_dir, install_dir, args, results)
+ cmd = ["cmake",
+ cmake_dir,
+ "-B",
+ build_dir,
+ "-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=#{install_dir}", # Windows
+ "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=#{install_dir}", # Not Windows
+ *Gem::Command.build_args,
+ *args]
+
+ runner.call(cmd, results, "cmake_configure", cmake_dir)
+ end
+
+ def compile(cmake_dir, build_dir, args, results)
+ cmd = ["cmake",
+ "--build",
+ build_dir.to_s,
+ "--config",
+ @profile.to_s]
+
+ runner.call(cmd, results, "cmake_compile", cmake_dir)
+ end
+
+ private
+
+ def check_presets(cmake_dir, args, results)
+ # Return if the user specified a preset
+ return unless args.grep(/--preset/i).empty?
+
+ cmd = ["cmake",
+ "--list-presets"]
+
+ presets = Array.new
+ begin
+ runner.call(cmd, presets, "cmake_presets", cmake_dir)
+
+ # Remove the first two lines of the array which is the current_directory and the command
+ # that was run
+ presets = presets[2..].join
+ results << <<~EOS
+ The gem author provided a list of presets that can be used to build the gem. To use a preset specify it on the command line:
+
+ gem install <gem_name> -- --preset <preset_name>
+
+ #{presets}
+ EOS
+ rescue Gem::InstallError
+ # Do nothing, CMakePresets.json was not included in the Gem
+ end
+ end
+end
diff --git a/lib/rubygems/ext/configure_builder.rb b/lib/rubygems/ext/configure_builder.rb
new file mode 100644
index 0000000000..230b214b3c
--- /dev/null
+++ b/lib/rubygems/ext/configure_builder.rb
@@ -0,0 +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.
+#++
+
+class Gem::Ext::ConfigureBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, configure_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for configure-based extensions. Ignoring"
+ end
+
+ unless File.exist?(File.join(configure_dir, "Makefile"))
+ cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args]
+
+ run cmd, results, class_name, configure_dir
+ end
+
+ make dest_path, results, configure_dir, target_rbconfig: target_rbconfig, n_jobs: n_jobs
+
+ results
+ end
+end
diff --git a/lib/rubygems/ext/ext_conf_builder.rb b/lib/rubygems/ext/ext_conf_builder.rb
new file mode 100644
index 0000000000..822454355d
--- /dev/null
+++ b/lib/rubygems/ext/ext_conf_builder.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+class Gem::Ext::ExtConfBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ require "fileutils"
+ require "tempfile"
+
+ 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)
+
+ destdir = ENV["DESTDIR"]
+
+ begin
+ cmd = ruby << File.basename(extension)
+ cmd << "--target-rbconfig=#{target_rbconfig.path}" if target_rbconfig.path
+ cmd.push(*args)
+
+ run(cmd, results, class_name, extension_dir) do |s, r|
+ mkmf_log = File.join(extension_dir, "mkmf.log")
+ if File.exist? mkmf_log
+ unless s.success?
+ r << "To see why this extension failed to compile, please check" \
+ " the mkmf.log which can be found here:\n"
+ r << " " + File.join(dest_path, "mkmf.log") + "\n"
+ end
+ FileUtils.mv mkmf_log, dest_path
+ end
+ end
+
+ ENV["DESTDIR"] = nil
+
+ make dest_path, results, extension_dir, tmp_dest_relative, target_rbconfig: target_rbconfig, n_jobs: n_jobs
+
+ full_tmp_dest = File.join(extension_dir, tmp_dest_relative)
+
+ is_cross_compiling = target_rbconfig["platform"] != RbConfig::CONFIG["platform"]
+ # Do not copy extension libraries by default when cross-compiling
+ # not to conflict with the one already built for the host platform.
+ if Gem.install_extension_in_lib && lib_dir && !is_cross_compiling
+ FileUtils.mkdir_p lib_dir
+ entries = Dir.entries(full_tmp_dest) - %w[. ..]
+ entries = entries.map {|entry| File.join full_tmp_dest, entry }
+ FileUtils.cp_r entries, lib_dir, remove_destination: true
+ 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
+
+ make dest_path, results, extension_dir, tmp_dest_relative, ["clean"], target_rbconfig: target_rbconfig
+ ensure
+ ENV["DESTDIR"] = destdir
+ end
+
+ results
+ rescue Gem::Ext::Builder::NoMakefileError => error
+ results << error.message
+ results << "Skipping make for #{extension} as no Makefile was found."
+ # We are good, do not re-raise the error.
+ ensure
+ FileUtils.rm_rf tmp_dest if tmp_dest
+ end
+
+ def self.get_relative_path(path, base)
+ path[0..base.length - 1] = "." if path.start_with?(base)
+ path
+ end
+end
diff --git a/lib/rubygems/ext/rake_builder.rb b/lib/rubygems/ext/rake_builder.rb
new file mode 100644
index 0000000000..d702d7f339
--- /dev/null
+++ b/lib/rubygems/ext/rake_builder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+class Gem::Ext::RakeBuilder < Gem::Ext::Builder
+ def self.build(extension, dest_path, results, args = [], lib_dir = nil, extension_dir = Dir.pwd,
+ target_rbconfig = Gem.target_rbconfig, n_jobs: nil)
+ if target_rbconfig.path
+ warn "--target-rbconfig is not yet supported for Rake extensions. Ignoring"
+ end
+
+ if /mkrf_conf/i.match?(File.basename(extension))
+ run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
+ end
+
+ rake = ENV["rake"]
+
+ if rake
+ rake = shellsplit(rake)
+ else
+ begin
+ rake = ruby << "-rrubygems" << Gem.bin_path("rake", "rake")
+ rescue Gem::Exception
+ rake = [Gem.default_exec_format % "rake"]
+ end
+ end
+
+ rake_args = ["RUBYARCHDIR=#{dest_path}", "RUBYLIBDIR=#{dest_path}", *args]
+ run(rake + rake_args, results, class_name, extension_dir)
+
+ results
+ end
+end
diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb
new file mode 100644
index 0000000000..e60cebd0cb
--- /dev/null
+++ b/lib/rubygems/gem_runner.rb
@@ -0,0 +1,88 @@
+# 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 "../rubygems"
+require_relative "command_manager"
+
+##
+# Run an instance of the gem program.
+#
+# Gem::GemRunner is only intended for internal use by RubyGems itself. It
+# does not form any public API and may change at any time for any reason.
+#
+# If you would like to duplicate functionality of `gem` commands, use the
+# classes they call directly.
+
+class Gem::GemRunner
+ def initialize
+ @command_manager_class = Gem::CommandManager
+ @config_file_class = Gem::ConfigFile
+ end
+
+ ##
+ # Run the gem command with the following arguments.
+
+ def run(args)
+ validate_encoding args
+ build_args = extract_build_args args
+
+ do_configuration args
+
+ begin
+ Gem.load_env_plugins
+ rescue StandardError
+ nil
+ end
+ Gem.load_plugins
+
+ cmd = @command_manager_class.instance
+
+ cmd.command_names.each do |command_name|
+ config_args = Gem.configuration[command_name]
+ config_args = case config_args
+ when String
+ config_args.split " "
+ else
+ Array(config_args)
+ end
+ Gem::Command.add_specific_extra_args command_name, config_args
+ end
+
+ cmd.run Gem.configuration.args, build_args
+ end
+
+ ##
+ # Separates the build arguments (those following <code>--</code>) from the
+ # other arguments in the list.
+
+ def extract_build_args(args) # :nodoc:
+ return [] unless offset = args.index("--")
+
+ build_args = args.slice!(offset...args.length)
+
+ build_args.shift
+
+ build_args
+ end
+
+ private
+
+ def validate_encoding(args)
+ invalid_arg = args.find {|arg| !arg.valid_encoding? }
+
+ if invalid_arg
+ raise Gem::OptionParser::InvalidArgument.new("'#{invalid_arg.scrub}' has invalid encoding")
+ end
+ end
+
+ def do_configuration(args)
+ Gem.configuration = @config_file_class.new(args)
+ Gem.use_paths Gem.configuration[:gemhome], Gem.configuration[:gempath]
+ Gem::Command.extra_args = Gem.configuration[:gem]
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb
new file mode 100644
index 0000000000..9c22c14fad
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities.rb
@@ -0,0 +1,398 @@
+# 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 = [:index_rubygems, :push_rubygem, :yank_rubygem, :add_owner, :remove_owner, :access_webhooks].freeze
+ EXCLUSIVELY_API_SCOPES = [:show_dashboard].freeze
+
+ include Gem::Text
+
+ attr_writer :host
+ attr_writer :scope
+
+ ##
+ # Add the --key option
+
+ def add_key_option
+ add_option("-k", "--key KEYNAME", Symbol,
+ "Use the given API key",
+ "from #{Gem.configuration.credentials_path}") do |value,options|
+ options[:key] = value
+ end
+ end
+
+ ##
+ # Add the --otp option
+
+ def add_otp_option
+ add_option("--otp CODE",
+ "Digit code for multifactor authentication",
+ "You can also use the environment variable GEM_HOST_OTP_CODE") do |value, options|
+ options[:otp] = value
+ end
+ end
+
+ ##
+ # The API key from the command options or from the user's configuration.
+
+ def api_key
+ if ENV["GEM_HOST_API_KEY"]
+ ENV["GEM_HOST_API_KEY"]
+ elsif options[:key]
+ verify_api_key options[:key]
+ elsif Gem.configuration.api_keys.key?(host)
+ Gem.configuration.api_keys[host]
+ else
+ Gem.configuration.rubygems_api_key
+ end
+ end
+
+ ##
+ # The OTP code from the command options or from the user's configuration.
+
+ def otp
+ options[:otp] || ENV["GEM_HOST_OTP_CODE"]
+ end
+
+ def webauthn_enabled?
+ options[:webauthn]
+ end
+
+ ##
+ # The host to connect to either from the RUBYGEMS_HOST environment variable
+ # or from the user's configuration
+
+ def host
+ configured_host = Gem.host unless
+ Gem.configuration.disable_default_gem_server
+
+ @host ||=
+ begin
+ env_rubygems_host = ENV["RUBYGEMS_HOST"]
+ env_rubygems_host = nil if env_rubygems_host&.empty?
+
+ env_rubygems_host || configured_host
+ end
+ end
+
+ ##
+ # Creates an RubyGems API to +host+ and +path+ with the given HTTP +method+.
+ #
+ # 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, credentials: {}, &block)
+ require_relative "vendored_net_http"
+
+ self.host = host if host
+ unless self.host
+ alert_error "You must specify a gem server"
+ terminate_interaction(ERROR_CODE)
+ end
+
+ if allowed_push_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}"
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ uri = Gem::URI.parse "#{self.host}/#{path}"
+ response = request_with_otp(method, uri, &block)
+
+ if mfa_unauthorized?(response)
+ fetch_otp(credentials)
+ response = request_with_otp(method, uri, &block)
+ end
+
+ if api_key_forbidden?(response)
+ update_scope(scope)
+ request_with_otp(method, uri, &block)
+ else
+ response
+ end
+ end
+
+ def mfa_unauthorized?(response)
+ response.is_a?(Gem::Net::HTTPUnauthorized) && response.body.start_with?("You have enabled multifactor authentication")
+ end
+
+ def update_scope(scope)
+ 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."
+
+ 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 identifier, password
+ request.body = Gem::URI.encode_www_form({ api_key: api_key }.merge(update_scope_params))
+ end
+
+ with_response response do |_resp|
+ say "Added #{scope} scope to the existing API key"
+ end
+ end
+
+ ##
+ # Signs in with the RubyGems API at +sign_in_host+ and sets the rubygems API
+ # key.
+
+ def sign_in(sign_in_host = nil, scope: nil)
+ sign_in_host ||= host
+ pretty_host = pretty_host(sign_in_host)
+ if api_key
+ say "You are already signed in on #{pretty_host}."
+ return
+ end
+ say "Enter your #{pretty_host} credentials."
+ say "Don't have an account yet? " \
+ "Create one at #{sign_in_host}/sign_up"
+
+ identifier = ask "Username/email: "
+ password = ask_for_password " Password: "
+ say "\n"
+
+ key_name = get_key_name(scope)
+ scope_params = get_scope_params(scope)
+ profile = get_user_profile(identifier, password)
+ mfa_params = get_mfa_params(profile)
+ all_params = scope_params.merge(mfa_params)
+ warning = profile["warning"]
+ credentials = { identifier: identifier, password: password }
+
+ say "#{warning}\n" if warning
+
+ response = rubygems_api_request(:post, "api/v1/api_key",
+ sign_in_host, credentials: credentials, scope: scope) do |request|
+ request.basic_auth identifier, password
+ request.body = Gem::URI.encode_www_form({ name: key_name }.merge(all_params))
+ end
+
+ with_response response do |resp|
+ say "Signed in with API key: #{key_name}."
+ set_api_key host, resp.body
+ end
+ end
+
+ ##
+ # Retrieves the pre-configured API key +key+ or terminates interaction with
+ # an error.
+
+ def verify_api_key(key)
+ if Gem.configuration.api_keys.key? key
+ Gem.configuration.api_keys[key]
+ else
+ alert_error "No such API key. Please add it to your configuration (done automatically on initial `gem push`)."
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ ##
+ # If +response+ is an HTTP Success (2XX) response, yields the response if a
+ # block was given or shows the response body to the user.
+ #
+ # If the response was not successful, shows an error to the user including
+ # the +error_prefix+ and the response body. If the response was a permanent redirect,
+ # shows an error to the user including the redirect location.
+
+ def with_response(response, error_prefix = nil)
+ case response
+ when Gem::Net::HTTPSuccess then
+ if block_given?
+ yield response
+ else
+ say clean_text(response.body)
+ end
+ when Gem::Net::HTTPPermanentRedirect, Gem::Net::HTTPRedirection then
+ message = "The request has redirected permanently to #{response["location"]}. Please check your defined push host URL."
+ message = "#{error_prefix}: #{message}" if error_prefix
+
+ say clean_text(message)
+ terminate_interaction(ERROR_CODE)
+ else
+ message = response.body
+ message = "#{error_prefix}: #{message}" if error_prefix
+
+ say clean_text(message)
+ terminate_interaction(ERROR_CODE)
+ end
+ end
+
+ ##
+ # Returns true when the user has enabled multifactor authentication from
+ # +response+ text and no otp provided by options.
+
+ def set_api_key(host, key)
+ if default_host?
+ Gem.configuration.rubygems_api_key = key
+ else
+ Gem.configuration.set_api_key host, key
+ end
+ end
+
+ private
+
+ def request_with_otp(method, uri, &block)
+ request_method = Gem::Net::HTTP.const_get method.to_s.capitalize
+
+ Gem::RemoteFetcher.fetcher.request(uri, request_method) do |req|
+ req["OTP"] = otp if otp
+ block.call(req)
+ end
+ ensure
+ options[:otp] = nil if webauthn_enabled?
+ end
+
+ def fetch_otp(credentials)
+ options[:otp] = if webauthn_url = webauthn_verification_url(credentials)
+ server = TCPServer.new 0
+ port = server.addr[1].to_s
+
+ url_with_port = "#{webauthn_url}?port=#{port}"
+ say "You have enabled multi-factor authentication. Please visit the following URL to authenticate via security device. If you can't verify using WebAuthn but have OTP enabled, you can re-run the gem signin command with the `--otp [your_code]` option."
+ say ""
+ say url_with_port
+ say ""
+
+ threads = [WebauthnListener.listener_thread(host, server), WebauthnPoller.poll_thread(options, host, webauthn_url, credentials)]
+ otp_thread = wait_for_otp_thread(*threads)
+
+ threads.each(&:join)
+
+ if error = otp_thread[:error]
+ alert_error error.message
+ terminate_interaction(1)
+ end
+
+ options[:webauthn] = true
+
+ say "You are verified with a security device. You may close the browser window."
+ otp_thread[:otp]
+ else
+ say "You have enabled multi-factor authentication. Please enter OTP code."
+ ask "Code: "
+ end
+ end
+
+ def wait_for_otp_thread(*threads)
+ loop do
+ threads.each do |otp_thread|
+ return otp_thread unless otp_thread.alive?
+ end
+ sleep 0.1
+ end
+ ensure
+ threads.each(&:exit)
+ end
+
+ def webauthn_verification_url(credentials)
+ response = rubygems_api_request(:post, "api/v1/webauthn_verification") do |request|
+ if credentials.empty?
+ request.add_field "Authorization", api_key
+ else
+ request.basic_auth credentials[:identifier], credentials[:password]
+ end
+ end
+ response.is_a?(Gem::Net::HTTPSuccess) ? response.body : nil
+ end
+
+ def pretty_host(host)
+ if default_host?
+ "RubyGems.org"
+ else
+ host
+ end
+ end
+
+ def get_scope_params(scope)
+ scope_params = { index_rubygems: true, push_rubygem: true }
+
+ if scope
+ scope_params = { scope => true }
+ else
+ 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
+
+ scope_params
+ end
+
+ def default_host?
+ host == Gem::DEFAULT_HOST
+ end
+
+ def get_user_profile(identifier, password)
+ return {} unless default_host?
+
+ response = rubygems_api_request(:get, "api/v1/profile/me.yaml") do |request|
+ request.basic_auth identifier, password
+ end
+
+ with_response response do |resp|
+ Gem::ConfigFile.load_with_rubygems_config_hash(clean_text(resp.body))
+ end
+ end
+
+ def get_mfa_params(profile)
+ mfa_level = profile["mfa"]
+ params = {}
+ if ["ui_only", "ui_and_gem_signin"].include?(mfa_level)
+ selected = ask_yes_no("Would you like to enable MFA for this key? (strongly recommended)")
+ params["mfa"] = true if selected
+ end
+ params
+ end
+
+ def get_key_name(scope)
+ hostname = Socket.gethostname || "unknown-host"
+ user = ENV["USER"] || ENV["USERNAME"] || "unknown-user"
+ ts = Time.now.strftime("%Y%m%d%H%M%S")
+ default_key_name = "#{hostname}-#{user}-#{ts}"
+
+ key_name = ask "API Key name [#{default_key_name}]: " unless scope
+ if key_name.nil? || key_name.empty?
+ default_key_name
+ else
+ key_name
+ end
+ end
+
+ def api_key_forbidden?(response)
+ response.is_a?(Gem::Net::HTTPForbidden) && response.body.start_with?("The API key doesn't have access")
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb
new file mode 100644
index 0000000000..3f56a077c9
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_listener.rb
@@ -0,0 +1,112 @@
+# frozen_string_literal: true
+
+require_relative "webauthn_listener/response"
+
+##
+# The WebauthnListener class retrieves an OTP after a user successfully WebAuthns with the Gem host.
+# An instance opens a socket using the TCPServer instance given and listens for a request from the Gem host.
+# The request should be a GET request to the root path and contains the OTP code in the form
+# of a query parameter `code`. The listener will return the code which will be used as the OTP for
+# API requests.
+#
+# Types of responses sent by the listener after receiving a request:
+# - 200 OK: OTP code was successfully retrieved
+# - 204 No Content: If the request was an OPTIONS request
+# - 400 Bad Request: If the request did not contain a query parameter `code`
+# - 404 Not Found: The request was not to the root path
+# - 405 Method Not Allowed: OTP code was not retrieved because the request was not a GET/OPTIONS request
+#
+# Example usage:
+#
+# thread = Gem::WebauthnListener.listener_thread("https://rubygems.example", server)
+# thread.join
+# otp = thread[:otp]
+# error = thread[:error]
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnListener
+ attr_reader :host
+
+ def initialize(host)
+ @host = host
+ end
+
+ def self.listener_thread(host, server)
+ Thread.new do
+ thread = Thread.current
+ thread.abort_on_exception = true
+ thread.report_on_exception = false
+ thread[:otp] = new(host).wait_for_otp_code(server)
+ rescue Gem::WebauthnVerificationError => e
+ thread[:error] = e
+ ensure
+ server.close
+ end
+ end
+
+ def wait_for_otp_code(server)
+ loop do
+ socket = server.accept
+ request_line = socket.gets
+
+ method, req_uri, _protocol = request_line.split(" ")
+ req_uri = Gem::URI.parse(req_uri)
+
+ responder = SocketResponder.new(socket)
+
+ unless root_path?(req_uri)
+ responder.send(NotFoundResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Page at #{req_uri.path} not found."
+ end
+
+ case method.upcase
+ when "OPTIONS"
+ responder.send(NoContentResponse.for(host))
+ next # will be GET
+ when "GET"
+ if otp = parse_otp_from_uri(req_uri)
+ responder.send(OkResponse.for(host))
+ return otp
+ end
+ responder.send(BadRequestResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Did not receive OTP from #{host}."
+ else
+ responder.send(MethodNotAllowedResponse.for(host))
+ raise Gem::WebauthnVerificationError, "Invalid HTTP method #{method.upcase} received."
+ end
+ end
+ end
+
+ private
+
+ def root_path?(uri)
+ uri.path == "/"
+ end
+
+ def parse_otp_from_uri(uri)
+ query = uri.query
+ return unless query && !query.empty?
+
+ query.split("&") do |param|
+ key, value = param.split("=", 2)
+ if value && Gem::URI.decode_www_form_component(key) == "code"
+ return Gem::URI.decode_www_form_component(value)
+ end
+ end
+
+ nil
+ end
+
+ class SocketResponder
+ def initialize(socket)
+ @socket = socket
+ end
+
+ def send(response)
+ @socket.print response.to_s
+ @socket.close
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
new file mode 100644
index 0000000000..17baa64fff
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_listener/response.rb
@@ -0,0 +1,163 @@
+# frozen_string_literal: true
+
+##
+# The WebauthnListener Response class is used by the WebauthnListener to create
+# responses to be sent to the Gem host. It creates a Gem::Net::HTTPResponse instance
+# when initialized and can be converted to the appropriate format to be sent by a socket using `to_s`.
+# Gem::Net::HTTPResponse instances cannot be directly sent over a socket.
+#
+# Types of response classes:
+# - OkResponse
+# - NoContentResponse
+# - BadRequestResponse
+# - NotFoundResponse
+# - MethodNotAllowedResponse
+#
+# Example usage:
+#
+# server = TCPServer.new(0)
+# socket = server.accept
+#
+# response = OkResponse.for("https://rubygems.example")
+# socket.print response.to_s
+# socket.close
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnListener
+ class Response
+ attr_reader :http_response
+
+ def self.for(host)
+ new(host)
+ end
+
+ def initialize(host)
+ @host = host
+
+ build_http_response
+ end
+
+ def to_s
+ status_line = "HTTP/#{@http_response.http_version} #{@http_response.code} #{@http_response.message}\r\n"
+ headers = @http_response.to_hash.map {|header, value| "#{header}: #{value.join(", ")}\r\n" }.join + "\r\n"
+ body = @http_response.body ? "#{@http_response.body}\n" : ""
+
+ status_line + headers + body
+ end
+
+ private
+
+ # Must be implemented in subclasses
+ def code
+ raise NotImplementedError
+ end
+
+ def reason_phrase
+ raise NotImplementedError
+ end
+
+ def body; end
+
+ def build_http_response
+ response_class = Gem::Net::HTTPResponse::CODE_TO_OBJ[code.to_s]
+ @http_response = response_class.new("1.1", code, reason_phrase)
+ @http_response.instance_variable_set(:@read, true)
+
+ add_connection_header
+ add_access_control_headers
+ add_body
+ end
+
+ def add_connection_header
+ @http_response["connection"] = "close"
+ end
+
+ def add_access_control_headers
+ @http_response["access-control-allow-origin"] = @host
+ @http_response["access-control-allow-methods"] = "POST"
+ @http_response["access-control-allow-headers"] = %w[Content-Type Authorization x-csrf-token]
+ end
+
+ def add_body
+ return unless body
+ @http_response["content-type"] = "text/plain; charset=utf-8"
+ @http_response["content-length"] = body.bytesize
+ @http_response.instance_variable_set(:@body, body)
+ end
+ end
+
+ class OkResponse < Response
+ private
+
+ def code
+ 200
+ end
+
+ def reason_phrase
+ "OK"
+ end
+
+ def body
+ "success"
+ end
+ end
+
+ class NoContentResponse < Response
+ private
+
+ def code
+ 204
+ end
+
+ def reason_phrase
+ "No Content"
+ end
+ end
+
+ class BadRequestResponse < Response
+ private
+
+ def code
+ 400
+ end
+
+ def reason_phrase
+ "Bad Request"
+ end
+
+ def body
+ "missing code parameter"
+ end
+ end
+
+ class NotFoundResponse < Response
+ private
+
+ def code
+ 404
+ end
+
+ def reason_phrase
+ "Not Found"
+ end
+ end
+
+ class MethodNotAllowedResponse < Response
+ private
+
+ def code
+ 405
+ end
+
+ def reason_phrase
+ "Method Not Allowed"
+ end
+
+ def add_access_control_headers
+ super
+ @http_response["allow"] = %w[GET OPTIONS]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemcutter_utilities/webauthn_poller.rb b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb
new file mode 100644
index 0000000000..fe3f163a88
--- /dev/null
+++ b/lib/rubygems/gemcutter_utilities/webauthn_poller.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+##
+# The WebauthnPoller class retrieves an OTP after a user successfully WebAuthns. An instance
+# polls the Gem host for the OTP code. The polling request (api/v1/webauthn_verification/<webauthn_token>/status.json)
+# is sent to the Gem host every 5 seconds and will timeout after 5 minutes. If the status field in the json response
+# is "success", the code field will contain the OTP code.
+#
+# Example usage:
+#
+# thread = Gem::WebauthnPoller.poll_thread(
+# {},
+# "RubyGems.org",
+# "https://rubygems.org/api/v1/webauthn_verification/odow34b93t6aPCdY",
+# { email: "email@example.com", password: "password" }
+# )
+# thread.join
+# otp = thread[:otp]
+# error = thread[:error]
+#
+
+module Gem::GemcutterUtilities
+ class WebauthnPoller
+ include Gem::GemcutterUtilities
+ TIMEOUT_IN_SECONDS = 300
+
+ attr_reader :options, :host
+
+ def initialize(options, host)
+ @options = options
+ @host = host
+ end
+
+ def self.poll_thread(options, host, webauthn_url, credentials)
+ Thread.new do
+ thread = Thread.current
+ thread.abort_on_exception = true
+ thread.report_on_exception = false
+ thread[:otp] = new(options, host).poll_for_otp(webauthn_url, credentials)
+ rescue Gem::WebauthnVerificationError, Gem::Timeout::Error => e
+ thread[:error] = e
+ end
+ end
+
+ def poll_for_otp(webauthn_url, credentials)
+ Gem::Timeout.timeout(TIMEOUT_IN_SECONDS) do
+ loop do
+ response = webauthn_verification_poll_response(webauthn_url, credentials)
+ raise Gem::WebauthnVerificationError, response.message unless response.is_a?(Gem::Net::HTTPSuccess)
+
+ require "json"
+ parsed_response = JSON.parse(response.body)
+ case parsed_response["status"]
+ when "pending"
+ sleep 5
+ when "success"
+ return parsed_response["code"]
+ else
+ raise Gem::WebauthnVerificationError, parsed_response.fetch("message", "Invalid response from server")
+ end
+ end
+ end
+ end
+
+ private
+
+ def webauthn_verification_poll_response(webauthn_url, credentials)
+ webauthn_token = %r{(?<=\/)[^\/]+(?=$)}.match(webauthn_url)[0]
+ rubygems_api_request(:get, "api/v1/webauthn_verification/#{webauthn_token}/status.json") do |request|
+ if credentials.empty?
+ request.add_field "Authorization", api_key
+ elsif credentials[:identifier] && credentials[:password]
+ request.basic_auth credentials[:identifier], credentials[:password]
+ else
+ raise Gem::WebauthnVerificationError, "Provided missing credentials"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/gemspec_helpers.rb b/lib/rubygems/gemspec_helpers.rb
new file mode 100644
index 0000000000..2b20fcafa1
--- /dev/null
+++ b/lib/rubygems/gemspec_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+
+##
+# Mixin methods for commands that work with gemspecs.
+
+module Gem::GemspecHelpers
+ def find_gemspec(glob = "*.gemspec")
+ gemspecs = Dir.glob(glob).sort
+
+ if gemspecs.size > 1
+ alert_error "Multiple gemspecs found: #{gemspecs}, please specify one"
+ terminate_interaction(1)
+ end
+
+ gemspecs.first
+ end
+end
diff --git a/lib/rubygems/install_message.rb b/lib/rubygems/install_message.rb
new file mode 100644
index 0000000000..a24e26b918
--- /dev/null
+++ b/lib/rubygems/install_message.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "user_interaction"
+
+##
+# A default post-install hook that displays "Successfully installed
+# some_gem-1.0"
+
+Gem.post_install do |installer|
+ ui = Gem::DefaultUserInteraction.ui
+ ui.say "Successfully installed #{installer.spec.full_name}"
+end
diff --git a/lib/rubygems/install_update_options.rb b/lib/rubygems/install_update_options.rb
new file mode 100644
index 0000000000..e8859cadaf
--- /dev/null
+++ b/lib/rubygems/install_update_options.rb
@@ -0,0 +1,224 @@
+# 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 "../rubygems"
+require_relative "security_option"
+
+##
+# Mixin methods for install and update options for Gem::Commands
+
+module Gem::InstallUpdateOptions
+ include Gem::SecurityOption
+
+ ##
+ # Add the install/update options to the option parser.
+
+ def add_install_update_options
+ add_option(:"Install/Update", "-i", "--install-dir DIR",
+ "Gem repository directory to get installed",
+ "gems") do |value, options|
+ options[:install_dir] = File.expand_path(value)
+ end
+
+ add_option(:"Install/Update", "-n", "--bindir DIR",
+ "Directory where executables will be",
+ "placed when the gem is installed") do |value, options|
+ options[:bin_dir] = File.expand_path(value)
+ end
+
+ add_option(:"Install/Update", "-j", "--build-jobs VALUE", Integer,
+ "Specify the number of jobs to pass to `make` when installing",
+ "gems with native extensions.",
+ "Defaults to the number of processors.",
+ "This option is ignored on the mswin platform or",
+ "if the MAKEFLAGS environment variable is set.") do |value, options|
+ options[:build_jobs] = value
+ end
+
+ add_option(:"Install/Update", "--document [TYPES]", Array,
+ "Generate documentation for installed gems",
+ "List the documentation types you wish to",
+ "generate. For example: rdoc,ri") do |value, options|
+ options[:document] = case value
+ when nil then %w[ri]
+ when false then []
+ else value
+ end
+ end
+
+ add_option(:"Install/Update", "--build-root DIR",
+ "Temporary installation root. Useful for building",
+ "packages. Do not use this when installing remote gems.") do |value, options|
+ options[:build_root] = File.expand_path(value)
+ end
+
+ add_option(:"Install/Update", "--vendor",
+ "Install gem into the vendor directory.",
+ "Only for use by gem repackagers.") do |_value, options|
+ unless Gem.vendor_dir
+ raise Gem::OptionParser::InvalidOption.new "your platform is not supported"
+ end
+
+ options[:vendor] = true
+ options[:install_dir] = Gem.vendor_dir
+ end
+
+ add_option(:"Install/Update", "-N", "--no-document",
+ "Disable documentation generation") do |_value, options|
+ options[:document] = []
+ end
+
+ add_option(:"Install/Update", "-E", "--[no-]env-shebang",
+ "Rewrite the shebang line on installed",
+ "scripts to use /usr/bin/env") do |value, options|
+ options[:env_shebang] = value
+ end
+
+ add_option(:"Install/Update", "-f", "--[no-]force",
+ "Force gem to install, bypassing dependency",
+ "checks") do |value, options|
+ options[:force] = value
+ end
+
+ add_option(:"Install/Update", "-w", "--[no-]wrappers",
+ "Use bin wrappers for executables",
+ "Not available on dosish platforms") do |value, options|
+ options[:wrappers] = value
+ end
+
+ add_security_option
+
+ add_option(:"Install/Update", "--ignore-dependencies",
+ "Do not install any required dependent gems") do |value, options|
+ options[:ignore_dependencies] = value
+ end
+
+ add_option(:"Install/Update", "--[no-]format-executable",
+ "Make installed executable names match Ruby.",
+ "If Ruby is ruby18, foo_exec will be",
+ "foo_exec18") do |value, options|
+ options[:format_executable] = value
+ end
+
+ add_option(:"Install/Update", "--[no-]user-install",
+ "Install in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
+ options[:user_install] = value
+ end
+
+ add_option(:"Install/Update", "--development",
+ "Install additional development",
+ "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|
+ 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|
+ options[:conservative] = true
+ options[:minimal_deps] = true
+ end
+
+ add_option(:"Install/Update", "--[no-]minimal-deps",
+ "Don't upgrade any dependencies that already",
+ "meet version requirements") do |value, options|
+ options[:minimal_deps] = value
+ end
+
+ add_option(:"Install/Update", "--[no-]post-install-message",
+ "Print post install message") do |value, options|
+ options[:post_install_message] = value
+ end
+
+ add_option(:"Install/Update", "-g", "--file [FILE]",
+ "Read from a gem dependencies API file and",
+ "install the listed gems") do |v,_o|
+ v ||= Gem::GEM_DEP_FILES.find do |file|
+ File.exist? file
+ end
+
+ unless v
+ message = v ? v : "(tried #{Gem::GEM_DEP_FILES.join ", "})"
+
+ raise Gem::OptionParser::InvalidArgument,
+ "cannot find gem dependencies file #{message}"
+ end
+
+ options[:gemdeps] = v
+ end
+
+ add_option(:"Install/Update", "--without GROUPS", Array,
+ "Omit the named groups (comma separated)",
+ "when installing from a gem dependencies",
+ "file") do |v,_o|
+ options[:without_groups].concat v.map(&:intern)
+ end
+
+ add_option(:Deprecated, "--default",
+ "Add the gem's full specification to",
+ "specifications/default and extract only its bin") do |v,_o|
+ end
+
+ add_option(:"Install/Update", "--explain",
+ "Rather than install the gems, indicate which would",
+ "be installed") do |v,_o|
+ options[:explain] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]lock",
+ "Create a lock file (when used with -g/--file)") do |v,_o|
+ options[:lock] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]suggestions",
+ "Suggest alternates when gems are not found") do |v,_o|
+ options[:suggest_alternate] = v
+ end
+
+ add_option(:"Install/Update", "--target-rbconfig [FILE]",
+ "rbconfig.rb for the deployment target platform") do |v, _o|
+ Gem.set_target_rbconfig(v)
+ end
+
+ add_option(:"Install/Update", "--[no-]build-extension",
+ "Build native extensions during installation.",
+ "Defaults to true") do |v, _o|
+ options[:build_extension] = v
+ end
+
+ add_option(:"Install/Update", "--[no-]install-plugin",
+ "Install plugins during installation.",
+ "Defaults to true") do |v, _o|
+ options[:install_plugin] = v
+ end
+ end
+
+ ##
+ # Default options for the gem install and update commands.
+
+ def install_update_options
+ {
+ document: %w[ri],
+ }
+ end
+
+ ##
+ # Default description for the gem install and update commands.
+
+ def install_update_defaults_str
+ "--document=ri"
+ end
+end
diff --git a/lib/rubygems/installer.rb b/lib/rubygems/installer.rb
new file mode 100644
index 0000000000..a6e1dc4730
--- /dev/null
+++ b/lib/rubygems/installer.rb
@@ -0,0 +1,1043 @@
+# 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 "installer_uninstaller_utils"
+require_relative "exceptions"
+require_relative "package"
+require_relative "ext"
+require_relative "user_interaction"
+
+##
+# The installer installs the files contained in the .gem into the Gem.home.
+#
+# Gem::Installer does the work of putting files in all the right places on the
+# filesystem including unpacking the gem into its gem dir, installing the
+# gemspec in the specifications dir, storing the cached gem in the cache dir,
+# and installing either wrappers or symlinks for executables.
+#
+# The installer invokes pre and post install hooks. Hooks can be added either
+# through a rubygems_plugin.rb file in an installed gem or via a
+# rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb
+# file. See Gem.pre_install and Gem.post_install for details.
+
+class Gem::Installer
+ ##
+ # Paths where env(1) might live. Some systems are broken and have it in
+ # /bin
+
+ ENV_PATHS = %w[/usr/bin/env /bin/env].freeze
+
+ ##
+ # Deprecated in favor of Gem::Ext::BuildError
+
+ ExtensionBuildError = Gem::Ext::BuildError # :nodoc:
+
+ include Gem::UserInteraction
+
+ include Gem::InstallerUninstallerUtils
+
+ ##
+ # The directory a gem's executables will be installed into
+
+ attr_reader :bin_dir
+
+ attr_reader :build_root # :nodoc:
+
+ ##
+ # The gem repository the gem will be installed into
+
+ attr_reader :gem_home
+
+ ##
+ # The options passed when the Gem::Installer was instantiated.
+
+ attr_reader :options
+
+ ##
+ # The gem package instance.
+
+ attr_reader :package
+
+ class << self
+ ##
+ # Overrides the executable format.
+ #
+ # This is a sprintf format with a "%s" which will be replaced with the
+ # executable name. It is based off the ruby executable name's difference
+ # from "ruby".
+
+ attr_writer :exec_format
+
+ # Defaults to use Ruby's program prefix and suffix.
+ def exec_format
+ @exec_format ||= Gem.default_exec_format
+ end
+ end
+
+ ##
+ # Construct an installer object for the gem file located at +path+
+
+ def self.at(path, options = {})
+ security_policy = options[:security_policy]
+ package = Gem::Package.new path, security_policy
+ new package, options
+ end
+
+ class FakePackage
+ attr_accessor :spec
+
+ attr_accessor :dir_mode
+ attr_accessor :prog_mode
+ attr_accessor :data_mode
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def extract_files(destination_dir, pattern = "*")
+ FileUtils.mkdir_p destination_dir
+
+ spec.files.each do |file|
+ file = File.join destination_dir, file
+ next if File.exist? file
+ FileUtils.mkdir_p File.dirname(file)
+ File.open file, "w" do |fp|
+ fp.puts "# #{file}"
+ end
+ end
+ end
+
+ def copy_to(path)
+ end
+ end
+
+ ##
+ # Construct an installer object for an ephemeral gem (one where we don't
+ # actually have a .gem file, just a spec)
+
+ def self.for_spec(spec, options = {})
+ # FIXME: we should have a real Package class for this
+ new FakePackage.new(spec), options
+ end
+
+ ##
+ # Constructs an Installer instance that will install the gem at +package+ which
+ # can either be a path or an instance of Gem::Package. +options+ is a Hash
+ # with the following keys:
+ #
+ # :bin_dir:: Where to put a bin wrapper if needed.
+ # :development:: Whether or not development dependencies should be installed.
+ # :env_shebang:: Use /usr/bin/env in bin wrappers.
+ # :force:: Overrides all version checks and security policy checks, except
+ # for a signed-gems-only policy.
+ # :format_executable:: Format the executable the same as the Ruby executable.
+ # If your Ruby is ruby18, foo_exec will be installed as
+ # foo_exec18.
+ # :ignore_dependencies:: Don't raise if a dependency is missing.
+ # :install_dir:: The directory to install the gem into.
+ # :security_policy:: Use the specified security policy. See Gem::Security
+ # :user_install:: Indicate that the gem should be unpacked into the users
+ # personal gem directory.
+ # :only_install_dir:: Only validate dependencies against what is in the
+ # install_dir
+ # :wrappers:: Install wrappers if true, symlinks if false.
+ # :build_args:: An Array of arguments to pass to the extension builder
+ # process. If not set, then Gem::Command.build_args is used
+ # :post_install_message:: Print gem post install message if true
+
+ def initialize(package, options = {})
+ require "fileutils"
+
+ @options = options
+ @package = package
+
+ process_options
+
+ @package.dir_mode = options[:dir_mode]
+ @package.prog_mode = options[:prog_mode]
+ @package.data_mode = options[:data_mode]
+ end
+
+ ##
+ # Checks if +filename+ exists in +@bin_dir+.
+ #
+ # If +@force+ is set +filename+ is overwritten.
+ #
+ # If +filename+ exists and it is a RubyGems wrapper for a different gem, then
+ # the user is consulted.
+ #
+ # If +filename+ exists and +@bin_dir+ is Gem.default_bindir (/usr/local) the
+ # user is consulted.
+ #
+ # Otherwise +filename+ is overwritten.
+
+ def check_executable_overwrite(filename) # :nodoc:
+ return if @force
+
+ generated_bin = File.join @bin_dir, formatted_program_filename(filename)
+
+ return unless File.exist? generated_bin
+
+ ruby_executable = false
+ existing = nil
+
+ File.open generated_bin, "rb" do |io|
+ line = io.gets
+ shebang = /^#!.*ruby/o
+
+ # TruffleRuby uses a bash prelude in default launchers
+ if load_relative_enabled? || RUBY_ENGINE == "truffleruby"
+ until line.nil? || shebang.match?(line) do
+ line = io.gets
+ end
+ end
+
+ next unless line&.match?(shebang)
+
+ io.gets # blankline
+
+ # TODO: detect a specially formatted comment instead of trying
+ # to find a string inside Ruby code.
+ next unless io.gets&.include?("This file was generated by RubyGems")
+
+ ruby_executable = true
+ existing = io.read.slice(/
+ ^\s*(
+ Gem\.activate_and_load_bin_path\( |
+ load \s Gem\.activate_bin_path\(
+ )
+ (['"])(.*?)(\2),
+ /x, 3)
+ end
+
+ return if spec.name == existing
+
+ # somebody has written to RubyGems' directory, overwrite, too bad
+ return if Gem.default_bindir != @bin_dir && !ruby_executable
+
+ question = "#{spec.name}'s executable \"#{filename}\" conflicts with ".dup
+
+ if ruby_executable
+ question << (existing || "an unknown executable")
+
+ return if ask_yes_no "#{question}\nOverwrite the executable?", false
+
+ conflict = "installed executable from #{existing}"
+ else
+ question << generated_bin
+
+ return if ask_yes_no "#{question}\nOverwrite the executable?", false
+
+ conflict = generated_bin
+ end
+
+ raise Gem::InstallError,
+ "\"#{filename}\" from #{spec.name} conflicts with #{conflict}"
+ end
+
+ ##
+ # Lazy accessor for the spec's gem directory.
+
+ def gem_dir
+ @gem_dir ||= File.join(gem_home, "gems", spec.full_name)
+ end
+
+ ##
+ # Lazy accessor for the installer's spec.
+
+ def spec
+ @package.spec
+ end
+
+ ##
+ # Installs the gem and returns a loaded Gem::Specification for the installed
+ # gem.
+ #
+ # The gem will be installed with the following structure:
+ #
+ # @gem_home/
+ # cache/<gem-version>.gem #=> a cached copy of the installed gem
+ # gems/<gem-version>/... #=> extracted files
+ # specifications/<gem-version>.gemspec #=> the Gem::Specification
+
+ def install
+ pre_install_checks
+
+ run_pre_install_hooks
+
+ # Set loaded_from to ensure extension_dir is correct
+ spec.loaded_from = spec_file
+
+ # Completely remove any previous gem files
+ FileUtils.rm_rf gem_dir
+ FileUtils.rm_rf spec.extension_dir
+
+ dir_mode = options[:dir_mode]
+ FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755
+
+ extract_files
+
+ build_extensions
+ write_build_info_file
+ run_post_build_hooks
+
+ generate_bin
+ if options[:install_plugin] == false
+ remove_stale_plugins
+ warn_skipped_plugins
+ else
+ generate_plugins
+ end
+
+ write_spec
+ write_cache_file
+
+ File.chmod(dir_mode, gem_dir) if dir_mode
+
+ say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
+
+ Gem::Specification.add_spec(spec) unless @install_dir
+
+ load_plugin unless options[:install_plugin] == false
+
+ run_post_install_hooks
+
+ spec
+ 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|
+ 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
+ end
+
+ def run_post_build_hooks # :nodoc:
+ Gem.post_build_hooks.each do |hook|
+ next unless hook.call(self) == false
+ FileUtils.rm_rf gem_dir
+
+ location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/
+
+ message = "post-build hook#{location} failed for #{spec.full_name}"
+ raise Gem::InstallError, message
+ end
+ end
+
+ def run_post_install_hooks # :nodoc:
+ Gem.post_install_hooks.each do |hook|
+ hook.call self
+ end
+ end
+
+ ##
+ #
+ # Return an Array of Specifications contained within the gem_home
+ # we'll be installing into.
+
+ def installed_specs
+ @installed_specs ||= begin
+ specs = []
+
+ Gem::Util.glob_files_in_dir("*.gemspec", File.join(gem_home, "specifications")).each do |path|
+ spec = Gem::Specification.load path
+ specs << spec if spec
+ end
+
+ specs
+ end
+ end
+
+ ##
+ # Ensure that the dependency is satisfied by the current installation of
+ # gem. If it is not an exception is raised.
+ #
+ # spec :: Gem::Specification
+ # dependency :: Gem::Dependency
+
+ def ensure_dependency(spec, dependency)
+ unless installation_satisfies_dependency? dependency
+ raise Gem::InstallError, "#{spec.name} requires #{dependency}"
+ end
+ true
+ end
+
+ ##
+ # True if the gems in the system satisfy +dependency+.
+
+ def installation_satisfies_dependency?(dependency)
+ return true if @options[:development] && dependency.type == :development
+ return true if installed_specs.detect {|s| dependency.matches_spec? s }
+ return false if @only_install_dir
+ !dependency.matching_specs.empty?
+ end
+
+ ##
+ # The location of the spec file that is installed.
+ #
+
+ def spec_file
+ File.join gem_home, "specifications", "#{spec.full_name}.gemspec"
+ end
+
+ def default_spec_dir
+ dir = File.join(gem_home, "specifications", "default")
+ FileUtils.mkdir_p dir
+ dir
+ end
+
+ ##
+ # The location of the default spec file for default gems.
+ #
+
+ def default_spec_file
+ File.join default_spec_dir, "#{spec.full_name}.gemspec"
+ end
+
+ ##
+ # Writes the .gemspec specification (in Ruby) to the gem home's
+ # specifications directory.
+
+ def write_spec
+ spec.installed_by_version = Gem.rubygems_version
+
+ Gem.write_binary(spec_file, spec.to_ruby_for_cache)
+ end
+
+ ##
+ # Writes the full .gemspec specification (in Ruby) to the gem home's
+ # specifications/default directory.
+ #
+ # In contrast to #write_spec, this keeps file lists, so the `gem contents`
+ # command works.
+
+ def write_default_spec
+ Gem.write_binary(default_spec_file, spec.to_ruby)
+ end
+
+ ##
+ # Creates windows .bat files for easy running of commands
+
+ def generate_windows_script(filename, bindir)
+ if Gem.win_platform?
+ script_name = formatted_program_filename(filename) + ".bat"
+ script_path = File.join bindir, File.basename(script_name)
+ File.open script_path, "w" do |file|
+ file.puts windows_stub_script(bindir, filename)
+ end
+
+ verbose script_path
+ end
+ end
+
+ def generate_bin # :nodoc:
+ executables = spec.executables
+ return if executables.nil? || executables.empty?
+
+ if @gem_home == Gem.user_dir
+ # If we get here, then one of the following likely happened:
+ # - `--user-install` was specified
+ # - `Gem::PathSupport#home` fell back to `Gem.user_dir`
+ # - GEM_HOME was manually set to `Gem.user_dir`
+
+ check_that_user_bin_dir_is_in_path(executables)
+ end
+
+ ensure_writable_dir @bin_dir
+
+ executables.each do |filename|
+ bin_path = File.join gem_dir, spec.bindir, filename
+ next unless File.exist? bin_path
+
+ mode = File.stat(bin_path).mode
+ dir_mode = options[:prog_mode] || (mode | 0o111)
+
+ unless dir_mode == mode
+ File.chmod dir_mode, bin_path
+ end
+
+ check_executable_overwrite filename
+
+ if @wrappers
+ generate_bin_script filename, @bin_dir
+ else
+ generate_bin_symlink filename, @bin_dir
+ end
+ end
+ end
+
+ def generate_plugins # :nodoc:
+ latest = Gem::Specification.latest_spec_for(spec.name)
+ return if latest && latest.version > spec.version
+
+ ensure_writable_dir @plugins_dir
+
+ if spec.plugins.empty?
+ remove_plugins_for(spec, @plugins_dir)
+ else
+ regenerate_plugins_for(spec, @plugins_dir)
+ end
+ rescue ArgumentError => e
+ raise e, "#{latest.name} #{latest.version} #{spec.name} #{spec.version}: #{e.message}"
+ end
+
+ ##
+ # Creates the scripts to run the applications in the gem.
+ #--
+ # The Windows script is generated in addition to the regular one due to a
+ # bug or misfeature in the Windows shell's pipe. See
+ # https://blade.ruby-lang.org/ruby-talk/193379
+
+ def generate_bin_script(filename, bindir)
+ bin_script_path = File.join bindir, formatted_program_filename(filename)
+
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
+ FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
+
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
+ end
+ end
+
+ verbose bin_script_path
+
+ generate_windows_script filename, bindir
+ end
+
+ ##
+ # Creates the symlinks to run the applications in the gem. Moves
+ # the symlink if the gem being installed has a newer version.
+
+ def generate_bin_symlink(filename, bindir)
+ src = File.join gem_dir, spec.bindir, filename
+ dst = File.join bindir, formatted_program_filename(filename)
+
+ if File.exist? dst
+ if File.symlink? dst
+ link = File.readlink(dst).split File::SEPARATOR
+ cur_version = Gem::Version.create(link[-3].sub(/^.*-/, ""))
+ return if spec.version < cur_version
+ end
+ File.unlink dst
+ end
+
+ FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose
+ rescue NotImplementedError, SystemCallError
+ alert_warning "Unable to use symlinks, installing wrapper"
+ generate_bin_script filename, bindir
+ end
+
+ ##
+ # Generates a #! line for +bin_file_name+'s wrapper copying arguments if
+ # necessary.
+ #
+ # If the :custom_shebang config is set, then it is used as a template
+ # for how to create the shebang used for to run a gem's executables.
+ #
+ # The template supports 4 expansions:
+ #
+ # $env the path to the unix env utility
+ # $ruby the path to the currently running ruby interpreter
+ # $exec the path to the gem's executable
+ # $name the name of the gem the executable is for
+ #
+
+ def shebang(bin_file_name)
+ path = File.join gem_dir, spec.bindir, bin_file_name
+ first_line = File.open(path, "rb", &:gets) || ""
+
+ if first_line.start_with?("#!")
+ # Preserve extra words on shebang line, like "-w". Thanks RPA.
+ shebang = first_line.sub(/\A\#!.*?ruby\S*((\s+\S+)+)/, "#!#{Gem.ruby}")
+ opts = $1
+ shebang.strip! # Avoid nasty ^M issues.
+ end
+
+ if which = Gem.configuration[:custom_shebang]
+ # replace bin_file_name with "ruby" to avoid endless loops
+ which = which.gsub(/ #{bin_file_name}$/," #{ruby_install_name}")
+
+ which = which.gsub(/\$(\w+)/) do
+ case $1
+ when "env"
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ when "ruby"
+ "#{Gem.ruby}#{opts}"
+ when "exec"
+ bin_file_name
+ when "name"
+ spec.name
+ end
+ end
+
+ "#!#{which}"
+ elsif @env_shebang
+ # Create a plain shebang line.
+ @env_path ||= ENV_PATHS.find {|env_path| File.executable? env_path }
+ "#!#{@env_path} #{ruby_install_name}"
+ else
+ "#{bash_prolog_script}#!#{Gem.ruby}#{opts}"
+ end
+ end
+
+ ##
+ # Ensures the Gem::Specification written out for this gem is loadable upon
+ # installation.
+
+ def ensure_loadable_spec
+ ruby = spec.to_ruby_for_cache
+
+ begin
+ eval ruby
+ rescue StandardError, SyntaxError => e
+ raise Gem::InstallError,
+ "The specification for #{spec.full_name} is corrupt (#{e.class})"
+ end
+ end
+
+ def ensure_dependencies_met # :nodoc:
+ deps = spec.runtime_dependencies
+ deps |= spec.development_dependencies if @development
+
+ deps.each do |dep_gem|
+ ensure_dependency spec, dep_gem
+ end
+ end
+
+ def process_options # :nodoc:
+ @options = {
+ 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]
+ @user_install = options[:user_install]
+ @ignore_dependencies = options[:ignore_dependencies]
+ @format_executable = options[:format_executable]
+ @wrappers = options[:wrappers]
+ @only_install_dir = options[:only_install_dir]
+
+ @bin_dir = options[:bin_dir]
+ @development = options[:development]
+ @build_root = options[:build_root]
+
+ @build_args = options[:build_args]
+ @build_jobs = options[:build_jobs]
+
+ @gem_home = @install_dir || user_install_dir || Gem.dir
+
+ # If the user has asked for the gem to be installed in a directory that is
+ # the system gem directory, then use the system bin directory, else create
+ # (or use) a new bin dir under the gem_home.
+ @bin_dir ||= Gem.bindir(@gem_home)
+
+ @plugins_dir = Gem.plugindir(@gem_home)
+
+ unless @build_root.nil?
+ @bin_dir = File.join(@build_root, @bin_dir.gsub(/^[a-zA-Z]:/, ""))
+ @gem_home = File.join(@build_root, @gem_home.gsub(/^[a-zA-Z]:/, ""))
+ @plugins_dir = File.join(@build_root, @plugins_dir.gsub(/^[a-zA-Z]:/, ""))
+ alert_warning "You build with buildroot.\n Build root: #{@build_root}\n Bin dir: #{@bin_dir}\n Gem home: #{@gem_home}\n Plugins dir: #{@plugins_dir}"
+ end
+ end
+
+ def check_that_user_bin_dir_is_in_path(executables) # :nodoc:
+ user_bin_dir = @bin_dir || Gem.bindir(gem_home)
+ user_bin_dir = user_bin_dir.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+
+ path = ENV["PATH"]
+ path = path.tr(File::ALT_SEPARATOR, File::SEPARATOR) if File::ALT_SEPARATOR
+
+ if Gem.win_platform?
+ path = path.downcase
+ user_bin_dir = user_bin_dir.downcase
+ end
+
+ path = path.split(File::PATH_SEPARATOR)
+
+ unless path.include? user_bin_dir
+ unless !Gem.win_platform? && (path.include? user_bin_dir.sub(ENV["HOME"], "~"))
+ alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables (#{executables.join(", ")}) will not run."
+ end
+ end
+ end
+
+ def verify_gem_home # :nodoc:
+ FileUtils.mkdir_p gem_home, mode: options[:dir_mode] && 0o755
+ end
+
+ def verify_spec
+ unless Gem::Specification::VALID_NAME_PATTERN.match?(spec.name)
+ raise Gem::InstallError, "#{spec} has an invalid name"
+ end
+
+ if spec.raw_require_paths.any? {|path| path =~ /\R/ }
+ raise Gem::InstallError, "#{spec} has an invalid require_paths"
+ end
+
+ if spec.extensions.any? {|ext| ext =~ /\R/ }
+ raise Gem::InstallError, "#{spec} has an invalid extensions"
+ end
+
+ if /\R/.match?(spec.platform.to_s)
+ raise Gem::InstallError, "#{spec.platform} is an invalid platform"
+ end
+
+ unless /\A\d+\z/.match?(spec.specification_version.to_s)
+ raise Gem::InstallError, "#{spec} has an invalid specification_version"
+ end
+
+ if spec.dependencies.any? {|dep| dep.type != :runtime && dep.type != :development }
+ raise Gem::InstallError, "#{spec} has an invalid dependencies"
+ end
+
+ if spec.dependencies.any? {|dep| dep.name =~ /(?:\R|[<>])/ }
+ raise Gem::InstallError, "#{spec} has an invalid dependencies"
+ end
+
+ if spec.executables.any? {|name| !name.is_a?(String) || name != File.basename(name) || /\A\.\.?\z|\R/.match?(name) }
+ raise Gem::InstallError, "#{spec} has an invalid executable"
+ end
+
+ raise Gem::InstallError, "#{spec} has an invalid bindir" unless spec.bindir.is_a?(String)
+
+ expanded_gem_dir = File.expand_path(gem_dir)
+ expanded_bindir = File.expand_path(File.join(gem_dir, spec.bindir))
+ unless expanded_bindir == expanded_gem_dir || expanded_bindir.start_with?("#{expanded_gem_dir}/")
+ raise Gem::InstallError, "#{spec} has an invalid bindir"
+ end
+ end
+
+ ##
+ # Return the text for an application file.
+
+ def app_script_text(bin_file_name)
+ # NOTE: that the `load` lines cannot be indented, as old RG versions match
+ # against the beginning of the line
+ escaped_bin_file_name = bin_file_name.gsub(/[\\']/) {|c| "\\#{c}" }
+ <<~TEXT
+ #{shebang bin_file_name}
+ #
+ # This file was generated by RubyGems.
+ #
+ # The application '#{spec.name}' is installed as part of a gem, and
+ # this file is here to facilitate running it.
+ #
+
+ require 'rubygems'
+ #{gemdeps_load(spec.name)}
+ version = "#{Gem::Requirement.default_prerelease}"
+
+ str = ARGV.first
+ if str
+ str = str.b[/\\A_(.*)_\\z/, 1]
+ if str and Gem::Version.correct?(str)
+ #{explicit_version_requirement(spec.name)}
+ ARGV.shift
+ end
+ end
+
+ if Gem.respond_to?(:activate_and_load_bin_path)
+ Gem.activate_and_load_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
+ else
+ load Gem.activate_bin_path('#{spec.name}', '#{escaped_bin_file_name}', version)
+ end
+ TEXT
+ end
+
+ def gemdeps_load(name)
+ return "" if name == "bundler"
+
+ <<~TEXT
+
+ Gem.use_gemdeps
+ TEXT
+ end
+
+ def explicit_version_requirement(name)
+ code = "version = str"
+ return code unless name == "bundler"
+
+ code += <<~TEXT
+
+ ENV['BUNDLER_VERSION'] = str
+ TEXT
+ end
+
+ ##
+ # return the stub script text used to launch the true Ruby script
+
+ def windows_stub_script(bindir, bin_file_name)
+ rb_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 = "ruby.exe" if ruby_exe.empty?
+
+ if File.exist?(File.join(bindir, ruby_exe))
+ # stub & ruby.exe within same folder. Portable
+ <<~TEXT
+ @ECHO OFF
+ @"%~dp0#{ruby_exe}" "%~dpn0" %*
+ TEXT
+ elsif bindir.downcase.start_with? rb_topdir.downcase
+ # stub within ruby folder, but not standard bin. Portable
+ require "pathname"
+ from = Pathname.new bindir
+ to = Pathname.new "#{rb_topdir}/bin"
+ rel = to.relative_path_from from
+ <<~TEXT
+ @ECHO OFF
+ @"%~dp0#{rel}/#{ruby_exe}" "%~dpn0" %*
+ TEXT
+ else
+ # outside ruby folder, maybe -user-install or bundler. Portable, but ruby
+ # is dependent on PATH
+ <<~TEXT
+ @ECHO OFF
+ @#{ruby_exe} "%~dpn0" %*
+ TEXT
+ end
+ end
+ ##
+ # Builds extensions. Valid types of extensions are extconf.rb files,
+ # configure scripts and rakefiles or mkrf_conf files.
+
+ def build_extensions
+ if options[:build_extension] == false
+ warn_skipped_extensions
+ return
+ end
+
+ builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs
+
+ builder.build_extensions
+ end
+
+ def warn_skipped_extensions # :nodoc:
+ return if spec.extensions.empty?
+
+ alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \
+ "To build extensions, run: gem pristine #{spec.name} --extensions"
+ end
+
+ def warn_skipped_plugins # :nodoc:
+ return if spec.plugins.empty?
+
+ alert_warning "#{spec.full_name} contains plugins that were not installed.\n" \
+ "To install plugins, run: gem pristine #{spec.name} --only-plugins"
+ end
+
+ def remove_stale_plugins # :nodoc:
+ return unless spec.plugins.empty?
+
+ ensure_writable_dir @plugins_dir
+ remove_plugins_for(spec, @plugins_dir)
+ end
+
+ ##
+ # Reads the file index and extracts each file into the gem directory.
+ #
+ # Ensures that files can't be installed outside the gem directory.
+
+ def extract_files
+ @package.extract_files gem_dir
+ end
+
+ ##
+ # Extracts only the bin/ files from the gem into the gem directory.
+ # This is used by default gems to allow a gem-aware stub to function
+ # without the full gem installed.
+
+ def extract_bin
+ @package.extract_files gem_dir, "#{spec.bindir}/*"
+ end
+
+ ##
+ # Prefix and suffix the program filename the same as ruby.
+
+ def formatted_program_filename(filename)
+ if @format_executable
+ self.class.exec_format % File.basename(filename)
+ else
+ filename
+ end
+ end
+
+ ##
+ #
+ # Return the target directory where the gem is to be installed. This
+ # directory is not guaranteed to be populated.
+ #
+
+ def dir
+ gem_dir.to_s
+ end
+
+ ##
+ # Filename of the gem being installed.
+
+ def gem
+ @package.gem.path
+ end
+
+ ##
+ # Performs various checks before installing the gem such as the install
+ # repository is writable and its directories exist, required Ruby and
+ # rubygems versions are met and that dependencies are installed.
+ #
+ # Version and dependency checks are skipped if this install is forced.
+ #
+ # The dependent check will be skipped if the install is ignoring dependencies.
+
+ def pre_install_checks
+ verify_gem_home
+
+ # The name and require_paths must be verified first, since it could contain
+ # ruby code that would be eval'ed in #ensure_loadable_spec
+ verify_spec
+
+ ensure_loadable_spec
+
+ Gem.ensure_gem_subdirectories gem_home
+
+ return true if @force
+
+ ensure_dependencies_met unless @ignore_dependencies
+
+ true
+ end
+
+ ##
+ # Writes the file containing the arguments for building this gem's
+ # extensions.
+
+ def write_build_info_file
+ return if build_args.empty?
+
+ build_info_dir = File.join gem_home, "build_info"
+
+ dir_mode = options[:dir_mode]
+ FileUtils.mkdir_p build_info_dir, mode: dir_mode && 0o755
+
+ build_info_file = File.join build_info_dir, "#{spec.full_name}.info"
+
+ File.open build_info_file, "w" do |io|
+ build_args.each do |arg|
+ io.puts arg
+ end
+ end
+
+ File.chmod(dir_mode, build_info_dir) if dir_mode
+ end
+
+ ##
+ # Writes the .gem file to the cache directory
+
+ def write_cache_file
+ cache_file = File.join gem_home, "cache", spec.file_name
+ @package.copy_to cache_file
+ end
+
+ def ensure_writable_dir(dir) # :nodoc:
+ require "fileutils"
+ FileUtils.mkdir_p dir, mode: options[:dir_mode] && 0o755
+
+ raise Gem::FilePermissionError.new(dir) unless File.writable? dir
+ end
+
+ private
+
+ def user_install_dir
+ # never install to user home in --build-root mode
+ return unless @build_root.nil?
+
+ # Please note that @user_install might have three states:
+ # * `true`: `--user-install`
+ # * `false`: `--no-user-install` and
+ # * `nil`: option was not specified
+ if @user_install || (@user_install.nil? && Gem.default_user_install)
+ Gem.user_dir
+ end
+ end
+
+ def build_args
+ @build_args ||= begin
+ require_relative "command"
+ Gem::Command.build_args
+ end
+ end
+
+ def build_jobs
+ @build_jobs ||= begin
+ require "etc"
+ Etc.nprocessors + 1
+ rescue LoadError
+ 1
+ end
+ end
+
+ def rb_config
+ Gem.target_rbconfig
+ end
+
+ def ruby_install_name
+ rb_config["ruby_install_name"]
+ end
+
+ def load_relative_enabled?
+ rb_config["LIBRUBY_RELATIVE"] == "yes"
+ end
+
+ def bash_prolog_script
+ if load_relative_enabled?
+ <<~EOS
+ #!/bin/sh
+ # -*- ruby -*-
+ _=_\\
+ =begin
+ bindir="${0%/*}"
+ ruby="$bindir/#{ruby_install_name}"
+ if [ ! -f "$ruby" ]; then
+ ruby="#{ruby_install_name}"
+ fi
+ exec "$ruby" "-x" "$0" "$@"
+ =end
+ EOS
+ else
+ ""
+ end
+ end
+
+ def load_plugin
+ specs = Gem::Specification.find_all_by_name(spec.name)
+ # If old version already exists, this plugin isn't loaded
+ # immediately. It's for avoiding a case that multiple versions
+ # are loaded at the same time.
+ return unless specs.size == 1
+
+ plugin_files = spec.plugins.filter_map do |plugin|
+ path = File.join(@plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}")
+ path if File.exist?(path)
+ end
+ Gem.load_plugin_files(plugin_files) unless plugin_files.empty?
+ end
+end
diff --git a/lib/rubygems/installer_uninstaller_utils.rb b/lib/rubygems/installer_uninstaller_utils.rb
new file mode 100644
index 0000000000..c5c2a52bab
--- /dev/null
+++ b/lib/rubygems/installer_uninstaller_utils.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+##
+# Helper methods for both Gem::Installer and Gem::Uninstaller
+
+module Gem::InstallerUninstallerUtils
+ def regenerate_plugins_for(spec, plugins_dir)
+ plugins = spec.plugins
+ return if plugins.empty?
+
+ require "pathname"
+
+ spec.plugins.each do |plugin|
+ plugin_script_path = File.join plugins_dir, "#{spec.name}_plugin#{File.extname(plugin)}"
+
+ File.open plugin_script_path, "wb" do |file|
+ file.puts "require_relative '#{Pathname.new(plugin).relative_path_from(Pathname.new(plugins_dir))}'"
+ end
+
+ verbose plugin_script_path
+ end
+ end
+
+ 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
new file mode 100644
index 0000000000..3b88c43149
--- /dev/null
+++ b/lib/rubygems/local_remote_options.rb
@@ -0,0 +1,146 @@
+# 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 "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 Gem::URI::HTTP do |value|
+ begin
+ uri = Gem::URI.parse value
+ rescue Gem::URI::InvalidURIError
+ raise Gem::OptionParser::InvalidArgument, value
+ end
+
+ valid_uri_schemes = ["http", "https", "file", "s3"]
+ unless valid_uri_schemes.include?(uri.scheme)
+ msg = "Invalid uri scheme for #{value}\nPreface URLs with one of #{valid_uri_schemes.map {|s| "#{s}://" }}"
+ raise ArgumentError, msg
+ end
+
+ value
+ end
+ end
+
+ ##
+ # Add local/remote options to the command line parser.
+
+ def add_local_remote_options
+ add_option(:"Local/Remote", "-l", "--local",
+ "Restrict operations to the LOCAL domain") do |_value, options|
+ options[:domain] = :local
+ end
+
+ add_option(:"Local/Remote", "-r", "--remote",
+ "Restrict operations to the REMOTE domain") do |_value, options|
+ options[:domain] = :remote
+ end
+
+ add_option(:"Local/Remote", "-b", "--both",
+ "Allow LOCAL and REMOTE operations") do |_value, options|
+ options[:domain] = :both
+ end
+
+ add_bulk_threshold_option
+ add_clear_sources_option
+ add_source_option
+ add_proxy_option
+ add_update_sources_option
+ end
+
+ ##
+ # Add the --bulk-threshold option
+
+ 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|
+ Gem.configuration.bulk_threshold = value.to_i
+ end
+ end
+
+ ##
+ # Add the --clear-sources option
+
+ def add_clear_sources_option
+ add_option(:"Local/Remote", "--clear-sources",
+ "Clear the gem sources") do |_value, options|
+ Gem.sources = nil
+ options[:sources_cleared] = true
+ end
+ end
+
+ ##
+ # Add the --http-proxy option
+
+ def add_proxy_option
+ accept_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
+ Gem.configuration[:http_proxy] = options[:http_proxy]
+ end
+ end
+
+ ##
+ # Add the --source option
+
+ def add_source_option
+ accept_uri_http
+
+ add_option(:"Local/Remote", "-s", "--source URL", Gem::URI::HTTP,
+ "Append URL to list of remote gem sources") do |source, options|
+ source << "/" unless source.end_with?("/")
+
+ if options.delete :sources_cleared
+ Gem.sources = [source]
+ else
+ Gem.sources << source unless Gem.sources.include?(source)
+ end
+ end
+ end
+
+ ##
+ # Add the --update-sources option
+
+ def add_update_sources_option
+ add_option(:Deprecated, "-u", "--[no-]update-sources",
+ "Update local source cache") do |value, _options|
+ Gem.configuration.update_sources = value
+ end
+ end
+
+ ##
+ # Is fetching of local and remote information enabled?
+
+ def both?
+ options[:domain] == :both
+ end
+
+ ##
+ # Is local fetching enabled?
+
+ def local?
+ [:local, :both].include?(options[:domain])
+ end
+
+ ##
+ # Is remote fetching enabled?
+
+ def remote?
+ [:remote, :both].include?(options[:domain])
+ end
+end
diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb
new file mode 100644
index 0000000000..cbdf4d7ac5
--- /dev/null
+++ b/lib/rubygems/name_tuple.rb
@@ -0,0 +1,125 @@
+# 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 = Gem::Platform::RUBY)
+ @name = name
+ @version = version
+
+ platform &&= platform.to_s
+ platform = Gem::Platform::RUBY if !platform || platform.empty?
+ @platform = platform
+ end
+
+ attr_reader :name, :version, :platform
+
+ ##
+ # Turn an array of [name, version, platform] into an array of
+ # NameTuple objects.
+
+ def self.from_list(list)
+ list.map {|t| new(*t) }
+ end
+
+ ##
+ # Turn an array of NameTuple objects back into an array of
+ # [name, version, platform] tuples.
+
+ def self.to_basic(list)
+ list.map(&:to_a)
+ end
+
+ ##
+ # A null NameTuple, ie name=nil, version=0
+
+ def self.null
+ new nil, Gem::Version.new(0), nil
+ end
+
+ ##
+ # Returns the full name (name-version) of this Gem. Platform information is
+ # included if it is not the default Ruby platform. This mimics the behavior
+ # of Gem::Specification#full_name.
+
+ def full_name
+ case @platform
+ when nil, "", Gem::Platform::RUBY
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{@platform}"
+ end
+ end
+
+ ##
+ # Indicate if this NameTuple matches the current platform.
+
+ def match_platform?
+ Gem::Platform.match_gem? @platform, @name
+ end
+
+ ##
+ # Indicate if this NameTuple is for a prerelease version.
+ def prerelease?
+ @version.prerelease?
+ end
+
+ ##
+ # Return the name that the gemspec file would be
+
+ def spec_name
+ "#{full_name}.gemspec"
+ end
+
+ ##
+ # Convert back to the [name, version, platform] tuple
+
+ def to_a
+ [@name, @version, @platform]
+ end
+
+ alias_method :deconstruct, :to_a
+
+ def deconstruct_keys(keys)
+ { name: @name, version: @version, platform: @platform }
+ end
+
+ def inspect # :nodoc:
+ "#<Gem::NameTuple #{@name}, #{@version}, #{@platform}>"
+ end
+
+ alias_method :to_s, :inspect # :nodoc:
+
+ def <=>(other)
+ [@name, @version, Gem::Platform.sort_priority(@platform)] <=>
+ [other.name, other.version, Gem::Platform.sort_priority(other.platform)]
+ end
+
+ include Comparable
+
+ ##
+ # Compare with +other+. Supports another NameTuple or an Array
+ # in the [name, version, platform] format.
+
+ def ==(other)
+ case other
+ when self.class
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ when Array
+ to_a == other
+ else
+ false
+ end
+ end
+
+ alias_method :eql?, :==
+
+ def hash
+ to_a.hash
+ end
+end
diff --git a/lib/rubygems/openssl.rb b/lib/rubygems/openssl.rb
new file mode 100644
index 0000000000..c44f619c4c
--- /dev/null
+++ b/lib/rubygems/openssl.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+autoload :OpenSSL, "openssl"
+
+module Gem
+ HAVE_OPENSSL = defined? OpenSSL::SSL # :nodoc:
+end
diff --git a/lib/rubygems/package.rb b/lib/rubygems/package.rb
new file mode 100644
index 0000000000..7e41b18f66
--- /dev/null
+++ b/lib/rubygems/package.rb
@@ -0,0 +1,769 @@
+# 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 "win_platform"
+require_relative "security"
+require_relative "user_interaction"
+
+##
+# Example using a Gem::Package
+#
+# Builds a .gem file given a Gem::Specification. A .gem file is a tarball
+# which contains a data.tar.gz, metadata.gz, checksums.yaml.gz and possibly
+# signatures.
+#
+# require 'rubygems'
+# require 'rubygems/package'
+#
+# spec = Gem::Specification.new do |s|
+# s.summary = "Ruby based make-like utility."
+# s.name = 'rake'
+# s.version = PKG_VERSION
+# s.requirements << 'none'
+# s.files = PKG_FILES
+# s.description = <<-EOF
+# Rake is a Make-like program implemented in Ruby. Tasks
+# and dependencies are specified in standard Ruby syntax.
+# EOF
+# end
+#
+# Gem::Package.build spec
+#
+# Reads a .gem file.
+#
+# require 'rubygems'
+# require 'rubygems/package'
+#
+# the_gem = Gem::Package.new(path_to_dot_gem)
+# the_gem.contents # get the files in the gem
+# the_gem.extract_files destination_directory # extract the gem into a directory
+# the_gem.spec # get the spec out of the gem
+# the_gem.verify # check the gem is OK (contains valid gem specification, contains a not corrupt contents archive)
+#
+# #files are the files in the .gem tar file, not the Ruby files in the gem
+# #extract_files and #contents automatically call #verify
+
+class Gem::Package
+ include Gem::UserInteraction
+
+ class Error < Gem::Exception; end
+
+ class FormatError < Error
+ attr_reader :path
+
+ def initialize(message, source = nil)
+ if source
+ @path = source.is_a?(String) ? source : source.path
+
+ message += " in #{path}" if path
+ end
+
+ super message
+ end
+ end
+
+ class PathError < Error
+ def initialize(destination, destination_dir)
+ super format("installing into parent path %s of %s is not allowed", destination, destination_dir)
+ end
+ end
+
+ class SymlinkError < Error
+ def initialize(name, destination, destination_dir)
+ super format("installing symlink '%s' pointing to parent path %s of %s is not allowed", name, destination, destination_dir)
+ end
+ end
+
+ class NonSeekableIO < Error; end
+
+ class TooLongFileName < Error; end
+
+ ##
+ # Raised when a tar file is corrupt
+
+ class TarInvalidError < Error; end
+
+ attr_accessor :build_time # :nodoc:
+
+ ##
+ # Checksums for the contents of the package
+
+ attr_reader :checksums
+
+ ##
+ # The files in this package. This is not the contents of the gem, just the
+ # files in the top-level container.
+
+ attr_reader :files
+
+ ##
+ # Reference to the gem being packaged.
+
+ attr_reader :gem
+
+ ##
+ # The security policy used for verifying the contents of this package.
+
+ attr_accessor :security_policy
+
+ ##
+ # Sets the Gem::Specification to use to build this package.
+
+ attr_writer :spec
+
+ ##
+ # Permission for directories
+ attr_accessor :dir_mode
+
+ ##
+ # Permission for program files
+ attr_accessor :prog_mode
+
+ ##
+ # Permission for other files
+ attr_accessor :data_mode
+
+ def self.build(spec, skip_validation = false, strict_validation = false, file_name = nil)
+ gem_file = file_name || spec.file_name
+
+ package = new gem_file
+ package.spec = spec
+ package.build skip_validation, strict_validation
+
+ gem_file
+ end
+
+ ##
+ # Creates a new Gem::Package for the file at +gem+. +gem+ can also be
+ # provided as an IO object.
+ #
+ # If +gem+ is an existing file in the old format a Gem::Package::Old will be
+ # returned.
+
+ def self.new(gem, security_policy = nil)
+ gem = if gem.is_a?(Gem::Package::Source)
+ gem
+ elsif gem.respond_to? :read
+ Gem::Package::IOSource.new gem
+ else
+ Gem::Package::FileSource.new gem
+ end
+
+ return super unless self == Gem::Package
+ return super unless gem.present?
+
+ return super unless gem.start
+ return super unless gem.start.include? "MD5SUM ="
+
+ Gem::Package::Old.new gem
+ end
+
+ ##
+ # Extracts the Gem::Specification and raw metadata from the .gem file at
+ # +path+.
+ #--
+
+ def self.raw_spec(path, security_policy = nil)
+ format = new(path, security_policy)
+ spec = format.spec
+
+ metadata = nil
+
+ File.open path, Gem.binary_mode do |io|
+ tar = Gem::Package::TarReader.new io
+ tar.each_entry do |entry|
+ case entry.full_name
+ when "metadata" then
+ metadata = entry.read
+ when "metadata.gz" then
+ metadata = Gem::Util.gunzip entry.read
+ end
+ end
+ end
+
+ [spec, metadata]
+ end
+
+ ##
+ # Creates a new package that will read or write to the file +gem+.
+
+ def initialize(gem, security_policy) # :notnew:
+ require "zlib"
+
+ @gem = gem
+
+ @build_time = Gem.source_date_epoch
+ @checksums = {}
+ @contents = nil
+ @digests = Hash.new {|h, algorithm| h[algorithm] = {} }
+ @files = nil
+ @security_policy = security_policy
+ @signatures = {}
+ @signer = nil
+ @spec = nil
+ end
+
+ ##
+ # Copies this package to +path+ (if possible)
+
+ def copy_to(path)
+ FileUtils.cp @gem.path, path unless File.exist? path
+ end
+
+ ##
+ # Adds a checksum for each entry in the gem to checksums.yaml.gz.
+
+ def add_checksums(tar)
+ Gem.load_yaml
+
+ checksums_by_algorithm = Hash.new {|h, algorithm| h[algorithm] = {} }
+
+ @checksums.each do |name, digests|
+ digests.each do |algorithm, digest|
+ checksums_by_algorithm[algorithm][name] = digest.hexdigest
+ end
+ end
+
+ tar.add_file_signed "checksums.yaml.gz", 0o444, @signer do |io|
+ gzip_to io do |gz_io|
+ if Gem.use_psych?
+ Psych.dump checksums_by_algorithm, gz_io
+ else
+ gz_io.write Gem::YAMLSerializer.dump(checksums_by_algorithm)
+ end
+ end
+ end
+ end
+
+ ##
+ # Adds the files listed in the packages's Gem::Specification to data.tar.gz
+ # and adds this file to the +tar+.
+
+ def add_contents(tar) # :nodoc:
+ 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
+ end
+ end
+ end
+
+ @checksums["data.tar.gz"] = digests
+ end
+
+ ##
+ # Adds files included the package's Gem::Specification to the +tar+ file
+
+ def add_files(tar) # :nodoc:
+ @spec.files.each do |file|
+ stat = File.lstat file
+
+ if stat.symlink?
+ tar.add_symlink file, File.readlink(file), stat.mode
+ end
+
+ next unless stat.file?
+
+ tar.add_file_simple file, stat.mode, stat.size do |dst_io|
+ File.open file, "rb" do |src_io|
+ copy_stream(src_io, dst_io, stat.size)
+ end
+ end
+ end
+ end
+
+ ##
+ # Adds the package's Gem::Specification to the +tar+ file
+
+ def add_metadata(tar) # :nodoc:
+ digests = tar.add_file_signed "metadata.gz", 0o444, @signer do |io|
+ gzip_to io do |gz_io|
+ gz_io.write @spec.to_yaml
+ end
+ end
+
+ @checksums["metadata.gz"] = digests
+ end
+
+ ##
+ # Builds this package based on the specification set by #spec=
+
+ def build(skip_validation = false, strict_validation = false)
+ raise ArgumentError, "skip_validation = true and strict_validation = true are incompatible" if skip_validation && strict_validation
+
+ Gem.load_yaml
+
+ @spec.validate true, strict_validation unless skip_validation
+
+ setup_signer(
+ signer_options: {
+ expiration_length_days: Gem.configuration.cert_expiration_length_days,
+ }
+ )
+
+ @gem.with_write_io do |gem_io|
+ Gem::Package::TarWriter.new gem_io do |gem|
+ add_metadata gem
+ add_contents gem
+ add_checksums gem
+ end
+ end
+
+ say <<-EOM
+ Successfully built RubyGem
+ Name: #{@spec.name}
+ Version: #{@spec.version}
+ File: #{File.basename @gem.path}
+EOM
+ ensure
+ @signer = nil
+ end
+
+ ##
+ # A list of file names contained in this gem
+
+ def contents
+ return @contents if @contents
+
+ verify unless @spec
+
+ @contents = []
+
+ @gem.with_read_io do |io|
+ gem_tar = Gem::Package::TarReader.new io
+
+ gem_tar.each do |entry|
+ next unless entry.full_name == "data.tar.gz"
+
+ open_tar_gz entry do |pkg_tar|
+ pkg_tar.each do |contents_entry|
+ @contents << contents_entry.full_name
+ end
+ end
+
+ return @contents
+ end
+ end
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ ##
+ # Creates a digest of the TarEntry +entry+ from the digest algorithm set by
+ # the security policy.
+
+ def digest(entry) # :nodoc:
+ algorithms = if @checksums
+ @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
+
+ return @digests if algorithms.nil? || algorithms.empty?
+
+ 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
+
+ @digests
+ end
+
+ ##
+ # Extracts the files in this package into +destination_dir+
+ #
+ # If +pattern+ is specified, only entries matching that glob will be
+ # extracted.
+
+ def extract_files(destination_dir, pattern = "*")
+ verify unless @spec
+
+ FileUtils.mkdir_p destination_dir, mode: dir_mode && 0o755
+
+ @gem.with_read_io do |io|
+ reader = Gem::Package::TarReader.new io
+
+ reader.each do |entry|
+ next unless entry.full_name == "data.tar.gz"
+
+ extract_tar_gz entry, destination_dir, pattern
+
+ break # ignore further entries
+ end
+ end
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ ##
+ # Extracts all the files in the gzipped tar archive +io+ into
+ # +destination_dir+.
+ #
+ # If an entry in the archive contains a relative path above
+ # +destination_dir+ or an absolute path is encountered an exception is
+ # raised.
+ #
+ # If +pattern+ is specified, only entries matching that glob will be
+ # extracted.
+
+ def extract_tar_gz(io, destination_dir, pattern = "*") # :nodoc:
+ destination_dir = File.realpath(destination_dir)
+
+ directories = []
+ symlinks = []
+
+ open_tar_gz io do |tar|
+ tar.each do |entry|
+ full_name = entry.full_name
+ next unless File.fnmatch pattern, full_name, File::FNM_DOTMATCH
+
+ destination = install_location full_name, destination_dir
+
+ if entry.symlink?
+ link_target = entry.header.linkname
+ real_destination = link_target.start_with?("/") ? link_target : File.expand_path(link_target, File.dirname(destination))
+
+ raise Gem::Package::SymlinkError.new(full_name, real_destination, destination_dir) unless
+ normalize_path(real_destination).start_with? normalize_path(destination_dir + "/")
+
+ symlinks << [full_name, link_target, destination, real_destination]
+ end
+
+ mkdir =
+ if entry.directory?
+ destination
+ else
+ File.dirname destination
+ end
+
+ unless directories.include?(mkdir)
+ FileUtils.mkdir_p mkdir, mode: dir_mode ? 0o755 : (entry.header.mode if entry.directory?)
+ directories << mkdir
+ end
+
+ real_mkdir = File.realpath(mkdir)
+ unless real_mkdir == destination_dir || normalize_path(real_mkdir).start_with?(normalize_path(destination_dir + "/"))
+ raise Gem::Package::PathError.new(real_mkdir, destination_dir)
+ end
+
+ if entry.file?
+ File.open(destination, "wb") do |out|
+ copy_stream(tar.io, out, entry.size)
+ # Flush needs to happen before chmod because there could be data
+ # in the IO buffer that needs to be written, and that could be
+ # written after the chmod (on close) which would mess up the perms
+ out.flush
+ out.chmod file_mode(entry.header.mode) & ~File.umask
+ end
+ end
+
+ verbose destination
+ end
+ end
+
+ symlinks.each do |name, target, destination, real_destination|
+ if File.exist?(real_destination)
+ create_symlink(target, destination)
+ else
+ alert_warning "#{@spec.full_name} ships with a dangling symlink named #{name} pointing to missing #{target} file. Ignoring"
+ end
+ end
+
+ if dir_mode
+ File.chmod(dir_mode, *directories)
+ end
+ end
+
+ def file_mode(mode) # :nodoc:
+ ((mode & 0o111).zero? ? data_mode : prog_mode) ||
+ # If we're not using one of the default modes, then we're going to fall
+ # back to the mode from the tarball. In this case we need to mask it down
+ # to fit into 2^16 bits (the maximum value for a mode in CRuby since it
+ # gets put into an unsigned short).
+ (mode & ((1 << 16) - 1))
+ end
+
+ ##
+ # Gzips content written to +gz_io+ to +io+.
+ #--
+ # Also sets the gzip modification time to the package build time to ease
+ # testing.
+
+ def gzip_to(io) # :yields: gz_io
+ gz_io = Zlib::GzipWriter.new io, Zlib::BEST_COMPRESSION
+ gz_io.mtime = @build_time
+
+ yield gz_io
+ ensure
+ gz_io.close
+ end
+
+ ##
+ # Returns the full path for installing +filename+.
+ #
+ # If +filename+ is not inside +destination_dir+ an exception is raised.
+
+ def install_location(filename, destination_dir) # :nodoc:
+ raise Gem::Package::PathError.new(filename, destination_dir) if
+ filename.start_with? "/"
+
+ destination_dir = File.realpath(destination_dir)
+ destination = File.expand_path(filename, destination_dir)
+
+ raise Gem::Package::PathError.new(destination, destination_dir) unless
+ normalize_path(destination).start_with? normalize_path(destination_dir + "/")
+
+ destination
+ end
+
+ if Gem.win_platform?
+ def normalize_path(pathname) # :nodoc:
+ pathname.downcase
+ end
+ else
+ def normalize_path(pathname) # :nodoc:
+ pathname
+ end
+ end
+
+ ##
+ # Loads a Gem::Specification from the TarEntry +entry+
+
+ def load_spec_from_metadata(entry) # :nodoc:
+ limit = 10 * 1024 * 1024
+ case entry.full_name
+ when "metadata" then
+ @spec = Gem::Specification.from_yaml limit_read(entry, "metadata", limit)
+ when "metadata.gz" then
+ Zlib::GzipReader.wrap(entry, external_encoding: Encoding::UTF_8) do |gzio|
+ @spec = Gem::Specification.from_yaml limit_read(gzio, "metadata.gz", limit)
+ end
+ end
+ end
+
+ ##
+ # Opens +io+ as a gzipped tar archive
+
+ def open_tar_gz(io) # :nodoc:
+ Zlib::GzipReader.wrap io do |gzio|
+ tar = Gem::Package::TarReader.new gzio
+
+ yield tar
+ ensure
+ # Consume remaining gzip data to prevent the
+ # "attempt to close unfinished zstream; reset forced" warning
+ # when the GzipReader is closed with unconsumed compressed data.
+ begin
+ IO.copy_stream(gzio, IO::NULL)
+ rescue Zlib::GzipFile::Error, IOError
+ nil
+ end
+ end
+ end
+
+ ##
+ # Reads and loads checksums.yaml.gz from the tar file +gem+
+
+ def read_checksums(gem)
+ Gem.load_yaml
+
+ @checksums = gem.seek "checksums.yaml.gz" do |entry|
+ Zlib::GzipReader.wrap entry do |gz_io|
+ Gem::SafeYAML.safe_load limit_read(gz_io, "checksums.yaml.gz", 10 * 1024 * 1024)
+ end
+ end
+ end
+
+ ##
+ # Prepares the gem for signing and checksum generation. If a signing
+ # certificate and key are not present only checksum generation is set up.
+
+ def setup_signer(signer_options: {})
+ passphrase = ENV["GEM_PRIVATE_KEY_PASSPHRASE"]
+ if @spec.signing_key
+ @signer =
+ Gem::Security::Signer.new(
+ @spec.signing_key,
+ @spec.cert_chain,
+ passphrase,
+ signer_options
+ )
+
+ @spec.signing_key = nil
+ @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(&:to_pem) if
+ @signer.cert_chain
+ end
+ end
+
+ ##
+ # The spec for this gem.
+ #
+ # If this is a package for a built gem the spec is loaded from the
+ # gem and returned. If this is a package for a gem being built the provided
+ # spec is returned.
+
+ def spec
+ verify unless @spec
+
+ @spec
+ end
+
+ ##
+ # Verifies that this gem:
+ #
+ # * Contains a valid gem specification
+ # * Contains a contents archive
+ # * The contents archive is not corrupt
+ #
+ # After verification the gem specification from the gem is available from
+ # #spec
+
+ def verify
+ @files = []
+ @spec = nil
+
+ @gem.with_read_io do |io|
+ Gem::Package::TarReader.new io do |reader|
+ read_checksums reader
+
+ verify_files reader
+ end
+ end
+
+ verify_checksums @digests, @checksums
+
+ @security_policy&.verify_signatures @spec, @digests, @signatures
+
+ true
+ rescue Gem::Security::Exception
+ @spec = nil
+ @files = []
+ raise
+ rescue Errno::ENOENT => e
+ raise Gem::Package::FormatError.new e.message
+ rescue Zlib::GzipFile::Error, EOFError, Gem::Package::TarInvalidError => e
+ raise Gem::Package::FormatError.new e.message, @gem
+ end
+
+ private
+
+ ##
+ # Verifies the +checksums+ against the +digests+. This check is not
+ # cryptographically secure. Missing checksums are ignored.
+
+ def verify_checksums(digests, checksums) # :nodoc:
+ return unless checksums
+
+ checksums.sort.each do |algorithm, gem_digests|
+ gem_digests.sort.each do |file_name, gem_hexdigest|
+ computed_digest = digests[algorithm][file_name]
+
+ unless computed_digest.hexdigest == gem_hexdigest
+ raise Gem::Package::FormatError.new \
+ "#{algorithm} checksum mismatch for #{file_name}", @gem
+ end
+ end
+ end
+ end
+
+ ##
+ # Verifies +entry+ in a .gem file.
+
+ def verify_entry(entry)
+ file_name = entry.full_name
+ @files << file_name
+
+ case file_name
+ when /\.sig$/ then
+ @signatures[$`] = limit_read(entry, file_name, 1024 * 1024) if @security_policy
+ return
+ else
+ digest entry
+ end
+
+ load_spec_from_metadata entry
+ rescue StandardError
+ warn "Exception while verifying #{@gem.path}"
+ raise
+ end
+
+ ##
+ # Verifies the files of the +gem+
+
+ def verify_files(gem)
+ gem.each do |entry|
+ verify_entry entry
+ end
+
+ unless @spec
+ raise Gem::Package::FormatError.new "package metadata is missing", @gem
+ end
+
+ unless @files.include? "data.tar.gz"
+ raise Gem::Package::FormatError.new \
+ "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(", ")})"
+ end
+ end
+
+ if RUBY_ENGINE == "truffleruby"
+ def copy_stream(src, dst, size) # :nodoc:
+ dst.write src.read(size)
+ end
+ else
+ def copy_stream(src, dst, size) # :nodoc:
+ IO.copy_stream(src, dst, size)
+ end
+ end
+
+ def limit_read(io, name, limit)
+ bytes = io.read(limit + 1)
+ raise Gem::Package::FormatError, "#{name} is too big (over #{limit} bytes)" if bytes.size > limit
+ bytes
+ end
+
+ if Gem.win_platform?
+ # Create a symlink and fallback to copy the file or directory on Windows,
+ # where symlink creation needs special privileges in form of the Developer Mode.
+ # JRuby on Windows raises TypeError from the wincode path-conversion helper
+ # when it cannot create the symlink, so fall back to copy in that case too.
+ def create_symlink(old_name, new_name)
+ File.symlink(old_name, new_name)
+ rescue Errno::EACCES, TypeError
+ from = File.expand_path(old_name, File.dirname(new_name))
+ FileUtils.cp_r(from, new_name)
+ end
+ else
+ def create_symlink(old_name, new_name)
+ File.symlink(old_name, new_name)
+ end
+ end
+end
+
+require_relative "package/digest_io"
+require_relative "package/source"
+require_relative "package/file_source"
+require_relative "package/io_source"
+require_relative "package/old"
+require_relative "package/tar_header"
+require_relative "package/tar_reader"
+require_relative "package/tar_reader/entry"
+require_relative "package/tar_writer"
diff --git a/lib/rubygems/package/digest_io.rb b/lib/rubygems/package/digest_io.rb
new file mode 100644
index 0000000000..f04ab97462
--- /dev/null
+++ b/lib/rubygems/package/digest_io.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+##
+# IO wrapper that creates digests of contents written to the IO it wraps.
+
+class Gem::Package::DigestIO
+ ##
+ # Collected digests for wrapped writes.
+ #
+ # {
+ # 'SHA1' => #<OpenSSL::Digest: [...]>,
+ # 'SHA512' => #<OpenSSL::Digest: [...]>,
+ # }
+
+ attr_reader :digests
+
+ ##
+ # Wraps +io+ and updates digest for each of the digest algorithms in
+ # the +digests+ Hash. Returns the digests hash. Example:
+ #
+ # io = StringIO.new
+ # digests = {
+ # 'SHA1' => OpenSSL::Digest.new('SHA1'),
+ # 'SHA512' => OpenSSL::Digest.new('SHA512'),
+ # }
+ #
+ # Gem::Package::DigestIO.wrap io, digests do |digest_io|
+ # digest_io.write "hello"
+ # end
+ #
+ # digests['SHA1'].hexdigest #=> "aaf4c61d[...]"
+ # digests['SHA512'].hexdigest #=> "9b71d224[...]"
+
+ def self.wrap(io, digests)
+ digest_io = new io, digests
+
+ yield digest_io
+
+ digests
+ end
+
+ ##
+ # Creates a new DigestIO instance. Using ::wrap is recommended, see the
+ # ::wrap documentation for documentation of +io+ and +digests+.
+
+ def initialize(io, digests)
+ @io = io
+ @digests = digests
+ end
+
+ ##
+ # Writes +data+ to the underlying IO and updates the digests
+
+ def write(data)
+ result = @io.write data
+
+ @digests.each do |_, digest|
+ digest << data
+ end
+
+ result
+ end
+end
diff --git a/lib/rubygems/package/file_source.rb b/lib/rubygems/package/file_source.rb
new file mode 100644
index 0000000000..d9717e0f2a
--- /dev/null
+++ b/lib/rubygems/package/file_source.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+##
+# The primary source of gems is a file on disk, including all usages
+# internal to rubygems.
+#
+# This is a private class, do not depend on it directly. Instead, pass a path
+# object to `Gem::Package.new`.
+
+class Gem::Package::FileSource < Gem::Package::Source # :nodoc: all
+ attr_reader :path
+
+ def initialize(path)
+ @path = path
+ end
+
+ def start
+ @start ||= File.read path, 20
+ end
+
+ def present?
+ File.exist? path
+ end
+
+ def with_write_io(&block)
+ File.open path, "wb", &block
+ end
+
+ def with_read_io(&block)
+ File.open path, "rb", &block
+ end
+end
diff --git a/lib/rubygems/package/io_source.rb b/lib/rubygems/package/io_source.rb
new file mode 100644
index 0000000000..227835dfce
--- /dev/null
+++ b/lib/rubygems/package/io_source.rb
@@ -0,0 +1,48 @@
+# 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
+# rubygems.org.
+#
+# This is a private class, do not depend on it directly. Instead, pass an IO
+# object to `Gem::Package.new`.
+
+class Gem::Package::IOSource < Gem::Package::Source # :nodoc: all
+ attr_reader :io
+
+ def initialize(io)
+ @io = io
+ end
+
+ def start
+ @start ||= begin
+ if io.pos > 0
+ raise Gem::Package::Error, "Cannot read start unless IO is at start"
+ end
+
+ value = io.read 20
+ io.rewind
+ value
+ end
+ end
+
+ def present?
+ true
+ end
+
+ def with_read_io
+ yield io
+ ensure
+ io.rewind
+ end
+
+ def with_write_io
+ yield io
+ ensure
+ io.rewind
+ end
+
+ def path
+ end
+end
diff --git a/lib/rubygems/package/old.rb b/lib/rubygems/package/old.rb
new file mode 100644
index 0000000000..1a13ac3e29
--- /dev/null
+++ b/lib/rubygems/package/old.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+##
+# The format class knows the guts of the ancient .gem file format and provides
+# the capability to read such ancient gems.
+#
+# Please pretend this doesn't exist.
+
+class Gem::Package::Old < Gem::Package
+ undef_method :spec=
+
+ ##
+ # Creates a new old-format package reader for +gem+. Old-format packages
+ # cannot be written.
+
+ def initialize(gem, security_policy)
+ require "fileutils"
+ require "zlib"
+ Gem.load_yaml
+
+ @contents = nil
+ @gem = gem
+ @security_policy = security_policy
+ @spec = nil
+ end
+
+ ##
+ # A list of file names contained in this gem
+
+ def contents
+ verify
+
+ return @contents if @contents
+
+ @gem.with_read_io do |io|
+ read_until_dashes io # spec
+ header = file_list io
+
+ @contents = header.map {|file| file["path"] }
+ end
+ end
+
+ ##
+ # Extracts the files in this package into +destination_dir+
+
+ def extract_files(destination_dir)
+ verify
+
+ errstr = "Error reading files from gem"
+
+ @gem.with_read_io do |io|
+ read_until_dashes io # spec
+ header = file_list io
+ raise Gem::Exception, errstr unless header
+
+ header.each do |entry|
+ full_name = entry["path"]
+
+ destination = install_location full_name, destination_dir
+
+ file_data = String.new
+
+ read_until_dashes io do |line|
+ file_data << line
+ end
+
+ file_data = file_data.strip.unpack1("m")
+ file_data = Zlib::Inflate.inflate file_data
+
+ raise Gem::Package::FormatError, "#{full_name} in #{@gem} is corrupt" if
+ file_data.length != entry["size"].to_i
+
+ FileUtils.rm_rf destination
+
+ FileUtils.mkdir_p File.dirname(destination), mode: dir_mode && 0o755
+
+ File.open destination, "wb", file_mode(entry["mode"]) do |out|
+ out.write file_data
+ end
+
+ verbose destination
+ end
+ end
+ rescue Zlib::DataError
+ raise Gem::Exception, errstr
+ end
+
+ ##
+ # Reads the file list section from the old-format gem +io+
+
+ def file_list(io) # :nodoc:
+ header = String.new
+
+ read_until_dashes io do |line|
+ header << line
+ end
+
+ Gem::SafeYAML.safe_load header
+ end
+
+ ##
+ # Reads lines until a "---" separator is found
+
+ def read_until_dashes(io) # :nodoc:
+ while (line = io.gets) && line.chomp.strip != "---" do
+ yield line if block_given?
+ end
+ end
+
+ ##
+ # Skips the Ruby self-install header in +io+.
+
+ def skip_ruby(io) # :nodoc:
+ loop do
+ line = io.gets
+
+ return if line.chomp == "__END__"
+ break unless line
+ end
+
+ raise Gem::Exception, "Failed to find end of Ruby script while reading gem"
+ end
+
+ ##
+ # The specification for this gem
+
+ def spec
+ verify
+
+ return @spec if @spec
+
+ yaml = String.new
+
+ @gem.with_read_io do |io|
+ skip_ruby io
+ read_until_dashes io do |line|
+ yaml << line
+ end
+ end
+
+ begin
+ @spec = Gem::Specification.from_yaml yaml
+ rescue Psych::SyntaxError
+ raise Gem::Exception, "Failed to parse gem specification out of gem file"
+ end
+ rescue ArgumentError
+ raise Gem::Exception, "Failed to parse gem specification out of gem file"
+ end
+
+ ##
+ # Raises an exception if a security policy that verifies data is active.
+ # Old format gems cannot be verified as signed.
+
+ def verify
+ return true unless @security_policy
+
+ raise Gem::Security::Exception,
+ "old format gems do not contain signatures and cannot be verified" if
+ @security_policy.verify_data
+
+ true
+ end
+end
diff --git a/lib/rubygems/package/source.rb b/lib/rubygems/package/source.rb
new file mode 100644
index 0000000000..8c44f8c305
--- /dev/null
+++ b/lib/rubygems/package/source.rb
@@ -0,0 +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
new file mode 100644
index 0000000000..dd20d65080
--- /dev/null
+++ b/lib/rubygems/package/tar_header.rb
@@ -0,0 +1,277 @@
+# 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
+
+##
+#--
+# struct tarfile_entry_posix {
+# char name[100]; # ASCII + (Z unless filled)
+# char mode[8]; # 0 padded, octal, null
+# char uid[8]; # ditto
+# char gid[8]; # ditto
+# char size[12]; # 0 padded, octal, null
+# char mtime[12]; # 0 padded, octal, null
+# char checksum[8]; # 0 padded, octal, null, space
+# char typeflag[1]; # file: "0" dir: "5"
+# char linkname[100]; # ASCII + (Z unless filled)
+# char magic[6]; # "ustar\0"
+# char version[2]; # "00"
+# char uname[32]; # ASCIIZ
+# char gname[32]; # ASCIIZ
+# char devmajor[8]; # 0 padded, octal, null
+# char devminor[8]; # o padded, octal, null
+# char prefix[155]; # ASCII + (Z unless filled)
+# };
+#++
+# A header for a tar file
+
+class Gem::Package::TarHeader
+ ##
+ # Fields in the tar header
+
+ FIELDS = [
+ :checksum,
+ :devmajor,
+ :devminor,
+ :gid,
+ :gname,
+ :linkname,
+ :magic,
+ :mode,
+ :mtime,
+ :name,
+ :prefix,
+ :size,
+ :typeflag,
+ :uid,
+ :uname,
+ :version,
+ ].freeze
+
+ ##
+ # Pack format for a tar header
+
+ PACK_FORMAT = ("a100" + # name
+ "a8" + # mode
+ "a8" + # uid
+ "a8" + # gid
+ "a12" + # size
+ "a12" + # mtime
+ "a7a" + # chksum
+ "a" + # typeflag
+ "a100" + # linkname
+ "a6" + # magic
+ "a2" + # version
+ "a32" + # uname
+ "a32" + # gname
+ "a8" + # devmajor
+ "a8" + # devminor
+ "a155").freeze # prefix
+
+ ##
+ # Unpack format for a tar header
+
+ UNPACK_FORMAT = ("A100" + # name
+ "A8" + # mode
+ "A8" + # uid
+ "A8" + # gid
+ "A12" + # size
+ "A12" + # mtime
+ "A8" + # checksum
+ "A" + # typeflag
+ "A100" + # linkname
+ "A6" + # magic
+ "A2" + # version
+ "A32" + # uname
+ "A32" + # gname
+ "A8" + # devmajor
+ "A8" + # devminor
+ "A155").freeze # prefix
+
+ attr_reader(*FIELDS)
+
+ EMPTY_HEADER = ("\0" * 512).b.freeze # :nodoc:
+
+ ##
+ # Creates a tar header from IO +stream+
+
+ def self.from(stream)
+ header = stream.read 512
+ return EMPTY if header == EMPTY_HEADER
+
+ fields = header.unpack UNPACK_FORMAT
+
+ new name: fields.shift,
+ mode: strict_oct(fields.shift),
+ uid: oct_or_256based(fields.shift),
+ gid: oct_or_256based(fields.shift),
+ size: strict_oct(fields.shift),
+ mtime: strict_oct(fields.shift),
+ checksum: strict_oct(fields.shift),
+ typeflag: fields.shift,
+ linkname: fields.shift,
+ magic: fields.shift,
+ version: strict_oct(fields.shift),
+ uname: fields.shift,
+ gname: fields.shift,
+ devmajor: strict_oct(fields.shift),
+ devminor: strict_oct(fields.shift),
+ prefix: fields.shift,
+
+ empty: false
+ end
+
+ def self.strict_oct(str)
+ str.strip!
+ return str.oct if /\A[0-7]*\z/.match?(str)
+
+ raise ArgumentError, "#{str.inspect} is not an octal string"
+ end
+
+ def self.oct_or_256based(str)
+ # \x80 flags a positive 256-based number
+ # \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.unpack1("@4N") if /\A[\x80\xff]/n.match?(str)
+ strict_oct(str)
+ end
+
+ ##
+ # Creates a new TarHeader using +vals+
+
+ def initialize(vals)
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
+ raise ArgumentError, ":name, :size, :prefix and :mode required"
+ end
+
+ @checksum = vals[:checksum] || ""
+ @devmajor = vals[:devmajor] || 0
+ @devminor = vals[:devminor] || 0
+ @gid = vals[:gid] || 0
+ @gname = vals[:gname] || "wheel"
+ @linkname = vals[:linkname]
+ @magic = vals[:magic] || "ustar"
+ @mode = vals[:mode]
+ @mtime = vals[:mtime] || 0
+ @name = vals[:name]
+ @prefix = vals[:prefix]
+ @size = vals[:size]
+ @typeflag = vals[:typeflag]
+ @typeflag = "0" if @typeflag.nil? || @typeflag.empty?
+ @uid = vals[:uid] || 0
+ @uname = vals[:uname] || "wheel"
+ @version = vals[:version] || "00"
+
+ @empty = vals[:empty]
+ end
+
+ EMPTY = new({ # :nodoc:
+ checksum: 0,
+ gname: "",
+ linkname: "",
+ magic: "",
+ mode: 0,
+ name: "",
+ prefix: "",
+ size: 0,
+ uname: "",
+ version: 0,
+
+ empty: true,
+ }).freeze
+ private_constant :EMPTY
+
+ ##
+ # Is the tar entry empty?
+
+ def empty?
+ @empty
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @checksum == other.checksum &&
+ @devmajor == other.devmajor &&
+ @devminor == other.devminor &&
+ @gid == other.gid &&
+ @gname == other.gname &&
+ @linkname == other.linkname &&
+ @magic == other.magic &&
+ @mode == other.mode &&
+ @mtime == other.mtime &&
+ @name == other.name &&
+ @prefix == other.prefix &&
+ @size == other.size &&
+ @typeflag == other.typeflag &&
+ @uid == other.uid &&
+ @uname == other.uname &&
+ @version == other.version
+ end
+
+ def to_s # :nodoc:
+ update_checksum
+ header
+ end
+
+ ##
+ # Updates the TarHeader's checksum
+
+ def update_checksum
+ header = header " " * 8
+ @checksum = oct calculate_checksum(header), 6
+ end
+
+ ##
+ # Header's full name, including prefix
+
+ def full_name
+ if prefix != ""
+ File.join prefix, name
+ else
+ name
+ end
+ end
+
+ private
+
+ def calculate_checksum(header)
+ header.sum(0)
+ end
+
+ def header(checksum = @checksum)
+ header = [
+ name,
+ oct(mode, 7),
+ oct(uid, 7),
+ oct(gid, 7),
+ oct(size, 11),
+ oct(mtime, 11),
+ checksum,
+ " ",
+ typeflag,
+ linkname,
+ magic,
+ oct(version, 2),
+ uname,
+ gname,
+ oct(devmajor, 7),
+ oct(devminor, 7),
+ prefix,
+ ]
+
+ header = header.pack PACK_FORMAT
+
+ header.ljust 512, "\0"
+ end
+
+ def oct(num, len)
+ format("%0#{len}o", num)
+ end
+end
diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb
new file mode 100644
index 0000000000..b66a8a62bc
--- /dev/null
+++ b/lib/rubygems/package/tar_reader.rb
@@ -0,0 +1,103 @@
+# 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
+
+class Gem::Package::TarReader
+ include Enumerable
+
+ ##
+ # Creates a new TarReader on +io+ and yields it to the block, if given.
+
+ def self.new(io)
+ reader = super
+
+ return reader unless block_given?
+
+ begin
+ yield reader
+ ensure
+ reader.close
+ end
+
+ nil
+ end
+
+ attr_reader :io # :nodoc:
+
+ ##
+ # Creates a new tar file reader on +io+ which needs to respond to #pos,
+ # #eof?, #read, #getc and #pos=
+
+ def initialize(io)
+ @io = io
+ @init_pos = io.pos
+ end
+
+ ##
+ # Close the tar file
+
+ def close
+ end
+
+ ##
+ # Iterates over files in the tarball yielding each entry
+
+ def each
+ return enum_for __method__ unless block_given?
+
+ until @io.eof? do
+ 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
+ yield entry
+ entry.close
+ end
+ end
+
+ alias_method :each_entry, :each
+
+ ##
+ # NOTE: Do not call #rewind during #each
+
+ def rewind
+ if @init_pos == 0
+ @io.rewind
+ else
+ @io.pos = @init_pos
+ end
+ end
+
+ ##
+ # Seeks through the tar file until it finds the +entry+ with +name+ and
+ # yields it. Rewinds the tar file to the beginning when the block
+ # terminates.
+
+ def seek(name) # :yields: entry
+ found = find do |entry|
+ entry.full_name == name
+ end
+
+ return unless found
+
+ yield found
+ ensure
+ rewind
+ end
+end
+
+require_relative "tar_reader/entry"
diff --git a/lib/rubygems/package/tar_reader/entry.rb b/lib/rubygems/package/tar_reader/entry.rb
new file mode 100644
index 0000000000..f837e86fd6
--- /dev/null
+++ b/lib/rubygems/package/tar_reader/entry.rb
@@ -0,0 +1,244 @@
+# 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
+
+ ##
+ # Creates a new tar entry for +header+ that will be read from +io+
+
+ def initialize(header, io)
+ @closed = false
+ @header = header
+ @io = io
+ @orig_pos = @io.pos
+ @end_pos = @orig_pos + @header.size
+ @read = 0
+ end
+
+ def check_closed # :nodoc:
+ raise IOError, "closed #{self.class}" if closed?
+ end
+
+ ##
+ # Number of bytes read out of the tar entry
+
+ def bytes_read
+ @read
+ end
+
+ ##
+ # 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
+
+ ##
+ # Is the tar entry closed?
+
+ def closed?
+ @closed
+ end
+
+ ##
+ # Are we at the end of the tar entry?
+
+ def eof?
+ check_closed
+
+ @read >= @header.size
+ end
+
+ ##
+ # Full name of the tar entry
+
+ def full_name
+ @header.full_name.force_encoding(Encoding::UTF_8)
+ rescue ArgumentError => e
+ raise unless e.message == "string contains null byte"
+ raise Gem::Package::TarInvalidError,
+ "tar is corrupt, name contains null byte"
+ end
+
+ ##
+ # Read one byte from the tar entry
+
+ def getc
+ return nil if eof?
+
+ ret = @io.getc
+ @read += 1 if ret
+
+ ret
+ end
+
+ ##
+ # Is this tar entry a directory?
+
+ def directory?
+ @header.typeflag == "5"
+ end
+
+ ##
+ # Is this tar entry a file?
+
+ def file?
+ @header.typeflag == "0"
+ end
+
+ ##
+ # Is this tar entry a symlink?
+
+ def symlink?
+ @header.typeflag == "2"
+ end
+
+ ##
+ # The position in the tar entry
+
+ def pos
+ check_closed
+
+ 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_method :length, :size
+
+ ##
+ # Reads +maxlen+ bytes from the tar file entry, or the rest of the entry if nil
+
+ def read(maxlen = nil)
+ if eof?
+ return maxlen.to_i.zero? ? "" : nil
+ end
+
+ 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, outbuf = "".b)
+ if eof? && maxlen > 0
+ raise EOFError, "end of file reached"
+ end
+
+ max_read = [maxlen, @header.size - @read].min
+
+ @io.readpartial(max_read, outbuf)
+ @read += outbuf.size
+
+ outbuf
+ 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
+ seek(0, IO::SEEK_SET)
+ end
+end
diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb
new file mode 100644
index 0000000000..39fed9e2af
--- /dev/null
+++ b/lib/rubygems/package/tar_writer.rb
@@ -0,0 +1,332 @@
+# 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
+
+class Gem::Package::TarWriter
+ class FileOverflow < StandardError; end
+
+ ##
+ # IO wrapper that allows writing a limited amount of data
+
+ class BoundedStream
+ ##
+ # Maximum number of bytes that can be written
+
+ attr_reader :limit
+
+ ##
+ # Number of bytes written
+
+ attr_reader :written
+
+ ##
+ # Wraps +io+ and allows up to +limit+ bytes to be written
+
+ def initialize(io, limit)
+ @io = io
+ @limit = limit
+ @written = 0
+ end
+
+ ##
+ # Writes +data+ onto the IO, raising a FileOverflow exception if the
+ # number of bytes will be more than #limit
+
+ def write(data)
+ if data.bytesize + @written > @limit
+ raise FileOverflow, "You tried to feed more data than fits in the file."
+ end
+ @io.write data
+ @written += data.bytesize
+ data.bytesize
+ end
+ end
+
+ ##
+ # IO wrapper that provides only #write
+
+ class RestrictedStream
+ ##
+ # Creates a new RestrictedStream wrapping +io+
+
+ def initialize(io)
+ @io = io
+ end
+
+ ##
+ # Writes +data+ onto the IO
+
+ def write(data)
+ @io.write data
+ end
+ end
+
+ ##
+ # Creates a new TarWriter, yielding it if a block is given
+
+ def self.new(io)
+ writer = super
+
+ return writer unless block_given?
+
+ begin
+ yield writer
+ ensure
+ writer.close
+ end
+
+ nil
+ end
+
+ ##
+ # Creates a new TarWriter that will write to +io+
+
+ def initialize(io)
+ @io = io
+ @closed = false
+ end
+
+ ##
+ # Adds file +name+ with permissions +mode+ and mtime +mtime+ (sets
+ # Gem.source_date_epoch if not specified), and yields an IO for
+ # writing the file to
+
+ def add_file(name, mode, mtime = nil) # :yields: io
+ check_closed
+
+ name, prefix = split_name name
+
+ init_pos = @io.pos
+ @io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header
+
+ yield RestrictedStream.new(@io) if block_given?
+
+ size = @io.pos - init_pos - 512
+
+ remainder = (512 - (size % 512)) % 512
+ @io.write "\0" * remainder
+
+ final_pos = @io.pos
+ @io.pos = init_pos
+
+ header = Gem::Package::TarHeader.new name: name, mode: mode,
+ size: size, prefix: prefix,
+ mtime: mtime || Gem.source_date_epoch
+
+ @io.write header
+ @io.pos = final_pos
+
+ self
+ end
+
+ ##
+ # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
+ # the file. The +digest_algorithm+ is written to a read-only +name+.sum
+ # file following the given file contents containing the digest name and
+ # hexdigest separated by a tab.
+ #
+ # The created digest object is returned.
+
+ def add_file_digest(name, mode, digest_algorithms) # :yields: io
+ digests = digest_algorithms.map do |digest_algorithm|
+ digest = digest_algorithm.new
+ digest_name =
+ if digest.respond_to? :name
+ digest.name
+ else
+ digest_algorithm.class.name[/::([^:]+)\z/, 1]
+ end
+
+ [digest_name, digest]
+ end
+
+ digests = Hash[*digests.flatten]
+
+ add_file name, mode do |io|
+ Gem::Package::DigestIO.wrap io, digests do |digest_io|
+ yield digest_io
+ end
+ end
+
+ digests
+ end
+
+ ##
+ # Adds +name+ with permissions +mode+ to the tar, yielding +io+ for writing
+ # the file. The +signer+ is used to add a digest file using its
+ # digest_algorithm per add_file_digest and a cryptographic signature in
+ # +name+.sig. If the signer has no key only the checksum file is added.
+ #
+ # Returns the digest.
+
+ def add_file_signed(name, mode, signer)
+ digest_algorithms = [
+ signer.digest_algorithm,
+ Gem::Security.create_digest("SHA512"),
+ ].compact.uniq
+
+ digests = add_file_digest name, mode, digest_algorithms do |io|
+ yield io
+ end
+
+ signature_digest = digests.values.compact.find do |digest|
+ digest_name =
+ if digest.respond_to? :name
+ digest.name
+ else
+ digest.class.name[/::([^:]+)\z/, 1]
+ end
+
+ digest_name == signer.digest_name
+ end
+
+ raise "no #{signer.digest_name} in #{digests.values.compact}" unless signature_digest
+
+ if signer.key
+ signature = signer.sign signature_digest.digest
+
+ add_file_simple "#{name}.sig", 0o444, signature.length do |io|
+ io.write signature
+ end
+ end
+
+ digests
+ end
+
+ ##
+ # Add file +name+ with permissions +mode+ +size+ bytes long. Yields an IO
+ # to write the file to.
+
+ def add_file_simple(name, mode, size) # :yields: io
+ check_closed
+
+ 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
+
+ @io.write header
+ os = BoundedStream.new @io, size
+
+ yield os if block_given?
+
+ min_padding = size - os.written
+ @io.write("\0" * min_padding)
+
+ remainder = (512 - (size % 512)) % 512
+ @io.write("\0" * remainder)
+
+ self
+ end
+
+ ##
+ # Adds symlink +name+ with permissions +mode+, linking to +target+.
+
+ def add_symlink(name, target, mode)
+ check_closed
+
+ 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
+
+ @io.write header
+
+ self
+ end
+
+ ##
+ # Raises IOError if the TarWriter is closed
+
+ def check_closed
+ raise IOError, "closed #{self.class}" if closed?
+ end
+
+ ##
+ # Closes the TarWriter
+
+ def close
+ check_closed
+
+ @io.write "\0" * 1024
+ flush
+
+ @closed = true
+ end
+
+ ##
+ # Is the TarWriter closed?
+
+ def closed?
+ @closed
+ end
+
+ ##
+ # Flushes the TarWriter's IO
+
+ def flush
+ check_closed
+
+ @io.flush if @io.respond_to? :flush
+ end
+
+ ##
+ # Creates a new directory in the tar file +name+ with +mode+
+
+ def mkdir(name, mode)
+ check_closed
+
+ 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
+
+ @io.write header
+
+ self
+ end
+
+ ##
+ # Splits +name+ into a name and prefix that can fit in the TarHeader
+
+ def split_name(name) # :nodoc:
+ if name.bytesize > 256
+ raise Gem::Package::TooLongFileName.new("File \"#{name}\" has a too long path (should be 256 or less)")
+ end
+
+ prefix = ""
+ if name.bytesize > 100
+ parts = name.split("/", -1) # parts are never empty here
+ name = parts.pop # initially empty for names with a trailing slash ("foo/.../bar/")
+ prefix = parts.join("/") # if empty, then it's impossible to split (parts is empty too)
+ while !parts.empty? && (prefix.bytesize > 155 || name.empty?)
+ name = parts.pop + "/" + name
+ prefix = parts.join("/")
+ end
+
+ if name.bytesize > 100 || prefix.empty?
+ raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long name (should be 100 or less)")
+ end
+
+ if prefix.bytesize > 155
+ raise Gem::Package::TooLongFileName.new("File \"#{prefix}/#{name}\" has a too long base path (should be 155 or less)")
+ end
+ end
+
+ [name, prefix]
+ end
+end
diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb
new file mode 100644
index 0000000000..d26411684d
--- /dev/null
+++ b/lib/rubygems/package_task.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+# Copyright (c) 2003, 2004 Jim Weirich, 2009 Eric Hodel
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require_relative "../rubygems"
+require_relative "package"
+require "rake/packagetask"
+
+##
+# Create a package based upon a Gem::Specification. Gem packages, as well as
+# zip files and tar/gzipped packages can be produced by this task.
+#
+# In addition to the Rake targets generated by Rake::PackageTask, a
+# Gem::PackageTask will also generate the following tasks:
+#
+# [<b>"<em>package_dir</em>/<em>name</em>-<em>version</em>.gem"</b>]
+# Create a RubyGems package with the given name and version.
+#
+# Example using a Gem::Specification:
+#
+# require 'rubygems'
+# require 'rubygems/package_task'
+#
+# spec = Gem::Specification.new do |s|
+# s.summary = "Ruby based make-like utility."
+# s.name = 'rake'
+# s.version = PKG_VERSION
+# s.requirements << 'none'
+# s.files = PKG_FILES
+# s.description = <<-EOF
+# Rake is a Make-like program implemented in Ruby. Tasks
+# and dependencies are specified in standard Ruby syntax.
+# EOF
+# end
+#
+# Gem::PackageTask.new(spec) do |pkg|
+# pkg.need_zip = true
+# pkg.need_tar = true
+# end
+
+class Gem::PackageTask < Rake::PackageTask
+ ##
+ # Ruby Gem::Specification containing the metadata for this package. The
+ # name, version and package_files are automatically determined from the
+ # gemspec and don't need to be explicitly provided.
+
+ attr_accessor :gem_spec
+
+ ##
+ # Create a Gem Package task library. Automatically define the gem if a
+ # block is given. If no block is supplied, then #define needs to be called
+ # to define the task.
+
+ def initialize(gem_spec)
+ init gem_spec
+ yield self if block_given?
+ define if block_given?
+ end
+
+ ##
+ # Initialization tasks without the "yield self" or define operations.
+
+ def init(gem)
+ super gem.full_name, :noversion
+ @gem_spec = gem
+ @package_files += gem_spec.files if gem_spec.files
+ @fileutils_output = $stdout
+ end
+
+ ##
+ # Create the Rake tasks and actions specified by this Gem::PackageTask.
+ # (+define+ is automatically called if a block is given to +new+).
+
+ def define
+ super
+
+ gem_file = File.basename gem_spec.cache_file
+ gem_path = File.join package_dir, gem_file
+ gem_dir = File.join package_dir, gem_spec.full_name
+
+ task package: [:gem]
+
+ directory package_dir
+ directory gem_dir
+
+ desc "Build the gem file #{gem_file}"
+ task gem: [gem_path]
+
+ trace = Rake.application.options.trace
+ Gem.configuration.verbose = trace
+
+ file gem_path => [package_dir, gem_dir] + @gem_spec.files do
+ chdir(gem_dir) do
+ when_writing "Creating #{gem_spec.file_name}" do
+ Gem::Package.build gem_spec
+
+ verbose trace do
+ mv gem_file, ".."
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb
new file mode 100644
index 0000000000..13091e29ba
--- /dev/null
+++ b/lib/rubygems/path_support.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+##
+#
+# Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings
+# to the rest of RubyGems.
+#
+class Gem::PathSupport
+ ##
+ # The default system path for managing Gems.
+ attr_reader :home
+
+ ##
+ # Array of paths to search for Gems.
+ attr_reader :path
+
+ ##
+ # Directory with spec cache
+ attr_reader :spec_cache_dir # :nodoc:
+
+ ##
+ #
+ # Constructor. Takes a single argument which is to be treated like a
+ # hashtable, or defaults to ENV, the system environment.
+ #
+ def initialize(env)
+ @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
+ 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).
+
+ def split_gem_path(gpaths, home)
+ # FIX: it should be [home, *path], not [*path, home]
+
+ gem_path = []
+
+ if gpaths
+ gem_path = gpaths.split(Gem.path_separator)
+ # Handle the path_separator being set to a regexp, which will cause
+ # end_with? to error
+ if /#{Gem.path_separator}\z/.match?(gpaths)
+ gem_path += default_path
+ end
+
+ if File::ALT_SEPARATOR
+ gem_path.map! do |this_path|
+ this_path.gsub File::ALT_SEPARATOR, File::SEPARATOR
+ end
+ end
+
+ gem_path << home
+ else
+ gem_path = default_path
+ end
+
+ gem_path.map {|path| expand(path) }.uniq
+ end
+
+ # Return the default Gem path
+ def default_path
+ Gem.default_path + [@home]
+ end
+
+ def expand(path)
+ if File.directory?(path)
+ File.realpath(path)
+ else
+ path
+ end
+ end
+end
diff --git a/lib/rubygems/platform.rb b/lib/rubygems/platform.rb
new file mode 100644
index 0000000000..367b00e7e1
--- /dev/null
+++ b/lib/rubygems/platform.rb
@@ -0,0 +1,392 @@
+# frozen_string_literal: true
+
+##
+# Available list of platforms for targeting Gem installations.
+#
+# See `gem help platform` for information on platform matching.
+
+class Gem::Platform
+ @local = nil
+
+ attr_accessor :cpu, :os, :version
+
+ def self.local(refresh: false)
+ return @local if @local && !refresh
+ @local = begin
+ arch = Gem.target_rbconfig["arch"]
+ arch = "#{arch}_60" if /mswin(?:32|64)$/.match?(arch)
+ new(arch)
+ end
+ end
+
+ def self.match_platforms?(platform, platforms)
+ platform = Gem::Platform.new(platform) unless platform.is_a?(Gem::Platform)
+ platforms.any? do |local_platform|
+ platform.nil? ||
+ local_platform == platform ||
+ (local_platform != Gem::Platform::RUBY && platform =~ local_platform)
+ end
+ end
+ private_class_method :match_platforms?
+
+ def self.match_spec?(spec)
+ match_gem?(spec.platform, spec.name)
+ end
+
+ if RUBY_ENGINE == "truffleruby"
+ def self.match_gem?(platform, gem_name)
+ raise "Not a string: #{gem_name.inspect}" unless String === gem_name
+
+ if REUSE_AS_BINARY_ON_TRUFFLERUBY.include?(gem_name)
+ match_platforms?(platform, [Gem::Platform::RUBY, Gem::Platform.local])
+ else
+ match_platforms?(platform, Gem.platforms)
+ end
+ end
+ else
+ def self.match_gem?(platform, gem_name)
+ match_platforms?(platform, Gem.platforms)
+ end
+ end
+
+ def self.sort_priority(platform)
+ platform == Gem::Platform::RUBY ? -1 : 1
+ end
+
+ def self.installable?(spec)
+ if spec.respond_to? :installable_platform?
+ spec.installable_platform?
+ else
+ match_spec? spec
+ end
+ end
+
+ def self.new(arch) # :nodoc:
+ case arch
+ when Gem::Platform::CURRENT then
+ Gem::Platform.local
+ when Gem::Platform::RUBY, nil, "" then
+ Gem::Platform::RUBY
+ else
+ super
+ end
+ end
+
+ def initialize(arch)
+ case arch
+ when Array then
+ @cpu, @os, @version = arch
+ when String then
+ cpu, os = arch.sub(/-+$/, "").split("-", 2)
+
+ @cpu = if cpu&.match?(/i\d86/)
+ "x86"
+ else
+ cpu
+ end
+
+ 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 /^macruby-?(\d+(?:\.\d+)*)?/ then ["macruby", $1]
+ when /freebsd-?(\d+)?/ then ["freebsd", $1]
+ when "java", "jruby" then ["java", nil]
+ when /^java-?(\d+(?:\.\d+)*)?/ then ["java", $1]
+ when /^dalvik-?(\d+)?$/ then ["dalvik", $1]
+ when /^dotnet$/ then ["dotnet", nil]
+ when /^dotnet-?(\d+(?:\.\d+)*)?/ then ["dotnet", $1]
+ when /linux-?(\w+)?/ then ["linux", $1]
+ when /mingw32/ then ["mingw32", nil]
+ when /mingw-?(\w+)?/ then ["mingw", $1]
+ when /(mswin\d+)(?:[_-](\d+))?/ then
+ os = $1
+ version = $2
+ @cpu = "x86" if @cpu.nil? && os.end_with?("32")
+ [os, version]
+ when /netbsdelf/ then ["netbsdelf", nil]
+ when /openbsd-?(\d+\.\d+)?/ then ["openbsd", $1]
+ when /solaris-?(\d+\.\d+)?/ then ["solaris", $1]
+ when /wasi/ then ["wasi", nil]
+ # test
+ when /^(\w+_platform)-?(\d+)?/ then [$1, $2]
+ else ["unknown", nil]
+ end
+ when Gem::Platform then
+ @cpu = arch.cpu
+ @os = arch.os
+ @version = arch.version
+ else
+ raise ArgumentError, "invalid argument #{arch.inspect}"
+ end
+ end
+
+ def to_a
+ [@cpu, @os, @version]
+ end
+
+ def to_s
+ to_a.compact.join(@cpu.nil? ? "" : "-")
+ end
+
+ ##
+ # Deconstructs the platform into an array for pattern matching.
+ # Returns [cpu, os, version].
+ #
+ # Gem::Platform.new("x86_64-linux").deconstruct #=> ["x86_64", "linux", nil]
+ #
+ # This enables array pattern matching:
+ #
+ # case Gem::Platform.new("arm64-darwin-21")
+ # in ["arm64", "darwin", version]
+ # # version => "21"
+ # end
+ alias_method :deconstruct, :to_a
+
+ ##
+ # Deconstructs the platform into a hash for pattern matching.
+ # Returns a hash with keys +:cpu+, +:os+, and +:version+.
+ #
+ # Gem::Platform.new("x86_64-darwin-20").deconstruct_keys(nil)
+ # #=> { cpu: "x86_64", os: "darwin", version: "20" }
+ #
+ # This enables hash pattern matching:
+ #
+ # case Gem::Platform.new("x86_64-linux")
+ # in cpu: "x86_64", os: "linux"
+ # # Matches Linux on x86_64
+ # end
+ def deconstruct_keys(keys)
+ { cpu: @cpu, os: @os, version: @version }
+ end
+
+ ##
+ # Is +other+ equal to this platform? Two platforms are equal if they have
+ # the same CPU, OS and version.
+
+ def ==(other)
+ self.class === other && to_a == other.to_a
+ end
+
+ alias_method :eql?, :==
+
+ def hash # :nodoc:
+ to_a.hash
+ end
+
+ ##
+ # Does +other+ match this platform? Two platforms match if they have the
+ # same CPU, or either has a CPU of 'universal', they have the same OS, and
+ # they have the same version, or either one has no version
+ #
+ # Additionally, the platform will match if the local CPU is 'arm' and the
+ # other CPU starts with "armv" (for generic 32-bit ARM family support).
+ #
+ # Of note, this method is not commutative. Indeed the OS 'linux' has a
+ # special case: the version is the libc name, yet while "no version" stands
+ # as a wildcard for a binary gem platform (as for other OSes), for the
+ # runtime platform "no version" stands for 'gnu'. To be able to distinguish
+ # these, the method receiver is the gem platform, while the argument is
+ # the runtime platform.
+ #
+ #--
+ # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb`
+
+ def ===(other)
+ return nil unless Gem::Platform === other
+
+ # universal-mingw32 matches x64-mingw-ucrt
+ return true if (@cpu == "universal" || other.cpu == "universal") &&
+ @os.start_with?("mingw") && other.os.start_with?("mingw")
+
+ # cpu
+ ([nil,"universal"].include?(@cpu) || [nil, "universal"].include?(other.cpu) || @cpu == other.cpu ||
+ (@cpu == "arm" && other.cpu.start_with?("armv"))) &&
+
+ # os
+ @os == other.os &&
+
+ # version
+ (
+ (@os != "linux" && (@version.nil? || other.version.nil?)) ||
+ (@os == "linux" && (normalized_linux_version == other.normalized_linux_version || ["musl#{@version}", "musleabi#{@version}", "musleabihf#{@version}"].include?(other.version))) ||
+ @version == other.version
+ )
+ end
+
+ #--
+ # NOTE: Until it can be removed, changes to this method must also be reflected in `bundler/lib/bundler/rubygems_ext.rb`
+
+ def normalized_linux_version
+ return nil unless @version
+
+ without_gnu_nor_abi_modifiers = @version.sub(/\Agnu/, "").sub(/eabi(hf)?\Z/, "")
+ return nil if without_gnu_nor_abi_modifiers.empty?
+
+ without_gnu_nor_abi_modifiers
+ end
+
+ ##
+ # Does +other+ match this platform? If +other+ is a String it will be
+ # converted to a Gem::Platform first. See #=== for matching rules.
+
+ def =~(other)
+ case other
+ when Gem::Platform then # nop
+ when String then
+ # This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007
+ other = case other
+ when /^i686-darwin(\d)/ then ["x86", "darwin", $1]
+ when /^i\d86-linux/ then ["x86", "linux", nil]
+ when "java", "jruby" then [nil, "java", nil]
+ when /^dalvik(\d+)?$/ then [nil, "dalvik", $1]
+ when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2]
+ when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2]
+ when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2]
+ when "powerpc-darwin" then ["powerpc", "darwin", nil]
+ when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1]
+ when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"]
+ when /universal-darwin(\d)/ then ["universal", "darwin", $1]
+ else other
+ end
+
+ other = Gem::Platform.new other
+ else
+ return nil
+ end
+
+ self === other
+ end
+
+ ##
+ # A pure-Ruby gem that may use Gem::Specification#extensions to build
+ # binary files.
+
+ RUBY = "ruby"
+
+ ##
+ # A platform-specific gem that is built for the packaging Ruby's platform.
+ # This will be replaced with Gem::Platform::local.
+
+ CURRENT = "current"
+
+ JAVA = Gem::Platform.new("java") # :nodoc:
+ MSWIN = Gem::Platform.new("mswin32") # :nodoc:
+ MSWIN64 = Gem::Platform.new("mswin64") # :nodoc:
+ MINGW = Gem::Platform.new("x86-mingw32") # :nodoc:
+ X64_MINGW_LEGACY = Gem::Platform.new("x64-mingw32") # :nodoc:
+ X64_MINGW = Gem::Platform.new("x64-mingw-ucrt") # :nodoc:
+ UNIVERSAL_MINGW = Gem::Platform.new("universal-mingw") # :nodoc:
+ WINDOWS = [MSWIN, MSWIN64, UNIVERSAL_MINGW].freeze # :nodoc:
+ X64_LINUX = Gem::Platform.new("x86_64-linux") # :nodoc:
+ X64_LINUX_MUSL = Gem::Platform.new("x86_64-linux-musl") # :nodoc:
+
+ GENERICS = [JAVA, *WINDOWS].freeze # :nodoc:
+ private_constant :GENERICS
+
+ GENERIC_CACHE = GENERICS.each_with_object({}) {|g, h| h[g] = g } # :nodoc:
+ private_constant :GENERIC_CACHE
+
+ class << self
+ ##
+ # Returns the generic platform for the given platform.
+
+ def generic(platform)
+ return Gem::Platform::RUBY if platform.nil? || platform == Gem::Platform::RUBY
+
+ GENERIC_CACHE[platform] ||= begin
+ found = GENERICS.find do |match|
+ platform === match
+ end
+ found || Gem::Platform::RUBY
+ end
+ end
+
+ ##
+ # Returns the platform specificity match for the given spec platform and user platform.
+
+ def platform_specificity_match(spec_platform, user_platform)
+ return -1 if spec_platform == user_platform
+ return 1_000_000 if spec_platform.nil? || spec_platform == Gem::Platform::RUBY || user_platform == Gem::Platform::RUBY
+
+ os_match(spec_platform, user_platform) +
+ cpu_match(spec_platform, user_platform) * 10 +
+ version_match(spec_platform, user_platform) * 100
+ end
+
+ ##
+ # Sorts and filters the best platform match for the given matching specs and platform.
+
+ def sort_and_filter_best_platform_match(matching, platform)
+ return matching if matching.one?
+
+ exact = matching.select {|spec| spec.platform == platform }
+ return exact if exact.any?
+
+ sorted_matching = sort_best_platform_match(matching, platform)
+ exemplary_spec = sorted_matching.first
+
+ sorted_matching.take_while {|spec| same_specificity?(platform, spec, exemplary_spec) && same_deps?(spec, exemplary_spec) }
+ end
+
+ ##
+ # Sorts the best platform match for the given matching specs and platform.
+
+ def sort_best_platform_match(matching, platform)
+ matching.sort_by.with_index do |spec, i|
+ [
+ platform_specificity_match(spec.platform, platform),
+ i, # for stable sort
+ ]
+ end
+ end
+
+ private
+
+ def same_specificity?(platform, spec, exemplary_spec)
+ platform_specificity_match(spec.platform, platform) == platform_specificity_match(exemplary_spec.platform, platform)
+ end
+
+ def same_deps?(spec, exemplary_spec)
+ spec.required_ruby_version == exemplary_spec.required_ruby_version &&
+ spec.required_rubygems_version == exemplary_spec.required_rubygems_version &&
+ spec.dependencies.sort == exemplary_spec.dependencies.sort
+ end
+
+ def os_match(spec_platform, user_platform)
+ if spec_platform.os == user_platform.os
+ 0
+ else
+ 1
+ end
+ end
+
+ def cpu_match(spec_platform, user_platform)
+ if spec_platform.cpu == user_platform.cpu
+ 0
+ elsif spec_platform.cpu == "arm" && user_platform.cpu.to_s.start_with?("arm")
+ 0
+ elsif spec_platform.cpu.nil? || spec_platform.cpu == "universal"
+ 1
+ else
+ 2
+ end
+ end
+
+ def version_match(spec_platform, user_platform)
+ if spec_platform.version == user_platform.version
+ 0
+ elsif spec_platform.version.nil?
+ 1
+ else
+ 2
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/psych_tree.rb b/lib/rubygems/psych_tree.rb
new file mode 100644
index 0000000000..8b4c425a33
--- /dev/null
+++ b/lib/rubygems/psych_tree.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gem
+ if defined? ::Psych::Visitors
+ class NoAliasYAMLTree < Psych::Visitors::YAMLTree
+ def self.create
+ new({})
+ end unless respond_to? :create
+
+ def visit_String(str)
+ return super unless str == "=" # or whatever you want
+
+ quote = Psych::Nodes::Scalar::SINGLE_QUOTED
+ @emitter.scalar str, nil, nil, false, true, quote
+ end
+
+ def visit_Hash(o)
+ super(o.compact)
+ end
+
+ # Noop this out so there are no anchors
+ def register(target, obj)
+ end
+
+ # This is ported over from the YAMLTree implementation in Ruby 1.9.3
+ def format_time(time)
+ if time.utc?
+ time.strftime("%Y-%m-%d %H:%M:%S.%9N Z")
+ else
+ time.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
+ end
+ end
+
+ private :format_time
+ end
+ end
+end
diff --git a/lib/rubygems/query_utils.rb b/lib/rubygems/query_utils.rb
new file mode 100644
index 0000000000..9849370b1a
--- /dev/null
+++ b/lib/rubygems/query_utils.rb
@@ -0,0 +1,349 @@
+# frozen_string_literal: true
+
+require_relative "local_remote_options"
+require_relative "spec_fetcher"
+require_relative "version_option"
+require_relative "text"
+
+module Gem::QueryUtils
+ include Gem::Text
+ include Gem::LocalRemoteOptions
+ include Gem::VersionOption
+
+ def add_query_options
+ add_option("-i", "--[no-]installed",
+ "Check for installed gem") do |value, options|
+ options[:installed] = value
+ end
+
+ add_option("-I", "Equivalent to --no-installed") do |_value, options|
+ options[:installed] = false
+ end
+
+ add_version_option command, "for use with --installed"
+
+ add_option("-d", "--[no-]details",
+ "Display detailed information of gem(s)") do |value, options|
+ options[:details] = value
+ end
+
+ add_option("--[no-]versions",
+ "Display only gem names") do |value, options|
+ options[:versions] = value
+ options[:details] = false unless value
+ end
+
+ add_option("-a", "--all",
+ "Display all gem versions") do |value, options|
+ options[:all] = value
+ end
+
+ add_option("-e", "--exact",
+ "Name of gem(s) to query on matches the",
+ "provided STRING") do |value, options|
+ options[:exact] = value
+ end
+
+ add_option("--[no-]prerelease",
+ "Display prerelease versions") do |value, options|
+ options[:prerelease] = value
+ end
+
+ add_local_remote_options
+ end
+
+ def defaults_str # :nodoc:
+ "--local --no-details --versions --no-installed"
+ end
+
+ def execute
+ gem_names = if args.empty?
+ [options[:name]]
+ else
+ options[:exact] ? args.map {|arg| /\A#{Regexp.escape(arg)}\Z/ } : args.map {|arg| /#{arg}/i }
+ end
+
+ terminate_interaction(check_installed_gems(gem_names)) if check_installed_gems?
+
+ gem_names.each {|n| show_gems(n) }
+ end
+
+ private
+
+ def check_installed_gems(gem_names)
+ exit_code = 0
+
+ if args.empty? && !gem_name?
+ alert_error "You must specify a gem name"
+ exit_code = 4
+ elsif gem_names.count > 1
+ alert_error "You must specify only ONE gem!"
+ exit_code = 4
+ else
+ installed = installed?(gem_names.first, options[:version])
+ installed = !installed unless options[:installed]
+
+ say(installed)
+ exit_code = 1 unless installed
+ end
+
+ exit_code
+ end
+
+ def check_installed_gems?
+ !options[:installed].nil?
+ end
+
+ def gem_name?
+ !options[:name].nil?
+ end
+
+ def prerelease
+ options[:prerelease]
+ end
+
+ def show_prereleases?
+ prerelease.nil? || prerelease
+ end
+
+ def args
+ options[:args].to_a
+ end
+
+ def display_header(type)
+ if (ui.outs.tty? && Gem.configuration.verbose) || both?
+ say
+ say "*** #{type} GEMS ***"
+ say
+ end
+ end
+
+ # Guts of original execute
+ def show_gems(name)
+ show_local_gems(name) if local?
+ show_remote_gems(name) if remote?
+ end
+
+ def show_local_gems(name, req = Gem::Requirement.default)
+ display_header("LOCAL")
+
+ specs = Gem::Specification.find_all do |s|
+ name_matches = name ? s.name =~ name : true
+ version_matches = show_prereleases? || !s.version.prerelease?
+
+ name_matches && version_matches
+ end.uniq(&:full_name)
+
+ spec_tuples = specs.map do |spec|
+ [spec.name_tuple, spec]
+ end
+
+ output_query_results(spec_tuples)
+ end
+
+ def show_remote_gems(name)
+ display_header("REMOTE")
+
+ fetcher = Gem::SpecFetcher.fetcher
+
+ spec_tuples = if name.nil?
+ fetcher.detect(specs_type) { true }
+ else
+ fetcher.detect(specs_type) do |name_tuple|
+ name === name_tuple.name && options[:version].satisfied_by?(name_tuple.version)
+ end
+ end
+
+ output_query_results(spec_tuples)
+ end
+
+ def specs_type
+ if options[:all] || options[:version].specific?
+ if options[:prerelease]
+ :complete
+ else
+ :released
+ end
+ elsif options[:prerelease]
+ :prerelease
+ else
+ :latest
+ end
+ end
+
+ ##
+ # Check if gem +name+ version +version+ is installed.
+
+ def installed?(name, req = Gem::Requirement.default)
+ Gem::Specification.any? {|s| s.name =~ name && req =~ s.version }
+ end
+
+ def output_query_results(spec_tuples)
+ output = []
+ versions = Hash.new {|h,name| h[name] = [] }
+
+ spec_tuples.each do |spec_tuple, source|
+ versions[spec_tuple.name] << [spec_tuple, source]
+ end
+
+ versions = versions.sort_by do |(n,_),_|
+ n.downcase
+ end
+
+ output_versions output, versions
+
+ say output.join(options[:details] ? "\n\n" : "\n")
+ end
+
+ def output_versions(output, versions)
+ versions.each do |_gem_name, matching_tuples|
+ matching_tuples = matching_tuples.sort_by {|n,_| n.version }.reverse
+
+ platforms = Hash.new {|h,version| h[version] = [] }
+
+ matching_tuples.each do |n, _|
+ platforms[n.version] << n.platform if n.platform
+ end
+
+ seen = {}
+
+ matching_tuples.delete_if do |n,_|
+ if seen[n.version]
+ true
+ else
+ seen[n.version] = true
+ false
+ end
+ end
+
+ output << clean_text(make_entry(matching_tuples, platforms))
+ end
+ end
+
+ def entry_details(entry, detail_tuple, specs, platforms)
+ return unless options[:details]
+
+ name_tuple, spec = detail_tuple
+
+ spec = spec.fetch_spec(name_tuple)if spec.respond_to?(:fetch_spec)
+
+ entry << "\n"
+
+ spec_platforms entry, platforms
+ spec_authors entry, spec
+ spec_homepage entry, spec
+ spec_license entry, spec
+ spec_loaded_from entry, spec, specs
+ spec_summary entry, spec
+ end
+
+ def entry_versions(entry, name_tuples, platforms, specs)
+ return unless options[:versions]
+
+ list =
+ if platforms.empty? || options[:details]
+ name_tuples.map(&:version).uniq
+ else
+ platforms.sort.reverse.map do |version, pls|
+ out = version.to_s
+
+ if options[:domain] == :local
+ default = specs.any? do |s|
+ !s.is_a?(Gem::Source) && s.version == version && s.default_gem?
+ end
+ out = "default: #{out}" if default
+ end
+
+ if pls != [Gem::Platform::RUBY]
+ platform_list = [pls.delete(Gem::Platform::RUBY), *pls.sort].compact
+ out = platform_list.unshift(out).join(" ")
+ end
+
+ out
+ end
+ end
+
+ entry << " (#{list.join ", "})"
+ end
+
+ def make_entry(entry_tuples, platforms)
+ detail_tuple = entry_tuples.first
+
+ name_tuples, specs = entry_tuples.flatten.partition do |item|
+ Gem::NameTuple === item
+ end
+
+ entry = [name_tuples.first.name]
+
+ entry_versions(entry, name_tuples, platforms, specs)
+ entry_details(entry, detail_tuple, specs, platforms)
+
+ entry.join
+ end
+
+ def spec_authors(entry, spec)
+ authors = "Author#{spec.authors.length > 1 ? "s" : ""}: ".dup
+ authors << spec.authors.join(", ")
+ entry << format_text(authors, 68, 4)
+ end
+
+ def spec_homepage(entry, spec)
+ return if spec.homepage.nil? || spec.homepage.empty?
+
+ entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4)
+ end
+
+ def spec_license(entry, spec)
+ return if spec.license.nil? || spec.license.empty?
+
+ licenses = "License#{spec.licenses.length > 1 ? "s" : ""}: ".dup
+ licenses << spec.licenses.join(", ")
+ entry << "\n" << format_text(licenses, 68, 4)
+ end
+
+ def spec_loaded_from(entry, spec, specs)
+ return unless spec.loaded_from
+
+ if specs.length == 1
+ default = spec.default_gem? ? " (default)" : nil
+ entry << "\n" << " Installed at#{default}: #{spec.base_dir}"
+ else
+ label = "Installed at"
+ specs.each do |s|
+ version = s.version.to_s
+ default = s.default_gem? ? ", default" : ""
+ entry << "\n" << " #{label} (#{version}#{default}): #{s.base_dir}"
+ label = " " * label.length
+ end
+ end
+ end
+
+ def spec_platforms(entry, platforms)
+ non_ruby = platforms.any? do |_, pls|
+ pls.any? {|pl| pl != Gem::Platform::RUBY }
+ end
+
+ return unless non_ruby
+
+ if platforms.length == 1
+ title = platforms.values.length == 1 ? "Platform" : "Platforms"
+ entry << " #{title}: #{platforms.values.sort.join(", ")}\n"
+ else
+ entry << " Platforms:\n"
+
+ sorted_platforms = platforms.sort
+
+ sorted_platforms.each do |version, pls|
+ label = " #{version}: "
+ data = format_text pls.sort.join(", "), 68, label.length
+ data[0, label.length] = label
+ entry << data << "\n"
+ end
+ end
+ end
+
+ def spec_summary(entry, spec)
+ 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
new file mode 100644
index 0000000000..3524b161b2
--- /dev/null
+++ b/lib/rubygems/rdoc.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+
+begin
+ require "rdoc/rubygems_hook"
+ module Gem
+ ##
+ # Returns whether RDoc defines its own install hooks through a RubyGems
+ # plugin. This and whatever is guarded by it can be removed once no
+ # supported Ruby ships with RDoc older than 6.9.0.
+
+ def self.rdoc_hooks_defined_via_plugin?
+ Gem::Version.new(::RDoc::VERSION) >= Gem::Version.new("6.9.0")
+ end
+
+ if rdoc_hooks_defined_via_plugin?
+ RDoc = ::RDoc::RubyGemsHook
+ else
+ RDoc = ::RDoc::RubygemsHook
+
+ Gem.done_installing(&Gem::RDoc.method(:generation_hook))
+ end
+ end
+rescue LoadError
+end
diff --git a/lib/rubygems/remote_fetcher.rb b/lib/rubygems/remote_fetcher.rb
new file mode 100644
index 0000000000..5b83dc6f6f
--- /dev/null
+++ b/lib/rubygems/remote_fetcher.rb
@@ -0,0 +1,350 @@
+# frozen_string_literal: true
+
+require_relative "../rubygems"
+require_relative "request"
+require_relative "request/connection_pools"
+require_relative "s3_uri_signer"
+require_relative "uri_formatter"
+require_relative "uri"
+require_relative "user_interaction"
+
+##
+# RemoteFetcher handles the details of fetching gems and gem information from
+# a remote source.
+
+class Gem::RemoteFetcher
+ include Gem::UserInteraction
+
+ ##
+ # A FetchError exception wraps up the various possible IO and HTTP failures
+ # that could happen while downloading from the internet.
+
+ class FetchError < Gem::Exception
+ ##
+ # The URI which was being accessed when the exception happened.
+
+ attr_accessor :uri, :original_uri
+
+ def initialize(message, uri)
+ uri = Gem::Uri.new(uri)
+
+ super uri.redact_credentials_from(message)
+
+ @original_uri = uri.to_s
+ @uri = uri.redacted.to_s
+ end
+
+ def to_s # :nodoc:
+ "#{super} (#{uri})"
+ end
+ end
+
+ ##
+ # A FetchError that indicates that the reason for not being
+ # able to fetch data was that the host could not be contacted
+
+ class UnknownHostError < FetchError
+ end
+ deprecate_constant(:UnknownHostError)
+
+ @fetcher = nil
+
+ ##
+ # Cached RemoteFetcher instance.
+
+ def self.fetcher
+ @fetcher ||= new Gem.configuration[:http_proxy]
+ end
+
+ attr_accessor :headers
+
+ ##
+ # Initialize a remote fetcher using the source URI and possible proxy
+ # information.
+ #
+ # +proxy+
+ # * [String]: explicit specification of proxy; overrides any environment
+ # variable setting
+ # * nil: respect environment variables (HTTP_PROXY, HTTP_PROXY_USER,
+ # HTTP_PROXY_PASS)
+ # * <tt>:no_proxy</tt>: ignore environment variables and _don't_ use a proxy
+ #
+ # +headers+: A set of additional HTTP headers to be sent to the server when
+ # fetching the gem.
+
+ def initialize(proxy = nil, dns = nil, headers = {})
+ require_relative "core_ext/tcpsocket_init" if Gem.configuration.ipv4_fallback_enabled
+ require_relative "vendored_net_http"
+ require_relative "vendor/uri/lib/uri"
+
+ Socket.do_not_reverse_lookup = true
+
+ @proxy = proxy
+ @pools = {}
+ @pool_lock = Thread::Mutex.new
+ @pool_size = 1
+ @cert_files = Gem::Request.get_cert_files
+
+ @headers = headers
+ end
+
+ ##
+ # Given a name and requirement, downloads this gem into cache and returns the
+ # filename. Returns nil if the gem cannot be located.
+ #--
+ # Should probably be integrated with #download below, but that will be a
+ # larger, more encompassing effort. -erikh
+
+ def download_to_cache(dependency)
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency
+
+ return if found.empty?
+
+ spec, source = found.max_by {|(s,_)| s.version }
+
+ download spec, source.uri
+ end
+
+ ##
+ # Moves the gem +spec+ from +source_uri+ to the cache dir unless it is
+ # already there. If the source_uri is local the gem cache dir copy is
+ # always replaced.
+
+ def download(spec, source_uri, install_dir = Gem.dir)
+ gem_file_name = File.basename spec.cache_file
+
+ install_cache_dir = File.join install_dir, "cache"
+ cache_dir =
+ if Gem.configuration.global_gem_cache
+ Gem.global_gem_cache_path
+ elsif Dir.pwd == install_dir # see fetch_command
+ install_dir
+ elsif File.writable?(install_cache_dir) || (File.writable?(install_dir) && !File.exist?(install_cache_dir))
+ install_cache_dir
+ else
+ File.join Gem.user_dir, "cache"
+ end
+
+ local_gem_path = File.join cache_dir, gem_file_name
+
+ require "fileutils"
+ 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
+
+ # Gem::URI.parse gets confused by MS Windows paths with forward slashes.
+ scheme = nil if /^[a-z]$/i.match?(scheme)
+
+ # REFACTOR: split this up and dispatch on scheme (eg download_http)
+ # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner
+ case scheme
+ when "http", "https", "s3" then
+ unless File.exist? local_gem_path
+ begin
+ verbose "Downloading gem #{gem_file_name}"
+
+ remote_gem_path = source_uri + "gems/#{gem_file_name}"
+
+ cache_update_path remote_gem_path, local_gem_path
+ rescue FetchError
+ raise if spec.original_platform == spec.platform
+
+ alternate_name = "#{spec.original_name}.gem"
+
+ verbose "Failed, downloading gem #{alternate_name}"
+
+ remote_gem_path = source_uri + "gems/#{alternate_name}"
+
+ cache_update_path remote_gem_path, local_gem_path
+ end
+ end
+ when "file" then
+ begin
+ path = source_uri.path
+ path = File.dirname(path) if File.extname(path) == ".gem"
+
+ remote_gem_path = Gem::Util.correct_for_windows_path(File.join(path, "gems", gem_file_name))
+
+ FileUtils.cp(remote_gem_path, local_gem_path)
+ rescue Errno::EACCES
+ local_gem_path = source_uri.to_s
+ end
+
+ verbose "Using local gem #{local_gem_path}"
+ when nil then
+ source_path = if Gem.win_platform? && source_uri.scheme &&
+ !source_uri.path.include?(":")
+ "#{source_uri.scheme}:#{source_uri.path}"
+ else
+ source_uri.path
+ end
+
+ source_path = Gem::UriFormatter.new(source_path).unescape
+
+ begin
+ FileUtils.cp source_path, local_gem_path unless
+ File.identical?(source_path, local_gem_path)
+ rescue Errno::EACCES
+ local_gem_path = source_uri.to_s
+ end
+
+ verbose "Using local gem #{local_gem_path}"
+ else
+ raise ArgumentError, "unsupported URI scheme #{source_uri.scheme}"
+ end
+
+ local_gem_path
+ end
+
+ ##
+ # File Fetcher. Dispatched by +fetch_path+. Use it instead.
+
+ def fetch_file(uri, *_)
+ Gem.read_binary Gem::Util.correct_for_windows_path uri.path
+ end
+
+ ##
+ # HTTP Fetcher. Dispatched by +fetch_path+. Use it instead.
+
+ def fetch_http(uri, last_modified = nil, head = false, depth = 0)
+ 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 Gem::Net::HTTPOK, Gem::Net::HTTPNotModified then
+ response.uri = uri
+ head ? response : response.body
+ 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"]
+ raise FetchError.new("redirecting but no redirect location was given", uri)
+ end
+ location = Gem::Uri.new location
+
+ if https?(uri) && !https?(location)
+ raise FetchError.new("redirecting to non-https resource: #{location}", uri)
+ end
+
+ fetch_http(location, last_modified, head, depth + 1)
+ else
+ custom_error = response["X-Error-Message"]
+ error_detail = custom_error || response.message
+ raise FetchError.new("Bad response #{error_detail} #{response.code}", uri)
+ end
+ end
+
+ alias_method :fetch_https, :fetch_http
+
+ ##
+ # Downloads +uri+ and returns it as a String.
+
+ def fetch_path(uri, mtime = nil, head = false)
+ uri = Gem::Uri.new uri
+
+ method = {
+ "http" => "fetch_http",
+ "https" => "fetch_http",
+ "s3" => "fetch_s3",
+ "file" => "fetch_file",
+ }.fetch(uri.scheme) { raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" }
+
+ data = send method, uri, mtime, head
+
+ if data && !head && uri.to_s.end_with?(".gz")
+ begin
+ data = Gem::Util.gunzip data
+ rescue Zlib::GzipFile::Error
+ raise FetchError.new("server did not return a valid file", uri)
+ end
+ end
+
+ data
+ rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
+ *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
+ raise FetchError.new("#{e.class}: #{e}", uri)
+ end
+
+ def fetch_s3(uri, mtime = nil, head = false)
+ begin
+ public_uri = s3_uri_signer(uri, head ? "HEAD" : "GET").sign
+ rescue Gem::S3URISigner::ConfigurationError, Gem::S3URISigner::InstanceProfileError => e
+ raise FetchError.new(e.message, "s3://#{uri.host}")
+ end
+ fetch_https public_uri, mtime, head
+ end
+
+ # we have our own signing code here to avoid a dependency on the aws-sdk gem
+ def s3_uri_signer(uri, method)
+ Gem::S3URISigner.new(uri, method)
+ end
+
+ ##
+ # Downloads +uri+ to +path+ if necessary. If no path is given, it just
+ # passes the data.
+
+ def cache_update_path(uri, path = nil, update = true)
+ mtime = begin
+ path && File.stat(path).mtime
+ rescue StandardError
+ nil
+ end
+
+ data = fetch_path(uri, mtime)
+
+ if data.nil? # indicates the server returned 304 Not Modified
+ return Gem.read_binary(path)
+ end
+
+ if update && path
+ Gem.write_binary(path, data)
+ end
+
+ data
+ end
+
+ ##
+ # 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)
+ proxy = proxy_for @proxy, uri
+ pool = pools_for(proxy).pool_for uri
+
+ request = Gem::Request.new uri, request_class, last_modified, pool
+
+ request.fetch do |req|
+ yield req if block_given?
+ end
+ end
+
+ def https?(uri)
+ uri.scheme.casecmp("https").zero?
+ end
+
+ def close_all
+ @pools.each_value(&:close_all)
+ end
+
+ private
+
+ def proxy_for(proxy, uri)
+ Gem::Request.proxy_uri(proxy || Gem::Request.get_proxy_from_env(uri.scheme))
+ end
+
+ def pools_for(proxy)
+ @pool_lock.synchronize do
+ @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files, @pool_size
+ end
+ end
+end
diff --git a/lib/rubygems/request.rb b/lib/rubygems/request.rb
new file mode 100644
index 0000000000..e817ee5704
--- /dev/null
+++ b/lib/rubygems/request.rb
@@ -0,0 +1,299 @@
+# frozen_string_literal: true
+
+require_relative "vendored_net_http"
+require_relative "user_interaction"
+require_relative "uri_formatter"
+
+class Gem::Request
+ extend Gem::UserInteraction
+ include Gem::UserInteraction
+
+ ###
+ # Legacy. This is used in tests.
+ def self.create_with_proxy(uri, request_class, last_modified, proxy) # :nodoc:
+ cert_files = get_cert_files
+ proxy ||= get_proxy_from_env(uri.scheme)
+ pool = ConnectionPools.new proxy_uri(proxy), cert_files
+
+ new(uri, request_class, last_modified, pool.pool_for(uri))
+ end
+
+ def self.proxy_uri(proxy) # :nodoc:
+ require_relative "vendor/uri/lib/uri"
+ case proxy
+ when :no_proxy then nil
+ when Gem::URI::HTTP then proxy
+ else Gem::URI.parse(proxy)
+ end
+ end
+
+ def initialize(uri, request_class, last_modified, pool)
+ @uri = uri
+ @request_class = request_class
+ @last_modified = last_modified
+ @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 self.get_cert_files
+ pattern = File.expand_path("./ssl_certs/*/*.pem", __dir__)
+ Dir.glob(pattern)
+ end
+
+ def self.configure_connection_for_https(connection, cert_files)
+ raise Gem::Exception.new("OpenSSL is not available. Install OpenSSL and rebuild Ruby (preferred) or use non-HTTPS sources") unless Gem::HAVE_OPENSSL
+
+ connection.use_ssl = true
+ connection.verify_mode =
+ Gem.configuration.ssl_verify_mode || OpenSSL::SSL::VERIFY_PEER
+ store = OpenSSL::X509::Store.new
+
+ if Gem.configuration.ssl_client_cert
+ pem = File.read Gem.configuration.ssl_client_cert
+ connection.cert = OpenSSL::X509::Certificate.new pem
+ connection.key = OpenSSL::PKey::RSA.new pem
+ end
+
+ store.set_default_paths
+ cert_files.each do |ssl_cert_file|
+ store.add_file ssl_cert_file
+ end
+ if Gem.configuration.ssl_ca_cert
+ if File.directory? Gem.configuration.ssl_ca_cert
+ store.add_path Gem.configuration.ssl_ca_cert
+ else
+ store.add_file Gem.configuration.ssl_ca_cert
+ end
+ end
+ connection.cert_store = store
+
+ connection.verify_callback = proc do |preverify_ok, store_context|
+ verify_certificate store_context unless preverify_ok
+
+ preverify_ok
+ end
+
+ connection
+ end
+
+ def self.verify_certificate(store_context)
+ depth = store_context.error_depth
+ error = store_context.error_string
+ number = store_context.error
+ cert = store_context.current_cert
+
+ ui.alert_error "SSL verification error at depth #{depth}: #{error} (#{number})"
+
+ extra_message = verify_certificate_message number, cert
+
+ ui.alert_error extra_message if extra_message
+ end
+
+ def self.verify_certificate_message(error_number, cert)
+ return unless cert
+ case error_number
+ when OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED then
+ require "time"
+ "Certificate #{cert.subject} expired at #{cert.not_after.iso8601}"
+ when OpenSSL::X509::V_ERR_CERT_NOT_YET_VALID then
+ require "time"
+ "Certificate #{cert.subject} not valid until #{cert.not_before.iso8601}"
+ when OpenSSL::X509::V_ERR_CERT_REJECTED then
+ "Certificate #{cert.subject} is rejected"
+ when OpenSSL::X509::V_ERR_CERT_UNTRUSTED then
+ "Certificate #{cert.subject} is not trusted"
+ when OpenSSL::X509::V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT then
+ "Certificate #{cert.issuer} is not trusted"
+ when OpenSSL::X509::V_ERR_INVALID_CA then
+ "Certificate #{cert.subject} is an invalid CA certificate"
+ when OpenSSL::X509::V_ERR_INVALID_PURPOSE then
+ "Certificate #{cert.subject} has an invalid purpose"
+ when OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN then
+ "Root certificate is not trusted (#{cert.subject})"
+ when OpenSSL::X509::V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY then
+ "You must add #{cert.issuer} to your local trusted store"
+ when
+ OpenSSL::X509::V_ERR_UNABLE_TO_VERIFY_LEAF_SIGNATURE then
+ "Cannot verify certificate issued by #{cert.issuer}"
+ end
+ end
+
+ ##
+ # Creates or an HTTP connection based on +uri+, or retrieves an existing
+ # connection, using a proxy if needed.
+
+ def connection_for(uri)
+ @connection_pool.checkout
+ rescue Gem::HAVE_OPENSSL ? OpenSSL::SSL::SSLError : Errno::EHOSTDOWN,
+ Errno::EHOSTDOWN => e
+ raise Gem::RemoteFetcher::FetchError.new(e.message, uri)
+ end
+
+ def fetch
+ request = @request_class.new @uri.request_uri
+
+ unless @uri.nil? || @uri.user.nil? || @uri.user.empty?
+ request.basic_auth Gem::UriFormatter.new(@uri.user).unescape,
+ Gem::UriFormatter.new(@uri.password).unescape
+ end
+
+ request.add_field "User-Agent", @user_agent
+ request.add_field "Connection", "keep-alive"
+ request.add_field "Keep-Alive", "30"
+
+ if @last_modified
+ require "time"
+ request.add_field "If-Modified-Since", @last_modified.httpdate
+ end
+
+ yield request if block_given?
+
+ perform_request request
+ end
+
+ ##
+ # Returns a proxy URI for the given +scheme+ if one is set in the
+ # environment variables.
+
+ def self.get_proxy_from_env(scheme = "http")
+ downcase_scheme = scheme.downcase
+ upcase_scheme = scheme.upcase
+ env_proxy = ENV["#{downcase_scheme}_proxy"] || ENV["#{upcase_scheme}_PROXY"]
+
+ no_env_proxy = env_proxy.nil? || env_proxy.empty?
+
+ if no_env_proxy
+ return ["https", "http"].include?(downcase_scheme) ? :no_proxy : get_proxy_from_env("http")
+ end
+
+ require "uri"
+ uri = Gem::URI(Gem::UriFormatter.new(env_proxy).normalize)
+
+ if uri && uri.user.nil? && uri.password.nil?
+ user = ENV["#{downcase_scheme}_proxy_user"] || ENV["#{upcase_scheme}_PROXY_USER"]
+ password = ENV["#{downcase_scheme}_proxy_pass"] || ENV["#{upcase_scheme}_PROXY_PASS"]
+
+ uri.user = Gem::UriFormatter.new(user).escape
+ uri.password = Gem::UriFormatter.new(password).escape
+ end
+
+ uri
+ end
+
+ def perform_request(request) # :nodoc:
+ connection = connection_for @uri
+
+ retried = false
+ bad_response = false
+
+ begin
+ @requests[connection] += 1
+
+ verbose "#{request.method} #{Gem::Uri.redact(@uri)}"
+
+ file_name = File.basename(@uri.path)
+ # perform download progress reporter only for gems
+ if request.response_body_permitted? && file_name =~ /\.gem$/
+ reporter = ui.download_reporter
+ response = connection.request(request) do |incomplete_response|
+ if Gem::Net::HTTPOK === incomplete_response
+ reporter.fetch(file_name, incomplete_response.content_length)
+ downloaded = 0
+ data = String.new
+
+ incomplete_response.read_body do |segment|
+ data << segment
+ downloaded += segment.length
+ reporter.update(downloaded)
+ end
+ reporter.done
+ if incomplete_response.respond_to? :body=
+ incomplete_response.body = data
+ else
+ incomplete_response.instance_variable_set(:@body, data)
+ end
+ end
+ end
+ else
+ response = connection.request request
+ end
+
+ verbose "#{response.code} #{response.message}"
+ rescue Gem::Net::HTTPBadResponse
+ verbose "bad response"
+
+ reset connection
+
+ raise Gem::RemoteFetcher::FetchError.new("too many bad responses", @uri) if bad_response
+
+ bad_response = true
+ retry
+ rescue Gem::Net::HTTPFatalError
+ verbose "fatal error"
+
+ raise Gem::RemoteFetcher::FetchError.new("fatal error", @uri)
+ # HACK: work around EOFError bug in Gem::Net::HTTP
+ # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
+ # to install gems.
+ rescue EOFError, Gem::Timeout::Error,
+ Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
+
+ requests = @requests[connection]
+ verbose "connection reset after #{requests} requests, retrying"
+
+ raise Gem::RemoteFetcher::FetchError.new("too many connection resets", @uri) if retried
+
+ reset connection
+
+ retried = true
+ retry
+ end
+
+ response
+ ensure
+ @connection_pool.checkin connection
+ end
+
+ ##
+ # Resets HTTP connection +connection+.
+
+ def reset(connection)
+ @requests.delete connection
+
+ connection.finish
+ connection.start
+ end
+
+ def user_agent
+ ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}".dup
+
+ ruby_version = RUBY_VERSION
+ ruby_version += "dev" if RUBY_PATCHLEVEL == -1
+
+ ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}"
+ if RUBY_PATCHLEVEL >= 0
+ ua << " patchlevel #{RUBY_PATCHLEVEL}"
+ else
+ ua << " revision #{RUBY_REVISION}"
+ end
+ ua << ")"
+
+ ua << " #{RUBY_ENGINE}" if RUBY_ENGINE != "ruby"
+
+ ua
+ end
+end
+
+require_relative "request/http_pool"
+require_relative "request/https_pool"
+require_relative "request/connection_pools"
diff --git a/lib/rubygems/request/connection_pools.rb b/lib/rubygems/request/connection_pools.rb
new file mode 100644
index 0000000000..01e7e0629a
--- /dev/null
+++ b/lib/rubygems/request/connection_pools.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+class Gem::Request::ConnectionPools # :nodoc:
+ @client = Gem::Net::HTTP
+
+ class << self
+ attr_accessor :client
+ end
+
+ def initialize(proxy_uri, cert_files, pool_size = 1)
+ @proxy_uri = proxy_uri
+ @cert_files = cert_files
+ @pools = {}
+ @pool_mutex = Thread::Mutex.new
+ @pool_size = pool_size
+ end
+
+ def pool_for(uri)
+ http_args = net_http_args(uri, @proxy_uri)
+ key = http_args + [https?(uri)]
+ @pool_mutex.synchronize do
+ @pools[key] ||=
+ if https? uri
+ Gem::Request::HTTPSPool.new(http_args, @cert_files, @proxy_uri, @pool_size)
+ else
+ Gem::Request::HTTPPool.new(http_args, @cert_files, @proxy_uri, @pool_size)
+ end
+ end
+ end
+
+ def close_all
+ @pools.each_value(&:close_all)
+ end
+
+ private
+
+ ##
+ # Returns list of no_proxy entries (if any) from the environment
+
+ def get_no_proxy_from_env
+ env_no_proxy = ENV["no_proxy"] || ENV["NO_PROXY"]
+
+ return [] if env_no_proxy.nil? || env_no_proxy.empty?
+
+ env_no_proxy.split(/\s*,\s*/)
+ end
+
+ def https?(uri)
+ uri.scheme.casecmp("https").zero?
+ end
+
+ def no_proxy?(host, env_no_proxy)
+ host = host.downcase
+
+ env_no_proxy.any? do |pattern|
+ env_no_proxy_pattern = pattern.downcase.dup
+
+ # Remove dot in front of pattern for wildcard matching
+ env_no_proxy_pattern[0] = "" if env_no_proxy_pattern[0] == "."
+
+ host_tokens = host.split(".")
+ pattern_tokens = env_no_proxy_pattern.split(".")
+
+ intersection = (host_tokens - pattern_tokens) | (pattern_tokens - host_tokens)
+
+ # When we do the split into tokens we miss a dot character, so add it back if we need it
+ missing_dot = intersection.length > 0 ? 1 : 0
+ start = intersection.join(".").size + missing_dot
+
+ no_proxy_host = host[start..-1]
+
+ env_no_proxy_pattern == no_proxy_host
+ end
+ end
+
+ def net_http_args(uri, proxy_uri)
+ hostname = uri.hostname
+ net_http_args = [hostname, uri.port]
+
+ no_proxy = get_no_proxy_from_env
+
+ if proxy_uri && !no_proxy?(hostname, no_proxy)
+ proxy_hostname = proxy_uri.respond_to?(:hostname) ? proxy_uri.hostname : proxy_uri.host
+ net_http_args + [
+ proxy_hostname,
+ proxy_uri.port,
+ Gem::UriFormatter.new(proxy_uri.user).unescape,
+ Gem::UriFormatter.new(proxy_uri.password).unescape,
+ ]
+ elsif no_proxy? hostname, no_proxy
+ net_http_args + [nil, nil]
+ else
+ net_http_args
+ end
+ end
+end
diff --git a/lib/rubygems/request/http_pool.rb b/lib/rubygems/request/http_pool.rb
new file mode 100644
index 0000000000..468502ca6b
--- /dev/null
+++ b/lib/rubygems/request/http_pool.rb
@@ -0,0 +1,54 @@
+# 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
+# connection that corresponds to `http_args`. This class is private, do not
+# use it.
+
+class Gem::Request::HTTPPool # :nodoc:
+ attr_reader :cert_files, :proxy_uri
+
+ def initialize(http_args, cert_files, proxy_uri, pool_size)
+ @http_args = http_args
+ @cert_files = cert_files
+ @proxy_uri = proxy_uri
+ @pool_size = pool_size
+
+ @queue = Thread::SizedQueue.new @pool_size
+ setup_queue
+ end
+
+ def checkout
+ @queue.pop || make_connection
+ end
+
+ def checkin(connection)
+ @queue.push connection
+ end
+
+ def close_all
+ until @queue.empty?
+ if (connection = @queue.pop(true)) && connection.started?
+ connection.finish
+ end
+ end
+
+ setup_queue
+ end
+
+ private
+
+ def make_connection
+ setup_connection Gem::Request::ConnectionPools.client.new(*@http_args)
+ end
+
+ def setup_connection(connection)
+ connection.start
+ connection
+ end
+
+ def setup_queue
+ @pool_size.times { @queue.push(nil) }
+ end
+end
diff --git a/lib/rubygems/request/https_pool.rb b/lib/rubygems/request/https_pool.rb
new file mode 100644
index 0000000000..cb1d4b59b6
--- /dev/null
+++ b/lib/rubygems/request/https_pool.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Gem::Request::HTTPSPool < Gem::Request::HTTPPool # :nodoc:
+ private
+
+ def setup_connection(connection)
+ Gem::Request.configure_connection_for_https(connection, @cert_files)
+ super
+ end
+end
diff --git a/lib/rubygems/request_set.rb b/lib/rubygems/request_set.rb
new file mode 100644
index 0000000000..eb8b4658f3
--- /dev/null
+++ b/lib/rubygems/request_set.rb
@@ -0,0 +1,514 @@
+# frozen_string_literal: true
+
+require_relative "vendored_tsort"
+
+##
+# A RequestSet groups a request to activate a set of dependencies.
+#
+# nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6'
+# pg = Gem::Dependency.new 'pg', '~> 0.14'
+#
+# set = Gem::RequestSet.new nokogiri, pg
+#
+# requests = set.resolve
+#
+# p requests.map { |r| r.full_name }
+# #=> ["nokogiri-1.6.0", "mini_portile-0.5.1", "pg-0.17.0"]
+
+class Gem::RequestSet
+ include Gem::TSort
+
+ ##
+ # Array of gems to install even if already installed
+
+ attr_accessor :always_install
+
+ attr_reader :dependencies
+
+ attr_accessor :development
+
+ ##
+ # Errors fetching gems during resolution.
+
+ attr_reader :errors
+
+ ##
+ # Set to true if you want to install only direct development dependencies.
+
+ attr_accessor :development_shallow
+
+ ##
+ # The set of git gems imported via load_gemdeps.
+
+ attr_reader :git_set # :nodoc:
+
+ ##
+ # When true, dependency resolution is not performed, only the requested gems
+ # are installed.
+
+ attr_accessor :ignore_dependencies
+
+ attr_reader :install_dir # :nodoc:
+
+ ##
+ # If true, allow dependencies to match prerelease gems.
+
+ attr_accessor :prerelease
+
+ ##
+ # When false no remote sets are used for resolving gems.
+
+ attr_accessor :remote
+
+ attr_reader :resolver # :nodoc:
+
+ ##
+ # Sets used for resolution
+
+ attr_reader :sets # :nodoc:
+
+ ##
+ # Treat missing dependencies as silent errors
+
+ attr_accessor :soft_missing
+
+ ##
+ # The set of vendor gems imported via load_gemdeps.
+
+ attr_reader :vendor_set # :nodoc:
+
+ ##
+ # The set of source gems imported via load_gemdeps.
+
+ attr_reader :source_set
+
+ ##
+ # Creates a RequestSet for a list of Gem::Dependency objects, +deps+. You
+ # can then #resolve and #install the resolved list of dependencies.
+ #
+ # nokogiri = Gem::Dependency.new 'nokogiri', '~> 1.6'
+ # pg = Gem::Dependency.new 'pg', '~> 0.14'
+ #
+ # set = Gem::RequestSet.new nokogiri, pg
+
+ def initialize(*deps)
+ @dependencies = deps
+
+ @always_install = []
+ @conservative = false
+ @dependency_names = {}
+ @development = false
+ @development_shallow = false
+ @errors = []
+ @git_set = nil
+ @ignore_dependencies = false
+ @install_dir = Gem.dir
+ @prerelease = false
+ @remote = true
+ @requests = []
+ @sets = []
+ @soft_missing = false
+ @sorted_requests = nil
+ @specs = nil
+ @vendor_set = nil
+ @source_set = nil
+
+ yield self if block_given?
+ end
+
+ ##
+ # Declare that a gem of name +name+ with +reqs+ requirements is needed.
+
+ def gem(name, *reqs)
+ if dep = @dependency_names[name]
+ dep.requirement.concat reqs
+ else
+ dep = Gem::Dependency.new name, *reqs
+ @dependency_names[name] = dep
+ @dependencies << dep
+ end
+ end
+
+ ##
+ # Add +deps+ Gem::Dependency objects to the set.
+
+ def import(deps)
+ @dependencies.concat deps
+ end
+
+ ##
+ # Installs gems for this RequestSet using the Gem::Installer +options+.
+ #
+ # If a +block+ is given an activation +request+ and +installer+ are yielded.
+ # The +installer+ will be +nil+ if a gem matching the request was already
+ # installed.
+
+ def install(options, &block) # :yields: request, installer
+ if dir = options[:install_dir]
+ requests = install_into dir, false, options, &block
+ return requests
+ end
+
+ @prerelease = options[:prerelease]
+
+ requests = []
+ download_queue = Thread::Queue.new
+
+ # Create a thread-safe list of gems to download
+ sorted_requests.each do |req|
+ download_queue << req
+ end
+
+ # Create N threads in a pool, have them download all the gems
+ 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
+
+ Thread.new do
+ # The pop method will block waiting for items, so the only way
+ # to stop a thread from running is to provide a final item that
+ # means the thread should stop.
+ while req = download_queue.pop
+ break if req == :stop
+ req.spec.download options unless req.installed?
+ end
+ end
+ end
+
+ # Wait for all the downloads to finish before continuing
+ threads.each(&:value)
+
+ # Install requested gems after they have been downloaded
+ sorted_requests.each do |req|
+ if req.installed? && @always_install.none? {|spec| spec == req.spec.spec }
+ req.spec.spec.build_extensions unless options[:build_extension] == false
+ yield req, nil if block_given?
+ next
+ end
+
+ spec =
+ begin
+ req.spec.install options do |installer|
+ yield req, installer if block_given?
+ end
+ rescue Gem::RuntimeRequirementNotMetError => e
+ suggestion = "There are no versions of #{req.request} compatible with your Ruby & RubyGems"
+ suggestion += ". Maybe try installing an older version of the gem you're looking for?" unless @always_install.include?(req.spec.spec)
+ e.suggestion = suggestion
+ raise
+ end
+
+ requests << spec
+ end
+
+ return requests if options[:gemdeps]
+
+ install_hooks requests, options
+
+ requests
+ end
+
+ ##
+ # Installs from the gem dependencies files in the +:gemdeps+ option in
+ # +options+, yielding to the +block+ as in #install.
+ #
+ # If +:without_groups+ is given in the +options+, those groups in the gem
+ # dependencies file are not used. See Gem::Installer for other +options+.
+
+ def install_from_gemdeps(options, &block)
+ gemdeps = options[:gemdeps]
+
+ @install_dir = options[:install_dir] || Gem.dir
+ @prerelease = options[:prerelease]
+ @remote = options[:domain] != :local
+ @conservative = true if options[:conservative]
+
+ gem_deps_api = load_gemdeps gemdeps, options[:without_groups], true
+
+ resolve
+
+ if options[:explain]
+ puts "Gems to install:"
+
+ sorted_requests.each do |spec|
+ puts " #{spec.full_name}"
+ end
+ else
+ installed = install options, &block
+
+ if options.fetch :lock, true
+ lockfile =
+ Gem::RequestSet::Lockfile.build self, gemdeps, gem_deps_api.dependencies
+ lockfile.write
+ end
+
+ installed
+ end
+ end
+
+ def install_into(dir, force = true, options = {})
+ gem_home = ENV["GEM_HOME"]
+ ENV["GEM_HOME"] = dir
+
+ existing = force ? [] : specs_in(dir)
+ existing.delete_if {|s| @always_install.include? s }
+
+ dir = File.expand_path dir
+
+ installed = []
+
+ options[:development] = false
+ options[:install_dir] = dir
+ options[:only_install_dir] = true
+ @prerelease = options[:prerelease]
+
+ sorted_requests.each do |request|
+ spec = request.spec
+
+ if existing.find {|s| s.full_name == spec.full_name }
+ yield request, nil if block_given?
+ next
+ end
+
+ spec.install options do |installer|
+ yield request, installer if block_given?
+ end
+
+ installed << request
+ end
+
+ install_hooks installed, options
+
+ installed
+ ensure
+ ENV["GEM_HOME"] = gem_home
+ end
+
+ ##
+ # Call hooks on installed gems
+
+ def install_hooks(requests, options)
+ specs = requests.map do |request|
+ case request
+ when Gem::Resolver::ActivationRequest then
+ request.spec.spec
+ else
+ request
+ end
+ end
+
+ require_relative "dependency_installer"
+ inst = Gem::DependencyInstaller.new options
+ inst.installed_gems.replace specs
+
+ Gem.done_installing_hooks.each do |hook|
+ hook.call inst, specs
+ end unless Gem.done_installing_hooks.empty?
+ end
+
+ ##
+ # Load a dependency management file.
+
+ def load_gemdeps(path, without_groups = [], installing = false)
+ @git_set = Gem::Resolver::GitSet.new
+ @vendor_set = Gem::Resolver::VendorSet.new
+ @source_set = Gem::Resolver::SourceSet.new
+
+ @git_set.root_dir = @install_dir
+
+ lock_file = "#{File.expand_path(path)}.lock"
+ if File.exist?(lock_file)
+ load_lockfile lock_file
+ end
+
+ gf = Gem::RequestSet::GemDependencyAPI.new self, path
+ gf.installing = installing
+ gf.without_groups = without_groups if without_groups
+ gf.load
+ end
+
+ def load_lockfile(lock_file) # :nodoc:
+ require "bundler"
+ require "bundler/lockfile_parser"
+
+ # Bundler::Source::Path resolves relative `remote:` paths against
+ # Bundler.root, which raises when there is no Gemfile in the working
+ # directory. Anchor it to the lockfile's directory so PATH sections in a
+ # `gem install -g` lockfile can be parsed without a Bundler environment.
+ previous_root = Bundler.instance_variable_get(:@root)
+ Bundler.instance_variable_set(:@root, Pathname.new(File.expand_path(File.dirname(lock_file))))
+
+ parser = Bundler::LockfileParser.new(File.read(lock_file), lockfile_path: lock_file)
+
+ parser.specs.group_by(&:source).each do |source, specs|
+ case source
+ when Bundler::Source::Rubygems
+ remotes = source.remotes.map {|remote| Gem::Source.new(remote.to_s) }
+ remotes << Gem::Source.new(Gem::DEFAULT_HOST) if remotes.empty?
+ lock_set = Gem::Resolver::LockSet.new(remotes)
+ specs.each do |spec|
+ added = lock_set.add(spec.name, spec.version.to_s, spec.platform)
+ spec.dependencies.each do |dep|
+ added.each {|s| s.add_dependency dep }
+ end
+ end
+ @sets << lock_set
+ when Bundler::Source::Git
+ git_set = Gem::Resolver::GitSet.new
+ git_set.root_dir = @install_dir
+ specs.each do |spec|
+ git_spec = git_set.add_git_spec(
+ spec.name,
+ spec.version.to_s,
+ source.uri.to_s,
+ source.revision,
+ source.submodules || false
+ )
+ spec.dependencies.each {|dep| git_spec.add_dependency dep }
+ end
+ @sets << git_set
+ when Bundler::Source::Path
+ vendor_set = Gem::Resolver::VendorSet.new
+ specs.each do |spec|
+ loaded = vendor_set.add_vendor_gem(spec.name, source.path.to_s)
+ spec.dependencies.each {|dep| loaded.dependencies << dep }
+ end
+ @sets << vendor_set
+ end
+ end
+
+ parser.dependencies.each_value do |dep|
+ gem dep.name, *dep.requirement.as_list
+ end
+ ensure
+ Bundler.instance_variable_set(:@root, previous_root) if defined?(previous_root)
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[RequestSet:", "]" do
+ q.breakable
+
+ if @remote
+ q.text "remote"
+ q.breakable
+ end
+
+ if @prerelease
+ q.text "prerelease"
+ q.breakable
+ end
+
+ if @development_shallow
+ q.text "shallow development"
+ q.breakable
+ elsif @development
+ q.text "development"
+ q.breakable
+ end
+
+ if @soft_missing
+ q.text "soft missing"
+ end
+
+ q.group 2, "[dependencies:", "]" do
+ q.breakable
+ @dependencies.map do |dep|
+ q.text dep.to_s
+ q.breakable
+ end
+ end
+
+ q.breakable
+ q.text "sets:"
+
+ q.breakable
+ q.pp @sets.map(&:class)
+ end
+ end
+
+ ##
+ # Resolve the requested dependencies and return an Array of Specification
+ # objects to be activated.
+
+ def resolve(set = Gem::Resolver::BestSet.new)
+ @sets << set
+ @sets << @git_set
+ @sets << @vendor_set
+ @sets << @source_set
+
+ set = Gem::Resolver.compose_sets(*@sets)
+ set.remote = @remote
+ set.prerelease = @prerelease
+
+ resolver = Gem::Resolver.new @dependencies, set
+ resolver.development = @development
+ resolver.development_shallow = @development_shallow
+ resolver.ignore_dependencies = @ignore_dependencies
+ resolver.soft_missing = @soft_missing
+
+ if @conservative
+ installed_gems = {}
+ Gem::Specification.find_all do |spec|
+ (installed_gems[spec.name] ||= []) << spec
+ end
+ resolver.skip_gems = installed_gems
+ end
+
+ @resolver = resolver
+
+ @requests = resolver.resolve
+
+ @errors = set.errors
+
+ @requests
+ end
+
+ ##
+ # Resolve the requested dependencies against the gems available via Gem.path
+ # and return an Array of Specification objects to be activated.
+
+ def resolve_current
+ resolve Gem::Resolver::CurrentSet.new
+ end
+
+ def sorted_requests
+ @sorted_requests ||= strongly_connected_components.flatten
+ end
+
+ def specs
+ @specs ||= @requests.map(&:full_spec)
+ end
+
+ def specs_in(dir)
+ Gem::Util.glob_files_in_dir("*.gemspec", File.join(dir, "specifications")).map do |g|
+ Gem::Specification.load g
+ end
+ end
+
+ def tsort_each_node(&block) # :nodoc:
+ @requests.each(&block)
+ end
+
+ def tsort_each_child(node) # :nodoc:
+ node.spec.dependencies.each do |dep|
+ 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)
+ end
+
+ unless match
+ next if dep.type == :development && @development_shallow
+ next if @soft_missing
+ raise Gem::DependencyError,
+ "Unresolved dependency found during sorting - #{dep} (requested by #{node.spec.full_name})"
+ end
+
+ yield match
+ end
+ end
+end
+
+require_relative "request_set/gem_dependency_api"
+require_relative "request_set/lockfile"
diff --git a/lib/rubygems/request_set/gem_dependency_api.rb b/lib/rubygems/request_set/gem_dependency_api.rb
new file mode 100644
index 0000000000..99d96f928b
--- /dev/null
+++ b/lib/rubygems/request_set/gem_dependency_api.rb
@@ -0,0 +1,841 @@
+# frozen_string_literal: true
+
+##
+# A semi-compatible DSL for the Bundler Gemfile and Isolate gem dependencies
+# files.
+#
+# To work with both the Bundler Gemfile and Isolate formats this
+# implementation takes some liberties to allow compatibility with each, most
+# notably in #source.
+#
+# A basic gem dependencies file will look like the following:
+#
+# source 'https://rubygems.org'
+#
+# gem 'rails', '3.2.14a
+# gem 'devise', '~> 2.1', '>= 2.1.3'
+# gem 'cancan'
+# gem 'airbrake'
+# gem 'pg'
+#
+# RubyGems recommends saving this as gem.deps.rb over Gemfile or Isolate.
+#
+# To install the gems in this Gemfile use `gem install -g` to install it and
+# create a lockfile. The lockfile will ensure that when you make changes to
+# your gem dependencies file a minimum amount of change is made to the
+# dependencies of your gems.
+#
+# RubyGems can activate all the gems in your dependencies file at startup
+# using the RUBYGEMS_GEMDEPS environment variable or through Gem.use_gemdeps.
+# See Gem.use_gemdeps for details and warnings.
+#
+# See `gem help install` and `gem help gem_dependencies` for further details.
+
+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],
+ }.freeze
+
+ mswin = Gem::Platform.new "x86-mswin32"
+ mswin64 = Gem::Platform.new "x64-mswin64"
+ x86_mingw = Gem::Platform.new "x86-mingw32"
+ x64_mingw = Gem::Platform.new "x64-mingw32"
+
+ 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,
+ }.freeze
+
+ gt_eq_0 = Gem::Requirement.new ">= 0"
+ tilde_gt_1_8_0 = Gem::Requirement.new "~> 1.8.0"
+ tilde_gt_1_9_0 = Gem::Requirement.new "~> 1.9.0"
+ tilde_gt_2_0_0 = Gem::Requirement.new "~> 2.0.0"
+ tilde_gt_2_1_0 = Gem::Requirement.new "~> 2.1.0"
+
+ 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,
+ }.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,
+ }.freeze
+
+ ##
+ # The gems required by #gem statements in the gem.deps.rb file
+
+ attr_reader :dependencies
+
+ ##
+ # A set of gems that are loaded via the +:git+ option to #gem
+
+ attr_reader :git_set # :nodoc:
+
+ ##
+ # A Hash containing gem names and files to require from those gems.
+
+ attr_reader :requires
+
+ ##
+ # A set of gems that are loaded via the +:path+ option to #gem
+
+ attr_reader :vendor_set # :nodoc:
+
+ ##
+ # The groups of gems to exclude from installation
+
+ attr_accessor :without_groups # :nodoc:
+
+ ##
+ # Creates a new GemDependencyAPI that will add dependencies to the
+ # Gem::RequestSet +set+ based on the dependency API description in +path+.
+
+ def initialize(set, path)
+ @set = set
+ @path = path
+
+ @current_groups = nil
+ @current_platforms = nil
+ @current_repository = nil
+ @dependencies = {}
+ @default_sources = true
+ @git_set = @set.git_set
+ @git_sources = {}
+ @installing = false
+ @requires = Hash.new {|h, name| h[name] = [] }
+ @vendor_set = @set.vendor_set
+ @source_set = @set.source_set
+ @gem_sources = {}
+ @without_groups = []
+
+ git_source :github do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/"
+
+ "https://github.com/#{repo_name}.git"
+ end
+
+ git_source :bitbucket do |repo_name|
+ repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include? "/"
+
+ user, = repo_name.split "/", 2
+
+ "https://#{user}@bitbucket.org/#{repo_name}.git"
+ end
+ end
+
+ ##
+ # Adds +dependencies+ to the request set if any of the +groups+ are allowed.
+ # This is used for gemspec dependencies.
+
+ def add_dependencies(groups, dependencies) # :nodoc:
+ return unless (groups & @without_groups).empty?
+
+ dependencies.each do |dep|
+ @set.gem dep.name, *dep.requirement.as_list
+ end
+ end
+
+ private :add_dependencies
+
+ ##
+ # Finds a gemspec with the given +name+ that lives at +path+.
+
+ def find_gemspec(name, path) # :nodoc:
+ glob = File.join path, "#{name}.gemspec"
+
+ spec_files = Dir[glob]
+
+ case spec_files.length
+ when 1 then
+ spec_file = spec_files.first
+
+ spec = Gem::Specification.load spec_file
+
+ return spec if spec
+
+ raise ArgumentError, "invalid gemspec #{spec_file}"
+ when 0 then
+ raise ArgumentError, "no gemspecs found at #{Dir.pwd}"
+ else
+ raise ArgumentError,
+ "found multiple gemspecs at #{Dir.pwd}, " \
+ "use the name: option to specify the one you want"
+ end
+ end
+
+ ##
+ # Changes the behavior of gem dependency file loading to installing mode.
+ # In installing mode certain restrictions are ignored such as ruby version
+ # mismatch checks.
+
+ def installing=(installing) # :nodoc:
+ @installing = installing
+ end
+
+ ##
+ # Loads the gem dependency file and returns self.
+
+ def load
+ instance_eval File.read(@path), @path, 1
+
+ self
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # :call-seq:
+ # gem(name)
+ # gem(name, *requirements)
+ # gem(name, *requirements, options)
+ #
+ # Specifies a gem dependency with the given +name+ and +requirements+. You
+ # may also supply +options+ following the +requirements+
+ #
+ # +options+ include:
+ #
+ # require: ::
+ # RubyGems does not provide any autorequire features so requires in a gem
+ # dependencies file are recorded but ignored.
+ #
+ # In bundler the require: option overrides the file to require during
+ # Bundler.require. By default the name of the dependency is required in
+ # Bundler. A single file or an Array of files may be given.
+ #
+ # To disable requiring any file give +false+:
+ #
+ # gem 'rake', require: false
+ #
+ # group: ::
+ # Place the dependencies in the given dependency group. A single group or
+ # an Array of groups may be given.
+ #
+ # See also #group
+ #
+ # platform: ::
+ # Only install the dependency on the given platform. A single platform or
+ # an Array of platforms may be given.
+ #
+ # See #platform for a list of platforms available.
+ #
+ # path: ::
+ # Install this dependency from an unpacked gem in the given directory.
+ #
+ # gem 'modified_gem', path: 'vendor/modified_gem'
+ #
+ # git: ::
+ # Install this dependency from a git repository:
+ #
+ # gem 'private_gem', git: 'git@my.company.example:private_gem.git'
+ #
+ # gist: ::
+ # Install this dependency from the gist ID:
+ #
+ # gem 'bang', gist: '1232884'
+ #
+ # github: ::
+ # Install this dependency from a github git repository:
+ #
+ # gem 'private_gem', github: 'my_company/private_gem'
+ #
+ # submodules: ::
+ # Set to +true+ to include submodules when fetching the git repository for
+ # git:, gist: and github: dependencies.
+ #
+ # ref: ::
+ # Use the given commit name or SHA for git:, gist: and github:
+ # dependencies.
+ #
+ # branch: ::
+ # Use the given branch for git:, gist: and github: dependencies.
+ #
+ # tag: ::
+ # Use the given tag for git:, gist: and github: dependencies.
+
+ def gem(name, *requirements)
+ options = requirements.pop if requirements.last.is_a?(Hash)
+ options ||= {}
+
+ options[:git] = @current_repository if @current_repository
+
+ source_set = false
+
+ source_set ||= gem_path name, options
+ source_set ||= gem_git name, options
+ source_set ||= gem_git_source name, options
+ source_set ||= gem_source name, options
+
+ duplicate = @dependencies.include? name
+
+ @dependencies[name] =
+ if requirements.empty? && !source_set
+ Gem::Requirement.default
+ elsif source_set
+ Gem::Requirement.source_set
+ else
+ Gem::Requirement.create requirements
+ end
+
+ return unless gem_platforms name, options
+
+ groups = gem_group name, options
+
+ return unless (groups & @without_groups).empty?
+
+ pin_gem_source name, :default unless source_set
+
+ gem_requires name, options
+
+ if duplicate
+ warn <<-WARNING
+Gem dependencies file #{@path} requires #{name} more than once.
+ WARNING
+ end
+
+ @set.gem name, *requirements
+ end
+
+ ##
+ # Handles the git: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the gist or git option was handled.
+
+ def gem_git(name, options) # :nodoc:
+ if gist = options.delete(:gist)
+ options[:git] = "https://gist.github.com/#{gist}.git"
+ end
+
+ return unless repository = options.delete(:git)
+
+ pin_gem_source name, :git, repository
+
+ reference = gem_git_reference options
+
+ submodules = options.delete :submodules
+
+ @git_set.add_git_gem name, repository, reference, submodules
+
+ true
+ end
+
+ ##
+ # Handles the git options from +options+ for git gem.
+ #
+ # Returns reference for the git gem.
+
+ def gem_git_reference(options) # :nodoc:
+ ref = options.delete :ref
+ branch = options.delete :branch
+ tag = options.delete :tag
+
+ reference = nil
+ reference ||= ref
+ reference ||= branch
+ reference ||= tag
+
+ if ref && branch
+ warn <<-WARNING
+Gem dependencies file #{@path} includes git reference for both ref and branch but only ref is used.
+ WARNING
+ end
+ if (ref || branch) && tag
+ warn <<-WARNING
+Gem dependencies file #{@path} includes git reference for both ref/branch and tag but only ref/branch is used.
+ WARNING
+ end
+
+ reference
+ end
+
+ private :gem_git
+
+ ##
+ # Handles a git gem option from +options+ for gem +name+ for a git source
+ # registered through git_source.
+ #
+ # Returns +true+ if the custom source option was handled.
+
+ def gem_git_source(name, options) # :nodoc:
+ return unless git_source = (@git_sources.keys & options.keys).last
+
+ source_callback = @git_sources[git_source]
+ source_param = options.delete git_source
+
+ git_url = source_callback.call source_param
+
+ options[:git] = git_url
+
+ gem_git name, options
+
+ true
+ end
+
+ private :gem_git_source
+
+ ##
+ # Handles the :group and :groups +options+ for the gem with the given
+ # +name+.
+
+ def gem_group(name, options) # :nodoc:
+ g = options.delete :group
+ all_groups = g ? Array(g) : []
+
+ groups = options.delete :groups
+ all_groups |= groups if groups
+
+ all_groups |= @current_groups if @current_groups
+
+ all_groups
+ end
+
+ private :gem_group
+
+ ##
+ # Handles the path: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the path option was handled.
+
+ def gem_path(name, options) # :nodoc:
+ return unless directory = options.delete(:path)
+
+ pin_gem_source name, :path, directory
+
+ @vendor_set.add_vendor_gem name, directory
+
+ true
+ end
+
+ private :gem_path
+
+ ##
+ # Handles the source: option from +options+ for gem +name+.
+ #
+ # Returns +true+ if the source option was handled.
+
+ def gem_source(name, options) # :nodoc:
+ return unless source = options.delete(:source)
+
+ pin_gem_source name, :source, source
+
+ @source_set.add_source_gem name, source
+
+ true
+ end
+
+ private :gem_source
+
+ ##
+ # Handles the platforms: option from +options+. Returns true if the
+ # 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.concat @current_platforms if @current_platforms
+
+ return true if platform_names.empty?
+
+ platform_names.any? do |platform_name|
+ raise ArgumentError, "unknown platform #{platform_name.inspect}" unless
+ platform = PLATFORM_MAP[platform_name]
+
+ next false unless Gem::Platform.match_gem? platform, name
+
+ if engines = ENGINE_MAP[platform_name]
+ next false unless engines.include? Gem.ruby_engine
+ end
+
+ case WINDOWS[platform_name]
+ when :only then
+ next false unless Gem.win_platform?
+ when :never then
+ next false if Gem.win_platform?
+ end
+
+ VERSION_MAP[platform_name].satisfied_by? Gem.ruby_version
+ end
+ end
+
+ private :gem_platforms
+
+ ##
+ # Records the require: option from +options+ and adds those files, or the
+ # default file to the require list for +name+.
+
+ def gem_requires(name, options) # :nodoc:
+ if options.include? :require
+ if requires = options.delete(:require)
+ @requires[name].concat Array requires
+ end
+ else
+ @requires[name] << name
+ end
+ raise ArgumentError, "Unhandled gem options #{options.inspect}" unless options.empty?
+ end
+
+ private :gem_requires
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for specifying gems from a git +repository+.
+ #
+ # git 'https://github.com/rails/rails.git' do
+ # gem 'activesupport'
+ # gem 'activerecord'
+ # end
+
+ def git(repository)
+ @current_repository = repository
+
+ yield
+ ensure
+ @current_repository = nil
+ end
+
+ ##
+ # Defines a custom git source that uses +name+ to expand git repositories
+ # for use in gems built from git repositories. You must provide a block
+ # that accepts a git repository name for expansion.
+
+ def git_source(name, &callback)
+ @git_sources[name] = callback
+ end
+
+ ##
+ # Returns the basename of the file the dependencies were loaded from
+
+ def gem_deps_file # :nodoc:
+ File.basename @path
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Loads dependencies from a gemspec file.
+ #
+ # +options+ include:
+ #
+ # name: ::
+ # The name portion of the gemspec file. Defaults to searching for any
+ # gemspec file in the current directory.
+ #
+ # gemspec name: 'my_gem'
+ #
+ # path: ::
+ # The path the gemspec lives in. Defaults to the current directory:
+ #
+ # gemspec 'my_gem', path: 'gemspecs', name: 'my_gem'
+ #
+ # development_group: ::
+ # The group to add development dependencies to. By default this is
+ # :development. Only one group may be specified.
+
+ def gemspec(options = {})
+ name = options.delete(:name) || "{,*}"
+ path = options.delete(:path) || "."
+ development_group = options.delete(:development_group) || :development
+
+ spec = find_gemspec name, path
+
+ groups = gem_group spec.name, {}
+
+ self_dep = Gem::Dependency.new spec.name, spec.version
+
+ add_dependencies groups, [self_dep]
+ add_dependencies groups, spec.runtime_dependencies
+
+ @dependencies[spec.name] = Gem::Requirement.source_set
+
+ spec.dependencies.each do |dep|
+ @dependencies[dep.name] = dep.requirement
+ end
+
+ groups << development_group
+
+ add_dependencies groups, spec.development_dependencies
+
+ @vendor_set.add_vendor_gem spec.name, path
+ gem_requires spec.name, options
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for placing a dependency in the given +groups+.
+ #
+ # group :development do
+ # gem 'debugger'
+ # end
+ #
+ # group :development, :test do
+ # gem 'minitest'
+ # end
+ #
+ # Groups can be excluded at install time using `gem install -g --without
+ # development`. See `gem help install` and `gem help gem_dependencies` for
+ # further details.
+
+ def group(*groups)
+ @current_groups = groups
+
+ yield
+ ensure
+ @current_groups = nil
+ end
+
+ ##
+ # Pins the gem +name+ to the given +source+. Adding a gem with the same
+ # name from a different +source+ will raise an exception.
+
+ def pin_gem_source(name, type = :default, source = nil)
+ source_description =
+ case type
+ when :default then "(default)"
+ when :path then "path: #{source}"
+ when :git then "git: #{source}"
+ when :source then "source: #{source}"
+ else "(unknown)"
+ end
+
+ raise ArgumentError,
+ "duplicate source #{source_description} for gem #{name}" if
+ @gem_sources.fetch(name, source) != source
+
+ @gem_sources[name] = source
+ end
+
+ private :pin_gem_source
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for restricting gems to a set of platforms.
+ #
+ # The gem dependencies platform is different from Gem::Platform. A platform
+ # gem.deps.rb platform matches on the ruby engine, the ruby version and
+ # whether or not windows is allowed.
+ #
+ # :ruby, :ruby_XY ::
+ # Matches non-windows, non-jruby implementations where X and Y can be used
+ # to match releases in the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :mri, :mri_XY ::
+ # Matches non-windows C Ruby (Matz Ruby) or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :mingw, :mingw_XY ::
+ # Matches 32 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :x64_mingw, :x64_mingw_XY ::
+ # Matches 64 bit C Ruby on MinGW or only the 1.8, 1.9, 2.0 or 2.1 series.
+ #
+ # :mswin, :mswin_XY ::
+ # Matches 32 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :mswin64, :mswin64_XY ::
+ # Matches 64 bit C Ruby on Microsoft Windows or only the 1.8, 1.9, 2.0 or
+ # 2.1 series.
+ #
+ # :jruby, :jruby_XY ::
+ # Matches JRuby or JRuby in 1.8 or 1.9 mode.
+ #
+ # :maglev ::
+ # Matches Maglev
+ #
+ # :rbx ::
+ # Matches non-windows Rubinius
+ #
+ # NOTE: There is inconsistency in what environment a platform matches. You
+ # may need to read the source to know the exact details.
+
+ def platform(*platforms)
+ @current_platforms = platforms
+
+ yield
+ ensure
+ @current_platforms = nil
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Block form for restricting gems to a particular set of platforms. See
+ # #platform.
+
+ alias_method :platforms, :platform
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Restricts this gem dependencies file to the given ruby +version+.
+ #
+ # You may also provide +engine:+ and +engine_version:+ options to restrict
+ # this gem dependencies file to a particular ruby engine and its engine
+ # version. This matching is performed by using the RUBY_ENGINE and
+ # RUBY_ENGINE_VERSION constants.
+
+ def ruby(version, options = {})
+ engine = options[:engine]
+ engine_version = options[:engine_version]
+
+ raise ArgumentError,
+ "You must specify engine_version along with the Ruby engine" if
+ engine && !engine_version
+
+ return true if @installing
+
+ 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}, " \
+ "but your #{gem_deps_file} requires #{engine}"
+
+ raise Gem::RubyVersionMismatch, message
+ end
+
+ if engine_version
+ if engine_version != RUBY_ENGINE_VERSION
+ message =
+ "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
+
+ true
+ end
+
+ ##
+ # :category: Gem Dependencies DSL
+ #
+ # Sets +url+ as a source for gems for this dependency API. RubyGems uses
+ # the default configured sources if no source was given. If a source is set
+ # only that source is used.
+ #
+ # This method differs in behavior from Bundler:
+ #
+ # * The +:gemcutter+, # +:rubygems+ and +:rubyforge+ sources are not
+ # supported as they are deprecated in bundler.
+ # * The +prepend:+ option is not supported. If you wish to order sources
+ # then list them in your preferred order.
+
+ def source(url)
+ Gem.sources.clear if @default_sources
+
+ @default_sources = false
+
+ Gem.sources << url
+ end
+end
diff --git a/lib/rubygems/request_set/lockfile.rb b/lib/rubygems/request_set/lockfile.rb
new file mode 100644
index 0000000000..8b9c9690d6
--- /dev/null
+++ b/lib/rubygems/request_set/lockfile.rb
@@ -0,0 +1,233 @@
+# 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
+# constructed.
+
+class Gem::RequestSet::Lockfile
+ ##
+ # Raised when a lockfile cannot be parsed
+
+ class ParseError < Gem::Exception
+ ##
+ # The column where the error was encountered
+
+ attr_reader :column
+
+ ##
+ # The line where the error was encountered
+
+ attr_reader :line
+
+ ##
+ # The location of the lock file
+
+ attr_reader :path
+
+ ##
+ # Raises a ParseError with the given +message+ which was encountered at a
+ # +line+ and +column+ while parsing.
+
+ def initialize(message, column, line, path)
+ @line = line
+ @column = column
+ @path = path
+ super "#{message} (at line #{line} column #{column})"
+ end
+ end
+
+ ##
+ # Creates a new Lockfile for the given Gem::RequestSet and +gem_deps_file+
+ # location.
+
+ def self.build(request_set, gem_deps_file, dependencies = nil)
+ request_set.resolve
+ dependencies ||= requests_to_deps request_set.sorted_requests
+ new request_set, gem_deps_file, dependencies
+ end
+
+ def self.requests_to_deps(requests) # :nodoc:
+ deps = {}
+
+ requests.each do |request|
+ spec = request.spec
+ name = request.name
+ requirement = request.request.dependency.requirement
+
+ deps[name] = if [Gem::Resolver::VendorSpecification,
+ Gem::Resolver::GitSpecification].include? spec.class
+ Gem::Requirement.source_set
+ else
+ requirement
+ end
+ end
+
+ deps
+ end
+
+ ##
+ # The platforms for this Lockfile
+
+ attr_reader :platforms
+
+ def initialize(request_set, gem_deps_file, dependencies)
+ @set = request_set
+ @dependencies = dependencies
+ @gem_deps_file = File.expand_path(gem_deps_file)
+ @gem_deps_dir = File.dirname(@gem_deps_file)
+ @platforms = []
+ end
+
+ def add_DEPENDENCIES(out) # :nodoc:
+ out << "DEPENDENCIES"
+
+ out.concat @dependencies.sort.map {|name, requirement|
+ " #{name}#{requirement.for_lockfile}"
+ }
+
+ out << nil
+ end
+
+ def add_GEM(out, spec_groups) # :nodoc:
+ return if spec_groups.empty?
+
+ source_groups = spec_groups.values.flatten.group_by do |request|
+ request.spec.source.uri
+ end
+
+ source_groups.sort_by {|group,| group.to_s }.map do |group, requests|
+ out << "GEM"
+ out << " remote: #{group}"
+ out << " specs:"
+
+ requests.sort_by(&:name).each do |request|
+ next if request.spec.name == "bundler"
+ platform = "-#{request.spec.platform}" unless
+ request.spec.platform == Gem::Platform::RUBY
+
+ out << " #{request.name} (#{request.version}#{platform})"
+
+ request.full_spec.dependencies.sort.each do |dependency|
+ next if dependency.type == :development
+
+ requirement = dependency.requirement
+ out << " #{dependency.name}#{requirement.for_lockfile}"
+ end
+ end
+ out << nil
+ end
+ end
+
+ def add_GIT(out, git_requests)
+ return if git_requests.empty?
+
+ by_repository_revision = git_requests.group_by do |request|
+ source = request.spec.source
+ [source.repository, source.rev_parse]
+ end
+
+ by_repository_revision.each do |(repository, revision), requests|
+ out << "GIT"
+ out << " remote: #{repository}"
+ out << " revision: #{revision}"
+ out << " specs:"
+
+ requests.sort_by(&:name).each do |request|
+ out << " #{request.name} (#{request.version})"
+
+ dependencies = request.spec.dependencies.sort_by(&:name)
+ dependencies.each do |dep|
+ out << " #{dep.name}#{dep.requirement.for_lockfile}"
+ end
+ end
+ out << nil
+ end
+ end
+
+ def relative_path_from(dest, base) # :nodoc:
+ dest = File.expand_path(dest)
+ base = File.expand_path(base)
+
+ if dest.index(base) == 0
+ offset = dest[base.size + 1..-1]
+
+ return "." unless offset
+
+ offset
+ else
+ dest
+ end
+ end
+
+ def add_PATH(out, path_requests) # :nodoc:
+ return if path_requests.empty?
+
+ out << "PATH"
+ path_requests.each do |request|
+ directory = File.expand_path(request.spec.source.uri)
+
+ out << " remote: #{relative_path_from directory, @gem_deps_dir}"
+ out << " specs:"
+ out << " #{request.name} (#{request.version})"
+ end
+
+ out << nil
+ end
+
+ def add_PLATFORMS(out) # :nodoc:
+ out << "PLATFORMS"
+
+ platforms = requests.map {|request| request.spec.platform }.uniq
+
+ platforms = platforms.sort_by(&:to_s)
+
+ platforms.each do |platform|
+ out << " #{platform}"
+ end
+
+ out << nil
+ end
+
+ def spec_groups
+ requests.group_by {|request| request.spec.class }
+ end
+
+ ##
+ # The contents of the lock file.
+
+ def to_s
+ out = []
+
+ groups = spec_groups
+
+ add_PATH out, groups.delete(Gem::Resolver::VendorSpecification) { [] }
+
+ add_GIT out, groups.delete(Gem::Resolver::GitSpecification) { [] }
+
+ add_GEM out, groups
+
+ add_PLATFORMS out
+
+ add_DEPENDENCIES out
+
+ out.join "\n"
+ end
+
+ ##
+ # Writes the lock file alongside the gem dependencies file
+
+ def write
+ content = to_s
+
+ File.open "#{@gem_deps_file}.lock", "w" do |io|
+ io.write content
+ end
+ end
+
+ private
+
+ def requests
+ @set.sorted_requests
+ end
+end
diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb
new file mode 100644
index 0000000000..0d3f98eb0f
--- /dev/null
+++ b/lib/rubygems/requirement.rb
@@ -0,0 +1,298 @@
+# frozen_string_literal: true
+
+require_relative "version"
+
+##
+# A Requirement is a set of one or more version restrictions. It supports a
+# few (<tt>=, !=, >, <, >=, <=, ~></tt>) different restriction operators.
+#
+# See Gem::Version for a description on how versions and requirements work
+# together in RubyGems.
+
+class Gem::Requirement
+ OPS = { # :nodoc:
+ "=" => lambda {|v, r| v == r },
+ "!=" => lambda {|v, r| v != r },
+ ">" => lambda {|v, r| v > r },
+ "<" => lambda {|v, r| v < r },
+ ">=" => lambda {|v, r| v >= r },
+ "<=" => lambda {|v, r| v <= r },
+ "~>" => lambda {|v, r| v >= r && v.release < r.bump },
+ }.freeze
+
+ SOURCE_SET_REQUIREMENT = Struct.new(:for_lockfile).new "!" # :nodoc:
+
+ quoted = Regexp.union(OPS.keys)
+ PATTERN_RAW = "\\s*(#{quoted})?\\s*(#{Gem::Version::VERSION_PATTERN})\\s*".freeze # :nodoc:
+
+ ##
+ # A regular expression that matches a requirement
+
+ PATTERN = /\A#{PATTERN_RAW}\z/
+
+ ##
+ # The default requirement matches any non-prerelease version
+
+ DefaultRequirement = [">=", Gem::Version.new(0)].freeze
+
+ ##
+ # The default requirement matches any version
+
+ DefaultPrereleaseRequirement = [">=", Gem::Version.new("0.a")].freeze
+
+ ##
+ # Raised when a bad requirement is encountered
+
+ class BadRequirementError < ArgumentError; end
+
+ ##
+ # Factory method to create a Gem::Requirement object. Input may be
+ # a Version, a String, or nil. Intended to simplify client code.
+ #
+ # If the input is "weird", the default version requirement is
+ # returned.
+
+ def self.create(*inputs)
+ return new inputs if inputs.length > 1
+
+ input = inputs.shift
+
+ case input
+ when Gem::Requirement then
+ input
+ when Gem::Version, Array then
+ new input
+ when "!" then
+ source_set
+ else
+ if input.respond_to? :to_str
+ new [input.to_str]
+ else
+ default
+ end
+ end
+ end
+
+ def self.default
+ new ">= 0"
+ end
+
+ def self.default_prerelease
+ new ">= 0.a"
+ end
+
+ ###
+ # A source set requirement, used for Gemfiles and lockfiles
+
+ def self.source_set # :nodoc:
+ SOURCE_SET_REQUIREMENT
+ end
+
+ ##
+ # Parse +obj+, returning an <tt>[op, version]</tt> pair. +obj+ can
+ # be a String or a Gem::Version.
+ #
+ # If +obj+ is a String, it can be either a full requirement
+ # specification, like <tt>">= 1.2"</tt>, or a simple version number,
+ # like <tt>"1.2"</tt>.
+ #
+ # parse("> 1.0") # => [">", Gem::Version.new("1.0")]
+ # parse("1.0") # => ["=", Gem::Version.new("1.0")]
+ # parse(Gem::Version.new("1.0")) # => ["=, Gem::Version.new("1.0")]
+
+ def self.parse(obj)
+ return ["=", obj] if Gem::Version === obj
+
+ unless PATTERN =~ obj.to_s
+ raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
+ end
+ op = -($1 || "=")
+ version = -$2
+
+ if op == ">=" && version == "0"
+ DefaultRequirement
+ elsif op == ">=" && version == "0.a"
+ DefaultPrereleaseRequirement
+ else
+ [op, Gem::Version.new(version)]
+ end
+ end
+
+ ##
+ # 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:
+
+ ##
+ # Constructs a requirement from +requirements+. Requirements can be
+ # Strings, Gem::Versions, or Arrays of those. +nil+ and duplicate
+ # requirements are ignored. An empty set of +requirements+ is the
+ # same as <tt>">= 0"</tt>.
+
+ def initialize(*requirements)
+ requirements = requirements.flatten
+ requirements.compact!
+ requirements.uniq!
+
+ if requirements.empty?
+ @requirements = [DefaultRequirement]
+ else
+ @requirements = requirements.map! {|r| self.class.parse r }
+ end
+ end
+
+ ##
+ # Concatenates the +new+ requirements onto this requirement.
+
+ def concat(new)
+ new = new.flatten
+ new.compact!
+ new.uniq!
+ new = new.map {|r| self.class.parse r }
+
+ @requirements.concat new
+ end
+
+ ##
+ # Formats this requirement for use in a Gem::RequestSet::Lockfile.
+
+ def for_lockfile # :nodoc:
+ return if @requirements == [DefaultRequirement]
+
+ list = requirements.sort_by do |_, version|
+ version
+ end.map do |op, version|
+ "#{op} #{version}"
+ end.uniq
+
+ " (#{list.join ", "})"
+ end
+
+ ##
+ # true if this gem has no requirements.
+
+ def none?
+ if @requirements.size == 1
+ @requirements[0] == DefaultRequirement
+ else
+ false
+ end
+ end
+
+ ##
+ # true if the requirement is for only an exact version
+
+ def exact?
+ return false unless @requirements.size == 1
+ @requirements[0][0] == "="
+ end
+
+ def as_list # :nodoc:
+ requirements.map {|op, version| "#{op} #{version}" }
+ end
+
+ def hash # :nodoc:
+ requirements.map {|r| r.first == "~>" ? [r[0], r[1].to_s] : r }.sort.hash
+ end
+
+ def marshal_dump # :nodoc:
+ [@requirements]
+ end
+
+ def marshal_load(array) # :nodoc:
+ @requirements = array[0]
+
+ raise TypeError, "wrong @requirements" unless Array === @requirements &&
+ @requirements.all? {|r| r.size == 2 && (r.first.is_a?(String) || r[0] = "=") && r.last.is_a?(Gem::Version) }
+ end
+
+ def yaml_initialize(tag, vals) # :nodoc:
+ vals.each do |ivar, val|
+ instance_variable_set "@#{ivar}", val
+ end
+ end
+
+ def init_with(coder) # :nodoc:
+ yaml_initialize coder.tag, coder.map
+ end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "requirements", @requirements
+ end
+
+ ##
+ # A requirement is a prerelease if any of the versions inside of it
+ # are prereleases
+
+ def prerelease?
+ requirements.any? {|r| r.last.prerelease? }
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 1, "Gem::Requirement.new(", ")" do
+ q.pp as_list
+ end
+ end
+
+ ##
+ # True if +version+ satisfies this Requirement.
+
+ def satisfied_by?(version)
+ raise ArgumentError, "Need a Gem::Version: #{version.inspect}" unless
+ Gem::Version === version
+ requirements.all? {|op, rv| OPS.fetch(op).call version, rv }
+ end
+
+ alias_method :===, :satisfied_by?
+ alias_method :=~, :satisfied_by?
+
+ ##
+ # True if the requirement will not always match the latest version.
+
+ def specific?
+ return true if @requirements.length > 1 # GIGO, > 1, > 2 is silly
+
+ !%w[> >=].include? @requirements.first.first # grab the operator
+ end
+
+ def to_s # :nodoc:
+ as_list.join ", "
+ end
+
+ def ==(other) # :nodoc:
+ return unless Gem::Requirement === other
+
+ # An == check is always necessary
+ return false unless _sorted_requirements == other._sorted_requirements
+
+ # An == check is sufficient unless any requirements use ~>
+ return true unless _tilde_requirements.any?
+
+ # If any requirements use ~> we use the stricter `#eql?` that also checks
+ # that version precision is the same
+ _tilde_requirements.eql?(other._tilde_requirements)
+ end
+
+ protected
+
+ def _sorted_requirements
+ @_sorted_requirements ||= requirements.sort_by(&:to_s)
+ end
+
+ 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
+ # This is needed for compatibility with older yaml
+ # gemspecs.
+
+ Requirement = Gem::Requirement # :nodoc:
+end
diff --git a/lib/rubygems/resolver.rb b/lib/rubygems/resolver.rb
new file mode 100644
index 0000000000..788206c056
--- /dev/null
+++ b/lib/rubygems/resolver.rb
@@ -0,0 +1,565 @@
+# frozen_string_literal: true
+
+require_relative "dependency"
+require_relative "exceptions"
+
+##
+# Given a set of Gem::Dependency objects as +needed+ and a way to query the
+# set of available specs via +set+, calculates a set of ActivationRequest
+# objects which indicate all the specs that should be activated to meet the
+# all the requirements.
+
+class Gem::Resolver
+ require_relative "vendored_pub_grub"
+
+ ##
+ # If the DEBUG_RESOLVER environment variable is set then debugging mode is
+ # enabled for the resolver. This will display information about the state
+ # of the resolver while a set of dependencies is being resolved.
+
+ DEBUG_RESOLVER = !ENV["DEBUG_RESOLVER"].nil?
+
+ ##
+ # Set to true if all development dependencies should be considered.
+
+ attr_accessor :development
+
+ ##
+ # Set to true if immediate development dependencies should be considered.
+
+ attr_accessor :development_shallow
+
+ ##
+ # When true, no dependencies are looked up for requested gems.
+
+ attr_accessor :ignore_dependencies
+
+ ##
+ # Hash of gems to skip resolution. Keyed by gem name, with arrays of
+ # gem specifications as values.
+
+ attr_accessor :skip_gems
+
+ ##
+ #
+
+ attr_accessor :soft_missing
+
+ ##
+ # Combines +sets+ into a ComposedSet that allows specification lookup in a
+ # uniform manner. If one of the +sets+ is itself a ComposedSet its sets are
+ # flattened into the result ComposedSet.
+
+ def self.compose_sets(*sets)
+ sets.compact!
+
+ sets = sets.flat_map do |set|
+ case set
+ when Gem::Resolver::BestSet then
+ set
+ when Gem::Resolver::ComposedSet then
+ set.sets
+ else
+ set
+ end
+ end
+
+ case sets.length
+ when 0 then
+ raise ArgumentError, "one set in the composition must be non-nil"
+ when 1 then
+ sets.first
+ else
+ Gem::Resolver::ComposedSet.new(*sets)
+ end
+ end
+
+ ##
+ # Creates a Resolver that queries only against the already installed gems
+ # for the +needed+ dependencies.
+
+ def self.for_current_gems(needed)
+ new needed, Gem::Resolver::CurrentSet.new
+ end
+
+ ##
+ # Create Resolver object which will resolve the tree starting
+ # with +needed+ Dependency objects.
+ #
+ # +set+ is an object that provides where to look for specifications to
+ # satisfy the Dependencies. This defaults to IndexSet, which will query
+ # rubygems.org.
+
+ def initialize(needed, set = nil)
+ @set = set || Gem::Resolver::IndexSet.new
+ @needed = needed
+
+ @development = false
+ @development_shallow = false
+ @ignore_dependencies = false
+ @skip_gems = {}
+ @soft_missing = false
+
+ @root_package = RootPackage.new
+ @root_version = Gem::PubGrub::Package.root_version
+
+ @packages = {}
+
+ @unfiltered_specs = Hash.new {|h, name| h[name] = find_unfiltered_specs_for(name) }
+ @all_specs = Hash.new {|h, name| h[name] = filter_specs(@unfiltered_specs[name]) }
+ @all_versions = Hash.new {|h, pkg| h[pkg] = @all_specs[pkg.to_s].map(&:version).uniq.sort }
+ @sorted_versions = Hash.new do |h, pkg|
+ h[pkg] = Gem::PubGrub::Package.root?(pkg) ? [@root_version] : @all_versions[pkg]
+ end
+ @cached_dependencies = Hash.new do |h, pkg|
+ h[pkg] = if Gem::PubGrub::Package.root?(pkg)
+ { @root_version => root_dependencies }
+ else
+ Hash.new {|v, ver| v[ver] = compute_dependencies(pkg, ver) }
+ end
+ end
+ @version_to_index = Hash.new {|h, pkg| h[pkg] = @sorted_versions[pkg].each_with_index.to_h }
+ @versions_for_cache = Hash.new {|h, pkg| h[pkg] = {} }
+ @spec_for_cache = Hash.new {|h, name| h[name] = build_spec_for_cache(name) }
+ end
+
+ ##
+ # Proceed with resolution! Returns an array of ActivationRequest objects.
+
+ def resolve
+ # Pre-check: raise UnsatisfiableDependencyError for root deps with no
+ # platform match. We filter by platform ONLY here (not required_ruby_version
+ # / required_rubygems_version): a foreign-platform gem is genuinely "not
+ # found", but a gem that exists yet is incompatible with the running Ruby
+ # should flow through the solver to a DependencyResolutionError that names
+ # the Ruby requirement. That matches Bundler (which models Ruby as a
+ # synthetic dependency, so this surfaces as a solve failure) and gives a
+ # clearer message than the platform-oriented UnsatisfiableDependencyError.
+ @needed.each do |dep|
+ next if @soft_missing
+ dep_request = DependencyRequest.new(dep, nil)
+ all = @set.find_all(dep_request)
+ matching = select_local_platforms(all)
+
+ next unless matching.empty?
+
+ exc = Gem::UnsatisfiableDependencyError.new(dep_request, all)
+ exc.errors = @set.errors
+ raise exc
+ end
+
+ solver = Gem::PubGrub::VersionSolver.new(
+ source: self,
+ root: @root_package,
+ strategy: Gem::Resolver::Strategy.new(self),
+ logger: make_logger
+ )
+ result = solver.solve
+
+ # Convert to Array<ActivationRequest>
+ needed_by_name = @needed.group_by(&:name)
+ result.filter_map do |package, version|
+ next if Gem::PubGrub::Package.root?(package)
+ spec = spec_for(package.to_s, version)
+ dep = needed_by_name[package.to_s]&.first || Gem::Dependency.new(package.to_s)
+ dep_request = DependencyRequest.new(dep, nil)
+ ActivationRequest.new(spec, dep_request)
+ end
+ rescue Gem::PubGrub::SolveFailure => e
+ extended = extract_extended_explanation(e.incompatibility)
+ if extended
+ message = "#{e.explanation}\n\n#{extended}"
+ raise Gem::DependencyResolutionError, Struct.new(:explanation).new(message)
+ else
+ raise Gem::DependencyResolutionError, e
+ end
+ end
+
+ # PubGrub source interface methods
+
+ def all_versions_for(package)
+ versions = @sorted_versions[package].reverse # highest first
+ name = package.to_s
+
+ if (skip_dep_gems = skip_gems[name]) && !skip_dep_gems.empty?
+ # Conservative mode: float the already-installed (skip) versions to the
+ # front so the solver prefers them. This sets *preference* only (it feeds
+ # the strategy's version-index map); it does not restrict availability, so
+ # every version stays selectable via versions_for. When an installed
+ # version is made impossible by a downstream conflict, the solver
+ # backtracks to a newer version instead of failing. Molinillo instead
+ # hard-restricted the candidate set to skip versions and raised.
+ #
+ # This reaches the same outcome as Bundler (upgrade-over-raise) for the
+ # common single-blocked-gem case, though the mechanism differs: Bundler
+ # hard-pins locked gems and selectively unlocks + re-solves on conflict,
+ # whereas we float as a preference and let PubGrub backtrack in one solve.
+ # The float can therefore over-upgrade when several installed gems are
+ # jointly involved in a conflict; that outcome-level divergence is
+ # accepted (see test_conservative_upgrades_when_installed_blocked).
+ skip_versions = skip_dep_gems.map(&:version)
+ preferred, rest = versions.partition {|v| skip_versions.include?(v) }
+ preferred + rest
+ else
+ # Prefer already-installed versions to avoid unnecessary upgrades
+ installed_versions = @all_specs[name].
+ select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }.
+ map(&:version)
+ if installed_versions.any?
+ preferred, rest = versions.partition {|v| installed_versions.include?(v) }
+ preferred + rest
+ else
+ versions
+ end
+ end
+ end
+
+ def versions_for(package, range = Gem::PubGrub::VersionRange.any)
+ @versions_for_cache[package][range] ||= begin
+ candidates = range.select_versions(@sorted_versions[package])
+
+ if Gem::PubGrub::Package.root?(package) ||
+ (@set.respond_to?(:prerelease) && @set.prerelease) ||
+ range_admits_prerelease?(range)
+ candidates
+ elsif @all_versions[package].any? {|v| !v.prerelease? }
+ candidates.reject(&:prerelease?)
+ else
+ # Only prereleases exist for this gem; fall back to them so
+ # dependencies like `>= 1.0` can still be satisfied.
+ candidates
+ end
+ end
+ end
+
+ def no_versions_incompatibility_for(_package, unsatisfied_term)
+ cause = Gem::PubGrub::Incompatibility::NoVersions.new(unsatisfied_term)
+
+ name = unsatisfied_term.package.to_s
+ constraint = unsatisfied_term.constraint
+ extended_explanation = build_extended_explanation(name, constraint)
+
+ custom_explanation = if extended_explanation
+ "#{constraint} could not be found in any repository"
+ end
+
+ Gem::Resolver::Incompatibility.new(
+ [unsatisfied_term],
+ cause: cause,
+ custom_explanation: custom_explanation,
+ extended_explanation: extended_explanation
+ )
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].filter_map do |dep_package_name, dep_constraint|
+ dep_package = dep_constraint.package
+
+ low = high = @version_to_index[package][version]
+
+ # find version low such that all >= low share the same dep
+ while low > 0 &&
+ package_deps[sorted_versions[low - 1]][dep_package_name] == dep_constraint
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length &&
+ package_deps[sorted_versions[high]][dep_package_name] == dep_constraint
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = Gem::PubGrub::VersionRange.new(min: low, max: high, include_min: !low.nil?)
+ self_constraint = Gem::PubGrub::VersionConstraint.new(package, range: range)
+
+ # No specs anywhere means an unknown package. Check @unfiltered_specs, not
+ # the filtered set, so a dep filtered out by platform/Ruby/prerelease falls
+ # through to NoVersions for proper hints instead. The band-scoped
+ # self_constraint lets clean sibling versions still resolve via backtracking.
+ if @unfiltered_specs[dep_package_name].empty?
+ cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint)
+ self_term = Gem::PubGrub::Term.new(self_constraint, true)
+ # PubGrub's default InvalidDependency rendering drops the version
+ # requirement ("depends on unknown package bar"). Supply a custom
+ # explanation so the missing dependency's constraint is preserved
+ # ("depends on bar = 0.5 which could not be found in any repository"),
+ # matching Molinillo's diagnostics.
+ return [Gem::PubGrub::Incompatibility.new(
+ [self_term],
+ cause: cause,
+ custom_explanation: "#{self_term.to_s(allow_every: true)} depends on #{dep_constraint} which could not be found in any repository"
+ )]
+ end
+
+ # An empty range means the requirement is self-contradictory (e.g. `> 2, < 1`).
+ if dep_constraint.range.empty?
+ return [Gem::Resolver::Incompatibility.new(
+ [Gem::PubGrub::Term.new(self_constraint, true)],
+ cause: Gem::PubGrub::Incompatibility::NoVersions.new(dep_constraint),
+ custom_explanation: "#{dep_package_name} cannot satisfy contradictory requirements #{dep_constraint.constraint_string}"
+ )]
+ end
+
+ Gem::PubGrub::Incompatibility.new(
+ [Gem::PubGrub::Term.new(self_constraint, true), Gem::PubGrub::Term.new(dep_constraint, false)],
+ cause: :dependency
+ )
+ end
+ end
+
+ ##
+ # Returns the gems in +specs+ that match the local platform.
+
+ def select_local_platforms(specs) # :nodoc:
+ specs.select do |spec|
+ Gem::Platform.installable? spec
+ end
+ end
+
+ private
+
+ def package_for(name)
+ @packages[name] ||= Gem::PubGrub::Package.new(name)
+ end
+
+ def root_dependencies
+ deps = {}
+ @needed.each do |dep|
+ constraint = Gem::PubGrub::RubyGems.requirement_to_constraint(package_for(dep.name), dep.requirement)
+ deps[dep.name] = deps.key?(dep.name) ? deps[dep.name].intersect(constraint) : constraint
+ end
+ deps
+ end
+
+ # Only the min bound is inspected: `~>` synthesises a max like `X.A`
+ # whose suffix looks prerelease to Gem::Version but is not the user's
+ # intent, so checking max would mis-admit prereleases for every `~>`.
+ def range_admits_prerelease?(range)
+ range.ranges.any? do |r|
+ next false if r.empty?
+ r.min&.prerelease?
+ end
+ end
+
+ def find_unfiltered_specs_for(name)
+ dep = Gem::Dependency.new(name, ">= 0.a")
+ dep_request = DependencyRequest.new(dep, nil)
+ @set.find_all(dep_request)
+ end
+
+ def filter_specs(specs)
+ filtered = select_local_platforms(specs)
+
+ unless @soft_missing
+ filtered = filtered.select do |s|
+ s.required_ruby_version.satisfied_by?(Gem.ruby_version) &&
+ s.required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ rescue StandardError
+ true
+ end
+ end
+
+ filtered
+ end
+
+ def spec_for(name, version)
+ @spec_for_cache[name][version]
+ end
+
+ def build_spec_for_cache(name)
+ # Rank sources by the order they were first supplied so that, when multiple
+ # sources offer the same version and platform, the earlier source wins.
+ source_rank = {}
+ @all_specs[name].each do |s|
+ source_rank[s.source] ||= source_rank.size
+ end
+
+ @all_specs[name].group_by(&:version).transform_values do |candidates|
+ next candidates.first if candidates.length == 1
+
+ # Prefer already-installed specs to avoid unnecessary downloads
+ installed = candidates.select {|s| s.is_a?(Gem::Resolver::InstalledSpecification) }
+ next installed.first if installed.length == 1
+ candidates = installed if installed.any?
+
+ # Among remaining candidates, prefer the most specific platform, then the
+ # earlier-supplied source.
+ candidates.min_by do |s|
+ [Gem::Platform.platform_specificity_match(s.platform, Gem::Platform.local),
+ source_rank[s.source]]
+ end
+ end
+ end
+
+ def compute_dependencies(package, version)
+ spec = spec_for(package.to_s, version)
+ return {} unless spec
+ return {} if @ignore_dependencies
+
+ spec.fetch_development_dependencies if @development && spec.respond_to?(:fetch_development_dependencies)
+
+ deps = {}
+ root_names = @needed.map(&:name)
+
+ spec.dependencies.each do |d|
+ next if d.name == package.to_s
+ next if d.type == :development && !@development
+ next if d.type == :development && @development_shallow && !root_names.include?(package.to_s)
+
+ dep_package = package_for(d.name)
+
+ # In force mode, skip deps that can't be satisfied - either no
+ # specs at all, or no specs matching the version requirement.
+ if @soft_missing
+ dep_specs = @all_specs[d.name]
+ matching = dep_specs.select {|s| d.requirement.satisfied_by?(s.version) }
+ next if matching.empty?
+ end
+
+ deps[d.name] = Gem::PubGrub::RubyGems.requirement_to_constraint(dep_package, d.requirement)
+ end
+
+ deps
+ end
+
+ def build_extended_explanation(name, constraint)
+ unfiltered = @unfiltered_specs[name]
+ return if unfiltered.empty?
+
+ filtered = @all_specs[name]
+ pkg = package_for(name)
+
+ # A prerelease hint applies when the source would strip prereleases for
+ # this constraint (global prerelease flag off and the constraint's range
+ # doesn't itself reach into prerelease territory) AND a prerelease of
+ # the gem exists somewhere.
+ prerelease_gated = !(@set.respond_to?(:prerelease) && @set.prerelease) &&
+ !range_admits_prerelease?(constraint.range)
+ has_prerelease_candidate = prerelease_gated &&
+ @all_versions[pkg].any?(&:prerelease?)
+
+ return if filtered.length == unfiltered.length && !has_prerelease_candidate
+
+ hints = []
+
+ # Check for specs that exist for other platforms
+ platform_specs = unfiltered.select do |s|
+ !Gem::Platform.installable?(s) && constraint.range.include?(s.version)
+ end
+ if platform_specs.any?
+ label = "#{name} (#{constraint.constraint_string})"
+ hints << "The source contains the following gems matching '#{label}':"
+ platform_specs.each do |s|
+ actual = s.respond_to?(:spec) ? s.spec : s
+ hints << " * #{actual.full_name}"
+ end
+ end
+
+ # Check for specs filtered by Ruby version
+ installable = select_local_platforms(unfiltered)
+ ruby_specs = installable.select do |s|
+ actual = s.respond_to?(:spec) ? s.spec : s
+ constraint.range.include?(s.version) &&
+ !actual.required_ruby_version.satisfied_by?(Gem.ruby_version)
+ rescue StandardError
+ false
+ end
+ if ruby_specs.any?
+ versions = ruby_specs.map(&:version).uniq.sort.reverse.first(3)
+ sample = ruby_specs.find {|s| s.version == versions.first }
+ actual = sample.respond_to?(:spec) ? sample.spec : sample
+ ruby_req = actual.required_ruby_version
+ hints << "#{name} #{versions.join(", ")} requires Ruby #{ruby_req} (you have #{Gem.ruby_version})"
+ end
+
+ # Check for specs filtered by prerelease status
+ if prerelease_gated
+ prerelease_versions = @all_versions[pkg].select(&:prerelease?)
+ if prerelease_versions.any?
+ versions = prerelease_versions.sort.reverse.first(3) # limit to avoid cluttering error output
+ hints << "#{name} #{versions.join(", ")} are pre-release versions. Use --prerelease to allow pre-release gems."
+ end
+ end
+
+ hints.empty? ? nil : hints.join("\n")
+ end
+
+ def extract_extended_explanation(incompatibility)
+ while incompatibility.cause.is_a?(Gem::PubGrub::Incompatibility::ConflictCause)
+ cause = incompatibility.cause
+
+ [cause.conflict, cause.other].each do |incompat|
+ if incompat.cause.is_a?(Gem::PubGrub::Incompatibility::NoVersions) &&
+ incompat.respond_to?(:extended_explanation) &&
+ incompat.extended_explanation
+ return incompat.extended_explanation
+ end
+ end
+
+ incompatibility = cause.conflict
+ end
+
+ nil
+ end
+
+ def make_logger
+ DEBUG_RESOLVER ? Gem::PubGrub::StderrLogger.new : Gem::PubGrub::NullLogger.new
+ end
+
+ # Custom root package so error messages say "your request depends on..."
+ # instead of PubGrub's default "root depends on...".
+ class RootPackage < Gem::PubGrub::Package
+ def initialize
+ super(:root)
+ end
+
+ def root?
+ true
+ end
+
+ def to_s
+ "your request"
+ end
+ end
+end
+
+require_relative "resolver/activation_request"
+require_relative "resolver/dependency_request"
+require_relative "resolver/incompatibility"
+require_relative "resolver/strategy"
+require_relative "resolver/requirement_list"
+require_relative "resolver/set"
+require_relative "resolver/api_set"
+require_relative "resolver/composed_set"
+require_relative "resolver/best_set"
+require_relative "resolver/current_set"
+require_relative "resolver/git_set"
+require_relative "resolver/index_set"
+require_relative "resolver/installer_set"
+require_relative "resolver/lock_set"
+require_relative "resolver/vendor_set"
+require_relative "resolver/source_set"
+
+require_relative "resolver/specification"
+require_relative "resolver/spec_specification"
+require_relative "resolver/api_specification"
+require_relative "resolver/git_specification"
+require_relative "resolver/index_specification"
+require_relative "resolver/installed_specification"
+require_relative "resolver/local_specification"
+require_relative "resolver/lock_specification"
+require_relative "resolver/vendor_specification"
diff --git a/lib/rubygems/resolver/activation_request.rb b/lib/rubygems/resolver/activation_request.rb
new file mode 100644
index 0000000000..5c722001b1
--- /dev/null
+++ b/lib/rubygems/resolver/activation_request.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+##
+# Specifies a Specification object that should be activated. Also contains a
+# dependency that was used to introduce this activation.
+
+class Gem::Resolver::ActivationRequest
+ ##
+ # The parent request for this activation request.
+
+ attr_reader :request
+
+ ##
+ # The specification to be activated.
+
+ attr_reader :spec
+
+ ##
+ # Creates a new ActivationRequest that will activate +spec+. The parent
+ # +request+ is used to provide diagnostics in case of conflicts.
+
+ def initialize(spec, request)
+ @spec = spec
+ @request = request
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Specification
+ @spec == other
+ when Gem::Resolver::ActivationRequest
+ @spec == other.spec
+ else
+ false
+ end
+ end
+
+ def eql?(other)
+ self == other
+ end
+
+ def hash
+ @spec.hash
+ end
+
+ ##
+ # Is this activation request for a development dependency?
+
+ def development?
+ @request.development?
+ end
+
+ ##
+ # Downloads a gem at +path+ and returns the file path.
+
+ def download(path)
+ Gem.ensure_gem_subdirectories path
+
+ if @spec.respond_to? :sources
+ exception = nil
+ path = @spec.sources.find do |source|
+ source.download full_spec, path
+ rescue exception
+ end
+ return path if path
+ raise exception if exception
+
+ elsif @spec.respond_to? :source
+ source = @spec.source
+ source.download full_spec, path
+
+ else
+ source = Gem.sources.first
+ source.download full_spec, path
+ end
+ end
+
+ ##
+ # The full name of the specification to be activated.
+
+ def full_name
+ name_tuple.full_name
+ end
+
+ alias_method :to_s, :full_name
+
+ ##
+ # The Gem::Specification for this activation request.
+
+ def full_spec
+ Gem::Specification === @spec ? @spec : @spec.spec
+ end
+
+ def inspect # :nodoc:
+ format("#<%s for %p from %s>", self.class, @spec, @request)
+ end
+
+ ##
+ # True if the requested gem has already been installed.
+
+ def installed?
+ case @spec
+ when Gem::Resolver::VendorSpecification then
+ true
+ else
+ this_spec = full_spec
+
+ Gem::Specification.any? do |s|
+ s == this_spec && s.base_dir == this_spec.base_dir
+ end
+ end
+ end
+
+ ##
+ # The name of this activation request's specification
+
+ def name
+ @spec.name
+ end
+
+ ##
+ # Return the ActivationRequest that contained the dependency
+ # that we were activated for.
+
+ def parent
+ @request.requester
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Activation request", "]" do
+ q.breakable
+ q.pp @spec
+
+ q.breakable
+ q.text " for "
+ q.pp @request
+ end
+ end
+
+ ##
+ # The version of this activation request's specification
+
+ def version
+ @spec.version
+ end
+
+ ##
+ # The platform of this activation request's specification
+
+ def platform
+ @spec.platform
+ end
+
+ private
+
+ def name_tuple
+ @name_tuple ||= Gem::NameTuple.new(name, version, platform)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set.rb b/lib/rubygems/resolver/api_set.rb
new file mode 100644
index 0000000000..3f443519d8
--- /dev/null
+++ b/lib/rubygems/resolver/api_set.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+##
+# The global rubygems pool, available via the Compact Index API.
+# Returns instances of APISpecification.
+
+class Gem::Resolver::APISet < Gem::Resolver::Set
+ autoload :GemParser, File.expand_path("api_set/gem_parser", __dir__)
+
+ ##
+ # The URI for the Compact Index API this APISet uses.
+
+ attr_reader :dep_uri # :nodoc:
+
+ ##
+ # The Gem::Source that gems are fetched from
+
+ attr_reader :source
+
+ ##
+ # The corresponding place to fetch gems.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new APISet that will retrieve gems from +uri+ using the Compact
+ # Index API URL +dep_uri+ which is described at
+ # https://guides.rubygems.org/rubygems-org-compact-index-api
+
+ def initialize(dep_uri = "https://index.rubygems.org/info/")
+ super()
+
+ dep_uri = Gem::URI dep_uri unless Gem::URI === dep_uri
+
+ @dep_uri = dep_uri
+ @uri = dep_uri + ".."
+
+ @data = Hash.new {|h,k| h[k] = [] }
+ @source = Gem::Source.new @uri
+
+ @to_fetch = []
+ end
+
+ ##
+ # Return an array of APISpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ if @to_fetch.include?(req.name)
+ prefetch_now
+ end
+
+ versions(req.name).each do |ver|
+ if req.dependency.match? req.name, ver[:number], @prerelease
+ res << Gem::Resolver::APISpecification.new(self, ver)
+ end
+ end
+
+ res
+ end
+
+ ##
+ # A hint run by the resolver to allow the Set to fetch
+ # data for DependencyRequests +reqs+.
+
+ def prefetch(reqs)
+ return unless @remote
+ names = reqs.map {|r| r.dependency.name }
+ needed = names - @data.keys - @to_fetch
+
+ @to_fetch += needed
+ end
+
+ def prefetch_now # :nodoc:
+ needed = @to_fetch
+ @to_fetch = []
+
+ needed.sort.each do |name|
+ versions(name)
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISet", "]" do
+ q.breakable
+ q.text "URI: #{@dep_uri}"
+
+ q.breakable
+ q.text "gem names:"
+ q.pp @data.keys
+ end
+ end
+
+ ##
+ # Return data for all versions of the gem +name+.
+
+ def versions(name) # :nodoc:
+ if @data.key?(name)
+ return @data[name]
+ end
+
+ uri = @dep_uri + name
+
+ begin
+ str = Gem::RemoteFetcher.fetcher.fetch_path uri
+ rescue Gem::RemoteFetcher::FetchError
+ @data[name] = []
+ else
+ lines(str).each do |ver|
+ number, platform, dependencies, requirements = parse_gem(ver)
+
+ platform ||= "ruby"
+ dependencies = dependencies.map {|dep_name, reqs| [dep_name, reqs.join(", ")] }
+ requirements = requirements.map {|req_name, reqs| [req_name.to_sym, reqs] }.to_h
+
+ @data[name] << { name: name, number: number, platform: platform, dependencies: dependencies, requirements: requirements }
+ end
+ end
+
+ @data[name]
+ end
+
+ private
+
+ def lines(str)
+ lines = str.split("\n")
+ header = lines.index("---")
+ header ? lines[header + 1..-1] : lines
+ end
+
+ def parse_gem(string)
+ @gem_parser ||= GemParser.new
+ @gem_parser.parse(string)
+ end
+end
diff --git a/lib/rubygems/resolver/api_set/gem_parser.rb b/lib/rubygems/resolver/api_set/gem_parser.rb
new file mode 100644
index 0000000000..4d827f4980
--- /dev/null
+++ b/lib/rubygems/resolver/api_set/gem_parser.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Gem::Resolver::APISet::GemParser
+ def parse(line)
+ version_and_platform, rest = line.split(" ", 2)
+ version, platform = version_and_platform.split("-", 2)
+ dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest
+ dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : []
+ requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : []
+ [version, platform, dependencies, requirements]
+ end
+
+ private
+
+ def parse_dependency(string)
+ dependency = string.split(":", 2)
+ dependency[-1] = dependency[-1].split("&") if dependency.size > 1
+ dependency[0] = -dependency[0]
+ dependency
+ end
+end
diff --git a/lib/rubygems/resolver/api_specification.rb b/lib/rubygems/resolver/api_specification.rb
new file mode 100644
index 0000000000..ccfd6fe084
--- /dev/null
+++ b/lib/rubygems/resolver/api_specification.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+##
+# Represents a specification retrieved via the Compact Index API.
+#
+# This is used to avoid loading the full Specification object when all we need
+# is the name, version, and dependencies.
+
+class Gem::Resolver::APISpecification < Gem::Resolver::Specification
+ ##
+ # We assume that all instances of this class are immutable;
+ # so avoid duplicated generation for performance.
+ @@cache = {}
+ def self.new(set, api_data)
+ cache_key = [set, api_data]
+ cache = @@cache[cache_key]
+ return cache if cache
+ @@cache[cache_key] = super
+ end
+
+ ##
+ # Creates an APISpecification for the given +set+ from the Compact Index API
+ # +api_data+.
+ #
+ # See https://guides.rubygems.org/rubygems-org-compact-index-api for the
+ # format of the +api_data+.
+
+ def initialize(set, api_data)
+ super()
+
+ @set = set
+ @name = api_data[:name]
+ @version = Gem::Version.new(api_data[:number]).freeze
+ @platform = Gem::Platform.new(api_data[:platform]).freeze
+ @original_platform = api_data[:platform].freeze
+ @dependencies = api_data[:dependencies].map do |name, ver|
+ Gem::Dependency.new(name, ver.split(/\s*,\s*/)).freeze
+ end.freeze
+ @required_ruby_version = Gem::Requirement.new(api_data.dig(:requirements, :ruby)).freeze
+ @required_rubygems_version = Gem::Requirement.new(api_data.dig(:requirements, :rubygems)).freeze
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @set.hash ^ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def fetch_development_dependencies # :nodoc:
+ spec = source.fetch_spec Gem::NameTuple.new @name, @version, @platform
+
+ @dependencies = spec.dependencies
+ end
+
+ def installable_platform? # :nodoc:
+ Gem::Platform.match_gem? @platform, @name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[APISpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+
+ q.breakable
+ q.text "set uri: #{@set.dep_uri}"
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this APISpecification.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @platform
+ source.fetch_spec tuple
+ rescue Gem::RemoteFetcher::FetchError
+ raise if @original_platform == @platform
+
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+ source.fetch_spec tuple
+ end
+ end
+
+ def source # :nodoc:
+ @set.source
+ end
+end
diff --git a/lib/rubygems/resolver/best_set.rb b/lib/rubygems/resolver/best_set.rb
new file mode 100644
index 0000000000..e647a2c11b
--- /dev/null
+++ b/lib/rubygems/resolver/best_set.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+##
+# The BestSet chooses the best available method to query a remote index.
+#
+# It combines IndexSet and APISet
+
+class Gem::Resolver::BestSet < Gem::Resolver::ComposedSet
+ ##
+ # Creates a BestSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize(sources = Gem.sources)
+ super()
+
+ @sources = sources
+ end
+
+ ##
+ # Picks which sets to use for the configured sources.
+
+ def pick_sets # :nodoc:
+ @sources.each_source do |source|
+ @sets << source.dependency_resolver_set(@prerelease)
+ end
+ end
+
+ def find_all(req) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def prefetch(reqs) # :nodoc:
+ pick_sets if @remote && @sets.empty?
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[BestSet", "]" do
+ q.breakable
+ q.text "sets:"
+
+ q.breakable
+ q.pp @sets
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/composed_set.rb b/lib/rubygems/resolver/composed_set.rb
new file mode 100644
index 0000000000..e67dd41754
--- /dev/null
+++ b/lib/rubygems/resolver/composed_set.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+##
+# A ComposedSet allows multiple sets to be queried like a single set.
+#
+# To create a composed set with any number of sets use:
+#
+# Gem::Resolver.compose_sets set1, set2
+#
+# This method will eliminate nesting of composed sets.
+
+class Gem::Resolver::ComposedSet < Gem::Resolver::Set
+ attr_reader :sets # :nodoc:
+
+ ##
+ # Creates a new ComposedSet containing +sets+. Use
+ # Gem::Resolver::compose_sets instead.
+
+ def initialize(*sets)
+ super()
+
+ @sets = sets
+ end
+
+ ##
+ # When +allow_prerelease+ is set to +true+ prereleases gems are allowed to
+ # match dependencies.
+
+ def prerelease=(allow_prerelease)
+ super
+
+ sets.each do |set|
+ set.prerelease = allow_prerelease
+ end
+ end
+
+ ##
+ # Sets the remote network access for all composed sets.
+
+ def remote=(remote)
+ super
+
+ @sets.each {|set| set.remote = remote }
+ end
+
+ def errors
+ @errors + @sets.flat_map(&:errors)
+ end
+
+ ##
+ # Finds all specs matching +req+ in all sets.
+
+ def find_all(req)
+ @sets.flat_map do |s|
+ s.find_all req
+ end
+ end
+
+ ##
+ # Prefetches +reqs+ in all sets.
+
+ def prefetch(reqs)
+ @sets.each {|s| s.prefetch(reqs) }
+ end
+end
diff --git a/lib/rubygems/resolver/current_set.rb b/lib/rubygems/resolver/current_set.rb
new file mode 100644
index 0000000000..370e445089
--- /dev/null
+++ b/lib/rubygems/resolver/current_set.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+##
+# A set which represents the installed gems. Respects
+# all the normal settings that control where to look
+# for installed gems.
+
+class Gem::Resolver::CurrentSet < Gem::Resolver::Set
+ def find_all(req)
+ req.dependency.matching_specs
+ end
+end
diff --git a/lib/rubygems/resolver/dependency_request.rb b/lib/rubygems/resolver/dependency_request.rb
new file mode 100644
index 0000000000..60b338277f
--- /dev/null
+++ b/lib/rubygems/resolver/dependency_request.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+##
+# Used Internally. Wraps a Dependency object to also track which spec
+# contained the Dependency.
+
+class Gem::Resolver::DependencyRequest
+ ##
+ # The wrapped Gem::Dependency
+
+ attr_reader :dependency
+
+ ##
+ # The request for this dependency.
+
+ attr_reader :requester
+
+ ##
+ # Creates a new DependencyRequest for +dependency+ from +requester+.
+ # +requester may be nil if the request came from a user.
+
+ def initialize(dependency, requester)
+ @dependency = dependency
+ @requester = requester
+ end
+
+ def ==(other) # :nodoc:
+ case other
+ when Gem::Dependency
+ @dependency == other
+ when Gem::Resolver::DependencyRequest
+ @dependency == other.dependency
+ else
+ false
+ end
+ end
+
+ ##
+ # Is this dependency a development dependency?
+
+ def development?
+ @dependency.type == :development
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #match? only matches prerelease versions when #dependency is a
+ # prerelease dependency.
+
+ def match?(spec, allow_prerelease = false)
+ @dependency.match? spec, nil, allow_prerelease
+ end
+
+ ##
+ # Does this dependency request match +spec+?
+ #
+ # NOTE: #matches_spec? matches prerelease versions. See also #match?
+
+ def matches_spec?(spec)
+ @dependency.matches_spec? spec
+ end
+
+ ##
+ # The name of the gem this dependency request is requesting.
+
+ def name
+ @dependency.name
+ end
+
+ def type
+ @dependency.type
+ end
+
+ ##
+ # Indicate that the request is for a gem explicitly requested by the user
+
+ def explicit?
+ @requester.nil?
+ end
+
+ ##
+ # Indicate that the request is for a gem requested as a dependency of
+ # another gem
+
+ def implicit?
+ !explicit?
+ end
+
+ ##
+ # Return a String indicating who caused this request to be added (only
+ # valid for implicit requests)
+
+ def request_context
+ @requester ? @requester.request : "(unknown)"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Dependency request ", "]" do
+ q.breakable
+ q.text @dependency.to_s
+
+ q.breakable
+ q.text " requested by "
+ q.pp @requester
+ end
+ end
+
+ ##
+ # The version requirement for this dependency request
+
+ def requirement
+ @dependency.requirement
+ end
+
+ def to_s # :nodoc:
+ @dependency.to_s
+ end
+end
diff --git a/lib/rubygems/resolver/git_set.rb b/lib/rubygems/resolver/git_set.rb
new file mode 100644
index 0000000000..2912378fe7
--- /dev/null
+++ b/lib/rubygems/resolver/git_set.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+##
+# A GitSet represents gems that are sourced from git repositories.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::GitSet.new
+# set.add_git_gem 'rake', 'git://example/rake.git', tag: 'rake-10.1.0'
+
+class Gem::Resolver::GitSet < Gem::Resolver::Set
+ ##
+ # The root directory for git gems in this set. This is usually Gem.dir, the
+ # installation directory for regular gems.
+
+ attr_accessor :root_dir
+
+ ##
+ # Contains repositories needing submodules
+
+ attr_reader :need_submodules # :nodoc:
+
+ ##
+ # A Hash containing git gem names for keys and a Hash of repository and
+ # git commit reference as values.
+
+ attr_reader :repositories # :nodoc:
+
+ ##
+ # A hash of gem names to Gem::Resolver::GitSpecifications
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @need_submodules = {}
+ @repositories = {}
+ @root_dir = Gem.dir
+ @specs = {}
+ end
+
+ def add_git_gem(name, repository, reference, submodules) # :nodoc:
+ @repositories[name] = [repository, reference]
+ @need_submodules[repository] = submodules
+ end
+
+ ##
+ # Adds and returns a GitSpecification with the given +name+ and +version+
+ # which came from a +repository+ at the given +reference+. If +submodules+
+ # is true they are checked out along with the repository.
+ #
+ # This fills in the prefetch information as enough information about the gem
+ # is present in the arguments.
+
+ def add_git_spec(name, version, repository, reference, submodules) # :nodoc:
+ add_git_gem name, repository, reference, submodules
+
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+
+ spec = Gem::Specification.new do |s|
+ s.name = name
+ s.version = version
+ end
+
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+
+ git_spec
+ end
+
+ ##
+ # Finds all git gems matching +req+
+
+ def find_all(req)
+ prefetch nil
+
+ specs.values.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Prefetches specifications from the git repositories in this set.
+
+ def prefetch(reqs)
+ return unless @specs.empty?
+
+ @repositories.each do |name, (repository, reference)|
+ source = Gem::Source::Git.new name, repository, reference
+ source.root_dir = @root_dir
+ source.remote = @remote
+
+ source.specs.each do |spec|
+ git_spec = Gem::Resolver::GitSpecification.new self, spec, source
+
+ @specs[spec.name] = git_spec
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSet", "]" do
+ next if @repositories.empty?
+ q.breakable
+
+ repos = @repositories.map do |name, (repository, reference)|
+ "#{name}: #{repository}@#{reference}"
+ end
+
+ q.seplist repos do |repo|
+ q.text repo
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/git_specification.rb b/lib/rubygems/resolver/git_specification.rb
new file mode 100644
index 0000000000..e587c17d2a
--- /dev/null
+++ b/lib/rubygems/resolver/git_specification.rb
@@ -0,0 +1,57 @@
+# 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:+
+# option.
+
+class Gem::Resolver::GitSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ def add_dependency(dependency) # :nodoc:
+ spec.dependencies << dependency
+ end
+
+ ##
+ # Installing a git gem only involves building the extensions and generating
+ # the executables.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ installer = Gem::Installer.for_spec spec, options
+
+ yield installer if block_given?
+
+ installer.run_pre_install_hooks
+ installer.build_extensions
+ installer.run_post_build_hooks
+ installer.generate_bin
+ installer.run_post_install_hooks
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[GitSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source:"
+ q.breakable
+ q.pp @source
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/incompatibility.rb b/lib/rubygems/resolver/incompatibility.rb
new file mode 100644
index 0000000000..57a60affb4
--- /dev/null
+++ b/lib/rubygems/resolver/incompatibility.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class Gem::Resolver::Incompatibility < Gem::PubGrub::Incompatibility
+ attr_reader :extended_explanation
+
+ def initialize(terms, cause:, custom_explanation: nil, extended_explanation: nil)
+ @extended_explanation = extended_explanation
+ super(terms, cause: cause, custom_explanation: custom_explanation)
+ end
+end
diff --git a/lib/rubygems/resolver/index_set.rb b/lib/rubygems/resolver/index_set.rb
new file mode 100644
index 0000000000..cddaf8773f
--- /dev/null
+++ b/lib/rubygems/resolver/index_set.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+##
+# The global rubygems pool represented via the traditional
+# source index.
+
+class Gem::Resolver::IndexSet < Gem::Resolver::Set
+ def initialize(source = nil) # :nodoc:
+ super()
+
+ @f =
+ if source
+ sources = Gem::SourceList.from [source]
+
+ Gem::SpecFetcher.new sources
+ else
+ Gem::SpecFetcher.fetcher
+ end
+
+ @all = Hash.new {|h,k| h[k] = [] }
+
+ list, errors = @f.available_specs :complete
+
+ @errors.concat errors
+
+ list.each do |uri, specs|
+ specs.each do |n|
+ @all[n.name] << [uri, n]
+ end
+ end
+
+ @specs = {}
+ end
+
+ ##
+ # Return an array of IndexSpecification objects matching
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ res = []
+
+ return res unless @remote
+
+ name = req.dependency.name
+
+ @all[name].each do |uri, n|
+ next unless req.match? n, @prerelease
+ res << Gem::Resolver::IndexSpecification.new(
+ self, n.name, n.version, uri, n.platform
+ )
+ end
+
+ res
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[IndexSet", "]" do
+ q.breakable
+ q.text "sources:"
+ q.breakable
+ q.pp @f.sources
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+
+ names = @all.values.flat_map do |tuples|
+ tuples.map do |_, tuple|
+ tuple.full_name
+ end
+ end
+
+ q.seplist names do |name|
+ q.text name
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/index_specification.rb b/lib/rubygems/resolver/index_specification.rb
new file mode 100644
index 0000000000..7b95608071
--- /dev/null
+++ b/lib/rubygems/resolver/index_specification.rb
@@ -0,0 +1,101 @@
+# 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+
+# and +version+ are needed.
+
+class Gem::Resolver::IndexSpecification < Gem::Resolver::Specification
+ ##
+ # An IndexSpecification is created from the index format described in `gem
+ # help generate_index`.
+ #
+ # The +set+ contains other specifications for this (URL) +source+.
+ #
+ # The +name+, +version+ and +platform+ are the name, version and platform of
+ # the gem.
+
+ def initialize(set, name, version, source, platform)
+ super()
+
+ @set = set
+ @name = name
+ @version = version
+ @source = source
+ @platform = Gem::Platform.new(platform.to_s)
+ @original_platform = platform.to_s
+
+ @spec = nil
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+ #
+ # A fallback is included because when generated, some marshalled specs have it
+ # set to +nil+.
+
+ def required_ruby_version
+ spec.required_ruby_version || Gem::Requirement.default
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+ #
+ # A fallback is included because the original version of the specification
+ # API didn't include that field, so some marshalled specs in the index have it
+ # set to +nil+.
+
+ def required_rubygems_version
+ spec.required_rubygems_version || Gem::Requirement.default
+ end
+
+ def ==(other)
+ self.class === other &&
+ @name == other.name &&
+ @version == other.version &&
+ @platform == other.platform
+ end
+
+ def hash
+ @name.hash ^ @version.hash ^ @platform.hash
+ end
+
+ def inspect # :nodoc:
+ format("#<%s %s source %s>", self.class, full_name, @source)
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[Index specification", "]" do
+ q.breakable
+ q.text full_name
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text @platform.to_s
+ end
+
+ q.breakable
+ q.text "source "
+ q.pp @source
+ end
+ end
+
+ ##
+ # Fetches a Gem::Specification for this IndexSpecification from the #source.
+
+ def spec # :nodoc:
+ @spec ||=
+ begin
+ tuple = Gem::NameTuple.new @name, @version, @original_platform
+
+ @source.fetch_spec tuple
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/installed_specification.rb b/lib/rubygems/resolver/installed_specification.rb
new file mode 100644
index 0000000000..8280ae4672
--- /dev/null
+++ b/lib/rubygems/resolver/installed_specification.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+##
+# An InstalledSpecification represents a gem that is already installed
+# locally.
+
+class Gem::Resolver::InstalledSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec
+ end
+
+ ##
+ # This is a null install as this specification is already installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+
+ ##
+ # Returns +true+ if this gem is installable for the current platform.
+
+ 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.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstalledSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp spec.dependencies
+ end
+ end
+
+ ##
+ # The source for this specification
+
+ def source
+ @source ||= Gem::Source::Installed.new
+ end
+end
diff --git a/lib/rubygems/resolver/installer_set.rb b/lib/rubygems/resolver/installer_set.rb
new file mode 100644
index 0000000000..42ce0890e2
--- /dev/null
+++ b/lib/rubygems/resolver/installer_set.rb
@@ -0,0 +1,271 @@
+# frozen_string_literal: true
+
+##
+# A set of gems for installation sourced from remote sources and local .gem
+# files
+
+class Gem::Resolver::InstallerSet < Gem::Resolver::Set
+ ##
+ # List of Gem::Specification objects that must always be installed.
+
+ attr_reader :always_install # :nodoc:
+
+ ##
+ # Only install gems in the always_install list
+
+ attr_accessor :ignore_dependencies # :nodoc:
+
+ ##
+ # Do not look in the installed set when finding specifications. This is
+ # used by the --install-dir option to `gem install`
+
+ attr_accessor :ignore_installed # :nodoc:
+
+ ##
+ # The remote_set looks up remote gems for installation.
+
+ attr_reader :remote_set # :nodoc:
+
+ ##
+ # Ignore ruby & rubygems specification constraints.
+ #
+
+ attr_accessor :force # :nodoc:
+
+ ##
+ # Creates a new InstallerSet that will look for gems in +domain+.
+
+ def initialize(domain)
+ super()
+
+ @domain = domain
+
+ @f = Gem::SpecFetcher.fetcher
+
+ @always_install = []
+ @ignore_dependencies = false
+ @ignore_installed = false
+ @local = {}
+ @local_source = Gem::Source::Local.new
+ @remote_set = Gem::Resolver::BestSet.new
+ @force = false
+ @specs = {}
+ end
+
+ ##
+ # Looks up the latest specification for +dependency+ and adds it to the
+ # always_install list.
+
+ def add_always_install(dependency)
+ request = Gem::Resolver::DependencyRequest.new dependency, nil
+
+ found = find_all request
+
+ found.delete_if do |s|
+ s.version.prerelease? && !s.local?
+ end unless dependency.prerelease?
+
+ found = found.select do |s|
+ Gem::Source::SpecificFile === s.source ||
+ Gem::Platform.match_spec?(s)
+ end
+
+ found = found.sort_by do |s|
+ [s.version, Gem::Platform.sort_priority(s.platform)]
+ end
+
+ newest = found.last
+
+ unless newest
+ exc = Gem::UnsatisfiableDependencyError.new request
+ exc.errors = errors
+
+ raise exc
+ end
+
+ unless @force
+ found_matching_metadata = found.reverse.find do |spec|
+ metadata_satisfied?(spec)
+ end
+
+ if found_matching_metadata.nil?
+ ensure_required_ruby_version_met(newest.spec)
+ ensure_required_rubygems_version_met(newest.spec)
+ else
+ newest = found_matching_metadata
+ end
+ end
+
+ @always_install << newest.spec
+ end
+
+ ##
+ # Adds a local gem requested using +dep_name+ with the given +spec+ that can
+ # be loaded and installed using the +source+.
+
+ def add_local(dep_name, spec, source)
+ @local[dep_name] = [spec, source]
+ end
+
+ ##
+ # Should local gems should be considered?
+
+ def consider_local? # :nodoc:
+ @domain == :both || @domain == :local
+ end
+
+ ##
+ # Should remote gems should be considered?
+
+ def consider_remote? # :nodoc:
+ @domain == :both || @domain == :remote
+ end
+
+ ##
+ # Errors encountered while resolving gems
+
+ def errors
+ @errors + @remote_set.errors
+ end
+
+ ##
+ # Returns an array of IndexSpecification objects matching DependencyRequest
+ # +req+.
+
+ def find_all(req)
+ res = []
+
+ dep = req.dependency
+
+ return res if @ignore_dependencies &&
+ @always_install.none? {|spec| dep.match? spec }
+
+ name = dep.name
+
+ dep.matching_specs.each do |gemspec|
+ next if @always_install.any? {|spec| spec.name == gemspec.name }
+
+ 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
+ end.map do |spec, source|
+ Gem::Resolver::LocalSpecification.new self, spec, source
+ end
+
+ res.concat matching_local
+
+ begin
+ @local_source.find_all_gems(name, dep.requirement).each do |local_spec|
+ res << Gem::Resolver::IndexSpecification.new(
+ self, local_spec.name, local_spec.version,
+ @local_source, local_spec.platform
+ )
+ end
+ rescue Gem::Package::FormatError
+ # ignore
+ end
+ end
+
+ res.concat @remote_set.find_all req if consider_remote? && matching_local.empty?
+
+ res
+ end
+
+ def prefetch(reqs)
+ @remote_set.prefetch(reqs) if consider_remote?
+ end
+
+ def prerelease=(allow_prerelease)
+ super
+
+ @remote_set.prerelease = allow_prerelease
+ end
+
+ def inspect # :nodoc:
+ always_install = @always_install.map(&:full_name)
+
+ format("#<%s domain: %s specs: %p always install: %p>", self.class, @domain, @specs.keys, always_install)
+ end
+
+ ##
+ # Called from IndexSpecification to get a true Specification
+ # object.
+
+ def load_spec(name, ver, platform, source) # :nodoc:
+ key = "#{name}-#{ver}-#{platform}"
+
+ @specs.fetch key do
+ tuple = Gem::NameTuple.new name, ver, platform
+
+ @specs[key] = source.fetch_spec tuple
+ end
+ end
+
+ ##
+ # Has a local gem for +dep_name+ been added to this set?
+
+ def local?(dep_name) # :nodoc:
+ spec, _ = @local[dep_name]
+
+ spec
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[InstallerSet", "]" do
+ q.breakable
+ q.text "domain: #{@domain}"
+
+ q.breakable
+ q.text "specs: "
+ q.pp @specs.keys
+
+ q.breakable
+ q.text "always install: "
+ q.pp @always_install
+ end
+ end
+
+ def remote=(remote) # :nodoc:
+ case @domain
+ when :local then
+ @domain = :both if remote
+ when :remote then
+ @domain = nil unless remote
+ when :both then
+ @domain = :local unless remote
+ end
+ end
+
+ private
+
+ def metadata_satisfied?(spec)
+ spec.required_ruby_version.satisfied_by?(Gem.ruby_version) &&
+ spec.required_rubygems_version.satisfied_by?(Gem.rubygems_version)
+ end
+
+ def ensure_required_ruby_version_met(spec) # :nodoc:
+ if rrv = spec.required_ruby_version
+ ruby_version = Gem.ruby_version
+ unless rrv.satisfied_by? ruby_version
+ raise Gem::RuntimeRequirementNotMetError,
+ "#{spec.full_name} requires Ruby version #{rrv}. The current ruby version is #{ruby_version}."
+ end
+ end
+ end
+
+ def ensure_required_rubygems_version_met(spec) # :nodoc:
+ if rrgv = spec.required_rubygems_version
+ 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}. " \
+ "Try 'gem update --system' to update RubyGems itself."
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/local_specification.rb b/lib/rubygems/resolver/local_specification.rb
new file mode 100644
index 0000000000..b57d40e795
--- /dev/null
+++ b/lib/rubygems/resolver/local_specification.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+##
+# A LocalSpecification comes from a .gem file on the local filesystem.
+
+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.is_a? Gem::Source::SpecificFile
+
+ super
+ end
+
+ def local? # :nodoc:
+ true
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LocalSpecification", "]" do
+ q.breakable
+ q.text "name: #{name}"
+
+ q.breakable
+ q.text "version: #{version}"
+
+ q.breakable
+ q.text "platform: #{platform}"
+
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp dependencies
+
+ q.breakable
+ q.text "source: #{@source.path}"
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/lock_set.rb b/lib/rubygems/resolver/lock_set.rb
new file mode 100644
index 0000000000..e5ee32a9a6
--- /dev/null
+++ b/lib/rubygems/resolver/lock_set.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+##
+# A set of gems from a gem dependencies lockfile.
+
+class Gem::Resolver::LockSet < Gem::Resolver::Set
+ attr_reader :specs # :nodoc:
+
+ ##
+ # Creates a new LockSet from the given +sources+
+
+ def initialize(sources)
+ super()
+
+ @sources = sources.map do |source|
+ Gem::Source::Lock.new source
+ end
+
+ @specs = []
+ end
+
+ ##
+ # Creates a new IndexSpecification in this set using the given +name+,
+ # +version+ and +platform+.
+ #
+ # The specification's set will be the current set, and the source will be
+ # the current set's source.
+
+ def add(name, version, platform) # :nodoc:
+ version = Gem::Version.new version
+ specs = [
+ Gem::Resolver::LockSpecification.new(self, name, version, @sources, platform),
+ ]
+
+ @specs.concat specs
+
+ specs
+ end
+
+ ##
+ # Returns an Array of IndexSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.select do |spec|
+ req.match? spec
+ end
+ end
+
+ ##
+ # Loads a Gem::Specification with the given +name+, +version+ and
+ # +platform+. +source+ is ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ dep = Gem::Dependency.new name, version
+
+ found = @specs.find do |spec|
+ dep.matches_spec?(spec) && spec.platform == platform
+ end
+
+ tuple = Gem::NameTuple.new found.name, found.version, found.platform
+
+ found.source.fetch_spec tuple
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSet", "]" do
+ q.breakable
+ q.text "source:"
+
+ q.breakable
+ q.pp @source
+
+ q.breakable
+ q.text "specs:"
+
+ q.breakable
+ 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
new file mode 100644
index 0000000000..06f912dd85
--- /dev/null
+++ b/lib/rubygems/resolver/lock_specification.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# The LockSpecification comes from a lockfile (Gem::RequestSet::Lockfile).
+#
+# A LockSpecification's dependency information is pre-filled from the
+# lockfile.
+
+class Gem::Resolver::LockSpecification < Gem::Resolver::Specification
+ attr_reader :sources
+
+ def initialize(set, name, version, sources, platform)
+ super()
+
+ @name = name
+ @platform = platform
+ @set = set
+ @source = sources.first
+ @sources = sources
+ @version = version
+
+ @dependencies = []
+ @spec = nil
+ end
+
+ ##
+ # This is a null install as a locked specification is considered installed.
+ # +options+ are ignored.
+
+ def install(options = {})
+ destination = options[:install_dir] || Gem.dir
+
+ if File.exist? File.join(destination, "specifications", spec.spec_name)
+ yield nil
+ return
+ end
+
+ super
+ end
+
+ ##
+ # Adds +dependency+ from the lockfile to this specification
+
+ def add_dependency(dependency) # :nodoc:
+ @dependencies << dependency
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[LockSpecification", "]" do
+ q.breakable
+ q.text "name: #{@name}"
+
+ q.breakable
+ q.text "version: #{@version}"
+
+ unless @platform == Gem::Platform::RUBY
+ q.breakable
+ q.text "platform: #{@platform}"
+ end
+
+ unless @dependencies.empty?
+ q.breakable
+ q.text "dependencies:"
+ q.breakable
+ q.pp @dependencies
+ end
+ end
+ end
+
+ ##
+ # A specification constructed from the lockfile is returned
+
+ def spec
+ @spec ||= Gem::Specification.find do |spec|
+ spec.name == @name && spec.version == @version
+ end
+
+ @spec ||= Gem::Specification.new do |s|
+ s.name = @name
+ s.version = @version
+ s.platform = @platform
+
+ s.dependencies.concat @dependencies
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/requirement_list.rb b/lib/rubygems/resolver/requirement_list.rb
new file mode 100644
index 0000000000..6f86f0f412
--- /dev/null
+++ b/lib/rubygems/resolver/requirement_list.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+##
+# The RequirementList is used to hold the requirements being considered
+# while resolving a set of gems.
+#
+# The RequirementList acts like a queue where the oldest items are removed
+# first.
+
+class Gem::Resolver::RequirementList
+ include Enumerable
+
+ ##
+ # Creates a new RequirementList.
+
+ def initialize
+ @exact = []
+ @list = []
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @exact = @exact.dup
+ @list = @list.dup
+ end
+
+ ##
+ # Adds Resolver::DependencyRequest +req+ to this requirements list.
+
+ def add(req)
+ if req.requirement.exact?
+ @exact.push req
+ else
+ @list.push req
+ end
+ req
+ end
+
+ ##
+ # Enumerates requirements in the list
+
+ def each # :nodoc:
+ return enum_for __method__ unless block_given?
+
+ @exact.each do |requirement|
+ yield requirement
+ end
+
+ @list.each do |requirement|
+ yield requirement
+ end
+ end
+
+ ##
+ # How many elements are in the list
+
+ def size
+ @exact.size + @list.size
+ end
+
+ ##
+ # Is the list empty?
+
+ def empty?
+ @exact.empty? && @list.empty?
+ end
+
+ ##
+ # Remove the oldest DependencyRequest from the list.
+
+ def remove
+ return @exact.shift unless @exact.empty?
+ @list.shift
+ end
+
+ ##
+ # Returns the oldest five entries from the list.
+
+ def next5
+ x = @exact[0,5]
+ x + @list[0,5 - x.size]
+ end
+end
diff --git a/lib/rubygems/resolver/set.rb b/lib/rubygems/resolver/set.rb
new file mode 100644
index 0000000000..243fee5fd5
--- /dev/null
+++ b/lib/rubygems/resolver/set.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+##
+# Resolver sets are used to look up specifications (and their
+# dependencies) used in resolution. This set is abstract.
+
+class Gem::Resolver::Set
+ ##
+ # Set to true to disable network access for this set
+
+ attr_accessor :remote
+
+ ##
+ # Errors encountered when resolving gems
+
+ attr_accessor :errors
+
+ ##
+ # When true, allows matching of requests to prerelease gems.
+
+ attr_accessor :prerelease
+
+ def initialize # :nodoc:
+ @prerelease = false
+ @remote = true
+ @errors = []
+ end
+
+ ##
+ # The find_all method must be implemented. It returns all Resolver
+ # Specification objects matching the given DependencyRequest +req+.
+
+ def find_all(req)
+ raise NotImplementedError
+ end
+
+ ##
+ # The #prefetch method may be overridden, but this is not necessary. This
+ # default implementation does nothing, which is suitable for sets where
+ # looking up a specification is cheap (such as installed gems).
+ #
+ # When overridden, the #prefetch method should look up specifications
+ # matching +reqs+.
+
+ def prefetch(reqs)
+ end
+
+ ##
+ # When true, this set is allowed to access the network when looking up
+ # specifications or dependencies.
+
+ def remote? # :nodoc:
+ @remote
+ end
+end
diff --git a/lib/rubygems/resolver/source_set.rb b/lib/rubygems/resolver/source_set.rb
new file mode 100644
index 0000000000..074b473edc
--- /dev/null
+++ b/lib/rubygems/resolver/source_set.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+##
+# The SourceSet chooses the best available method to query a remote index.
+#
+# Kind off like BestSet but filters the sources for gems
+
+class Gem::Resolver::SourceSet < Gem::Resolver::Set
+ ##
+ # Creates a SourceSet for the given +sources+ or Gem::sources if none are
+ # specified. +sources+ must be a Gem::SourceList.
+
+ def initialize
+ super()
+
+ @links = {}
+ @sets = {}
+ end
+
+ def find_all(req) # :nodoc:
+ if set = get_set(req.dependency.name)
+ set.find_all req
+ else
+ []
+ end
+ end
+
+ # potentially no-op
+ def prefetch(reqs) # :nodoc:
+ reqs.each do |req|
+ if set = get_set(req.dependency.name)
+ set.prefetch reqs
+ end
+ end
+ end
+
+ def add_source_gem(name, source)
+ @links[name] = source
+ end
+
+ private
+
+ def get_set(name)
+ link = @links[name]
+ @sets[link] ||= Gem::Source.new(link).dependency_resolver_set(@prerelease) if link
+ end
+end
diff --git a/lib/rubygems/resolver/spec_specification.rb b/lib/rubygems/resolver/spec_specification.rb
new file mode 100644
index 0000000000..00ef9fdba0
--- /dev/null
+++ b/lib/rubygems/resolver/spec_specification.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+##
+# The Resolver::SpecSpecification contains common functionality for
+# Resolver specifications that are backed by a Gem::Specification.
+
+class Gem::Resolver::SpecSpecification < Gem::Resolver::Specification
+ ##
+ # A SpecSpecification is created for a +set+ for a Gem::Specification in
+ # +spec+. The +source+ is either where the +spec+ came from, or should be
+ # loaded from.
+
+ def initialize(set, spec, source = nil)
+ @set = set
+ @source = source
+ @spec = spec
+ end
+
+ ##
+ # The dependencies of the gem for this specification
+
+ def dependencies
+ spec.dependencies
+ end
+
+ ##
+ # The required_ruby_version constraint for this specification
+
+ def required_ruby_version
+ spec.required_ruby_version
+ end
+
+ ##
+ # The required_rubygems_version constraint for this specification
+
+ def required_rubygems_version
+ spec.required_rubygems_version
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{spec.name}-#{spec.version}"
+ end
+
+ ##
+ # The name of the gem for this specification
+
+ def name
+ spec.name
+ end
+
+ ##
+ # The platform this gem works on.
+
+ def platform
+ spec.platform
+ end
+
+ ##
+ # The version of the gem for this 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
new file mode 100644
index 0000000000..d2098ef0e2
--- /dev/null
+++ b/lib/rubygems/resolver/specification.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+##
+# A Resolver::Specification contains a subset of the information
+# contained in a Gem::Specification. Only the information necessary for
+# dependency resolution in the resolver is included.
+
+class Gem::Resolver::Specification
+ ##
+ # The dependencies of the gem for this specification
+
+ attr_reader :dependencies
+
+ ##
+ # The name of the gem for this specification
+
+ attr_reader :name
+
+ ##
+ # The platform this gem works on.
+
+ attr_reader :platform
+
+ ##
+ # The set this specification came from.
+
+ attr_reader :set
+
+ ##
+ # The source for this specification
+
+ attr_reader :source
+
+ ##
+ # The Gem::Specification for this Resolver::Specification.
+ #
+ # Implementers, note that #install updates @spec, so be sure to cache the
+ # Gem::Specification in @spec when overriding.
+
+ attr_reader :spec
+
+ ##
+ # The version of the gem for this specification.
+
+ attr_reader :version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_ruby_version
+
+ ##
+ # The required_ruby_version constraint for this specification.
+
+ attr_reader :required_rubygems_version
+
+ ##
+ # Sets default instance variables for the specification.
+
+ def initialize
+ @dependencies = nil
+ @name = nil
+ @platform = nil
+ @set = nil
+ @source = nil
+ @version = nil
+ @required_ruby_version = Gem::Requirement.default
+ @required_rubygems_version = Gem::Requirement.default
+ end
+
+ ##
+ # Fetches development dependencies if the source does not provide them by
+ # default (see APISpecification).
+
+ def fetch_development_dependencies # :nodoc:
+ end
+
+ ##
+ # The name and version of the specification.
+ #
+ # Unlike Gem::Specification#full_name, the platform is not included.
+
+ def full_name
+ "#{@name}-#{@version}"
+ end
+
+ ##
+ # Installs this specification using the Gem::Installer +options+. The
+ # install method yields a Gem::Installer instance, which indicates the
+ # gem will be installed, or +nil+, which indicates the gem is already
+ # installed.
+ #
+ # After installation #spec is updated to point to the just-installed
+ # specification.
+
+ def install(options = {})
+ require_relative "../installer"
+
+ gem = download options
+
+ installer = Gem::Installer.at gem, options
+
+ yield installer if block_given?
+
+ @spec = installer.install
+ end
+
+ def download(options)
+ dir = options[:install_dir] || Gem.dir
+
+ Gem.ensure_gem_subdirectories dir
+
+ source.download spec, dir
+ end
+
+ ##
+ # Returns true if this specification is installable on this platform.
+
+ def installable_platform?
+ Gem::Platform.match_spec? spec
+ end
+
+ def local? # :nodoc:
+ false
+ end
+end
diff --git a/lib/rubygems/resolver/strategy.rb b/lib/rubygems/resolver/strategy.rb
new file mode 100644
index 0000000000..bf0dbb6adc
--- /dev/null
+++ b/lib/rubygems/resolver/strategy.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+# Custom PubGrub strategy with caching for version selection.
+# Modeled after Bundler's strategy to avoid redundant versions_for
+# calls during the solver's package selection loop.
+
+class Gem::Resolver::Strategy
+ def initialize(source)
+ @source = source
+ @package_priority_cache = Hash.new {|h, pkg| h[pkg] = {} }
+
+ @version_indexes = Hash.new do |h, k|
+ if Gem::PubGrub::Package.root?(k)
+ h[k] = { Gem::PubGrub::Package.root_version => 0 }
+ else
+ h[k] = @source.all_versions_for(k).each.with_index.to_h
+ end
+ end
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+ [package, most_preferred_version_of(package, range)]
+ end
+
+ private
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+ indexes = @version_indexes[package]
+ versions.min_by {|version| indexes[version] || Float::INFINITY }
+ end
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ @package_priority_cache[package][range] ||= begin
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/vendor_set.rb b/lib/rubygems/resolver/vendor_set.rb
new file mode 100644
index 0000000000..293a1e3331
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_set.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+##
+# A VendorSet represents gems that have been unpacked into a specific
+# directory that contains a gemspec.
+#
+# This is used for gem dependency file support.
+#
+# Example:
+#
+# set = Gem::Resolver::VendorSet.new
+#
+# set.add_vendor_gem 'rake', 'vendor/rake'
+#
+# The directory vendor/rake must contain an unpacked rake gem along with a
+# rake.gemspec (watching the given name).
+
+class Gem::Resolver::VendorSet < Gem::Resolver::Set
+ ##
+ # The specifications for this set.
+
+ attr_reader :specs # :nodoc:
+
+ def initialize # :nodoc:
+ super()
+
+ @directories = {}
+ @specs = {}
+ end
+
+ ##
+ # Adds a specification to the set with the given +name+ which has been
+ # unpacked into the given +directory+.
+
+ def add_vendor_gem(name, directory) # :nodoc:
+ gemspec = File.join directory, "#{name}.gemspec"
+
+ spec = Gem::Specification.load gemspec
+
+ raise Gem::GemNotFoundException,
+ "unable to find #{gemspec} for gem #{name}" unless spec
+
+ spec.full_gem_path = File.expand_path directory
+
+ @specs[spec.name] = spec
+ @directories[spec] = directory
+
+ spec
+ end
+
+ ##
+ # Returns an Array of VendorSpecification objects matching the
+ # DependencyRequest +req+.
+
+ def find_all(req)
+ @specs.values.select do |spec|
+ req.match? spec
+ end.map do |spec|
+ source = Gem::Source::Vendor.new @directories[spec]
+ Gem::Resolver::VendorSpecification.new self, spec, source
+ end
+ end
+
+ ##
+ # Loads a spec with the given +name+. +version+, +platform+ and +source+ are
+ # ignored.
+
+ def load_spec(name, version, platform, source) # :nodoc:
+ @specs.fetch name
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "[VendorSet", "]" do
+ next if @directories.empty?
+ q.breakable
+
+ dirs = @directories.map do |spec, directory|
+ "#{spec.full_name}: #{directory}"
+ end
+
+ q.seplist dirs do |dir|
+ q.text dir
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/resolver/vendor_specification.rb b/lib/rubygems/resolver/vendor_specification.rb
new file mode 100644
index 0000000000..ac78f54558
--- /dev/null
+++ b/lib/rubygems/resolver/vendor_specification.rb
@@ -0,0 +1,23 @@
+# 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:+
+# option.
+
+class Gem::Resolver::VendorSpecification < Gem::Resolver::SpecSpecification
+ def ==(other) # :nodoc:
+ self.class === other &&
+ @set == other.set &&
+ @spec == other.spec &&
+ @source == other.source
+ end
+
+ ##
+ # This is a null install as this gem was unpacked into a directory.
+ # +options+ are ignored.
+
+ def install(options = {})
+ yield nil
+ end
+end
diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb
new file mode 100644
index 0000000000..148cba38c4
--- /dev/null
+++ b/lib/rubygems/s3_uri_signer.rb
@@ -0,0 +1,226 @@
+# frozen_string_literal: true
+
+require_relative "openssl"
+require_relative "user_interaction"
+
+##
+# S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems
+# More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
+class Gem::S3URISigner
+ include Gem::UserInteraction
+
+ class ConfigurationError < Gem::Exception
+ def initialize(message)
+ super message
+ end
+
+ def to_s # :nodoc:
+ super.to_s
+ end
+ end
+
+ class InstanceProfileError < Gem::Exception
+ def initialize(message)
+ super message
+ end
+
+ def to_s # :nodoc:
+ super.to_s
+ end
+ end
+
+ attr_accessor :uri
+ attr_accessor :method
+
+ def initialize(uri, method)
+ @uri = uri
+ @method = method
+ end
+
+ ##
+ # Signs S3 URI using query-params according to the reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
+ def sign(expiration = 86_400)
+ s3_config = fetch_s3_config
+
+ current_time = Time.now.utc
+ date_time = current_time.strftime("%Y%m%dT%H%M%SZ")
+ date = date_time[0,8]
+
+ credential_info = "#{date}/#{s3_config.region}/s3/aws4_request"
+ canonical_host = "#{uri.host}.s3.#{s3_config.region}.amazonaws.com"
+
+ query_params = generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
+ canonical_request = generate_canonical_request(canonical_host, query_params)
+ string_to_sign = generate_string_to_sign(date_time, credential_info, canonical_request)
+ signature = generate_signature(s3_config, date, string_to_sign)
+
+ Gem::URI.parse("https://#{canonical_host}#{uri.path}?#{query_params}&X-Amz-Signature=#{signature}")
+ end
+
+ private
+
+ S3Config = Struct.new :access_key_id, :secret_access_key, :security_token, :region
+
+ def generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
+ canonical_params = {}
+ canonical_params["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256"
+ canonical_params["X-Amz-Credential"] = "#{s3_config.access_key_id}/#{credential_info}"
+ canonical_params["X-Amz-Date"] = date_time
+ canonical_params["X-Amz-Expires"] = expiration.to_s
+ canonical_params["X-Amz-SignedHeaders"] = "host"
+ canonical_params["X-Amz-Security-Token"] = s3_config.security_token if s3_config.security_token
+
+ # Sorting is required to generate proper signature
+ canonical_params.sort.to_h.map do |key, value|
+ "#{base64_uri_escape(key)}=#{base64_uri_escape(value)}"
+ end.join("&")
+ end
+
+ def generate_canonical_request(canonical_host, query_params)
+ [
+ method.upcase,
+ uri.path,
+ query_params,
+ "host:#{canonical_host}",
+ "", # empty params
+ "host",
+ "UNSIGNED-PAYLOAD",
+ ].join("\n")
+ end
+
+ def generate_string_to_sign(date_time, credential_info, canonical_request)
+ [
+ "AWS4-HMAC-SHA256",
+ date_time,
+ credential_info,
+ OpenSSL::Digest::SHA256.hexdigest(canonical_request),
+ ].join("\n")
+ end
+
+ def generate_signature(s3_config, date, string_to_sign)
+ date_key = OpenSSL::HMAC.digest("sha256", "AWS4" + s3_config.secret_access_key, date)
+ date_region_key = OpenSSL::HMAC.digest("sha256", date_key, s3_config.region)
+ date_region_service_key = OpenSSL::HMAC.digest("sha256", date_region_key, "s3")
+ signing_key = OpenSSL::HMAC.digest("sha256", date_region_service_key, "aws4_request")
+ OpenSSL::HMAC.hexdigest("sha256", signing_key, string_to_sign)
+ end
+
+ ##
+ # Extracts S3 configuration for S3 bucket
+ def fetch_s3_config
+ return S3Config.new(uri.user, uri.password, nil, "us-east-1") if uri.user && uri.password
+
+ s3_source = Gem.configuration[:s3_source] || Gem.configuration["s3_source"]
+ host = uri.host
+ raise ConfigurationError.new("no s3_source key exists in .gemrc") unless s3_source
+
+ auth = s3_source[host] || s3_source[host.to_sym]
+ raise ConfigurationError.new("no key for host #{host} in s3_source in .gemrc") unless auth
+
+ provider = auth[:provider] || auth["provider"]
+ case provider
+ when "env"
+ id = ENV["AWS_ACCESS_KEY_ID"]
+ secret = ENV["AWS_SECRET_ACCESS_KEY"]
+ security_token = ENV["AWS_SESSION_TOKEN"]
+ when "instance_profile"
+ credentials = ec2_metadata_credentials_json
+ id = credentials["AccessKeyId"]
+ secret = credentials["SecretAccessKey"]
+ security_token = credentials["Token"]
+ else
+ id = auth[:id] || auth["id"]
+ secret = auth[:secret] || auth["secret"]
+ security_token = auth[:security_token] || auth["security_token"]
+ end
+
+ raise ConfigurationError.new("s3_source for #{host} missing id or secret") unless id && secret
+
+ region = auth[:region] || auth["region"] || "us-east-1"
+ S3Config.new(id, secret, security_token, region)
+ end
+
+ def base64_uri_escape(str)
+ str.gsub(%r{[\+/=\n]}, BASE64_URI_TRANSLATE)
+ end
+
+ def ec2_metadata_credentials_json
+ require_relative "vendored_net_http"
+ require_relative "request"
+ require_relative "request/connection_pools"
+ require "json"
+
+ # First try V2 fallback to V1
+ res = nil
+ begin
+ res = ec2_metadata_credentials_imds_v2
+ rescue InstanceProfileError
+ alert_warning "Unable to access ec2 credentials via IMDSv2, falling back to IMDSv1"
+ res = ec2_metadata_credentials_imds_v1
+ end
+ res
+ end
+
+ def ec2_metadata_credentials_imds_v2
+ token = ec2_metadata_token
+ iam_info = ec2_metadata_request(EC2_IAM_INFO, token:)
+ # Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
+ role_name = iam_info["InstanceProfileArn"].split("/").last
+ ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token:)
+ end
+
+ def ec2_metadata_credentials_imds_v1
+ iam_info = ec2_metadata_request(EC2_IAM_INFO, token: nil)
+ # Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
+ role_name = iam_info["InstanceProfileArn"].split("/").last
+ ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name, token: nil)
+ end
+
+ def ec2_metadata_request(url, token:)
+ request = ec2_iam_request(Gem::URI(url), Gem::Net::HTTP::Get)
+
+ response = request.fetch do |req|
+ if token
+ req.add_field "X-aws-ec2-metadata-token", token
+ end
+ end
+
+ case response
+ when Gem::Net::HTTPOK then
+ JSON.parse(response.body)
+ else
+ raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
+ end
+ end
+
+ def ec2_metadata_token
+ request = ec2_iam_request(Gem::URI(EC2_IAM_TOKEN), Gem::Net::HTTP::Put)
+
+ response = request.fetch do |req|
+ req.add_field "X-aws-ec2-metadata-token-ttl-seconds", 60
+ end
+
+ case response
+ when Gem::Net::HTTPOK then
+ response.body
+ else
+ raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
+ end
+ end
+
+ def ec2_iam_request(uri, verb)
+ @request_pool ||= create_request_pool(uri)
+ Gem::Request.new(uri, verb, nil, @request_pool)
+ end
+
+ def create_request_pool(uri)
+ proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme))
+ certs = Gem::Request.get_cert_files
+ Gem::Request::ConnectionPools.new(proxy_uri, certs).pool_for(uri)
+ end
+
+ BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze
+ EC2_IAM_TOKEN = "http://169.254.169.254/latest/api/token"
+ EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info"
+ EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
+end
diff --git a/lib/rubygems/safe_marshal.rb b/lib/rubygems/safe_marshal.rb
new file mode 100644
index 0000000000..871f24727d
--- /dev/null
+++ b/lib/rubygems/safe_marshal.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require "stringio"
+
+require_relative "safe_marshal/reader"
+require_relative "safe_marshal/visitors/to_ruby"
+
+module Gem
+ ###
+ # This module is used for safely loading Marshal specs from a gem. The
+ # `safe_load` method defined on this module is specifically designed for
+ # loading Gem specifications.
+
+ module SafeMarshal
+ PERMITTED_CLASSES = %w[
+ Date
+ Time
+ Rational
+
+ Gem::Dependency
+ Gem::NameTuple
+ Gem::Platform
+ Gem::Requirement
+ Gem::Specification
+ Gem::Version
+ Gem::Version::Requirement
+
+ YAML::Syck::DefaultKey
+ YAML::PrivateType
+ ].freeze
+ private_constant :PERMITTED_CLASSES
+
+ PERMITTED_SYMBOLS = %w[
+ development
+ runtime
+
+ name
+ number
+ platform
+ dependencies
+ ].freeze
+ private_constant :PERMITTED_SYMBOLS
+
+ PERMITTED_IVARS = {
+ "String" => %w[E encoding @taguri @debug_created_info],
+ "Time" => %w[
+ offset zone nano_num nano_den submicro
+ @_zone @marshal_with_utc_coercion
+ ],
+ "Gem::Dependency" => %w[
+ @name @requirement @prerelease @version_requirement @version_requirements @type
+ @force_ruby_platform
+ ],
+ "Gem::NameTuple" => %w[@name @version @platform],
+ "Gem::Platform" => %w[@os @cpu @version],
+ "Psych::PrivateType" => %w[@value @type_id],
+ "YAML::PrivateType" => %w[@value @type_id],
+ }.freeze
+ private_constant :PERMITTED_IVARS
+
+ def self.safe_load(input)
+ load(input, permitted_classes: PERMITTED_CLASSES, permitted_symbols: PERMITTED_SYMBOLS, permitted_ivars: PERMITTED_IVARS)
+ end
+
+ def self.load(input, permitted_classes: [::Symbol], permitted_symbols: [], permitted_ivars: {})
+ root = Reader.new(StringIO.new(input, "r").binmode).read!
+
+ Visitors::ToRuby.new(
+ permitted_classes: permitted_classes,
+ permitted_symbols: permitted_symbols,
+ permitted_ivars: permitted_ivars,
+ ).visit(root)
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/elements.rb b/lib/rubygems/safe_marshal/elements.rb
new file mode 100644
index 0000000000..f8874b1b2f
--- /dev/null
+++ b/lib/rubygems/safe_marshal/elements.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+module Gem
+ module SafeMarshal
+ module Elements
+ class Element
+ end
+
+ class Symbol < Element
+ def initialize(name)
+ @name = name
+ end
+ attr_reader :name
+ end
+
+ class UserDefined < Element
+ def initialize(name, binary_string)
+ @name = name
+ @binary_string = binary_string
+ end
+
+ attr_reader :name, :binary_string
+ end
+
+ class UserMarshal < Element
+ def initialize(name, data)
+ @name = name
+ @data = data
+ end
+
+ attr_reader :name, :data
+ end
+
+ class String < Element
+ def initialize(str)
+ @str = str
+ end
+
+ attr_reader :str
+ end
+
+ class Hash < Element
+ def initialize(pairs)
+ @pairs = pairs
+ end
+
+ attr_reader :pairs
+ end
+
+ class HashWithDefaultValue < Hash
+ def initialize(pairs, default)
+ super(pairs)
+ @default = default
+ end
+
+ attr_reader :default
+ end
+
+ class Array < Element
+ def initialize(elements)
+ @elements = elements
+ end
+
+ attr_reader :elements
+ end
+
+ class Integer < Element
+ def initialize(int)
+ @int = int
+ end
+
+ attr_reader :int
+ end
+
+ class True < Element
+ def initialize
+ end
+ TRUE = new.freeze
+ end
+
+ class False < Element
+ def initialize
+ end
+
+ FALSE = new.freeze
+ end
+
+ class WithIvars < Element
+ def initialize(object, ivars)
+ @object = object
+ @ivars = ivars
+ end
+
+ attr_reader :object, :ivars
+ end
+
+ class Object < Element
+ def initialize(name)
+ @name = name
+ end
+ attr_reader :name
+ end
+
+ class Nil < Element
+ NIL = new.freeze
+ end
+
+ class ObjectLink < Element
+ def initialize(offset)
+ @offset = offset
+ end
+ attr_reader :offset
+ end
+
+ class SymbolLink < Element
+ def initialize(offset)
+ @offset = offset
+ end
+ attr_reader :offset
+ end
+
+ class Float < Element
+ def initialize(string)
+ @string = string
+ end
+ attr_reader :string
+ end
+
+ class Bignum < Element # rubocop:disable Lint/UnifiedInteger
+ def initialize(sign, data)
+ @sign = sign
+ @data = data
+ end
+ attr_reader :sign, :data
+ end
+
+ class UserClass < Element
+ def initialize(name, wrapped_object)
+ @name = name
+ @wrapped_object = wrapped_object
+ end
+ attr_reader :name, :wrapped_object
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/reader.rb b/lib/rubygems/safe_marshal/reader.rb
new file mode 100644
index 0000000000..4362d65fd6
--- /dev/null
+++ b/lib/rubygems/safe_marshal/reader.rb
@@ -0,0 +1,325 @@
+# frozen_string_literal: true
+
+require_relative "elements"
+
+module Gem
+ module SafeMarshal
+ class Reader
+ class Error < StandardError
+ end
+
+ class UnsupportedVersionError < Error
+ end
+
+ class UnconsumedBytesError < Error
+ end
+
+ class NotImplementedError < Error
+ end
+
+ class EOFError < Error
+ end
+
+ class DataTooShortError < Error
+ end
+
+ class NegativeLengthError < Error
+ end
+
+ def initialize(io)
+ @io = io
+ end
+
+ def read!
+ read_header
+ root = read_element
+ raise UnconsumedBytesError, "expected EOF, got #{@io.read(10).inspect}... after top-level element #{root.class}" unless @io.eof?
+ root
+ end
+
+ private
+
+ MARSHAL_VERSION = [Marshal::MAJOR_VERSION, Marshal::MINOR_VERSION].map(&:chr).join.freeze
+ private_constant :MARSHAL_VERSION
+
+ def read_header
+ v = @io.read(2)
+ raise UnsupportedVersionError, "Unsupported marshal version #{v.bytes.map(&:ord).join(".")}, expected #{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}" unless v == MARSHAL_VERSION
+ end
+
+ def read_bytes(n)
+ raise NegativeLengthError if n < 0
+ str = @io.read(n)
+ raise EOFError, "expected #{n} bytes, got EOF" if str.nil?
+ raise DataTooShortError, "expected #{n} bytes, got #{str.inspect}" unless str.bytesize == n
+ str
+ end
+
+ def read_byte
+ @io.getbyte || raise(EOFError, "Unexpected EOF")
+ end
+
+ def read_integer
+ b = read_byte
+
+ case b
+ when 0x00
+ 0
+ when 0x01
+ read_byte
+ when 0x02
+ read_byte | (read_byte << 8)
+ when 0x03
+ read_byte | (read_byte << 8) | (read_byte << 16)
+ when 0x04
+ read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24)
+ when 0xFC
+ read_byte | (read_byte << 8) | (read_byte << 16) | (read_byte << 24) | -0x100000000
+ when 0xFD
+ read_byte | (read_byte << 8) | (read_byte << 16) | -0x1000000
+ when 0xFE
+ read_byte | (read_byte << 8) | -0x10000
+ when 0xFF
+ read_byte | -0x100
+ else
+ signed = (b ^ 128) - 128
+ if b >= 128
+ signed + 5
+ else
+ signed - 5
+ end
+ end
+ end
+
+ def read_element
+ type = read_byte
+ case type
+ when 34 then read_string # ?"
+ when 48 then read_nil # ?0
+ when 58 then read_symbol # ?:
+ when 59 then read_symbol_link # ?;
+ when 64 then read_object_link # ?@
+ when 70 then read_false # ?F
+ when 73 then read_object_with_ivars # ?I
+ when 84 then read_true # ?T
+ when 85 then read_user_marshal # ?U
+ when 91 then read_array # ?[
+ when 102 then read_float # ?f
+ when 105 then Elements::Integer.new(read_integer) # ?i
+ when 108 then read_bignum # ?l
+ when 111 then read_object # ?o
+ when 117 then read_user_defined # ?u
+ when 123 then read_hash # ?{
+ when 125 then read_hash_with_default_value # ?}
+ when 101 then read_extended_object # ?e
+ when 99 then read_class # ?c
+ when 109 then read_module # ?m
+ when 77 then read_class_or_module # ?M
+ when 100 then read_data # ?d
+ when 47 then read_regexp # ?/
+ when 83 then read_struct # ?S
+ when 67 then read_user_class # ?C
+ else
+ raise Error, "Unknown marshal type discriminator #{type.chr.inspect} (#{type})"
+ end
+ end
+
+ STRING_E_SYMBOL = Elements::Symbol.new("E").freeze
+ private_constant :STRING_E_SYMBOL
+
+ def read_symbol
+ len = read_integer
+ if len == 1
+ byte = read_byte
+ if byte == 69 # ?E
+ STRING_E_SYMBOL
+ else
+ Elements::Symbol.new(byte.chr)
+ end
+ else
+ name = read_bytes(len)
+ Elements::Symbol.new(name)
+ end
+ end
+
+ EMPTY_STRING = Elements::String.new("".b.freeze).freeze
+ private_constant :EMPTY_STRING
+
+ def read_string
+ length = read_integer
+ return EMPTY_STRING if length == 0
+ str = read_bytes(length)
+ Elements::String.new(str)
+ end
+
+ def read_true
+ Elements::True::TRUE
+ end
+
+ def read_false
+ Elements::False::FALSE
+ end
+
+ def read_user_defined
+ name = read_element
+ binary_string = read_bytes(read_integer)
+ Elements::UserDefined.new(name, binary_string)
+ end
+
+ EMPTY_ARRAY = Elements::Array.new([].freeze).freeze
+ private_constant :EMPTY_ARRAY
+
+ def read_array
+ length = read_integer
+ return EMPTY_ARRAY if length == 0
+ raise NegativeLengthError if length < 0
+ elements = Array.new(length) do
+ read_element
+ end
+ Elements::Array.new(elements)
+ end
+
+ def read_object_with_ivars
+ object = read_element
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ ivars = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::WithIvars.new(object, ivars)
+ end
+
+ def read_symbol_link
+ offset = read_integer
+ Elements::SymbolLink.new(offset)
+ end
+
+ def read_user_marshal
+ name = read_element
+ data = read_element
+ Elements::UserMarshal.new(name, data)
+ end
+
+ # profiling bundle install --full-index shows that
+ # offset 6 is by far the most common object link,
+ # so we special case it to avoid allocating a new
+ # object a third of the time.
+ # the following are all the object links that
+ # appear more than 10000 times in my profiling
+
+ OBJECT_LINKS = {
+ 6 => Elements::ObjectLink.new(6).freeze,
+ 30 => Elements::ObjectLink.new(30).freeze,
+ 81 => Elements::ObjectLink.new(81).freeze,
+ 34 => Elements::ObjectLink.new(34).freeze,
+ 38 => Elements::ObjectLink.new(38).freeze,
+ 50 => Elements::ObjectLink.new(50).freeze,
+ 91 => Elements::ObjectLink.new(91).freeze,
+ 42 => Elements::ObjectLink.new(42).freeze,
+ 46 => Elements::ObjectLink.new(46).freeze,
+ 150 => Elements::ObjectLink.new(150).freeze,
+ 100 => Elements::ObjectLink.new(100).freeze,
+ 104 => Elements::ObjectLink.new(104).freeze,
+ 108 => Elements::ObjectLink.new(108).freeze,
+ 242 => Elements::ObjectLink.new(242).freeze,
+ 246 => Elements::ObjectLink.new(246).freeze,
+ 139 => Elements::ObjectLink.new(139).freeze,
+ 143 => Elements::ObjectLink.new(143).freeze,
+ 114 => Elements::ObjectLink.new(114).freeze,
+ 308 => Elements::ObjectLink.new(308).freeze,
+ 200 => Elements::ObjectLink.new(200).freeze,
+ 54 => Elements::ObjectLink.new(54).freeze,
+ 62 => Elements::ObjectLink.new(62).freeze,
+ 1_286_245 => Elements::ObjectLink.new(1_286_245).freeze,
+ }.freeze
+ private_constant :OBJECT_LINKS
+
+ def read_object_link
+ offset = read_integer
+ OBJECT_LINKS[offset] || Elements::ObjectLink.new(offset)
+ end
+
+ EMPTY_HASH = Elements::Hash.new([].freeze).freeze
+ private_constant :EMPTY_HASH
+
+ def read_hash
+ length = read_integer
+ return EMPTY_HASH if length == 0
+ pairs = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::Hash.new(pairs)
+ end
+
+ def read_hash_with_default_value
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ pairs = Array.new(length) do
+ [read_element, read_element]
+ end
+ default = read_element
+ Elements::HashWithDefaultValue.new(pairs, default)
+ end
+
+ def read_object
+ name = read_element
+ object = Elements::Object.new(name)
+ length = read_integer
+ raise NegativeLengthError if length < 0
+ ivars = Array.new(length) do
+ [read_element, read_element]
+ end
+ Elements::WithIvars.new(object, ivars)
+ end
+
+ def read_nil
+ Elements::Nil::NIL
+ end
+
+ def read_float
+ string = read_bytes(read_integer)
+ Elements::Float.new(string)
+ end
+
+ def read_bignum
+ sign = read_byte
+ data = read_bytes(read_integer * 2)
+ Elements::Bignum.new(sign, data)
+ end
+
+ def read_extended_object
+ raise NotImplementedError, "Reading Marshal objects of type extended_object is not implemented"
+ end
+
+ def read_class
+ raise NotImplementedError, "Reading Marshal objects of type class is not implemented"
+ end
+
+ def read_module
+ raise NotImplementedError, "Reading Marshal objects of type module is not implemented"
+ end
+
+ def read_class_or_module
+ raise NotImplementedError, "Reading Marshal objects of type class_or_module is not implemented"
+ end
+
+ def read_data
+ raise NotImplementedError, "Reading Marshal objects of type data is not implemented"
+ end
+
+ def read_regexp
+ raise NotImplementedError, "Reading Marshal objects of type regexp is not implemented"
+ end
+
+ def read_struct
+ raise NotImplementedError, "Reading Marshal objects of type struct is not implemented"
+ end
+
+ def read_user_class
+ name = read_element
+ wrapped_object = read_element
+ Elements::UserClass.new(name, wrapped_object)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/stream_printer.rb b/lib/rubygems/safe_marshal/visitors/stream_printer.rb
new file mode 100644
index 0000000000..162b36ad05
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/stream_printer.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "visitor"
+
+module Gem::SafeMarshal
+ module Visitors
+ class StreamPrinter < Visitor
+ def initialize(io, indent: "")
+ @io = io
+ @indent = indent
+ @level = 0
+ end
+
+ def visit(target)
+ @io.write("#{@indent * @level}#{target.class}")
+ target.instance_variables.each do |ivar|
+ value = target.instance_variable_get(ivar)
+ next if Elements::Element === value || Array === value
+ @io.write(" #{ivar}=#{value.inspect}")
+ end
+ @io.write("\n")
+ begin
+ @level += 1
+ super
+ ensure
+ @level -= 1
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/to_ruby.rb b/lib/rubygems/safe_marshal/visitors/to_ruby.rb
new file mode 100644
index 0000000000..a1f9481776
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/to_ruby.rb
@@ -0,0 +1,428 @@
+# frozen_string_literal: true
+
+require_relative "visitor"
+
+module Gem::SafeMarshal
+ module Visitors
+ class ToRuby < Visitor
+ def initialize(permitted_classes:, permitted_symbols:, permitted_ivars:)
+ @permitted_classes = permitted_classes
+ @permitted_symbols = ["E"].concat(permitted_symbols).concat(permitted_classes)
+ @permitted_ivars = permitted_ivars
+
+ @objects = []
+ @symbols = []
+ @class_cache = {}
+
+ @stack = ["root"]
+ @stack_idx = 1
+ end
+
+ def inspect # :nodoc:
+ format("#<%s permitted_classes: %p permitted_symbols: %p permitted_ivars: %p>",
+ self.class, @permitted_classes, @permitted_symbols, @permitted_ivars)
+ end
+
+ def visit(target)
+ stack_idx = @stack_idx
+ super
+ ensure
+ @stack_idx = stack_idx - 1
+ end
+
+ private
+
+ def push_stack(element)
+ @stack[@stack_idx] = element
+ @stack_idx += 1
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Array(a)
+ array = register_object([])
+
+ elements = a.elements
+ size = elements.size
+ idx = 0
+ # not idiomatic, but there's a huge number of IMEMOs allocated here, so we avoid the block
+ # because this is such a hot path when doing a bundle install with the full index
+ while idx < size
+ push_stack idx
+ array << visit(elements[idx])
+ idx += 1
+ end
+
+ array
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Symbol(s)
+ name = s.name
+ raise UnpermittedSymbolError.new(symbol: name, stack: formatted_stack) unless @permitted_symbols.include?(name)
+ visit_symbol_type(s)
+ end
+
+ def map_ivars(klass, ivars)
+ stack_idx = @stack_idx
+ ivars.map.with_index do |(k, v), i|
+ @stack_idx = stack_idx
+
+ push_stack "ivar_"
+ push_stack i
+ k = resolve_ivar(klass, k)
+
+ @stack_idx = stack_idx
+ push_stack k
+
+ next k, visit(v)
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_WithIvars(e)
+ object_offset = @objects.size
+ push_stack "object"
+ object = visit(e.object)
+ ivars = map_ivars(object.class, e.ivars)
+
+ case e.object
+ when Elements::UserDefined
+ if object.class == ::Time
+ internal = []
+
+ ivars.reject! do |k, v|
+ case k
+ when :offset, :zone, :nano_num, :nano_den, :submicro
+ internal << [k, v]
+ true
+ else
+ false
+ end
+ end
+
+ s = e.object.binary_string
+ # 122 is the largest integer that can be represented in marshal in a single byte
+ raise TimeTooLargeError.new("binary string too large", stack: formatted_stack) if s.bytesize > 122
+
+ marshal_string = "\x04\bIu:\tTime".b
+ marshal_string.concat(s.bytesize + 5)
+ marshal_string << s
+ # internal is limited to 5, so no overflow is possible
+ marshal_string.concat(internal.size + 5)
+
+ internal.each do |k, v|
+ k = k.name
+ # ivar name can't be too large because only known ivars are in the internal ivars list
+ marshal_string.concat(":")
+ marshal_string.concat(k.bytesize + 5)
+ marshal_string.concat(k)
+ dumped = Marshal.dump(v)
+ dumped[0, 2] = ""
+ marshal_string.concat(dumped)
+ end
+
+ object = @objects[object_offset] = Marshal.load(marshal_string)
+ end
+ when Elements::String
+ enc = nil
+
+ ivars.reject! do |k, v|
+ case k
+ when :E
+ case v
+ when TrueClass
+ enc = "UTF-8"
+ when FalseClass
+ enc = "US-ASCII"
+ else
+ raise FormatError, "Unexpected value for String :E #{v.inspect}"
+ end
+ when :encoding
+ enc = v
+ else
+ next false
+ end
+ true
+ end
+
+ object.force_encoding(enc) if enc
+ end
+
+ ivars.each do |k, v|
+ object.instance_variable_set k, v
+ end
+ object
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Hash(o)
+ hash = register_object({})
+
+ o.pairs.each_with_index do |(k, v), i|
+ push_stack i
+ k = visit(k)
+ push_stack k
+ hash[k] = visit(v)
+ end
+
+ hash
+ end
+
+ def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(o)
+ hash = visit_Gem_SafeMarshal_Elements_Hash(o)
+ push_stack :default
+ hash.default = visit(o.default)
+ hash
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Object(o)
+ register_object(resolve_class(o.name).allocate)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_ObjectLink(o)
+ @objects.fetch(o.offset)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_SymbolLink(o)
+ @symbols.fetch(o.offset)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserDefined(o)
+ register_object(call_method(resolve_class(o.name), :_load, o.binary_string))
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserMarshal(o)
+ klass = resolve_class(o.name)
+ compat = COMPAT_CLASSES.fetch(klass, nil)
+ idx = @objects.size
+ object = register_object(call_method(compat || klass, :allocate))
+
+ push_stack :data
+ ret = call_method(object, :marshal_load, visit(o.data))
+
+ if compat
+ object = @objects[idx] = ret
+ end
+
+ object
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Integer(i)
+ i.int
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Nil(_)
+ nil
+ end
+
+ def visit_Gem_SafeMarshal_Elements_True(_)
+ true
+ end
+
+ def visit_Gem_SafeMarshal_Elements_False(_)
+ false
+ end
+
+ def visit_Gem_SafeMarshal_Elements_String(s)
+ register_object(+s.str)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Float(f)
+ register_object(
+ case f.string
+ when "inf"
+ ::Float::INFINITY
+ when "-inf"
+ -::Float::INFINITY
+ when "nan"
+ ::Float::NAN
+ else
+ f.string.to_f
+ end
+ )
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Bignum(b)
+ result = 0
+ b.data.each_byte.with_index do |byte, exp|
+ result += (byte * 2**(exp * 8))
+ end
+
+ case b.sign
+ when 43 # ?+
+ result
+ when 45 # ?-
+ -result
+ else
+ raise FormatError, "Unexpected sign for Bignum #{b.sign.chr.inspect} (#{b.sign})"
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserClass(r)
+ if resolve_class(r.name) == ::Hash && r.wrapped_object.is_a?(Elements::Hash)
+
+ hash = register_object({}.compare_by_identity)
+
+ o = r.wrapped_object
+ o.pairs.each_with_index do |(k, v), i|
+ push_stack i
+ k = visit(k)
+ push_stack k
+ hash[k] = visit(v)
+ end
+
+ if o.is_a?(Elements::HashWithDefaultValue)
+ push_stack :default
+ hash.default = visit(o.default)
+ end
+
+ hash
+ else
+ raise UnsupportedError.new("Unsupported user class #{resolve_class(r.name)} in marshal stream", stack: formatted_stack)
+ end
+ end
+
+ def resolve_class(n)
+ @class_cache[n] ||= begin
+ to_s = resolve_symbol_name(n)
+ raise UnpermittedClassError.new(name: to_s, stack: formatted_stack) unless @permitted_classes.include?(to_s)
+ visit_symbol_type(n)
+ begin
+ ::Object.const_get(to_s)
+ rescue NameError
+ raise ArgumentError, "Undefined class #{to_s.inspect}"
+ end
+ end
+ end
+
+ class RationalCompat
+ def marshal_load(s)
+ num, den = s
+ raise ArgumentError, "Expected 2 ints" unless s.size == 2 && num.is_a?(Integer) && den.is_a?(Integer)
+ Rational(num, den)
+ end
+ end
+ private_constant :RationalCompat
+
+ COMPAT_CLASSES = {}.tap do |h|
+ h[Rational] = RationalCompat
+ end.compare_by_identity.freeze
+ private_constant :COMPAT_CLASSES
+
+ def resolve_ivar(klass, name)
+ to_s = resolve_symbol_name(name)
+
+ raise UnpermittedIvarError.new(symbol: to_s, klass: klass, stack: formatted_stack) unless @permitted_ivars.fetch(klass.name, [].freeze).include?(to_s)
+
+ visit_symbol_type(name)
+ end
+
+ def visit_symbol_type(element)
+ case element
+ when Elements::Symbol
+ sym = element.name.to_sym
+ @symbols << sym
+ sym
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element)
+ end
+ end
+
+ # This is a hot method, so avoid respond_to? checks on every invocation
+ if :read.respond_to?(:name)
+ def resolve_symbol_name(element)
+ case element
+ when Elements::Symbol
+ element.name
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element).name
+ else
+ raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}"
+ end
+ end
+ else
+ def resolve_symbol_name(element)
+ case element
+ when Elements::Symbol
+ element.name
+ when Elements::SymbolLink
+ visit_Gem_SafeMarshal_Elements_SymbolLink(element).to_s
+ else
+ raise FormatError, "Expected symbol or symbol link, got #{element.inspect} @ #{formatted_stack.join(".")}"
+ end
+ end
+ end
+
+ def register_object(o)
+ @objects << o
+ o
+ end
+
+ def call_method(receiver, method, *args)
+ receiver.__send__(method, *args)
+ rescue NoMethodError => e
+ raise unless e.receiver == receiver
+
+ raise MethodCallError, "Unable to call #{method.inspect} on #{receiver.inspect}, perhaps it is a class using marshal compat, which is not visible in ruby? #{e}"
+ end
+
+ def formatted_stack
+ formatted = []
+ @stack[0, @stack_idx].each do |e|
+ if e.is_a?(Integer)
+ if formatted.last == "ivar_"
+ formatted[-1] = "ivar_#{e}"
+ else
+ formatted << "[#{e}]"
+ end
+ else
+ formatted << e
+ end
+ end
+ formatted
+ end
+
+ class Error < StandardError
+ end
+
+ class TimeTooLargeError < Error
+ def initialize(message, stack:)
+ super "#{message} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedSymbolError < Error
+ def initialize(symbol:, stack:)
+ @symbol = symbol
+ @stack = stack
+ super "Attempting to load unpermitted symbol #{symbol.inspect} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedIvarError < Error
+ def initialize(symbol:, klass:, stack:)
+ @symbol = symbol
+ @klass = klass
+ @stack = stack
+ super "Attempting to set unpermitted ivar #{symbol.inspect} on object of class #{klass} @ #{stack.join "."}"
+ end
+ end
+
+ class UnpermittedClassError < Error
+ def initialize(name:, stack:)
+ @name = name
+ @stack = stack
+ super "Attempting to load unpermitted class #{name.inspect} @ #{stack.join "."}"
+ end
+ end
+
+ class UnsupportedError < Error
+ def initialize(message, stack:)
+ super "#{message} @ #{stack.join "."}"
+ end
+ end
+
+ class FormatError < Error
+ end
+
+ class MethodCallError < Error
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_marshal/visitors/visitor.rb b/lib/rubygems/safe_marshal/visitors/visitor.rb
new file mode 100644
index 0000000000..c9a079dc0e
--- /dev/null
+++ b/lib/rubygems/safe_marshal/visitors/visitor.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gem::SafeMarshal::Visitors
+ class Visitor
+ def visit(target)
+ send DISPATCH.fetch(target.class), target
+ end
+
+ private
+
+ DISPATCH = Gem::SafeMarshal::Elements.constants.each_with_object({}) do |c, h|
+ next if c == :Element
+
+ klass = Gem::SafeMarshal::Elements.const_get(c)
+ h[klass] = :"visit_#{klass.name.gsub("::", "_")}"
+ h.default = :visit_unknown_element
+ end.compare_by_identity.freeze
+ private_constant :DISPATCH
+
+ def visit_unknown_element(e)
+ raise ArgumentError, "Attempting to visit unknown element #{e.inspect}"
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Array(target)
+ target.elements.each {|e| visit(e) }
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Bignum(target); end
+ def visit_Gem_SafeMarshal_Elements_False(target); end
+ def visit_Gem_SafeMarshal_Elements_Float(target); end
+
+ def visit_Gem_SafeMarshal_Elements_Hash(target)
+ target.pairs.each do |k, v|
+ visit(k)
+ visit(v)
+ end
+ end
+
+ def visit_Gem_SafeMarshal_Elements_HashWithDefaultValue(target)
+ visit_Gem_SafeMarshal_Elements_Hash(target)
+ visit(target.default)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_Integer(target); end
+ def visit_Gem_SafeMarshal_Elements_Nil(target); end
+
+ def visit_Gem_SafeMarshal_Elements_Object(target)
+ visit(target.name)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_ObjectLink(target); end
+ def visit_Gem_SafeMarshal_Elements_String(target); end
+ def visit_Gem_SafeMarshal_Elements_Symbol(target); end
+ def visit_Gem_SafeMarshal_Elements_SymbolLink(target); end
+ def visit_Gem_SafeMarshal_Elements_True(target); end
+
+ def visit_Gem_SafeMarshal_Elements_UserDefined(target)
+ visit(target.name)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_UserMarshal(target)
+ visit(target.name)
+ visit(target.data)
+ end
+
+ def visit_Gem_SafeMarshal_Elements_WithIvars(target)
+ visit(target.object)
+ target.ivars.each do |k, v|
+ visit(k)
+ visit(v)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/safe_yaml.rb b/lib/rubygems/safe_yaml.rb
new file mode 100644
index 0000000000..f4bba00136
--- /dev/null
+++ b/lib/rubygems/safe_yaml.rb
@@ -0,0 +1,55 @@
+# 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
+ # loading Gem specifications. For loading other YAML safely, please see
+ # Psych.safe_load
+
+ module SafeYAML
+ PERMITTED_CLASSES = %w[
+ Symbol
+ Time
+ Date
+ Gem::Dependency
+ Gem::Platform
+ Gem::Requirement
+ Gem::Specification
+ Gem::Version
+ Gem::Version::Requirement
+ ].freeze
+
+ PERMITTED_SYMBOLS = %w[
+ development
+ runtime
+ ].freeze
+
+ @aliases_enabled = true
+ def self.aliases_enabled=(value) # :nodoc:
+ @aliases_enabled = !!value
+ end
+
+ def self.aliases_enabled? # :nodoc:
+ @aliases_enabled
+ end
+
+ def self.safe_load(input)
+ if Gem.use_psych?
+ ::Psych.safe_load(input, permitted_classes: PERMITTED_CLASSES,
+ permitted_symbols: PERMITTED_SYMBOLS, aliases: @aliases_enabled)
+ else
+ Gem::YAMLSerializer.load(
+ input,
+ permitted_classes: PERMITTED_CLASSES,
+ permitted_symbols: PERMITTED_SYMBOLS,
+ aliases: aliases_enabled?
+ )
+ end
+ end
+
+ class << self
+ alias_method :load, :safe_load
+ end
+ end
+end
diff --git a/lib/rubygems/security.rb b/lib/rubygems/security.rb
new file mode 100644
index 0000000000..69ba87b07f
--- /dev/null
+++ b/lib/rubygems/security.rb
@@ -0,0 +1,615 @@
+# 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 "exceptions"
+require_relative "openssl"
+
+##
+# = Signing gems
+#
+# The Gem::Security implements cryptographic signatures for gems. The section
+# below is a step-by-step guide to using signed gems and generating your own.
+#
+# == Walkthrough
+#
+# === Building your certificate
+#
+# In order to start signing your gems, you'll need to build a private key and
+# a self-signed certificate. Here's how:
+#
+# # build a private key and certificate for yourself:
+# $ gem cert --build you@example.com
+#
+# This could take anywhere from a few seconds to a minute or two, depending on
+# the speed of your computer (public key algorithms aren't exactly the
+# speediest crypto algorithms in the world). When it's finished, you'll see
+# the files "gem-private_key.pem" and "gem-public_cert.pem" in the current
+# directory.
+#
+# First things first: Move both files to ~/.gem if you don't already have a
+# key and certificate in that directory. Ensure the file permissions make the
+# key unreadable by others (by default the file is saved securely).
+#
+# Keep your private key hidden; if it's compromised, someone can sign packages
+# as you (note: PKI has ways of mitigating the risk of stolen keys; more on
+# that later).
+#
+# === Signing Gems
+#
+# In RubyGems 2 and newer there is no extra work to sign a gem. RubyGems will
+# automatically find your key and certificate in your home directory and use
+# them to sign newly packaged gems.
+#
+# If your certificate is not self-signed (signed by a third party) RubyGems
+# will attempt to load the certificate chain from the trusted certificates.
+# Use <code>gem cert --add signing_cert.pem</code> to add your signers as
+# trusted certificates. See below for further information on certificate
+# chains.
+#
+# If you build your gem it will automatically be signed. If you peek inside
+# your gem file, you'll see a couple of new files have been added:
+#
+# $ tar tf your-gem-1.0.gem
+# metadata.gz
+# metadata.gz.sig # metadata signature
+# data.tar.gz
+# data.tar.gz.sig # data signature
+# checksums.yaml.gz
+# checksums.yaml.gz.sig # checksums signature
+#
+# === Manually signing gems
+#
+# If you wish to store your key in a separate secure location you'll need to
+# set your gems up for signing by hand. To do this, set the
+# <code>signing_key</code> and <code>cert_chain</code> in the gemspec before
+# packaging your gem:
+#
+# s.signing_key = '/secure/path/to/gem-private_key.pem'
+# s.cert_chain = %w[/secure/path/to/gem-public_cert.pem]
+#
+# When you package your gem with these options set RubyGems will automatically
+# load your key and certificate from the secure paths.
+#
+# === Signed gems and security policies
+#
+# Now let's verify the signature. Go ahead and install the gem, but add the
+# following options: <code>-P HighSecurity</code>, like this:
+#
+# # install the gem with using the security policy "HighSecurity"
+# $ sudo gem install your.gem -P HighSecurity
+#
+# The <code>-P</code> option sets your security policy -- we'll talk about
+# that in just a minute. Eh, what's this?
+#
+# $ gem install -P HighSecurity your-gem-1.0.gem
+# ERROR: While executing gem ... (Gem::Security::Exception)
+# root cert /CN=you/DC=example is not trusted
+#
+# The culprit here is the security policy. RubyGems has several different
+# security policies. Let's take a short break and go over the security
+# policies. Here's a list of the available security policies, and a brief
+# description of each one:
+#
+# * NoSecurity - Well, no security at all. Signed packages are treated like
+# unsigned packages.
+# * LowSecurity - Pretty much no security. If a package is signed then
+# RubyGems will make sure the signature matches the signing
+# certificate, and that the signing certificate hasn't expired, but
+# that's it. A malicious user could easily circumvent this kind of
+# security.
+# * MediumSecurity - Better than LowSecurity and NoSecurity, but still
+# fallible. Package contents are verified against the signing
+# certificate, and the signing certificate is checked for validity,
+# and checked against the rest of the certificate chain (if you don't
+# know what a certificate chain is, stay tuned, we'll get to that).
+# The biggest improvement over LowSecurity is that MediumSecurity
+# won't install packages that are signed by untrusted sources.
+# Unfortunately, MediumSecurity still isn't totally secure -- a
+# malicious user can still unpack the gem, strip the signatures, and
+# distribute the gem unsigned.
+# * HighSecurity - Here's the bugger that got us into this mess.
+# The HighSecurity policy is identical to the MediumSecurity policy,
+# except that it does not allow unsigned gems. A malicious user
+# doesn't have a whole lot of options here; they can't modify the
+# package contents without invalidating the signature, and they can't
+# modify or remove signature or the signing certificate chain, or
+# RubyGems will simply refuse to install the package. Oh well, maybe
+# they'll have better luck causing problems for CPAN users instead :).
+#
+# The reason RubyGems refused to install your shiny new signed gem was because
+# it was from an untrusted source. Well, your code is infallible (naturally),
+# so you need to add yourself as a trusted source:
+#
+# # add trusted certificate
+# gem cert --add ~/.gem/gem-public_cert.pem
+#
+# You've now added your public certificate as a trusted source. Now you can
+# install packages signed by your private key without any hassle. Let's try
+# the install command above again:
+#
+# # install the gem with using the HighSecurity policy (and this time
+# # without any shenanigans)
+# $ gem install -P HighSecurity your-gem-1.0.gem
+# Successfully installed your-gem-1.0
+# 1 gem installed
+#
+# This time RubyGems will accept your signed package and begin installing.
+#
+# While you're waiting for RubyGems to work it's magic, have a look at some of
+# the other security commands by running <code>gem help cert</code>:
+#
+# Options:
+# -a, --add CERT Add a trusted certificate.
+# -l, --list [FILTER] List trusted certificates where the
+# subject contains FILTER
+# -r, --remove FILTER Remove trusted certificates where the
+# subject contains FILTER
+# -b, --build EMAIL_ADDR Build private key and self-signed
+# certificate for EMAIL_ADDR
+# -C, --certificate CERT Signing certificate for --sign
+# -K, --private-key KEY Key for --sign or --build
+# -A, --key-algorithm ALGORITHM Select key algorithm for --build from RSA, DSA, or EC. Defaults to RSA.
+# -s, --sign CERT Signs CERT with the key from -K
+# and the certificate from -C
+# -d, --days NUMBER_OF_DAYS Days before the certificate expires
+# -R, --re-sign Re-signs the certificate from -C with the key from -K
+#
+# We've already covered the <code>--build</code> option, and the
+# <code>--add</code>, <code>--list</code>, and <code>--remove</code> commands
+# seem fairly straightforward; they allow you to add, list, and remove the
+# certificates in your trusted certificate list. But what's with this
+# <code>--sign</code> option?
+#
+# === Certificate chains
+#
+# To answer that question, let's take a look at "certificate chains", a
+# concept I mentioned earlier. There are a couple of problems with
+# self-signed certificates: first of all, self-signed certificates don't offer
+# a whole lot of security. Sure, the certificate says Yukihiro Matsumoto, but
+# how do I know it was actually generated and signed by matz himself unless he
+# gave me the certificate in person?
+#
+# The second problem is scalability. Sure, if there are 50 gem authors, then
+# I have 50 trusted certificates, no problem. What if there are 500 gem
+# authors? 1000? Having to constantly add new trusted certificates is a
+# pain, and it actually makes the trust system less secure by encouraging
+# RubyGems users to blindly trust new certificates.
+#
+# Here's where certificate chains come in. A certificate chain establishes an
+# arbitrarily long chain of trust between an issuing certificate and a child
+# certificate. So instead of trusting certificates on a per-developer basis,
+# we use the PKI concept of certificate chains to build a logical hierarchy of
+# trust. Here's a hypothetical example of a trust hierarchy based (roughly)
+# on geography:
+#
+# --------------------------
+# | rubygems@rubygems.org |
+# --------------------------
+# |
+# -----------------------------------
+# | |
+# ---------------------------- -----------------------------
+# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com |
+# ---------------------------- -----------------------------
+# | | | |
+# --------------- ---------------- ----------- --------------
+# | drbrain | | zenspider | | pabs@dc | | tomcope@dc |
+# --------------- ---------------- ----------- --------------
+#
+#
+# Now, rather than having 4 trusted certificates (one for drbrain, zenspider,
+# pabs@dc, and tomecope@dc), a user could actually get by with one
+# certificate, the "rubygems@rubygems.org" certificate.
+#
+# Here's how it works:
+#
+# I install "rdoc-3.12.gem", a package signed by "drbrain". I've never heard
+# of "drbrain", but his certificate has a valid signature from the
+# "seattle.rb@seattlerb.org" certificate, which in turn has a valid signature
+# from the "rubygems@rubygems.org" certificate. Voila! At this point, it's
+# much more reasonable for me to trust a package signed by "drbrain", because
+# I can establish a chain to "rubygems@rubygems.org", which I do trust.
+#
+# === Signing certificates
+#
+# The <code>--sign</code> option allows all this to happen. A developer
+# creates their build certificate with the <code>--build</code> option, then
+# has their certificate signed by taking it with them to their next regional
+# Ruby meetup (in our hypothetical example), and it's signed there by the
+# person holding the regional RubyGems signing certificate, which is signed at
+# the next RubyConf by the holder of the top-level RubyGems certificate. At
+# each point the issuer runs the same command:
+#
+# # sign a certificate with the specified key and certificate
+# # (note that this modifies client_cert.pem!)
+# $ gem cert -K /mnt/floppy/issuer-priv_key.pem -C issuer-pub_cert.pem
+# --sign client_cert.pem
+#
+# Then the holder of issued certificate (in this case, your buddy "drbrain"),
+# can start using this signed certificate to sign RubyGems. By the way, in
+# order to let everyone else know about his new fancy signed certificate,
+# "drbrain" would save his newly signed certificate as
+# <code>~/.gem/gem-public_cert.pem</code>
+#
+# Obviously this RubyGems trust infrastructure doesn't exist yet. Also, in
+# the "real world", issuers actually generate the child certificate from a
+# certificate request, rather than sign an existing certificate. And our
+# hypothetical infrastructure is missing a certificate revocation system.
+# These are that can be fixed in the future...
+#
+# At this point you should know how to do all of these new and interesting
+# things:
+#
+# * build a gem signing key and certificate
+# * adjust your security policy
+# * modify your trusted certificate list
+# * sign a certificate
+#
+# == Manually verifying signatures
+#
+# In case you don't trust RubyGems you can verify gem signatures manually:
+#
+# 1. Fetch and unpack the gem
+#
+# gem fetch some_signed_gem
+# tar -xf some_signed_gem-1.0.gem
+#
+# 2. Grab the public key from the gemspec
+#
+# gem spec some_signed_gem-1.0.gem cert_chain | \
+# ruby -rpsych -e 'puts Psych.load($stdin)' > public_key.crt
+#
+# 3. Generate a SHA1 hash of the data.tar.gz
+#
+# openssl dgst -sha1 < data.tar.gz > my.hash
+#
+# 4. Verify the signature
+#
+# openssl rsautl -verify -inkey public_key.crt -certin \
+# -in data.tar.gz.sig > verified.hash
+#
+# 5. Compare your hash to the verified hash
+#
+# diff -s verified.hash my.hash
+#
+# 6. Repeat 5 and 6 with metadata.gz
+#
+# == OpenSSL Reference
+#
+# The .pem files generated by --build and --sign are PEM files. Here's a
+# couple of useful OpenSSL commands for manipulating them:
+#
+# # convert a PEM format X509 certificate into DER format:
+# # (note: Windows .cer files are X509 certificates in DER format)
+# $ openssl x509 -in input.pem -outform der -out output.der
+#
+# # print out the certificate in a human-readable format:
+# $ openssl x509 -in input.pem -noout -text
+#
+# And you can do the same thing with the private key file as well:
+#
+# # convert a PEM format RSA key into DER format:
+# $ openssl rsa -in input_key.pem -outform der -out output_key.der
+#
+# # print out the key in a human readable format:
+# $ openssl rsa -in input_key.pem -noout -text
+#
+# == Bugs/TODO
+#
+# * There's no way to define a system-wide trust list.
+# * custom security policies (from a YAML file, etc)
+# * Simple method to generate a signed certificate request
+# * Support for OCSP, SCVP, CRLs, or some other form of cert status check
+# (list is in order of preference)
+# * Support for encrypted private keys
+# * Some sort of semi-formal trust hierarchy (see long-winded explanation
+# above)
+# * Path discovery (for gem certificate chains that don't have a self-signed
+# root) -- by the way, since we don't have this, THE ROOT OF THE CERTIFICATE
+# CHAIN MUST BE SELF SIGNED if Policy#verify_root is true (and it is for the
+# MediumSecurity and HighSecurity policies)
+# * Better explanation of X509 naming (ie, we don't have to use email
+# addresses)
+# * Honor AIA field (see note about OCSP above)
+# * Honor extension restrictions
+# * Might be better to store the certificate chain as a PKCS#7 or PKCS#12
+# file, instead of an array embedded in the metadata.
+#
+# == Original author
+#
+# Paul Duncan <pabs@pablotron.org>
+# https://pablotron.org/
+
+module Gem::Security
+ ##
+ # Gem::Security default exception type
+
+ class Exception < Gem::Exception; end
+
+ ##
+ # Used internally to select the signing digest from all computed digests
+
+ DIGEST_NAME = "SHA256" # :nodoc:
+
+ ##
+ # Length of keys created by RSA and DSA keys
+
+ RSA_DSA_KEY_LENGTH = 3072
+
+ ##
+ # Default algorithm to use when building a key pair
+
+ DEFAULT_KEY_ALGORITHM = "RSA"
+
+ ##
+ # Named curve used for Elliptic Curve
+
+ EC_NAME = "secp384r1"
+
+ ##
+ # Cipher used to encrypt the key pair used to sign gems.
+ # Must be in the list returned by OpenSSL::Cipher.ciphers
+
+ KEY_CIPHER = OpenSSL::Cipher.new("AES-256-CBC") if defined?(OpenSSL::Cipher)
+
+ ##
+ # One day in seconds
+
+ ONE_DAY = 86_400
+
+ ##
+ # One year in seconds
+
+ ONE_YEAR = ONE_DAY * 365
+
+ ##
+ # The default set of extensions are:
+ #
+ # * The certificate is not a certificate authority
+ # * The key for the certificate may be used for key and data encipherment
+ # and digital signatures
+ # * The certificate contains a subject key identifier
+
+ EXTENSIONS = {
+ "basicConstraints" => "CA:FALSE",
+ "keyUsage" =>
+ "keyEncipherment,dataEncipherment,digitalSignature",
+ "subjectKeyIdentifier" => "hash",
+ }.freeze
+
+ def self.alt_name_or_x509_entry(certificate, x509_entry)
+ alt_name = certificate.extensions.find do |extension|
+ extension.oid == "#{x509_entry}AltName"
+ end
+
+ return alt_name.value if alt_name
+
+ certificate.send x509_entry
+ end
+
+ ##
+ # Creates an unsigned certificate for +subject+ and +key+. The lifetime of
+ # the key is from the current time to +age+ which defaults to one year.
+ #
+ # The +extensions+ restrict the key to the indicated uses.
+
+ 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)
+ cert.version = 2
+ cert.serial = serial
+
+ cert.not_before = Time.now
+ cert.not_after = Time.now + age
+
+ cert.subject = subject
+
+ ef = OpenSSL::X509::ExtensionFactory.new nil, cert
+
+ cert.extensions = extensions.map do |ext_name, value|
+ ef.create_extension ext_name, value
+ end
+
+ cert
+ end
+
+ ##
+ # Gets the right public key from a PKey instance
+
+ def self.get_public_key(key)
+ # Ruby 3.0 (Ruby/OpenSSL 2.2) or later
+ return OpenSSL::PKey.read(key.public_to_der) if key.respond_to?(:public_to_der)
+ return key.public_key unless key.is_a?(OpenSSL::PKey::EC)
+
+ ec_key = OpenSSL::PKey::EC.new(key.group.curve_name)
+ ec_key.public_key = key.public_key
+ ec_key
+ end
+
+ ##
+ # Creates a self-signed certificate with an issuer and subject from +email+,
+ # a subject alternative name of +email+ and the given +extensions+ for the
+ # +key+.
+
+ def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS)
+ subject = email_to_name email
+
+ extensions = extensions.merge "subjectAltName" => "email:#{email}"
+
+ create_cert_self_signed subject, key, age, extensions
+ end
+
+ ##
+ # 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)
+ certificate = create_cert subject, key, age, extensions
+
+ sign certificate, key, certificate, age, extensions, serial
+ end
+
+ ##
+ # Creates a new digest instance using the specified +algorithm+. The default
+ # is SHA256.
+
+ def self.create_digest(algorithm = DIGEST_NAME)
+ OpenSSL::Digest.new(algorithm)
+ end
+
+ ##
+ # Creates a new key pair of the specified +algorithm+. RSA, DSA, and EC
+ # are supported.
+
+ def self.create_key(algorithm)
+ if defined?(OpenSSL::PKey)
+ case algorithm.downcase
+ when "dsa"
+ OpenSSL::PKey::DSA.new(RSA_DSA_KEY_LENGTH)
+ when "rsa"
+ OpenSSL::PKey::RSA.new(RSA_DSA_KEY_LENGTH)
+ when "ec"
+ OpenSSL::PKey::EC.generate(EC_NAME)
+ else
+ raise Gem::Security::Exception,
+ "#{algorithm} algorithm not found. RSA, DSA, and EC algorithms are supported."
+ end
+ end
+ end
+
+ ##
+ # Turns +email_address+ into an OpenSSL::X509::Name
+
+ def self.email_to_name(email_address)
+ email_address = email_address.gsub(/[^\w@.-]+/i, "_")
+
+ cn, dcs = email_address.split "@"
+
+ dcs = dcs.split "."
+
+ OpenSSL::X509::Name.new([
+ ["CN", cn],
+ *dcs.map {|dc| ["DC", dc] },
+ ])
+ end
+
+ ##
+ # Signs +expired_certificate+ with +private_key+ if the keys match and the
+ # expired certificate was self-signed.
+ #--
+ # TODO increment serial
+
+ 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.to_s unless
+ expired_certificate.check_private_key(private_key)
+
+ unless expired_certificate.subject.to_s ==
+ expired_certificate.issuer.to_s
+ subject = alt_name_or_x509_entry expired_certificate, :subject
+ issuer = alt_name_or_x509_entry expired_certificate, :issuer
+
+ raise Gem::Security::Exception,
+ "#{subject} is not self-signed, contact #{issuer} " \
+ "to obtain a valid certificate"
+ end
+
+ serial = expired_certificate.serial + 1
+
+ create_cert_self_signed(expired_certificate.subject, private_key, age,
+ extensions, serial)
+ end
+
+ ##
+ # Resets the trust directory for verifying gems.
+
+ def self.reset
+ @trust_dir = nil
+ end
+
+ ##
+ # Sign the public key from +certificate+ with the +signing_key+ and
+ # +signing_cert+, using the Gem::Security::DIGEST_NAME. Uses the
+ # default certificate validity range and extensions.
+ #
+ # Returns the newly signed certificate.
+
+ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ signee_subject = certificate.subject
+ signee_key = certificate.public_key
+
+ alt_name = certificate.extensions.find do |extension|
+ extension.oid == "subjectAltName"
+ end
+
+ extensions = extensions.merge "subjectAltName" => alt_name.value if
+ alt_name
+
+ issuer_alt_name = signing_cert.extensions.find do |extension|
+ extension.oid == "subjectAltName"
+ end
+
+ extensions = extensions.merge "issuerAltName" => issuer_alt_name.value if
+ issuer_alt_name
+
+ signed = create_cert signee_subject, signee_key, age, extensions, serial
+ signed.issuer = signing_cert.subject
+
+ signed.sign signing_key, Gem::Security::DIGEST_NAME
+ end
+
+ ##
+ # Returns a Gem::Security::TrustDir which wraps the directory where trusted
+ # certificates live.
+
+ def self.trust_dir
+ return @trust_dir if @trust_dir
+
+ dir = File.join Gem.user_home, ".gem", "trust"
+
+ @trust_dir ||= Gem::Security::TrustDir.new dir
+ end
+
+ ##
+ # Enumerates the trusted certificates via Gem::Security::TrustDir.
+
+ def self.trusted_certificates(&block)
+ trust_dir.each_certificate(&block)
+ end
+
+ ##
+ # Writes +pemmable+, which must respond to +to_pem+ to +path+ with the given
+ # +permissions+. If passed +cipher+ and +passphrase+ those arguments will be
+ # passed to +to_pem+.
+
+ def self.write(pemmable, path, permissions = 0o600, passphrase = nil, cipher = KEY_CIPHER)
+ path = File.expand_path path
+
+ File.open path, "wb", permissions do |io|
+ if passphrase && cipher
+ io.write pemmable.to_pem cipher, passphrase
+ else
+ io.write pemmable.to_pem
+ end
+ end
+
+ path
+ end
+
+ reset
+end
+
+if Gem::HAVE_OPENSSL
+ require_relative "security/policy"
+ require_relative "security/policies"
+ require_relative "security/trust_dir"
+end
+
+require_relative "security/signer"
diff --git a/lib/rubygems/security/policies.rb b/lib/rubygems/security/policies.rb
new file mode 100644
index 0000000000..41f66043ad
--- /dev/null
+++ b/lib/rubygems/security/policies.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+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
+ )
+
+ ##
+ # AlmostNo security policy: only verify that the signing certificate is the
+ # one that actually signed the data. Make no attempt to verify the signing
+ # certificate chain.
+ #
+ # This policy is basically useless. better than nothing, but can still be
+ # easily spoofed, and is not recommended.
+
+ AlmostNoSecurity = Policy.new(
+ "Almost No Security",
+ verify_data: true,
+ verify_signer: false,
+ verify_chain: false,
+ verify_root: false,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Low security policy: only verify that the signing certificate is actually
+ # the gem signer, and that the signing certificate is valid.
+ #
+ # This policy is better than nothing, but can still be easily spoofed, and
+ # is not recommended.
+
+ LowSecurity = Policy.new(
+ "Low Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: false,
+ verify_root: false,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Medium security policy: verify the signing certificate, verify the signing
+ # certificate chain all the way to the root certificate, and only trust root
+ # certificates that we have explicitly allowed trust for.
+ #
+ # This security policy is reasonable, but it allows unsigned packages, so a
+ # malicious person could simply delete the package signature and pass the
+ # gem off as unsigned.
+
+ MediumSecurity = Policy.new(
+ "Medium Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: true,
+ only_signed: false
+ )
+
+ ##
+ # High security policy: only allow signed gems to be installed, verify the
+ # signing certificate, verify the signing certificate chain all the way to
+ # the root certificate, and only trust root certificates that we have
+ # explicitly allowed trust for.
+ #
+ # This security policy is significantly more difficult to bypass, and offers
+ # a reasonable guarantee that the contents of the gem have not been altered.
+
+ HighSecurity = Policy.new(
+ "High Security",
+ verify_data: true,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: true,
+ only_signed: true
+ )
+
+ ##
+ # Policy used to verify a certificate and key when signing a gem
+
+ SigningPolicy = Policy.new(
+ "Signing Policy",
+ verify_data: false,
+ verify_signer: true,
+ verify_chain: true,
+ verify_root: true,
+ only_trusted: false,
+ only_signed: false
+ )
+
+ ##
+ # Hash of configured security policies
+
+ Policies = {
+ "NoSecurity" => NoSecurity,
+ "AlmostNoSecurity" => AlmostNoSecurity,
+ "LowSecurity" => LowSecurity,
+ "MediumSecurity" => MediumSecurity,
+ "HighSecurity" => HighSecurity,
+ # SigningPolicy is not intended for use by `gem -P` so do not list it
+ }.freeze
+end
diff --git a/lib/rubygems/security/policy.rb b/lib/rubygems/security/policy.rb
new file mode 100644
index 0000000000..128958ab80
--- /dev/null
+++ b/lib/rubygems/security/policy.rb
@@ -0,0 +1,288 @@
+# frozen_string_literal: true
+
+require_relative "../user_interaction"
+
+##
+# A Gem::Security::Policy object encapsulates the settings for verifying
+# signed gem files. This is the base class. You can either declare an
+# instance of this or use one of the preset security policies in
+# Gem::Security::Policies.
+
+class Gem::Security::Policy
+ include Gem::UserInteraction
+
+ attr_reader :name
+
+ attr_accessor :only_signed
+ attr_accessor :only_trusted
+ attr_accessor :verify_chain
+ attr_accessor :verify_data
+ attr_accessor :verify_root
+ attr_accessor :verify_signer
+
+ ##
+ # Create a new Gem::Security::Policy object with the given mode and
+ # options.
+
+ def initialize(name, policy = {}, opt = {})
+ @name = name
+
+ @opt = opt
+
+ # Default to security
+ @only_signed = true
+ @only_trusted = true
+ @verify_chain = true
+ @verify_data = true
+ @verify_root = true
+ @verify_signer = true
+
+ policy.each_pair do |key, val|
+ case key
+ when :verify_data then @verify_data = val
+ when :verify_signer then @verify_signer = val
+ when :verify_chain then @verify_chain = val
+ when :verify_root then @verify_root = val
+ when :only_trusted then @only_trusted = val
+ when :only_signed then @only_signed = val
+ end
+ end
+ end
+
+ ##
+ # Verifies each certificate in +chain+ has signed the following certificate
+ # and is valid for the given +time+.
+
+ def check_chain(chain, time)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+ raise Gem::Security::Exception, "empty signing chain" if chain.empty?
+
+ begin
+ chain.each_cons 2 do |issuer, cert|
+ check_cert cert, issuer, time
+ end
+
+ true
+ rescue Gem::Security::Exception => e
+ raise Gem::Security::Exception, "invalid signing chain: #{e.message}"
+ end
+ end
+
+ ##
+ # Verifies that +data+ matches the +signature+ created by +public_key+ and
+ # the +digest+ algorithm.
+
+ def check_data(public_key, digest, signature, data)
+ raise Gem::Security::Exception, "invalid signature" unless
+ public_key.verify digest, signature, data.digest
+
+ true
+ end
+
+ ##
+ # Ensures that +signer+ is valid for +time+ and was signed by the +issuer+.
+ # If the +issuer+ is +nil+ no verification is performed.
+
+ def check_cert(signer, issuer, time)
+ raise Gem::Security::Exception, "missing signing certificate" unless
+ signer
+
+ message = "certificate #{signer.subject}"
+
+ if (not_before = signer.not_before) && not_before > time
+ raise Gem::Security::Exception,
+ "#{message} not valid before #{not_before}"
+ end
+
+ if (not_after = signer.not_after) && not_after < time
+ raise Gem::Security::Exception, "#{message} not valid after #{not_after}"
+ end
+
+ if issuer && !signer.verify(issuer.public_key)
+ raise Gem::Security::Exception,
+ "#{message} was not issued by #{issuer.subject}"
+ end
+
+ true
+ end
+
+ ##
+ # Ensures the public key of +key+ matches the public key in +signer+
+
+ def check_key(signer, key)
+ unless signer && key
+ return true unless @only_signed
+
+ raise Gem::Security::Exception, "missing key or signature"
+ end
+
+ raise Gem::Security::Exception,
+ "certificate #{signer.subject} does not match the signing key" unless
+ signer.check_private_key(key)
+
+ true
+ end
+
+ ##
+ # Ensures the root certificate in +chain+ is self-signed and valid for
+ # +time+.
+
+ def check_root(chain, time)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, "missing root certificate" unless root
+
+ raise Gem::Security::Exception,
+ "root certificate #{root.subject} is not self-signed " \
+ "(issuer #{root.issuer})" if
+ root.issuer != root.subject
+
+ check_cert root, root, time
+ end
+
+ ##
+ # Ensures the root of +chain+ has a trusted certificate in Gem::Security.trust_dir and
+ # the digests of the two certificates match according to +digester+
+
+ def check_trust(chain, digester, trust_dir)
+ raise Gem::Security::Exception, "missing signing chain" unless chain
+
+ root = chain.first
+
+ raise Gem::Security::Exception, "missing root certificate" unless root
+
+ path = Gem::Security.trust_dir.cert_path root
+
+ unless File.exist? path
+ message = "root cert #{root.subject} is not trusted".dup
+
+ message << " (root of signing cert #{chain.last.subject})" if
+ chain.length > 1
+
+ raise Gem::Security::Exception, message
+ end
+
+ save_cert = OpenSSL::X509::Certificate.new File.read path
+ save_dgst = digester.digest save_cert.public_key.to_pem
+
+ pkey_str = root.public_key.to_pem
+ cert_dgst = digester.digest pkey_str
+
+ raise Gem::Security::Exception,
+ "trusted root certificate #{root.subject} checksum " \
+ "does not match signing root certificate checksum" unless
+ save_dgst == cert_dgst
+
+ true
+ end
+
+ ##
+ # Extracts the email or subject from +certificate+
+
+ def subject(certificate) # :nodoc:
+ certificate.extensions.each do |extension|
+ next unless extension.oid == "subjectAltName"
+
+ return extension.value
+ end
+
+ certificate.subject.to_s
+ end
+
+ def inspect # :nodoc:
+ 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
+
+ ##
+ # For +full_name+, verifies the certificate +chain+ is valid, the +digests+
+ # match the signatures +signatures+ created by the signer depending on the
+ # +policy+ settings.
+ #
+ # If +key+ is given it is used to validate the signing certificate.
+
+ def verify(chain, key = nil, digests = {}, signatures = {}, full_name = "(unknown)")
+ if signatures.empty?
+ if @only_signed
+ raise Gem::Security::Exception,
+ "unsigned gems are not allowed by the #{name} policy"
+ elsif digests.empty?
+ # lack of signatures is irrelevant if there is nothing to check
+ # against
+ else
+ alert_warning "#{full_name} is not signed"
+ return
+ end
+ end
+
+ opt = @opt
+ digester = Gem::Security.create_digest
+ trust_dir = opt[:trust_dir]
+ time = Time.now
+
+ _, signer_digests = digests.find do |_algorithm, file_digests|
+ file_digests.values.first.name == Gem::Security::DIGEST_NAME
+ end
+
+ if @verify_data
+ raise Gem::Security::Exception, "no digests provided (probable bug)" if
+ signer_digests.nil? || signer_digests.empty?
+ else
+ signer_digests = {}
+ end
+
+ signer = chain.last
+
+ check_key signer, key if key
+
+ check_cert signer, nil, time if @verify_signer
+
+ check_chain chain, time if @verify_chain
+
+ check_root chain, time if @verify_root
+
+ if @only_trusted
+ check_trust chain, digester, trust_dir
+ elsif signatures.empty? && digests.empty?
+ # trust is irrelevant if there's no signatures to verify
+ else
+ alert_warning "#{subject signer} is not trusted for #{full_name}"
+ end
+
+ signatures.each do |file, _|
+ digest = signer_digests[file]
+
+ raise Gem::Security::Exception, "missing digest for #{file}" unless
+ digest
+ end
+
+ signer_digests.each do |file, digest|
+ signature = signatures[file]
+
+ raise Gem::Security::Exception, "missing signature for #{file}" unless
+ signature
+
+ check_data signer.public_key, digester, signature, digest if @verify_data
+ end
+
+ true
+ end
+
+ ##
+ # Extracts the certificate chain from the +spec+ and calls #verify to ensure
+ # the signatures and certificate chain is valid according to the policy..
+
+ def verify_signatures(spec, digests, signatures)
+ chain = spec.cert_chain.map do |cert_pem|
+ OpenSSL::X509::Certificate.new cert_pem
+ end
+
+ verify chain, nil, digests, signatures, spec.full_name
+
+ true
+ end
+
+ alias_method :to_s, :name # :nodoc:
+end
diff --git a/lib/rubygems/security/signer.rb b/lib/rubygems/security/signer.rb
new file mode 100644
index 0000000000..eeeeb52906
--- /dev/null
+++ b/lib/rubygems/security/signer.rb
@@ -0,0 +1,212 @@
+# frozen_string_literal: true
+
+##
+# Basic OpenSSL-based package signing class.
+
+require_relative "../user_interaction"
+
+class Gem::Security::Signer
+ include Gem::UserInteraction
+
+ ##
+ # The chain of certificates for signing including the signing certificate
+
+ attr_accessor :cert_chain
+
+ ##
+ # The private key for the signing certificate
+
+ attr_accessor :key
+
+ ##
+ # The digest algorithm used to create the signature
+
+ attr_reader :digest_algorithm
+
+ ##
+ # The name of the digest algorithm, used to pull digests out of the hash by
+ # name.
+
+ attr_reader :digest_name # :nodoc:
+
+ ##
+ # Gem::Security::Signer options
+
+ attr_reader :options
+
+ DEFAULT_OPTIONS = {
+ expiration_length_days: 365,
+ }.freeze
+
+ ##
+ # Attempts to re-sign an expired cert with a given private key
+ def self.re_sign_cert(expired_cert, expired_cert_path, private_key)
+ return unless expired_cert.not_after < Time.now
+
+ expiry = expired_cert.not_after.strftime("%Y%m%d%H%M%S")
+ expired_cert_file = "#{File.basename(expired_cert_path)}.expired.#{expiry}"
+ new_expired_cert_path = File.join(Gem.user_home, ".gem", expired_cert_file)
+
+ Gem::Security.write(expired_cert, new_expired_cert_path)
+
+ re_signed_cert = Gem::Security.re_sign(
+ expired_cert,
+ private_key,
+ Gem::Security::ONE_DAY * Gem.configuration.cert_expiration_length_days
+ )
+
+ Gem::Security.write(re_signed_cert, expired_cert_path)
+
+ yield(expired_cert_path, new_expired_cert_path) if block_given?
+ end
+
+ ##
+ # Creates a new signer with an RSA +key+ or path to a key, and a certificate
+ # +chain+ containing X509 certificates, encoding certificates or paths to
+ # certificates.
+
+ def initialize(key, cert_chain, passphrase = nil, options = {})
+ @cert_chain = cert_chain
+ @key = key
+ @passphrase = passphrase
+ @options = DEFAULT_OPTIONS.merge(options)
+
+ unless @key
+ default_key = File.join Gem.default_key_path
+ @key = default_key if File.exist? default_key
+ end
+
+ unless @cert_chain
+ default_cert = File.join Gem.default_cert_path
+ @cert_chain = [default_cert] if File.exist? default_cert
+ end
+
+ @digest_name = Gem::Security::DIGEST_NAME
+ @digest_algorithm = Gem::Security.create_digest(@digest_name)
+
+ if @key && !@key.is_a?(OpenSSL::PKey::PKey)
+ @key = OpenSSL::PKey.read(File.read(@key), @passphrase)
+ end
+
+ if @cert_chain
+ @cert_chain = @cert_chain.compact.map do |cert|
+ next cert if OpenSSL::X509::Certificate === cert
+
+ cert = File.read cert if File.exist? cert
+
+ OpenSSL::X509::Certificate.new cert
+ end
+
+ load_cert_chain
+ end
+ end
+
+ ##
+ # Extracts the full name of +cert+. If the certificate has a subjectAltName
+ # this value is preferred, otherwise the subject is used.
+
+ def extract_name(cert) # :nodoc:
+ subject_alt_name = cert.extensions.find {|e| e.oid == "subjectAltName" }
+
+ if subject_alt_name
+ /\Aemail:/ =~ subject_alt_name.value # rubocop:disable Performance/StartWith
+
+ $' || subject_alt_name.value
+ else
+ cert.subject
+ end
+ end
+
+ ##
+ # Loads any missing issuers in the cert chain from the trusted certificates.
+ #
+ # If the issuer does not exist it is ignored as it will be checked later.
+
+ def load_cert_chain # :nodoc:
+ return if @cert_chain.empty?
+
+ while @cert_chain.first.issuer.to_s != @cert_chain.first.subject.to_s do
+ issuer = Gem::Security.trust_dir.issuer_of @cert_chain.first
+
+ break unless issuer # cert chain is verified later
+
+ @cert_chain.unshift issuer
+ end
+ end
+
+ ##
+ # Sign data with given digest algorithm
+
+ def sign(data)
+ return unless @key
+
+ raise Gem::Security::Exception, "no certs provided" if @cert_chain.empty?
+
+ if @cert_chain.length == 1 && @cert_chain.last.not_after < Time.now
+ alert("Your certificate has expired, trying to re-sign it...")
+
+ re_sign_key(
+ expiration_length: (Gem::Security::ONE_DAY * options[:expiration_length_days])
+ )
+ end
+
+ full_name = extract_name @cert_chain.last
+
+ Gem::Security::SigningPolicy.verify @cert_chain, @key, {}, {}, full_name
+
+ @key.sign @digest_algorithm.new, data
+ end
+
+ ##
+ # Attempts to re-sign the private key if the signing certificate is expired.
+ #
+ # The key will be re-signed if:
+ # * The expired certificate is self-signed
+ # * The expired certificate is saved at ~/.gem/gem-public_cert.pem
+ # and the private key is saved at ~/.gem/gem-private_key.pem
+ # * There is no file matching the expiry date at
+ # ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S
+ #
+ # If the signing certificate can be re-signed the expired certificate will
+ # be saved as ~/.gem/gem-public_cert.pem.expired.%Y%m%d%H%M%S where the
+ # expiry time (not after) is used for the timestamp.
+
+ def re_sign_key(expiration_length: Gem::Security::ONE_YEAR) # :nodoc:
+ old_cert = @cert_chain.last
+
+ disk_cert_path = File.join(Gem.default_cert_path)
+ disk_cert = begin
+ File.read(disk_cert_path)
+ rescue StandardError
+ nil
+ end
+
+ disk_key_path = File.join(Gem.default_key_path)
+ disk_key = begin
+ OpenSSL::PKey.read(File.read(disk_key_path), @passphrase)
+ rescue StandardError
+ nil
+ end
+
+ return unless disk_key
+
+ if disk_key.to_pem == @key.to_pem && disk_cert == old_cert.to_pem
+ expiry = old_cert.not_after.strftime("%Y%m%d%H%M%S")
+ old_cert_file = "gem-public_cert.pem.expired.#{expiry}"
+ old_cert_path = File.join(Gem.user_home, ".gem", old_cert_file)
+
+ unless File.exist?(old_cert_path)
+ Gem::Security.write(old_cert, old_cert_path)
+
+ cert = Gem::Security.re_sign(old_cert, @key, expiration_length)
+
+ Gem::Security.write(cert, disk_cert_path)
+
+ alert("Your cert: #{disk_cert_path} has been auto re-signed with the key: #{disk_key_path}")
+ alert("Your expired cert will be located at: #{old_cert_path}")
+
+ @cert_chain = [cert]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/security/trust_dir.rb b/lib/rubygems/security/trust_dir.rb
new file mode 100644
index 0000000000..d23d161cfe
--- /dev/null
+++ b/lib/rubygems/security/trust_dir.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+##
+# The TrustDir manages the trusted certificates for gem signature
+# verification.
+
+class Gem::Security::TrustDir
+ ##
+ # Default permissions for the trust directory and its contents
+
+ DEFAULT_PERMISSIONS = {
+ trust_dir: 0o700,
+ trusted_cert: 0o600,
+ }.freeze
+
+ ##
+ # The directory where trusted certificates will be stored.
+
+ attr_reader :dir
+
+ ##
+ # Creates a new TrustDir using +dir+ where the directory and file
+ # permissions will be checked according to +permissions+
+
+ def initialize(dir, permissions = DEFAULT_PERMISSIONS)
+ @dir = dir
+ @permissions = permissions
+
+ @digester = Gem::Security.create_digest
+ end
+
+ ##
+ # Returns the path to the trusted +certificate+
+
+ def cert_path(certificate)
+ name_path certificate.subject
+ end
+
+ ##
+ # Enumerates trusted certificates.
+
+ def each_certificate
+ return enum_for __method__ unless block_given?
+
+ glob = File.join @dir, "*.pem"
+
+ Dir[glob].each do |certificate_file|
+ certificate = load_certificate certificate_file
+
+ yield certificate, certificate_file
+ rescue OpenSSL::X509::CertificateError
+ next # HACK: warn
+ end
+ end
+
+ ##
+ # Returns the issuer certificate of the given +certificate+ if it exists in
+ # the trust directory.
+
+ def issuer_of(certificate)
+ path = name_path certificate.issuer
+
+ return unless File.exist? path
+
+ load_certificate path
+ end
+
+ ##
+ # Returns the path to the trusted certificate with the given ASN.1 +name+
+
+ def name_path(name)
+ digest = @digester.hexdigest name.to_s
+
+ File.join @dir, "cert-#{digest}.pem"
+ end
+
+ ##
+ # Loads the given +certificate_file+
+
+ def load_certificate(certificate_file)
+ pem = File.read certificate_file
+
+ OpenSSL::X509::Certificate.new pem
+ end
+
+ ##
+ # Add a certificate to trusted certificate list.
+
+ def trust_cert(certificate)
+ verify
+
+ destination = cert_path certificate
+
+ File.open destination, "wb", 0o600 do |io|
+ io.write certificate.to_pem
+ io.chmod(@permissions[:trusted_cert])
+ end
+ end
+
+ ##
+ # Make sure the trust directory exists. If it does exist, make sure it's
+ # actually a directory. If not, then create it with the appropriate
+ # permissions.
+
+ def verify
+ require "fileutils"
+ if File.exist? @dir
+ raise Gem::Security::Exception,
+ "trust directory #{@dir} is not a directory" unless
+ File.directory? @dir
+
+ FileUtils.chmod 0o700, @dir
+ else
+ 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
new file mode 100644
index 0000000000..3a101fe9db
--- /dev/null
+++ b/lib/rubygems/security_option.rb
@@ -0,0 +1,43 @@
+# 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 "../rubygems"
+
+# forward-declare
+
+module Gem::Security # :nodoc:
+ class Policy # :nodoc:
+ end
+end
+
+##
+# Mixin methods for security option for Gem::Commands
+
+module Gem::SecurityOption
+ def add_security_option
+ Gem::OptionParser.accept Gem::Security::Policy do |value|
+ require_relative "security"
+
+ raise Gem::OptionParser::InvalidArgument, "OpenSSL not installed" unless
+ defined?(Gem::Security::HighSecurity)
+
+ policy = Gem::Security::Policies[value]
+ unless policy
+ valid = Gem::Security::Policies.keys.sort
+ raise Gem::OptionParser::InvalidArgument, "#{value} (#{valid.join ", "} are valid)"
+ end
+ policy
+ end
+
+ add_option(:"Install/Update", "-P", "--trust-policy POLICY",
+ Gem::Security::Policy,
+ "Specify gem trust policy") do |value, options|
+ options[:security_policy] = value
+ end
+ end
+end
diff --git a/lib/rubygems/source.rb b/lib/rubygems/source.rb
new file mode 100644
index 0000000000..86717e3e71
--- /dev/null
+++ b/lib/rubygems/source.rb
@@ -0,0 +1,253 @@
+# frozen_string_literal: true
+
+require_relative "text"
+##
+# A Source knows how to list and fetch gems from a RubyGems marshal index.
+#
+# There are other Source subclasses for installed gems, local gems, the
+# Compact Index API and so-forth.
+
+class Gem::Source
+ include Comparable
+ include Gem::Text
+
+ FILES = { # :nodoc:
+ released: "specs",
+ latest: "latest_specs",
+ prerelease: "prerelease_specs",
+ }.freeze
+
+ ##
+ # The URI this source will fetch gems from.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new Source which will use the index located at +uri+.
+
+ def initialize(uri)
+ require_relative "uri"
+ @uri = Gem::Uri.parse!(uri)
+ @update_cache = nil
+ end
+
+ ##
+ # Sources are ordered by installation preference.
+
+ def <=>(other)
+ case other
+ when Gem::Source::Installed,
+ Gem::Source::Local,
+ Gem::Source::Lock,
+ Gem::Source::SpecificFile,
+ Gem::Source::Git,
+ Gem::Source::Vendor then
+ -1
+ when Gem::Source then
+ unless @uri
+ return 0 unless other.uri
+ return 1
+ end
+
+ 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
+ end
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other && @uri == other.uri
+ end
+
+ alias_method :eql?, :== # :nodoc:
+
+ ##
+ # Returns a Set that can fetch specifications from this source.
+ #
+ # The set will optionally fetch prereleases if requested.
+ #
+ def dependency_resolver_set(prerelease = false)
+ new_dependency_resolver_set.tap {|set| set.prerelease = prerelease }
+ end
+
+ def hash # :nodoc:
+ @uri.hash
+ end
+
+ ##
+ # Returns the local directory to write +uri+ to.
+
+ def cache_dir(uri)
+ # Correct for windows paths
+ 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
+
+ ##
+ # Returns true when it is possible and safe to update the cache directory.
+
+ def update_cache?
+ return @update_cache unless @update_cache.nil?
+ @update_cache =
+ begin
+ File.stat(Gem.user_home).uid == Process.uid
+ rescue Errno::ENOENT
+ false
+ end
+ end
+
+ ##
+ # Fetches a specification for the given Gem::NameTuple.
+
+ def fetch_spec(name_tuple)
+ fetcher = Gem::RemoteFetcher.fetcher
+
+ spec_file_name = name_tuple.spec_name
+
+ source_uri = enforce_trailing_slash(uri) + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
+
+ cache_dir = cache_dir source_uri
+
+ local_spec = File.join cache_dir, spec_file_name
+
+ if File.exist? local_spec
+ spec = Gem.read_binary local_spec
+ Gem.load_safe_marshal
+ spec = begin
+ Gem::SafeMarshal.safe_load(spec)
+ rescue StandardError
+ nil
+ end
+ return spec if spec
+ end
+
+ source_uri.path << ".rz"
+
+ spec = fetcher.fetch_path source_uri
+ spec = Gem::Util.inflate spec
+
+ if update_cache?
+ require "fileutils"
+ FileUtils.mkdir_p cache_dir
+
+ File.open local_spec, "wb" do |io|
+ io.write spec
+ end
+ end
+
+ Gem.load_safe_marshal
+ # TODO: Investigate setting Gem::Specification#loaded_from to a URI
+ Gem::SafeMarshal.safe_load spec
+ end
+
+ ##
+ # Loads +type+ kind of specs fetching from +@uri+ if the on-disk cache is
+ # out of date.
+ #
+ # +type+ is one of the following:
+ #
+ # :released => Return the list of all released specs
+ # :latest => Return the list of only the highest version of each gem
+ # :prerelease => Return the list of all prerelease only specs
+ #
+
+ def load_specs(type)
+ file = FILES[type]
+ fetcher = Gem::RemoteFetcher.fetcher
+ file_name = "#{file}.#{Gem.marshal_version}"
+ spec_path = enforce_trailing_slash(uri) + "#{file_name}.gz"
+ cache_dir = cache_dir spec_path
+ local_file = File.join(cache_dir, file_name)
+ retried = false
+
+ if update_cache?
+ require "fileutils"
+ FileUtils.mkdir_p cache_dir
+ end
+
+ spec_dump = fetcher.cache_update_path spec_path, local_file, update_cache?
+
+ Gem.load_safe_marshal
+ begin
+ Gem::NameTuple.from_list Gem::SafeMarshal.safe_load(spec_dump)
+ rescue ArgumentError
+ if update_cache? && !retried
+ FileUtils.rm local_file
+ retried = true
+ retry
+ else
+ raise Gem::Exception.new("Invalid spec cache file in #{local_file}")
+ end
+ end
+ end
+
+ ##
+ # Downloads +spec+ and writes it to +dir+. See also
+ # Gem::RemoteFetcher#download.
+
+ def download(spec, dir = Dir.pwd)
+ fetcher = Gem::RemoteFetcher.fetcher
+ fetcher.download spec, uri.to_s, dir
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Remote:", "]" do
+ q.breakable
+ q.text @uri.to_s
+
+ if api = uri
+ q.breakable
+ q.text "API URI: "
+ q.text api.to_s
+ end
+ end
+ end
+ end
+
+ def typo_squatting?(host, distance_threshold = 4)
+ return if @uri.host.nil?
+ levenshtein_distance(@uri.host, host).between? 1, distance_threshold
+ end
+
+ private
+
+ def new_dependency_resolver_set
+ return Gem::Resolver::IndexSet.new self if uri.scheme == "file"
+
+ fetch_uri = if uri.host == "rubygems.org"
+ index_uri = uri.dup
+ index_uri.host = "index.rubygems.org"
+ index_uri
+ else
+ uri
+ end
+
+ bundler_api_uri = enforce_trailing_slash(fetch_uri) + "versions"
+
+ begin
+ fetcher = Gem::RemoteFetcher.fetcher
+ response = fetcher.fetch_path bundler_api_uri, nil, true
+ rescue Gem::RemoteFetcher::FetchError
+ Gem::Resolver::IndexSet.new self
+ else
+ Gem::Resolver::APISet.new response.uri + "./info/"
+ end
+ end
+
+ def enforce_trailing_slash(uri)
+ uri.merge(uri.path.gsub(%r{/+$}, "") + "/")
+ end
+end
+
+require_relative "source/git"
+require_relative "source/installed"
+require_relative "source/specific_file"
+require_relative "source/local"
+require_relative "source/lock"
+require_relative "source/vendor"
diff --git a/lib/rubygems/source/git.rb b/lib/rubygems/source/git.rb
new file mode 100644
index 0000000000..baf2f9dd4c
--- /dev/null
+++ b/lib/rubygems/source/git.rb
@@ -0,0 +1,244 @@
+# frozen_string_literal: true
+
+##
+# A git gem for use in a gem dependencies file.
+#
+# Example:
+#
+# source =
+# Gem::Source::Git.new 'rake', 'git@example:rake.git', 'rake-10.1.0', false
+#
+# source.specs
+
+class Gem::Source::Git < Gem::Source
+ ##
+ # The name of the gem created by this git gem.
+
+ attr_reader :name
+
+ ##
+ # The commit reference used for checking out this git gem.
+
+ attr_reader :reference
+
+ ##
+ # When false the cache for this repository will not be updated.
+
+ attr_accessor :remote
+
+ ##
+ # The git repository this gem is sourced from.
+
+ attr_reader :repository
+
+ ##
+ # The directory for cache and git gem installation
+
+ attr_accessor :root_dir
+
+ ##
+ # Does this repository need submodules checked out too?
+
+ attr_reader :need_submodules
+
+ ##
+ # Creates a new git gem source for a gems from loaded from +repository+ at
+ # the given +reference+. The +name+ is only used to track the repository
+ # back to a gem dependencies file, it has no real significance as a git
+ # repository may contain multiple gems. If +submodules+ is true, submodules
+ # will be checked out when the gem is installed.
+
+ def initialize(name, repository, reference, submodules = false)
+ require_relative "../uri"
+ @uri = Gem::Uri.parse(repository)
+ @name = name
+ @repository = repository
+ @reference = reference || "HEAD"
+ @need_submodules = submodules
+
+ @remote = true
+ @root_dir = Gem.dir
+ end
+
+ def <=>(other)
+ case other
+ when Gem::Source::Git then
+ 0
+ when Gem::Source::Vendor,
+ Gem::Source::Lock then
+ -1
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def ==(other) # :nodoc:
+ super &&
+ @name == other.name &&
+ @repository == other.repository &&
+ @reference == other.reference &&
+ @need_submodules == other.need_submodules
+ end
+
+ def git_command
+ ENV.fetch("git", "git")
+ end
+
+ ##
+ # Checks out the files for the repository into the install_dir.
+
+ def checkout # :nodoc:
+ cache
+
+ return false unless File.exist? repo_cache_dir
+
+ unless File.exist? install_dir
+ system git_command, "clone", "--quiet", "--no-checkout",
+ repo_cache_dir, install_dir
+ end
+
+ Dir.chdir install_dir do
+ system git_command, "fetch", "--quiet", "--force", "--tags", install_dir
+
+ success = system git_command, "reset", "--quiet", "--hard", rev_parse
+
+ if @need_submodules
+ require "open3"
+ _, status = Open3.capture2e(git_command, "submodule", "update", "--quiet", "--init", "--recursive")
+
+ success &&= status.success?
+ end
+
+ success
+ end
+ end
+
+ ##
+ # Creates a local cache repository for the git gem.
+
+ def cache # :nodoc:
+ return unless @remote
+
+ if File.exist? repo_cache_dir
+ Dir.chdir repo_cache_dir do
+ system git_command, "fetch", "--quiet", "--force", "--tags",
+ @repository, "refs/heads/*:refs/heads/*"
+ end
+ else
+ system git_command, "clone", "--quiet", "--bare", "--no-hardlinks",
+ @repository, repo_cache_dir
+ end
+ end
+
+ ##
+ # Directory where git gems get unpacked and so-forth.
+
+ def base_dir # :nodoc:
+ File.join @root_dir, "bundler"
+ end
+
+ ##
+ # A short reference for use in git gem directories
+
+ def dir_shortref # :nodoc:
+ rev_parse[0..11]
+ end
+
+ ##
+ # Nothing to download for git gems
+
+ def download(full_spec, path) # :nodoc:
+ end
+
+ ##
+ # The directory where the git gem will be installed.
+
+ def install_dir # :nodoc:
+ return unless File.exist? repo_cache_dir
+
+ File.join base_dir, "gems", "#{@name}-#{dir_shortref}"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Git: ", "]" do
+ q.breakable
+ q.text @repository
+
+ q.breakable
+ q.text @reference
+ end
+ end
+ end
+
+ ##
+ # The directory where the git gem's repository will be cached.
+
+ def repo_cache_dir # :nodoc:
+ File.join @root_dir, "cache", "bundler", "git", "#{@name}-#{uri_hash}"
+ end
+
+ ##
+ # Converts the git reference for the repository into a commit hash.
+
+ def rev_parse # :nodoc:
+ hash = nil
+
+ Dir.chdir repo_cache_dir do
+ hash = Gem::Util.popen(git_command, "rev-parse", @reference).strip
+ end
+
+ raise Gem::Exception,
+ "unable to find reference #{@reference} in #{@repository}" unless
+ $?.success?
+
+ hash
+ end
+
+ ##
+ # Loads all gemspecs in the repository
+
+ def specs
+ checkout
+
+ return [] unless install_dir
+
+ Dir.chdir install_dir do
+ Dir["{,*,*/*}.gemspec"].filter_map do |spec_file|
+ directory = File.dirname spec_file
+ file = File.basename spec_file
+
+ Dir.chdir directory do
+ spec = Gem::Specification.load file
+ if spec
+ spec.base_dir = base_dir
+
+ spec.extension_dir =
+ File.join base_dir, "extensions", Gem::Platform.local.to_s,
+ Gem.extension_api_version, "#{name}-#{dir_shortref}"
+
+ spec.full_gem_path = File.dirname spec.loaded_from if spec
+ end
+ spec
+ end
+ end
+ end
+ end
+
+ ##
+ # A hash for the git gem based on the git repository Gem::URI.
+
+ def uri_hash # :nodoc:
+ require_relative "../openssl"
+
+ normalized =
+ if @repository.match?(%r{^\w+://(\w+@)?})
+ uri = Gem::URI(@repository).normalize.to_s.sub %r{/$},""
+ uri.sub(/\A(\w+)/) { $1.downcase }
+ else
+ @repository
+ end
+
+ OpenSSL::Digest::SHA1.hexdigest normalized
+ end
+end
diff --git a/lib/rubygems/source/installed.rb b/lib/rubygems/source/installed.rb
new file mode 100644
index 0000000000..f5c96fee51
--- /dev/null
+++ b/lib/rubygems/source/installed.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+##
+# Represents an installed gem. This is used for dependency resolution.
+
+class Gem::Source::Installed < Gem::Source
+ def initialize # :nodoc:
+ @uri = nil
+ end
+
+ ##
+ # Installed sources sort before all other sources
+
+ def <=>(other)
+ case other
+ when Gem::Source::Git,
+ Gem::Source::Lock,
+ Gem::Source::Vendor then
+ -1
+ when Gem::Source::Installed then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+
+ ##
+ # We don't need to download an installed gem
+
+ def download(spec, path)
+ nil
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.text "[Installed]"
+ end
+ end
+end
diff --git a/lib/rubygems/source/local.rb b/lib/rubygems/source/local.rb
new file mode 100644
index 0000000000..4bef31a265
--- /dev/null
+++ b/lib/rubygems/source/local.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+##
+# The local source finds gems in the current directory for fulfilling
+# dependencies.
+
+class Gem::Source::Local < Gem::Source
+ def initialize # :nodoc:
+ @specs = nil
+ @api_uri = nil
+ @uri = nil
+ @load_specs_names = {}
+ end
+
+ ##
+ # Local sorts before Gem::Source and after Gem::Source::Installed
+
+ def <=>(other)
+ case other
+ when Gem::Source::Installed,
+ Gem::Source::Lock then
+ -1
+ when Gem::Source::Local then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def inspect # :nodoc:
+ keys = @specs ? @specs.keys.sort : "NOT LOADED"
+ format("#<%s specs: %p>", self.class, keys)
+ end
+
+ def load_specs(type) # :nodoc:
+ @load_specs_names[type] ||= begin
+ names = []
+
+ @specs = {}
+
+ Dir["*.gem"].each do |file|
+ 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
+
+ names
+ end
+ end
+
+ def find_gem(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc:
+ find_all_gems(gem_name, version, prerelease).max_by(&:version)
+ end
+
+ def find_all_gems(gem_name, version = Gem::Requirement.default, prerelease = false) # :nodoc:
+ load_specs :complete
+
+ found = []
+
+ @specs.each do |n, data|
+ 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
+ end
+
+ def fetch_spec(name) # :nodoc:
+ load_specs :complete
+
+ if data = @specs[name]
+ data.last.spec
+ else
+ raise Gem::Exception, "Unable to find spec for #{name.inspect}"
+ end
+ end
+
+ def download(spec, cache_dir = nil) # :nodoc:
+ load_specs :complete
+
+ @specs.each do |_name, data|
+ return data[0] if data[1].spec == spec
+ end
+
+ raise Gem::Exception, "Unable to find file for '#{spec.full_name}'"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[Local gems:", "]" do
+ q.breakable
+ if @specs
+ q.seplist @specs.keys do |v|
+ q.text v.full_name
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/source/lock.rb b/lib/rubygems/source/lock.rb
new file mode 100644
index 0000000000..70849210bd
--- /dev/null
+++ b/lib/rubygems/source/lock.rb
@@ -0,0 +1,49 @@
+# 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
+# dependency lock files.
+
+class Gem::Source::Lock < Gem::Source
+ ##
+ # The wrapped Gem::Source
+
+ attr_reader :wrapped
+
+ ##
+ # Creates a new Lock source that wraps +source+ and moves it earlier in the
+ # sort list.
+
+ def initialize(source)
+ @wrapped = source
+ end
+
+ def <=>(other) # :nodoc:
+ case other
+ when Gem::Source::Lock then
+ @wrapped <=> other.wrapped
+ when Gem::Source then
+ 1
+ end
+ end
+
+ def ==(other) # :nodoc:
+ (self <=> other) == 0
+ end
+
+ def hash # :nodoc:
+ @wrapped.hash ^ 3
+ end
+
+ ##
+ # Delegates to the wrapped source's fetch_spec method.
+
+ def fetch_spec(name_tuple)
+ @wrapped.fetch_spec name_tuple
+ end
+
+ def uri # :nodoc:
+ @wrapped.uri
+ end
+end
diff --git a/lib/rubygems/source/specific_file.rb b/lib/rubygems/source/specific_file.rb
new file mode 100644
index 0000000000..dde1d48a21
--- /dev/null
+++ b/lib/rubygems/source/specific_file.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+##
+# A source representing a single .gem file. This is used for installation of
+# local gems.
+
+class Gem::Source::SpecificFile < Gem::Source
+ ##
+ # The path to the gem for this specific file.
+
+ attr_reader :path
+
+ ##
+ # Creates a new SpecificFile for the gem in +file+
+
+ def initialize(file)
+ @uri = nil
+ @path = ::File.expand_path(file)
+
+ @package = Gem::Package.new @path
+ @spec = @package.spec
+ @name = @spec.name_tuple
+ end
+
+ ##
+ # The Gem::Specification extracted from this .gem.
+
+ attr_reader :spec
+
+ def load_specs(*a) # :nodoc:
+ [@name]
+ end
+
+ def fetch_spec(name) # :nodoc:
+ return @spec if name == @name
+ raise Gem::Exception, "Unable to find '#{name}'"
+ end
+
+ def download(spec, dir = nil) # :nodoc:
+ return @path if spec == @spec
+ raise Gem::Exception, "Unable to download '#{spec.full_name}'"
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ q.group 2, "[SpecificFile:", "]" do
+ q.breakable
+ q.text @path
+ end
+ end
+ end
+
+ ##
+ # Orders this source against +other+.
+ #
+ # If +other+ is a SpecificFile from a different gem name +nil+ is returned.
+ #
+ # If +other+ is a SpecificFile from the same gem name the versions are
+ # compared using Gem::Version#<=>
+ #
+ # Otherwise Gem::Source#<=> is used.
+
+ def <=>(other)
+ case other
+ when Gem::Source::SpecificFile then
+ return nil if @spec.name != other.spec.name
+
+ @spec.version <=> other.spec.version
+ else
+ super
+ end
+ end
+end
diff --git a/lib/rubygems/source/vendor.rb b/lib/rubygems/source/vendor.rb
new file mode 100644
index 0000000000..44ef614441
--- /dev/null
+++ b/lib/rubygems/source/vendor.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+##
+# This represents a vendored source that is similar to an installed gem.
+
+class Gem::Source::Vendor < Gem::Source::Installed
+ ##
+ # Creates a new Vendor source for a gem that was unpacked at +path+.
+
+ def initialize(path)
+ @uri = path
+ end
+
+ def <=>(other)
+ case other
+ when Gem::Source::Lock then
+ -1
+ when Gem::Source::Vendor then
+ 0
+ when Gem::Source then
+ 1
+ end
+ end
+end
diff --git a/lib/rubygems/source_list.rb b/lib/rubygems/source_list.rb
new file mode 100644
index 0000000000..19bf4595c4
--- /dev/null
+++ b/lib/rubygems/source_list.rb
@@ -0,0 +1,182 @@
+# frozen_string_literal: true
+
+##
+# The SourceList represents the sources rubygems has been configured to use.
+# A source may be created from an array of sources:
+#
+# Gem::SourceList.from %w[https://rubygems.example https://internal.example]
+#
+# Or by adding them:
+#
+# sources = Gem::SourceList.new
+# sources << 'https://rubygems.example'
+#
+# The most common way to get a SourceList is Gem.sources.
+
+class Gem::SourceList
+ include Enumerable
+
+ ##
+ # Creates a new SourceList
+
+ def initialize
+ @sources = []
+ end
+
+ ##
+ # The sources in this list
+
+ attr_reader :sources
+
+ ##
+ # Creates a new SourceList from an array of sources.
+
+ def self.from(ary)
+ list = new
+
+ list.replace ary
+
+ list
+ end
+
+ def initialize_copy(other) # :nodoc:
+ @sources = @sources.dup
+ end
+
+ ##
+ # 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)
+ end
+
+ @sources << src unless @sources.include?(src)
+ src
+ end
+
+ ##
+ # Prepends +obj+ to the beginning of the source list which may be a Gem::Source, Gem::URI or URI
+ # Moves +obj+ to the beginning of the list if already present.
+ # String.
+
+ def prepend(obj)
+ src = case obj
+ when Gem::Source
+ obj
+ else
+ Gem::Source.new(obj)
+ end
+
+ @sources.delete(src) if @sources.include?(src)
+ @sources.unshift(src)
+ src
+ end
+
+ ##
+ # Appends +obj+ to the end of the source list, moving it if already present.
+ # +obj+ may be a Gem::Source, Gem::URI or URI String.
+ # Moves +obj+ to the end of the list if already present.
+
+ def append(obj)
+ src = case obj
+ when Gem::Source
+ obj
+ else
+ Gem::Source.new(obj)
+ end
+
+ @sources.delete(src) if @sources.include?(src)
+ @sources << src
+ src
+ end
+
+ ##
+ # Replaces this SourceList with the sources in +other+ See #<< for
+ # acceptable items in +other+.
+
+ def replace(other)
+ clear
+
+ other.each do |x|
+ self << x
+ end
+
+ self
+ end
+
+ ##
+ # Removes all sources from the SourceList.
+
+ def clear
+ @sources.clear
+ end
+
+ ##
+ # Yields each source URI in the list.
+
+ def each
+ @sources.each {|s| yield s.uri.to_s }
+ end
+
+ ##
+ # Yields each source in the list.
+
+ def each_source(&b)
+ @sources.each(&b)
+ end
+
+ ##
+ # Returns true if there are no sources in this SourceList.
+
+ def empty?
+ @sources.empty?
+ end
+
+ def ==(other) # :nodoc:
+ to_a == other
+ end
+
+ ##
+ # Returns an Array of source URI Strings.
+
+ def to_a
+ @sources.map {|x| x.uri.to_s }
+ end
+
+ alias_method :to_ary, :to_a
+
+ ##
+ # Returns the first source in the list.
+
+ def first
+ @sources.first
+ end
+
+ ##
+ # Returns true if this source list includes +other+ which may be a
+ # Gem::Source or a source URI.
+
+ def include?(other)
+ if other.is_a? Gem::Source
+ @sources.include? other
+ else
+ @sources.find {|x| x.uri.to_s == other.to_s }
+ end
+ end
+
+ ##
+ # Deletes +source+ from the source list which may be a Gem::Source or a URI.
+
+ def delete(source)
+ if source.is_a? Gem::Source
+ @sources.delete source
+ else
+ @sources.delete_if {|x| x.uri.to_s == source.to_s }
+ end
+ end
+end
diff --git a/lib/rubygems/spec_fetcher.rb b/lib/rubygems/spec_fetcher.rb
new file mode 100644
index 0000000000..835dedf948
--- /dev/null
+++ b/lib/rubygems/spec_fetcher.rb
@@ -0,0 +1,290 @@
+# frozen_string_literal: true
+
+require_relative "remote_fetcher"
+require_relative "user_interaction"
+require_relative "errors"
+require_relative "text"
+require_relative "name_tuple"
+
+##
+# SpecFetcher handles metadata updates from remote gem repositories.
+
+class Gem::SpecFetcher
+ include Gem::UserInteraction
+ include Gem::Text
+
+ ##
+ # Cache of latest specs
+
+ attr_reader :latest_specs # :nodoc:
+
+ ##
+ # Sources for this SpecFetcher
+
+ attr_reader :sources # :nodoc:
+
+ ##
+ # Cache of all released specs
+
+ attr_reader :specs # :nodoc:
+
+ ##
+ # Cache of prerelease specs
+
+ attr_reader :prerelease_specs # :nodoc:
+
+ @fetcher = nil
+
+ ##
+ # Default fetcher instance. Use this instead of ::new to reduce object
+ # allocation.
+
+ def self.fetcher
+ @fetcher ||= new
+ end
+
+ def self.fetcher=(fetcher) # :nodoc:
+ @fetcher = fetcher
+ end
+
+ ##
+ # Creates a new SpecFetcher. Ordinarily you want to use the default fetcher
+ # from Gem::SpecFetcher::fetcher which uses the Gem.sources.
+ #
+ # If you need to retrieve specifications from a different +source+, you can
+ # send it as an argument.
+
+ def initialize(sources = nil)
+ @sources = sources || Gem.sources
+
+ @update_cache =
+ begin
+ File.stat(Gem.user_home).uid == Process.uid
+ rescue Errno::EACCES, Errno::ENOENT
+ false
+ end
+
+ @specs = {}
+ @latest_specs = {}
+ @prerelease_specs = {}
+
+ @caches = {
+ latest: @latest_specs,
+ prerelease: @prerelease_specs,
+ released: @specs,
+ }
+
+ @fetcher = Gem::RemoteFetcher.fetcher
+ end
+
+ ##
+ #
+ # Find and fetch gem name tuples that match +dependency+.
+ #
+ # If +matching_platform+ is false, gems for all platforms are returned.
+
+ def search_for_dependency(dependency, matching_platform = true)
+ found = {}
+
+ rejected_specs = {}
+
+ list, errors = available_specs(dependency.identity)
+
+ 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
+ end
+
+ found[source] = specs.select do |tup|
+ if dependency.match?(tup)
+ if matching_platform && !Gem::Platform.match_gem?(tup.platform, tup.name)
+ pm = (
+ rejected_specs[dependency] ||= \
+ Gem::PlatformMismatch.new(tup.name, tup.version))
+ pm.add_platform tup.platform
+ false
+ else
+ true
+ end
+ end
+ end
+ end
+
+ errors += rejected_specs.values
+
+ tuples = []
+
+ found.each do |source, specs|
+ specs.each do |s|
+ tuples << [s, source]
+ end
+ end
+
+ tuples = tuples.sort_by {|x| x[0].version }
+
+ [tuples, errors]
+ end
+
+ ##
+ # Return all gem name tuples who's names match +obj+
+
+ def detect(type = :complete)
+ tuples = []
+
+ list, _ = available_specs(type)
+ list.each do |source, specs|
+ specs.each do |tup|
+ if yield(tup)
+ tuples << [tup, source]
+ end
+ end
+ end
+
+ tuples
+ end
+
+ ##
+ # Find and fetch specs that match +dependency+.
+ #
+ # If +matching_platform+ is false, gems for all platforms are returned.
+
+ def spec_for_dependency(dependency, matching_platform = true)
+ tuples, errors = search_for_dependency(dependency, matching_platform)
+
+ specs = []
+ tuples.each do |tup, source|
+ spec = source.fetch_spec(tup)
+ rescue Gem::RemoteFetcher::FetchError => e
+ errors << Gem::SourceFetchProblem.new(source, e)
+ else
+ specs << [spec, source]
+ end
+
+ [specs, errors]
+ end
+
+ ##
+ # Suggests gems based on the supplied +gem_name+. Returns an array of
+ # alternative gem names.
+
+ def suggest_gems_from_name(gem_name, type = :latest, num_results = 5)
+ gem_name = gem_name.downcase.tr("_-", "")
+
+ # All results for 3-character-or-shorter (minus hyphens/underscores) gem
+ # names get rejected, so we just return an empty array immediately instead.
+ return [] if gem_name.length <= 3
+
+ max = gem_name.size / 2
+ names = available_specs(type).first.values.flatten(1)
+
+ min_length = gem_name.length - max
+ max_length = gem_name.length + max
+
+ gem_name_with_postfix = "#{gem_name}ruby"
+ gem_name_with_prefix = "ruby#{gem_name}"
+
+ matches = names.filter_map do |n|
+ len = n.name.length
+ # If the gem doesn't support the current platform, bail early.
+ next unless n.match_platform?
+
+ # If the length is min_length or shorter, we've done `max` deletions.
+ # This would be rejected later, so we skip it for performance.
+ next if len <= min_length
+
+ # The candidate name, normalized the same as gem_name.
+ normalized_name = n.name.downcase
+ normalized_name.tr!("_-", "")
+
+ # If the gem is "{NAME}-ruby" and "ruby-{NAME}", we want to return it.
+ # But we already removed hyphens, so we check "{NAME}ruby" and "ruby{NAME}".
+ next [n.name, 0] if normalized_name == gem_name_with_postfix
+ next [n.name, 0] if normalized_name == gem_name_with_prefix
+
+ # If the length is max_length or longer, we've done `max` insertions.
+ # This would be rejected later, so we skip it for performance.
+ next if len >= max_length
+
+ # If we found an exact match (after stripping underscores and hyphens),
+ # that's our most likely candidate.
+ # Return it immediately, and skip the rest of the loop.
+ return [n.name] if normalized_name == gem_name
+
+ distance = levenshtein_distance gem_name, normalized_name
+
+ # Skip current candidate, if the edit distance is greater than allowed.
+ next if distance >= max
+
+ # If all else fails, return the name and the calculated distance.
+ [n.name, distance]
+ end
+
+ matches = if matches.empty? && type != :prerelease
+ suggest_gems_from_name gem_name, :prerelease
+ else
+ matches.uniq.sort_by {|_name, dist| dist }
+ end
+
+ matches.map {|name, _dist| name }.uniq.first(num_results)
+ end
+
+ ##
+ # Returns a list of gems available for each source in Gem::sources.
+ #
+ # +type+ can be one of 3 values:
+ # :released => Return the list of all released specs
+ # :complete => Return the list of all specs
+ # :latest => Return the list of only the highest version of each gem
+ # :prerelease => Return the list of all prerelease only specs
+ #
+
+ def available_specs(type)
+ errors = []
+ list = {}
+
+ @sources.each_source do |source|
+ 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]
+ end
+
+ ##
+ # Retrieves NameTuples from +source+ of the given +type+ (:prerelease,
+ # etc.). If +gracefully_ignore+ is true, errors are ignored.
+
+ def tuples_for(source, type, gracefully_ignore = false) # :nodoc:
+ @caches[type][source.uri] ||=
+ source.load_specs(type).sort_by(&:name)
+ rescue Gem::RemoteFetcher::FetchError
+ raise unless gracefully_ignore
+ []
+ end
+end
diff --git a/lib/rubygems/specification.rb b/lib/rubygems/specification.rb
new file mode 100644
index 0000000000..51729d755b
--- /dev/null
+++ b/lib/rubygems/specification.rb
@@ -0,0 +1,2604 @@
+# 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 "basic_specification"
+require_relative "stub_specification"
+require_relative "platform"
+require_relative "specification_record"
+
+require "rbconfig"
+
+##
+# The Specification class contains the information for a gem. Typically
+# defined in a .gemspec file or a Rakefile, and looks like this:
+#
+# Gem::Specification.new do |s|
+# s.name = 'example'
+# s.version = '0.1.0'
+# s.licenses = ['MIT']
+# s.summary = "This is an example!"
+# s.description = "Much longer explanation of the example!"
+# s.authors = ["Ruby Coder"]
+# s.email = 'rubycoder@example.com'
+# s.files = ["lib/example.rb"]
+# s.homepage = 'https://rubygems.org/gems/example'
+# s.metadata = { "source_code_uri" => "https://github.com/example/example" }
+# end
+#
+# Starting in RubyGems 2.0, a Specification can hold arbitrary
+# metadata. See #metadata for restrictions on the format and size of metadata
+# items you may add to a specification.
+#
+# Specifications must be deterministic, as in the example above. For instance,
+# you cannot define attributes conditionally:
+#
+# # INVALID: do not do this.
+# unless RUBY_ENGINE == "jruby"
+# s.extensions << "ext/example/extconf.rb"
+# end
+#
+
+class Gem::Specification < Gem::BasicSpecification
+ # REFACTOR: Consider breaking out this version stuff into a separate
+ # module. There's enough special stuff around it that it may justify
+ # a separate class.
+
+ ##
+ # The version number of a specification that does not specify one
+ # (i.e. RubyGems 0.7 or earlier).
+
+ NONEXISTENT_SPECIFICATION_VERSION = -1
+
+ ##
+ # The specification version applied to any new Specification instances
+ # created. This should be bumped whenever something in the spec format
+ # changes.
+ #
+ # Specification Version History:
+ #
+ # spec ruby
+ # ver ver yyyy-mm-dd description
+ # -1 <0.8.0 pre-spec-version-history
+ # 1 0.8.0 2004-08-01 Deprecated "test_suite_file" for "test_files"
+ # "test_file=x" is a shortcut for "test_files=[x]"
+ # 2 0.9.5 2007-10-01 Added "required_rubygems_version"
+ # Now forward-compatible with future versions
+ # 3 1.3.2 2009-01-03 Added Fixnum validation to specification_version
+ # 4 1.9.0 2011-06-07 Added metadata
+ #--
+ # When updating this number, be sure to also update #to_ruby.
+ #
+ # NOTE RubyGems < 1.2 cannot load specification versions > 2.
+
+ CURRENT_SPECIFICATION_VERSION = 4 # :nodoc:
+
+ ##
+ # An informal list of changes to the specification. The highest-valued
+ # key should be equal to the CURRENT_SPECIFICATION_VERSION.
+
+ SPECIFICATION_VERSION_HISTORY = { # :nodoc:
+ -1 => ["(RubyGems versions up to and including 0.7 did not have versioned specifications)"],
+ 1 => [
+ 'Deprecated "test_suite_file" in favor of the new, but equivalent, "test_files"',
+ '"test_file=x" is a shortcut for "test_files=[x]"',
+ ],
+ 2 => [
+ 'Added "required_rubygems_version"',
+ "Now forward-compatible with future versions",
+ ],
+ 3 => [
+ "Added Fixnum validation to the specification_version",
+ ],
+ 4 => [
+ "Added sandboxed freeform metadata to the specification version.",
+ ],
+ }.freeze
+
+ MARSHAL_FIELDS = { # :nodoc:
+ -1 => 16,
+ 1 => 16,
+ 2 => 16,
+ 3 => 17,
+ 4 => 18,
+ }.freeze
+
+ today = Time.now.utc
+ TODAY = Time.utc(today.year, today.month, today.day) # :nodoc:
+
+ @load_cache = {} # :nodoc:
+ @load_cache_mutex = Thread::Mutex.new
+
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:
+
+ # :startdoc:
+
+ ##
+ # List of attribute names: [:name, :version, ...]
+
+ @@required_attributes = [:rubygems_version,
+ :specification_version,
+ :name,
+ :version,
+ :date,
+ :summary,
+ :require_paths]
+
+ ##
+ # 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,
+ }.freeze
+
+ # rubocop:disable Style/MutableConstant
+ INITIALIZE_CODE_FOR_DEFAULTS = {} # :nodoc:
+ # rubocop:enable Style/MutableConstant
+
+ @@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
+ else
+ "default_value(:#{k}).dup"
+ end
+ end
+
+ @@attributes = @@default_value.keys.sort_by(&:to_s)
+ @@array_attributes = @@default_value.select {|_k,v| v.is_a?(Array) }.keys
+ @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition do |k|
+ @@default_value[k].nil?
+ end
+
+ # Sentinel object to represent "not found" stubs
+ NOT_FOUND = Struct.new(:to_spec, :this).new # :nodoc:
+ deprecate_constant :NOT_FOUND
+
+ # Tracking removed method calls to warn users during build time.
+ REMOVED_METHODS = [:rubyforge_project=, :mark_version].freeze # :nodoc:
+ def removed_method_calls
+ @removed_method_calls ||= []
+ end
+
+ ######################################################################
+ # :section: Required gemspec attributes
+
+ ##
+ # This gem's name.
+ #
+ # Usage:
+ #
+ # spec.name = 'rake'
+
+ attr_accessor :name
+
+ ##
+ # This gem's version.
+ #
+ # The version string can contain numbers and periods, such as +1.0.0+.
+ # A gem is a 'prerelease' gem if the version has a letter in it, such as
+ # +1.0.0.pre+.
+ #
+ # Usage:
+ #
+ # spec.version = '0.4.1'
+
+ attr_reader :version
+
+ ##
+ # A short summary of this gem's description. Displayed in <tt>gem list -d</tt>.
+ #
+ # The #description should be more detailed than the summary.
+ #
+ # Usage:
+ #
+ # spec.summary = "This is a small summary of my gem"
+
+ attr_reader :summary
+
+ ##
+ # Files included in this gem. You cannot append to this accessor, you must
+ # assign to it.
+ #
+ # Only add files you can require to this list, not directories, etc.
+ #
+ # Directories are automatically stripped from this list when building a gem,
+ # other non-files cause an error.
+ #
+ # Usage:
+ #
+ # require 'rake'
+ # spec.files = FileList['lib/**/*.rb',
+ # 'bin/*',
+ # '[A-Z]*'].to_a
+ #
+ # # or without Rake...
+ # spec.files = Dir['lib/**/*.rb'] + Dir['bin/*']
+ # spec.files += Dir['[A-Z]*']
+ # spec.files.reject! { |fn| fn.include? "CVS" }
+
+ def files
+ # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks)
+ # DOC: Why isn't it normal? Why does it suck? How can we fix this?
+ @files = [@files,
+ @test_files,
+ add_bindir(@executables),
+ @extra_rdoc_files,
+ @extensions].flatten.compact.uniq.sort
+ end
+
+ ##
+ # A list of authors for this gem.
+ #
+ # Alternatively, a single author can be specified by assigning a string to
+ # +spec.author+
+ #
+ # Usage:
+ #
+ # spec.authors = ['John Jones', 'Mary Smith']
+
+ def authors=(value)
+ @authors = Array(value).flatten.grep(String)
+ end
+
+ ######################################################################
+ # :section: Recommended gemspec attributes
+
+ ##
+ # The version of Ruby required by this gem
+ #
+ # Usage:
+ #
+ # spec.required_ruby_version = '>= 2.7.0'
+
+ attr_reader :required_ruby_version
+
+ ##
+ # A long description of this gem
+ #
+ # The description should be more detailed than the summary but not
+ # excessively long. A few paragraphs is a recommended length with no
+ # examples or formatting.
+ #
+ # Usage:
+ #
+ # spec.description = <<~EOF
+ # Rake is a Make-like program implemented in Ruby. Tasks and
+ # dependencies are specified in standard Ruby syntax.
+ # EOF
+
+ attr_reader :description
+
+ ##
+ # A contact email address (or addresses) for this gem
+ #
+ # Usage:
+ #
+ # spec.email = 'john.jones@example.com'
+ # spec.email = ['jack@example.com', 'jill@example.com']
+
+ attr_accessor :email
+
+ ##
+ # The URL of this gem's home page
+ #
+ # Usage:
+ #
+ # spec.homepage = 'https://github.com/ruby/rake'
+
+ attr_accessor :homepage
+
+ ##
+ # The license for this gem.
+ #
+ # The license must be no more than 64 characters.
+ #
+ # This should just be the name of your license. The full text of the license
+ # should be inside of the gem (at the top level) when you build it.
+ #
+ # The simplest way is to specify the standard SPDX ID
+ # https://spdx.org/licenses/ for the license.
+ # Ideally, you should pick one that is OSI (Open Source Initiative)
+ # https://opensource.org/licenses/ approved.
+ #
+ # The most commonly used OSI-approved licenses are MIT and Apache-2.0.
+ # GitHub also provides a license picker at https://choosealicense.com/.
+ #
+ # You can also use a custom license file along with your gemspec and specify
+ # a LicenseRef-<idstring>, where idstring is the name of the file containing
+ # the license text.
+ #
+ # You should specify a license for your gem so that people know how they are
+ # permitted to use it and any restrictions you're placing on it. Not
+ # specifying a license means all rights are reserved; others have no right
+ # to use the code for any purpose.
+ #
+ # You can set multiple licenses with #licenses=
+ #
+ # Usage:
+ # spec.license = 'MIT'
+
+ def license=(o)
+ self.licenses = [o]
+ end
+
+ ##
+ # The license(s) for the library.
+ #
+ # Each license must be a short name, no more than 64 characters.
+ #
+ # This should just be the name of your license. The full
+ # text of the license should be inside of the gem when you build it.
+ #
+ # See #license= for more discussion
+ #
+ # Usage:
+ # spec.licenses = ['MIT', 'GPL-2.0']
+
+ def licenses=(licenses)
+ @licenses = Array licenses
+ end
+
+ ##
+ # The metadata holds extra data for this gem that may be useful to other
+ # consumers and is settable by gem authors.
+ #
+ # Metadata items have the following restrictions:
+ #
+ # * The metadata must be a Hash object
+ # * All keys and values must be Strings
+ # * Keys can be a maximum of 128 bytes and values can be a maximum of 1024
+ # bytes
+ # * All strings must be UTF-8, no binary data is allowed
+ #
+ # You can use metadata to specify links to your gem's homepage, codebase,
+ # documentation, wiki, mailing list, issue tracker and changelog.
+ #
+ # s.metadata = {
+ # "bug_tracker_uri" => "https://example.com/user/bestgemever/issues",
+ # "changelog_uri" => "https://example.com/user/bestgemever/CHANGELOG.md",
+ # "documentation_uri" => "https://www.example.info/gems/bestgemever/0.0.1",
+ # "homepage_uri" => "https://bestgemever.example.io",
+ # "mailing_list_uri" => "https://groups.example.com/bestgemever",
+ # "source_code_uri" => "https://example.com/user/bestgemever",
+ # "wiki_uri" => "https://example.com/user/bestgemever/wiki",
+ # "funding_uri" => "https://example.com/donate"
+ # }
+ #
+ # These links will be used on your gem's page on rubygems.org and must pass
+ # validation against following regex.
+ #
+ # %r{\Ahttps?:\/\/([^\s:@]+:[^\s:@]*@)?[A-Za-z\d\-]+(\.[A-Za-z\d\-]+)+\.?(:\d{1,5})?([\/?]\S*)?\z}
+
+ attr_accessor :metadata
+
+ ######################################################################
+ # :section: Optional gemspec attributes
+
+ ##
+ # Singular (alternative) writer for #authors
+ #
+ # Usage:
+ #
+ # spec.author = 'John Jones'
+
+ def author=(o)
+ self.authors = [o]
+ end
+
+ ##
+ # The path in the gem for executable scripts. Usually 'exe'
+ #
+ # Usage:
+ #
+ # spec.bindir = 'exe'
+
+ attr_accessor :bindir
+
+ ##
+ # The certificate chain used to sign this gem. See Gem::Security for
+ # details.
+
+ attr_accessor :cert_chain
+
+ ##
+ # A message that gets displayed after the gem is installed.
+ #
+ # Usage:
+ #
+ # spec.post_install_message = "Thanks for installing!"
+
+ attr_accessor :post_install_message
+
+ ##
+ # The platform this gem runs on.
+ #
+ # This is usually Gem::Platform::RUBY or Gem::Platform::CURRENT.
+ #
+ # Most gems contain pure Ruby code; they should simply leave the default
+ # value in place. Some gems contain C (or other) code to be compiled into a
+ # Ruby "extension". The gem should leave the default value in place unless
+ # the code will only compile on a certain type of system. Some gems consist
+ # of pre-compiled code ("binary gems"). It's especially important that they
+ # set the platform attribute appropriately. A shortcut is to set the
+ # platform to Gem::Platform::CURRENT, which will cause the gem builder to set
+ # the platform to the appropriate value for the system on which the build is
+ # being performed.
+ #
+ # If this attribute is set to a non-default value, it will be included in
+ # the filename of the gem when it is built such as:
+ # nokogiri-1.6.0-x86-mingw32.gem
+ #
+ # Usage:
+ #
+ # spec.platform = Gem::Platform.local
+
+ def platform=(platform)
+ @original_platform = platform
+
+ case platform
+ when Gem::Platform::CURRENT then
+ @new_platform = Gem::Platform.local
+ @original_platform = @new_platform.to_s
+
+ when Gem::Platform then
+ @new_platform = platform
+
+ # legacy constants
+ when nil, Gem::Platform::RUBY then
+ @new_platform = Gem::Platform::RUBY
+ when "mswin32" then # was Gem::Platform::WIN32
+ @new_platform = Gem::Platform.new "x86-mswin32"
+ when "i586-linux" then # was Gem::Platform::LINUX_586
+ @new_platform = Gem::Platform.new "x86-linux"
+ when "powerpc-darwin" then # was Gem::Platform::DARWIN
+ @new_platform = Gem::Platform.new "ppc-darwin"
+ else
+ @new_platform = Gem::Platform.new platform
+ end
+
+ @platform = @new_platform.to_s
+ end
+
+ ##
+ # Paths in the gem to add to <code>$LOAD_PATH</code> when this gem is
+ # activated.
+ #--
+ # See also #require_paths
+ #++
+ # If you have an extension you do not need to add <code>"ext"</code> to the
+ # require path, the extension build process will copy the extension files
+ # into "lib" for you.
+ #
+ # The default value is <code>"lib"</code>
+ #
+ # Usage:
+ #
+ # # If all library files are in the root directory...
+ # spec.require_paths = ['.']
+
+ def require_paths=(val)
+ @require_paths = Array(val)
+ end
+
+ ##
+ # The RubyGems version required by this gem
+
+ attr_reader :required_rubygems_version
+
+ ##
+ # The key used to sign this gem. See Gem::Security for details.
+
+ attr_accessor :signing_key
+
+ ##
+ # Adds a development dependency named +gem+ with +requirements+ to this
+ # gem.
+ #
+ # Usage:
+ #
+ # spec.add_development_dependency 'example', '~> 1.1', '>= 1.1.4'
+ #
+ # Development dependencies aren't installed by default and aren't
+ # activated when a gem is required.
+
+ def add_development_dependency(gem, *requirements)
+ add_dependency_with_type(gem, :development, requirements)
+ end
+
+ ##
+ # Adds a runtime dependency named +gem+ with +requirements+ to this gem.
+ #
+ # Usage:
+ #
+ # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4'
+
+ def add_dependency(gem, *requirements)
+ if requirements.uniq.size != requirements.size
+ warn "WARNING: duplicated #{gem} dependency #{requirements}"
+ end
+
+ add_dependency_with_type(gem, :runtime, requirements)
+ end
+
+ ##
+ # Executables included in the gem.
+ #
+ # 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.
+ #
+ # Executables included may only be ruby scripts, not scripts for other
+ # languages or compiled binaries.
+ #
+ # Usage:
+ #
+ # spec.executables << 'rake'
+
+ def executables
+ @executables ||= []
+ end
+
+ ##
+ # Extensions to build when installing the gem, specifically the paths to
+ # 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.
+ #
+ # Usage:
+ #
+ # spec.extensions << 'ext/rmagic/extconf.rb'
+ #
+ # See Gem::Ext::Builder for information about writing extensions for gems.
+
+ def extensions
+ @extensions ||= []
+ end
+
+ ##
+ # Extra files to add to RDoc such as README or doc/examples.txt
+ #
+ # When the user elects to generate the RDoc documentation for a gem (typically
+ # at install time), all the library files are sent to RDoc for processing.
+ # This option allows you to have some non-code files included for a more
+ # complete set of documentation.
+ #
+ # Usage:
+ #
+ # spec.extra_rdoc_files = ['README', 'doc/user-guide.txt']
+
+ def extra_rdoc_files
+ @extra_rdoc_files ||= []
+ end
+
+ ##
+ # The version of RubyGems that installed this gem. Returns
+ # <code>Gem::Version.new(0)</code> for gems installed by versions earlier
+ # than RubyGems 2.2.0.
+
+ def installed_by_version # :nodoc:
+ @installed_by_version ||= Gem::Version.new(0)
+ end
+
+ ##
+ # Sets the version of RubyGems that installed this gem. See also
+ # #installed_by_version.
+
+ def installed_by_version=(version) # :nodoc:
+ @installed_by_version = Gem::Version.new version
+ end
+
+ ##
+ # Specifies the rdoc options to be used when generating API documentation.
+ #
+ # Usage:
+ #
+ # spec.rdoc_options << '--title' << 'Rake -- Ruby Make' <<
+ # '--main' << 'README' <<
+ # '--line-numbers'
+
+ def rdoc_options
+ @rdoc_options ||= []
+ end
+
+ LATEST_RUBY_WITHOUT_PATCH_VERSIONS = Gem::Version.new("2.1")
+
+ ##
+ # The version of Ruby required by this gem. The ruby version can be
+ # specified to the patch-level:
+ #
+ # $ ruby -v -e 'p Gem.ruby_version'
+ # ruby 2.0.0p247 (2013-06-27 revision 41674) [x86_64-darwin12.4.0]
+ # #<Gem::Version "2.0.0.247">
+ #
+ # Prereleases can also be specified.
+ #
+ # Usage:
+ #
+ # # This gem will work with 1.8.6 or greater...
+ # spec.required_ruby_version = '>= 1.8.6'
+ #
+ # # Only with final releases of major version 2 where minor version is at least 3
+ # spec.required_ruby_version = '~> 2.3'
+ #
+ # # Only prereleases or final releases after 2.6.0.preview2
+ # spec.required_ruby_version = '> 2.6.0.preview2'
+ #
+ # # This gem will work with 2.3.0 or greater, including major version 3, but lesser than 4.0.0
+ # spec.required_ruby_version = '>= 2.3', '< 4'
+
+ def required_ruby_version=(req)
+ @required_ruby_version = Gem::Requirement.create req
+
+ @required_ruby_version.requirements.map! do |op, v|
+ if v >= LATEST_RUBY_WITHOUT_PATCH_VERSIONS && v.release.segments.size == 4
+ [op == "~>" ? "=" : op, Gem::Version.new(v.segments.tap {|s| s.delete_at(3) }.join("."))]
+ else
+ [op, v]
+ end
+ end
+ end
+
+ ##
+ # The RubyGems version required by this gem
+
+ def required_rubygems_version=(req)
+ @required_rubygems_version = Gem::Requirement.create req
+ end
+
+ ##
+ # Lists the external (to RubyGems) requirements that must be met for this gem
+ # to work. It's simply information for the user.
+ #
+ # Usage:
+ #
+ # spec.requirements << 'libmagick, v6.0'
+ # spec.requirements << 'A good graphics card'
+
+ def requirements
+ @requirements ||= []
+ end
+
+ ##
+ # A collection of unit test files. They will be loaded as unit tests when
+ # the user requests a gem to be unit tested.
+ #
+ # Usage:
+ # spec.test_files = Dir.glob('test/tc_*.rb')
+ # spec.test_files = ['tests/test-suite.rb']
+
+ def test_files=(files) # :nodoc:
+ @test_files = Array files
+ 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
+
+ ##
+ # True when this gemspec has been activated. This attribute is not persisted.
+
+ attr_accessor :activated
+
+ alias_method :activated?, :activated
+
+ ##
+ # Autorequire was used by old RubyGems to automatically require a file.
+ #
+ # Deprecated: It is neither supported nor functional.
+
+ attr_accessor :autorequire # :nodoc:
+
+ ##
+ # Allows deinstallation of gems with legacy platforms.
+
+ attr_writer :original_platform # :nodoc:
+
+ ##
+ # The Gem::Specification version of this gemspec.
+ #
+ # Do not set this, it is set automatically when the gem is packaged.
+
+ attr_accessor :specification_version
+
+ def self._all # :nodoc:
+ specification_record.all
+ end
+
+ def self.clear_load_cache # :nodoc:
+ @load_cache_mutex.synchronize do
+ @load_cache.clear
+ end
+ end
+ private_class_method :clear_load_cache
+
+ def self.gem_path # :nodoc:
+ Gem.path
+ end
+ private_class_method :gem_path
+
+ def self.each_gemspec(dirs) # :nodoc:
+ dirs.each do |dir|
+ Gem::Util.glob_files_in_dir("*.gemspec", dir).each do |path|
+ yield path
+ end
+ end
+ end
+
+ def self.gemspec_stubs_in(dir, pattern) # :nodoc:
+ Gem::Util.glob_files_in_dir(pattern, dir).map {|path| yield path }.select(&:valid?)
+ end
+
+ def self.each_spec(dirs) # :nodoc:
+ each_gemspec(dirs) do |path|
+ spec = self.load path
+ yield spec if spec
+ end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every installed gem
+
+ def self.stubs
+ specification_record.stubs
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for default gems
+
+ def self.default_stubs(pattern = "*.gemspec")
+ base_dir = Gem.default_dir
+ gems_dir = File.join base_dir, "gems"
+ gemspec_stubs_in(Gem.default_specifications_dir, pattern) do |path|
+ Gem::StubSpecification.default_gemspec_stub(path, base_dir, gems_dir)
+ end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for installed gem named +name+
+ # only returns stubs that match Gem.platforms
+
+ def self.stubs_for(name)
+ specification_record.stubs_for(name)
+ end
+
+ ##
+ # Finds stub specifications matching a pattern from the standard locations,
+ # optionally filtering out specs not matching the current platform
+ #
+ def self.stubs_for_pattern(pattern, match_platform = true) # :nodoc:
+ specification_record.stubs_for_pattern(pattern, match_platform)
+ end
+
+ def self._resort!(specs) # :nodoc:
+ specs.sort! do |a, b|
+ names = a.name <=> b.name
+ next names if names.nonzero?
+ versions = b.version <=> a.version
+ next versions if versions.nonzero?
+ platforms = Gem::Platform.sort_priority(b.platform) <=> Gem::Platform.sort_priority(a.platform)
+ next platforms if platforms.nonzero?
+ default_gem = a.default_gem_priority <=> b.default_gem_priority
+ next default_gem if default_gem.nonzero?
+ a.base_dir_priority(gem_path) <=> b.base_dir_priority(gem_path)
+ end
+ end
+
+ ##
+ # Loads the default specifications. It should be called only once.
+
+ def self.load_defaults
+ each_spec([Gem.default_specifications_dir]) do |spec|
+ # #load returns nil if the spec is bad, so we just ignore
+ # it at this stage
+ Gem.register_default_spec(spec)
+ end
+ end
+
+ ##
+ # Adds +spec+ to the known specifications, keeping the collection
+ # properly sorted.
+
+ def self.add_spec(spec)
+ specification_record.add_spec(spec)
+ end
+
+ ##
+ # Removes +spec+ from the known specs.
+
+ def self.remove_spec(spec)
+ specification_record.remove_spec(spec)
+ end
+
+ ##
+ # Returns all specifications. This method is discouraged from use.
+ # You probably want to use one of the Enumerable methods instead.
+
+ def self.all
+ warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless
+ Gem::Deprecate.skip
+ _all
+ end
+
+ ##
+ # Sets the known specs to +specs+.
+
+ def self.all=(specs)
+ specification_record.all = specs
+ end
+
+ ##
+ # Return full names of all specs in sorted order.
+
+ def self.all_names
+ specification_record.all_names
+ end
+
+ ##
+ # Return the list of all array-oriented instance variables.
+ #--
+ # Not sure why we need to use so much stupid reflection in here...
+
+ def self.array_attributes
+ @@array_attributes.dup
+ end
+
+ ##
+ # Return the list of all instance variables.
+ #--
+ # Not sure why we need to use so much stupid reflection in here...
+
+ def self.attribute_names
+ @@attributes.dup
+ end
+
+ ##
+ # Return the directories that Specification uses to find specs.
+
+ def self.dirs
+ @@dirs ||= Gem::SpecificationRecord.dirs_from(gem_path)
+ end
+
+ ##
+ # Set the directories that Specification uses to find specs. Setting
+ # this resets the list of known specs.
+
+ def self.dirs=(dirs)
+ reset
+
+ @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs))
+ end
+
+ extend Enumerable
+
+ ##
+ # Enumerate every known spec. See ::dirs= and ::add_spec to set the list of
+ # specs.
+
+ def self.each(&block)
+ specification_record.each(&block)
+ end
+
+ ##
+ # Returns every spec that matches +name+ and optional +requirements+.
+
+ def self.find_all_by_name(name, *requirements)
+ specification_record.find_all_by_name(name, *requirements)
+ end
+
+ ##
+ # Returns every spec that has the given +full_name+
+
+ def self.find_all_by_full_name(full_name)
+ stubs.select {|s| s.full_name == full_name }.map(&:to_spec)
+ end
+
+ ##
+ # Find the best specification matching a +name+ and +requirements+. Raises
+ # if the dependency doesn't resolve to a valid specification.
+
+ def self.find_by_name(name, *requirements)
+ requirements = Gem::Requirement.default if requirements.empty?
+
+ 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)
+ specification_record.find_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not loaded. This method is different than
+ # +find_inactive_by_path+ as it will filter out loaded specs by their name.
+
+ def self.find_unloaded_by_path(path)
+ specification_record.find_unloaded_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not activated.
+
+ def self.find_inactive_by_path(path)
+ specification_record.find_inactive_by_path(path)
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+, among
+ # those already activated.
+
+ def self.find_active_stub_by_path(path)
+ specification_record.find_active_stub_by_path(path)
+ end
+
+ ##
+ # Return currently unresolved specs that contain the file matching +path+.
+
+ def self.find_in_unresolved(path)
+ unresolved_specs.find_all {|spec| spec.contains_requirable_file? path }
+ end
+
+ ##
+ # Search through all unresolved deps and sub-dependencies and return
+ # specs that contain the file matching +path+.
+
+ def self.find_in_unresolved_tree(path)
+ unresolved_specs.each do |spec|
+ spec.traverse do |_from_spec, _dep, to_spec, trail|
+ if to_spec.has_conflicts? || to_spec.conflicts_when_loaded_with?(trail)
+ :next
+ else
+ return trail.reverse if to_spec.contains_requirable_file? path
+ end
+ end
+ end
+
+ []
+ end
+
+ def self.unresolved_specs
+ unresolved_deps.values.flat_map(&:to_specs)
+ end
+ private_class_method :unresolved_specs
+
+ ##
+ # Special loader for YAML files. When a Specification object is loaded
+ # from a YAML file, it bypasses the normal Ruby object initialization
+ # routine (#initialize). This method makes up for that and deals with
+ # gems of different ages.
+ #
+ # +input+ can be anything that YAML.load() accepts: String or IO.
+
+ def self.from_yaml(input)
+ Gem.load_yaml
+
+ input = normalize_yaml_input input
+ spec = Gem::SafeYAML.safe_load input
+
+ if spec && spec.class == FalseClass
+ raise Gem::EndOfYAMLException
+ end
+
+ unless Gem::Specification === spec
+ raise Gem::Exception, "YAML data doesn't evaluate to gem specification"
+ end
+
+ spec.specification_version ||= NONEXISTENT_SPECIFICATION_VERSION
+ spec.reset_nil_attributes_to_default
+ spec.flatten_require_paths
+
+ spec
+ end
+
+ ##
+ # Return the latest specs, optionally including prerelease specs if
+ # +prerelease+ is true.
+
+ def self.latest_specs(prerelease = false)
+ specification_record.latest_specs(prerelease)
+ end
+
+ ##
+ # Return the latest installed spec for gem +name+.
+
+ def self.latest_spec_for(name)
+ specification_record.latest_spec_for(name)
+ end
+
+ def self._latest_specs(specs, prerelease = false) # :nodoc:
+ result = {}
+
+ specs.reverse_each do |spec|
+ unless prerelease
+ next if spec.version.prerelease?
+ end
+
+ result[spec.name] = spec
+ end
+
+ result.flat_map(&:last).sort_by(&:name)
+ end
+
+ ##
+ # Loads Ruby format gemspec from +file+.
+
+ def self.load(file)
+ return unless file
+
+ spec = @load_cache_mutex.synchronize { @load_cache[file] }
+ return spec if spec
+
+ return unless File.file?(file)
+
+ code = Gem.open_file(file, "r:UTF-8:-", &:read)
+
+ begin
+ spec = eval code, binding, file
+
+ if Gem::Specification === spec
+ spec.loaded_from = File.expand_path file.to_s
+ @load_cache_mutex.synchronize do
+ prev = @load_cache[file]
+ if prev
+ spec = prev
+ else
+ @load_cache[file] = spec
+ end
+ end
+ return spec
+ end
+
+ warn "[#{file}] isn't a Gem::Specification (#{spec.class} instead)."
+ rescue SignalException, SystemExit
+ raise
+ rescue SyntaxError, StandardError => e
+ warn "Invalid gemspec in [#{file}]: #{e}"
+ end
+
+ nil
+ end
+
+ ##
+ # Specification attributes that must be non-nil
+
+ def self.non_nil_attributes
+ @@non_nil_attributes.dup
+ end
+
+ ##
+ # Make sure the YAML specification is properly formatted with dashes
+
+ def self.normalize_yaml_input(input)
+ result = input.respond_to?(:read) ? input.read : input
+ result = "--- " + result unless result.start_with?("--- ")
+ result = result.dup
+ result.gsub!(/ !!null \n/, " \n")
+ # date: 2011-04-26 00:00:00.000000000Z
+ # date: 2011-04-26 00:00:00.000000000 Z
+ result.gsub!(/^(date: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+?)Z/, '\1 Z')
+ result
+ end
+
+ ##
+ # Return a list of all outdated local gem names. This method is HEAVY
+ # as it must go fetch specifications from the server.
+ #
+ # Use outdated_and_latest_version if you wish to retrieve the latest remote
+ # version as well.
+
+ def self.outdated
+ outdated_and_latest_version.map {|local, _| local.name }
+ end
+
+ ##
+ # Enumerates the outdated local gems yielding the local specification and
+ # the latest remote version.
+ #
+ # This method may take some time to return as it must check each local gem
+ # against the server's index.
+
+ def self.outdated_and_latest_version
+ return enum_for __method__ unless block_given?
+
+ # TODO: maybe we should switch to rubygems' version service?
+ fetcher = Gem::SpecFetcher.fetcher
+
+ latest_specs(true).each do |local_spec|
+ dependency =
+ Gem::Dependency.new local_spec.name, ">= #{local_spec.version}"
+
+ remotes, = fetcher.search_for_dependency dependency
+ remotes = remotes.map {|n, _| n.version }
+
+ latest_remote = remotes.sort.last
+
+ yield [local_spec, latest_remote] if
+ latest_remote && local_spec.version < latest_remote
+ end
+
+ nil
+ end
+
+ ##
+ # Is +name+ a required attribute?
+
+ def self.required_attribute?(name)
+ @@required_attributes.include? name.to_sym
+ end
+
+ ##
+ # Required specification attributes
+
+ def self.required_attributes
+ @@required_attributes.dup
+ end
+
+ ##
+ # Reset the list of known specs, running pre and post reset hooks
+ # registered in Gem.
+
+ def self.reset
+ @@dirs = nil
+ Gem.pre_reset_hooks.each(&:call)
+ @specification_record = nil
+ clear_load_cache
+
+ unless unresolved_deps.empty?
+ unresolved = unresolved_deps.filter_map do |name, dep|
+ matching_versions = find_all_by_name(name)
+ next if dep.latest_version? && matching_versions.any?(&:default_gem?)
+
+ [dep, matching_versions.uniq(&:full_name)]
+ end.to_h
+
+ unless unresolved.empty?
+ warn "WARN: Unresolved or ambiguous specs during Gem::Specification.reset:"
+ unresolved.each do |dep, versions|
+ warn " #{dep}"
+
+ unless versions.empty?
+ warn " Available/installed versions of this gem:"
+ versions.each {|s| warn " - #{s.version}" }
+ end
+ end
+ warn "WARN: Clearing out unresolved specs. Try 'gem cleanup <gem>'"
+ warn "Please report a bug if this causes problems."
+ end
+
+ unresolved_deps.clear
+ end
+ Gem.post_reset_hooks.each(&:call)
+ end
+
+ ##
+ # Keeps track of all currently known specifications
+
+ def self.specification_record
+ @specification_record ||= Gem::SpecificationRecord.new(dirs)
+ end
+
+ # DOC: This method needs documented or nodoc'd
+ def self.unresolved_deps
+ @unresolved_deps ||= Hash.new {|h, n| h[n] = Gem::Dependency.new n }
+ end
+
+ ##
+ # Load custom marshal format, re-initializing defaults as needed
+
+ def self._load(str)
+ Gem.load_yaml
+ Gem.load_safe_marshal
+
+ yaml_set = false
+ retry_count = 0
+
+ array = begin
+ Gem::SafeMarshal.safe_load str
+ rescue ArgumentError => e
+ # Avoid an infinite retry loop when the argument error has nothing to do
+ # with the classes not being defined.
+ # 1 retry each allowed in case all 3 of
+ # - YAML
+ # - YAML::Syck::DefaultKey
+ # - YAML::PrivateType
+ # need to be defined
+ raise if retry_count >= 3
+
+ #
+ # Some very old marshaled specs included references to `YAML::PrivateType`
+ # and `YAML::Syck::DefaultKey` constants due to bugs in the old emitter
+ # that generated them. Workaround the issue by defining the necessary
+ # constants and retrying.
+ #
+ message = e.message
+ raise unless message.include?("YAML::")
+
+ unless Object.const_defined?(:YAML)
+ Object.const_set "YAML", Module.new
+ yaml_set = true
+ end
+
+ if message.include?("YAML::Syck::")
+ YAML.const_set "Syck", YAML unless YAML.const_defined?(:Syck)
+
+ YAML::Syck.const_set "DefaultKey", Class.new if message.include?("YAML::Syck::DefaultKey") && !YAML::Syck.const_defined?(:DefaultKey)
+ elsif message.include?("YAML::PrivateType") && !YAML.const_defined?(:PrivateType)
+ YAML.const_set "PrivateType", Class.new { attr_accessor :type_id, :value }
+ end
+
+ retry_count += 1
+ retry
+ ensure
+ Object.__send__(:remove_const, "YAML") if yaml_set
+ end
+
+ spec = Gem::Specification.new
+ spec.instance_variable_set :@specification_version, array[1]
+
+ current_version = CURRENT_SPECIFICATION_VERSION
+
+ field_count = if spec.specification_version > current_version
+ spec.instance_variable_set :@specification_version,
+ current_version
+ MARSHAL_FIELDS[current_version]
+ else
+ MARSHAL_FIELDS[spec.specification_version]
+ end
+
+ if array.size < field_count
+ raise TypeError, "invalid Gem::Specification format #{array.inspect}"
+ end
+
+ spec.instance_variable_set :@rubygems_version, array[0]
+ # spec version
+ spec.instance_variable_set :@name, array[2]
+ spec.instance_variable_set :@version, array[3]
+ spec.date = array[4]
+ spec.instance_variable_set :@summary, array[5]
+ spec.instance_variable_set :@required_ruby_version, array[6]
+ spec.instance_variable_set :@required_rubygems_version, array[7]
+ spec.platform = array[8]
+ spec.instance_variable_set :@dependencies, array[9]
+ # offset due to rubyforge_project removal
+ spec.instance_variable_set :@email, array[11]
+ spec.instance_variable_set :@authors, array[12]
+ spec.instance_variable_set :@description, array[13]
+ spec.instance_variable_set :@homepage, array[14]
+ # offset due to has_rdoc removal
+ spec.instance_variable_set :@licenses, array[17]
+ spec.instance_variable_set :@metadata, array[18]
+ spec.instance_variable_set :@loaded, false
+ spec.instance_variable_set :@activated, false
+
+ spec
+ end
+
+ def <=>(other) # :nodoc:
+ sort_obj <=> other.sort_obj
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ name == other.name &&
+ version == other.version &&
+ platform == other.platform
+ end
+
+ ##
+ # Dump only crucial instance variables.
+ #--
+ # MAINTAIN ORDER!
+ # (down with the man)
+
+ def _dump(limit)
+ Marshal.dump [
+ @rubygems_version,
+ @specification_version,
+ @name,
+ @version,
+ date,
+ @summary,
+ @required_ruby_version,
+ @required_rubygems_version,
+ @original_platform,
+ @dependencies,
+ "", # rubyforge_project
+ @email,
+ @authors,
+ @description,
+ @homepage,
+ true, # has_rdoc
+ @new_platform,
+ @licenses,
+ @metadata,
+ ]
+ end
+
+ ##
+ # Activate this spec, registering it as a loaded spec and adding
+ # it's lib paths to $LOAD_PATH. Returns true if the spec was
+ # activated, false if it was previously activated. Freaks out if
+ # there are conflicts upon activation.
+
+ def activate
+ other = Gem.loaded_specs[name]
+ if other
+ check_version_conflict other
+ return false
+ end
+
+ raise_if_conflicts
+
+ activate_dependencies
+ add_self_to_load_path
+
+ Gem.loaded_specs[name] = self
+ @activated = true
+ @loaded = true
+
+ true
+ end
+
+ ##
+ # Activate all unambiguously resolved runtime dependencies of this
+ # spec. Add any ambiguous dependencies to the unresolved list to be
+ # resolved later, as needed.
+
+ def activate_dependencies
+ unresolved = Gem::Specification.unresolved_deps
+
+ runtime_dependencies.each do |spec_dep|
+ if loaded = Gem.loaded_specs[spec_dep.name]
+ next if spec_dep.matches_spec? loaded
+
+ msg = "can't satisfy '#{spec_dep}', already activated '#{loaded.full_name}'"
+ e = Gem::LoadError.new msg
+ e.name = spec_dep.name
+
+ raise e
+ end
+
+ specs = spec_dep.matching_specs(true).uniq(&:full_name)
+
+ if specs.size == 0
+ raise Gem::MissingSpecError.new(spec_dep.name, spec_dep.requirement, "at: #{spec_file}")
+ elsif specs.size == 1
+ specs.first.activate
+ else
+ name = spec_dep.name
+ unresolved[name] = unresolved[name].merge spec_dep
+ end
+ end
+
+ unresolved.delete self.name
+ end
+
+ ##
+ # Abbreviate the spec for downloading. Abbreviated specs are only used for
+ # searching, downloading and related activities and do not need deployment
+ # specific information (e.g. list of files). So we abbreviate the spec,
+ # making it much smaller for quicker downloads.
+
+ def abbreviate
+ self.files = []
+ self.test_files = []
+ self.rdoc_options = []
+ self.extra_rdoc_files = []
+ self.cert_chain = []
+ end
+
+ ##
+ # Sanitize the descriptive fields in the spec. Sometimes non-ASCII
+ # characters will garble the site index. Non-ASCII characters will
+ # be replaced by their XML entity equivalent.
+
+ def sanitize
+ self.summary = sanitize_string(summary)
+ self.description = sanitize_string(description)
+ self.post_install_message = sanitize_string(post_install_message)
+ self.authors = authors.collect {|a| sanitize_string(a) }
+ end
+
+ ##
+ # Sanitize a single string.
+
+ def sanitize_string(string)
+ return string unless string
+
+ # 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
+ end
+
+ ##
+ # Returns an array with bindir attached to each executable in the
+ # +executables+ list
+
+ def add_bindir(executables)
+ return nil if executables.nil?
+
+ if @bindir
+ Array(executables).map {|e| File.join(@bindir, e) }
+ else
+ executables
+ end
+ rescue StandardError
+ nil
+ end
+
+ ##
+ # Adds a dependency on gem +dependency+ with type +type+ that requires
+ # +requirements+. Valid types are currently <tt>:runtime</tt> and
+ # <tt>:development</tt>.
+
+ def add_dependency_with_type(dependency, type, requirements)
+ requirements = if requirements.empty?
+ Gem::Requirement.default
+ else
+ requirements.flatten
+ end
+
+ unless dependency.respond_to?(:name) &&
+ dependency.respond_to?(:requirement)
+ dependency = Gem::Dependency.new(dependency.to_s, requirements, type)
+ end
+
+ dependencies << dependency
+ end
+
+ private :add_dependency_with_type
+
+ alias_method :add_runtime_dependency, :add_dependency
+
+ ##
+ # Adds this spec's require paths to LOAD_PATH, in the proper location.
+
+ def add_self_to_load_path
+ return if default_gem?
+
+ paths = full_require_paths
+
+ Gem.add_to_load_path(*paths)
+ end
+
+ ##
+ # Singular reader for #authors. Returns the first author in the list
+
+ def author
+ (val = authors) && val.first
+ end
+
+ ##
+ # The list of author names who wrote this gem.
+ #
+ # spec.authors = ['Chad Fowler', 'Jim Weirich', 'Rich Kilmer']
+
+ def authors
+ @authors ||= []
+ end
+
+ ##
+ # Returns the full path to installed gem's bin directory.
+ #
+ # NOTE: do not confuse this with +bindir+, which is just 'bin', not
+ # a full path.
+
+ def bin_dir
+ @bin_dir ||= File.join gem_dir, bindir
+ end
+
+ ##
+ # Returns the full path to an executable named +name+ in this gem.
+
+ def bin_file(name)
+ File.join bin_dir, name
+ end
+
+ ##
+ # Returns the build_args used to install the gem
+
+ def build_args
+ if File.exist? build_info_file
+ build_info = File.readlines build_info_file
+ build_info = build_info.map(&:strip)
+ build_info.delete ""
+ build_info
+ else
+ []
+ end
+ end
+
+ ##
+ # Builds extensions for this platform if the gem has extensions listed and
+ # the gem.build_complete file is missing.
+
+ 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 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
+ # extension we are about to build.
+ unresolved_deps = Gem::Specification.unresolved_deps.dup
+ Gem::Specification.unresolved_deps.clear
+
+ require_relative "config_file"
+ require_relative "ext"
+ require_relative "user_interaction"
+
+ ui = Gem::SilentUI.new
+ Gem::DefaultUserInteraction.use_ui ui do
+ builder = Gem::Ext::Builder.new self
+ builder.build_extensions
+ end
+ ensure
+ ui&.close
+ Gem::Specification.unresolved_deps.replace unresolved_deps
+ end
+ end
+
+ ##
+ # Returns the full path to the build info directory
+
+ def build_info_dir
+ File.join base_dir, "build_info"
+ end
+
+ ##
+ # Returns the full path to the file containing the build
+ # information generated when the gem was installed
+
+ def build_info_file
+ File.join build_info_dir, "#{full_name}.info"
+ end
+
+ ##
+ # Returns the full path to the cache directory containing this
+ # spec's cached gem.
+
+ def cache_dir
+ File.join base_dir, "cache"
+ end
+
+ ##
+ # Returns the full path to the cached gem for this spec.
+
+ def cache_file
+ File.join cache_dir, "#{full_name}.gem"
+ end
+
+ ##
+ # Return any possible conflicts against the currently loaded specs.
+
+ def conflicts
+ conflicts = {}
+ runtime_dependencies.each do |dep|
+ spec = Gem.loaded_specs[dep.name]
+ if spec && !spec.satisfies_requirement?(dep)
+ (conflicts[spec] ||= []) << dep
+ end
+ end
+ env_req = Gem.env_requirement(name)
+ (conflicts[self] ||= []) << env_req unless env_req.satisfied_by? version
+ conflicts
+ end
+
+ ##
+ # return true if there will be conflict when spec if loaded together with the list of specs.
+
+ def conflicts_when_loaded_with?(list_of_specs) # :nodoc:
+ result = list_of_specs.any? do |spec|
+ spec.runtime_dependencies.any? {|dep| (dep.name == name) && !satisfies_requirement?(dep) }
+ end
+ result
+ end
+
+ ##
+ # Return true if there are possible conflicts against the currently loaded specs.
+
+ def has_conflicts?
+ return true unless Gem.env_requirement(name).satisfied_by?(version)
+ 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.
+ #
+ # If SOURCE_DATE_EPOCH is set as an environment variable, use that to support
+ # reproducible builds; otherwise, default to the current UTC date.
+ #
+ # Details on SOURCE_DATE_EPOCH:
+ # https://reproducible-builds.org/specs/source-date-epoch/
+
+ def date
+ @date ||= Time.utc(*Gem.source_date_epoch.utc.to_a[3..5].reverse)
+ end
+
+ DateLike = Object.new # :nodoc:
+ def DateLike.===(obj) # :nodoc:
+ defined?(::Date) && Date === obj
+ end
+
+ DateTimeFormat = # :nodoc:
+ /\A
+ (\d{4})-(\d{2})-(\d{2})
+ (\s+ \d{2}:\d{2}:\d{2}\.\d+ \s* (Z | [-+]\d\d:\d\d) )?
+ \Z/x
+
+ ##
+ # The date this gem was created
+ #
+ # DO NOT set this, it is set automatically when the gem is packaged.
+
+ def date=(date)
+ # We want to end up with a Time object with one-day resolution.
+ # 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
+ end
+ end
+
+ ##
+ # The default value for specification attribute +name+
+
+ def default_value(name)
+ @@default_value[name]
+ end
+
+ ##
+ # A list of Gem::Dependency objects this gem depends on.
+ #
+ # Use #add_dependency or #add_development_dependency to add dependencies to
+ # a gem.
+
+ def dependencies
+ @dependencies ||= []
+ end
+
+ ##
+ # Return a list of all gems that have a dependency on this gemspec. The
+ # list is structured with entries that conform to:
+ #
+ # [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]]
+
+ def dependent_gems(check_dev = true)
+ out = []
+ Gem::Specification.each do |spec|
+ deps = check_dev ? spec.dependencies : spec.runtime_dependencies
+ deps.each do |dep|
+ next unless satisfies_requirement?(dep)
+ sats = []
+ find_all_satisfiers(dep) do |sat|
+ sats << sat
+ end
+ out << [spec, dep, sats]
+ end
+ end
+ out
+ end
+
+ ##
+ # Returns all specs that matches this spec's runtime dependencies.
+
+ def dependent_specs
+ runtime_dependencies.flat_map(&:to_specs)
+ end
+
+ ##
+ # A detailed description of this gem. See also #summary
+
+ def description=(str)
+ @description = str.to_s
+ end
+
+ ##
+ # List of dependencies that are used for development
+
+ def development_dependencies
+ dependencies.select {|d| d.type == :development }
+ end
+
+ ##
+ # Returns the full path to this spec's documentation directory. If +type+
+ # is given it will be appended to the end. For example:
+ #
+ # spec.doc_dir # => "/path/to/gem_repo/doc/a-1"
+ #
+ # spec.doc_dir 'ri' # => "/path/to/gem_repo/doc/a-1/ri"
+
+ def doc_dir(type = nil)
+ @doc_dir ||= File.join base_dir, "doc", full_name
+
+ if type
+ File.join @doc_dir, type
+ else
+ @doc_dir
+ end
+ end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "version", @version
+ coder.add "platform", platform.to_s
+ coder.add "original_platform", original_platform.to_s if platform.to_s != original_platform.to_s
+
+ attributes = @@attributes.map(&:to_s) - %w[name version platform]
+ attributes.each do |name|
+ value = instance_variable_get("@#{name}")
+ coder.add name, value unless value.nil?
+ end
+ end
+
+ def eql?(other) # :nodoc:
+ self.class === other && same_attributes?(other)
+ end
+
+ ##
+ # Singular accessor for #executables
+
+ def executable
+ (val = executables) && val.first
+ end
+
+ ##
+ # Singular accessor for #executables
+
+ def executable=(o)
+ self.executables = [o]
+ end
+
+ ##
+ # Sets executables to +value+, ensuring it is an array.
+
+ def executables=(value)
+ @executables = Array(value)
+ end
+
+ ##
+ # Sets extensions to +extensions+, ensuring it is an array.
+
+ def extensions=(extensions)
+ @extensions = Array extensions
+ end
+
+ ##
+ # Sets extra_rdoc_files to +files+, ensuring it is an array.
+
+ def extra_rdoc_files=(files)
+ @extra_rdoc_files = Array files
+ end
+
+ ##
+ # The default (generated) file name of the gem. See also #spec_name.
+ #
+ # spec.file_name # => "example-1.0.gem"
+
+ def file_name
+ "#{full_name}.gem"
+ end
+
+ ##
+ # Sets files to +files+, ensuring it is an array.
+
+ def files=(files)
+ @files = Array files
+ end
+
+ ##
+ # Finds all gems that satisfy +dep+
+
+ def find_all_satisfiers(dep)
+ Gem::Specification.each do |spec|
+ yield spec if spec.satisfies_requirement? dep
+ end
+ end
+
+ private :find_all_satisfiers
+
+ ##
+ # Creates a duplicate spec without large blobs that aren't used at runtime.
+
+ def for_cache
+ spec = dup
+
+ spec.files = nil
+ spec.test_files = nil
+
+ spec
+ end
+
+ ##
+ # Work around old bundler versions removing my methods
+ # Can be removed once RubyGems can no longer install Bundler 2.5
+
+ def gem_dir # :nodoc:
+ super
+ end
+
+ def gems_dir
+ @gems_dir ||= File.join(base_dir, "gems")
+ end
+
+ ##
+ # True if this gem has files in test_files
+
+ def has_unit_tests? # :nodoc:
+ !test_files.empty?
+ end
+
+ # :stopdoc:
+ alias_method :has_test_suite?, :has_unit_tests?
+ # :startdoc:
+
+ def hash # :nodoc:
+ name.hash ^ version.hash
+ end
+
+ def init_with(coder) # :nodoc:
+ @installed_by_version ||= nil
+ yaml_initialize coder.tag, coder.map
+ end
+
+ eval <<-RUBY, binding, __FILE__, __LINE__ + 1
+ # frozen_string_literal: true
+
+ def set_nil_attributes_to_nil
+ #{@@nil_attributes.map {|key| "@#{key} = nil" }.join "; "}
+ end
+ private :set_nil_attributes_to_nil
+
+ def set_not_nil_attributes_to_default_values
+ #{@@non_nil_attributes.map {|key| "@#{key} = #{INITIALIZE_CODE_FOR_DEFAULTS[key]}" }.join ";"}
+ end
+ private :set_not_nil_attributes_to_default_values
+ RUBY
+
+ ##
+ # Specification constructor. Assigns the default values to the attributes
+ # and yields itself for further initialization. Optionally takes +name+ and
+ # +version+.
+
+ def initialize(name = nil, version = nil)
+ super()
+ @gems_dir = nil
+ @base_dir = nil
+ @loaded = false
+ @activated = false
+ @loaded_from = nil
+ @original_platform = nil
+ @installed_by_version = nil
+
+ set_nil_attributes_to_nil
+ set_not_nil_attributes_to_default_values
+
+ @new_platform = Gem::Platform::RUBY
+
+ self.name = name if name
+ self.version = version if version
+
+ if (platform = Gem.platforms.last) && platform != Gem::Platform::RUBY && platform != Gem::Platform.local
+ self.platform = platform
+ end
+
+ yield self if block_given?
+ end
+
+ ##
+ # 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|
+ name = :"@#{name}"
+ next unless other_spec.instance_variable_defined? name
+
+ begin
+ val = other_spec.instance_variable_get(name)
+ if val
+ instance_variable_set name, val.dup
+ elsif Gem.configuration.really_verbose
+ warn "WARNING: #{full_name} has an invalid nil value for #{name}"
+ end
+ rescue TypeError
+ e = Gem::FormatException.new \
+ "#{full_name} has an invalid value for #{name}"
+
+ e.file_path = loaded_from
+ raise e
+ end
+ end
+
+ @required_ruby_version = other_spec.required_ruby_version.dup
+ @required_rubygems_version = other_spec.required_rubygems_version.dup
+ end
+
+ def base_dir
+ return Gem.dir unless loaded_from
+ @base_dir ||= if default_gem?
+ File.dirname File.dirname File.dirname loaded_from
+ else
+ File.dirname File.dirname loaded_from
+ end
+ end
+
+ def inspect # :nodoc:
+ if $DEBUG
+ super
+ else
+ "#{super[0..-2]} #{full_name}>"
+ end
+ end
+
+ ##
+ # Files in the Gem under one of the require_paths
+
+ def lib_files
+ @files.select do |file|
+ require_paths.any? do |path|
+ file.start_with? path
+ end
+ end
+ end
+
+ ##
+ # Singular accessor for #licenses
+
+ def license
+ licenses.first
+ end
+
+ ##
+ # Plural accessor for setting licenses
+ #
+ # See #license= for details
+
+ def licenses
+ @licenses ||= []
+ end
+
+ def internal_init # :nodoc:
+ super
+ @bin_dir = nil
+ @doc_dir = nil
+ @ri_dir = nil
+ @spec_dir = nil
+ @spec_file = nil
+ end
+
+ ##
+ # Track removed method calls to warn about during build time.
+ # Warn about unknown attributes while loading a spec.
+
+ def method_missing(sym, *a, &b) # :nodoc:
+ if REMOVED_METHODS.include?(sym)
+ removed_method_calls << sym
+ return
+ end
+
+ if @specification_version > CURRENT_SPECIFICATION_VERSION &&
+ sym.to_s.end_with?("=")
+ warn "ignoring #{sym} loading #{full_name}" if $DEBUG
+ else
+ super
+ end
+ end
+
+ ##
+ # Is this specification missing its extensions? When this returns true you
+ # probably want to build_extensions
+
+ def missing_extensions?
+ return false if RUBY_ENGINE == "jruby"
+ return false if extensions.empty?
+ return false if default_gem?
+ return false if File.exist? gem_build_complete_path
+
+ true
+ end
+
+ ##
+ # Normalize the list of files so that:
+ # * All file lists have redundancies removed.
+ # * Files referenced in the extra_rdoc_files are included in the package
+ # file list.
+
+ def normalize
+ if defined?(@extra_rdoc_files) && @extra_rdoc_files
+ @extra_rdoc_files.uniq!
+ @files ||= []
+ @files.concat(@extra_rdoc_files)
+ end
+
+ @files = @files.uniq.sort if @files
+ @extensions = @extensions.uniq.sort if @extensions
+ @test_files = @test_files.uniq.sort if @test_files
+ @executables = @executables.uniq.sort if @executables
+ @extra_rdoc_files = @extra_rdoc_files.uniq.sort if @extra_rdoc_files
+ end
+
+ ##
+ # Return a NameTuple that represents this Specification
+
+ def name_tuple
+ Gem::NameTuple.new name, version, original_platform
+ end
+
+ ##
+ # Returns the full name (name-version) of this gemspec using the original
+ # platform. For use with legacy gems.
+
+ def original_name # :nodoc:
+ if platform == Gem::Platform::RUBY || platform.nil?
+ "#{@name}-#{@version}"
+ else
+ "#{@name}-#{@version}-#{@original_platform}"
+ end
+ end
+
+ ##
+ # Cruft. Use +platform+.
+
+ def original_platform # :nodoc:
+ @original_platform ||= platform
+ end
+
+ ##
+ # The platform this gem runs on. See Gem::Platform for details.
+
+ def platform
+ @new_platform ||= Gem::Platform::RUBY # rubocop:disable Naming/MemoizedInstanceVariableName
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group 2, "Gem::Specification.new do |s|", "end" do
+ q.breakable
+
+ attributes = @@attributes - [:name, :version]
+ attributes.unshift :installed_by_version
+ attributes.unshift :version
+ attributes.unshift :name
+
+ attributes.each do |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} = "
+
+ 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
+ end
+ end
+ end
+
+ ##
+ # Raise an exception if the version of this spec conflicts with the one
+ # that is already loaded (+other+)
+
+ def check_version_conflict(other) # :nodoc:
+ 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.
+
+ msg = "can't activate #{full_name}, already activated #{other.full_name}"
+
+ e = Gem::LoadError.new msg
+ e.name = name
+
+ raise e
+ end
+
+ private :check_version_conflict
+
+ ##
+ # Check the spec for possible conflicts and freak out if there are any.
+
+ def raise_if_conflicts # :nodoc:
+ if has_conflicts?
+ raise Gem::ConflictError.new self, conflicts
+ end
+ end
+
+ ##
+ # Sets rdoc_options to +value+, ensuring it is a flat array of strings.
+ # Handles malformed gemspecs where rdoc_options may be a Hash or contain Hashes.
+
+ def rdoc_options=(options)
+ @rdoc_options = Array(options).flat_map do |opt|
+ opt.is_a?(Hash) ? opt.to_a.flatten.map(&:to_s) : opt
+ end
+ end
+
+ ##
+ # Singular accessor for #require_paths
+
+ def require_path
+ (val = require_paths) && val.first
+ end
+
+ ##
+ # Singular accessor for #require_paths
+
+ def require_path=(path)
+ self.require_paths = Array(path)
+ end
+
+ ##
+ # Set requirements to +req+, ensuring it is an array.
+
+ def requirements=(req)
+ @requirements = Array req
+ end
+
+ def respond_to_missing?(m, include_private = false) # :nodoc:
+ false
+ end
+
+ ##
+ # Returns the full path to this spec's ri directory.
+
+ def ri_dir
+ @ri_dir ||= File.join base_dir, "ri", full_name
+ end
+
+ ##
+ # Return a string containing a Ruby code representation of the given
+ # object.
+
+ def ruby_code(obj)
+ case obj
+ when String then obj.dump + ".freeze"
+ 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 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(#{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)})"
+ else raise Gem::Exception, "ruby_code case not handled: #{obj.class}"
+ end
+ end
+
+ private :ruby_code
+
+ ##
+ # List of dependencies that will automatically be activated at runtime.
+
+ def runtime_dependencies
+ dependencies.select(&:runtime?)
+ end
+
+ ##
+ # True if this gem has the same attributes as +other+.
+
+ def same_attributes?(spec)
+ @@attributes.all? {|name, _default| send(name) == spec.send(name) }
+ end
+
+ private :same_attributes?
+
+ ##
+ # Checks if this specification meets the requirement of +dependency+.
+
+ def satisfies_requirement?(dependency)
+ @name == dependency.name &&
+ dependency.requirement.satisfied_by?(@version)
+ end
+
+ ##
+ # Returns an object you can use to sort specifications in #sort_by.
+
+ def sort_obj
+ [@name, @version, Gem::Platform.sort_priority(@new_platform)]
+ end
+
+ ##
+ # Used by Gem::Resolver to order Gem::Specification objects
+
+ def source # :nodoc:
+ Gem::Source::Installed.new
+ end
+
+ ##
+ # Returns the full path to the directory containing this spec's
+ # gemspec file. eg: /usr/local/lib/ruby/gems/1.8/specifications
+
+ def spec_dir
+ @spec_dir ||= File.join base_dir, "specifications"
+ end
+
+ ##
+ # Returns the full path to this spec's gemspec file.
+ # eg: /usr/local/lib/ruby/gems/1.8/specifications/mygem-1.0.gemspec
+
+ def spec_file
+ @spec_file ||= File.join spec_dir, "#{full_name}.gemspec"
+ end
+
+ ##
+ # The default name of the gemspec. See also #file_name
+ #
+ # spec.spec_name # => "example-1.0.gemspec"
+
+ def spec_name
+ "#{full_name}.gemspec"
+ end
+
+ ##
+ # A short summary of this gem's description.
+
+ def summary=(str)
+ @summary = str.to_s.strip.
+ gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').gsub(/\n[ \t]*/, " ") # so. weird.
+ end
+
+ ##
+ # Singular accessor for #test_files
+
+ def test_file # :nodoc:
+ (val = test_files) && val.first
+ end
+
+ ##
+ # Singular mutator for #test_files
+
+ def test_file=(file) # :nodoc:
+ self.test_files = [file]
+ end
+
+ ##
+ # Test files included in this gem. You cannot append to this accessor, you
+ # must assign to it.
+
+ def test_files # :nodoc:
+ # Handle the possibility that we have @test_suite_file but not
+ # @test_files. This will happen when an old gem is loaded via
+ # YAML.
+ if defined? @test_suite_file
+ @test_files = [@test_suite_file].flatten
+ @test_suite_file = nil
+ end
+ if defined?(@test_files) && @test_files
+ @test_files
+ else
+ @test_files = []
+ end
+ end
+
+ ##
+ # Returns a Ruby code representation of this specification, such that it can
+ # be eval'ed and reconstruct the same specification later. Attributes that
+ # still have their default values are omitted.
+
+ def to_ruby
+ result = []
+ result << "# -*- encoding: utf-8 -*-"
+ result << "#{Gem::StubSpecification::PREFIX}#{name} #{version} #{platform} #{raw_require_paths.join("\0")}"
+ result << "#{Gem::StubSpecification::PREFIX}#{extensions.join "\0"}" unless
+ extensions.empty?
+ result << nil
+ result << "Gem::Specification.new do |s|"
+
+ result << " s.name = #{ruby_code name}"
+ result << " s.version = #{ruby_code version}"
+ unless platform.nil? || platform == Gem::Platform::RUBY
+ result << " s.platform = #{ruby_code original_platform}"
+ end
+ result << ""
+ result << " s.required_rubygems_version = #{ruby_code required_rubygems_version} if s.respond_to? :required_rubygems_version="
+
+ if metadata && !metadata.empty?
+ result << " s.metadata = #{ruby_code metadata} if s.respond_to? :metadata="
+ end
+ result << " s.require_paths = #{ruby_code raw_require_paths}"
+
+ handled = [
+ :dependencies,
+ :name,
+ :platform,
+ :require_paths,
+ :required_rubygems_version,
+ :specification_version,
+ :version,
+ :metadata,
+ :signing_key,
+ ]
+
+ @@attributes.each do |attr_name|
+ next if handled.include? 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 = #{ruby_code signing_key}"
+ end
+
+ if @installed_by_version
+ result << nil
+ result << " s.installed_by_version = #{ruby_code Gem::VERSION}"
+ end
+
+ unless dependencies.empty?
+ result << nil
+ result << " s.specification_version = #{specification_version}"
+ result << nil
+
+ dependencies.each do |dep|
+ dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK
+ result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>.freeze, #{ruby_code dep.requirements_list})"
+ end
+ end
+
+ result << "end"
+ result << nil
+
+ result.join "\n"
+ end
+
+ ##
+ # Returns a Ruby lighter-weight code representation of this specification,
+ # used for indexing only.
+ #
+ # See #to_ruby.
+
+ def to_ruby_for_cache
+ for_cache.to_ruby
+ end
+
+ def to_s # :nodoc:
+ "#<Gem::Specification name=#{@name} version=#{@version}>"
+ end
+
+ ##
+ # Returns self
+
+ def to_spec
+ self
+ end
+
+ def to_yaml(opts = {}) # :nodoc:
+ Gem.load_yaml
+
+ if Gem.use_psych?
+ # Because the user can switch the YAML engine behind our
+ # back, we have to check again here to make sure that our
+ # psych code was properly loaded, and load it if not.
+ unless Gem.const_defined?(:NoAliasYAMLTree)
+ require_relative "psych_tree"
+ end
+
+ builder = Gem::NoAliasYAMLTree.create
+ builder << self
+ ast = builder.tree
+
+ require "stringio"
+ io = StringIO.new
+ io.set_encoding Encoding::UTF_8
+
+ Psych::Visitors::Emitter.new(io).accept(ast)
+
+ io.string.gsub(/ !!null \n/, " \n")
+ else
+ Gem::YAMLSerializer.dump(self)
+ end
+ end
+
+ ##
+ # Recursively walk dependencies of this spec, executing the +block+ for each
+ # hop.
+
+ def traverse(trail = [], visited = {}, &block)
+ trail.push(self)
+ begin
+ runtime_dependencies.each do |dep|
+ dep.matching_specs(true).each do |dep_spec|
+ next if visited.key?(dep_spec)
+ visited[dep_spec] = true
+ trail.push(dep_spec)
+ begin
+ result = block[self, dep, dep_spec, trail]
+ ensure
+ trail.pop
+ 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
+ trail.pop
+ end
+ end
+
+ ##
+ # Checks that the specification contains all required fields, and does a
+ # very basic sanity check.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks..
+
+ def validate(packaging = true, strict = false)
+ normalize
+
+ validation_policy = Gem::SpecificationPolicy.new(self)
+ validation_policy.packaging = packaging
+ validation_policy.validate(strict)
+ end
+
+ def keep_only_files_and_directories
+ @executables.delete_if {|x| File.directory?(File.join(@bindir, x)) }
+ @extensions.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @extra_rdoc_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ @test_files.delete_if {|x| File.directory?(x) && !File.symlink?(x) }
+ end
+
+ def validate_for_resolution
+ Gem::SpecificationPolicy.new(self).validate_for_resolution
+ end
+
+ ##
+ # Set the version to +version+.
+
+ def version=(version)
+ @version = version.nil? ? version : Gem::Version.create(version)
+ end
+
+ def stubbed?
+ false
+ end
+
+ def yaml_initialize(tag, vals) # :nodoc:
+ vals.each do |ivar, val|
+ case ivar
+ when "date"
+ # Force Date to go through the extra coerce logic in date=
+ self.date = val
+ when "platform"
+ self.platform = val
+ when "rdoc_options"
+ self.rdoc_options = val
+ when "requirements"
+ self.requirements = val
+ else
+ instance_variable_set "@#{ivar}", val
+ end
+ end
+ end
+
+ ##
+ # Reset nil attributes to their default values to make the spec valid
+
+ def reset_nil_attributes_to_default
+ nil_attributes = self.class.non_nil_attributes.find_all do |name|
+ !instance_variable_defined?("@#{name}") || instance_variable_get("@#{name}").nil?
+ end
+
+ nil_attributes.each do |attribute|
+ default = default_value attribute
+
+ value = case default
+ 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:
+ return unless raw_require_paths.first.is_a?(Array)
+
+ warn "#{name} #{version} includes a gemspec with `require_paths` set to an array of arrays. Newer versions of this gem might've already fixed this"
+ raw_require_paths.flatten!
+ end
+
+ def raw_require_paths # :nodoc:
+ @require_paths
+ end
+end
diff --git a/lib/rubygems/specification_policy.rb b/lib/rubygems/specification_policy.rb
new file mode 100644
index 0000000000..478e294e09
--- /dev/null
+++ b/lib/rubygems/specification_policy.rb
@@ -0,0 +1,557 @@
+# frozen_string_literal: true
+
+require_relative "user_interaction"
+
+class Gem::SpecificationPolicy
+ include Gem::UserInteraction
+
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :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} # :nodoc:
+
+ METADATA_LINK_KEYS = %w[
+ homepage_uri
+ changelog_uri
+ source_code_uri
+ documentation_uri
+ wiki_uri
+ mailing_list_uri
+ bug_tracker_uri
+ download_uri
+ funding_uri
+ ].freeze # :nodoc:
+
+ def initialize(specification)
+ @warnings = 0
+
+ @specification = specification
+ end
+
+ ##
+ # If set to true, run packaging-specific checks, as well.
+
+ attr_accessor :packaging
+
+ ##
+ # Does a sanity check on the specification.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks.
+ #
+ # It also performs some validations that do not raise but print warning
+ # messages instead.
+
+ def validate(strict = false)
+ validate_required!
+ validate_required_metadata!
+
+ validate_optional(strict) if packaging || strict
+
+ true
+ end
+
+ ##
+ # Does a sanity check on the specification.
+ #
+ # Raises InvalidSpecificationException if the spec does not pass the
+ # checks.
+ #
+ # Only runs checks that are considered necessary for the specification to be
+ # functional.
+
+ def validate_required!
+ validate_nil_attributes
+
+ validate_rubygems_version
+
+ validate_required_attributes
+
+ validate_name
+
+ validate_require_paths
+
+ @specification.keep_only_files_and_directories
+
+ validate_non_files
+
+ validate_self_inclusion_in_files_list
+
+ validate_specification_version
+
+ validate_platform
+
+ validate_array_attributes
+
+ validate_authors_field
+
+ validate_licenses_length
+
+ validate_duplicate_dependencies
+ end
+
+ def validate_required_metadata!
+ validate_metadata
+
+ validate_lazy_metadata
+ end
+
+ def validate_optional(strict)
+ validate_licenses
+
+ validate_permissions
+
+ validate_values
+
+ validate_dependencies
+
+ validate_required_ruby_version
+
+ validate_extensions
+
+ validate_removed_attributes
+
+ validate_unique_links
+
+ if @warnings > 0
+ if strict
+ error "specification has warnings"
+ else
+ alert_warning help_text
+ end
+ end
+ end
+
+ ##
+ # Implementation for Specification#validate_for_resolution
+
+ def validate_for_resolution
+ validate_required!
+ end
+
+ ##
+ # Implementation for Specification#validate_metadata
+
+ def validate_metadata
+ metadata = @specification.metadata
+
+ unless Hash === metadata
+ error "metadata must be a hash"
+ end
+
+ metadata.each do |key, value|
+ entry = "metadata['#{key}']"
+ unless key.is_a?(String)
+ error "metadata keys must be a String"
+ end
+
+ if key.size > 128
+ error "metadata key is too large (#{key.size} > 128)"
+ end
+
+ unless value.is_a?(String)
+ error "#{entry} value must be a String"
+ end
+
+ if value.size > 1024
+ error "#{entry} value is too large (#{value.size} > 1024)"
+ end
+
+ next unless METADATA_LINK_KEYS.include? key
+ unless VALID_URI_PATTERN.match?(value)
+ error "#{entry} has invalid link: #{value.inspect}"
+ end
+ end
+ end
+
+ ##
+ # Checks that no duplicate dependencies are specified.
+
+ def validate_duplicate_dependencies # :nodoc:
+ # NOTE: see REFACTOR note in Gem::Dependency about types - this might be brittle
+ seen = Gem::Dependency::TYPES.inject({}) {|types, type| types.merge({ type => {} }) }
+
+ error_messages = []
+ @specification.dependencies.each do |dep|
+ if prev = seen[dep.type][dep.name]
+ error_messages << <<-MESSAGE
+duplicate dependency on #{dep}, (#{prev.requirement}) use:
+ add_#{dep.type}_dependency \"#{dep.name}\", \"#{dep.requirement}\", \"#{prev.requirement}\"
+ MESSAGE
+ end
+
+ seen[dep.type][dep.name] = dep
+ end
+ if error_messages.any?
+ error error_messages.join
+ end
+ end
+
+ ##
+ # Checks that the gem does not depend on itself.
+
+ def validate_dependencies # :nodoc:
+ error_messages = []
+ @specification.dependencies.each do |dep|
+ if dep.name == @specification.name # error on self reference
+ error_messages << "Dependencies of this gem include a self-reference."
+ end
+ end
+
+ error error_messages.join if error_messages.any?
+ 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.
+ #
+ # Implementation for Specification#validate_permissions
+
+ def validate_permissions
+ return if Gem.win_platform?
+
+ @specification.files.each do |file|
+ next unless File.file?(file)
+ next if File.stat(file).mode & 0o444 == 0o444
+ warning "#{file} is not world-readable"
+ end
+
+ @specification.executables.each do |name|
+ exec = File.join @specification.bindir, name
+ next unless File.file?(exec)
+ next if File.stat(exec).executable?
+ warning "#{exec} is not executable"
+ end
+ end
+
+ private
+
+ def validate_nil_attributes
+ nil_attributes = Gem::Specification.non_nil_attributes.select do |attrname|
+ @specification.instance_variable_get("@#{attrname}").nil?
+ end
+ return if nil_attributes.empty?
+ error "#{nil_attributes.join ", "} must not be nil"
+ end
+
+ def validate_rubygems_version
+ return unless packaging
+
+ rubygems_version = @specification.rubygems_version
+
+ return if rubygems_version == Gem::VERSION
+
+ warning "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}"
+
+ @specification.rubygems_version = Gem::VERSION
+ end
+
+ def validate_required_attributes
+ Gem::Specification.required_attributes.each do |symbol|
+ unless @specification.send symbol
+ error "missing value for attribute #{symbol}"
+ end
+ end
+ end
+
+ def validate_name
+ name = @specification.name
+
+ if !name.is_a?(String)
+ error "invalid value for attribute name: \"#{name.inspect}\" must be a string"
+ elsif !/[a-zA-Z]/.match?(name)
+ error "invalid value for attribute name: #{name.dump} must include at least one letter"
+ elsif !VALID_NAME_PATTERN.match?(name)
+ error "invalid value for attribute name: #{name.dump} can only include letters, numbers, dashes, and underscores"
+ elsif SPECIAL_CHARACTERS.match?(name)
+ error "invalid value for attribute name: #{name.dump} cannot begin with a period, dash, or underscore"
+ end
+ end
+
+ def validate_require_paths
+ return unless @specification.raw_require_paths.empty?
+
+ error "specification must have at least one require_path"
+ end
+
+ def validate_non_files
+ return unless packaging
+
+ non_files = @specification.files.reject {|x| File.file?(x) || File.symlink?(x) }
+
+ unless non_files.empty?
+ error "[\"#{non_files.join "\", \""}\"] are not files"
+ end
+ end
+
+ def validate_self_inclusion_in_files_list
+ file_name = @specification.file_name
+
+ return unless @specification.files.include?(file_name)
+
+ error "#{@specification.full_name} contains itself (#{file_name}), check your files list"
+ end
+
+ def validate_specification_version
+ return if @specification.specification_version.is_a?(Integer)
+
+ error "specification_version must be an Integer (did you mean version?)"
+ end
+
+ def validate_platform
+ platform = @specification.platform
+
+ case platform
+ when Gem::Platform, Gem::Platform::RUBY # ok
+ else
+ error "invalid platform #{platform.inspect}, see Gem::Platform"
+ end
+ end
+
+ def validate_array_attributes
+ Gem::Specification.array_attributes.each do |field|
+ validate_array_attribute(field)
+ end
+ end
+
+ def validate_array_attribute(field)
+ val = @specification.send(field)
+ klass = case field
+ when :dependencies then
+ Gem::Dependency
+ else
+ String
+ end
+
+ unless Array === val && val.all? {|x| x.is_a?(klass) || (field == :licenses && x.nil?) }
+ error "#{field} must be an Array of #{klass}"
+ end
+ end
+
+ def validate_authors_field
+ return unless @specification.authors.empty?
+
+ error "authors may not be empty"
+ end
+
+ def validate_licenses_length
+ 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
+ end
+ end
+
+ def validate_licenses
+ licenses = @specification.licenses
+
+ licenses.each do |license|
+ 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 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
+ HOMEPAGE_URI_PATTERN = /\A[a-z][a-z\d+.-]*:/i
+
+ def validate_lazy_metadata
+ unless @specification.authors.grep(LAZY_PATTERN).empty?
+ error "#{LAZY} is not an author"
+ end
+
+ unless Array(@specification.email).grep(LAZY_PATTERN).empty?
+ error "#{LAZY} is not an email"
+ end
+
+ if LAZY_PATTERN.match?(@specification.description)
+ error "#{LAZY} is not a description"
+ end
+
+ if LAZY_PATTERN.match?(@specification.summary)
+ error "#{LAZY} is not a summary"
+ end
+
+ homepage = @specification.homepage
+
+ # Make sure a homepage is valid HTTP/HTTPS URI
+ if homepage && !homepage.empty?
+ require_relative "vendor/uri/lib/uri"
+ begin
+ 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 Gem::URI::InvalidURIError
+ error "\"#{homepage}\" is not a valid HTTP URI"
+ end
+ end
+ end
+
+ def validate_values
+ %w[author homepage summary files].each do |attribute|
+ validate_attribute_present(attribute)
+ end
+
+ if @specification.description == @specification.summary
+ warning "description and summary are identical"
+ end
+
+ # TODO: raise at some given date
+ warning "deprecated autorequire specified" if @specification.autorequire
+
+ @specification.executables.each do |executable|
+ validate_executable(executable)
+ validate_shebang_line_in(executable)
+ end
+
+ @specification.files.select {|f| File.symlink?(f) }.each do |file|
+ warning "#{file} is a symlink, which is not supported on all platforms"
+ end
+ end
+
+ def validate_attribute_present(attribute)
+ value = @specification.send attribute
+ warning("no #{attribute} specified") if value.nil? || value.empty?
+ end
+
+ def validate_executable(executable)
+ separators = [File::SEPARATOR, File::ALT_SEPARATOR, File::PATH_SEPARATOR].compact.map {|sep| Regexp.escape(sep) }.join
+ return unless executable.match?(/[\s#{separators}]/)
+
+ error "executable \"#{executable}\" contains invalid characters"
+ end
+
+ def validate_shebang_line_in(executable)
+ executable_path = File.join(@specification.bindir, executable)
+ return if File.read(executable_path, 2) == "#!"
+
+ warning "#{executable_path} is missing #! line"
+ end
+
+ def validate_removed_attributes # :nodoc:
+ @specification.removed_method_calls.each do |attr|
+ warning("#{attr} is deprecated and ignored. Please remove this from your gemspec to ensure that your gem continues to build in the future.")
+ end
+ end
+
+ def validate_extensions # :nodoc:
+ require_relative "ext"
+ builder = Gem::Ext::Builder.new(@specification)
+
+ validate_rake_extensions(builder)
+ validate_rust_extensions(builder)
+ validate_extension_require_relative
+ end
+
+ def validate_rust_extensions(builder) # :nodoc:
+ rust_extension = @specification.extensions.any? {|s| builder.builder_for(s).is_a? Gem::Ext::CargoBuilder }
+ missing_cargo_lock = !@specification.files.any? {|f| f.end_with?("Cargo.lock") }
+
+ error <<-ERROR if rust_extension && missing_cargo_lock
+You have specified rust based extension, but Cargo.lock is not part of the gem files. Please run `cargo generate-lockfile` or any other command to generate Cargo.lock and ensure it is added to your gem files section in gemspec.
+ ERROR
+ end
+
+ def validate_rake_extensions(builder) # :nodoc:
+ rake_extension = @specification.extensions.any? {|s| builder.builder_for(s) == Gem::Ext::RakeBuilder }
+ rake_dependency = @specification.dependencies.any? {|d| d.name == "rake" && d.type == :runtime }
+
+ warning <<-WARNING if rake_extension && !rake_dependency
+You have specified rake based extension, but rake is not added as runtime dependency. It is recommended to add rake as a runtime dependency in gemspec since there's no guarantee rake will be already installed.
+ WARNING
+ end
+
+ def validate_extension_require_relative # :nodoc:
+ return unless @specification.extensions.any?
+
+ require_paths = @specification.require_paths
+
+ @specification.files.each do |rb_file|
+ next unless rb_file.end_with?(".rb")
+ next unless require_paths.any? {|rp| rb_file.start_with?("#{rp}/") }
+ next unless File.file?(rb_file)
+
+ File.foreach(rb_file).with_index(1) do |line, lineno|
+ next unless line =~ /^\s*require_relative\s+["']([^"']+)["']/
+
+ required_path = Regexp.last_match(1)
+ resolved = File.join(File.dirname(rb_file), required_path)
+
+ next if @specification.files.any? {|f| f == "#{resolved}.rb" || f == resolved }
+
+ warning <<~WARNING
+ #{rb_file}:#{lineno} uses `require_relative "#{required_path}"` to load a compiled extension.
+ This will break in RubyGems 4.2, which will stop copying compiled extensions into the gem's lib directory.
+ Use `require` instead of `require_relative` to load compiled extensions.
+ WARNING
+ end
+ end
+ end
+
+ def validate_unique_links
+ links = @specification.metadata.slice(*METADATA_LINK_KEYS)
+ grouped = links.group_by {|_key, uri| uri }
+ grouped.each do |uri, copies|
+ next unless copies.length > 1
+ keys = copies.map(&:first).join("\n ")
+ warning <<~WARNING
+ You have specified the uri:
+ #{uri}
+ for all of the following keys:
+ #{keys}
+ Only the first one will be shown on rubygems.org
+ WARNING
+ end
+ end
+
+ def warning(statement) # :nodoc:
+ @warnings += 1
+
+ alert_warning statement
+ end
+
+ def error(statement) # :nodoc:
+ raise Gem::InvalidSpecificationException, statement
+ ensure
+ alert_warning help_text
+ end
+
+ def help_text # :nodoc:
+ "See https://guides.rubygems.org/specification-reference/ for help"
+ end
+end
diff --git a/lib/rubygems/specification_record.rb b/lib/rubygems/specification_record.rb
new file mode 100644
index 0000000000..c7e5cbedb5
--- /dev/null
+++ b/lib/rubygems/specification_record.rb
@@ -0,0 +1,225 @@
+# frozen_string_literal: true
+
+module Gem
+ class SpecificationRecord
+ def self.dirs_from(paths)
+ paths.map do |path|
+ File.join(path, "specifications")
+ end
+ end
+
+ def self.from_path(path)
+ new(dirs_from([path]))
+ end
+
+ def initialize(dirs)
+ @all = nil
+ @stubs = nil
+ @stubs_by_name = {}
+ @spec_with_requirable_file = {}
+ @active_stub_with_requirable_file = {}
+
+ @dirs = dirs
+ end
+
+ # Sentinel object to represent "not found" stubs
+ NOT_FOUND = Struct.new(:to_spec, :this).new
+ private_constant :NOT_FOUND
+
+ ##
+ # Returns the list of all specifications in the record
+
+ def all
+ @all ||= stubs.map(&:to_spec)
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+
+ def stubs
+ @stubs ||= begin
+ pattern = "*.gemspec"
+ stubs = stubs_for_pattern(pattern, false)
+
+ @stubs_by_name = stubs.select {|s| Gem::Platform.match_spec? s }.group_by(&:name)
+ stubs
+ end
+ end
+
+ ##
+ # Returns a Gem::StubSpecification for every specification in the record
+ # named +name+ only returns stubs that match Gem.platforms
+
+ def stubs_for(name)
+ if @stubs
+ @stubs_by_name[name] || []
+ else
+ @stubs_by_name[name] ||= stubs_for_pattern("#{name}-*.gemspec").select do |s|
+ s.name == name
+ end
+ end
+ end
+
+ ##
+ # Finds stub specifications matching a pattern in the record, optionally
+ # filtering out specs not matching the current platform
+
+ def stubs_for_pattern(pattern, match_platform = true)
+ installed_stubs = installed_stubs(pattern)
+ installed_stubs.select! {|s| Gem::Platform.match_spec? s } if match_platform
+ stubs = installed_stubs + Gem::Specification.default_stubs(pattern)
+ Gem::Specification._resort!(stubs)
+ stubs
+ end
+
+ ##
+ # Adds +spec+ to the record, keeping the collection properly sorted.
+
+ def add_spec(spec)
+ return if all.include? spec
+
+ all << spec
+ stubs << spec
+ (@stubs_by_name[spec.name] ||= []) << spec
+
+ Gem::Specification._resort!(@stubs_by_name[spec.name])
+ Gem::Specification._resort!(stubs)
+ end
+
+ ##
+ # Removes +spec+ from the record.
+
+ def remove_spec(spec)
+ all.delete spec.to_spec
+ stubs.delete spec
+ (@stubs_by_name[spec.name] || []).delete spec
+ end
+
+ ##
+ # Sets the specs known by the record to +specs+.
+
+ def all=(specs)
+ @stubs_by_name = specs.group_by(&:name)
+ @all = @stubs = specs
+ end
+
+ ##
+ # Return full names of all specs in the record in sorted order.
+
+ def all_names
+ all.map(&:full_name)
+ end
+
+ include Enumerable
+
+ ##
+ # Enumerate every known spec.
+
+ def each
+ return enum_for(:each) unless block_given?
+
+ all.each do |x|
+ yield x
+ end
+ end
+
+ ##
+ # Returns every spec in the record that matches +name+ and optional +requirements+.
+
+ def find_all_by_name(name, *requirements)
+ req = Gem::Requirement.create(*requirements)
+ env_req = Gem.env_requirement(name)
+
+ matches = stubs_for(name).find_all do |spec|
+ req.satisfied_by?(spec.version) && env_req.satisfied_by?(spec.version)
+ end.map(&:to_spec)
+
+ if name == "bundler" && !req.specific?
+ require_relative "bundler_version_finder"
+ Gem::BundlerVersionFinder.prioritize!(matches)
+ end
+
+ matches
+ end
+
+ ##
+ # Return the best specification in the record that contains the file matching +path+.
+
+ def find_by_path(path)
+ path = path.dup.freeze
+ spec = @spec_with_requirable_file[path] ||= stubs.find do |s|
+ s.contains_requirable_file? path
+ end || NOT_FOUND
+
+ spec.to_spec
+ end
+
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not loaded. This method is different than
+ # +find_inactive_by_path+ as it will filter out loaded specs by their name.
+
+ def find_unloaded_by_path(path)
+ stub = stubs.find do |s|
+ next if Gem.loaded_specs[s.name]
+ s.contains_requirable_file? path
+ end
+ stub&.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+ amongst the specs that are not activated.
+
+ def find_inactive_by_path(path)
+ stub = stubs.find do |s|
+ next if s.activated?
+ s.contains_requirable_file? path
+ end
+ stub&.to_spec
+ end
+
+ ##
+ # Return the best specification in the record that contains the file
+ # matching +path+, among those already activated.
+
+ def find_active_stub_by_path(path)
+ stub = @active_stub_with_requirable_file[path] ||= stubs.find do |s|
+ s.activated? && s.contains_requirable_file?(path)
+ end || NOT_FOUND
+
+ stub.this
+ end
+
+ ##
+ # Return the latest specs in the record, optionally including prerelease
+ # specs if +prerelease+ is true.
+
+ def latest_specs(prerelease)
+ Gem::Specification._latest_specs stubs, prerelease
+ end
+
+ ##
+ # Return the latest installed spec in the record for gem +name+.
+
+ def latest_spec_for(name)
+ latest_specs(true).find {|installed_spec| installed_spec.name == name }
+ end
+
+ private
+
+ def installed_stubs(pattern)
+ map_stubs(pattern) do |path, base_dir, gems_dir|
+ Gem::StubSpecification.gemspec_stub(path, base_dir, gems_dir)
+ end
+ end
+
+ def map_stubs(pattern)
+ @dirs.flat_map do |dir|
+ base_dir = File.dirname dir
+ gems_dir = File.join base_dir, "gems"
+ Gem::Specification.gemspec_stubs_in(dir, pattern) {|path| yield path, base_dir, gems_dir }
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/ssl_certs/.document b/lib/rubygems/ssl_certs/.document
new file mode 100644
index 0000000000..fb66f13c33
--- /dev/null
+++ b/lib/rubygems/ssl_certs/.document
@@ -0,0 +1 @@
+# Ignore all files in this directory
diff --git a/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem
new file mode 100644
index 0000000000..8afb219058
--- /dev/null
+++ b/lib/rubygems/ssl_certs/rubygems.org/GlobalSign.pem
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
+A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
+Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
+MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
+A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
+RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
+gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
+KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
+QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
+XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
+DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
+LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
+RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
+jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
+6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
+mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
+Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
+WD9f
+-----END CERTIFICATE-----
diff --git a/lib/rubygems/stub_specification.rb b/lib/rubygems/stub_specification.rb
new file mode 100644
index 0000000000..53b337ed85
--- /dev/null
+++ b/lib/rubygems/stub_specification.rb
@@ -0,0 +1,236 @@
+# 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
+# information.
+
+class Gem::StubSpecification < Gem::BasicSpecification
+ # :nodoc:
+ PREFIX = "# stub: "
+
+ # :nodoc:
+ OPEN_MODE = "r:UTF-8:-"
+
+ class StubLine # :nodoc: all
+ attr_reader :name, :version, :platform, :require_paths, :extensions,
+ :full_name
+
+ NO_EXTENSIONS = [].freeze
+
+ # These are common require paths.
+ REQUIRE_PATHS = { # :nodoc:
+ "lib" => "lib",
+ "test" => "test",
+ "ext" => "ext",
+ }.freeze
+
+ # These are common require path lists. This hash is used to optimize
+ # and consolidate require_path objects. Most specs just specify "lib"
+ # in their require paths, so lets take advantage of that by pre-allocating
+ # a require path list for that case.
+ REQUIRE_PATH_LIST = { # :nodoc:
+ "lib" => ["lib"].freeze,
+ }.freeze
+
+ def initialize(data, extensions)
+ parts = data[PREFIX.length..-1].split(" ", 4)
+ @name = -parts[0]
+ @version = if Gem::Version.correct?(parts[1])
+ Gem::Version.new(parts[1])
+ else
+ Gem::Version.new(0)
+ end
+
+ @platform = Gem::Platform.new parts[2]
+ @extensions = extensions
+ @full_name = if platform == Gem::Platform::RUBY
+ "#{name}-#{version}"
+ else
+ "#{name}-#{version}-#{platform}"
+ end
+
+ path_list = parts.last
+ @require_paths = REQUIRE_PATH_LIST[path_list] || path_list.split("\0").map! do |x|
+ REQUIRE_PATHS[x] || x
+ end
+ end
+ end
+
+ def self.default_gemspec_stub(filename, base_dir, gems_dir)
+ new filename, base_dir, gems_dir, true
+ end
+
+ def self.gemspec_stub(filename, base_dir, gems_dir)
+ new filename, base_dir, gems_dir, false
+ end
+
+ attr_reader :base_dir, :gems_dir
+
+ def initialize(filename, base_dir, gems_dir, default_gem)
+ super()
+
+ self.loaded_from = filename
+ @data = nil
+ @name = nil
+ @spec = nil
+ @base_dir = base_dir
+ @gems_dir = gems_dir
+ @default_gem = default_gem
+ end
+
+ ##
+ # True when this gem has been activated
+
+ def activated?
+ @activated ||= !loaded_spec.nil?
+ end
+
+ def default_gem?
+ @default_gem
+ end
+
+ def build_extensions # :nodoc:
+ return if default_gem?
+ return if extensions.empty?
+
+ to_spec.build_extensions
+ end
+
+ ##
+ # If the gemspec contains a stubline, returns a StubLine instance. Otherwise
+ # returns the full Gem::Specification.
+
+ def data
+ unless @data
+ begin
+ saved_lineno = $.
+
+ Gem.open_file loaded_from, OPEN_MODE do |file|
+ file.readline # discard encoding line
+ stubline = file.readline
+ if stubline.start_with?(PREFIX)
+ extline = file.readline
+
+ extensions =
+ if extline.delete_prefix!(PREFIX)
+ extline.chomp!
+ extline.split "\0"
+ else
+ StubLine::NO_EXTENSIONS
+ end
+
+ stubline.chomp! # readline(chomp: true) allocates 3x as much as .readline.chomp!
+ @data = StubLine.new stubline, extensions
+ end
+ rescue EOFError
+ end
+ ensure
+ $. = saved_lineno
+ end
+ end
+
+ @data ||= to_spec
+ end
+
+ private :data
+
+ def raw_require_paths # :nodoc:
+ data.require_paths
+ end
+
+ def missing_extensions?
+ return false if RUBY_ENGINE == "jruby"
+ return false if default_gem?
+ return false if extensions.empty?
+ return false if File.exist? gem_build_complete_path
+
+ to_spec.missing_extensions?
+ end
+
+ ##
+ # Name of the gem
+
+ def name
+ data.name
+ end
+
+ ##
+ # Platform of the gem
+
+ def platform
+ data.platform
+ end
+
+ ##
+ # Extensions for this gem
+
+ def extensions
+ data.extensions
+ end
+
+ ##
+ # Version of the gem
+
+ def version
+ data.version
+ end
+
+ def full_name
+ data.full_name
+ end
+
+ ##
+ # The full Gem::Specification for this gem, loaded from evalling its gemspec
+
+ def spec
+ @spec ||= loaded_spec if @data
+ @spec ||= Gem::Specification.load(loaded_from)
+ end
+ alias_method :to_spec, :spec
+
+ ##
+ # Is this StubSpecification valid? i.e. have we found a stub line, OR does
+ # the filename contain a valid gemspec?
+
+ def valid?
+ data
+ end
+
+ ##
+ # Is there a stub line present for this StubSpecification?
+
+ def stubbed?
+ data.is_a? StubLine
+ end
+
+ def ==(other) # :nodoc:
+ self.class === other &&
+ name == other.name &&
+ version == other.version &&
+ platform == other.platform
+ end
+
+ alias_method :eql?, :== # :nodoc:
+
+ def hash # :nodoc:
+ name.hash ^ version.hash ^ platform.hash
+ end
+
+ def <=>(other) # :nodoc:
+ sort_obj <=> other.sort_obj
+ end
+
+ def sort_obj # :nodoc:
+ [name, version, Gem::Platform.sort_priority(platform)]
+ end
+
+ private
+
+ def loaded_spec
+ spec = Gem.loaded_specs[name]
+ return unless spec && spec.version == version && spec.default_gem? == default_gem?
+
+ spec
+ end
+end
diff --git a/lib/rubygems/target_rbconfig.rb b/lib/rubygems/target_rbconfig.rb
new file mode 100644
index 0000000000..21d90ee9db
--- /dev/null
+++ b/lib/rubygems/target_rbconfig.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+##
+# A TargetConfig is a wrapper around an RbConfig object that provides a
+# consistent interface for querying configuration for *deployment target
+# platform*, where the gem being installed is intended to run on.
+#
+# The TargetConfig is typically created from the RbConfig of the running Ruby
+# process, but can also be created from an RbConfig file on disk for cross-
+# compiling gems.
+
+class Gem::TargetRbConfig
+ attr_reader :path
+
+ def initialize(rbconfig, path)
+ @rbconfig = rbconfig
+ @path = path
+ end
+
+ ##
+ # Creates a TargetRbConfig for the platform that RubyGems is running on.
+
+ def self.for_running_ruby
+ new(::RbConfig, nil)
+ end
+
+ ##
+ # Creates a TargetRbConfig from the RbConfig file at the given path.
+ # Typically used for cross-compiling gems.
+
+ def self.from_path(rbconfig_path)
+ namespace = Module.new do |m|
+ # Load the rbconfig.rb file within a new anonymous module to avoid
+ # conflicts with the rbconfig for the running platform.
+ Kernel.load rbconfig_path, m
+ end
+ rbconfig = namespace.const_get(:RbConfig)
+
+ new(rbconfig, rbconfig_path)
+ end
+
+ ##
+ # Queries the configuration for the given key.
+
+ def [](key)
+ @rbconfig::CONFIG[key]
+ end
+end
diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb
new file mode 100644
index 0000000000..0550dc473d
--- /dev/null
+++ b/lib/rubygems/text.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+##
+# A collection of text-wrangling methods
+
+module Gem::Text
+ ##
+ # Remove any non-printable characters and make the text suitable for
+ # printing.
+ def clean_text(text)
+ text = text.gsub(/[\000-\b\v-\f\016-\037\177]/, ".")
+
+ # Match C1 control characters (U+0080-U+009F) as codepoints. This requires
+ # a valid UTF-8 string so the regexp does not split a multibyte sequence;
+ # strings in other encodings are left unchanged.
+ if text.encoding == Encoding::UTF_8 && text.valid_encoding?
+ text = text.gsub(/[\u0080-\u009f]/, ".")
+ end
+
+ text
+ end
+
+ def truncate_text(text, description, max_length = 100_000)
+ raise ArgumentError, "max_length must be positive" unless max_length > 0
+ return text if text.size <= max_length
+ "Truncating #{description} to #{max_length.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse} characters:\n" + text[0, max_length]
+ end
+
+ ##
+ # Wraps +text+ to +wrap+ characters and optionally indents by +indent+
+ # characters
+
+ def format_text(text, wrap, indent = 0)
+ result = []
+ work = clean_text(text)
+
+ while work.length > wrap do
+ if work =~ /^(.{0,#{wrap}})[ \n]/
+ result << $1.rstrip
+ work.slice!(0, $&.length)
+ else
+ result << work.slice!(0, wrap)
+ end
+ end
+
+ result << work if work.length.nonzero?
+ result.join("\n").gsub(/^/, " " * indent)
+ end
+
+ def min3(a, b, c) # :nodoc:
+ if a < b && a < c
+ a
+ elsif b < c
+ b
+ else
+ c
+ end
+ end
+
+ # Returns a value representing the "cost" of transforming str1 into str2
+ # Vendored version of DidYouMean::Levenshtein.distance from the ruby/did_you_mean gem @ 1.4.0
+ # https://github.com/ruby/did_you_mean/blob/2ddf39b874808685965dbc47d344cf6c7651807c/lib/did_you_mean/levenshtein.rb#L7-L37
+ def levenshtein_distance(str1, str2)
+ n = str1.length
+ m = str2.length
+ return m if n.zero?
+ return n if m.zero?
+
+ d = (0..m).to_a
+ x = nil
+
+ # to avoid duplicating an enumerable object, create it outside of the loop
+ str2_codepoints = str2.codepoints
+
+ str1.each_codepoint.with_index(1) do |char1, i|
+ j = 0
+ while j < m
+ cost = char1 == str2_codepoints[j] ? 0 : 1
+ x = min3(
+ d[j + 1] + 1, # insertion
+ i + 1, # deletion
+ d[j] + cost # substitution
+ )
+ d[j] = i
+ i = x
+
+ j += 1
+ end
+ d[m] = x
+ end
+
+ x
+ end
+end
diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
new file mode 100644
index 0000000000..fe4c3a80cf
--- /dev/null
+++ b/lib/rubygems/uninstaller.rb
@@ -0,0 +1,440 @@
+# frozen_string_literal: true
+
+#--
+# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
+# All rights reserved.
+# See LICENSE.txt for permissions.
+#++
+
+require "fileutils"
+require_relative "../rubygems"
+require_relative "installer_uninstaller_utils"
+require_relative "dependency_list"
+require_relative "user_interaction"
+
+##
+# An Uninstaller.
+#
+# The uninstaller fires pre and post uninstall hooks. Hooks can be added
+# either through a rubygems_plugin.rb file in an installed gem or via a
+# rubygems/defaults/#{RUBY_ENGINE}.rb or rubygems/defaults/operating_system.rb
+# file. See Gem.pre_uninstall and Gem.post_uninstall for details.
+
+class Gem::Uninstaller
+ include Gem::UserInteraction
+
+ include Gem::InstallerUninstallerUtils
+
+ ##
+ # The directory a gem's executables will be installed into
+
+ attr_reader :bin_dir
+
+ ##
+ # The gem repository the gem will be uninstalled from
+
+ attr_reader :gem_home
+
+ ##
+ # The Gem::Specification for the gem being uninstalled, only set during
+ # #uninstall_gem
+
+ attr_reader :spec
+
+ ##
+ # Constructs an uninstaller that will uninstall gem named +gem+.
+ # +options+ is a Hash with the following keys:
+ #
+ # :version:: Version requirement for the gem to uninstall. If not specified,
+ # uses Gem::Requirement.default.
+ # :install_dir:: The directory where the gem is installed. If not specified,
+ # uses Gem.dir.
+ # :executables:: Whether executables should be removed without confirmation or not. If nil, asks the user explicitly.
+ # :all:: If more than one version matches the requirement, whether to forcefully remove all matching versions or ask the user to select specific matching versions that should be removed.
+ # :ignore:: Ignore broken dependency checks when uninstalling.
+ # :bin_dir:: Directory containing executables to remove. If not specified,
+ # uses Gem.bindir.
+ # :format_executable:: In order to find executables to be removed, format executable names using Gem::Installer.exec_format.
+ # :abort_on_dependent:: Directly abort uninstallation if dependencies would be broken, rather than asking the user for confirmation.
+ # :check_dev:: When checking if uninstalling gem would leave broken dependencies around, also consider development dependencies.
+ # :force:: Set both :all and :ignore to true for forced uninstallation.
+ # :user_install:: Uninstall from user gem directory instead of system directory.
+
+ def initialize(gem, options = {})
+ @gem = gem
+ @version = options[:version] || Gem::Requirement.default
+ @install_dir = options[:install_dir]
+ @gem_home = File.realpath(@install_dir || Gem.dir)
+ @user_dir = File.exist?(Gem.user_dir) ? File.realpath(Gem.user_dir) : Gem.user_dir
+ @force_executables = options[:executables]
+ @force_all = options[:all]
+ @force_ignore = options[:ignore]
+ @bin_dir = options[:bin_dir]
+ @format_executable = options[:format_executable]
+ @abort_on_dependent = options[:abort_on_dependent]
+ @check_dev = options[:check_dev]
+
+ if options[:force]
+ @force_all = true
+ @force_ignore = true
+ end
+
+ # only add user directory if install_dir is not set
+ @user_install = false
+ @user_install = options[:user_install] unless @install_dir
+
+ # Optimization: populated during #uninstall
+ @default_specs_matching_uninstall_params = []
+ end
+
+ ##
+ # Performs the uninstall of the gem. This removes the spec, the Gem
+ # directory, and the cached .gem file.
+
+ def uninstall
+ dependency = Gem::Dependency.new @gem, @version
+
+ list = []
+
+ specification_record.stubs.each do |spec|
+ next unless dependency.matches_spec? spec
+
+ list << spec
+ end
+
+ if list.empty?
+ raise Gem::InstallError, "gem #{@gem.inspect} is not installed"
+ end
+
+ default_specs, list = list.partition(&:default_gem?)
+ warn_cannot_uninstall_default_gems(default_specs - list)
+ @default_specs_matching_uninstall_params = default_specs.map(&:to_spec)
+
+ list, other_repo_specs = list.partition do |spec|
+ @gem_home == spec.base_dir ||
+ (@user_install && spec.base_dir == @user_dir)
+ end
+
+ list.sort!
+
+ if list.empty?
+ return unless other_repo_specs.any?
+
+ other_repos = other_repo_specs.map(&:base_dir).uniq
+
+ message = ["#{@gem} is not installed in GEM_HOME, try:"]
+ message.concat other_repos.map {|repo|
+ "\tgem uninstall -i #{repo} #{@gem}"
+ }
+
+ raise Gem::InstallError, message.join("\n")
+ elsif @force_all
+ remove_all list
+
+ elsif list.size > 1
+ gem_names = list.map(&:full_name_with_location)
+ gem_names << "All versions"
+
+ say
+ _, index = choose_from_list "Select gem to uninstall:", gem_names
+
+ if index == list.size
+ remove_all list
+ elsif index && index >= 0 && index < list.size
+ uninstall_gem list[index]
+ else
+ say "Error: must enter a number [1-#{list.size + 1}]"
+ end
+ else
+ uninstall_gem list.first
+ end
+ end
+
+ ##
+ # Uninstalls gem +spec+
+
+ def uninstall_gem(stub)
+ spec = stub.to_spec
+
+ @spec = spec
+
+ unless dependencies_ok? spec
+ if abort_on_dependent? || !ask_if_ok(spec)
+ raise Gem::DependencyRemovalException,
+ "Uninstallation aborted due to dependent gem(s)"
+ end
+ end
+
+ Gem.pre_uninstall_hooks.each do |hook|
+ hook.call self
+ end
+
+ remove_executables @spec
+ remove_plugins @spec
+ remove @spec
+
+ specification_record.remove_spec(stub)
+
+ regenerate_plugins
+
+ Gem.post_uninstall_hooks.each do |hook|
+ hook.call self
+ end
+
+ @spec = nil
+ end
+
+ ##
+ # Removes installed executables and batch files (windows only) for +spec+.
+
+ def remove_executables(spec)
+ return if spec.executables.empty? || default_spec_matches?(spec)
+
+ executables = spec.executables.clone
+
+ # Leave any executables created by other installed versions
+ # of this gem installed.
+
+ list = Gem::Specification.find_all do |s|
+ s.name == spec.name && s.version != spec.version
+ end
+
+ list.each do |s|
+ s.executables.each do |exe_name|
+ executables.delete exe_name
+ end
+ end
+
+ return if executables.empty?
+
+ executables = executables.map {|exec| formatted_program_filename exec }
+
+ remove = if @force_executables.nil?
+ ask_yes_no("Remove executables:\n" \
+ "\t#{executables.join ", "}\n\n" \
+ "in addition to the gem?",
+ true)
+ else
+ @force_executables
+ end
+
+ if remove
+ bin_dir = @bin_dir || Gem.bindir(spec.base_dir)
+
+ raise Gem::FilePermissionError, bin_dir unless File.writable? bin_dir
+
+ executables.each do |exe_name|
+ say "Removing #{exe_name}"
+
+ exe_file = File.join bin_dir, exe_name
+
+ safe_delete { FileUtils.rm exe_file }
+ safe_delete { FileUtils.rm "#{exe_file}.bat" }
+ end
+ else
+ say "Executables and scripts will remain installed."
+ end
+ end
+
+ ##
+ # Removes all gems in +list+.
+ #
+ # NOTE: removes uninstalled gems from +list+.
+
+ def remove_all(list)
+ list.each {|spec| uninstall_gem spec }
+ end
+
+ ##
+ # spec:: the spec of the gem to be uninstalled
+
+ def remove(spec)
+ unless path_ok?(@gem_home, spec) ||
+ (@user_install && path_ok?(@user_dir, spec))
+ e = Gem::GemNotInHomeException.new \
+ "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}"
+ e.spec = spec
+
+ raise e
+ end
+
+ raise Gem::FilePermissionError, spec.base_dir unless
+ File.writable?(spec.base_dir)
+
+ full_gem_path = spec.full_gem_path
+ exclusions = []
+
+ if default_spec_matches?(spec) && spec.executables.any?
+ exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) }
+ exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path
+ end
+
+ safe_delete { rm_r full_gem_path, exclusions: exclusions }
+ safe_delete { FileUtils.rm_r spec.extension_dir }
+
+ old_platform_name = spec.original_name
+
+ gem = spec.cache_file
+ gem = File.join(spec.cache_dir, "#{old_platform_name}.gem") unless
+ File.exist? gem
+
+ safe_delete { FileUtils.rm_r gem }
+
+ begin
+ Gem::RDoc.new(spec).remove
+ rescue NameError
+ end
+
+ gemspec = spec.spec_file
+
+ unless File.exist? gemspec
+ gemspec = File.join(File.dirname(gemspec), "#{old_platform_name}.gemspec")
+ end
+
+ safe_delete { FileUtils.rm_r gemspec }
+ announce_deletion_of(spec)
+ end
+
+ ##
+ # Remove any plugin wrappers for +spec+.
+
+ def remove_plugins(spec) # :nodoc:
+ return if spec.plugins.empty?
+
+ remove_plugins_for(spec, plugin_dir_for(spec))
+ end
+
+ ##
+ # Regenerates plugin wrappers after removal.
+
+ def regenerate_plugins
+ latest = specification_record.latest_spec_for(@spec.name)
+ return if latest.nil?
+
+ regenerate_plugins_for(latest, plugin_dir_for(@spec))
+ end
+
+ ##
+ # Is +spec+ in +gem_dir+?
+
+ def path_ok?(gem_dir, spec)
+ full_path = File.join gem_dir, "gems", spec.full_name
+ original_path = File.join gem_dir, "gems", spec.original_name
+
+ full_path == spec.full_gem_path || original_path == spec.full_gem_path
+ end
+
+ ##
+ # Returns true if it is OK to remove +spec+ or this is a forced
+ # uninstallation.
+
+ def dependencies_ok?(spec) # :nodoc:
+ return true if @force_ignore
+
+ deplist = Gem::DependencyList.from_specs
+ deplist.ok_to_remove?(spec.full_name, @check_dev)
+ end
+
+ ##
+ # Should the uninstallation abort if a dependency will go unsatisfied?
+ #
+ # See ::new.
+
+ def abort_on_dependent? # :nodoc:
+ @abort_on_dependent
+ end
+
+ ##
+ # Asks if it is OK to remove +spec+. Returns true if it is OK.
+
+ def ask_if_ok(spec) # :nodoc:
+ msg = [""]
+ msg << "You have requested to uninstall the gem:"
+ msg << "\t#{spec.full_name}"
+ msg << ""
+
+ siblings = Gem::Specification.select do |s|
+ s.name == spec.name && s.full_name != spec.full_name
+ end
+
+ spec.dependent_gems(@check_dev).each do |dep_spec, dep, _satlist|
+ unless siblings.any? {|s| s.satisfies_requirement? dep }
+ msg << "#{dep_spec.name}-#{dep_spec.version} depends on #{dep}"
+ end
+ end
+
+ msg << "If you remove this gem, these dependencies will not be met."
+ msg << "Continue with Uninstall?"
+ ask_yes_no(msg.join("\n"), false)
+ end
+
+ ##
+ # Returns the formatted version of the executable +filename+
+
+ def formatted_program_filename(filename) # :nodoc:
+ # TODO perhaps the installer should leave a small manifest
+ # of what it did for us to find rather than trying to recreate
+ # it again.
+ if @format_executable
+ require_relative "installer"
+ Gem::Installer.exec_format % File.basename(filename)
+ else
+ filename
+ end
+ end
+
+ def safe_delete(&block)
+ block.call
+ rescue Errno::ENOENT
+ nil
+ rescue Errno::EPERM
+ e = Gem::UninstallError.new
+ e.spec = @spec
+
+ raise e
+ end
+
+ private
+
+ def rm_r(path, exclusions:)
+ FileUtils::Entry_.new(path).postorder_traverse do |ent|
+ ent.remove unless exclusions.include?(ent.path)
+ end
+ end
+
+ def specification_record
+ @specification_record ||= @install_dir ? Gem::SpecificationRecord.from_path(@install_dir) : Gem::Specification.specification_record
+ end
+
+ def announce_deletion_of(spec)
+ name = spec.full_name
+ say "Successfully uninstalled #{name}"
+ if default_spec_matches?(spec)
+ say(
+ "There was both a regular copy and a default copy of #{name}. The " \
+ "regular copy was successfully uninstalled, but the default copy " \
+ "was left around because default gems can't be removed."
+ )
+ end
+ end
+
+ # @return true if the specs of any default gems are `==` to the given `spec`.
+ def default_spec_matches?(spec)
+ !default_specs_that_match(spec).empty?
+ end
+
+ # @return [Array] specs of default gems that are `==` to the given `spec`.
+ def default_specs_that_match(spec)
+ @default_specs_matching_uninstall_params.select {|default_spec| spec == default_spec }
+ end
+
+ def warn_cannot_uninstall_default_gems(specs)
+ specs.each do |spec|
+ say "Gem #{spec.full_name} cannot be uninstalled because it is a default gem"
+ end
+ end
+
+ def plugin_dir_for(spec)
+ Gem.plugindir(spec.base_dir)
+ end
+end
diff --git a/lib/rubygems/unknown_command_spell_checker.rb b/lib/rubygems/unknown_command_spell_checker.rb
new file mode 100644
index 0000000000..ee5c2fbe04
--- /dev/null
+++ b/lib/rubygems/unknown_command_spell_checker.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class Gem::UnknownCommandSpellChecker
+ attr_reader :error
+
+ def initialize(error)
+ @error = error
+ end
+
+ def corrections
+ @corrections ||=
+ spell_checker.correct(error.unknown_command).map(&:inspect)
+ end
+
+ private
+
+ def spell_checker
+ dictionary = Gem::CommandManager.instance.command_names
+ DidYouMean::SpellChecker.new(dictionary: dictionary)
+ end
+end
diff --git a/lib/rubygems/update_suggestion.rb b/lib/rubygems/update_suggestion.rb
new file mode 100644
index 0000000000..6f3ec5f493
--- /dev/null
+++ b/lib/rubygems/update_suggestion.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+##
+# Mixin methods for Gem::Command to promote available RubyGems update
+
+module Gem::UpdateSuggestion
+ ONE_WEEK = 7 * 24 * 60 * 60
+
+ ##
+ # Message to promote available RubyGems update with related gem update command.
+
+ def update_suggestion
+ <<-MESSAGE
+
+A new release of RubyGems is available: #{Gem.rubygems_version} → #{Gem.latest_rubygems_version}!
+Run `gem update --system #{Gem.latest_rubygems_version}` to update your installation.
+
+ MESSAGE
+ end
+
+ ##
+ # Determines if current environment is eligible for update suggestion.
+
+ def eligible_for_update?
+ # explicit opt-out
+ return false if Gem.configuration[:prevent_update_suggestion]
+ return false if ENV["RUBYGEMS_PREVENT_UPDATE_SUGGESTION"]
+
+ # focus only on human usage of final RubyGems releases
+ return false unless Gem.ui.tty?
+ return false if Gem.rubygems_version.prerelease?
+ return false if Gem.disable_system_update_message
+ return false if Gem::CIDetector.ci?
+
+ # check makes sense only when we can store timestamp of last try
+ # otherwise we will not be able to prevent "annoying" update message
+ # on each command call
+ return unless Gem.configuration.state_file_writable?
+
+ # load time of last check, ensure the difference is enough to repeat the suggestion
+ check_time = Time.now.to_i
+ last_update_check = Gem.configuration.last_update_check
+ return false if (check_time - last_update_check) < ONE_WEEK
+
+ # compare current and latest version, this is the part where
+ # latest rubygems spec is fetched from remote
+ (Gem.rubygems_version < Gem.latest_rubygems_version).tap do |eligible|
+ # store the time of last successful check into state file
+ Gem.configuration.last_update_check = check_time
+
+ return eligible
+ end
+ rescue StandardError # don't block install command on any problem
+ false
+ end
+end
diff --git a/lib/rubygems/uri.rb b/lib/rubygems/uri.rb
new file mode 100644
index 0000000000..d729c67d26
--- /dev/null
+++ b/lib/rubygems/uri.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+##
+# The Uri handles rubygems source URIs.
+#
+
+class Gem::Uri
+ ##
+ # Parses and redacts uri
+
+ def self.redact(uri)
+ new(uri).redacted
+ end
+
+ ##
+ # Parses uri, raising if it's invalid
+
+ def self.parse!(uri)
+ require_relative "vendor/uri/lib/uri"
+
+ raise Gem::URI::InvalidURIError unless uri
+
+ return uri unless uri.is_a?(String)
+
+ # Always escape URI's to deal with potential spaces and such
+ # It should also be considered that source_uri may already be
+ # a valid URI with escaped characters. e.g. "{DESede}" is encoded
+ # as "%7BDESede%7D". If this is escaped again the percentage
+ # symbols will be escaped.
+ begin
+ Gem::URI.parse(uri)
+ rescue Gem::URI::InvalidURIError
+ Gem::URI.parse(Gem::URI::RFC2396_PARSER.escape(uri))
+ end
+ end
+
+ ##
+ # Parses uri, returning the original uri if it's invalid
+
+ def self.parse(uri)
+ parse!(uri)
+ rescue Gem::URI::InvalidURIError
+ uri
+ end
+
+ def initialize(source_uri)
+ @parsed_uri = parse(source_uri)
+ end
+
+ def redacted
+ return self unless valid_uri?
+
+ if token? || oauth_basic?
+ with_redacted_user
+ elsif password?
+ with_redacted_password
+ else
+ self
+ end
+ end
+
+ def to_s
+ @parsed_uri.to_s
+ end
+
+ def redact_credentials_from(text)
+ return text unless valid_uri? && password? && text.include?(to_s)
+
+ text.sub(password, "REDACTED")
+ end
+
+ def method_missing(method_name, *args, &blk)
+ if @parsed_uri.respond_to?(method_name)
+ @parsed_uri.send(method_name, *args, &blk)
+ else
+ super
+ end
+ end
+
+ def respond_to_missing?(method_name, include_private = false)
+ @parsed_uri.respond_to?(method_name, include_private) || super
+ end
+
+ protected
+
+ # Add a protected reader for the cloned instance to access the original object's parsed uri
+ attr_reader :parsed_uri
+
+ private
+
+ def parse!(uri)
+ self.class.parse!(uri)
+ end
+
+ def parse(uri)
+ self.class.parse(uri)
+ end
+
+ def with_redacted_user
+ clone.tap {|uri| uri.user = "REDACTED" }
+ end
+
+ def with_redacted_password
+ clone.tap {|uri| uri.password = "REDACTED" }
+ end
+
+ def valid_uri?
+ !@parsed_uri.is_a?(String)
+ end
+
+ def password?
+ !!password
+ end
+
+ def oauth_basic?
+ password == "x-oauth-basic"
+ end
+
+ def token?
+ !user.nil? && password.nil?
+ end
+
+ def initialize_copy(original)
+ @parsed_uri = original.parsed_uri.clone
+ end
+end
diff --git a/lib/rubygems/uri_formatter.rb b/lib/rubygems/uri_formatter.rb
new file mode 100644
index 0000000000..8856fdadd2
--- /dev/null
+++ b/lib/rubygems/uri_formatter.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+##
+# The UriFormatter handles URIs from user-input and escaping.
+#
+# uf = Gem::UriFormatter.new 'example.com'
+#
+# p uf.normalize #=> 'http://example.com'
+
+class Gem::UriFormatter
+ ##
+ # The URI to be formatted.
+
+ attr_reader :uri
+
+ ##
+ # Creates a new URI formatter for +uri+.
+
+ def initialize(uri)
+ require "cgi/escape"
+ require "cgi/util" unless defined?(CGI::EscapeExt)
+
+ @uri = uri
+ end
+
+ ##
+ # Escapes the #uri for use as a CGI parameter
+
+ def escape
+ return unless @uri
+ CGI.escape @uri
+ end
+
+ ##
+ # Normalize the URI by adding "http://" if it is missing.
+
+ def normalize
+ /^(https?|ftp|file):/i.match?(@uri) ? @uri : "http://#{@uri}"
+ end
+
+ ##
+ # Unescapes the #uri which came from a CGI parameter
+
+ def unescape
+ return unless @uri
+ CGI.unescape @uri
+ end
+end
diff --git a/lib/rubygems/user_interaction.rb b/lib/rubygems/user_interaction.rb
new file mode 100644
index 0000000000..9fe3e755c4
--- /dev/null
+++ b/lib/rubygems/user_interaction.rb
@@ -0,0 +1,647 @@
+# 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 "text"
+
+##
+# Module that defines the default UserInteraction. Any class including this
+# module will have access to the +ui+ method that returns the default UI.
+
+module Gem::DefaultUserInteraction
+ include Gem::Text
+
+ ##
+ # The default UI is a class variable of the singleton class for this
+ # module.
+
+ @ui = nil
+
+ ##
+ # Return the default UI.
+
+ def self.ui
+ @ui ||= Gem::ConsoleUI.new
+ end
+
+ ##
+ # Set the default UI. If the default UI is never explicitly set, a simple
+ # console based UserInteraction will be used automatically.
+
+ def self.ui=(new_ui)
+ @ui = new_ui
+ end
+
+ ##
+ # Use +new_ui+ for the duration of +block+.
+
+ def self.use_ui(new_ui)
+ old_ui = @ui
+ @ui = new_ui
+ yield
+ ensure
+ @ui = old_ui
+ end
+
+ ##
+ # See DefaultUserInteraction::ui
+
+ def ui
+ Gem::DefaultUserInteraction.ui
+ end
+
+ ##
+ # See DefaultUserInteraction::ui=
+
+ def ui=(new_ui)
+ Gem::DefaultUserInteraction.ui = new_ui
+ end
+
+ ##
+ # See DefaultUserInteraction::use_ui
+
+ def use_ui(new_ui, &block)
+ Gem::DefaultUserInteraction.use_ui(new_ui, &block)
+ end
+end
+
+##
+# UserInteraction allows RubyGems to interact with the user through standard
+# methods that can be replaced with more-specific UI methods for different
+# displays.
+#
+# Since UserInteraction dispatches to a concrete UI class you may need to
+# reference other classes for specific behavior such as Gem::ConsoleUI or
+# Gem::SilentUI.
+#
+# Example:
+#
+# class X
+# include Gem::UserInteraction
+#
+# def get_answer
+# n = ask("What is the meaning of life?")
+# end
+# end
+
+module Gem::UserInteraction
+ include Gem::DefaultUserInteraction
+
+ ##
+ # Displays an alert +statement+. Asks a +question+ if given.
+
+ def alert(statement, question = nil)
+ ui.alert statement, question
+ end
+
+ ##
+ # Displays an error +statement+ to the error output location. Asks a
+ # +question+ if given.
+
+ def alert_error(statement, question = nil)
+ ui.alert_error statement, question
+ end
+
+ ##
+ # Displays a warning +statement+ to the warning output location. Asks a
+ # +question+ if given.
+
+ def alert_warning(statement, question = nil)
+ ui.alert_warning statement, question
+ end
+
+ ##
+ # Asks a +question+ and returns the answer.
+
+ def ask(question)
+ ui.ask question
+ end
+
+ ##
+ # Asks for a password with a +prompt+
+
+ def ask_for_password(prompt)
+ ui.ask_for_password prompt
+ end
+
+ ##
+ # Asks a yes or no +question+. Returns true for yes, false for no.
+
+ def ask_yes_no(question, default = nil)
+ ui.ask_yes_no question, default
+ end
+
+ ##
+ # Asks the user to answer +question+ with an answer from the given +list+.
+
+ def choose_from_list(question, list)
+ ui.choose_from_list question, list
+ end
+
+ ##
+ # Displays the given +statement+ on the standard output (or equivalent).
+
+ def say(statement = "")
+ ui.say statement
+ end
+
+ ##
+ # Terminates the RubyGems process with the given +exit_code+
+
+ def terminate_interaction(exit_code = 0)
+ ui.terminate_interaction exit_code
+ end
+
+ ##
+ # Calls +say+ with +msg+ or the results of the block if really_verbose
+ # is true.
+
+ def verbose(msg = nil)
+ say(clean_text(msg || yield)) if Gem.configuration.really_verbose
+ end
+end
+
+##
+# Gem::StreamUI implements a simple stream based user interface.
+
+class Gem::StreamUI
+ ##
+ # The input stream
+
+ attr_reader :ins
+
+ ##
+ # The output stream
+
+ attr_reader :outs
+
+ ##
+ # The error stream
+
+ attr_reader :errs
+
+ ##
+ # Creates a new StreamUI wrapping +in_stream+ for user input, +out_stream+
+ # for standard output, +err_stream+ for error output. If +usetty+ is true
+ # 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)
+ @ins = in_stream
+ @outs = out_stream
+ @errs = err_stream
+ @usetty = usetty
+ end
+
+ ##
+ # Returns true if TTY methods should be used on this StreamUI.
+
+ def tty?
+ @usetty && @ins.tty?
+ end
+
+ ##
+ # Prints a formatted backtrace to the errors stream if backtraces are
+ # enabled.
+
+ def backtrace(exception)
+ return unless Gem.configuration.backtrace
+
+ @errs.puts "\t#{exception.backtrace.join "\n\t"}"
+ end
+
+ ##
+ # Choose from a list of options. +question+ is a prompt displayed above
+ # the list. +list+ is a list of option strings. Returns the pair
+ # [option_name, option_index].
+
+ def choose_from_list(question, list)
+ @outs.puts question
+
+ list.each_with_index do |item, index|
+ @outs.puts " #{index + 1}. #{item}"
+ end
+
+ @outs.print "> "
+ @outs.flush
+
+ result = @ins.gets
+
+ return nil, nil unless result
+
+ result = result.strip.to_i - 1
+ return nil, nil unless (0...list.size) === result
+ [list[result], result]
+ end
+
+ ##
+ # Ask a question. Returns a true for yes, false for no. If not connected
+ # to a tty, raises an exception if default is nil, otherwise returns
+ # default.
+
+ def ask_yes_no(question, default = nil)
+ unless tty?
+ if default.nil?
+ raise Gem::OperationNotSupportedError,
+ "Not connected to a tty and no default specified"
+ else
+ return default
+ end
+ end
+
+ default_answer = case default
+ 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
+ end
+ end
+
+ result
+ end
+
+ ##
+ # Ask a question. Returns an answer if connected to a tty, nil otherwise.
+
+ def ask(question)
+ return nil unless tty?
+
+ @outs.print(question + " ")
+ @outs.flush
+
+ result = @ins.gets
+ result&.chomp!
+ result
+ end
+
+ ##
+ # Ask for a password. Does not echo response to terminal.
+
+ def ask_for_password(question)
+ return nil unless tty?
+
+ @outs.print(question, " ")
+ @outs.flush
+
+ password = _gets_noecho
+ @outs.puts
+ password&.chomp!
+ password
+ end
+
+ def require_io_console
+ @require_io_console ||= begin
+ begin
+ require "io/console"
+ rescue LoadError
+ end
+ true
+ end
+ end
+
+ def _gets_noecho
+ require_io_console
+ @ins.noecho { @ins.gets }
+ end
+
+ ##
+ # Display a statement.
+
+ def say(statement = "")
+ @outs.puts statement
+ end
+
+ ##
+ # Display an informational alert. Will ask +question+ if it is not nil.
+
+ def alert(statement, question = nil)
+ @outs.puts "INFO: #{statement}"
+ ask(question) if question
+ end
+
+ ##
+ # Display a warning on stderr. Will ask +question+ if it is not nil.
+
+ def alert_warning(statement, question = nil)
+ @errs.puts "WARNING: #{statement}"
+ ask(question) if question
+ end
+
+ ##
+ # Display an error message in a location expected to get error messages.
+ # Will ask +question+ if it is not nil.
+
+ def alert_error(statement, question = nil)
+ @errs.puts "ERROR: #{statement}"
+ ask(question) if question
+ end
+
+ ##
+ # Terminate the application with exit code +status+, running any exit
+ # handlers that might have been defined.
+
+ def terminate_interaction(status = 0)
+ close
+ raise Gem::SystemExitException, status
+ end
+
+ def close
+ end
+
+ ##
+ # Return a progress reporter object chosen from the current verbosity.
+
+ def progress_reporter(*args)
+ case Gem.configuration.verbose
+ when nil, false
+ SilentProgressReporter.new(@outs, *args)
+ when true
+ SimpleProgressReporter.new(@outs, *args)
+ else
+ VerboseProgressReporter.new(@outs, *args)
+ end
+ end
+
+ ##
+ # An absolutely silent progress reporter.
+
+ class SilentProgressReporter
+ ##
+ # The count of items is never updated for the silent progress reporter.
+
+ attr_reader :count
+
+ ##
+ # Creates a silent progress reporter that ignores all input arguments.
+
+ def initialize(out_stream, size, initial_message, terminal_message = nil)
+ end
+
+ ##
+ # Does not print +message+ when updated as this object has taken a vow of
+ # silence.
+
+ def updated(message)
+ end
+
+ ##
+ # Does not print anything when complete as this object has taken a vow of
+ # silence.
+
+ def done
+ end
+ end
+
+ ##
+ # A basic dotted progress reporter.
+
+ class SimpleProgressReporter
+ include Gem::DefaultUserInteraction
+
+ ##
+ # The number of progress items counted so far.
+
+ attr_reader :count
+
+ ##
+ # Creates a new progress reporter that will write to +out_stream+ for
+ # +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")
+ @out = out_stream
+ @total = size
+ @count = 0
+ @terminal_message = terminal_message
+
+ @out.puts initial_message
+ end
+
+ ##
+ # Prints out a dot and ignores +message+.
+
+ def updated(message)
+ @count += 1
+ @out.print "."
+ @out.flush
+ end
+
+ ##
+ # Prints out the terminal message.
+
+ def done
+ @out.puts "\n#{@terminal_message}"
+ end
+ end
+
+ ##
+ # A progress reporter that prints out messages about the current progress.
+
+ class VerboseProgressReporter
+ include Gem::DefaultUserInteraction
+
+ ##
+ # The number of progress items counted so far.
+
+ attr_reader :count
+
+ ##
+ # Creates a new progress reporter that will write to +out_stream+ for
+ # +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")
+ @out = out_stream
+ @total = size
+ @count = 0
+ @terminal_message = terminal_message
+
+ @out.puts initial_message
+ end
+
+ ##
+ # Prints out the position relative to the total and the +message+.
+
+ def updated(message)
+ @count += 1
+ @out.puts "#{@count}/#{@total}: #{message}"
+ end
+
+ ##
+ # Prints out the terminal message.
+
+ def done
+ @out.puts @terminal_message
+ end
+ end
+
+ ##
+ # Return a download reporter object chosen from the current verbosity
+
+ def download_reporter(*args)
+ if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty?
+ SilentDownloadReporter.new(@outs, *args)
+ else
+ ThreadedDownloadReporter.new(@outs, *args)
+ end
+ end
+
+ ##
+ # An absolutely silent download reporter.
+
+ class SilentDownloadReporter
+ ##
+ # The silent download reporter ignores all arguments
+
+ def initialize(out_stream, *args)
+ end
+
+ ##
+ # The silent download reporter does not display +filename+ or care about
+ # +filesize+ because it is silent.
+
+ def fetch(filename, filesize)
+ end
+
+ ##
+ # Nothing can update the silent download reporter.
+
+ def update(current)
+ end
+
+ ##
+ # The silent download reporter won't tell you when the download is done.
+ # Because it is silent.
+
+ def done
+ end
+ end
+
+ ##
+ # A progress reporter that behaves nicely with threaded downloading.
+
+ class ThreadedDownloadReporter
+ MUTEX = Thread::Mutex.new
+
+ ##
+ # The current file name being displayed
+
+ attr_reader :file_name
+
+ ##
+ # Creates a new threaded download reporter that will display on
+ # +out_stream+. The other arguments are ignored.
+
+ def initialize(out_stream, *args)
+ @file_name = nil
+ @out = out_stream
+ end
+
+ ##
+ # Tells the download reporter that the +file_name+ is being fetched.
+ # The other arguments are ignored.
+
+ def fetch(file_name, *args)
+ if @file_name.nil?
+ @file_name = file_name
+ locked_puts "Fetching #{@file_name}"
+ end
+ end
+
+ ##
+ # Updates the threaded download reporter for the given number of +bytes+.
+
+ def update(bytes)
+ # Do nothing.
+ end
+
+ ##
+ # Indicates the download is complete.
+
+ def done
+ # Do nothing.
+ end
+
+ private
+
+ def locked_puts(message)
+ MUTEX.synchronize do
+ @out.puts message
+ end
+ end
+ end
+end
+
+##
+# Subclass of StreamUI that instantiates the user interaction using $stdin,
+# $stdout, and $stderr.
+
+class Gem::ConsoleUI < Gem::StreamUI
+ ##
+ # The Console UI has no arguments as it defaults to reading input from
+ # stdin, output to stdout and warnings or errors to stderr.
+
+ def initialize
+ super $stdin, $stdout, $stderr, true
+ end
+end
+
+##
+# SilentUI is a UI choice that is absolutely silent.
+
+class Gem::SilentUI < Gem::StreamUI
+ ##
+ # The SilentUI has no arguments as it does not use any stream.
+
+ def initialize
+ io = NullIO.new
+ super io, io, io, false
+ end
+
+ def close
+ end
+
+ def download_reporter(*args) # :nodoc:
+ SilentDownloadReporter.new(@outs, *args)
+ end
+
+ def progress_reporter(*args) # :nodoc:
+ SilentProgressReporter.new(@outs, *args)
+ end
+
+ ##
+ # An absolutely silent IO.
+
+ class NullIO
+ def puts(*args)
+ end
+
+ def print(*args)
+ end
+
+ def flush
+ end
+
+ def gets(*args)
+ end
+
+ def tty?
+ false
+ end
+ end
+end
diff --git a/lib/rubygems/util.rb b/lib/rubygems/util.rb
new file mode 100644
index 0000000000..ee4106c6ce
--- /dev/null
+++ b/lib/rubygems/util.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+##
+# This module contains various utility methods as module methods.
+
+module Gem::Util
+ ##
+ # Zlib::GzipReader wrapper that unzips +data+.
+
+ def self.gunzip(data)
+ require "zlib"
+ require "stringio"
+ data = StringIO.new(data, "r")
+
+ gzip_reader = begin
+ Zlib::GzipReader.new(data)
+ rescue Zlib::GzipFile::Error => e
+ raise e.class, e.inspect, e.backtrace
+ end
+
+ unzipped = gzip_reader.read
+ unzipped.force_encoding Encoding::BINARY
+ unzipped
+ end
+
+ ##
+ # Zlib::GzipWriter wrapper that zips +data+.
+
+ def self.gzip(data)
+ require "zlib"
+ require "stringio"
+ zipped = StringIO.new(String.new, "w")
+ zipped.set_encoding Encoding::BINARY
+
+ Zlib::GzipWriter.wrap zipped do |io|
+ io.write data
+ end
+
+ zipped.string
+ end
+
+ ##
+ # A Zlib::Inflate#inflate wrapper
+
+ def self.inflate(data)
+ require "zlib"
+ Zlib::Inflate.inflate data
+ end
+
+ ##
+ # This calls IO.popen and reads the result
+
+ def self.popen(*command)
+ IO.popen command, &:read
+ end
+
+ ##
+ # Enumerates the parents of +directory+.
+
+ def self.traverse_parents(directory, &block)
+ return enum_for __method__, directory unless block_given?
+
+ here = File.expand_path directory
+ loop do
+ begin
+ Dir.chdir here, &block
+ rescue StandardError
+ Errno::EACCES
+ end
+
+ new_here = File.expand_path("..", here)
+ return if new_here == here # toplevel
+ here = new_here
+ end
+ end
+
+ ##
+ # Globs for files matching +pattern+ inside of +directory+,
+ # returning absolute paths to the matching files.
+
+ def self.glob_files_in_dir(glob, base_path)
+ Dir.glob(glob, base: base_path).map! {|f| File.expand_path(f, base_path) }
+ end
+
+ ##
+ # 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.match?(/[a-z]/i) && path[2].chr == ":"
+ path[1..-1]
+ else
+ path
+ end
+ end
+end
diff --git a/lib/rubygems/util/atomic_file_writer.rb b/lib/rubygems/util/atomic_file_writer.rb
new file mode 100644
index 0000000000..32767c6a79
--- /dev/null
+++ b/lib/rubygems/util/atomic_file_writer.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+# Based on ActiveSupport's AtomicFile implementation
+# Copyright (c) David Heinemeier Hansson
+# https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/file/atomic.rb
+# Licensed under the MIT License
+
+module Gem
+ class AtomicFileWriter
+ ##
+ # Write to a file atomically. Useful for situations where you don't
+ # want other processes or threads to see half-written files.
+
+ def self.open(file_name)
+ require "securerandom" unless defined?(SecureRandom)
+
+ old_stat = begin
+ File.stat(file_name)
+ rescue SystemCallError
+ nil
+ end
+
+ # Names can't be longer than 255B
+ tmp_suffix = ".tmp.#{SecureRandom.hex}"
+ dirname = File.dirname(file_name)
+ basename = File.basename(file_name)
+ tmp_path = File.join(dirname, ".#{basename.byteslice(0, 254 - tmp_suffix.bytesize)}#{tmp_suffix}")
+
+ flags = File::RDWR | File::CREAT | File::EXCL | File::BINARY
+ flags |= File::SHARE_DELETE if defined?(File::SHARE_DELETE)
+
+ File.open(tmp_path, flags) do |temp_file|
+ temp_file.binmode
+ if old_stat
+ # Set correct permissions on new file
+ begin
+ File.chown(old_stat.uid, old_stat.gid, temp_file.path)
+ # This operation will affect filesystem ACL's
+ File.chmod(old_stat.mode, temp_file.path)
+ rescue Errno::EPERM, Errno::EACCES
+ # Changing file ownership failed, moving on.
+ end
+ end
+
+ return_val = yield temp_file
+ rescue StandardError => error
+ begin
+ temp_file.close
+ rescue StandardError
+ nil
+ end
+
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ nil
+ end
+
+ raise error
+ else
+ begin
+ File.rename(temp_file.path, file_name)
+ rescue StandardError
+ begin
+ File.unlink(temp_file.path)
+ rescue StandardError
+ end
+
+ raise
+ end
+
+ return_val
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/util/licenses.rb b/lib/rubygems/util/licenses.rb
new file mode 100644
index 0000000000..caf53d0b7e
--- /dev/null
+++ b/lib/rubygems/util/licenses.rb
@@ -0,0 +1,888 @@
+# frozen_string_literal: true
+
+# This is generated by generate_spdx_license_list.rb, any edits to this
+# file will be discarded.
+
+require_relative "../text"
+
+class Gem::Licenses
+ extend Gem::Text
+
+ NONSTANDARD = "Nonstandard"
+ LICENSE_REF = "LicenseRef-.+"
+
+ # Software Package Data Exchange (SPDX) standard open-source software
+ # license identifiers
+ LICENSE_IDENTIFIERS = %w[
+ 0BSD
+ 3D-Slicer-1.0
+ AAL
+ ADSL
+ AFL-1.1
+ AFL-1.2
+ AFL-2.0
+ AFL-2.1
+ AFL-3.0
+ AGPL-1.0-only
+ AGPL-1.0-or-later
+ AGPL-3.0-only
+ AGPL-3.0-or-later
+ ALGLIB-Documentation
+ AMD-newlib
+ AMDPLPA
+ AML
+ AML-glslang
+ AMPAS
+ ANTLR-PD
+ ANTLR-PD-fallback
+ APAFML
+ APL-1.0
+ APSL-1.0
+ APSL-1.1
+ APSL-1.2
+ APSL-2.0
+ ASWF-Digital-Assets-1.0
+ ASWF-Digital-Assets-1.1
+ Abstyles
+ AdaCore-doc
+ Adobe-2006
+ Adobe-Display-PostScript
+ Adobe-Glyph
+ Adobe-Utopia
+ Advanced-Cryptics-Dictionary
+ Afmparse
+ Aladdin
+ Apache-1.0
+ Apache-1.1
+ Apache-2.0
+ App-s2p
+ Arphic-1999
+ Artistic-1.0
+ Artistic-1.0-Perl
+ Artistic-1.0-cl8
+ Artistic-2.0
+ Artistic-dist
+ Aspell-RU
+ BOLA-1.1
+ BSD-1-Clause
+ BSD-2-Clause
+ BSD-2-Clause-Darwin
+ BSD-2-Clause-Patent
+ BSD-2-Clause-Views
+ BSD-2-Clause-first-lines
+ BSD-2-Clause-pkgconf-disclaimer
+ BSD-3-Clause
+ BSD-3-Clause-Attribution
+ BSD-3-Clause-Clear
+ BSD-3-Clause-HP
+ BSD-3-Clause-LBNL
+ BSD-3-Clause-Modification
+ BSD-3-Clause-No-Military-License
+ BSD-3-Clause-No-Nuclear-License
+ BSD-3-Clause-No-Nuclear-License-2014
+ BSD-3-Clause-No-Nuclear-Warranty
+ BSD-3-Clause-Open-MPI
+ BSD-3-Clause-Sun
+ BSD-3-Clause-Tso
+ BSD-3-Clause-acpica
+ BSD-3-Clause-flex
+ BSD-4-Clause
+ BSD-4-Clause-Shortened
+ BSD-4-Clause-UC
+ BSD-4.3RENO
+ BSD-4.3TAHOE
+ BSD-Advertising-Acknowledgement
+ BSD-Attribution-HPND-disclaimer
+ BSD-Inferno-Nettverk
+ BSD-Mark-Modifications
+ BSD-Protection
+ BSD-Source-Code
+ BSD-Source-beginning-file
+ BSD-Systemics
+ BSD-Systemics-W3Works
+ BSL-1.0
+ BUSL-1.1
+ Baekmuk
+ Bahyph
+ Barr
+ Beerware
+ BitTorrent-1.0
+ BitTorrent-1.1
+ Bitstream-Charter
+ Bitstream-Vera
+ BlueOak-1.0.0
+ Boehm-GC
+ Boehm-GC-without-fee
+ Borceux
+ Brian-Gladman-2-Clause
+ Brian-Gladman-3-Clause
+ Buddy
+ C-UDA-1.0
+ CAL-1.0
+ CAL-1.0-Combined-Work-Exception
+ CAPEC-tou
+ CATOSL-1.1
+ CC-BY-1.0
+ CC-BY-2.0
+ CC-BY-2.5
+ CC-BY-2.5-AU
+ CC-BY-3.0
+ CC-BY-3.0-AT
+ CC-BY-3.0-AU
+ CC-BY-3.0-DE
+ CC-BY-3.0-IGO
+ CC-BY-3.0-NL
+ CC-BY-3.0-US
+ CC-BY-4.0
+ CC-BY-NC-1.0
+ CC-BY-NC-2.0
+ CC-BY-NC-2.5
+ CC-BY-NC-3.0
+ CC-BY-NC-3.0-DE
+ CC-BY-NC-4.0
+ CC-BY-NC-ND-1.0
+ CC-BY-NC-ND-2.0
+ CC-BY-NC-ND-2.5
+ CC-BY-NC-ND-3.0
+ CC-BY-NC-ND-3.0-DE
+ CC-BY-NC-ND-3.0-IGO
+ CC-BY-NC-ND-4.0
+ CC-BY-NC-SA-1.0
+ CC-BY-NC-SA-2.0
+ CC-BY-NC-SA-2.0-DE
+ CC-BY-NC-SA-2.0-FR
+ CC-BY-NC-SA-2.0-UK
+ CC-BY-NC-SA-2.5
+ CC-BY-NC-SA-3.0
+ CC-BY-NC-SA-3.0-DE
+ CC-BY-NC-SA-3.0-IGO
+ CC-BY-NC-SA-4.0
+ CC-BY-ND-1.0
+ CC-BY-ND-2.0
+ CC-BY-ND-2.5
+ CC-BY-ND-3.0
+ CC-BY-ND-3.0-DE
+ CC-BY-ND-4.0
+ CC-BY-SA-1.0
+ CC-BY-SA-2.0
+ CC-BY-SA-2.0-UK
+ CC-BY-SA-2.1-JP
+ CC-BY-SA-2.5
+ CC-BY-SA-3.0
+ CC-BY-SA-3.0-AT
+ CC-BY-SA-3.0-DE
+ CC-BY-SA-3.0-IGO
+ CC-BY-SA-4.0
+ CC-PDDC
+ CC-PDM-1.0
+ CC-SA-1.0
+ CC0-1.0
+ CDDL-1.0
+ CDDL-1.1
+ CDL-1.0
+ CDLA-Permissive-1.0
+ CDLA-Permissive-2.0
+ CDLA-Sharing-1.0
+ CECILL-1.0
+ CECILL-1.1
+ CECILL-2.0
+ CECILL-2.1
+ CECILL-B
+ CECILL-C
+ CERN-OHL-1.1
+ CERN-OHL-1.2
+ CERN-OHL-P-2.0
+ CERN-OHL-S-2.0
+ CERN-OHL-W-2.0
+ CFITSIO
+ CMU-Mach
+ CMU-Mach-nodoc
+ CNRI-Jython
+ CNRI-Python
+ CNRI-Python-GPL-Compatible
+ COIL-1.0
+ CPAL-1.0
+ CPL-1.0
+ CPOL-1.02
+ CUA-OPL-1.0
+ Caldera
+ Caldera-no-preamble
+ Catharon
+ ClArtistic
+ Clips
+ Community-Spec-1.0
+ Condor-1.1
+ Cornell-Lossless-JPEG
+ Cronyx
+ Crossword
+ CryptoSwift
+ CrystalStacker
+ Cube
+ D-FSL-1.0
+ DEC-3-Clause
+ DL-DE-BY-2.0
+ DL-DE-ZERO-2.0
+ DOC
+ DRL-1.0
+ DRL-1.1
+ DSDP
+ DocBook-DTD
+ DocBook-Schema
+ DocBook-Stylesheet
+ DocBook-XML
+ Dotseqn
+ ECL-1.0
+ ECL-2.0
+ EFL-1.0
+ EFL-2.0
+ EPICS
+ EPL-1.0
+ EPL-2.0
+ ESA-PL-permissive-2.4
+ ESA-PL-strong-copyleft-2.4
+ ESA-PL-weak-copyleft-2.4
+ EUDatagrid
+ EUPL-1.0
+ EUPL-1.1
+ EUPL-1.2
+ Elastic-2.0
+ Entessa
+ ErlPL-1.1
+ Eurosym
+ FBM
+ FDK-AAC
+ FSFAP
+ FSFAP-no-warranty-disclaimer
+ FSFUL
+ FSFULLR
+ FSFULLRSD
+ FSFULLRWD
+ FSL-1.1-ALv2
+ FSL-1.1-MIT
+ FTL
+ Fair
+ Ferguson-Twofish
+ Frameworx-1.0
+ FreeBSD-DOC
+ FreeImage
+ Furuseth
+ GCR-docs
+ GD
+ GFDL-1.1-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-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-invariants-only
+ GFDL-1.3-invariants-or-later
+ GFDL-1.3-no-invariants-only
+ GFDL-1.3-no-invariants-or-later
+ GFDL-1.3-only
+ GFDL-1.3-or-later
+ GL2PS
+ GLWTPL
+ GPL-1.0-only
+ GPL-1.0-or-later
+ GPL-2.0-only
+ GPL-2.0-or-later
+ GPL-3.0-only
+ GPL-3.0-or-later
+ Game-Programming-Gems
+ Giftware
+ Glide
+ Glulxe
+ Graphics-Gems
+ Gutmann
+ HDF5
+ HIDAPI
+ HP-1986
+ HP-1989
+ HPND
+ HPND-DEC
+ HPND-Fenneberg-Livingston
+ HPND-INRIA-IMAG
+ HPND-Intel
+ HPND-Kevlin-Henney
+ HPND-MIT-disclaimer
+ HPND-Markus-Kuhn
+ HPND-Netrek
+ HPND-Pbmplus
+ HPND-SMC
+ HPND-UC
+ HPND-UC-export-US
+ HPND-doc
+ HPND-doc-sell
+ HPND-export-US
+ HPND-export-US-acknowledgement
+ HPND-export-US-modify
+ HPND-export2-US
+ HPND-merchantability-variant
+ HPND-sell-MIT-disclaimer-xserver
+ HPND-sell-regexpr
+ HPND-sell-variant
+ HPND-sell-variant-MIT-disclaimer
+ HPND-sell-variant-MIT-disclaimer-rev
+ HPND-sell-variant-critical-systems
+ HTMLTIDY
+ HaskellReport
+ Hippocratic-2.1
+ IBM-pibs
+ ICU
+ IEC-Code-Components-EULA
+ IJG
+ IJG-short
+ IPA
+ IPL-1.0
+ ISC
+ ISC-Veillard
+ ISO-permission
+ ImageMagick
+ Imlib2
+ Info-ZIP
+ Inner-Net-2.0
+ InnoSetup
+ Intel
+ Intel-ACPI
+ Interbase-1.0
+ JPL-image
+ JPNIC
+ JSON
+ Jam
+ JasPer-2.0
+ Kastrup
+ Kazlib
+ Knuth-CTAN
+ LAL-1.2
+ LAL-1.3
+ LGPL-2.0-only
+ LGPL-2.0-or-later
+ LGPL-2.1-only
+ LGPL-2.1-or-later
+ LGPL-3.0-only
+ LGPL-3.0-or-later
+ LGPLLR
+ LOOP
+ LPD-document
+ LPL-1.0
+ LPL-1.02
+ LPPL-1.0
+ LPPL-1.1
+ LPPL-1.2
+ LPPL-1.3a
+ LPPL-1.3c
+ LZMA-SDK-9.11-to-9.20
+ LZMA-SDK-9.22
+ Latex2e
+ Latex2e-translated-notice
+ Leptonica
+ LiLiQ-P-1.1
+ LiLiQ-R-1.1
+ LiLiQ-Rplus-1.1
+ Libpng
+ Linux-OpenIB
+ Linux-man-pages-1-para
+ Linux-man-pages-copyleft
+ Linux-man-pages-copyleft-2-para
+ Linux-man-pages-copyleft-var
+ Lucida-Bitmap-Fonts
+ MIPS
+ MIT
+ MIT-0
+ MIT-CMU
+ MIT-Click
+ MIT-Festival
+ MIT-Khronos-old
+ MIT-Modern-Variant
+ MIT-STK
+ MIT-Wu
+ MIT-advertising
+ MIT-enna
+ MIT-feh
+ MIT-open-group
+ MIT-testregex
+ MITNFA
+ MMIXware
+ MMPL-1.0.1
+ MPEG-SSG
+ MPL-1.0
+ MPL-1.1
+ MPL-2.0
+ MPL-2.0-no-copyleft-exception
+ MS-LPL
+ MS-PL
+ MS-RL
+ MTLL
+ Mackerras-3-Clause
+ Mackerras-3-Clause-acknowledgment
+ MakeIndex
+ Martin-Birgmeier
+ McPhee-slideshow
+ Minpack
+ MirOS
+ Motosoto
+ MulanPSL-1.0
+ MulanPSL-2.0
+ Multics
+ Mup
+ NAIST-2003
+ NASA-1.3
+ NBPL-1.0
+ NCBI-PD
+ NCGL-UK-2.0
+ NCL
+ NCSA
+ NGPL
+ NICTA-1.0
+ NIST-PD
+ NIST-PD-TNT
+ NIST-PD-fallback
+ NIST-Software
+ NLOD-1.0
+ NLOD-2.0
+ NLPL
+ NOSL
+ NPL-1.0
+ NPL-1.1
+ NPOSL-3.0
+ NRL
+ NTIA-PD
+ NTP
+ NTP-0
+ Naumen
+ NetCDF
+ Newsletr
+ Nokia
+ Noweb
+ O-UDA-1.0
+ OAR
+ OCCT-PL
+ OCLC-2.0
+ ODC-By-1.0
+ ODbL-1.0
+ OFFIS
+ OFL-1.0
+ OFL-1.0-RFN
+ OFL-1.0-no-RFN
+ OFL-1.1
+ OFL-1.1-RFN
+ OFL-1.1-no-RFN
+ OGC-1.0
+ OGDL-Taiwan-1.0
+ OGL-Canada-2.0
+ OGL-UK-1.0
+ OGL-UK-2.0
+ OGL-UK-3.0
+ OGTSL
+ OLDAP-1.1
+ OLDAP-1.2
+ OLDAP-1.3
+ OLDAP-1.4
+ OLDAP-2.0
+ OLDAP-2.0.1
+ OLDAP-2.1
+ OLDAP-2.2
+ OLDAP-2.2.1
+ OLDAP-2.2.2
+ OLDAP-2.3
+ OLDAP-2.4
+ OLDAP-2.5
+ OLDAP-2.6
+ OLDAP-2.7
+ OLDAP-2.8
+ OLFL-1.3
+ OML
+ OPL-1.0
+ OPL-UK-3.0
+ OPUBL-1.0
+ OSC-1.0
+ OSET-PL-2.1
+ OSL-1.0
+ OSL-1.1
+ OSL-2.0
+ OSL-2.1
+ OSL-3.0
+ OSSP
+ OpenMDW-1.0
+ OpenPBS-2.3
+ OpenSSL
+ OpenSSL-standalone
+ OpenVision
+ PADL
+ PDDL-1.0
+ PHP-3.0
+ PHP-3.01
+ PPL
+ PSF-2.0
+ ParaType-Free-Font-1.3
+ Parity-6.0.0
+ Parity-7.0.0
+ Pixar
+ Plexus
+ PolyForm-Noncommercial-1.0.0
+ PolyForm-Small-Business-1.0.0
+ PostgreSQL
+ Python-2.0
+ Python-2.0.1
+ QPL-1.0
+ QPL-1.0-INRIA-2004
+ Qhull
+ RHeCos-1.1
+ RPL-1.1
+ RPL-1.5
+ RPSL-1.0
+ RSA-MD
+ RSCPL
+ Rdisc
+ Ruby
+ Ruby-pty
+ SAX-PD
+ SAX-PD-2.0
+ SCEA
+ SGI-B-1.0
+ SGI-B-1.1
+ SGI-B-2.0
+ SGI-OpenGL
+ SGMLUG-PM
+ SGP4
+ SHL-0.5
+ SHL-0.51
+ SISSL
+ SISSL-1.2
+ SL
+ SMAIL-GPL
+ SMLNJ
+ SMPPL
+ SNIA
+ SOFA
+ SPL-1.0
+ SSH-OpenSSH
+ SSH-short
+ SSLeay-standalone
+ SSPL-1.0
+ SUL-1.0
+ SWL
+ Saxpath
+ SchemeReport
+ Sendmail
+ Sendmail-8.23
+ Sendmail-Open-Source-1.1
+ SimPL-2.0
+ Sleepycat
+ Soundex
+ Spencer-86
+ Spencer-94
+ Spencer-99
+ SugarCRM-1.1.3
+ Sun-PPP
+ Sun-PPP-2000
+ SunPro
+ Symlinks
+ TAPR-OHL-1.0
+ TCL
+ TCP-wrappers
+ TGPPL-1.0
+ TMate
+ TORQUE-1.1
+ TOSL
+ TPDL
+ TPL-1.0
+ TTWL
+ TTYP0
+ TU-Berlin-1.0
+ TU-Berlin-2.0
+ TekHVC
+ TermReadKey
+ ThirdEye
+ TrustedQSL
+ UCAR
+ UCL-1.0
+ UMich-Merit
+ UPL-1.0
+ URT-RLE
+ Ubuntu-font-1.0
+ UnRAR
+ Unicode-3.0
+ Unicode-DFS-2015
+ Unicode-DFS-2016
+ Unicode-TOU
+ UnixCrypt
+ Unlicense
+ Unlicense-libtelnet
+ Unlicense-libwhirlpool
+ VOSTROM
+ VSL-1.0
+ Vim
+ Vixie-Cron
+ W3C
+ W3C-19980720
+ W3C-20150513
+ WTFNMFPL
+ WTFPL
+ Watcom-1.0
+ Widget-Workshop
+ WordNet
+ Wsuipa
+ X11
+ X11-distribute-modifications-variant
+ X11-no-permit-persons
+ X11-swapped
+ XFree86-1.1
+ XSkat
+ Xdebug-1.03
+ Xerox
+ Xfig
+ Xnet
+ YPL-1.0
+ YPL-1.1
+ ZPL-1.1
+ ZPL-2.0
+ ZPL-2.1
+ Zed
+ Zeeff
+ Zend-2.0
+ Zimbra-1.3
+ Zimbra-1.4
+ Zlib
+ any-OSI
+ any-OSI-perl-modules
+ bcrypt-Solar-Designer
+ blessing
+ bzip2-1.0.6
+ check-cvs
+ checkmk
+ copyleft-next-0.3.0
+ copyleft-next-0.3.1
+ curl
+ cve-tou
+ diffmark
+ dtoa
+ dvipdfm
+ eGenix
+ etalab-2.0
+ fwlw
+ gSOAP-1.3b
+ generic-xts
+ gnuplot
+ gtkbook
+ hdparm
+ hyphen-bulgarian
+ iMatix
+ jove
+ libpng-1.6.35
+ libpng-2.0
+ libselinux-1.0
+ libtiff
+ libutil-David-Nugent
+ lsof
+ magaz
+ mailprio
+ man2html
+ metamail
+ mpi-permissive
+ mpich2
+ mplus
+ ngrep
+ pkgconf
+ pnmstitch
+ psfrag
+ psutils
+ python-ldap
+ radvd
+ snprintf
+ softSurfer
+ ssh-keyscan
+ swrule
+ threeparttable
+ ulem
+ w3m
+ wwl
+ xinetd
+ xkeyboard-config-Zinoviev
+ xlock
+ xpp
+ xzoom
+ zlib-acknowledgement
+ ].freeze
+
+ DEPRECATED_LICENSE_IDENTIFIERS = %w[
+ AGPL-1.0
+ AGPL-3.0
+ BSD-2-Clause-FreeBSD
+ BSD-2-Clause-NetBSD
+ GFDL-1.1
+ GFDL-1.2
+ GFDL-1.3
+ GPL-1.0
+ GPL-1.0+
+ GPL-2.0
+ GPL-2.0+
+ GPL-2.0-with-GCC-exception
+ GPL-2.0-with-autoconf-exception
+ GPL-2.0-with-bison-exception
+ GPL-2.0-with-classpath-exception
+ GPL-2.0-with-font-exception
+ GPL-3.0
+ GPL-3.0+
+ GPL-3.0-with-GCC-exception
+ GPL-3.0-with-autoconf-exception
+ LGPL-2.0
+ LGPL-2.0+
+ LGPL-2.1
+ LGPL-2.1+
+ LGPL-3.0
+ LGPL-3.0+
+ Net-SNMP
+ Nunit
+ StandardML-NJ
+ bzip2-1.0.5
+ eCos-2.0
+ wxWindows
+ ].freeze
+
+ # exception identifiers
+ EXCEPTION_IDENTIFIERS = %w[
+ 389-exception
+ Asterisk-exception
+ Asterisk-linking-protocols-exception
+ Autoconf-exception-2.0
+ Autoconf-exception-3.0
+ Autoconf-exception-generic
+ Autoconf-exception-generic-3.0
+ Autoconf-exception-macro
+ Bison-exception-1.24
+ Bison-exception-2.2
+ Bootloader-exception
+ CGAL-linking-exception
+ CLISP-exception-2.0
+ Classpath-exception-2.0
+ Classpath-exception-2.0-short
+ DigiRule-FOSS-exception
+ Digia-Qt-LGPL-exception-1.1
+ FLTK-exception
+ Fawkes-Runtime-exception
+ Font-exception-2.0
+ GCC-exception-2.0
+ GCC-exception-2.0-note
+ GCC-exception-3.1
+ GNAT-exception
+ GNOME-examples-exception
+ GNU-compiler-exception
+ GPL-3.0-389-ds-base-exception
+ GPL-3.0-interface-exception
+ GPL-3.0-linking-exception
+ GPL-3.0-linking-source-exception
+ GPL-CC-1.0
+ GStreamer-exception-2005
+ GStreamer-exception-2008
+ Gmsh-exception
+ Independent-modules-exception
+ KiCad-libraries-exception
+ LGPL-3.0-linking-exception
+ LLGPL
+ LLVM-exception
+ LZMA-exception
+ Libtool-exception
+ Linux-syscall-note
+ OCCT-exception-1.0
+ OCaml-LGPL-linking-exception
+ OpenJDK-assembly-exception-1.0
+ PCRE2-exception
+ PS-or-PDF-font-exception-20170817
+ QPL-1.0-INRIA-2004-exception
+ Qt-GPL-exception-1.0
+ Qt-LGPL-exception-1.1
+ Qwt-exception-1.0
+ RRDtool-FLOSS-exception-2.0
+ SANE-exception
+ SHL-2.0
+ SHL-2.1
+ SWI-exception
+ Simple-Library-Usage-exception
+ Swift-exception
+ Texinfo-exception
+ UBDL-exception
+ Universal-FOSS-exception-1.0
+ WxWindows-exception-3.1
+ cryptsetup-OpenSSL-exception
+ eCos-exception-2.0
+ erlang-otp-linking-exception
+ fmt-exception
+ freertos-exception-2.0
+ gnu-javamail-exception
+ harbour-exception
+ i2p-gpl-java-exception
+ kvirc-openssl-exception
+ libpri-OpenH323-exception
+ mif-exception
+ mxml-exception
+ openvpn-openssl-exception
+ polyparse-exception
+ romic-exception
+ rsync-linking-exception
+ sqlitestudio-OpenSSL-exception
+ stunnel-exception
+ u-boot-exception-2.0
+ vsftpd-openssl-exception
+ x11vnc-openssl-exception
+ ].freeze
+
+ DEPRECATED_EXCEPTION_IDENTIFIERS = %w[
+ Nokia-Qt-exception-1.1
+ ].freeze
+
+ VALID_REGEXP = /
+ \A
+ (?:
+ #{Regexp.union(LICENSE_IDENTIFIERS)}
+ \+?
+ (?:\s WITH \s #{Regexp.union(EXCEPTION_IDENTIFIERS)})?
+ | #{NONSTANDARD}
+ | #{LICENSE_REF}
+ )
+ \Z
+ /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)
+ 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)
+ by_distance = LICENSE_IDENTIFIERS.group_by do |identifier|
+ levenshtein_distance(identifier, license)
+ end
+ lowest = by_distance.keys.min
+ return unless lowest < license.size
+ by_distance[lowest]
+ end
+end
diff --git a/lib/rubygems/validator.rb b/lib/rubygems/validator.rb
new file mode 100644
index 0000000000..eb5b513570
--- /dev/null
+++ b/lib/rubygems/validator.rb
@@ -0,0 +1,144 @@
+# 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 "package"
+require_relative "installer"
+
+##
+# Validator performs various gem file and gem database validation
+
+class Gem::Validator
+ include Gem::UserInteraction
+
+ def initialize # :nodoc:
+ require "find"
+ end
+
+ private
+
+ def find_files_for_gem(gem_directory)
+ installed_files = []
+
+ Find.find gem_directory do |file_name|
+ 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
+
+ installed_files
+ end
+
+ public
+
+ ##
+ # Describes a problem with a file in a gem.
+
+ ErrorData = Struct.new :path, :problem do
+ def <=>(other) # :nodoc:
+ return nil unless self.class === other
+
+ [path, problem] <=> [other.path, other.problem]
+ end
+ end
+
+ ##
+ # Checks the gem directory for the following potential
+ # inconsistencies/problems:
+ #
+ # * Checksum gem itself
+ # * For each file in each gem, check consistency of installed versions
+ # * Check for files that aren't part of the gem but are in the gems directory
+ # * 1 cache - 1 spec - 1 directory.
+ #
+ # returns a hash of ErrorData objects, keyed on the problem gem's name.
+ #--
+ # TODO needs further cleanup
+
+ def alien(gems = [])
+ errors = Hash.new {|h,k| h[k] = {} }
+
+ Gem::Specification.each do |spec|
+ unless gems.empty?
+ next unless gems.include? spec.name
+ end
+ next if spec.default_gem?
+
+ gem_name = spec.file_name
+ gem_path = spec.cache_file
+ spec_path = spec.spec_file
+ gem_directory = spec.full_gem_path
+
+ unless File.directory? gem_directory
+ errors[gem_name][spec.full_name] =
+ "Gem registered but doesn't exist at #{gem_directory}"
+ next
+ end
+
+ unless File.exist? spec_path
+ errors[gem_name][spec_path] = "Spec file missing for installed gem"
+ end
+
+ begin
+ unless File.readable?(gem_path)
+ raise Gem::VerificationError, "missing gem file #{gem_path}"
+ end
+
+ good, gone, unreadable = nil, nil, nil, nil
+
+ File.open gem_path, Gem.binary_mode do |_file|
+ package = Gem::Package.new gem_path
+
+ good, gone = package.contents.partition do |file_name|
+ File.exist? File.join(gem_directory, file_name)
+ end
+
+ gone.sort.each do |path|
+ errors[gem_name][path] = "Missing file"
+ end
+
+ good, unreadable = good.partition do |file_name|
+ File.readable? File.join(gem_directory, file_name)
+ end
+
+ unreadable.sort.each do |path|
+ errors[gem_name][path] = "Unreadable file"
+ end
+
+ good.each do |entry, data|
+ next unless data # HACK: `gem check -a mkrf`
+
+ 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
+ end
+ end
+ end
+
+ installed_files = find_files_for_gem(gem_directory)
+ extras = installed_files - good - unreadable
+
+ extras.each do |extra|
+ errors[gem_name][extra] = "Extra file"
+ end
+ rescue Gem::VerificationError => e
+ errors[gem_name][gem_path] = e.message
+ end
+ end
+
+ errors.each do |name, subhash|
+ errors[name] = subhash.map do |path, msg|
+ ErrorData.new path, msg
+ end.sort
+ end
+
+ errors
+ end
+end
diff --git a/lib/rubygems/vendor/.document b/lib/rubygems/vendor/.document
new file mode 100644
index 0000000000..0c43bbd6b3
--- /dev/null
+++ b/lib/rubygems/vendor/.document
@@ -0,0 +1 @@
+# Vendored files do not need to be documented
diff --git a/lib/rubygems/vendor/net-http/lib/net/http.rb b/lib/rubygems/vendor/net-http/lib/net/http.rb
new file mode 100644
index 0000000000..4800cd25f1
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http.rb
@@ -0,0 +1,2608 @@
+# frozen_string_literal: true
+#
+# = net/http.rb
+#
+# Copyright (c) 1999-2007 Yukihiro Matsumoto
+# Copyright (c) 1999-2007 Minero Aoki
+# Copyright (c) 2001 GOTOU Yuuzou
+#
+# Written and maintained by Minero Aoki <aamine@loveruby.net>.
+# HTTPS support added by GOTOU Yuuzou <gotoyuzo@notwork.org>.
+#
+# This file is derived from "http-access.rb".
+#
+# Documented by Minero Aoki; converted to RDoc by William Webber.
+#
+# This program is free software. You can re-distribute and/or
+# modify this program under the same terms of ruby itself ---
+# Ruby Distribution License or GNU General Public License.
+#
+# See Gem::Net::HTTP for an overview and examples.
+#
+
+require_relative '../../../net-protocol/lib/net/protocol'
+require_relative '../../../uri/lib/uri'
+require_relative '../../../resolv/lib/resolv'
+autoload :OpenSSL, 'openssl'
+
+module Gem::Net #:nodoc:
+
+ # :stopdoc:
+ class HTTPBadResponse < StandardError; end
+ class HTTPHeaderSyntaxError < StandardError; end
+ # :startdoc:
+
+ # \Class \Gem::Net::HTTP provides a rich library that implements the client
+ # in a client-server model that uses the \HTTP request-response protocol.
+ # For information about \HTTP, see:
+ #
+ # - {Hypertext Transfer Protocol}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol].
+ # - {Technical overview}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Technical_overview].
+ #
+ # == About the Examples
+ #
+ # :include: doc/net-http/examples.rdoc
+ #
+ # == Strategies
+ #
+ # - If you will make only a few GET requests,
+ # consider using {OpenURI}[https://docs.ruby-lang.org/en/master/OpenURI.html].
+ # - If you will make only a few requests of all kinds,
+ # consider using the various singleton convenience methods in this class.
+ # Each of the following methods automatically starts and finishes
+ # a {session}[rdoc-ref:Gem::Net::HTTP@Sessions] that sends a single request:
+ #
+ # # Return string response body.
+ # Gem::Net::HTTP.get(hostname, path)
+ # Gem::Net::HTTP.get(uri)
+ #
+ # # Write string response body to $stdout.
+ # Gem::Net::HTTP.get_print(hostname, path)
+ # Gem::Net::HTTP.get_print(uri)
+ #
+ # # Return response as Gem::Net::HTTPResponse object.
+ # Gem::Net::HTTP.get_response(hostname, path)
+ # Gem::Net::HTTP.get_response(uri)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Gem::Net::HTTP.post(uri, data)
+ # params = {title: 'foo', body: 'bar', userId: 1}
+ # Gem::Net::HTTP.post_form(uri, params)
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # Gem::Net::HTTP.put(uri, data)
+ #
+ # - If performance is important, consider using sessions, which lower request overhead.
+ # This {session}[rdoc-ref:Gem::Net::HTTP@Sessions] has multiple requests for
+ # {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]
+ # and {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # # Session started automatically before block execution.
+ # http.get(path)
+ # http.head(path)
+ # body = 'Some text'
+ # http.post(path, body) # Can also have a block.
+ # http.put(path, body)
+ # http.delete(path)
+ # http.options(path)
+ # http.trace(path)
+ # http.patch(path, body) # Can also have a block.
+ # http.copy(path)
+ # http.lock(path, body)
+ # http.mkcol(path, body)
+ # http.move(path)
+ # http.propfind(path, body)
+ # http.proppatch(path, body)
+ # http.unlock(path, body)
+ # # Session finished automatically at block exit.
+ # end
+ #
+ # The methods cited above are convenience methods that, via their few arguments,
+ # allow minimal control over the requests.
+ # For greater control, consider using {request objects}[rdoc-ref:Gem::Net::HTTPRequest].
+ #
+ # == URIs
+ #
+ # On the internet, a Gem::URI
+ # ({Universal Resource Identifier}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier])
+ # is a string that identifies a particular resource.
+ # It consists of some or all of: scheme, hostname, path, query, and fragment;
+ # see {Gem::URI syntax}[https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#Syntax].
+ #
+ # A Ruby {Gem::URI::Generic}[https://docs.ruby-lang.org/en/master/Gem::URI/Generic.html] object
+ # represents an internet Gem::URI.
+ # It provides, among others, methods
+ # +scheme+, +hostname+, +path+, +query+, and +fragment+.
+ #
+ # === Schemes
+ #
+ # An internet \Gem::URI has
+ # a {scheme}[https://en.wikipedia.org/wiki/List_of_URI_schemes].
+ #
+ # The two schemes supported in \Gem::Net::HTTP are <tt>'https'</tt> and <tt>'http'</tt>:
+ #
+ # uri.scheme # => "https"
+ # Gem::URI('http://example.com').scheme # => "http"
+ #
+ # === Hostnames
+ #
+ # A hostname identifies a server (host) to which requests may be sent:
+ #
+ # hostname = uri.hostname # => "jsonplaceholder.typicode.com"
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # # Some HTTP stuff.
+ # end
+ #
+ # === Paths
+ #
+ # A host-specific path identifies a resource on the host:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/todos/1'
+ # hostname = _uri.hostname
+ # path = _uri.path
+ # Gem::Net::HTTP.get(hostname, path)
+ #
+ # === Queries
+ #
+ # A host-specific query adds name/value pairs to the Gem::URI:
+ #
+ # _uri = uri.dup
+ # params = {userId: 1, completed: false}
+ # _uri.query = Gem::URI.encode_www_form(params)
+ # _uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com?userId=1&completed=false>
+ # Gem::Net::HTTP.get(_uri)
+ #
+ # === Fragments
+ #
+ # A {Gem::URI fragment}[https://en.wikipedia.org/wiki/URI_fragment] has no effect
+ # in \Gem::Net::HTTP;
+ # the same data is returned, regardless of whether a fragment is included.
+ #
+ # == Request Headers
+ #
+ # Request headers may be used to pass additional information to the host,
+ # similar to arguments passed in a method call;
+ # each header is a name/value pair.
+ #
+ # Each of the \Gem::Net::HTTP methods that sends a request to the host
+ # has optional argument +headers+,
+ # where the headers are expressed as a hash of field-name/value pairs:
+ #
+ # headers = {Accept: 'application/json', Connection: 'Keep-Alive'}
+ # Gem::Net::HTTP.get(uri, headers)
+ #
+ # See lists of both standard request fields and common request fields at
+ # {Request Fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+ # A host may also accept other custom fields.
+ #
+ # == \HTTP Sessions
+ #
+ # A _session_ is a connection between a server (host) and a client that:
+ #
+ # - Is begun by instance method Gem::Net::HTTP#start.
+ # - May contain any number of requests.
+ # - Is ended by instance method Gem::Net::HTTP#finish.
+ #
+ # See example sessions at {Strategies}[rdoc-ref:Gem::Net::HTTP@Strategies].
+ #
+ # === Session Using \Gem::Net::HTTP.start
+ #
+ # If you have many requests to make to a single host (and port),
+ # consider using singleton method Gem::Net::HTTP.start with a block;
+ # the method handles the session automatically by:
+ #
+ # - Calling #start before block execution.
+ # - Executing the block.
+ # - Calling #finish after block execution.
+ #
+ # In the block, you can use these instance methods,
+ # each of which that sends a single request:
+ #
+ # - {HTTP methods}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods]:
+ #
+ # - #get, #request_get: GET.
+ # - #head, #request_head: HEAD.
+ # - #post, #request_post: POST.
+ # - #delete: DELETE.
+ # - #options: OPTIONS.
+ # - #trace: TRACE.
+ # - #patch: PATCH.
+ #
+ # - {WebDAV methods}[https://en.wikipedia.org/wiki/WebDAV#Implementation]:
+ #
+ # - #copy: COPY.
+ # - #lock: LOCK.
+ # - #mkcol: MKCOL.
+ # - #move: MOVE.
+ # - #propfind: PROPFIND.
+ # - #proppatch: PROPPATCH.
+ # - #unlock: UNLOCK.
+ #
+ # === Session Using \Gem::Net::HTTP.start and \Gem::Net::HTTP.finish
+ #
+ # You can manage a session manually using methods #start and #finish:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.get('/todos/1')
+ # http.get('/todos/2')
+ # http.delete('/posts/1')
+ # http.finish # Needed to free resources.
+ #
+ # === Single-Request Session
+ #
+ # Certain convenience methods automatically handle a session by:
+ #
+ # - Creating an \HTTP object
+ # - Starting a session.
+ # - Sending a single request.
+ # - Finishing the session.
+ # - Destroying the object.
+ #
+ # Such methods that send GET requests:
+ #
+ # - ::get: Returns the string response body.
+ # - ::get_print: Writes the string response body to $stdout.
+ # - ::get_response: Returns a Gem::Net::HTTPResponse object.
+ #
+ # Such methods that send POST requests:
+ #
+ # - ::post: Posts data to the host.
+ # - ::post_form: Posts form data to the host.
+ #
+ # == \HTTP Requests and Responses
+ #
+ # Many of the methods above are convenience methods,
+ # each of which sends a request and returns a string
+ # without directly using \Gem::Net::HTTPRequest and \Gem::Net::HTTPResponse objects.
+ #
+ # You can, however, directly create a request object, send the request,
+ # and retrieve the response object; see:
+ #
+ # - Gem::Net::HTTPRequest.
+ # - Gem::Net::HTTPResponse.
+ #
+ # == Following Redirection
+ #
+ # Each returned response is an instance of a subclass of Gem::Net::HTTPResponse.
+ # See the {response class hierarchy}[rdoc-ref:Gem::Net::HTTPResponse@Response+Subclasses].
+ #
+ # In particular, class Gem::Net::HTTPRedirection is the parent
+ # of all redirection classes.
+ # This allows you to craft a case statement to handle redirections properly:
+ #
+ # def fetch(uri, limit = 10)
+ # # You should choose a better exception.
+ # raise ArgumentError, 'Too many HTTP redirects' if limit == 0
+ #
+ # res = Gem::Net::HTTP.get_response(Gem::URI(uri))
+ # case res
+ # when Gem::Net::HTTPSuccess # Any success class.
+ # res
+ # when Gem::Net::HTTPRedirection # Any redirection class.
+ # location = res['Location']
+ # warn "Redirected to #{location}"
+ # fetch(location, limit - 1)
+ # else # Any other class.
+ # res.value
+ # end
+ # end
+ #
+ # fetch(uri)
+ #
+ # == Basic Authentication
+ #
+ # Basic authentication is performed according to
+ # {RFC2617}[http://www.ietf.org/rfc/rfc2617.txt]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.basic_auth('user', 'pass')
+ # res = Gem::Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end
+ #
+ # == Streaming Response Bodies
+ #
+ # By default \Gem::Net::HTTP reads an entire response into memory. If you are
+ # handling large files or wish to implement a progress bar you can instead
+ # stream the body directly to an IO.
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # http.request(req) do |res|
+ # open('t.tmp', 'w') do |f|
+ # res.read_body do |chunk|
+ # f.write chunk
+ # end
+ # end
+ # end
+ # end
+ #
+ # == HTTPS
+ #
+ # HTTPS is enabled for an \HTTP connection by Gem::Net::HTTP#use_ssl=:
+ #
+ # Gem::Net::HTTP.start(hostname, :use_ssl => true) do |http|
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # res = http.request(req)
+ # end
+ #
+ # Or if you simply want to make a GET request, you may pass in a Gem::URI
+ # object that has an \HTTPS URL. \Gem::Net::HTTP automatically turns on TLS
+ # verification if the Gem::URI object has a 'https' Gem::URI scheme:
+ #
+ # uri # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Gem::Net::HTTP.get(uri)
+ #
+ # == Proxy Server
+ #
+ # An \HTTP object can have
+ # a {proxy server}[https://en.wikipedia.org/wiki/Proxy_server].
+ #
+ # You can create an \HTTP object with a proxy server
+ # using method Gem::Net::HTTP.new or method Gem::Net::HTTP.start.
+ #
+ # The proxy may be defined either by argument +p_addr+
+ # or by environment variable <tt>'http_proxy'</tt>.
+ #
+ # === Proxy Using Argument +p_addr+ as a \String
+ #
+ # When argument +p_addr+ is a string hostname,
+ # the returned +http+ has the given host as its proxy:
+ #
+ # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example')
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The port, username, and password for the proxy may also be given:
+ #
+ # http = Gem::Net::HTTP.new(hostname, nil, 'proxy.example', 8000, 'pname', 'ppass')
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.proxy? # => true
+ # http.proxy_from_env? # => false
+ # http.proxy_address # => "proxy.example"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Proxy Using '<tt>ENV['http_proxy']</tt>'
+ #
+ # When environment variable <tt>'http_proxy'</tt>
+ # is set to a \Gem::URI string,
+ # the returned +http+ will have the server at that Gem::URI as its proxy;
+ # note that the \Gem::URI string must have a protocol
+ # such as <tt>'http'</tt> or <tt>'https'</tt>:
+ #
+ # ENV['http_proxy'] = 'http://example.com'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # # These use default values.
+ # http.proxy_port # => 80
+ # http.proxy_user # => nil
+ # http.proxy_pass # => nil
+ #
+ # The \Gem::URI string may include proxy username, password, and port number:
+ #
+ # ENV['http_proxy'] = 'http://pname:ppass@example.com:8000'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proxy? # => true
+ # http.proxy_from_env? # => true
+ # http.proxy_address # => "example.com"
+ # http.proxy_port # => 8000
+ # http.proxy_user # => "pname"
+ # http.proxy_pass # => "ppass"
+ #
+ # === Filtering Proxies
+ #
+ # With method Gem::Net::HTTP.new (but not Gem::Net::HTTP.start),
+ # you can use argument +p_no_proxy+ to filter proxies:
+ #
+ # - Reject a certain address:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain domains or subdomains:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy.example', 8000, 'pname', 'ppass', 'proxy.example')
+ # http.proxy_address # => nil
+ #
+ # - Reject certain addresses and port combinations:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:1234')
+ # http.proxy_address # => "proxy.example"
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # - Reject a list of the types above delimited using a comma:
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'proxy.example', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # http = Gem::Net::HTTP.new('example.com', nil, 'my.proxy', 8000, 'pname', 'ppass', 'my.proxy,proxy.example:8000')
+ # http.proxy_address # => nil
+ #
+ # == Compression and Decompression
+ #
+ # \Gem::Net::HTTP does not compress the body of a request before sending.
+ #
+ # By default, \Gem::Net::HTTP adds header <tt>'Accept-Encoding'</tt>
+ # to a new {request object}[rdoc-ref:Gem::Net::HTTPRequest]:
+ #
+ # Gem::Net::HTTP::Get.new(uri)['Accept-Encoding']
+ # # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ #
+ # This requests the server to zip-encode the response body if there is one;
+ # the server is not required to do so.
+ #
+ # \Gem::Net::HTTP does not automatically decompress a response body
+ # if the response has header <tt>'Content-Range'</tt>.
+ #
+ # Otherwise decompression (or not) depends on the value of header
+ # {Content-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-encoding-response-header]:
+ #
+ # - <tt>'deflate'</tt>, <tt>'gzip'</tt>, or <tt>'x-gzip'</tt>:
+ # decompresses the body and deletes the header.
+ # - <tt>'none'</tt> or <tt>'identity'</tt>:
+ # does not decompress the body, but deletes the header.
+ # - Any other value:
+ # leaves the body and header unchanged.
+ #
+ # == What's Here
+ #
+ # First, what's elsewhere. Class Gem::Net::HTTP:
+ #
+ # - Inherits from {class Object}[https://docs.ruby-lang.org/en/master/Object.html#class-Object-label-What-27s+Here].
+ #
+ # This is a categorized summary of methods and attributes.
+ #
+ # === \Gem::Net::HTTP Objects
+ #
+ # - {::new}[rdoc-ref:Gem::Net::HTTP.new]:
+ # Creates a new instance.
+ # - {#inspect}[rdoc-ref:Gem::Net::HTTP#inspect]:
+ # Returns a string representation of +self+.
+ #
+ # === Sessions
+ #
+ # - {::start}[rdoc-ref:Gem::Net::HTTP.start]:
+ # Begins a new session in a new \Gem::Net::HTTP object.
+ # - {#started?}[rdoc-ref:Gem::Net::HTTP#started?]:
+ # Returns whether in a session.
+ # - {#finish}[rdoc-ref:Gem::Net::HTTP#finish]:
+ # Ends an active session.
+ # - {#start}[rdoc-ref:Gem::Net::HTTP#start]:
+ # Begins a new session in an existing \Gem::Net::HTTP object (+self+).
+ #
+ # === Connections
+ #
+ # - {:continue_timeout}[rdoc-ref:Gem::Net::HTTP#continue_timeout]:
+ # Returns the continue timeout.
+ # - {#continue_timeout=}[rdoc-ref:Gem::Net::HTTP#continue_timeout=]:
+ # Sets the continue timeout seconds.
+ # - {:keep_alive_timeout}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout]:
+ # Returns the keep-alive timeout.
+ # - {:keep_alive_timeout=}[rdoc-ref:Gem::Net::HTTP#keep_alive_timeout=]:
+ # Sets the keep-alive timeout.
+ # - {:max_retries}[rdoc-ref:Gem::Net::HTTP#max_retries]:
+ # Returns the maximum retries.
+ # - {#max_retries=}[rdoc-ref:Gem::Net::HTTP#max_retries=]:
+ # Sets the maximum retries.
+ # - {:open_timeout}[rdoc-ref:Gem::Net::HTTP#open_timeout]:
+ # Returns the open timeout.
+ # - {:open_timeout=}[rdoc-ref:Gem::Net::HTTP#open_timeout=]:
+ # Sets the open timeout.
+ # - {:read_timeout}[rdoc-ref:Gem::Net::HTTP#read_timeout]:
+ # Returns the open timeout.
+ # - {:read_timeout=}[rdoc-ref:Gem::Net::HTTP#read_timeout=]:
+ # Sets the read timeout.
+ # - {:ssl_timeout}[rdoc-ref:Gem::Net::HTTP#ssl_timeout]:
+ # Returns the ssl timeout.
+ # - {:ssl_timeout=}[rdoc-ref:Gem::Net::HTTP#ssl_timeout=]:
+ # Sets the ssl timeout.
+ # - {:write_timeout}[rdoc-ref:Gem::Net::HTTP#write_timeout]:
+ # Returns the write timeout.
+ # - {write_timeout=}[rdoc-ref:Gem::Net::HTTP#write_timeout=]:
+ # Sets the write timeout.
+ #
+ # === Requests
+ #
+ # - {::get}[rdoc-ref:Gem::Net::HTTP.get]:
+ # Sends a GET request and returns the string response body.
+ # - {::get_print}[rdoc-ref:Gem::Net::HTTP.get_print]:
+ # Sends a GET request and write the string response body to $stdout.
+ # - {::get_response}[rdoc-ref:Gem::Net::HTTP.get_response]:
+ # Sends a GET request and returns a response object.
+ # - {::post_form}[rdoc-ref:Gem::Net::HTTP.post_form]:
+ # Sends a POST request with form data and returns a response object.
+ # - {::post}[rdoc-ref:Gem::Net::HTTP.post]:
+ # Sends a POST request with data and returns a response object.
+ # - {::put}[rdoc-ref:Gem::Net::HTTP.put]:
+ # Sends a PUT request with data and returns a response object.
+ # - {#copy}[rdoc-ref:Gem::Net::HTTP#copy]:
+ # Sends a COPY request and returns a response object.
+ # - {#delete}[rdoc-ref:Gem::Net::HTTP#delete]:
+ # Sends a DELETE request and returns a response object.
+ # - {#get}[rdoc-ref:Gem::Net::HTTP#get]:
+ # Sends a GET request and returns a response object.
+ # - {#head}[rdoc-ref:Gem::Net::HTTP#head]:
+ # Sends a HEAD request and returns a response object.
+ # - {#lock}[rdoc-ref:Gem::Net::HTTP#lock]:
+ # Sends a LOCK request and returns a response object.
+ # - {#mkcol}[rdoc-ref:Gem::Net::HTTP#mkcol]:
+ # Sends a MKCOL request and returns a response object.
+ # - {#move}[rdoc-ref:Gem::Net::HTTP#move]:
+ # Sends a MOVE request and returns a response object.
+ # - {#options}[rdoc-ref:Gem::Net::HTTP#options]:
+ # Sends a OPTIONS request and returns a response object.
+ # - {#patch}[rdoc-ref:Gem::Net::HTTP#patch]:
+ # Sends a PATCH request and returns a response object.
+ # - {#post}[rdoc-ref:Gem::Net::HTTP#post]:
+ # Sends a POST request and returns a response object.
+ # - {#propfind}[rdoc-ref:Gem::Net::HTTP#propfind]:
+ # Sends a PROPFIND request and returns a response object.
+ # - {#proppatch}[rdoc-ref:Gem::Net::HTTP#proppatch]:
+ # Sends a PROPPATCH request and returns a response object.
+ # - {#put}[rdoc-ref:Gem::Net::HTTP#put]:
+ # Sends a PUT request and returns a response object.
+ # - {#request}[rdoc-ref:Gem::Net::HTTP#request]:
+ # Sends a request and returns a response object.
+ # - {#request_get}[rdoc-ref:Gem::Net::HTTP#request_get]:
+ # Sends a GET request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_head}[rdoc-ref:Gem::Net::HTTP#request_head]:
+ # Sends a HEAD request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#request_post}[rdoc-ref:Gem::Net::HTTP#request_post]:
+ # Sends a POST request and forms a response object;
+ # if a block given, calls the block with the object,
+ # otherwise returns the object.
+ # - {#send_request}[rdoc-ref:Gem::Net::HTTP#send_request]:
+ # Sends a request and returns a response object.
+ # - {#trace}[rdoc-ref:Gem::Net::HTTP#trace]:
+ # Sends a TRACE request and returns a response object.
+ # - {#unlock}[rdoc-ref:Gem::Net::HTTP#unlock]:
+ # Sends an UNLOCK request and returns a response object.
+ #
+ # === Responses
+ #
+ # - {:close_on_empty_response}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response]:
+ # Returns whether to close connection on empty response.
+ # - {:close_on_empty_response=}[rdoc-ref:Gem::Net::HTTP#close_on_empty_response=]:
+ # Sets whether to close connection on empty response.
+ # - {:ignore_eof}[rdoc-ref:Gem::Net::HTTP#ignore_eof]:
+ # Returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:ignore_eof=}[rdoc-ref:Gem::Net::HTTP#ignore_eof=]:
+ # Sets whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers.
+ # - {:response_body_encoding}[rdoc-ref:Gem::Net::HTTP#response_body_encoding]:
+ # Returns the encoding to use for the response body.
+ # - {#response_body_encoding=}[rdoc-ref:Gem::Net::HTTP#response_body_encoding=]:
+ # Sets the response body encoding.
+ #
+ # === Proxies
+ #
+ # - {:proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {:proxy_address=}[rdoc-ref:Gem::Net::HTTP#proxy_address=]:
+ # Sets the proxy address.
+ # - {::proxy_class?}[rdoc-ref:Gem::Net::HTTP.proxy_class?]:
+ # Returns whether +self+ is a proxy class.
+ # - {#proxy?}[rdoc-ref:Gem::Net::HTTP#proxy?]:
+ # Returns whether +self+ has a proxy.
+ # - {#proxy_address}[rdoc-ref:Gem::Net::HTTP#proxy_address]:
+ # Returns the proxy address.
+ # - {#proxy_from_env?}[rdoc-ref:Gem::Net::HTTP#proxy_from_env?]:
+ # Returns whether the proxy is taken from an environment variable.
+ # - {:proxy_from_env=}[rdoc-ref:Gem::Net::HTTP#proxy_from_env=]:
+ # Sets whether the proxy is to be taken from an environment variable.
+ # - {:proxy_pass}[rdoc-ref:Gem::Net::HTTP#proxy_pass]:
+ # Returns the proxy password.
+ # - {:proxy_pass=}[rdoc-ref:Gem::Net::HTTP#proxy_pass=]:
+ # Sets the proxy password.
+ # - {:proxy_port}[rdoc-ref:Gem::Net::HTTP#proxy_port]:
+ # Returns the proxy port.
+ # - {:proxy_port=}[rdoc-ref:Gem::Net::HTTP#proxy_port=]:
+ # Sets the proxy port.
+ # - {#proxy_user}[rdoc-ref:Gem::Net::HTTP#proxy_user]:
+ # Returns the proxy user name.
+ # - {:proxy_user=}[rdoc-ref:Gem::Net::HTTP#proxy_user=]:
+ # Sets the proxy user.
+ #
+ # === Security
+ #
+ # - {:ca_file}[rdoc-ref:Gem::Net::HTTP#ca_file]:
+ # Returns the path to a CA certification file.
+ # - {:ca_file=}[rdoc-ref:Gem::Net::HTTP#ca_file=]:
+ # Sets the path to a CA certification file.
+ # - {:ca_path}[rdoc-ref:Gem::Net::HTTP#ca_path]:
+ # Returns the path of to CA directory containing certification files.
+ # - {:ca_path=}[rdoc-ref:Gem::Net::HTTP#ca_path=]:
+ # Sets the path of to CA directory containing certification files.
+ # - {:cert}[rdoc-ref:Gem::Net::HTTP#cert]:
+ # Returns the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert=}[rdoc-ref:Gem::Net::HTTP#cert=]:
+ # Sets the OpenSSL::X509::Certificate object to be used for client certification.
+ # - {:cert_store}[rdoc-ref:Gem::Net::HTTP#cert_store]:
+ # Returns the X509::Store to be used for verifying peer certificate.
+ # - {:cert_store=}[rdoc-ref:Gem::Net::HTTP#cert_store=]:
+ # Sets the X509::Store to be used for verifying peer certificate.
+ # - {:ciphers}[rdoc-ref:Gem::Net::HTTP#ciphers]:
+ # Returns the available SSL ciphers.
+ # - {:ciphers=}[rdoc-ref:Gem::Net::HTTP#ciphers=]:
+ # Sets the available SSL ciphers.
+ # - {:extra_chain_cert}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert]:
+ # Returns the extra X509 certificates to be added to the certificate chain.
+ # - {:extra_chain_cert=}[rdoc-ref:Gem::Net::HTTP#extra_chain_cert=]:
+ # Sets the extra X509 certificates to be added to the certificate chain.
+ # - {:key}[rdoc-ref:Gem::Net::HTTP#key]:
+ # Returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:key=}[rdoc-ref:Gem::Net::HTTP#key=]:
+ # Sets the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ # - {:max_version}[rdoc-ref:Gem::Net::HTTP#max_version]:
+ # Returns the maximum SSL version.
+ # - {:max_version=}[rdoc-ref:Gem::Net::HTTP#max_version=]:
+ # Sets the maximum SSL version.
+ # - {:min_version}[rdoc-ref:Gem::Net::HTTP#min_version]:
+ # Returns the minimum SSL version.
+ # - {:min_version=}[rdoc-ref:Gem::Net::HTTP#min_version=]:
+ # Sets the minimum SSL version.
+ # - {#peer_cert}[rdoc-ref:Gem::Net::HTTP#peer_cert]:
+ # Returns the X509 certificate chain for the session's socket peer.
+ # - {:ssl_version}[rdoc-ref:Gem::Net::HTTP#ssl_version]:
+ # Returns the SSL version.
+ # - {:ssl_version=}[rdoc-ref:Gem::Net::HTTP#ssl_version=]:
+ # Sets the SSL version.
+ # - {#use_ssl=}[rdoc-ref:Gem::Net::HTTP#use_ssl=]:
+ # Sets whether a new session is to use Transport Layer Security.
+ # - {#use_ssl?}[rdoc-ref:Gem::Net::HTTP#use_ssl?]:
+ # Returns whether +self+ uses SSL.
+ # - {:verify_callback}[rdoc-ref:Gem::Net::HTTP#verify_callback]:
+ # Returns the callback for the server certification verification.
+ # - {:verify_callback=}[rdoc-ref:Gem::Net::HTTP#verify_callback=]:
+ # Sets the callback for the server certification verification.
+ # - {:verify_depth}[rdoc-ref:Gem::Net::HTTP#verify_depth]:
+ # Returns the maximum depth for the certificate chain verification.
+ # - {:verify_depth=}[rdoc-ref:Gem::Net::HTTP#verify_depth=]:
+ # Sets the maximum depth for the certificate chain verification.
+ # - {:verify_hostname}[rdoc-ref:Gem::Net::HTTP#verify_hostname]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_hostname=}[rdoc-ref:Gem::Net::HTTP#verify_hostname=]:
+ # Sets he flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode}[rdoc-ref:Gem::Net::HTTP#verify_mode]:
+ # Returns the flags for server the certification verification at the beginning of the SSL/TLS session.
+ # - {:verify_mode=}[rdoc-ref:Gem::Net::HTTP#verify_mode=]:
+ # Sets the flags for server the certification verification at the beginning of the SSL/TLS session.
+ #
+ # === Addresses and Ports
+ #
+ # - {:address}[rdoc-ref:Gem::Net::HTTP#address]:
+ # Returns the string host name or host IP.
+ # - {::default_port}[rdoc-ref:Gem::Net::HTTP.default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::http_default_port}[rdoc-ref:Gem::Net::HTTP.http_default_port]:
+ # Returns integer 80, the default port to use for HTTP requests.
+ # - {::https_default_port}[rdoc-ref:Gem::Net::HTTP.https_default_port]:
+ # Returns integer 443, the default port to use for HTTPS requests.
+ # - {#ipaddr}[rdoc-ref:Gem::Net::HTTP#ipaddr]:
+ # Returns the IP address for the connection.
+ # - {#ipaddr=}[rdoc-ref:Gem::Net::HTTP#ipaddr=]:
+ # Sets the IP address for the connection.
+ # - {:local_host}[rdoc-ref:Gem::Net::HTTP#local_host]:
+ # Returns the string local host used to establish the connection.
+ # - {:local_host=}[rdoc-ref:Gem::Net::HTTP#local_host=]:
+ # Sets the string local host used to establish the connection.
+ # - {:local_port}[rdoc-ref:Gem::Net::HTTP#local_port]:
+ # Returns the integer local port used to establish the connection.
+ # - {:local_port=}[rdoc-ref:Gem::Net::HTTP#local_port=]:
+ # Sets the integer local port used to establish the connection.
+ # - {:port}[rdoc-ref:Gem::Net::HTTP#port]:
+ # Returns the integer port number.
+ #
+ # === \HTTP Version
+ #
+ # - {::version_1_2?}[rdoc-ref:Gem::Net::HTTP.version_1_2?]
+ # (aliased as {::version_1_2}[rdoc-ref:Gem::Net::HTTP.version_1_2]):
+ # Returns true; retained for compatibility.
+ #
+ # === Debugging
+ #
+ # - {#set_debug_output}[rdoc-ref:Gem::Net::HTTP#set_debug_output]:
+ # Sets the output stream for debugging.
+ #
+ class HTTP < Protocol
+
+ # :stopdoc:
+ VERSION = "0.9.1"
+ HTTPVersion = '1.1'
+ begin
+ require 'zlib'
+ HAVE_ZLIB=true
+ rescue LoadError
+ HAVE_ZLIB=false
+ end
+ # :startdoc:
+
+ # Returns +true+; retained for compatibility.
+ def HTTP.version_1_2
+ true
+ end
+
+ # Returns +true+; retained for compatibility.
+ def HTTP.version_1_2?
+ true
+ end
+
+ # Returns +false+; retained for compatibility.
+ def HTTP.version_1_1? #:nodoc:
+ false
+ end
+
+ class << HTTP
+ alias is_version_1_1? version_1_1? #:nodoc:
+ alias is_version_1_2? version_1_2? #:nodoc:
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get_print(hostname, path, port = 80) -> nil
+ # Gem::Net::HTTP:get_print(uri, headers = {}, port = uri.port) -> nil
+ #
+ # Like Gem::Net::HTTP.get, but writes the returned body to $stdout;
+ # returns +nil+.
+ def HTTP.get_print(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port) {|res|
+ res.read_body do |chunk|
+ $stdout.print chunk
+ end
+ }
+ nil
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get(hostname, path, port = 80) -> body
+ # Gem::Net::HTTP:get(uri, headers = {}, port = uri.port) -> body
+ #
+ # Sends a GET request and returns the \HTTP response body as a string.
+ #
+ # With string arguments +hostname+ and +path+:
+ #
+ # hostname = 'jsonplaceholder.typicode.com'
+ # path = '/todos/1'
+ # puts Gem::Net::HTTP.get(hostname, path)
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ #
+ # With Gem::URI object +uri+ and optional hash argument +headers+:
+ #
+ # uri = Gem::URI('https://jsonplaceholder.typicode.com/todos/1')
+ # headers = {'Content-type' => 'application/json; charset=UTF-8'}
+ # Gem::Net::HTTP.get(uri, headers)
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Get: request class for \HTTP method +GET+.
+ # - Gem::Net::HTTP#get: convenience method for \HTTP method +GET+.
+ #
+ def HTTP.get(uri_or_host, path_or_headers = nil, port = nil)
+ get_response(uri_or_host, path_or_headers, port).body
+ end
+
+ # :call-seq:
+ # Gem::Net::HTTP.get_response(hostname, path, port = 80) -> http_response
+ # Gem::Net::HTTP:get_response(uri, headers = {}, port = uri.port) -> http_response
+ #
+ # Like Gem::Net::HTTP.get, but returns a Gem::Net::HTTPResponse object
+ # instead of the body string.
+ def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block)
+ if path_or_headers && !path_or_headers.is_a?(Hash)
+ host = uri_or_host
+ path = path_or_headers
+ new(host, port || HTTP.default_port).start {|http|
+ return http.request_get(path, &block)
+ }
+ else
+ uri = uri_or_host
+ headers = path_or_headers
+ start(uri.hostname, uri.port,
+ :use_ssl => uri.scheme == 'https') {|http|
+ return http.request_get(uri, headers, &block)
+ }
+ end
+ end
+
+ # Posts data to a host; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Gem::Net::HTTP.post(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Post: request class for \HTTP method +POST+.
+ # - Gem::Net::HTTP#post: convenience method for \HTTP method +POST+.
+ #
+ def HTTP.post(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.post(url, data, header)
+ }
+ end
+
+ # Posts data to a host; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a Gem::URI;
+ # argument +data+ must be a hash:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = {title: 'foo', body: 'bar', userId: 1}
+ # res = Gem::Net::HTTP.post_form(_uri, data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": "1",
+ # "id": 101
+ # }
+ #
+ def HTTP.post_form(url, params)
+ req = Post.new(url)
+ req.form_data = params
+ req.basic_auth url.user, url.password if url.user
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.request(req)
+ }
+ end
+
+ # Sends a PUT request to the server; returns a Gem::Net::HTTPResponse object.
+ #
+ # Argument +url+ must be a URL;
+ # argument +data+ must be a string:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # data = '{"title": "foo", "body": "bar", "userId": 1}'
+ # headers = {'content-type': 'application/json'}
+ # res = Gem::Net::HTTP.put(_uri, data, headers) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # puts res.body
+ #
+ # Output:
+ #
+ # {
+ # "title": "foo",
+ # "body": "bar",
+ # "userId": 1,
+ # "id": 101
+ # }
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Put: request class for \HTTP method +PUT+.
+ # - Gem::Net::HTTP#put: convenience method for \HTTP method +PUT+.
+ #
+ def HTTP.put(url, data, header = nil)
+ start(url.hostname, url.port,
+ :use_ssl => url.scheme == 'https' ) {|http|
+ http.put(url, data, header)
+ }
+ end
+
+ #
+ # \HTTP session management
+ #
+
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Gem::Net::HTTP.default_port # => 80
+ #
+ def HTTP.default_port
+ http_default_port()
+ end
+
+ # Returns integer +80+, the default port to use for \HTTP requests:
+ #
+ # Gem::Net::HTTP.http_default_port # => 80
+ #
+ def HTTP.http_default_port
+ 80
+ end
+
+ # Returns integer +443+, the default port to use for HTTPS requests:
+ #
+ # Gem::Net::HTTP.https_default_port # => 443
+ #
+ def HTTP.https_default_port
+ 443
+ end
+
+ def HTTP.socket_type #:nodoc: obsolete
+ BufferedIO
+ end
+
+ # :call-seq:
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) -> http
+ # HTTP.start(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, opts) {|http| ... } -> object
+ #
+ # Creates a new \Gem::Net::HTTP object, +http+, via \Gem::Net::HTTP.new:
+ #
+ # - For arguments +address+ and +port+, see Gem::Net::HTTP.new.
+ # - For proxy-defining arguments +p_addr+ through +p_pass+,
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ # - For argument +opts+, see below.
+ #
+ # With no block given:
+ #
+ # - Calls <tt>http.start</tt> with no block (see #start),
+ # which opens a TCP connection and \HTTP session.
+ # - Returns +http+.
+ # - The caller should call #finish to close the session:
+ #
+ # http = Gem::Net::HTTP.start(hostname)
+ # http.started? # => true
+ # http.finish
+ # http.started? # => false
+ #
+ # With a block given:
+ #
+ # - Calls <tt>http.start</tt> with the block (see #start), which:
+ #
+ # - Opens a TCP connection and \HTTP session.
+ # - Calls the block,
+ # which may make any number of requests to the host.
+ # - Closes the \HTTP session and TCP connection on block exit.
+ # - Returns the block's value +object+.
+ #
+ # - Returns +object+.
+ #
+ # Example:
+ #
+ # hostname = 'jsonplaceholder.typicode.com'
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # puts http.get('/todos/1').body
+ # puts http.get('/todos/2').body
+ # end
+ #
+ # Output:
+ #
+ # {
+ # "userId": 1,
+ # "id": 1,
+ # "title": "delectus aut autem",
+ # "completed": false
+ # }
+ # {
+ # "userId": 1,
+ # "id": 2,
+ # "title": "quis ut nam facilis et officia qui",
+ # "completed": false
+ # }
+ #
+ # If the last argument given is a hash, it is the +opts+ hash,
+ # where each key is a method or accessor to be called,
+ # and its value is the value to be set.
+ #
+ # The keys may include:
+ #
+ # - #ca_file
+ # - #ca_path
+ # - #cert
+ # - #cert_store
+ # - #ciphers
+ # - #close_on_empty_response
+ # - +ipaddr+ (calls #ipaddr=)
+ # - #keep_alive_timeout
+ # - #key
+ # - #open_timeout
+ # - #read_timeout
+ # - #ssl_timeout
+ # - #ssl_version
+ # - +use_ssl+ (calls #use_ssl=)
+ # - #verify_callback
+ # - #verify_depth
+ # - #verify_mode
+ # - #write_timeout
+ #
+ # Note: If +port+ is +nil+ and <tt>opts[:use_ssl]</tt> is a truthy value,
+ # the value passed to +new+ is Gem::Net::HTTP.https_default_port, not +port+.
+ #
+ def HTTP.start(address, *arg, &block) # :yield: +http+
+ arg.pop if opt = Hash.try_convert(arg[-1])
+ port, p_addr, p_port, p_user, p_pass = *arg
+ p_addr = :ENV if arg.size < 2
+ port = https_default_port if !port && opt && opt[:use_ssl]
+ http = new(address, port, p_addr, p_port, p_user, p_pass)
+ http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr]
+
+ if opt
+ if opt[:use_ssl]
+ opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt)
+ end
+ http.methods.grep(/\A(\w+)=\z/) do |meth|
+ key = $1.to_sym
+ opt.key?(key) or next
+ http.__send__(meth, opt[key])
+ end
+ end
+
+ http.start(&block)
+ end
+
+ class << HTTP
+ alias newobj new # :nodoc:
+ end
+
+ # Returns a new \Gem::Net::HTTP object +http+
+ # (but does not open a TCP connection or \HTTP session).
+ #
+ # With only string argument +address+ given
+ # (and <tt>ENV['http_proxy']</tt> undefined or +nil+),
+ # the returned +http+:
+ #
+ # - Has the given address.
+ # - Has the default port number, Gem::Net::HTTP.default_port (80).
+ # - Has no proxy.
+ #
+ # Example:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.address # => "jsonplaceholder.typicode.com"
+ # http.port # => 80
+ # http.proxy? # => false
+ #
+ # With integer argument +port+ also given,
+ # the returned +http+ has the given port:
+ #
+ # http = Gem::Net::HTTP.new(hostname, 8000)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:8000 open=false>
+ # http.port # => 8000
+ #
+ # For proxy-defining arguments +p_addr+ through +p_no_proxy+,
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ #
+ def HTTP.new(address, port = nil, p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_no_proxy = nil, p_use_ssl = nil)
+ http = super address, port
+
+ if proxy_class? then # from Gem::Net::HTTP::Proxy()
+ http.proxy_from_env = @proxy_from_env
+ http.proxy_address = @proxy_address
+ http.proxy_port = @proxy_port
+ http.proxy_user = @proxy_user
+ http.proxy_pass = @proxy_pass
+ http.proxy_use_ssl = @proxy_use_ssl
+ elsif p_addr == :ENV then
+ http.proxy_from_env = true
+ else
+ if p_addr && p_no_proxy && !Gem::URI::Generic.use_proxy?(address, address, port, p_no_proxy)
+ p_addr = nil
+ p_port = nil
+ end
+ http.proxy_address = p_addr
+ http.proxy_port = p_port || default_port
+ http.proxy_user = p_user
+ http.proxy_pass = p_pass
+ http.proxy_use_ssl = p_use_ssl
+ end
+
+ http
+ end
+
+ class << HTTP
+ # Allows to set the default configuration that will be used
+ # when creating a new connection.
+ #
+ # Example:
+ #
+ # Gem::Net::HTTP.default_configuration = {
+ # read_timeout: 1,
+ # write_timeout: 1
+ # }
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.open_timeout # => 60
+ # http.read_timeout # => 1
+ # http.write_timeout # => 1
+ #
+ attr_accessor :default_configuration
+ end
+
+ # Creates a new \Gem::Net::HTTP object for the specified server address,
+ # without opening the TCP connection or initializing the \HTTP session.
+ # The +address+ should be a DNS hostname or IP address.
+ def initialize(address, port = nil) # :nodoc:
+ defaults = {
+ keep_alive_timeout: 2,
+ close_on_empty_response: false,
+ open_timeout: 60,
+ read_timeout: 60,
+ write_timeout: 60,
+ continue_timeout: nil,
+ max_retries: 1,
+ debug_output: nil,
+ response_body_encoding: false,
+ ignore_eof: true
+ }
+ options = defaults.merge(self.class.default_configuration || {})
+
+ @address = address
+ @port = (port || HTTP.default_port)
+ @ipaddr = nil
+ @local_host = nil
+ @local_port = nil
+ @curr_http_version = HTTPVersion
+ @keep_alive_timeout = options[:keep_alive_timeout]
+ @last_communicated = nil
+ @close_on_empty_response = options[:close_on_empty_response]
+ @socket = nil
+ @started = false
+ @open_timeout = options[:open_timeout]
+ @read_timeout = options[:read_timeout]
+ @write_timeout = options[:write_timeout]
+ @continue_timeout = options[:continue_timeout]
+ @max_retries = options[:max_retries]
+ @debug_output = options[:debug_output]
+ @response_body_encoding = options[:response_body_encoding]
+ @ignore_eof = options[:ignore_eof]
+ @tcpsocket_supports_open_timeout = nil
+
+ @proxy_from_env = false
+ @proxy_uri = nil
+ @proxy_address = nil
+ @proxy_port = nil
+ @proxy_user = nil
+ @proxy_pass = nil
+ @proxy_use_ssl = nil
+
+ @use_ssl = false
+ @ssl_context = nil
+ @ssl_session = nil
+ @sspi_enabled = false
+ SSL_IVNAMES.each do |ivname|
+ instance_variable_set ivname, nil
+ end
+ end
+
+ # Returns a string representation of +self+:
+ #
+ # Gem::Net::HTTP.new(hostname).inspect
+ # # => "#<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>"
+ #
+ def inspect
+ "#<#{self.class} #{@address}:#{@port} open=#{started?}>"
+ end
+
+ # *WARNING* This method opens a serious security hole.
+ # Never use this method in production code.
+ #
+ # Sets the output stream for debugging:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # File.open('t.tmp', 'w') do |file|
+ # http.set_debug_output(file)
+ # http.start
+ # http.get('/nosuch/1')
+ # http.finish
+ # end
+ # puts File.read('t.tmp')
+ #
+ # Output:
+ #
+ # opening connection to jsonplaceholder.typicode.com:80...
+ # opened
+ # <- "GET /nosuch/1 HTTP/1.1\r\nAccept-Encoding: gzip;q=1.0,deflate;q=0.6,identity;q=0.3\r\nAccept: */*\r\nUser-Agent: Ruby\r\nHost: jsonplaceholder.typicode.com\r\n\r\n"
+ # -> "HTTP/1.1 404 Not Found\r\n"
+ # -> "Date: Mon, 12 Dec 2022 21:14:11 GMT\r\n"
+ # -> "Content-Type: application/json; charset=utf-8\r\n"
+ # -> "Content-Length: 2\r\n"
+ # -> "Connection: keep-alive\r\n"
+ # -> "X-Powered-By: Express\r\n"
+ # -> "X-Ratelimit-Limit: 1000\r\n"
+ # -> "X-Ratelimit-Remaining: 999\r\n"
+ # -> "X-Ratelimit-Reset: 1670879660\r\n"
+ # -> "Vary: Origin, Accept-Encoding\r\n"
+ # -> "Access-Control-Allow-Credentials: true\r\n"
+ # -> "Cache-Control: max-age=43200\r\n"
+ # -> "Pragma: no-cache\r\n"
+ # -> "Expires: -1\r\n"
+ # -> "X-Content-Type-Options: nosniff\r\n"
+ # -> "Etag: W/\"2-vyGp6PvFo4RvsFtPoIWeCReyIC8\"\r\n"
+ # -> "Via: 1.1 vegur\r\n"
+ # -> "CF-Cache-Status: MISS\r\n"
+ # -> "Server-Timing: cf-q-config;dur=1.3000000762986e-05\r\n"
+ # -> "Report-To: {\"endpoints\":[{\"url\":\"https:\\/\\/a.nel.cloudflare.com\\/report\\/v3?s=yOr40jo%2BwS1KHzhTlVpl54beJ5Wx2FcG4gGV0XVrh3X9OlR5q4drUn2dkt5DGO4GDcE%2BVXT7CNgJvGs%2BZleIyMu8CLieFiDIvOviOY3EhHg94m0ZNZgrEdpKD0S85S507l1vsEwEHkoTm%2Ff19SiO\"}],\"group\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "NEL: {\"success_fraction\":0,\"report_to\":\"cf-nel\",\"max_age\":604800}\r\n"
+ # -> "Server: cloudflare\r\n"
+ # -> "CF-RAY: 778977dc484ce591-DFW\r\n"
+ # -> "alt-svc: h3=\":443\"; ma=86400, h3-29=\":443\"; ma=86400\r\n"
+ # -> "\r\n"
+ # reading 2 bytes...
+ # -> "{}"
+ # read 2 bytes
+ # Conn keep-alive
+ #
+ def set_debug_output(output)
+ warn 'Gem::Net::HTTP#set_debug_output called after HTTP started', uplevel: 1 if started?
+ @debug_output = output
+ end
+
+ # Returns the string host name or host IP given as argument +address+ in ::new.
+ attr_reader :address
+
+ # Returns the integer port number given as argument +port+ in ::new.
+ attr_reader :port
+
+ # Sets or returns the string local host used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_host
+
+ # Sets or returns the integer local port used to establish the connection;
+ # initially +nil+.
+ attr_accessor :local_port
+
+ # Returns the encoding to use for the response body;
+ # see #response_body_encoding=.
+ attr_reader :response_body_encoding
+
+ # Sets the encoding to be used for the response body;
+ # returns the encoding.
+ #
+ # The given +value+ may be:
+ #
+ # - An Encoding object.
+ # - The name of an encoding.
+ # - An alias for an encoding name.
+ #
+ # See {Encoding}[https://docs.ruby-lang.org/en/master/Encoding.html].
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.response_body_encoding = Encoding::US_ASCII # => #<Encoding:US-ASCII>
+ # http.response_body_encoding = 'US-ASCII' # => "US-ASCII"
+ # http.response_body_encoding = 'ASCII' # => "ASCII"
+ #
+ def response_body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @response_body_encoding = value
+ end
+
+ # Sets whether to determine the proxy from environment variable
+ # '<tt>ENV['http_proxy']</tt>';
+ # see {Proxy Using ENV['http_proxy']}[rdoc-ref:Gem::Net::HTTP@Proxy+Using+-27ENV-5B-27http_proxy-27-5D-27].
+ attr_writer :proxy_from_env
+
+ # Sets the proxy address;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_address
+
+ # Sets the proxy port;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_port
+
+ # Sets the proxy user;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_user
+
+ # Sets the proxy password;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_pass
+
+ # Sets whether the proxy uses SSL;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ attr_writer :proxy_use_ssl
+
+ # Returns the IP address for the connection.
+ #
+ # If the session has not been started,
+ # returns the value set by #ipaddr=,
+ # or +nil+ if it has not been set:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # If the session has been started,
+ # returns the IP address from the socket:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.ipaddr # => "172.67.155.76"
+ # http.finish
+ #
+ def ipaddr
+ started? ? @socket.io.peeraddr[3] : @ipaddr
+ end
+
+ # Sets the IP address for the connection:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.ipaddr # => nil
+ # http.ipaddr = '172.67.155.76'
+ # http.ipaddr # => "172.67.155.76"
+ #
+ # The IP address may not be set if the session has been started.
+ def ipaddr=(addr)
+ raise IOError, "ipaddr value changed, but session already started" if started?
+ @ipaddr = addr
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to wait for a connection to open;
+ # initially 60.
+ # If the connection is not made in the given interval,
+ # an exception is raised.
+ attr_accessor :open_timeout
+
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be read (via one read(2) call);
+ # see #read_timeout=.
+ attr_reader :read_timeout
+
+ # Returns the numeric (\Integer or \Float) number of seconds
+ # to wait for one block to be written (via one write(2) call);
+ # see #write_timeout=.
+ attr_reader :write_timeout
+
+ # Sets the maximum number of times to retry an idempotent request in case of
+ # \Gem::Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET,
+ # Errno::ECONNABORTED, Errno::EPIPE, OpenSSL::SSL::SSLError,
+ # Gem::Timeout::Error.
+ # The initial value is 1.
+ #
+ # Argument +retries+ must be a non-negative numeric value:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.max_retries = 2 # => 2
+ # http.max_retries # => 2
+ #
+ def max_retries=(retries)
+ retries = retries.to_int
+ if retries < 0
+ raise ArgumentError, 'max_retries should be non-negative integer number'
+ end
+ @max_retries = retries
+ end
+
+ # Returns the maximum number of times to retry an idempotent request;
+ # see #max_retries=.
+ attr_reader :max_retries
+
+ # Sets the read timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.read_timeout # => 60
+ # http.get('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # http.read_timeout = 0
+ # http.get('/todos/1') # Raises Gem::Net::ReadTimeout.
+ #
+ def read_timeout=(sec)
+ @socket.read_timeout = sec if @socket
+ @read_timeout = sec
+ end
+
+ # Sets the write timeout, in seconds, for +self+ to integer +sec+;
+ # the initial value is 60.
+ #
+ # Argument +sec+ must be a non-negative numeric value:
+ #
+ # _uri = uri.dup
+ # _uri.path = '/posts'
+ # body = 'bar' * 200000
+ # data = <<EOF
+ # {"title": "foo", "body": "#{body}", "userId": "1"}
+ # EOF
+ # headers = {'content-type': 'application/json'}
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.write_timeout # => 60
+ # http.post(_uri.path, data, headers)
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ # http.write_timeout = 0
+ # http.post(_uri.path, data, headers) # Raises Gem::Net::WriteTimeout.
+ #
+ def write_timeout=(sec)
+ @socket.write_timeout = sec if @socket
+ @write_timeout = sec
+ end
+
+ # Returns the continue timeout value;
+ # see continue_timeout=.
+ attr_reader :continue_timeout
+
+ # Sets the continue timeout value,
+ # which is the number of seconds to wait for an expected 100 Continue response.
+ # If the \HTTP object does not receive a response in this many seconds
+ # it sends the request body.
+ def continue_timeout=(sec)
+ @socket.continue_timeout = sec if @socket
+ @continue_timeout = sec
+ end
+
+ # Sets or returns the numeric (\Integer or \Float) number of seconds
+ # to keep the connection open after a request is sent;
+ # initially 2.
+ # If a new request is made during the given interval,
+ # the still-open connection is used;
+ # otherwise the connection will have been closed
+ # and a new connection is opened.
+ attr_accessor :keep_alive_timeout
+
+ # Sets or returns whether to ignore end-of-file when reading a response body
+ # with <tt>Content-Length</tt> headers;
+ # initially +true+.
+ attr_accessor :ignore_eof
+
+ # Returns +true+ if the \HTTP session has been started:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.started? # => false
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # http.started?
+ # end # => true
+ # http.started? # => false
+ #
+ def started?
+ @started
+ end
+
+ alias active? started? #:nodoc: obsolete
+
+ # Sets or returns whether to close the connection when the response is empty;
+ # initially +false+.
+ attr_accessor :close_on_empty_response
+
+ # Returns +true+ if +self+ uses SSL, +false+ otherwise.
+ # See Gem::Net::HTTP#use_ssl=.
+ def use_ssl?
+ @use_ssl
+ end
+
+ # Sets whether a new session is to use
+ # {Transport Layer Security}[https://en.wikipedia.org/wiki/Transport_Layer_Security]:
+ #
+ # Raises IOError if attempting to change during a session.
+ #
+ # Raises OpenSSL::SSL::SSLError if the port is not an HTTPS port.
+ def use_ssl=(flag)
+ flag = flag ? true : false
+ if started? and @use_ssl != flag
+ raise IOError, "use_ssl value changed, but session already started"
+ end
+ @use_ssl = flag
+ end
+
+ SSL_ATTRIBUTES = [
+ :ca_file,
+ :ca_path,
+ :cert,
+ :cert_store,
+ :ciphers,
+ :extra_chain_cert,
+ :key,
+ :ssl_timeout,
+ :ssl_version,
+ :min_version,
+ :max_version,
+ :verify_callback,
+ :verify_depth,
+ :verify_mode,
+ :verify_hostname,
+ ].freeze # :nodoc:
+
+ SSL_IVNAMES = SSL_ATTRIBUTES.map { |a| "@#{a}".to_sym }.freeze # :nodoc:
+
+ # Sets or returns the path to a CA certification file in PEM format.
+ attr_accessor :ca_file
+
+ # Sets or returns the path of to CA directory
+ # containing certification files in PEM format.
+ attr_accessor :ca_path
+
+ # Sets or returns the OpenSSL::X509::Certificate object
+ # to be used for client certification.
+ attr_accessor :cert
+
+ # Sets or returns the X509::Store to be used for verifying peer certificate.
+ attr_accessor :cert_store
+
+ # Sets or returns the available SSL ciphers.
+ # See {OpenSSL::SSL::SSLContext#ciphers=}[OpenSSL::SSL::SSL::Context#ciphers=].
+ attr_accessor :ciphers
+
+ # Sets or returns the extra X509 certificates to be added to the certificate chain.
+ # See {OpenSSL::SSL::SSLContext#add_certificate}[OpenSSL::SSL::SSL::Context#add_certificate].
+ attr_accessor :extra_chain_cert
+
+ # Sets or returns the OpenSSL::PKey::RSA or OpenSSL::PKey::DSA object.
+ attr_accessor :key
+
+ # Sets or returns the SSL timeout seconds.
+ attr_accessor :ssl_timeout
+
+ # Sets or returns the SSL version.
+ # See {OpenSSL::SSL::SSLContext#ssl_version=}[OpenSSL::SSL::SSL::Context#ssl_version=].
+ attr_accessor :ssl_version
+
+ # Sets or returns the minimum SSL version.
+ # See {OpenSSL::SSL::SSLContext#min_version=}[OpenSSL::SSL::SSL::Context#min_version=].
+ attr_accessor :min_version
+
+ # Sets or returns the maximum SSL version.
+ # See {OpenSSL::SSL::SSLContext#max_version=}[OpenSSL::SSL::SSL::Context#max_version=].
+ attr_accessor :max_version
+
+ # Sets or returns the callback for the server certification verification.
+ attr_accessor :verify_callback
+
+ # Sets or returns the maximum depth for the certificate chain verification.
+ attr_accessor :verify_depth
+
+ # Sets or returns the flags for server the certification verification
+ # at the beginning of the SSL/TLS session.
+ # OpenSSL::SSL::VERIFY_NONE or OpenSSL::SSL::VERIFY_PEER are acceptable.
+ attr_accessor :verify_mode
+
+ # Sets or returns whether to verify that the server certificate is valid
+ # for the hostname.
+ # See {OpenSSL::SSL::SSLContext#verify_hostname=}[OpenSSL::SSL::SSL::Context#verify_hostname=].
+ attr_accessor :verify_hostname
+
+ # Returns the X509 certificate chain (an array of strings)
+ # for the session's socket peer,
+ # or +nil+ if none.
+ def peer_cert
+ if not use_ssl? or not @socket
+ return nil
+ end
+ @socket.io.peer_cert
+ end
+
+ # Starts an \HTTP session.
+ #
+ # Without a block, returns +self+:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.start
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=true>
+ # http.started? # => true
+ # http.finish
+ #
+ # With a block, calls the block with +self+,
+ # finishes the session when the block exits,
+ # and returns the block's value:
+ #
+ # http.start do |http|
+ # http
+ # end
+ # # => #<Gem::Net::HTTP jsonplaceholder.typicode.com:80 open=false>
+ # http.started? # => false
+ #
+ def start # :yield: http
+ raise IOError, 'HTTP session already opened' if @started
+ if block_given?
+ begin
+ do_start
+ return yield(self)
+ ensure
+ do_finish
+ end
+ end
+ do_start
+ self
+ end
+
+ # Finishes the \HTTP session:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.start
+ # http.started? # => true
+ # http.finish # => nil
+ # http.started? # => false
+ #
+ # Raises IOError if not in a session.
+ def finish
+ raise IOError, 'HTTP session not yet started' unless started?
+ do_finish
+ end
+
+ # :stopdoc:
+ def do_start
+ connect
+ @started = true
+ end
+ private :do_start
+
+ def connect
+ if use_ssl?
+ # reference early to load OpenSSL before connecting,
+ # as OpenSSL may take time to load.
+ @ssl_context = OpenSSL::SSL::SSLContext.new
+ end
+
+ if proxy? then
+ conn_addr = proxy_address
+ conn_port = proxy_port
+ else
+ conn_addr = conn_address
+ conn_port = port
+ end
+
+ debug "opening connection to #{conn_addr}:#{conn_port}..."
+ begin
+ s = timeouted_connect(conn_addr, conn_port)
+ rescue => e
+ if (defined?(IO::TimeoutError) && e.is_a?(IO::TimeoutError)) || e.is_a?(Errno::ETIMEDOUT) # for compatibility with previous versions
+ e = Gem::Net::OpenTimeout.new(e)
+ end
+ raise e, "Failed to open TCP connection to " +
+ "#{conn_addr}:#{conn_port} (#{e.message})"
+ end
+ s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
+ debug "opened"
+ if use_ssl?
+ if proxy?
+ if @proxy_use_ssl
+ proxy_sock = OpenSSL::SSL::SSLSocket.new(s)
+ ssl_socket_connect(proxy_sock, @open_timeout)
+ else
+ proxy_sock = s
+ end
+ proxy_sock = BufferedIO.new(proxy_sock, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ buf = +"CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" \
+ "Host: #{@address}:#{@port}\r\n"
+ if proxy_user
+ credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0')
+ buf << "Proxy-Authorization: Basic #{credential}\r\n"
+ end
+ buf << "\r\n"
+ proxy_sock.write(buf)
+ HTTPResponse.read_new(proxy_sock).value
+ # assuming nothing left in buffers after successful CONNECT response
+ end
+
+ ssl_parameters = Hash.new
+ iv_list = instance_variables
+ SSL_IVNAMES.each_with_index do |ivname, i|
+ if iv_list.include?(ivname)
+ value = instance_variable_get(ivname)
+ unless value.nil?
+ ssl_parameters[SSL_ATTRIBUTES[i]] = value
+ end
+ end
+ end
+ @ssl_context.set_params(ssl_parameters)
+ unless @ssl_context.session_cache_mode.nil? # a dummy method on JRuby
+ @ssl_context.session_cache_mode =
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT |
+ OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE
+ end
+ if @ssl_context.respond_to?(:session_new_cb) # not implemented under JRuby
+ @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess }
+ end
+
+ # Still do the post_connection_check below even if connecting
+ # to IP address
+ verify_hostname = @ssl_context.verify_hostname
+
+ # Server Name Indication (SNI) RFC 3546/6066
+ case @address
+ when Gem::Resolv::IPv4::Regex, Gem::Resolv::IPv6::Regex
+ # don't set SNI, as IP addresses in SNI is not valid
+ # per RFC 6066, section 3.
+
+ # Avoid openssl warning
+ @ssl_context.verify_hostname = false
+ else
+ ssl_host_address = @address
+ end
+
+ debug "starting SSL for #{conn_addr}:#{conn_port}..."
+ s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
+ s.sync_close = true
+ s.hostname = ssl_host_address if s.respond_to?(:hostname=) && ssl_host_address
+
+ if @ssl_session and
+ Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout
+ s.session = @ssl_session
+ end
+ ssl_socket_connect(s, @open_timeout)
+ if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && verify_hostname
+ s.post_connection_check(@address)
+ end
+ debug "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}"
+ end
+ @socket = BufferedIO.new(s, read_timeout: @read_timeout,
+ write_timeout: @write_timeout,
+ continue_timeout: @continue_timeout,
+ debug_output: @debug_output)
+ @last_communicated = nil
+ on_connect
+ rescue => exception
+ if s
+ debug "Conn close because of connect error #{exception}"
+ s.close
+ end
+ raise
+ end
+ private :connect
+
+ tcp_socket_parameters = TCPSocket.instance_method(:initialize).parameters
+ TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT = if tcp_socket_parameters != [[:rest]]
+ tcp_socket_parameters.include?([:key, :open_timeout])
+ else
+ # Use Socket.tcp to find out since there is no parameters information for TCPSocket#initialize
+ # See discussion in https://github.com/ruby/net-http/pull/224
+ Socket.method(:tcp).parameters.include?([:key, :open_timeout])
+ end
+ private_constant :TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
+
+ def timeouted_connect(conn_addr, conn_port)
+ if TCP_SOCKET_NEW_HAS_OPEN_TIMEOUT
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port, open_timeout: @open_timeout)
+ else
+ Gem::Timeout.timeout(@open_timeout, Gem::Net::OpenTimeout) {
+ TCPSocket.open(conn_addr, conn_port, @local_host, @local_port)
+ }
+ end
+ end
+ private :timeouted_connect
+
+ def on_connect
+ end
+ private :on_connect
+
+ def do_finish
+ @started = false
+ @socket.close if @socket
+ @socket = nil
+ end
+ private :do_finish
+
+ #
+ # proxy
+ #
+
+ public
+
+ # no proxy
+ @is_proxy_class = false
+ @proxy_from_env = false
+ @proxy_addr = nil
+ @proxy_port = nil
+ @proxy_user = nil
+ @proxy_pass = nil
+ @proxy_use_ssl = nil
+
+ # Creates an \HTTP proxy class which behaves like \Gem::Net::HTTP, but
+ # performs all access via the specified proxy.
+ #
+ # This class is obsolete. You may pass these same parameters directly to
+ # \Gem::Net::HTTP.new. See Gem::Net::HTTP.new for details of the arguments.
+ def HTTP.Proxy(p_addr = :ENV, p_port = nil, p_user = nil, p_pass = nil, p_use_ssl = nil) #:nodoc:
+ return self unless p_addr
+
+ Class.new(self) {
+ @is_proxy_class = true
+
+ if p_addr == :ENV then
+ @proxy_from_env = true
+ @proxy_address = nil
+ @proxy_port = nil
+ else
+ @proxy_from_env = false
+ @proxy_address = p_addr
+ @proxy_port = p_port || default_port
+ end
+
+ @proxy_user = p_user
+ @proxy_pass = p_pass
+ @proxy_use_ssl = p_use_ssl
+ }
+ end
+
+ # :startdoc:
+
+ class << HTTP
+ # Returns true if self is a class which was created by HTTP::Proxy.
+ def proxy_class?
+ defined?(@is_proxy_class) ? @is_proxy_class : false
+ end
+
+ # Returns the address of the proxy host, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_address
+
+ # Returns the port number of the proxy host, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_port
+
+ # Returns the user name for accessing the proxy, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_user
+
+ # Returns the password for accessing the proxy, or +nil+ if none;
+ # see Gem::Net::HTTP@Proxy+Server.
+ attr_reader :proxy_pass
+
+ # Use SSL when talking to the proxy. If Gem::Net::HTTP does not use a proxy, nil.
+ attr_reader :proxy_use_ssl
+ end
+
+ # Returns +true+ if a proxy server is defined, +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy?
+ !!(@proxy_from_env ? proxy_uri : @proxy_address)
+ end
+
+ # Returns +true+ if the proxy server is defined in the environment,
+ # +false+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_from_env?
+ @proxy_from_env
+ end
+
+ # The proxy Gem::URI determined from the environment for this connection.
+ def proxy_uri # :nodoc:
+ return if @proxy_uri == false
+ @proxy_uri ||= Gem::URI::HTTP.new(
+ "http", nil, address, port, nil, nil, nil, nil, nil
+ ).find_proxy || false
+ @proxy_uri || nil
+ end
+
+ # Returns the address of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_address
+ if @proxy_from_env then
+ proxy_uri&.hostname
+ else
+ @proxy_address
+ end
+ end
+
+ # Returns the port number of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_port
+ if @proxy_from_env then
+ proxy_uri&.port
+ else
+ @proxy_port
+ end
+ end
+
+ # Returns the user name of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_user
+ if @proxy_from_env
+ user = proxy_uri&.user
+ unescape(user) if user
+ else
+ @proxy_user
+ end
+ end
+
+ # Returns the password of the proxy server, if defined, +nil+ otherwise;
+ # see {Proxy Server}[rdoc-ref:Gem::Net::HTTP@Proxy+Server].
+ def proxy_pass
+ if @proxy_from_env
+ pass = proxy_uri&.password
+ unescape(pass) if pass
+ else
+ @proxy_pass
+ end
+ end
+
+ alias proxyaddr proxy_address #:nodoc: obsolete
+ alias proxyport proxy_port #:nodoc: obsolete
+
+ private
+ # :stopdoc:
+
+ def unescape(value)
+ require 'cgi/escape'
+ require 'cgi/util' unless defined?(CGI::EscapeExt)
+ CGI.unescape(value)
+ end
+
+ # without proxy, obsolete
+
+ def conn_address # :nodoc:
+ @ipaddr || address()
+ end
+
+ def conn_port # :nodoc:
+ port()
+ end
+
+ def edit_path(path)
+ if proxy?
+ if path.start_with?("ftp://") || use_ssl?
+ path
+ else
+ "http://#{addr_port}#{path}"
+ end
+ else
+ path
+ end
+ end
+ # :startdoc:
+
+ #
+ # HTTP operations
+ #
+
+ public
+
+ # :call-seq:
+ # get(path, initheader = nil) {|res| ... }
+ #
+ # Sends a GET request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.get('/todos/1') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.get('/') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Get: request class for \HTTP method GET.
+ # - Gem::Net::HTTP.get: sends GET request, returns response body.
+ #
+ def get(path, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ res = nil
+
+ request(Get.new(path, initheader)) {|r|
+ r.read_body dest, &block
+ res = r
+ }
+ res
+ end
+
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+:
+ #
+ # res = http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # res.body # => nil
+ # res.to_hash.take(3)
+ # # =>
+ # [["date", ["Wed, 15 Feb 2023 15:25:42 GMT"]],
+ # ["content-type", ["application/json; charset=utf-8"]],
+ # ["connection", ["close"]]]
+ #
+ def head(path, initheader = nil)
+ request(Head.new(path, initheader))
+ end
+
+ # :call-seq:
+ # post(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a POST request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.post('/todos', data) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\",\n \"id\": 201\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.post('/todos', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Post: request class for \HTTP method POST.
+ # - Gem::Net::HTTP.post: sends POST request, returns response body.
+ #
+ def post(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ send_entity(path, data, initheader, dest, Post, &block)
+ end
+
+ # :call-seq:
+ # patch(path, data, initheader = nil) {|res| ... }
+ #
+ # Sends a PATCH request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Patch object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With a block given, calls the block with the response body:
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.patch('/todos/1', data) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false,\n \"{\\\"userId\\\": 1, \\\"id\\\": 1, \\\"title\\\": \\\"delectus aut autem\\\", \\\"completed\\\": false}\": \"\"\n}"
+ #
+ # With no block given, simply returns the response object:
+ #
+ # http.patch('/todos/1', data) # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def patch(path, data, initheader = nil, dest = nil, &block) # :yield: +body_segment+
+ send_entity(path, data, initheader, dest, Patch, &block)
+ end
+
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.put('/todos/1', data) # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Related:
+ #
+ # - Gem::Net::HTTP::Put: request class for \HTTP method PUT.
+ # - Gem::Net::HTTP.put: sends PUT request, returns response body.
+ #
+ def put(path, data, initheader = nil)
+ request(Put.new(path, initheader), data)
+ end
+
+ # Sends a PROPPATCH request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Proppatch object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.proppatch('/todos/1', data)
+ #
+ def proppatch(path, body, initheader = nil)
+ request(Proppatch.new(path, initheader), body)
+ end
+
+ # Sends a LOCK request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Lock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.lock('/todos/1', data)
+ #
+ def lock(path, body, initheader = nil)
+ request(Lock.new(path, initheader), body)
+ end
+
+ # Sends an UNLOCK request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Unlock object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.unlock('/todos/1', data)
+ #
+ def unlock(path, body, initheader = nil)
+ request(Unlock.new(path, initheader), body)
+ end
+
+ # Sends an Options request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Options object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.options('/')
+ #
+ def options(path, initheader = nil)
+ request(Options.new(path, initheader))
+ end
+
+ # Sends a PROPFIND request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Propfind object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.propfind('/todos/1', data)
+ #
+ def propfind(path, body = nil, initheader = {'Depth' => '0'})
+ request(Propfind.new(path, initheader), body)
+ end
+
+ # Sends a DELETE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Delete object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.delete('/todos/1')
+ #
+ def delete(path, initheader = {'Depth' => 'Infinity'})
+ request(Delete.new(path, initheader))
+ end
+
+ # Sends a MOVE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Move object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.move('/todos/1')
+ #
+ def move(path, initheader = nil)
+ request(Move.new(path, initheader))
+ end
+
+ # Sends a COPY request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Copy object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.copy('/todos/1')
+ #
+ def copy(path, initheader = nil)
+ request(Copy.new(path, initheader))
+ end
+
+ # Sends a MKCOL request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Mkcol object
+ # created from string +path+, string +body+, and initial headers hash +initheader+.
+ #
+ # data = '{"userId": 1, "id": 1, "title": "delectus aut autem", "completed": false}'
+ # http.mkcol('/todos/1', data)
+ # http = Gem::Net::HTTP.new(hostname)
+ #
+ def mkcol(path, body = nil, initheader = nil)
+ request(Mkcol.new(path, initheader), body)
+ end
+
+ # Sends a TRACE request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Trace object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.trace('/todos/1')
+ #
+ def trace(path, initheader = nil)
+ request(Trace.new(path, initheader))
+ end
+
+ # Sends a GET request to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The request is based on the Gem::Net::HTTP::Get object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.request_get('/todos') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # With a block given, calls the block with the response object
+ # and returns the response object:
+ #
+ # http.request_get('/todos') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Gem::Net::HTTPOK 200 OK readbody=false>
+ #
+ def request_get(path, initheader = nil, &block) # :yield: +response+
+ request(Get.new(path, initheader), &block)
+ end
+
+ # Sends a HEAD request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Head object
+ # created from string +path+ and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.head('/todos/1') # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ def request_head(path, initheader = nil, &block)
+ request(Head.new(path, initheader), &block)
+ end
+
+ # Sends a POST request to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The request is based on the Gem::Net::HTTP::Post object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.post('/todos', 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response body
+ # and returns the response object:
+ #
+ # http.post('/todos', 'xyzzy') do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # Output:
+ #
+ # "{\n \"xyzzy\": \"\",\n \"id\": 201\n}"
+ #
+ def request_post(path, data, initheader = nil, &block) # :yield: +response+
+ request Post.new(path, initheader), data, &block
+ end
+
+ # Sends a PUT request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTP::Put object
+ # created from string +path+, string +data+, and initial headers hash +initheader+.
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.put('/todos/1', 'xyzzy')
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ def request_put(path, data, initheader = nil, &block) #:nodoc:
+ request Put.new(path, initheader), data, &block
+ end
+
+ alias get2 request_get #:nodoc: obsolete
+ alias head2 request_head #:nodoc: obsolete
+ alias post2 request_post #:nodoc: obsolete
+ alias put2 request_put #:nodoc: obsolete
+
+ # Sends an \HTTP request to the server;
+ # returns an instance of a subclass of Gem::Net::HTTPResponse.
+ #
+ # The request is based on the Gem::Net::HTTPRequest object
+ # created from string +path+, string +data+, and initial headers hash +header+.
+ # That object is an instance of the
+ # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses],
+ # that corresponds to the given uppercase string +name+,
+ # which must be
+ # an {HTTP request method}[https://en.wikipedia.org/wiki/HTTP#Request_methods]
+ # or a {WebDAV request method}[https://en.wikipedia.org/wiki/WebDAV#Implementation].
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # http.send_request('GET', '/todos/1')
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ # http.send_request('POST', '/todos', 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def send_request(name, path, data = nil, header = nil)
+ has_response_body = name != 'HEAD'
+ r = HTTPGenericRequest.new(name,(data ? true : false),has_response_body,path,header)
+ request r, data
+ end
+
+ # Sends the given request +req+ to the server;
+ # forms the response into a Gem::Net::HTTPResponse object.
+ #
+ # The given +req+ must be an instance of a
+ # {subclass of Gem::Net::HTTPRequest}[rdoc-ref:Gem::Net::HTTPRequest@Request+Subclasses].
+ # Argument +body+ should be given only if needed for the request.
+ #
+ # With no block given, returns the response object:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ #
+ # req = Gem::Net::HTTP::Get.new('/todos/1')
+ # http.request(req)
+ # # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # req = Gem::Net::HTTP::Post.new('/todos')
+ # http.request(req, 'xyzzy')
+ # # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ # With a block given, calls the block with the response and returns the response:
+ #
+ # req = Gem::Net::HTTP::Get.new('/todos/1')
+ # http.request(req) do |res|
+ # p res
+ # end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+ #
+ # Output:
+ #
+ # #<Gem::Net::HTTPOK 200 OK readbody=false>
+ #
+ def request(req, body = nil, &block) # :yield: +response+
+ unless started?
+ start {
+ req['connection'] ||= 'close'
+ return request(req, body, &block)
+ }
+ end
+ if proxy_user()
+ req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl?
+ end
+ req.set_body_internal body
+ res = transport_request(req, &block)
+ if sspi_auth?(res)
+ sspi_auth(req)
+ res = transport_request(req, &block)
+ end
+ res
+ end
+
+ private
+
+ # Executes a request which uses a representation
+ # and returns its body.
+ def send_entity(path, data, initheader, dest, type, &block)
+ res = nil
+ request(type.new(path, initheader), data) {|r|
+ r.read_body dest, &block
+ res = r
+ }
+ res
+ end
+
+ # :stopdoc:
+
+ IDEMPOTENT_METHODS_ = %w/GET HEAD PUT DELETE OPTIONS TRACE/.freeze # :nodoc:
+
+ def transport_request(req)
+ count = 0
+ begin
+ begin_transport req
+ res = catch(:response) {
+ begin
+ req.exec @socket, @curr_http_version, edit_path(req.path)
+ rescue Errno::EPIPE
+ # Failure when writing full request, but we can probably
+ # still read the received response.
+ end
+
+ begin
+ res = HTTPResponse.read_new(@socket)
+ res.decode_content = req.decode_content
+ res.body_encoding = @response_body_encoding
+ res.ignore_eof = @ignore_eof
+ end while res.kind_of?(HTTPInformation)
+
+ res.uri = req.uri
+
+ res
+ }
+ res.reading_body(@socket, req.response_body_permitted?) {
+ if block_given?
+ count = max_retries # Don't restart in the middle of a download
+ yield res
+ end
+ }
+ rescue Gem::Net::OpenTimeout
+ raise
+ rescue Gem::Net::ReadTimeout, IOError, EOFError,
+ Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT,
+ # avoid a dependency on OpenSSL
+ defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError,
+ Gem::Timeout::Error => exception
+ if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method)
+ count += 1
+ @socket.close if @socket
+ debug "Conn close because of error #{exception}, and retry"
+ retry
+ end
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise
+ end
+
+ end_transport req, res
+ res
+ rescue => exception
+ debug "Conn close because of error #{exception}"
+ @socket.close if @socket
+ raise exception
+ end
+
+ def begin_transport(req)
+ if @socket.closed?
+ connect
+ elsif @last_communicated
+ if @last_communicated + @keep_alive_timeout < Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ debug 'Conn close because of keep_alive_timeout'
+ @socket.close
+ connect
+ elsif @socket.io.to_io.wait_readable(0) && @socket.eof?
+ debug "Conn close because of EOF"
+ @socket.close
+ connect
+ end
+ end
+
+ if not req.response_body_permitted? and @close_on_empty_response
+ req['connection'] ||= 'close'
+ end
+
+ req.update_uri address, port, use_ssl?
+ req['host'] ||= addr_port()
+ end
+
+ def end_transport(req, res)
+ @curr_http_version = res.http_version
+ @last_communicated = nil
+ if @socket.closed?
+ debug 'Conn socket closed'
+ elsif not res.body and @close_on_empty_response
+ debug 'Conn close'
+ @socket.close
+ elsif keep_alive?(req, res)
+ debug 'Conn keep-alive'
+ @last_communicated = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ else
+ debug 'Conn close'
+ @socket.close
+ end
+ end
+
+ def keep_alive?(req, res)
+ return false if req.connection_close?
+ if @curr_http_version <= '1.0'
+ res.connection_keep_alive?
+ else # HTTP/1.1 or later
+ not res.connection_close?
+ end
+ end
+
+ def sspi_auth?(res)
+ return false unless @sspi_enabled
+ if res.kind_of?(HTTPProxyAuthenticationRequired) and
+ proxy? and res["Proxy-Authenticate"].include?("Negotiate")
+ begin
+ require 'win32/sspi'
+ true
+ rescue LoadError
+ false
+ end
+ else
+ false
+ end
+ end
+
+ def sspi_auth(req)
+ n = Win32::SSPI::NegotiateAuth.new
+ req["Proxy-Authorization"] = "Negotiate #{n.get_initial_token}"
+ # Some versions of ISA will close the connection if this isn't present.
+ req["Connection"] = "Keep-Alive"
+ req["Proxy-Connection"] = "Keep-Alive"
+ res = transport_request(req)
+ authphrase = res["Proxy-Authenticate"] or return res
+ req["Proxy-Authorization"] = "Negotiate #{n.complete_authentication(authphrase)}"
+ rescue => err
+ raise HTTPAuthenticationError.new('HTTP authentication failed', err)
+ end
+
+ #
+ # utils
+ #
+
+ private
+
+ def addr_port
+ addr = address
+ addr = "[#{addr}]" if addr.include?(":")
+ default_port = use_ssl? ? HTTP.https_default_port : HTTP.http_default_port
+ default_port == port ? addr : "#{addr}:#{port}"
+ end
+
+ # Adds a message to debugging output
+ def debug(msg)
+ return unless @debug_output
+ @debug_output << msg
+ @debug_output << "\n"
+ end
+
+ alias_method :D, :debug
+ end
+
+ # for backward compatibility until Ruby 4.0
+ # https://bugs.ruby-lang.org/issues/20900
+ # https://github.com/bblimke/webmock/pull/1081
+ HTTPSession = HTTP
+ deprecate_constant :HTTPSession
+end
+
+require_relative 'http/exceptions'
+
+require_relative 'http/header'
+
+require_relative 'http/generic_request'
+require_relative 'http/request'
+require_relative 'http/requests'
+
+require_relative 'http/response'
+require_relative 'http/responses'
+
+require_relative 'http/proxy_delta'
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb
new file mode 100644
index 0000000000..218df9a8bd
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/exceptions.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+module Gem::Net
+ # Gem::Net::HTTP exception class.
+ # You cannot use Gem::Net::HTTPExceptions directly; instead, you must use
+ # its subclasses.
+ module HTTPExceptions # :nodoc:
+ def initialize(msg, res) #:nodoc:
+ super msg
+ @response = res
+ end
+ attr_reader :response
+ alias data response #:nodoc: obsolete
+ end
+
+ # :stopdoc:
+ class HTTPError < ProtocolError
+ include HTTPExceptions
+ end
+
+ class HTTPRetriableError < ProtoRetriableError
+ include HTTPExceptions
+ end
+
+ class HTTPClientException < ProtoServerError
+ include HTTPExceptions
+ end
+
+ class HTTPFatalError < ProtoFatalError
+ include HTTPExceptions
+ end
+
+ # We cannot use the name "HTTPServerError", it is the name of the response.
+ HTTPServerException = HTTPClientException # :nodoc:
+ deprecate_constant(:HTTPServerException)
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb
new file mode 100644
index 0000000000..d6496d4ac1
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/generic_request.rb
@@ -0,0 +1,429 @@
+# frozen_string_literal: true
+#
+# \HTTPGenericRequest is the parent of the Gem::Net::HTTPRequest class.
+#
+# Do not use this directly; instead, use a subclass of Gem::Net::HTTPRequest.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+class Gem::Net::HTTPGenericRequest
+
+ include Gem::Net::HTTPHeader
+
+ def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) # :nodoc:
+ @method = m
+ @request_has_body = reqbody
+ @response_has_body = resbody
+
+ if Gem::URI === uri_or_path then
+ raise ArgumentError, "not an HTTP Gem::URI" unless Gem::URI::HTTP === uri_or_path
+ hostname = uri_or_path.host
+ raise ArgumentError, "no host component for Gem::URI" unless (hostname && hostname.length > 0)
+ @uri = uri_or_path.dup
+ @path = uri_or_path.request_uri
+ raise ArgumentError, "no HTTP request path given" unless @path
+ else
+ @uri = nil
+ raise ArgumentError, "no HTTP request path given" unless uri_or_path
+ raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty?
+ @path = uri_or_path.dup
+ end
+
+ @decode_content = false
+
+ if Gem::Net::HTTP::HAVE_ZLIB then
+ if !initheader ||
+ !initheader.keys.any? { |k|
+ %w[accept-encoding range].include? k.downcase
+ } then
+ @decode_content = true if @response_has_body
+ initheader = initheader ? initheader.dup : {}
+ initheader["accept-encoding"] =
+ "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ end
+ end
+
+ initialize_http_header initheader
+ self['Accept'] ||= '*/*'
+ self['User-Agent'] ||= 'Ruby'
+ self['Host'] ||= @uri.authority if @uri
+ @body = nil
+ @body_stream = nil
+ @body_data = nil
+ end
+
+ # Returns the string method name for the request:
+ #
+ # Gem::Net::HTTP::Get.new(uri).method # => "GET"
+ # Gem::Net::HTTP::Post.new(uri).method # => "POST"
+ #
+ attr_reader :method
+
+ # Returns the string path for the request:
+ #
+ # Gem::Net::HTTP::Get.new(uri).path # => "/"
+ # Gem::Net::HTTP::Post.new('example.com').path # => "example.com"
+ #
+ attr_reader :path
+
+ # Returns the Gem::URI object for the request, or +nil+ if none:
+ #
+ # Gem::Net::HTTP::Get.new(uri).uri
+ # # => #<Gem::URI::HTTPS https://jsonplaceholder.typicode.com/>
+ # Gem::Net::HTTP::Get.new('example.com').uri # => nil
+ #
+ attr_reader :uri
+
+ # Returns +false+ if the request's header <tt>'Accept-Encoding'</tt>
+ # has been set manually or deleted
+ # (indicating that the user intends to handle encoding in the response),
+ # +true+ otherwise:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+ # req['Accept-Encoding'] # => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3"
+ # req.decode_content # => true
+ # req['Accept-Encoding'] = 'foo'
+ # req.decode_content # => false
+ # req.delete('Accept-Encoding')
+ # req.decode_content # => false
+ #
+ attr_reader :decode_content
+
+ # Returns a string representation of the request:
+ #
+ # Gem::Net::HTTP::Post.new(uri).inspect # => "#<Gem::Net::HTTP::Post POST>"
+ #
+ def inspect
+ "\#<#{self.class} #{@method}>"
+ end
+
+ # Returns a string representation of the request with the details for pp:
+ #
+ # require 'pp'
+ # post = Gem::Net::HTTP::Post.new(uri)
+ # post.inspect # => "#<Gem::Net::HTTP::Post POST>"
+ # post.pretty_inspect
+ # # => #<Gem::Net::HTTP::Post
+ # POST
+ # path="/"
+ # headers={"accept-encoding" => ["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept" => ["*/*"],
+ # "user-agent" => ["Ruby"],
+ # "host" => ["www.ruby-lang.org"]}>
+ #
+ def pretty_print(q)
+ q.object_group(self) {
+ q.breakable
+ q.text @method
+ q.breakable
+ q.text "path="; q.pp @path
+ q.breakable
+ q.text "headers="; q.pp to_hash
+ }
+ end
+
+ ##
+ # Don't automatically decode response content-encoding if the user indicates
+ # they want to handle it.
+
+ def []=(key, val) # :nodoc:
+ @decode_content = false if key.downcase == 'accept-encoding'
+
+ super key, val
+ end
+
+ # Returns whether the request may have a body:
+ #
+ # Gem::Net::HTTP::Post.new(uri).request_body_permitted? # => true
+ # Gem::Net::HTTP::Get.new(uri).request_body_permitted? # => false
+ #
+ def request_body_permitted?
+ @request_has_body
+ end
+
+ # Returns whether the response may have a body:
+ #
+ # Gem::Net::HTTP::Post.new(uri).response_body_permitted? # => true
+ # Gem::Net::HTTP::Head.new(uri).response_body_permitted? # => false
+ #
+ def response_body_permitted?
+ @response_has_body
+ end
+
+ def body_exist? # :nodoc:
+ warn "Gem::Net::HTTPRequest#body_exist? is obsolete; use response_body_permitted?", uplevel: 1 if $VERBOSE
+ response_body_permitted?
+ end
+
+ # Returns the string body for the request, or +nil+ if there is none:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ attr_reader :body
+
+ # Sets the body for the request:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri)
+ # req.body # => nil
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.body # => "{\"title\": \"foo\",\"body\": \"bar\",\"userId\": 1}"
+ #
+ def body=(str)
+ @body = str
+ @body_stream = nil
+ @body_data = nil
+ str
+ end
+
+ # Returns the body stream object for the request, or +nil+ if there is none:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ attr_reader :body_stream
+
+ # Sets the body stream for the request:
+ #
+ # req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body_stream # => nil
+ # require 'stringio'
+ # req.body_stream = StringIO.new('xyzzy') # => #<StringIO:0x0000027d1e5affa8>
+ # req.body_stream # => #<StringIO:0x0000027d1e5affa8>
+ #
+ def body_stream=(input)
+ @body = nil
+ @body_stream = input
+ @body_data = nil
+ input
+ end
+
+ def set_body_internal(str) #:nodoc: internal use only
+ raise ArgumentError, "both of body argument and HTTPRequest#body set" if str and (@body or @body_stream)
+ self.body = str if str
+ if @body.nil? && @body_stream.nil? && @body_data.nil? && request_body_permitted?
+ self.body = ''
+ end
+ end
+
+ #
+ # write
+ #
+
+ def exec(sock, ver, path) #:nodoc: internal use only
+ if @body
+ send_request_with_body sock, ver, path, @body
+ elsif @body_stream
+ send_request_with_body_stream sock, ver, path, @body_stream
+ elsif @body_data
+ send_request_with_body_data sock, ver, path, @body_data
+ else
+ write_header sock, ver, path
+ end
+ end
+
+ def update_uri(addr, port, ssl) # :nodoc: internal use only
+ # reflect the connection and @path to @uri
+ return unless @uri
+
+ if ssl
+ scheme = 'https'
+ klass = Gem::URI::HTTPS
+ else
+ scheme = 'http'
+ klass = Gem::URI::HTTP
+ end
+
+ if host = self['host']
+ host = Gem::URI.parse("//#{host}").host # Remove a port component from the existing Host header
+ elsif host = @uri.host
+ else
+ host = addr
+ end
+ # convert the class of the Gem::URI
+ if @uri.is_a?(klass)
+ @uri.host = host
+ @uri.port = port
+ else
+ @uri = klass.new(
+ scheme, @uri.userinfo,
+ host, port, nil,
+ @uri.path, nil, @uri.query, nil)
+ end
+ end
+
+ private
+
+ # :stopdoc:
+
+ class Chunker #:nodoc:
+ def initialize(sock)
+ @sock = sock
+ @prev = nil
+ end
+
+ def write(buf)
+ # avoid memcpy() of buf, buf can huge and eat memory bandwidth
+ rv = buf.bytesize
+ @sock.write("#{rv.to_s(16)}\r\n", buf, "\r\n")
+ rv
+ end
+
+ def finish
+ @sock.write("0\r\n\r\n")
+ end
+ end
+
+ def send_request_with_body(sock, ver, path, body)
+ self.content_length = body.bytesize
+ delete 'Transfer-Encoding'
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ sock.write body
+ end
+
+ def send_request_with_body_stream(sock, ver, path, f)
+ unless content_length() or chunked?
+ raise ArgumentError,
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
+ end
+ write_header sock, ver, path
+ wait_for_continue sock, ver if sock.continue_timeout
+ if chunked?
+ chunker = Chunker.new(sock)
+ IO.copy_stream(f, chunker)
+ chunker.finish
+ else
+ IO.copy_stream(f, sock)
+ end
+ end
+
+ def send_request_with_body_data(sock, ver, path, params)
+ if /\Amultipart\/form-data\z/i !~ self.content_type
+ self.content_type = 'application/x-www-form-urlencoded'
+ return send_request_with_body(sock, ver, path, Gem::URI.encode_www_form(params))
+ end
+
+ opt = @form_option.dup
+ require 'securerandom' unless defined?(SecureRandom)
+ opt[:boundary] ||= SecureRandom.urlsafe_base64(40)
+ self.set_content_type(self.content_type, boundary: opt[:boundary])
+ if chunked?
+ write_header sock, ver, path
+ encode_multipart_form_data(sock, params, opt)
+ else
+ require 'tempfile'
+ file = Tempfile.new('multipart')
+ file.binmode
+ encode_multipart_form_data(file, params, opt)
+ file.rewind
+ self.content_length = file.size
+ write_header sock, ver, path
+ IO.copy_stream(file, sock)
+ file.close(true)
+ end
+ end
+
+ def encode_multipart_form_data(out, params, opt)
+ charset = opt[:charset]
+ boundary = opt[:boundary]
+ require 'securerandom' unless defined?(SecureRandom)
+ boundary ||= SecureRandom.urlsafe_base64(40)
+ chunked_p = chunked?
+
+ buf = +''
+ params.each do |key, value, h={}|
+ key = quote_string(key, charset)
+ filename =
+ h.key?(:filename) ? h[:filename] :
+ value.respond_to?(:to_path) ? File.basename(value.to_path) :
+ nil
+
+ buf << "--#{boundary}\r\n"
+ if filename
+ filename = quote_string(filename, charset)
+ type = h[:content_type] || 'application/octet-stream'
+ buf << "Content-Disposition: form-data; " \
+ "name=\"#{key}\"; filename=\"#{filename}\"\r\n" \
+ "Content-Type: #{type}\r\n\r\n"
+ if !out.respond_to?(:write) || !value.respond_to?(:read)
+ # if +out+ is not an IO or +value+ is not an IO
+ buf << (value.respond_to?(:read) ? value.read : value)
+ elsif value.respond_to?(:size) && chunked_p
+ # if +out+ is an IO and +value+ is a File, use IO.copy_stream
+ flush_buffer(out, buf, chunked_p)
+ out << "%x\r\n" % value.size if chunked_p
+ IO.copy_stream(value, out)
+ out << "\r\n" if chunked_p
+ else
+ # +out+ is an IO, and +value+ is not a File but an IO
+ flush_buffer(out, buf, chunked_p)
+ 1 while flush_buffer(out, value.read(4096), chunked_p)
+ end
+ else
+ # non-file field:
+ # HTML5 says, "The parts of the generated multipart/form-data
+ # resource that correspond to non-file fields must not have a
+ # Content-Type header specified."
+ buf << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
+ buf << (value.respond_to?(:read) ? value.read : value)
+ end
+ buf << "\r\n"
+ end
+ buf << "--#{boundary}--\r\n"
+ flush_buffer(out, buf, chunked_p)
+ out << "0\r\n\r\n" if chunked_p
+ end
+
+ def quote_string(str, charset)
+ str = str.encode(charset, fallback:->(c){'&#%d;'%c.encode("UTF-8").ord}) if charset
+ str.gsub(/[\\"]/, '\\\\\&')
+ end
+
+ def flush_buffer(out, buf, chunked_p)
+ return unless buf
+ out << "%x\r\n"%buf.bytesize if chunked_p
+ out << buf
+ out << "\r\n" if chunked_p
+ buf.clear
+ end
+
+ ##
+ # Waits up to the continue timeout for a response from the server provided
+ # we're speaking HTTP 1.1 and are expecting a 100-continue response.
+
+ def wait_for_continue(sock, ver)
+ if ver >= '1.1' and @header['expect'] and
+ @header['expect'].include?('100-continue')
+ if sock.io.to_io.wait_readable(sock.continue_timeout)
+ res = Gem::Net::HTTPResponse.read_new(sock)
+ unless res.kind_of?(Gem::Net::HTTPContinue)
+ res.decode_content = @decode_content
+ throw :response, res
+ end
+ end
+ end
+ end
+
+ def write_header(sock, ver, path)
+ reqline = "#{@method} #{path} HTTP/#{ver}"
+ if /[\r\n]/ =~ reqline
+ raise ArgumentError, "A Request-Line must not contain CR or LF"
+ end
+ buf = +''
+ buf << reqline << "\r\n"
+ each_capitalized do |k,v|
+ buf << "#{k}: #{v}\r\n"
+ end
+ buf << "\r\n"
+ sock.write buf
+ end
+
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/header.rb b/lib/rubygems/vendor/net-http/lib/net/http/header.rb
new file mode 100644
index 0000000000..bc68cd2eef
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/header.rb
@@ -0,0 +1,985 @@
+# frozen_string_literal: true
+#
+# The \HTTPHeader module provides access to \HTTP headers.
+#
+# The module is included in:
+#
+# - Gem::Net::HTTPGenericRequest (and therefore Gem::Net::HTTPRequest).
+# - Gem::Net::HTTPResponse.
+#
+# The headers are a hash-like collection of key/value pairs called _fields_.
+#
+# == Request and Response Fields
+#
+# Headers may be included in:
+#
+# - A Gem::Net::HTTPRequest object:
+# the object's headers will be sent with the request.
+# Any fields may be defined in the request;
+# see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+# - A Gem::Net::HTTPResponse object:
+# the objects headers are usually those returned from the host.
+# Fields may be retrieved from the object;
+# see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters]
+# and {Iterators}[rdoc-ref:Gem::Net::HTTPHeader@Iterators].
+#
+# Exactly which fields should be sent or expected depends on the host;
+# see:
+#
+# - {Request fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields].
+# - {Response fields}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields].
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Fields
+#
+# A header field is a key/value pair.
+#
+# === Field Keys
+#
+# A field key may be:
+#
+# - A string: Key <tt>'Accept'</tt> is treated as if it were
+# <tt>'Accept'.downcase</tt>; i.e., <tt>'accept'</tt>.
+# - A symbol: Key <tt>:Accept</tt> is treated as if it were
+# <tt>:Accept.to_s.downcase</tt>; i.e., <tt>'accept'</tt>.
+#
+# Examples:
+#
+# req = Gem::Net::HTTP::Get.new(uri)
+# req[:accept] # => "*/*"
+# req['Accept'] # => "*/*"
+# req['ACCEPT'] # => "*/*"
+#
+# req['accept'] = 'text/html'
+# req[:accept] = 'text/html'
+# req['ACCEPT'] = 'text/html'
+#
+# === Field Values
+#
+# A field value may be returned as an array of strings or as a string:
+#
+# - These methods return field values as arrays:
+#
+# - #get_fields: Returns the array value for the given key,
+# or +nil+ if it does not exist.
+# - #to_hash: Returns a hash of all header fields:
+# each key is a field name; its value is the array value for the field.
+#
+# - These methods return field values as string;
+# the string value for a field is equivalent to
+# <tt>self[key.downcase.to_s].join(', '))</tt>:
+#
+# - #[]: Returns the string value for the given key,
+# or +nil+ if it does not exist.
+# - #fetch: Like #[], but accepts a default value
+# to be returned if the key does not exist.
+#
+# The field value may be set:
+#
+# - #[]=: Sets the value for the given key;
+# the given value may be a string, a symbol, an array, or a hash.
+# - #add_field: Adds a given value to a value for the given key
+# (not overwriting the existing value).
+# - #delete: Deletes the field for the given key.
+#
+# Example field values:
+#
+# - \String:
+#
+# req['Accept'] = 'text/html' # => "text/html"
+# req['Accept'] # => "text/html"
+# req.get_fields('Accept') # => ["text/html"]
+#
+# - \Symbol:
+#
+# req['Accept'] = :text # => :text
+# req['Accept'] # => "text"
+# req.get_fields('Accept') # => ["text"]
+#
+# - Simple array:
+#
+# req[:foo] = %w[bar baz bat]
+# req[:foo] # => "bar, baz, bat"
+# req.get_fields(:foo) # => ["bar", "baz", "bat"]
+#
+# - Simple hash:
+#
+# req[:foo] = {bar: 0, baz: 1, bat: 2}
+# req[:foo] # => "bar, 0, baz, 1, bat, 2"
+# req.get_fields(:foo) # => ["bar", "0", "baz", "1", "bat", "2"]
+#
+# - Nested:
+#
+# req[:foo] = [%w[bar baz], {bat: 0, bam: 1}]
+# req[:foo] # => "bar, baz, bat, 0, bam, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "0", "bam", "1"]
+#
+# req[:foo] = {bar: %w[baz bat], bam: {bah: 0, bad: 1}}
+# req[:foo] # => "bar, baz, bat, bam, bah, 0, bad, 1"
+# req.get_fields(:foo) # => ["bar", "baz", "bat", "bam", "bah", "0", "bad", "1"]
+#
+# == Convenience Methods
+#
+# Various convenience methods retrieve values, set values, query values,
+# set form values, or iterate over fields.
+#
+# === Setters
+#
+# \Method #[]= can set any field, but does little to validate the new value;
+# some of the other setter methods provide some validation:
+#
+# - #[]=: Sets the string or array value for the given key.
+# - #add_field: Creates or adds to the array value for the given key.
+# - #basic_auth: Sets the string authorization header for <tt>'Authorization'</tt>.
+# - #content_length=: Sets the integer length for field <tt>'Content-Length</tt>.
+# - #content_type=: Sets the string value for field <tt>'Content-Type'</tt>.
+# - #proxy_basic_auth: Sets the string authorization header for <tt>'Proxy-Authorization'</tt>.
+# - #set_range: Sets the value for field <tt>'Range'</tt>.
+#
+# === Form Setters
+#
+# - #set_form: Sets an HTML form data set.
+# - #set_form_data: Sets header fields and a body from HTML form data.
+#
+# === Getters
+#
+# \Method #[] can retrieve the value of any field that exists,
+# but always as a string;
+# some of the other getter methods return something different
+# from the simple string value:
+#
+# - #[]: Returns the string field value for the given key.
+# - #content_length: Returns the integer value of field <tt>'Content-Length'</tt>.
+# - #content_range: Returns the Range value of field <tt>'Content-Range'</tt>.
+# - #content_type: Returns the string value of field <tt>'Content-Type'</tt>.
+# - #fetch: Returns the string field value for the given key.
+# - #get_fields: Returns the array field value for the given +key+.
+# - #main_type: Returns first part of the string value of field <tt>'Content-Type'</tt>.
+# - #sub_type: Returns second part of the string value of field <tt>'Content-Type'</tt>.
+# - #range: Returns an array of Range objects of field <tt>'Range'</tt>, or +nil+.
+# - #range_length: Returns the integer length of the range given in field <tt>'Content-Range'</tt>.
+# - #type_params: Returns the string parameters for <tt>'Content-Type'</tt>.
+#
+# === Queries
+#
+# - #chunked?: Returns whether field <tt>'Transfer-Encoding'</tt> is set to <tt>'chunked'</tt>.
+# - #connection_close?: Returns whether field <tt>'Connection'</tt> is set to <tt>'close'</tt>.
+# - #connection_keep_alive?: Returns whether field <tt>'Connection'</tt> is set to <tt>'keep-alive'</tt>.
+# - #key?: Returns whether a given key exists.
+#
+# === Iterators
+#
+# - #each_capitalized: Passes each field capitalized-name/value pair to the block.
+# - #each_capitalized_name: Passes each capitalized field name to the block.
+# - #each_header: Passes each field name/value pair to the block.
+# - #each_name: Passes each field name to the block.
+# - #each_value: Passes each string field value to the block.
+#
+module Gem::Net::HTTPHeader
+ # The maximum length of HTTP header keys.
+ MAX_KEY_LENGTH = 1024
+ # The maximum length of HTTP header values.
+ MAX_FIELD_LENGTH = 65536
+
+ def initialize_http_header(initheader) #:nodoc:
+ @header = {}
+ return unless initheader
+ initheader.each do |key, value|
+ warn "net/http: duplicated HTTP header: #{key}", uplevel: 3 if key?(key) and $VERBOSE
+ if value.nil?
+ warn "net/http: nil HTTP header: #{key}", uplevel: 3 if $VERBOSE
+ else
+ value = value.strip # raise error for invalid byte sequences
+ if key.to_s.bytesize > MAX_KEY_LENGTH
+ raise ArgumentError, "too long (#{key.bytesize} bytes) header: #{key[0, 30].inspect}..."
+ end
+ if value.to_s.bytesize > MAX_FIELD_LENGTH
+ raise ArgumentError, "header #{key} has too long field value: #{value.bytesize}"
+ end
+ if value.count("\r\n") > 0
+ raise ArgumentError, "header #{key} has field value #{value.inspect}, this cannot include CR/LF"
+ end
+ @header[key.downcase.to_s] = [value]
+ end
+ end
+ end
+
+ def size #:nodoc: obsolete
+ @header.size
+ end
+
+ alias length size #:nodoc: obsolete
+
+ # Returns the string field value for the case-insensitive field +key+,
+ # or +nil+ if there is no such key;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Connection'] # => "keep-alive"
+ # res['Nosuch'] # => nil
+ #
+ # Note that some field values may be retrieved via convenience methods;
+ # see {Getters}[rdoc-ref:Gem::Net::HTTPHeader@Getters].
+ def [](key)
+ a = @header[key.downcase.to_s] or return nil
+ a.join(', ')
+ end
+
+ # Sets the value for the case-insensitive +key+ to +val+,
+ # overwriting the previous value if the field exists;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req['Accept'] # => "*/*"
+ # req['Accept'] = 'text/html'
+ # req['Accept'] # => "text/html"
+ #
+ # Note that some field values may be set via convenience methods;
+ # see {Setters}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+ def []=(key, val)
+ unless val
+ @header.delete key.downcase.to_s
+ return val
+ end
+ set_field(key, val)
+ end
+
+ # Adds value +val+ to the value array for field +key+ if the field exists;
+ # creates the field with the given +key+ and +val+ if it does not exist.
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.add_field('Foo', 'bar')
+ # req['Foo'] # => "bar"
+ # req.add_field('Foo', 'baz')
+ # req['Foo'] # => "bar, baz"
+ # req.add_field('Foo', %w[baz bam])
+ # req['Foo'] # => "bar, baz, baz, bam"
+ # req.get_fields('Foo') # => ["bar", "baz", "baz", "bam"]
+ #
+ def add_field(key, val)
+ stringified_downcased_key = key.downcase.to_s
+ if @header.key?(stringified_downcased_key)
+ append_field_value(@header[stringified_downcased_key], val)
+ else
+ set_field(key, val)
+ end
+ end
+
+ # :stopdoc:
+ private def set_field(key, val)
+ case val
+ when Enumerable
+ ary = []
+ append_field_value(ary, val)
+ @header[key.downcase.to_s] = ary
+ else
+ val = val.to_s # for compatibility use to_s instead of to_str
+ if val.b.count("\r\n") > 0
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ @header[key.downcase.to_s] = [val]
+ end
+ end
+
+ private def append_field_value(ary, val)
+ case val
+ when Enumerable
+ val.each{|x| append_field_value(ary, x)}
+ else
+ val = val.to_s
+ if /[\r\n]/n.match?(val.b)
+ raise ArgumentError, 'header field value cannot include CR/LF'
+ end
+ ary.push val
+ end
+ end
+ # :startdoc:
+
+ # Returns the array field value for the given +key+,
+ # or +nil+ if there is no such field;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.get_fields('Connection') # => ["keep-alive"]
+ # res.get_fields('Nosuch') # => nil
+ #
+ def get_fields(key)
+ stringified_downcased_key = key.downcase.to_s
+ return nil unless @header[stringified_downcased_key]
+ @header[stringified_downcased_key].dup
+ end
+
+ # call-seq:
+ # fetch(key, default_val = nil) {|key| ... } -> object
+ # fetch(key, default_val = nil) -> value or default_val
+ #
+ # With a block, returns the string value for +key+ if it exists;
+ # otherwise returns the value of the block;
+ # ignores the +default_val+;
+ # see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ #
+ # # Field exists; block not called.
+ # res.fetch('Connection') do |value|
+ # fail 'Cannot happen'
+ # end # => "keep-alive"
+ #
+ # # Field does not exist; block called.
+ # res.fetch('Nosuch') do |value|
+ # value.downcase
+ # end # => "nosuch"
+ #
+ # With no block, returns the string value for +key+ if it exists;
+ # otherwise, returns +default_val+ if it was given;
+ # otherwise raises an exception:
+ #
+ # res.fetch('Connection', 'Foo') # => "keep-alive"
+ # res.fetch('Nosuch', 'Foo') # => "Foo"
+ # res.fetch('Nosuch') # Raises KeyError.
+ #
+ def fetch(key, *args, &block) #:yield: +key+
+ a = @header.fetch(key.downcase.to_s, *args, &block)
+ a.kind_of?(Array) ? a.join(', ') : a
+ end
+
+ # Calls the block with each key/value pair:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_header do |key, value|
+ # p [key, value] if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # ["content-type", "application/json; charset=utf-8"]
+ # ["connection", "keep-alive"]
+ # ["cache-control", "max-age=43200"]
+ # ["cf-cache-status", "HIT"]
+ # ["cf-ray", "771d17e9bc542cf5-ORD"]
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Gem::Net::HTTPHeader#each is an alias for Gem::Net::HTTPHeader#each_header.
+ def each_header #:yield: +key+, +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,va|
+ yield k, va.join(', ')
+ end
+ end
+
+ alias each each_header
+
+ # Calls the block with each field key:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_key do |key|
+ # p key if key.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "content-type"
+ # "connection"
+ # "cache-control"
+ # "cf-cache-status"
+ # "cf-ray"
+ #
+ # Returns an enumerator if no block is given.
+ #
+ # Gem::Net::HTTPHeader#each_name is an alias for Gem::Net::HTTPHeader#each_key.
+ def each_name(&block) #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key(&block)
+ end
+
+ alias each_key each_name
+
+ # Calls the block with each capitalized field name:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_capitalized_name do |key|
+ # p key if key.start_with?('C')
+ # end
+ #
+ # Output:
+ #
+ # "Content-Type"
+ # "Connection"
+ # "Cache-Control"
+ # "Cf-Cache-Status"
+ # "Cf-Ray"
+ #
+ # The capitalization is system-dependent;
+ # see {Case Mapping}[https://docs.ruby-lang.org/en/master/case_mapping_rdoc.html].
+ #
+ # Returns an enumerator if no block is given.
+ def each_capitalized_name #:yield: +key+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_key do |k|
+ yield capitalize(k)
+ end
+ end
+
+ # Calls the block with each string field value:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.each_value do |value|
+ # p value if value.start_with?('c')
+ # end
+ #
+ # Output:
+ #
+ # "chunked"
+ # "cf-q-config;dur=6.0000002122251e-06"
+ # "cloudflare"
+ #
+ # Returns an enumerator if no block is given.
+ def each_value #:yield: +value+
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each_value do |va|
+ yield va.join(', ')
+ end
+ end
+
+ # Removes the header for the given case-insensitive +key+
+ # (see {Fields}[rdoc-ref:Gem::Net::HTTPHeader@Fields]);
+ # returns the deleted value, or +nil+ if no such field exists:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.delete('Accept') # => ["*/*"]
+ # req.delete('Nosuch') # => nil
+ #
+ def delete(key)
+ @header.delete(key.downcase.to_s)
+ end
+
+ # Returns +true+ if the field for the case-insensitive +key+ exists, +false+ otherwise:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.key?('Accept') # => true
+ # req.key?('Nosuch') # => false
+ #
+ def key?(key)
+ @header.key?(key.downcase.to_s)
+ end
+
+ # Returns a hash of the key/value pairs:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.to_hash
+ # # =>
+ # {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+ # "accept"=>["*/*"],
+ # "user-agent"=>["Ruby"],
+ # "host"=>["jsonplaceholder.typicode.com"]}
+ #
+ def to_hash
+ @header.dup
+ end
+
+ # Like #each_header, but the keys are returned in capitalized form.
+ #
+ # Gem::Net::HTTPHeader#canonical_each is an alias for Gem::Net::HTTPHeader#each_capitalized.
+ def each_capitalized
+ block_given? or return enum_for(__method__) { @header.size }
+ @header.each do |k,v|
+ yield capitalize(k), v.join(', ')
+ end
+ end
+
+ alias canonical_each each_capitalized
+
+ def capitalize(name) # :nodoc:
+ name.to_s.split('-'.freeze).map {|s| s.capitalize }.join('-'.freeze)
+ end
+ private :capitalize
+
+ # Returns an array of Range objects that represent
+ # the value of field <tt>'Range'</tt>,
+ # or +nil+ if there is no such field;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req['Range'] = 'bytes=0-99,200-299,400-499'
+ # req.range # => [0..99, 200..299, 400..499]
+ # req.delete('Range')
+ # req.range # # => nil
+ #
+ def range
+ return nil unless @header['range']
+
+ value = self['Range']
+ # byte-range-set = *( "," OWS ) ( byte-range-spec / suffix-byte-range-spec )
+ # *( OWS "," [ OWS ( byte-range-spec / suffix-byte-range-spec ) ] )
+ # corrected collected ABNF
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#section-5.4.1
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p5-range-19#appendix-C
+ # http://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-19#section-3.2.5
+ unless /\Abytes=((?:,[ \t]*)*(?:\d+-\d*|-\d+)(?:[ \t]*,(?:[ \t]*\d+-\d*|-\d+)?)*)\z/ =~ value
+ raise Gem::Net::HTTPHeaderSyntaxError, "invalid syntax for byte-ranges-specifier: '#{value}'"
+ end
+
+ byte_range_set = $1
+ result = byte_range_set.split(/,/).map {|spec|
+ m = /(\d+)?\s*-\s*(\d+)?/i.match(spec) or
+ raise Gem::Net::HTTPHeaderSyntaxError, "invalid byte-range-spec: '#{spec}'"
+ d1 = m[1].to_i
+ d2 = m[2].to_i
+ if m[1] and m[2]
+ if d1 > d2
+ raise Gem::Net::HTTPHeaderSyntaxError, "last-byte-pos MUST greater than or equal to first-byte-pos but '#{spec}'"
+ end
+ d1..d2
+ elsif m[1]
+ d1..-1
+ elsif m[2]
+ -d2..-1
+ else
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range is not specified'
+ end
+ }
+ # if result.empty?
+ # byte-range-set must include at least one byte-range-spec or suffix-byte-range-spec
+ # but above regexp already denies it.
+ if result.size == 1 && result[0].begin == 0 && result[0].end == -1
+ raise Gem::Net::HTTPHeaderSyntaxError, 'only one suffix-byte-range-spec with zero suffix-length'
+ end
+ result
+ end
+
+ # call-seq:
+ # set_range(length) -> length
+ # set_range(offset, length) -> range
+ # set_range(begin..length) -> range
+ #
+ # Sets the value for field <tt>'Range'</tt>;
+ # see {Range request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#range-request-header]:
+ #
+ # With argument +length+:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.set_range(100) # => 100
+ # req['Range'] # => "bytes=0-99"
+ #
+ # With arguments +offset+ and +length+:
+ #
+ # req.set_range(100, 100) # => 100...200
+ # req['Range'] # => "bytes=100-199"
+ #
+ # With argument +range+:
+ #
+ # req.set_range(100..199) # => 100..199
+ # req['Range'] # => "bytes=100-199"
+ #
+ # Gem::Net::HTTPHeader#range= is an alias for Gem::Net::HTTPHeader#set_range.
+ def set_range(r, e = nil)
+ unless r
+ @header.delete 'range'
+ return r
+ end
+ r = (r...r+e) if e
+ case r
+ when Numeric
+ n = r.to_i
+ rangestr = (n > 0 ? "0-#{n-1}" : "-#{-n}")
+ when Range
+ first = r.first
+ last = r.end
+ last -= 1 if r.exclude_end?
+ if last == -1
+ rangestr = (first > 0 ? "#{first}-" : "-#{-first}")
+ else
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range.first is negative' if first < 0
+ raise Gem::Net::HTTPHeaderSyntaxError, 'range.last is negative' if last < 0
+ raise Gem::Net::HTTPHeaderSyntaxError, 'must be .first < .last' if first > last
+ rangestr = "#{first}-#{last}"
+ end
+ else
+ raise TypeError, 'Range/Integer is required'
+ end
+ @header['range'] = ["bytes=#{rangestr}"]
+ r
+ end
+
+ alias range= set_range
+
+ # Returns the value of field <tt>'Content-Length'</tt> as an integer,
+ # or +nil+ if there is no such field;
+ # see {Content-Length request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-request-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/nosuch/1')
+ # res.content_length # => 2
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res.content_length # => nil
+ #
+ def content_length
+ return nil unless key?('Content-Length')
+ len = self['Content-Length'].slice(/\d+/) or
+ raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Length format'
+ len.to_i
+ end
+
+ # Sets the value of field <tt>'Content-Length'</tt> to the given numeric;
+ # see {Content-Length response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-length-response-header]:
+ #
+ # _uri = uri.dup
+ # hostname = _uri.hostname # => "jsonplaceholder.typicode.com"
+ # _uri.path = '/posts' # => "/posts"
+ # req = Gem::Net::HTTP::Post.new(_uri) # => #<Gem::Net::HTTP::Post POST>
+ # req.body = '{"title": "foo","body": "bar","userId": 1}'
+ # req.content_length = req.body.size # => 42
+ # req.content_type = 'application/json'
+ # res = Gem::Net::HTTP.start(hostname) do |http|
+ # http.request(req)
+ # end # => #<Gem::Net::HTTPCreated 201 Created readbody=true>
+ #
+ def content_length=(len)
+ unless len
+ @header.delete 'content-length'
+ return nil
+ end
+ @header['content-length'] = [len.to_i.to_s]
+ end
+
+ # Returns +true+ if field <tt>'Transfer-Encoding'</tt>
+ # exists and has value <tt>'chunked'</tt>,
+ # +false+ otherwise;
+ # see {Transfer-Encoding response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#transfer-encoding-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Transfer-Encoding'] # => "chunked"
+ # res.chunked? # => true
+ #
+ def chunked?
+ return false unless @header['transfer-encoding']
+ field = self['Transfer-Encoding']
+ (/(?:\A|[^\-\w])chunked(?![\-\w])/i =~ field) ? true : false
+ end
+
+ # Returns a Range object representing the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res['Content-Range'] # => "bytes 0-499/1000"
+ # res.content_range # => 0..499
+ #
+ def content_range
+ return nil unless @header['content-range']
+ m = %r<\A\s*(\w+)\s+(\d+)-(\d+)/(\d+|\*)>.match(self['Content-Range']) or
+ raise Gem::Net::HTTPHeaderSyntaxError, 'wrong Content-Range format'
+ return unless m[1] == 'bytes'
+ m[2].to_i .. m[3].to_i
+ end
+
+ # Returns the integer representing length of the value of field
+ # <tt>'Content-Range'</tt>, or +nil+ if no such field exists;
+ # see {Content-Range response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-range-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['Content-Range'] # => nil
+ # res['Content-Range'] = 'bytes 0-499/1000'
+ # res.range_length # => 500
+ #
+ def range_length
+ r = content_range() or return nil
+ r.end - r.begin + 1
+ end
+
+ # Returns the {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.content_type # => "application/json"
+ #
+ def content_type
+ main = main_type()
+ return nil unless main
+
+ sub = sub_type()
+ if sub
+ "#{main}/#{sub}"
+ else
+ main
+ end
+ end
+
+ # Returns the leading ('type') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.main_type # => "application"
+ #
+ def main_type
+ return nil unless @header['content-type']
+ self['Content-Type'].split(';').first.to_s.split('/')[0].to_s.strip
+ end
+
+ # Returns the trailing ('subtype') part of the
+ # {media type}[https://en.wikipedia.org/wiki/Media_type]
+ # from the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.sub_type # => "json"
+ #
+ def sub_type
+ return nil unless @header['content-type']
+ _, sub = *self['Content-Type'].split(';').first.to_s.split('/')
+ return nil unless sub
+ sub.strip
+ end
+
+ # Returns the trailing ('parameters') part of the value of field <tt>'Content-Type'</tt>,
+ # or +nil+ if no such field exists;
+ # see {Content-Type response header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-response-header]:
+ #
+ # res = Gem::Net::HTTP.get_response(hostname, '/todos/1')
+ # res['content-type'] # => "application/json; charset=utf-8"
+ # res.type_params # => {"charset"=>"utf-8"}
+ #
+ def type_params
+ result = {}
+ list = self['Content-Type'].to_s.split(';')
+ list.shift
+ list.each do |param|
+ k, v = *param.split('=', 2)
+ result[k.strip] = v.strip
+ end
+ result
+ end
+
+ # Sets the value of field <tt>'Content-Type'</tt>;
+ # returns the new value;
+ # see {Content-Type request header}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#content-type-request-header]:
+ #
+ # req = Gem::Net::HTTP::Get.new(uri)
+ # req.set_content_type('application/json') # => ["application/json"]
+ #
+ # Gem::Net::HTTPHeader#content_type= is an alias for Gem::Net::HTTPHeader#set_content_type.
+ def set_content_type(type, params = {})
+ @header['content-type'] = [type + params.map{|k,v|"; #{k}=#{v}"}.join('')]
+ end
+
+ alias content_type= set_content_type
+
+ # Sets the request body to a URL-encoded string derived from argument +params+,
+ # and sets request header field <tt>'Content-Type'</tt>
+ # to <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The resulting request is suitable for HTTP request +POST+ or +PUT+.
+ #
+ # Argument +params+ must be suitable for use as argument +enum+ to
+ # {Gem::URI.encode_www_form}[https://docs.ruby-lang.org/en/master/Gem::URI.html#method-c-encode_www_form].
+ #
+ # With only argument +params+ given,
+ # sets the body to a URL-encoded string with the default separator <tt>'&'</tt>:
+ #
+ # req = Gem::Net::HTTP::Post.new('example.com')
+ #
+ # req.set_form_data(q: 'ruby', lang: 'en')
+ # req.body # => "q=ruby&lang=en"
+ # req['Content-Type'] # => "application/x-www-form-urlencoded"
+ #
+ # req.set_form_data([['q', 'ruby'], ['lang', 'en']])
+ # req.body # => "q=ruby&lang=en"
+ #
+ # req.set_form_data(q: ['ruby', 'perl'], lang: 'en')
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # req.set_form_data([['q', 'ruby'], ['q', 'perl'], ['lang', 'en']])
+ # req.body # => "q=ruby&q=perl&lang=en"
+ #
+ # With string argument +sep+ also given,
+ # uses that string as the separator:
+ #
+ # req.set_form_data({q: 'ruby', lang: 'en'}, '|')
+ # req.body # => "q=ruby|lang=en"
+ #
+ # Gem::Net::HTTPHeader#form_data= is an alias for Gem::Net::HTTPHeader#set_form_data.
+ def set_form_data(params, sep = '&')
+ query = Gem::URI.encode_www_form(params)
+ query.gsub!(/&/, sep) if sep != '&'
+ self.body = query
+ self.content_type = 'application/x-www-form-urlencoded'
+ end
+
+ alias form_data= set_form_data
+
+ # Stores form data to be used in a +POST+ or +PUT+ request.
+ #
+ # The form data given in +params+ consists of zero or more fields;
+ # each field is:
+ #
+ # - A scalar value.
+ # - A name/value pair.
+ # - An IO stream opened for reading.
+ #
+ # Argument +params+ should be an
+ # {Enumerable}[https://docs.ruby-lang.org/en/master/Enumerable.html#module-Enumerable-label-Enumerable+in+Ruby+Classes]
+ # (method <tt>params.map</tt> will be called),
+ # and is often an array or hash.
+ #
+ # First, we set up a request:
+ #
+ # _uri = uri.dup
+ # _uri.path ='/posts'
+ # req = Gem::Net::HTTP::Post.new(_uri)
+ #
+ # <b>Argument +params+ As an Array</b>
+ #
+ # When +params+ is an array,
+ # each of its elements is a subarray that defines a field;
+ # the subarray may contain:
+ #
+ # - One string:
+ #
+ # req.set_form([['foo'], ['bar'], ['baz']])
+ #
+ # - Two strings:
+ #
+ # req.set_form([%w[foo 0], %w[bar 1], %w[baz 2]])
+ #
+ # - When argument +enctype+ (see below) is given as
+ # <tt>'multipart/form-data'</tt>:
+ #
+ # - A string name and an IO stream opened for reading:
+ #
+ # require 'stringio'
+ # req.set_form([['file', StringIO.new('Ruby is cool.')]])
+ #
+ # - A string name, an IO stream opened for reading,
+ # and an options hash, which may contain these entries:
+ #
+ # - +:filename+: The name of the file to use.
+ # - +:content_type+: The content type of the uploaded file.
+ #
+ # Example:
+ #
+ # req.set_form([['file', file, {filename: "other-filename.foo"}]]
+ #
+ # The various forms may be mixed:
+ #
+ # req.set_form(['foo', %w[bar 1], ['file', file]])
+ #
+ # <b>Argument +params+ As a Hash</b>
+ #
+ # When +params+ is a hash,
+ # each of its entries is a name/value pair that defines a field:
+ #
+ # - The name is a string.
+ # - The value may be:
+ #
+ # - +nil+.
+ # - Another string.
+ # - An IO stream opened for reading
+ # (only when argument +enctype+ -- see below -- is given as
+ # <tt>'multipart/form-data'</tt>).
+ #
+ # Examples:
+ #
+ # # Nil-valued fields.
+ # req.set_form({'foo' => nil, 'bar' => nil, 'baz' => nil})
+ #
+ # # String-valued fields.
+ # req.set_form({'foo' => 0, 'bar' => 1, 'baz' => 2})
+ #
+ # # IO-valued field.
+ # require 'stringio'
+ # req.set_form({'file' => StringIO.new('Ruby is cool.')})
+ #
+ # # Mixture of fields.
+ # req.set_form({'foo' => nil, 'bar' => 1, 'file' => file})
+ #
+ # Optional argument +enctype+ specifies the value to be given
+ # to field <tt>'Content-Type'</tt>, and must be one of:
+ #
+ # - <tt>'application/x-www-form-urlencoded'</tt> (the default).
+ # - <tt>'multipart/form-data'</tt>;
+ # see {RFC 7578}[https://www.rfc-editor.org/rfc/rfc7578].
+ #
+ # Optional argument +formopt+ is a hash of options
+ # (applicable only when argument +enctype+
+ # is <tt>'multipart/form-data'</tt>)
+ # that may include the following entries:
+ #
+ # - +:boundary+: The value is the boundary string for the multipart message.
+ # If not given, the boundary is a random string.
+ # See {Boundary}[https://www.rfc-editor.org/rfc/rfc7578#section-4.1].
+ # - +:charset+: Value is the character set for the form submission.
+ # Field names and values of non-file fields should be encoded with this charset.
+ #
+ def set_form(params, enctype='application/x-www-form-urlencoded', formopt={})
+ @body_data = params
+ @body = nil
+ @body_stream = nil
+ @form_option = formopt
+ case enctype
+ when /\Aapplication\/x-www-form-urlencoded\z/i,
+ /\Amultipart\/form-data\z/i
+ self.content_type = enctype
+ else
+ raise ArgumentError, "invalid enctype: #{enctype}"
+ end
+ end
+
+ # Sets header <tt>'Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.basic_auth('my_account', 'my_password')
+ # req['Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def basic_auth(account, password)
+ @header['authorization'] = [basic_encode(account, password)]
+ end
+
+ # Sets header <tt>'Proxy-Authorization'</tt> using the given
+ # +account+ and +password+ strings:
+ #
+ # req.proxy_basic_auth('my_account', 'my_password')
+ # req['Proxy-Authorization']
+ # # => "Basic bXlfYWNjb3VudDpteV9wYXNzd29yZA=="
+ #
+ def proxy_basic_auth(account, password)
+ @header['proxy-authorization'] = [basic_encode(account, password)]
+ end
+
+ def basic_encode(account, password) # :nodoc:
+ 'Basic ' + ["#{account}:#{password}"].pack('m0')
+ end
+ private :basic_encode
+
+ # Returns whether the HTTP session is to be closed.
+ def connection_close?
+ token = /(?:\A|,)\s*close\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+ # Returns whether the HTTP session is to be kept alive.
+ def connection_keep_alive?
+ token = /(?:\A|,)\s*keep-alive\s*(?:\z|,)/i
+ @header['connection']&.grep(token) {return true}
+ @header['proxy-connection']&.grep(token) {return true}
+ false
+ end
+
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb
new file mode 100644
index 0000000000..137295a883
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/proxy_delta.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+module Gem::Net::HTTP::ProxyDelta #:nodoc: internal use only
+ private
+
+ def conn_address
+ proxy_address()
+ end
+
+ def conn_port
+ proxy_port()
+ end
+
+ def edit_path(path)
+ use_ssl? ? path : "http://#{addr_port()}#{path}"
+ end
+end
+
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/request.rb b/lib/rubygems/vendor/net-http/lib/net/http/request.rb
new file mode 100644
index 0000000000..495ec9be54
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/request.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Gem::Net::HTTP request classes.
+# The class should not be used directly;
+# instead you should use its subclasses, listed below.
+#
+# == Creating a Request
+#
+# An request object may be created with either a Gem::URI or a string hostname:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('https://jsonplaceholder.typicode.com/')
+# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+# req = Gem::Net::HTTP::Get.new(uri.hostname) # => #<Gem::Net::HTTP::Get GET>
+#
+# And with any of the subclasses:
+#
+# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
+# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
+# # ...
+#
+# The new instance is suitable for use as the argument to Gem::Net::HTTP#request.
+#
+# == Request Headers
+#
+# A new request object has these header fields by default:
+#
+# req.to_hash
+# # =>
+# {"accept-encoding"=>["gzip;q=1.0,deflate;q=0.6,identity;q=0.3"],
+# "accept"=>["*/*"],
+# "user-agent"=>["Ruby"],
+# "host"=>["jsonplaceholder.typicode.com"]}
+#
+# See:
+#
+# - {Request header Accept-Encoding}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Accept-Encoding]
+# and {Compression and Decompression}[rdoc-ref:Gem::Net::HTTP@Compression+and+Decompression].
+# - {Request header Accept}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#accept-request-header].
+# - {Request header User-Agent}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#user-agent-request-header].
+# - {Request header Host}[https://en.wikipedia.org/wiki/List_of_HTTP_header_fields#host-request-header].
+#
+# You can add headers or override default headers:
+#
+# # res = Gem::Net::HTTP::Get.new(uri, {'foo' => '0', 'bar' => '1'})
+#
+# This class (and therefore its subclasses) also includes (indirectly)
+# module Gem::Net::HTTPHeader, which gives access to its
+# {methods for setting headers}[rdoc-ref:Gem::Net::HTTPHeader@Setters].
+#
+# == Request Subclasses
+#
+# Subclasses for HTTP requests:
+#
+# - Gem::Net::HTTP::Get
+# - Gem::Net::HTTP::Head
+# - Gem::Net::HTTP::Post
+# - Gem::Net::HTTP::Put
+# - Gem::Net::HTTP::Delete
+# - Gem::Net::HTTP::Options
+# - Gem::Net::HTTP::Trace
+# - Gem::Net::HTTP::Patch
+#
+# Subclasses for WebDAV requests:
+#
+# - Gem::Net::HTTP::Propfind
+# - Gem::Net::HTTP::Proppatch
+# - Gem::Net::HTTP::Mkcol
+# - Gem::Net::HTTP::Copy
+# - Gem::Net::HTTP::Move
+# - Gem::Net::HTTP::Lock
+# - Gem::Net::HTTP::Unlock
+#
+class Gem::Net::HTTPRequest < Gem::Net::HTTPGenericRequest
+ # Creates an HTTP request object for +path+.
+ #
+ # +initheader+ are the default headers to use. Gem::Net::HTTP adds
+ # Accept-Encoding to enable compression of the response body unless
+ # Accept-Encoding or Range are supplied in +initheader+.
+
+ def initialize(path, initheader = nil)
+ super self.class::METHOD,
+ self.class::REQUEST_HAS_BODY,
+ self.class::RESPONSE_HAS_BODY,
+ path, initheader
+ end
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/requests.rb b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb
new file mode 100644
index 0000000000..f990761042
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/requests.rb
@@ -0,0 +1,444 @@
+# frozen_string_literal: true
+
+# HTTP/1.1 methods --- RFC2616
+
+# \Class for representing
+# {HTTP method GET}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#GET_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Get.new(uri) # => #<Gem::Net::HTTP::Get GET>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP.get: sends +GET+ request, returns response body.
+# - Gem::Net::HTTP#get: sends +GET+ request, returns response object.
+#
+class Gem::Net::HTTP::Get < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'GET'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method HEAD}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#HEAD_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Head.new(uri) # => #<Gem::Net::HTTP::Head HEAD>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: no.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP#head: sends +HEAD+ request, returns response object.
+#
+class Gem::Net::HTTP::Head < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'HEAD'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = false
+end
+
+# \Class for representing
+# {HTTP method POST}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#POST_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Post.new(uri) # => #<Gem::Net::HTTP::Post POST>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: yes.
+#
+# Related:
+#
+# - Gem::Net::HTTP.post: sends +POST+ request, returns response object.
+# - Gem::Net::HTTP#post: sends +POST+ request, returns response object.
+#
+class Gem::Net::HTTP::Post < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'POST'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PUT}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PUT_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Put.new(uri) # => #<Gem::Net::HTTP::Put PUT>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP.put: sends +PUT+ request, returns response object.
+# - Gem::Net::HTTP#put: sends +PUT+ request, returns response object.
+#
+class Gem::Net::HTTP::Put < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PUT'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method DELETE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#DELETE_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts/1'
+# req = Gem::Net::HTTP::Delete.new(uri) # => #<Gem::Net::HTTP::Delete DELETE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#delete: sends +DELETE+ request, returns response object.
+#
+class Gem::Net::HTTP::Delete < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'DELETE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method OPTIONS}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#OPTIONS_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Options.new(uri) # => #<Gem::Net::HTTP::Options OPTIONS>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: optional.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#options: sends +OPTIONS+ request, returns response object.
+#
+class Gem::Net::HTTP::Options < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'OPTIONS'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method TRACE}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#TRACE_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Trace.new(uri) # => #<Gem::Net::HTTP::Trace TRACE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: no.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: yes.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: yes.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#trace: sends +TRACE+ request, returns response object.
+#
+class Gem::Net::HTTP::Trace < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'TRACE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {HTTP method PATCH}[https://en.wikipedia.org/w/index.php?title=Hypertext_Transfer_Protocol#PATCH_method]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# uri.path = '/posts'
+# req = Gem::Net::HTTP::Patch.new(uri) # => #<Gem::Net::HTTP::Patch PATCH>
+# req.body = '{"title": "foo","body": "bar","userId": 1}'
+# req.content_type = 'application/json'
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Properties:
+#
+# - Request body: yes.
+# - Response body: yes.
+# - {Safe}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods]: no.
+# - {Idempotent}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Idempotent_methods]: no.
+# - {Cacheable}[https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Cacheable_methods]: no.
+#
+# Related:
+#
+# - Gem::Net::HTTP#patch: sends +PATCH+ request, returns response object.
+#
+class Gem::Net::HTTP::Patch < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+#
+# WebDAV methods --- RFC2518
+#
+
+# \Class for representing
+# {WebDAV method PROPFIND}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Propfind.new(uri) # => #<Gem::Net::HTTP::Propfind PROPFIND>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#propfind: sends +PROPFIND+ request, returns response object.
+#
+class Gem::Net::HTTP::Propfind < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPFIND'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method PROPPATCH}[http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Proppatch.new(uri) # => #<Gem::Net::HTTP::Proppatch PROPPATCH>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#proppatch: sends +PROPPATCH+ request, returns response object.
+#
+class Gem::Net::HTTP::Proppatch < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'PROPPATCH'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MKCOL}[http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Mkcol.new(uri) # => #<Gem::Net::HTTP::Mkcol MKCOL>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#mkcol: sends +MKCOL+ request, returns response object.
+#
+class Gem::Net::HTTP::Mkcol < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MKCOL'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method COPY}[http://www.webdav.org/specs/rfc4918.html#METHOD_COPY]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Copy.new(uri) # => #<Gem::Net::HTTP::Copy COPY>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#copy: sends +COPY+ request, returns response object.
+#
+class Gem::Net::HTTP::Copy < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'COPY'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method MOVE}[http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Move.new(uri) # => #<Gem::Net::HTTP::Move MOVE>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#move: sends +MOVE+ request, returns response object.
+#
+class Gem::Net::HTTP::Move < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'MOVE'
+ REQUEST_HAS_BODY = false
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method LOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_LOCK]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Lock.new(uri) # => #<Gem::Net::HTTP::Lock LOCK>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#lock: sends +LOCK+ request, returns response object.
+#
+class Gem::Net::HTTP::Lock < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'LOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
+
+# \Class for representing
+# {WebDAV method UNLOCK}[http://www.webdav.org/specs/rfc4918.html#METHOD_UNLOCK]:
+#
+# require 'rubygems/vendor/net-http/lib/net/http'
+# uri = Gem::URI('http://example.com')
+# hostname = uri.hostname # => "example.com"
+# req = Gem::Net::HTTP::Unlock.new(uri) # => #<Gem::Net::HTTP::Unlock UNLOCK>
+# res = Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end
+#
+# See {Request Headers}[rdoc-ref:Gem::Net::HTTPRequest@Request+Headers].
+#
+# Related:
+#
+# - Gem::Net::HTTP#unlock: sends +UNLOCK+ request, returns response object.
+#
+class Gem::Net::HTTP::Unlock < Gem::Net::HTTPRequest
+ # :stopdoc:
+ METHOD = 'UNLOCK'
+ REQUEST_HAS_BODY = true
+ RESPONSE_HAS_BODY = true
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/response.rb b/lib/rubygems/vendor/net-http/lib/net/http/response.rb
new file mode 100644
index 0000000000..dc164f1504
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/response.rb
@@ -0,0 +1,739 @@
+# frozen_string_literal: true
+
+# This class is the base class for \Gem::Net::HTTP response classes.
+#
+# == About the Examples
+#
+# :include: doc/net-http/examples.rdoc
+#
+# == Returned Responses
+#
+# \Method Gem::Net::HTTP.get_response returns
+# an instance of one of the subclasses of \Gem::Net::HTTPResponse:
+#
+# Gem::Net::HTTP.get_response(uri)
+# # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+# Gem::Net::HTTP.get_response(hostname, '/nosuch')
+# # => #<Gem::Net::HTTPNotFound 404 Not Found readbody=true>
+#
+# As does method Gem::Net::HTTP#request:
+#
+# req = Gem::Net::HTTP::Get.new(uri)
+# Gem::Net::HTTP.start(hostname) do |http|
+# http.request(req)
+# end # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+#
+# \Class \Gem::Net::HTTPResponse includes module Gem::Net::HTTPHeader,
+# which provides access to response header values via (among others):
+#
+# - \Hash-like method <tt>[]</tt>.
+# - Specific reader methods, such as +content_type+.
+#
+# Examples:
+#
+# res = Gem::Net::HTTP.get_response(uri) # => #<Gem::Net::HTTPOK 200 OK readbody=true>
+# res['Content-Type'] # => "text/html; charset=UTF-8"
+# res.content_type # => "text/html"
+#
+# == Response Subclasses
+#
+# \Class \Gem::Net::HTTPResponse has a subclass for each
+# {HTTP status code}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes].
+# You can look up the response class for a given code:
+#
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['200'] # => Gem::Net::HTTPOK
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['400'] # => Gem::Net::HTTPBadRequest
+# Gem::Net::HTTPResponse::CODE_TO_OBJ['404'] # => Gem::Net::HTTPNotFound
+#
+# And you can retrieve the status code for a response object:
+#
+# Gem::Net::HTTP.get_response(uri).code # => "200"
+# Gem::Net::HTTP.get_response(hostname, '/nosuch').code # => "404"
+#
+# The response subclasses (indentation shows class hierarchy):
+#
+# - Gem::Net::HTTPUnknownResponse (for unhandled \HTTP extensions).
+#
+# - Gem::Net::HTTPInformation:
+#
+# - Gem::Net::HTTPContinue (100)
+# - Gem::Net::HTTPSwitchProtocol (101)
+# - Gem::Net::HTTPProcessing (102)
+# - Gem::Net::HTTPEarlyHints (103)
+#
+# - Gem::Net::HTTPSuccess:
+#
+# - Gem::Net::HTTPOK (200)
+# - Gem::Net::HTTPCreated (201)
+# - Gem::Net::HTTPAccepted (202)
+# - Gem::Net::HTTPNonAuthoritativeInformation (203)
+# - Gem::Net::HTTPNoContent (204)
+# - Gem::Net::HTTPResetContent (205)
+# - Gem::Net::HTTPPartialContent (206)
+# - Gem::Net::HTTPMultiStatus (207)
+# - Gem::Net::HTTPAlreadyReported (208)
+# - Gem::Net::HTTPIMUsed (226)
+#
+# - Gem::Net::HTTPRedirection:
+#
+# - Gem::Net::HTTPMultipleChoices (300)
+# - Gem::Net::HTTPMovedPermanently (301)
+# - Gem::Net::HTTPFound (302)
+# - Gem::Net::HTTPSeeOther (303)
+# - Gem::Net::HTTPNotModified (304)
+# - Gem::Net::HTTPUseProxy (305)
+# - Gem::Net::HTTPTemporaryRedirect (307)
+# - Gem::Net::HTTPPermanentRedirect (308)
+#
+# - Gem::Net::HTTPClientError:
+#
+# - Gem::Net::HTTPBadRequest (400)
+# - Gem::Net::HTTPUnauthorized (401)
+# - Gem::Net::HTTPPaymentRequired (402)
+# - Gem::Net::HTTPForbidden (403)
+# - Gem::Net::HTTPNotFound (404)
+# - Gem::Net::HTTPMethodNotAllowed (405)
+# - Gem::Net::HTTPNotAcceptable (406)
+# - Gem::Net::HTTPProxyAuthenticationRequired (407)
+# - Gem::Net::HTTPRequestTimeOut (408)
+# - Gem::Net::HTTPConflict (409)
+# - Gem::Net::HTTPGone (410)
+# - Gem::Net::HTTPLengthRequired (411)
+# - Gem::Net::HTTPPreconditionFailed (412)
+# - Gem::Net::HTTPRequestEntityTooLarge (413)
+# - Gem::Net::HTTPRequestURITooLong (414)
+# - Gem::Net::HTTPUnsupportedMediaType (415)
+# - Gem::Net::HTTPRequestedRangeNotSatisfiable (416)
+# - Gem::Net::HTTPExpectationFailed (417)
+# - Gem::Net::HTTPMisdirectedRequest (421)
+# - Gem::Net::HTTPUnprocessableEntity (422)
+# - Gem::Net::HTTPLocked (423)
+# - Gem::Net::HTTPFailedDependency (424)
+# - Gem::Net::HTTPUpgradeRequired (426)
+# - Gem::Net::HTTPPreconditionRequired (428)
+# - Gem::Net::HTTPTooManyRequests (429)
+# - Gem::Net::HTTPRequestHeaderFieldsTooLarge (431)
+# - Gem::Net::HTTPUnavailableForLegalReasons (451)
+#
+# - Gem::Net::HTTPServerError:
+#
+# - Gem::Net::HTTPInternalServerError (500)
+# - Gem::Net::HTTPNotImplemented (501)
+# - Gem::Net::HTTPBadGateway (502)
+# - Gem::Net::HTTPServiceUnavailable (503)
+# - Gem::Net::HTTPGatewayTimeOut (504)
+# - Gem::Net::HTTPVersionNotSupported (505)
+# - Gem::Net::HTTPVariantAlsoNegotiates (506)
+# - Gem::Net::HTTPInsufficientStorage (507)
+# - Gem::Net::HTTPLoopDetected (508)
+# - Gem::Net::HTTPNotExtended (510)
+# - Gem::Net::HTTPNetworkAuthenticationRequired (511)
+#
+# There is also the Gem::Net::HTTPBadResponse exception which is raised when
+# there is a protocol error.
+#
+class Gem::Net::HTTPResponse
+ class << self
+ # true if the response has a body.
+ def body_permitted?
+ self::HAS_BODY
+ end
+
+ def exception_type # :nodoc: internal use only
+ self::EXCEPTION_TYPE
+ end
+
+ def read_new(sock) #:nodoc: internal use only
+ httpv, code, msg = read_status_line(sock)
+ res = response_class(code).new(httpv, code, msg)
+ each_response_header(sock) do |k,v|
+ res.add_field k, v
+ end
+ res
+ end
+
+ private
+ # :stopdoc:
+
+ def read_status_line(sock)
+ str = sock.readline
+ m = /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)(?:\s+(.*))?\z/in.match(str) or
+ raise Gem::Net::HTTPBadResponse, "wrong status line: #{str.dump}"
+ m.captures
+ end
+
+ def response_class(code)
+ CODE_TO_OBJ[code] or
+ CODE_CLASS_TO_OBJ[code[0,1]] or
+ Gem::Net::HTTPUnknownResponse
+ end
+
+ def each_response_header(sock)
+ key = value = nil
+ while true
+ line = sock.readuntil("\n", true).sub(/\s+\z/, '')
+ break if line.empty?
+ if line[0] == ?\s or line[0] == ?\t and value
+ value << ' ' unless value.empty?
+ value << line.strip
+ else
+ yield key, value if key
+ key, value = line.strip.split(/\s*:\s*/, 2)
+ raise Gem::Net::HTTPBadResponse, 'wrong header line format' if value.nil?
+ end
+ end
+ yield key, value if key
+ end
+ end
+
+ # next is to fix bug in RDoc, where the private inside class << self
+ # spills out.
+ public
+
+ include Gem::Net::HTTPHeader
+
+ def initialize(httpv, code, msg) #:nodoc: internal use only
+ @http_version = httpv
+ @code = code
+ @message = msg
+ initialize_http_header nil
+ @body = nil
+ @read = false
+ @uri = nil
+ @decode_content = false
+ @body_encoding = false
+ @ignore_eof = true
+ end
+
+ # The HTTP version supported by the server.
+ attr_reader :http_version
+
+ # The HTTP result code string. For example, '302'. You can also
+ # determine the response type by examining which response subclass
+ # the response object is an instance of.
+ attr_reader :code
+
+ # The HTTP result message sent by the server. For example, 'Not Found'.
+ attr_reader :message
+ alias msg message # :nodoc: obsolete
+
+ # The Gem::URI used to fetch this response. The response Gem::URI is only available
+ # if a Gem::URI was used to create the request.
+ attr_reader :uri
+
+ # Set to true automatically when the request did not contain an
+ # Accept-Encoding header from the user.
+ attr_accessor :decode_content
+
+ # Returns the value set by body_encoding=, or +false+ if none;
+ # see #body_encoding=.
+ attr_reader :body_encoding
+
+ # Sets the encoding that should be used when reading the body:
+ #
+ # - If the given value is an Encoding object, that encoding will be used.
+ # - Otherwise if the value is a string, the value of
+ # {Encoding#find(value)}[https://docs.ruby-lang.org/en/master/Encoding.html#method-c-find]
+ # will be used.
+ # - Otherwise an encoding will be deduced from the body itself.
+ #
+ # Examples:
+ #
+ # http = Gem::Net::HTTP.new(hostname)
+ # req = Gem::Net::HTTP::Get.new('/')
+ #
+ # http.request(req) do |res|
+ # p res.body.encoding # => #<Encoding:ASCII-8BIT>
+ # end
+ #
+ # http.request(req) do |res|
+ # res.body_encoding = "UTF-8"
+ # p res.body.encoding # => #<Encoding:UTF-8>
+ # end
+ #
+ def body_encoding=(value)
+ value = Encoding.find(value) if value.is_a?(String)
+ @body_encoding = value
+ end
+
+ # Whether to ignore EOF when reading bodies with a specified Content-Length
+ # header.
+ attr_accessor :ignore_eof
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{@code} #{@message} readbody=#{@read}>"
+ end
+
+ #
+ # response <-> exception relationship
+ #
+
+ def code_type #:nodoc:
+ self.class
+ end
+
+ def error! #:nodoc:
+ message = @code
+ message = "#{message} #{@message.dump}" if @message
+ raise error_type().new(message, self)
+ end
+
+ def error_type #:nodoc:
+ self.class::EXCEPTION_TYPE
+ end
+
+ # Raises an HTTP error if the response is not 2xx (success).
+ def value
+ error! unless self.kind_of?(Gem::Net::HTTPSuccess)
+ end
+
+ def uri= uri # :nodoc:
+ @uri = uri.dup if uri
+ end
+
+ #
+ # header (for backward compatibility only; DO NOT USE)
+ #
+
+ def response #:nodoc:
+ warn "Gem::Net::HTTPResponse#response is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def header #:nodoc:
+ warn "Gem::Net::HTTPResponse#header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ def read_header #:nodoc:
+ warn "Gem::Net::HTTPResponse#read_header is obsolete", uplevel: 1 if $VERBOSE
+ self
+ end
+
+ #
+ # body
+ #
+
+ def reading_body(sock, reqmethodallowbody) #:nodoc: internal use only
+ @socket = sock
+ @body_exist = reqmethodallowbody && self.class.body_permitted?
+ begin
+ yield
+ self.body # ensure to read body
+ ensure
+ @socket = nil
+ end
+ end
+
+ # Gets the entity body returned by the remote HTTP server.
+ #
+ # If a block is given, the body is passed to the block, and
+ # the body is provided in fragments, as it is read in from the socket.
+ #
+ # If +dest+ argument is given, response is read into that variable,
+ # with <code>dest#<<</code> method (it could be String or IO, or any
+ # other object responding to <code><<</code>).
+ #
+ # Calling this method a second or subsequent time for the same
+ # HTTPResponse object will return the value already read.
+ #
+ # http.request_get('/index.html') {|res|
+ # puts res.read_body
+ # }
+ #
+ # http.request_get('/index.html') {|res|
+ # p res.read_body.object_id # 538149362
+ # p res.read_body.object_id # 538149362
+ # }
+ #
+ # # using iterator
+ # http.request_get('/index.html') {|res|
+ # res.read_body do |segment|
+ # print segment
+ # end
+ # }
+ #
+ def read_body(dest = nil, &block)
+ if @read
+ raise IOError, "#{self.class}\#read_body called twice" if dest or block
+ return @body
+ end
+ to = procdest(dest, block)
+ stream_check
+ if @body_exist
+ read_body_0 to
+ @body = to
+ else
+ @body = nil
+ end
+ @read = true
+ return if @body.nil?
+
+ case enc = @body_encoding
+ when Encoding, false, nil
+ # Encoding: force given encoding
+ # false/nil: do not force encoding
+ else
+ # other value: detect encoding from body
+ enc = detect_encoding(@body)
+ end
+
+ @body.force_encoding(enc) if enc
+
+ @body
+ end
+
+ # Returns the string response body;
+ # note that repeated calls for the unmodified body return a cached string:
+ #
+ # path = '/todos/1'
+ # Gem::Net::HTTP.start(hostname) do |http|
+ # res = http.get(path)
+ # p res.body
+ # p http.head(path).body # No body.
+ # end
+ #
+ # Output:
+ #
+ # "{\n \"userId\": 1,\n \"id\": 1,\n \"title\": \"delectus aut autem\",\n \"completed\": false\n}"
+ # nil
+ #
+ def body
+ read_body()
+ end
+
+ # Sets the body of the response to the given value.
+ def body=(value)
+ @body = value
+ end
+
+ alias entity body #:nodoc: obsolete
+
+ private
+
+ # :nodoc:
+ def detect_encoding(str, encoding=nil)
+ if encoding
+ elsif encoding = type_params['charset']
+ elsif encoding = check_bom(str)
+ else
+ encoding = case content_type&.downcase
+ when %r{text/x(?:ht)?ml|application/(?:[^+]+\+)?xml}
+ /\A<xml[ \t\r\n]+
+ version[ \t\r\n]*=[ \t\r\n]*(?:"[0-9.]+"|'[0-9.]*')[ \t\r\n]+
+ encoding[ \t\r\n]*=[ \t\r\n]*
+ (?:"([A-Za-z][\-A-Za-z0-9._]*)"|'([A-Za-z][\-A-Za-z0-9._]*)')/x =~ str
+ encoding = $1 || $2 || Encoding::UTF_8
+ when %r{text/html.*}
+ sniff_encoding(str)
+ end
+ end
+ return encoding
+ end
+
+ # :nodoc:
+ def sniff_encoding(str, encoding=nil)
+ # the encoding sniffing algorithm
+ # http://www.w3.org/TR/html5/parsing.html#determining-the-character-encoding
+ if enc = scanning_meta(str)
+ enc
+ # 6. last visited page or something
+ # 7. frequency
+ elsif str.ascii_only?
+ Encoding::US_ASCII
+ elsif str.dup.force_encoding(Encoding::UTF_8).valid_encoding?
+ Encoding::UTF_8
+ end
+ # 8. implementation-defined or user-specified
+ end
+
+ # :nodoc:
+ def check_bom(str)
+ case str.byteslice(0, 2)
+ when "\xFE\xFF"
+ return Encoding::UTF_16BE
+ when "\xFF\xFE"
+ return Encoding::UTF_16LE
+ end
+ if "\xEF\xBB\xBF" == str.byteslice(0, 3)
+ return Encoding::UTF_8
+ end
+ nil
+ end
+
+ # :nodoc:
+ def scanning_meta(str)
+ require 'strscan'
+ ss = StringScanner.new(str)
+ if ss.scan_until(/<meta[\t\n\f\r ]*/)
+ attrs = {} # attribute_list
+ got_pragma = false
+ need_pragma = nil
+ charset = nil
+
+ # step: Attributes
+ while attr = get_attribute(ss)
+ name, value = *attr
+ next if attrs[name]
+ attrs[name] = true
+ case name
+ when 'http-equiv'
+ got_pragma = true if value == 'content-type'
+ when 'content'
+ encoding = extracting_encodings_from_meta_elements(value)
+ unless charset
+ charset = encoding
+ end
+ need_pragma = true
+ when 'charset'
+ need_pragma = false
+ charset = value
+ end
+ end
+
+ # step: Processing
+ return if need_pragma.nil?
+ return if need_pragma && !got_pragma
+
+ charset = Encoding.find(charset) rescue nil
+ return unless charset
+ charset = Encoding::UTF_8 if charset == Encoding::UTF_16
+ return charset # tentative
+ end
+ nil
+ end
+
+ def get_attribute(ss)
+ ss.scan(/[\t\n\f\r \/]*/)
+ if ss.peek(1) == '>'
+ ss.getch
+ return nil
+ end
+ name = ss.scan(/[^=\t\n\f\r \/>]*/)
+ name.downcase!
+ raise if name.empty?
+ ss.skip(/[\t\n\f\r ]*/)
+ if ss.getch != '='
+ value = ''
+ return [name, value]
+ end
+ ss.skip(/[\t\n\f\r ]*/)
+ case ss.peek(1)
+ when '"'
+ ss.getch
+ value = ss.scan(/[^"]+/)
+ value.downcase!
+ ss.getch
+ when "'"
+ ss.getch
+ value = ss.scan(/[^']+/)
+ value.downcase!
+ ss.getch
+ when '>'
+ value = ''
+ else
+ value = ss.scan(/[^\t\n\f\r >]+/)
+ value.downcase!
+ end
+ [name, value]
+ end
+
+ def extracting_encodings_from_meta_elements(value)
+ # http://dev.w3.org/html5/spec/fetching-resources.html#algorithm-for-extracting-an-encoding-from-a-meta-element
+ if /charset[\t\n\f\r ]*=(?:"([^"]*)"|'([^']*)'|["']|\z|([^\t\n\f\r ;]+))/i =~ value
+ return $1 || $2 || $3
+ end
+ return nil
+ end
+
+ ##
+ # Checks for a supported Content-Encoding header and yields an Inflate
+ # wrapper for this response's socket when zlib is present. If the
+ # Content-Encoding is not supported or zlib is missing, the plain socket is
+ # yielded.
+ #
+ # If a Content-Range header is present, a plain socket is yielded as the
+ # bytes in the range may not be a complete deflate block.
+
+ def inflater # :nodoc:
+ return yield @socket unless Gem::Net::HTTP::HAVE_ZLIB
+ return yield @socket unless @decode_content
+ return yield @socket if self['content-range']
+
+ v = self['content-encoding']
+ case v&.downcase
+ when 'deflate', 'gzip', 'x-gzip' then
+ self.delete 'content-encoding'
+
+ inflate_body_io = Inflater.new(@socket)
+
+ begin
+ yield inflate_body_io
+ success = true
+ ensure
+ begin
+ inflate_body_io.finish
+ if self['content-length']
+ self['content-length'] = inflate_body_io.bytes_inflated.to_s
+ end
+ rescue => err
+ # Ignore #finish's error if there is an exception from yield
+ raise err if success
+ end
+ end
+ when 'none', 'identity' then
+ self.delete 'content-encoding'
+
+ yield @socket
+ else
+ yield @socket
+ end
+ end
+
+ def read_body_0(dest)
+ inflater do |inflate_body_io|
+ if chunked?
+ read_chunked dest, inflate_body_io
+ return
+ end
+
+ @socket = inflate_body_io
+
+ clen = content_length()
+ if clen
+ @socket.read clen, dest, @ignore_eof
+ return
+ end
+ clen = range_length()
+ if clen
+ @socket.read clen, dest
+ return
+ end
+ @socket.read_all dest
+ end
+ end
+
+ ##
+ # read_chunked reads from +@socket+ for chunk-size, chunk-extension, CRLF,
+ # etc. and +chunk_data_io+ for chunk-data which may be deflate or gzip
+ # encoded.
+ #
+ # See RFC 2616 section 3.6.1 for definitions
+
+ def read_chunked(dest, chunk_data_io) # :nodoc:
+ total = 0
+ while true
+ line = @socket.readline
+ hexlen = line.slice(/[0-9a-fA-F]+/) or
+ raise Gem::Net::HTTPBadResponse, "wrong chunk size line: #{line}"
+ len = hexlen.hex
+ break if len == 0
+ begin
+ chunk_data_io.read len, dest
+ ensure
+ total += len
+ @socket.read 2 # \r\n
+ end
+ end
+ until @socket.readline.empty?
+ # none
+ end
+ end
+
+ def stream_check
+ raise IOError, 'attempt to read body out of block' if @socket.nil? || @socket.closed?
+ end
+
+ def procdest(dest, block)
+ raise ArgumentError, 'both arg and block given for HTTP method' if
+ dest and block
+ if block
+ Gem::Net::ReadAdapter.new(block)
+ else
+ dest || +''
+ end
+ end
+
+ ##
+ # Inflater is a wrapper around Gem::Net::BufferedIO that transparently inflates
+ # zlib and gzip streams.
+
+ class Inflater # :nodoc:
+
+ ##
+ # Creates a new Inflater wrapping +socket+
+
+ def initialize socket
+ @socket = socket
+ # zlib with automatic gzip detection
+ @inflate = Zlib::Inflate.new(32 + Zlib::MAX_WBITS)
+ end
+
+ ##
+ # Finishes the inflate stream.
+
+ def finish
+ return if @inflate.total_in == 0
+ @inflate.finish
+ end
+
+ ##
+ # The number of bytes inflated, used to update the Content-Length of
+ # the response.
+
+ def bytes_inflated
+ @inflate.total_out
+ end
+
+ ##
+ # Returns a Gem::Net::ReadAdapter that inflates each read chunk into +dest+.
+ #
+ # This allows a large response body to be inflated without storing the
+ # entire body in memory.
+
+ def inflate_adapter(dest)
+ if dest.respond_to?(:set_encoding)
+ dest.set_encoding(Encoding::ASCII_8BIT)
+ elsif dest.respond_to?(:force_encoding)
+ dest.force_encoding(Encoding::ASCII_8BIT)
+ end
+ block = proc do |compressed_chunk|
+ @inflate.inflate(compressed_chunk) do |chunk|
+ compressed_chunk.clear
+ dest << chunk
+ end
+ end
+
+ Gem::Net::ReadAdapter.new(block)
+ end
+
+ ##
+ # Reads +clen+ bytes from the socket, inflates them, then writes them to
+ # +dest+. +ignore_eof+ is passed down to Gem::Net::BufferedIO#read
+ #
+ # Unlike Gem::Net::BufferedIO#read, this method returns more than +clen+ bytes.
+ # At this time there is no way for a user of Gem::Net::HTTPResponse to read a
+ # specific number of bytes from the HTTP response body, so this internal
+ # API does not return the same number of bytes as were requested.
+ #
+ # See https://bugs.ruby-lang.org/issues/6492 for further discussion.
+
+ def read clen, dest, ignore_eof = false
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read clen, temp_dest, ignore_eof
+ end
+
+ ##
+ # Reads the rest of the socket, inflates it, then writes it to +dest+.
+
+ def read_all dest
+ temp_dest = inflate_adapter(dest)
+
+ @socket.read_all temp_dest
+ end
+
+ end
+
+end
+
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/responses.rb b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb
new file mode 100644
index 0000000000..62ce1cba1b
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/responses.rb
@@ -0,0 +1,1242 @@
+# frozen_string_literal: true
+#--
+# https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+
+module Gem::Net
+
+ # Unknown HTTP response
+ class HTTPUnknownResponse < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for informational (1xx) HTTP response classes.
+ #
+ # An informational response indicates that the request was received and understood.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.1xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response].
+ #
+ class HTTPInformation < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = false
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for success (2xx) HTTP response classes.
+ #
+ # A success response indicates the action requested by the client
+ # was received, understood, and accepted.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.2xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#2xx_success].
+ #
+ class HTTPSuccess < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPError #
+ end
+
+ # Parent class for redirection (3xx) HTTP response classes.
+ #
+ # A redirection response indicates the client must take additional action
+ # to complete the request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.3xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_redirection].
+ #
+ class HTTPRedirection < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPRetriableError #
+ end
+
+ # Parent class for client error (4xx) HTTP response classes.
+ #
+ # A client error response indicates that the client may have caused an error.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.4xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#4xx_client_errors].
+ #
+ class HTTPClientError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPClientException #
+ end
+
+ # Parent class for server error (5xx) HTTP response classes.
+ #
+ # A server error response indicates that the server failed to fulfill a request.
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#status.5xx].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#5xx_server_errors].
+ #
+ class HTTPServerError < HTTPResponse
+ # :stopdoc:
+ HAS_BODY = true
+ EXCEPTION_TYPE = HTTPFatalError #
+ end
+
+ # Response class for +Continue+ responses (status code 100).
+ #
+ # A +Continue+ response indicates that the server has received the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-100-continue].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#100].
+ #
+ class HTTPContinue < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Switching Protocol</tt> responses (status code 101).
+ #
+ # The <tt>Switching Protocol<tt> response indicates that the server has received
+ # a request to switch protocols, and has agreed to do so.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/101].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#101].
+ #
+ class HTTPSwitchProtocol < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +Processing+ responses (status code 102).
+ #
+ # The +Processing+ response indicates that the server has received
+ # and is processing the request, but no response is available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 2518}[https://www.rfc-editor.org/rfc/rfc2518#section-10.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#102].
+ #
+ class HTTPProcessing < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Early Hints</tt> responses (status code 103).
+ #
+ # The <tt>Early Hints</tt> indicates that the server has received
+ # and is processing the request, and contains certain headers;
+ # the final response is not available yet.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/103].
+ # - {RFC 8297}[https://www.rfc-editor.org/rfc/rfc8297.html#section-2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#103].
+ #
+ class HTTPEarlyHints < HTTPInformation
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for +OK+ responses (status code 200).
+ #
+ # The +OK+ response indicates that the server has received
+ # a request and has responded successfully.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-200-ok].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#200].
+ #
+ class HTTPOK < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Created+ responses (status code 201).
+ #
+ # The +Created+ response indicates that the server has received
+ # and has fulfilled a request to create a new resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/201].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-201-created].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#201].
+ #
+ class HTTPCreated < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for +Accepted+ responses (status code 202).
+ #
+ # The +Accepted+ response indicates that the server has received
+ # and is processing a request, but the processing has not yet been completed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/202].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-202-accepted].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#202].
+ #
+ class HTTPAccepted < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Non-Authoritative Information</tt> responses (status code 203).
+ #
+ # The <tt>Non-Authoritative Information</tt> response indicates that the server
+ # is a transforming proxy (such as a Web accelerator)
+ # that received a 200 OK response from its origin,
+ # and is returning a modified version of the origin's response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/203].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-203-non-authoritative-infor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#203].
+ #
+ class HTTPNonAuthoritativeInformation < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>No Content</tt> responses (status code 204).
+ #
+ # The <tt>No Content</tt> response indicates that the server
+ # successfully processed the request, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/204].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-204-no-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#204].
+ #
+ class HTTPNoContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Reset Content</tt> responses (status code 205).
+ #
+ # The <tt>Reset Content</tt> response indicates that the server
+ # successfully processed the request,
+ # asks that the client reset its document view, and is not returning any content.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/205].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-205-reset-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#205].
+ #
+ class HTTPResetContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Partial Content</tt> responses (status code 206).
+ #
+ # The <tt>Partial Content</tt> response indicates that the server is delivering
+ # only part of the resource (byte serving)
+ # due to a Range header in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/206].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-206-partial-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#206].
+ #
+ class HTTPPartialContent < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multi-Status (WebDAV)</tt> responses (status code 207).
+ #
+ # The <tt>Multi-Status (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the message body can contain a number of separate response codes.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4818}[https://www.rfc-editor.org/rfc/rfc4918#section-11.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#207].
+ #
+ class HTTPMultiStatus < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Already Reported (WebDAV)</tt> responses (status code 208).
+ #
+ # The <tt>Already Reported (WebDAV)</tt> response indicates that the server
+ # has received the request,
+ # and that the members of a DAV binding have already been enumerated
+ # in a preceding part of the (multi-status) response,
+ # and are not being included again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 5842}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#208].
+ #
+ class HTTPAlreadyReported < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>IM Used</tt> responses (status code 226).
+ #
+ # The <tt>IM Used</tt> response indicates that the server has fulfilled a request
+ # for the resource, and the response is a representation of the result
+ # of one or more instance-manipulations applied to the current instance.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 3229}[https://www.rfc-editor.org/rfc/rfc3229.html#section-10.4.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#226].
+ #
+ class HTTPIMUsed < HTTPSuccess
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Multiple Choices</tt> responses (status code 300).
+ #
+ # The <tt>Multiple Choices</tt> response indicates that the server
+ # offers multiple options for the resource from which the client may choose.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/300].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-300-multiple-choices].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#300].
+ #
+ class HTTPMultipleChoices < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMultipleChoice = HTTPMultipleChoices
+
+ # Response class for <tt>Moved Permanently</tt> responses (status code 301).
+ #
+ # The <tt>Moved Permanently</tt> response indicates that links or records
+ # returning this response should be updated to use the given URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/301].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-301-moved-permanently].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#301].
+ #
+ class HTTPMovedPermanently < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Found</tt> responses (status code 302).
+ #
+ # The <tt>Found</tt> response indicates that the client
+ # should look at (browse to) another URL.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-302-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#302].
+ #
+ class HTTPFound < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPMovedTemporarily = HTTPFound
+
+ # Response class for <tt>See Other</tt> responses (status code 303).
+ #
+ # The response to the request can be found under another Gem::URI using the GET method.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/303].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-303-see-other].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#303].
+ #
+ class HTTPSeeOther < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Modified</tt> responses (status code 304).
+ #
+ # Indicates that the resource has not been modified since the version
+ # specified by the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-304-not-modified].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304].
+ #
+ class HTTPNotModified < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Use Proxy</tt> responses (status code 305).
+ #
+ # The requested resource is available only through a proxy,
+ # whose address is provided in the response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-305-use-proxy].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#305].
+ #
+ class HTTPUseProxy < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = false
+ end
+
+ # Response class for <tt>Temporary Redirect</tt> responses (status code 307).
+ #
+ # The request should be repeated with another Gem::URI;
+ # however, future requests should still use the original Gem::URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/307].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-307-temporary-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#307].
+ #
+ class HTTPTemporaryRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Permanent Redirect</tt> responses (status code 308).
+ #
+ # This and all future requests should be directed to the given Gem::URI.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/308].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-308-permanent-redirect].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#308].
+ #
+ class HTTPPermanentRedirect < HTTPRedirection
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Request</tt> responses (status code 400).
+ #
+ # The server cannot or will not process the request due to an apparent client error.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-400-bad-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#400].
+ #
+ class HTTPBadRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unauthorized</tt> responses (status code 401).
+ #
+ # Authentication is required, but either was not provided or failed.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-401-unauthorized].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#401].
+ #
+ class HTTPUnauthorized < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payment Required</tt> responses (status code 402).
+ #
+ # Reserved for future use.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-402-payment-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#402].
+ #
+ class HTTPPaymentRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Forbidden</tt> responses (status code 403).
+ #
+ # The request contained valid data and was understood by the server,
+ # but the server is refusing action.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#403].
+ #
+ class HTTPForbidden < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Found</tt> responses (status code 404).
+ #
+ # The requested resource could not be found but may be available in the future.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-404-not-found].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#404].
+ #
+ class HTTPNotFound < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Method Not Allowed</tt> responses (status code 405).
+ #
+ # The request method is not supported for the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-405-method-not-allowed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#405].
+ #
+ class HTTPMethodNotAllowed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Acceptable</tt> responses (status code 406).
+ #
+ # The requested resource is capable of generating only content
+ # that not acceptable according to the Accept headers sent in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-406-not-acceptable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#406].
+ #
+ class HTTPNotAcceptable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Proxy Authentication Required</tt> responses (status code 407).
+ #
+ # The client must first authenticate itself with the proxy.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-407-proxy-authentication-re].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#407].
+ #
+ class HTTPProxyAuthenticationRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Gem::Timeout</tt> responses (status code 408).
+ #
+ # The server timed out waiting for the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-408-request-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#408].
+ #
+ class HTTPRequestTimeout < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestTimeOut = HTTPRequestTimeout
+
+ # Response class for <tt>Conflict</tt> responses (status code 409).
+ #
+ # The request could not be processed because of conflict in the current state of the resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-409-conflict].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#409].
+ #
+ class HTTPConflict < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gone</tt> responses (status code 410).
+ #
+ # The resource requested was previously in use but is no longer available
+ # and will not be available again.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-410-gone].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#410].
+ #
+ class HTTPGone < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Length Required</tt> responses (status code 411).
+ #
+ # The request did not specify the length of its content,
+ # which is required by the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-411-length-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#411].
+ #
+ class HTTPLengthRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Failed</tt> responses (status code 412).
+ #
+ # The server does not meet one of the preconditions
+ # specified in the request headers.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-412-precondition-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#412].
+ #
+ class HTTPPreconditionFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Payload Too Large</tt> responses (status code 413).
+ #
+ # The request is larger than the server is willing or able to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-413-content-too-large].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#413].
+ #
+ class HTTPPayloadTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestEntityTooLarge = HTTPPayloadTooLarge
+
+ # Response class for <tt>Gem::URI Too Long</tt> responses (status code 414).
+ #
+ # The Gem::URI provided was too long for the server to process.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-414-uri-too-long].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#414].
+ #
+ class HTTPURITooLong < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestURITooLong = HTTPURITooLong
+ HTTPRequestURITooLarge = HTTPRequestURITooLong
+
+ # Response class for <tt>Unsupported Media Type</tt> responses (status code 415).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-415-unsupported-media-type].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#415].
+ #
+ class HTTPUnsupportedMediaType < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Range Not Satisfiable</tt> responses (status code 416).
+ #
+ # The request entity has a media type which the server or resource does not support.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-416-range-not-satisfiable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#416].
+ #
+ class HTTPRangeNotSatisfiable < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPRequestedRangeNotSatisfiable = HTTPRangeNotSatisfiable
+
+ # Response class for <tt>Expectation Failed</tt> responses (status code 417).
+ #
+ # The server cannot meet the requirements of the Expect request-header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-417-expectation-failed].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#417].
+ #
+ class HTTPExpectationFailed < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 418 I'm a teapot - RFC 2324; a joke RFC
+ # See https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#418.
+
+ # 420 Enhance Your Calm - Twitter
+
+ # Response class for <tt>Misdirected Request</tt> responses (status code 421).
+ #
+ # The request was directed at a server that is not able to produce a response.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-421-misdirected-request].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#421].
+ #
+ class HTTPMisdirectedRequest < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unprocessable Entity</tt> responses (status code 422).
+ #
+ # The request was well-formed but had semantic errors.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-422-unprocessable-content].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#422].
+ #
+ class HTTPUnprocessableEntity < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Locked (WebDAV)</tt> responses (status code 423).
+ #
+ # The requested resource is locked.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#423].
+ #
+ class HTTPLocked < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Failed Dependency (WebDAV)</tt> responses (status code 424).
+ #
+ # The request failed because it depended on another request and that request failed.
+ # See {424 Failed Dependency (WebDAV)}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#424].
+ #
+ class HTTPFailedDependency < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # 425 Too Early
+ # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#425.
+
+ # Response class for <tt>Upgrade Required</tt> responses (status code 426).
+ #
+ # The client should switch to the protocol given in the Upgrade header field.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-426-upgrade-required].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#426].
+ #
+ class HTTPUpgradeRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Precondition Required</tt> responses (status code 428).
+ #
+ # The origin server requires the request to be conditional.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#428].
+ #
+ class HTTPPreconditionRequired < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Too Many Requests</tt> responses (status code 429).
+ #
+ # The user has sent too many requests in a given amount of time.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-4].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#429].
+ #
+ class HTTPTooManyRequests < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Request Header Fields Too Large</tt> responses (status code 431).
+ #
+ # An individual header field is too large,
+ # or all the header fields collectively, are too large.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#431].
+ #
+ class HTTPRequestHeaderFieldsTooLarge < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Unavailable For Legal Reasons</tt> responses (status code 451).
+ #
+ # A server operator has received a legal demand to deny access to a resource or to a set of resources
+ # that includes the requested resource.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451].
+ # - {RFC 7725}[https://www.rfc-editor.org/rfc/rfc7725.html#section-3].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#451].
+ #
+ class HTTPUnavailableForLegalReasons < HTTPClientError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 444 No Response - Nginx
+ # 449 Retry With - Microsoft
+ # 450 Blocked by Windows Parental Controls - Microsoft
+ # 499 Client Closed Request - Nginx
+
+ # Response class for <tt>Internal Server Error</tt> responses (status code 500).
+ #
+ # An unexpected condition was encountered and no more specific message is suitable.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-500-internal-server-error].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#500].
+ #
+ class HTTPInternalServerError < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Not Implemented</tt> responses (status code 501).
+ #
+ # The server either does not recognize the request method,
+ # or it lacks the ability to fulfil the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-501-not-implemented].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#501].
+ #
+ class HTTPNotImplemented < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Bad Gateway</tt> responses (status code 502).
+ #
+ # The server was acting as a gateway or proxy
+ # and received an invalid response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-502-bad-gateway].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#502].
+ #
+ class HTTPBadGateway < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Service Unavailable</tt> responses (status code 503).
+ #
+ # The server cannot handle the request
+ # (because it is overloaded or down for maintenance).
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-503-service-unavailable].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#503].
+ #
+ class HTTPServiceUnavailable < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Gateway Gem::Timeout</tt> responses (status code 504).
+ #
+ # The server was acting as a gateway or proxy
+ # and did not receive a timely response from the upstream server.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-504-gateway-timeout].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#504].
+ #
+ class HTTPGatewayTimeout < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ HTTPGatewayTimeOut = HTTPGatewayTimeout
+
+ # Response class for <tt>HTTP Version Not Supported</tt> responses (status code 505).
+ #
+ # The server does not support the HTTP version used in the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505].
+ # - {RFC 9110}[https://www.rfc-editor.org/rfc/rfc9110.html#name-505-http-version-not-suppor].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#505].
+ #
+ class HTTPVersionNotSupported < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Variant Also Negotiates</tt> responses (status code 506).
+ #
+ # Transparent content negotiation for the request results in a circular reference.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506].
+ # - {RFC 2295}[https://www.rfc-editor.org/rfc/rfc2295#section-8.1].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#506].
+ #
+ class HTTPVariantAlsoNegotiates < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Insufficient Storage (WebDAV)</tt> responses (status code 507).
+ #
+ # The server is unable to store the representation needed to complete the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507].
+ # - {RFC 4918}[https://www.rfc-editor.org/rfc/rfc4918#section-11.5].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#507].
+ #
+ class HTTPInsufficientStorage < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Loop Detected (WebDAV)</tt> responses (status code 508).
+ #
+ # The server detected an infinite loop while processing the request.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/508].
+ # - {RFC 5942}[https://www.rfc-editor.org/rfc/rfc5842.html#section-7.2].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#508].
+ #
+ class HTTPLoopDetected < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+ # 509 Bandwidth Limit Exceeded - Apache bw/limited extension
+
+ # Response class for <tt>Not Extended</tt> responses (status code 510).
+ #
+ # Further extensions to the request are required for the server to fulfill it.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/510].
+ # - {RFC 2774}[https://www.rfc-editor.org/rfc/rfc2774.html#section-7].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#510].
+ #
+ class HTTPNotExtended < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+ # Response class for <tt>Network Authentication Required</tt> responses (status code 511).
+ #
+ # The client needs to authenticate to gain network access.
+ #
+ # :include: doc/net-http/included_getters.rdoc
+ #
+ # References:
+ #
+ # - {Mozilla}[https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511].
+ # - {RFC 6585}[https://www.rfc-editor.org/rfc/rfc6585#section-6].
+ # - {Wikipedia}[https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#511].
+ #
+ class HTTPNetworkAuthenticationRequired < HTTPServerError
+ # :stopdoc:
+ HAS_BODY = true
+ end
+
+end
+
+class Gem::Net::HTTPResponse
+ # :stopdoc:
+ CODE_CLASS_TO_OBJ = {
+ '1' => Gem::Net::HTTPInformation,
+ '2' => Gem::Net::HTTPSuccess,
+ '3' => Gem::Net::HTTPRedirection,
+ '4' => Gem::Net::HTTPClientError,
+ '5' => Gem::Net::HTTPServerError
+ }.freeze
+ CODE_TO_OBJ = {
+ '100' => Gem::Net::HTTPContinue,
+ '101' => Gem::Net::HTTPSwitchProtocol,
+ '102' => Gem::Net::HTTPProcessing,
+ '103' => Gem::Net::HTTPEarlyHints,
+
+ '200' => Gem::Net::HTTPOK,
+ '201' => Gem::Net::HTTPCreated,
+ '202' => Gem::Net::HTTPAccepted,
+ '203' => Gem::Net::HTTPNonAuthoritativeInformation,
+ '204' => Gem::Net::HTTPNoContent,
+ '205' => Gem::Net::HTTPResetContent,
+ '206' => Gem::Net::HTTPPartialContent,
+ '207' => Gem::Net::HTTPMultiStatus,
+ '208' => Gem::Net::HTTPAlreadyReported,
+ '226' => Gem::Net::HTTPIMUsed,
+
+ '300' => Gem::Net::HTTPMultipleChoices,
+ '301' => Gem::Net::HTTPMovedPermanently,
+ '302' => Gem::Net::HTTPFound,
+ '303' => Gem::Net::HTTPSeeOther,
+ '304' => Gem::Net::HTTPNotModified,
+ '305' => Gem::Net::HTTPUseProxy,
+ '307' => Gem::Net::HTTPTemporaryRedirect,
+ '308' => Gem::Net::HTTPPermanentRedirect,
+
+ '400' => Gem::Net::HTTPBadRequest,
+ '401' => Gem::Net::HTTPUnauthorized,
+ '402' => Gem::Net::HTTPPaymentRequired,
+ '403' => Gem::Net::HTTPForbidden,
+ '404' => Gem::Net::HTTPNotFound,
+ '405' => Gem::Net::HTTPMethodNotAllowed,
+ '406' => Gem::Net::HTTPNotAcceptable,
+ '407' => Gem::Net::HTTPProxyAuthenticationRequired,
+ '408' => Gem::Net::HTTPRequestTimeout,
+ '409' => Gem::Net::HTTPConflict,
+ '410' => Gem::Net::HTTPGone,
+ '411' => Gem::Net::HTTPLengthRequired,
+ '412' => Gem::Net::HTTPPreconditionFailed,
+ '413' => Gem::Net::HTTPPayloadTooLarge,
+ '414' => Gem::Net::HTTPURITooLong,
+ '415' => Gem::Net::HTTPUnsupportedMediaType,
+ '416' => Gem::Net::HTTPRangeNotSatisfiable,
+ '417' => Gem::Net::HTTPExpectationFailed,
+ '421' => Gem::Net::HTTPMisdirectedRequest,
+ '422' => Gem::Net::HTTPUnprocessableEntity,
+ '423' => Gem::Net::HTTPLocked,
+ '424' => Gem::Net::HTTPFailedDependency,
+ '426' => Gem::Net::HTTPUpgradeRequired,
+ '428' => Gem::Net::HTTPPreconditionRequired,
+ '429' => Gem::Net::HTTPTooManyRequests,
+ '431' => Gem::Net::HTTPRequestHeaderFieldsTooLarge,
+ '451' => Gem::Net::HTTPUnavailableForLegalReasons,
+
+ '500' => Gem::Net::HTTPInternalServerError,
+ '501' => Gem::Net::HTTPNotImplemented,
+ '502' => Gem::Net::HTTPBadGateway,
+ '503' => Gem::Net::HTTPServiceUnavailable,
+ '504' => Gem::Net::HTTPGatewayTimeout,
+ '505' => Gem::Net::HTTPVersionNotSupported,
+ '506' => Gem::Net::HTTPVariantAlsoNegotiates,
+ '507' => Gem::Net::HTTPInsufficientStorage,
+ '508' => Gem::Net::HTTPLoopDetected,
+ '510' => Gem::Net::HTTPNotExtended,
+ '511' => Gem::Net::HTTPNetworkAuthenticationRequired,
+ }.freeze
+end
diff --git a/lib/rubygems/vendor/net-http/lib/net/http/status.rb b/lib/rubygems/vendor/net-http/lib/net/http/status.rb
new file mode 100644
index 0000000000..9110b108b8
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/http/status.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative '../http'
+
+if $0 == __FILE__
+ require 'open-uri'
+ File.foreach(__FILE__) do |line|
+ puts line
+ break if line.start_with?('end')
+ end
+ puts
+ puts "Gem::Net::HTTP::STATUS_CODES = {"
+ url = "https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv"
+ Gem::URI(url).read.each_line do |line|
+ code, mes, = line.split(',')
+ next if ['(Unused)', 'Unassigned', 'Description'].include?(mes)
+ puts " #{code} => '#{mes}',"
+ end
+ puts "} # :nodoc:"
+end
+
+Gem::Net::HTTP::STATUS_CODES = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+ 103 => 'Early Hints',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 208 => 'Already Reported',
+ 226 => 'IM Used',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 308 => 'Permanent Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Content Too Large',
+ 414 => 'URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 421 => 'Misdirected Request',
+ 422 => 'Unprocessable Content',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 425 => 'Too Early',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 451 => 'Unavailable For Legal Reasons',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 508 => 'Loop Detected',
+ 510 => 'Not Extended (OBSOLETED)',
+ 511 => 'Network Authentication Required',
+} # :nodoc:
diff --git a/lib/rubygems/vendor/net-http/lib/net/https.rb b/lib/rubygems/vendor/net-http/lib/net/https.rb
new file mode 100644
index 0000000000..f104c85c81
--- /dev/null
+++ b/lib/rubygems/vendor/net-http/lib/net/https.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+=begin
+
+= net/https -- SSL/TLS enhancement for Gem::Net::HTTP.
+
+ This file has been merged with net/http. There is no longer any need to
+ require_relative 'https' to use HTTPS.
+
+ See Gem::Net::HTTP for details on how to make HTTPS connections.
+
+== Info
+ 'OpenSSL for Ruby 2' project
+ Copyright (C) 2001 GOTOU Yuuzou <gotoyuzo@notwork.org>
+ All rights reserved.
+
+== Licence
+ This program is licensed under the same licence as Ruby.
+ (See the file 'LICENCE'.)
+
+=end
+
+require_relative 'http'
+require 'openssl'
diff --git a/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb
new file mode 100644
index 0000000000..53d34d8d98
--- /dev/null
+++ b/lib/rubygems/vendor/net-protocol/lib/net/protocol.rb
@@ -0,0 +1,544 @@
+# frozen_string_literal: true
+#
+# = net/protocol.rb
+#
+#--
+# Copyright (c) 1999-2004 Yukihiro Matsumoto
+# Copyright (c) 1999-2004 Minero Aoki
+#
+# written and maintained by Minero Aoki <aamine@loveruby.net>
+#
+# This program is free software. You can re-distribute and/or
+# modify this program under the same terms as Ruby itself,
+# Ruby Distribute License or GNU General Public License.
+#
+# $Id$
+#++
+#
+# WARNING: This file is going to remove.
+# Do not rely on the implementation written in this file.
+#
+
+require 'socket'
+require_relative '../../../timeout/lib/timeout'
+require 'io/wait'
+
+module Gem::Net # :nodoc:
+
+ class Protocol #:nodoc: internal use only
+ VERSION = "0.2.2"
+
+ private
+ def Protocol.protocol_param(name, val)
+ module_eval(<<-End, __FILE__, __LINE__ + 1)
+ def #{name}
+ #{val}
+ end
+ End
+ end
+
+ def ssl_socket_connect(s, timeout)
+ if timeout
+ while true
+ raise Gem::Net::OpenTimeout if timeout <= 0
+ start = Process.clock_gettime Process::CLOCK_MONOTONIC
+ # to_io is required because SSLSocket doesn't have wait_readable yet
+ case s.connect_nonblock(exception: false)
+ when :wait_readable; s.to_io.wait_readable(timeout)
+ when :wait_writable; s.to_io.wait_writable(timeout)
+ else; break
+ end
+ timeout -= Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
+ end
+ else
+ s.connect
+ end
+ end
+ end
+
+
+ class ProtocolError < StandardError; end
+ class ProtoSyntaxError < ProtocolError; end
+ class ProtoFatalError < ProtocolError; end
+ class ProtoUnknownError < ProtocolError; end
+ class ProtoServerError < ProtocolError; end
+ class ProtoAuthError < ProtocolError; end
+ class ProtoCommandError < ProtocolError; end
+ class ProtoRetriableError < ProtocolError; end
+ ProtocRetryError = ProtoRetriableError
+
+ ##
+ # OpenTimeout, a subclass of Gem::Timeout::Error, is raised if a connection cannot
+ # be created within the open_timeout.
+
+ class OpenTimeout < Gem::Timeout::Error; end
+
+ ##
+ # ReadTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
+ # response cannot be read within the read_timeout.
+
+ class ReadTimeout < Gem::Timeout::Error
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
+
+ ##
+ # WriteTimeout, a subclass of Gem::Timeout::Error, is raised if a chunk of the
+ # response cannot be written within the write_timeout. Not raised on Windows.
+
+ class WriteTimeout < Gem::Timeout::Error
+ def initialize(io = nil)
+ @io = io
+ end
+ attr_reader :io
+
+ def message
+ msg = super
+ if @io
+ msg = "#{msg} with #{@io.inspect}"
+ end
+ msg
+ end
+ end
+
+
+ class BufferedIO #:nodoc: internal use only
+ def initialize(io, read_timeout: 60, write_timeout: 60, continue_timeout: nil, debug_output: nil)
+ @io = io
+ @read_timeout = read_timeout
+ @write_timeout = write_timeout
+ @continue_timeout = continue_timeout
+ @debug_output = debug_output
+ @rbuf = ''.b
+ @rbuf_empty = true
+ @rbuf_offset = 0
+ end
+
+ attr_reader :io
+ attr_accessor :read_timeout
+ attr_accessor :write_timeout
+ attr_accessor :continue_timeout
+ attr_accessor :debug_output
+
+ def inspect
+ "#<#{self.class} io=#{@io}>"
+ end
+
+ def eof?
+ @io.eof?
+ end
+
+ def closed?
+ @io.closed?
+ end
+
+ def close
+ @io.close
+ end
+
+ #
+ # Read
+ #
+
+ public
+
+ def read(len, dest = ''.b, ignore_eof = false)
+ LOG "reading #{len} bytes..."
+ read_bytes = 0
+ begin
+ while read_bytes + rbuf_size < len
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
+ rbuf_fill
+ end
+ s = rbuf_consume(len - read_bytes)
+ read_bytes += s.bytesize
+ dest << s
+ rescue EOFError
+ raise unless ignore_eof
+ end
+ LOG "read #{read_bytes} bytes"
+ dest
+ end
+
+ def read_all(dest = ''.b)
+ LOG 'reading all...'
+ read_bytes = 0
+ begin
+ while true
+ if s = rbuf_consume_all
+ read_bytes += s.bytesize
+ dest << s
+ end
+ rbuf_fill
+ end
+ rescue EOFError
+ ;
+ end
+ LOG "read #{read_bytes} bytes"
+ dest
+ end
+
+ def readuntil(terminator, ignore_eof = false)
+ offset = @rbuf_offset
+ begin
+ until idx = @rbuf.index(terminator, offset)
+ offset = @rbuf.bytesize
+ rbuf_fill
+ end
+ return rbuf_consume(idx + terminator.bytesize - @rbuf_offset)
+ rescue EOFError
+ raise unless ignore_eof
+ return rbuf_consume
+ end
+ end
+
+ def readline
+ readuntil("\n").chop
+ end
+
+ private
+
+ BUFSIZE = 1024 * 16
+
+ def rbuf_fill
+ tmp = @rbuf_empty ? @rbuf : nil
+ case rv = @io.read_nonblock(BUFSIZE, tmp, exception: false)
+ when String
+ @rbuf_empty = false
+ if rv.equal?(tmp)
+ @rbuf_offset = 0
+ else
+ @rbuf << rv
+ rv.clear
+ end
+ return
+ when :wait_readable
+ (io = @io.to_io).wait_readable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io)
+ # continue looping
+ when :wait_writable
+ # OpenSSL::Buffering#read_nonblock may fail with IO::WaitWritable.
+ # http://www.openssl.org/support/faq.html#PROG10
+ (io = @io.to_io).wait_writable(@read_timeout) or raise Gem::Net::ReadTimeout.new(io)
+ # continue looping
+ when nil
+ raise EOFError, 'end of file reached'
+ end while true
+ end
+
+ def rbuf_flush
+ if @rbuf_empty
+ @rbuf.clear
+ @rbuf_offset = 0
+ end
+ nil
+ end
+
+ def rbuf_size
+ @rbuf.bytesize - @rbuf_offset
+ end
+
+ def rbuf_consume_all
+ rbuf_consume if rbuf_size > 0
+ end
+
+ def rbuf_consume(len = nil)
+ if @rbuf_offset == 0 && (len.nil? || len == @rbuf.bytesize)
+ s = @rbuf
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ elsif len.nil?
+ s = @rbuf.byteslice(@rbuf_offset..-1)
+ @rbuf = ''.b
+ @rbuf_offset = 0
+ @rbuf_empty = true
+ else
+ s = @rbuf.byteslice(@rbuf_offset, len)
+ @rbuf_offset += len
+ @rbuf_empty = @rbuf_offset == @rbuf.bytesize
+ rbuf_flush
+ end
+
+ @debug_output << %Q[-> #{s.dump}\n] if @debug_output
+ s
+ end
+
+ #
+ # Write
+ #
+
+ public
+
+ def write(*strs)
+ writing {
+ write0(*strs)
+ }
+ end
+
+ alias << write
+
+ def writeline(str)
+ writing {
+ write0 str + "\r\n"
+ }
+ end
+
+ private
+
+ def writing
+ @written_bytes = 0
+ @debug_output << '<- ' if @debug_output
+ yield
+ @debug_output << "\n" if @debug_output
+ bytes = @written_bytes
+ @written_bytes = nil
+ bytes
+ end
+
+ def write0(*strs)
+ @debug_output << strs.map(&:dump).join if @debug_output
+ orig_written_bytes = @written_bytes
+ strs.each_with_index do |str, i|
+ need_retry = true
+ case len = @io.write_nonblock(str, exception: false)
+ when Integer
+ @written_bytes += len
+ len -= str.bytesize
+ if len == 0
+ if strs.size == i+1
+ return @written_bytes - orig_written_bytes
+ else
+ need_retry = false
+ # next string
+ end
+ elsif len < 0
+ str = str.byteslice(len, -len)
+ else # len > 0
+ need_retry = false
+ # next string
+ end
+ # continue looping
+ when :wait_writable
+ (io = @io.to_io).wait_writable(@write_timeout) or raise Gem::Net::WriteTimeout.new(io)
+ # continue looping
+ end while need_retry
+ end
+ end
+
+ #
+ # Logging
+ #
+
+ private
+
+ def LOG_off
+ @save_debug_out = @debug_output
+ @debug_output = nil
+ end
+
+ def LOG_on
+ @debug_output = @save_debug_out
+ end
+
+ def LOG(msg)
+ return unless @debug_output
+ @debug_output << msg + "\n"
+ end
+ end
+
+
+ class InternetMessageIO < BufferedIO #:nodoc: internal use only
+ def initialize(*, **)
+ super
+ @wbuf = nil
+ end
+
+ #
+ # Read
+ #
+
+ def each_message_chunk
+ LOG 'reading message...'
+ LOG_off()
+ read_bytes = 0
+ while (line = readuntil("\r\n")) != ".\r\n"
+ read_bytes += line.size
+ yield line.delete_prefix('.')
+ end
+ LOG_on()
+ LOG "read message (#{read_bytes} bytes)"
+ end
+
+ # *library private* (cannot handle 'break')
+ def each_list_item
+ while (str = readuntil("\r\n")) != ".\r\n"
+ yield str.chop
+ end
+ end
+
+ def write_message_0(src)
+ prev = @written_bytes
+ each_crlf_line(src) do |line|
+ write0 dot_stuff(line)
+ end
+ @written_bytes - prev
+ end
+
+ #
+ # Write
+ #
+
+ def write_message(src)
+ LOG "writing message from #{src.class}"
+ LOG_off()
+ len = writing {
+ using_each_crlf_line {
+ write_message_0 src
+ }
+ }
+ LOG_on()
+ LOG "wrote #{len} bytes"
+ len
+ end
+
+ def write_message_by_block(&block)
+ LOG 'writing message from block'
+ LOG_off()
+ len = writing {
+ using_each_crlf_line {
+ begin
+ block.call(WriteAdapter.new(self.method(:write_message_0)))
+ rescue LocalJumpError
+ # allow `break' from writer block
+ end
+ }
+ }
+ LOG_on()
+ LOG "wrote #{len} bytes"
+ len
+ end
+
+ private
+
+ def dot_stuff(s)
+ s.sub(/\A\./, '..')
+ end
+
+ def using_each_crlf_line
+ @wbuf = ''.b
+ yield
+ if not @wbuf.empty? # unterminated last line
+ write0 dot_stuff(@wbuf.chomp) + "\r\n"
+ elsif @written_bytes == 0 # empty src
+ write0 "\r\n"
+ end
+ write0 ".\r\n"
+ @wbuf = nil
+ end
+
+ def each_crlf_line(src)
+ buffer_filling(@wbuf, src) do
+ while line = @wbuf.slice!(/\A[^\r\n]*(?:\n|\r(?:\n|(?!\z)))/)
+ yield line.chomp("\n") + "\r\n"
+ end
+ end
+ end
+
+ def buffer_filling(buf, src)
+ case src
+ when String # for speeding up.
+ 0.step(src.size - 1, 1024) do |i|
+ buf << src[i, 1024]
+ yield
+ end
+ when File # for speeding up.
+ while s = src.read(1024)
+ buf << s
+ yield
+ end
+ else # generic reader
+ src.each do |str|
+ buf << str
+ yield if buf.size > 1024
+ end
+ yield unless buf.empty?
+ end
+ end
+ end
+
+
+ #
+ # The writer adapter class
+ #
+ class WriteAdapter
+ def initialize(writer)
+ @writer = writer
+ end
+
+ def inspect
+ "#<#{self.class} writer=#{@writer.inspect}>"
+ end
+
+ def write(str)
+ @writer.call(str)
+ end
+
+ alias print write
+
+ def <<(str)
+ write str
+ self
+ end
+
+ def puts(str = '')
+ write str.chomp("\n") + "\n"
+ end
+
+ def printf(*args)
+ write sprintf(*args)
+ end
+ end
+
+
+ class ReadAdapter #:nodoc: internal use only
+ def initialize(block)
+ @block = block
+ end
+
+ def inspect
+ "#<#{self.class}>"
+ end
+
+ def <<(str)
+ call_block(str, &@block) if @block
+ end
+
+ private
+
+ # This method is needed because @block must be called by yield,
+ # not Proc#call. You can see difference when using `break' in
+ # the block.
+ def call_block(str)
+ yield str
+ end
+ end
+
+
+ module NetPrivate #:nodoc: obsolete
+ Socket = ::Gem::Net::InternetMessageIO
+ end
+
+end # module Gem::Net
diff --git a/lib/rubygems/vendor/optparse/lib/optionparser.rb b/lib/rubygems/vendor/optparse/lib/optionparser.rb
new file mode 100644
index 0000000000..4b9b40d82a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optionparser.rb
@@ -0,0 +1,2 @@
+# frozen_string_literal: false
+require_relative 'optparse'
diff --git a/lib/rubygems/vendor/optparse/lib/optparse.rb b/lib/rubygems/vendor/optparse/lib/optparse.rb
new file mode 100644
index 0000000000..d39d9dd4e0
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse.rb
@@ -0,0 +1,2467 @@
+# frozen_string_literal: true
+#
+# optparse.rb - command-line option analysis with the Gem::OptionParser class.
+#
+# Author:: Nobu Nakada
+# Documentation:: Nobu Nakada and Gavin Sinclair.
+#
+# See Gem::OptionParser for documentation.
+#
+require 'set' unless defined?(Set)
+
+#--
+# == Developer Documentation (not for RDoc output)
+#
+# === Class tree
+#
+# - Gem::OptionParser:: front end
+# - Gem::OptionParser::Switch:: each switches
+# - Gem::OptionParser::List:: options list
+# - Gem::OptionParser::ParseError:: errors on parsing
+# - Gem::OptionParser::AmbiguousOption
+# - Gem::OptionParser::NeedlessArgument
+# - Gem::OptionParser::MissingArgument
+# - Gem::OptionParser::InvalidOption
+# - Gem::OptionParser::InvalidArgument
+# - Gem::OptionParser::AmbiguousArgument
+#
+# === Object relationship diagram
+#
+# +--------------+
+# | Gem::OptionParser |<>-----+
+# +--------------+ | +--------+
+# | ,-| Switch |
+# on_head -------->+---------------+ / +--------+
+# accept/reject -->| List |<|>-
+# | |<|>- +----------+
+# on ------------->+---------------+ `-| argument |
+# : : | class |
+# +---------------+ |==========|
+# on_tail -------->| | |pattern |
+# +---------------+ |----------|
+# Gem::OptionParser.accept ->| DefaultList | |converter |
+# reject |(shared between| +----------+
+# | all instances)|
+# +---------------+
+#
+#++
+#
+# == Gem::OptionParser
+#
+# === New to +Gem::OptionParser+?
+#
+# See the {Tutorial}[optparse/tutorial.rdoc].
+#
+# === Introduction
+#
+# Gem::OptionParser is a class for command-line option analysis. It is much more
+# advanced, yet also easier to use, than GetoptLong, and is a more Ruby-oriented
+# solution.
+#
+# === Features
+#
+# 1. The argument specification and the code to handle it are written in the
+# same place.
+# 2. It can output an option summary; you don't need to maintain this string
+# separately.
+# 3. Optional and mandatory arguments are specified very gracefully.
+# 4. Arguments can be automatically converted to a specified class.
+# 5. Arguments can be restricted to a certain set.
+#
+# All of these features are demonstrated in the examples below. See
+# #make_switch for full documentation.
+#
+# === Minimal example
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
+#
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# options[:verbose] = v
+# end
+# end.parse!
+#
+# p options
+# p ARGV
+#
+# === Generating Help
+#
+# Gem::OptionParser can be used to automatically generate help for the commands you
+# write:
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# Options = Struct.new(:name)
+#
+# class Parser
+# def self.parse(options)
+# args = Options.new("world")
+#
+# opt_parser = Gem::OptionParser.new do |parser|
+# parser.banner = "Usage: example.rb [options]"
+#
+# parser.on("-nNAME", "--name=NAME", "Name to say hello to") do |n|
+# args.name = n
+# end
+#
+# parser.on("-h", "--help", "Prints this help") do
+# puts parser
+# exit
+# end
+# end
+#
+# opt_parser.parse!(options)
+# return args
+# end
+# end
+# options = Parser.parse %w[--help]
+#
+# #=>
+# # Usage: example.rb [options]
+# # -n, --name=NAME Name to say hello to
+# # -h, --help Prints this help
+#
+# === Required Arguments
+#
+# For options that require an argument, option specification strings may include an
+# option name in all caps. If an option is used without the required argument,
+# an exception will be raised.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.on("-r", "--require LIBRARY",
+# "Require the LIBRARY before executing your script") do |lib|
+# puts "You required #{lib}!"
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -r
+# optparse-test.rb:9:in '<main>': missing argument: -r (Gem::OptionParser::MissingArgument)
+# $ ruby optparse-test.rb -r my-library
+# You required my-library!
+#
+# === Type Coercion
+#
+# Gem::OptionParser supports the ability to coerce command line arguments
+# into objects for us.
+#
+# Gem::OptionParser comes with a few ready-to-use kinds of type
+# coercion. They are:
+#
+# - Date -- Anything accepted by +Date.parse+ (need to require +optparse/date+)
+# - DateTime -- Anything accepted by +DateTime.parse+ (need to require +optparse/date+)
+# - Time -- Anything accepted by +Time.httpdate+ or +Time.parse+ (need to require +optparse/time+)
+# - URI -- Anything accepted by +Gem::URI.parse+ (need to require +optparse/uri+)
+# - Shellwords -- Anything accepted by +Shellwords.shellwords+ (need to require +optparse/shellwords+)
+# - String -- Any non-empty string
+# - Integer -- Any integer. Will convert octal. (e.g. 124, -3, 040)
+# - Float -- Any float. (e.g. 10, 3.14, -100E+13)
+# - Numeric -- Any integer, float, or rational (1, 3.4, 1/3)
+# - DecimalInteger -- Like +Integer+, but no octal format.
+# - OctalInteger -- Like +Integer+, but no decimal format.
+# - DecimalNumeric -- Decimal integer or float.
+# - TrueClass -- Accepts '+, yes, true, -, no, false' and
+# defaults as +true+
+# - FalseClass -- Same as +TrueClass+, but defaults to +false+
+# - Array -- Strings separated by ',' (e.g. 1,2,3)
+# - Regexp -- Regular expressions. Also includes options.
+#
+# We can also add our own coercions, which we will cover below.
+#
+# ==== Using Built-in Conversions
+#
+# As an example, the built-in +Time+ conversion is used. The other built-in
+# conversions behave in the same way.
+# Gem::OptionParser will attempt to parse the argument
+# as a +Time+. If it succeeds, that time will be passed to the
+# handler block. Otherwise, an exception will be raised.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+# require 'rubygems/vendor/optparse/lib/optparse/time'
+# Gem::OptionParser.new do |parser|
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# p time
+# end
+# end.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb -t nonsense
+# ... invalid argument: -t nonsense (Gem::OptionParser::InvalidArgument)
+# $ ruby optparse-test.rb -t 10-11-12
+# 2010-11-12 00:00:00 -0500
+# $ ruby optparse-test.rb -t 9:30
+# 2014-08-13 09:30:00 -0400
+#
+# ==== Creating Custom Conversions
+#
+# The +accept+ method on Gem::OptionParser may be used to create converters.
+# It specifies which conversion block to call whenever a class is specified.
+# The example below uses it to fetch a +User+ object before the +on+ handler receives it.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# User = Struct.new(:id, :name)
+#
+# def find_user id
+# not_found = ->{ raise "No User Found for id #{id}" }
+# [ User.new(1, "Sam"),
+# User.new(2, "Gandalf") ].find(not_found) do |u|
+# u.id == id
+# end
+# end
+#
+# op = Gem::OptionParser.new
+# op.accept(User) do |user_id|
+# find_user user_id.to_i
+# end
+#
+# op.on("--user ID", User) do |user|
+# puts user
+# end
+#
+# op.parse!
+#
+# Used:
+#
+# $ ruby optparse-test.rb --user 1
+# #<struct User id=1, name="Sam">
+# $ ruby optparse-test.rb --user 2
+# #<struct User id=2, name="Gandalf">
+# $ ruby optparse-test.rb --user 3
+# optparse-test.rb:15:in 'block in find_user': No User Found for id 3 (RuntimeError)
+#
+# === Store options to a Hash
+#
+# The +into+ option of +order+, +parse+ and so on methods stores command line options into a Hash.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+#
+# options = {}
+# Gem::OptionParser.new do |parser|
+# parser.on('-a')
+# parser.on('-b NUM', Integer)
+# parser.on('-v', '--verbose')
+# end.parse!(into: options)
+#
+# p options
+#
+# Used:
+#
+# $ ruby optparse-test.rb -a
+# {:a=>true}
+# $ ruby optparse-test.rb -a -v
+# {:a=>true, :verbose=>true}
+# $ ruby optparse-test.rb -a -b 100
+# {:a=>true, :b=>100}
+#
+# === Complete example
+#
+# The following example is a complete Ruby program. You can run it and see the
+# effect of specifying various options. This is probably the best way to learn
+# the features of +optparse+.
+#
+# require 'rubygems/vendor/optparse/lib/optparse'
+# require 'rubygems/vendor/optparse/lib/optparse/time'
+# require 'ostruct'
+# require 'pp'
+#
+# class OptparseExample
+# Version = '1.0.0'
+#
+# CODES = %w[iso-2022-jp shift_jis euc-jp utf8 binary]
+# CODE_ALIASES = { "jis" => "iso-2022-jp", "sjis" => "shift_jis" }
+#
+# class ScriptOptions
+# attr_accessor :library, :inplace, :encoding, :transfer_type,
+# :verbose, :extension, :delay, :time, :record_separator,
+# :list
+#
+# def initialize
+# self.library = []
+# self.inplace = false
+# self.encoding = "utf8"
+# self.transfer_type = :auto
+# self.verbose = false
+# end
+#
+# def define_options(parser)
+# parser.banner = "Usage: example.rb [options]"
+# parser.separator ""
+# parser.separator "Specific options:"
+#
+# # add additional options
+# perform_inplace_option(parser)
+# delay_execution_option(parser)
+# execute_at_time_option(parser)
+# specify_record_separator_option(parser)
+# list_example_option(parser)
+# specify_encoding_option(parser)
+# optional_option_argument_with_keyword_completion_option(parser)
+# boolean_verbose_option(parser)
+#
+# parser.separator ""
+# parser.separator "Common options:"
+# # No argument, shows at tail. This will print an options summary.
+# # Try it and see!
+# parser.on_tail("-h", "--help", "Show this message") do
+# puts parser
+# exit
+# end
+# # Another typical switch to print the version.
+# parser.on_tail("--version", "Show version") do
+# puts Version
+# exit
+# end
+# end
+#
+# def perform_inplace_option(parser)
+# # Specifies an optional option argument
+# parser.on("-i", "--inplace [EXTENSION]",
+# "Edit ARGV files in place",
+# "(make backup if EXTENSION supplied)") do |ext|
+# self.inplace = true
+# self.extension = ext || ''
+# self.extension.sub!(/\A\.?(?=.)/, ".") # Ensure extension begins with dot.
+# end
+# end
+#
+# def delay_execution_option(parser)
+# # Cast 'delay' argument to a Float.
+# parser.on("--delay N", Float, "Delay N seconds before executing") do |n|
+# self.delay = n
+# end
+# end
+#
+# def execute_at_time_option(parser)
+# # Cast 'time' argument to a Time object.
+# parser.on("-t", "--time [TIME]", Time, "Begin execution at given time") do |time|
+# self.time = time
+# end
+# end
+#
+# def specify_record_separator_option(parser)
+# # Cast to octal integer.
+# parser.on("-F", "--irs [OCTAL]", Gem::OptionParser::OctalInteger,
+# "Specify record separator (default \\0)") do |rs|
+# self.record_separator = rs
+# end
+# end
+#
+# def list_example_option(parser)
+# # List of arguments.
+# parser.on("--list x,y,z", Array, "Example 'list' of arguments") do |list|
+# self.list = list
+# end
+# end
+#
+# def specify_encoding_option(parser)
+# # Keyword completion. We are specifying a specific set of arguments (CODES
+# # and CODE_ALIASES - notice the latter is a Hash), and the user may provide
+# # the shortest unambiguous text.
+# code_list = (CODE_ALIASES.keys + CODES).join(', ')
+# parser.on("--code CODE", CODES, CODE_ALIASES, "Select encoding",
+# "(#{code_list})") do |encoding|
+# self.encoding = encoding
+# end
+# end
+#
+# def optional_option_argument_with_keyword_completion_option(parser)
+# # Optional '--type' option argument with keyword completion.
+# parser.on("--type [TYPE]", [:text, :binary, :auto],
+# "Select transfer type (text, binary, auto)") do |t|
+# self.transfer_type = t
+# end
+# end
+#
+# def boolean_verbose_option(parser)
+# # Boolean switch.
+# parser.on("-v", "--[no-]verbose", "Run verbosely") do |v|
+# self.verbose = v
+# end
+# end
+# end
+#
+# #
+# # Return a structure describing the options.
+# #
+# def parse(args)
+# # The options specified on the command line will be collected in
+# # *options*.
+#
+# @options = ScriptOptions.new
+# @args = Gem::OptionParser.new do |parser|
+# @options.define_options(parser)
+# parser.parse!(args)
+# end
+# @options
+# end
+#
+# attr_reader :parser, :options
+# end # class OptparseExample
+#
+# example = OptparseExample.new
+# options = example.parse(ARGV)
+# pp options # example.options
+# pp ARGV
+#
+# === Shell Completion
+#
+# For modern shells (e.g. bash, zsh, etc.), you can use shell
+# completion for command line options.
+#
+# === Further documentation
+#
+# The above examples, along with the accompanying
+# {Tutorial}[optparse/tutorial.rdoc],
+# should be enough to learn how to use this class.
+# If you have any questions, file a ticket at http://bugs.ruby-lang.org.
+#
+class Gem::OptionParser
+ # The version string
+ VERSION = "0.8.0"
+ Version = VERSION # for compatibility
+
+ # :stopdoc:
+ NoArgument = [NO_ARGUMENT = :NONE, nil].freeze
+ RequiredArgument = [REQUIRED_ARGUMENT = :REQUIRED, true].freeze
+ OptionalArgument = [OPTIONAL_ARGUMENT = :OPTIONAL, false].freeze
+ # :startdoc:
+
+ #
+ # Keyword completion module. This allows partial arguments to be specified
+ # and resolved against a list of acceptable values.
+ #
+ module Completion
+ # :nodoc:
+
+ def self.regexp(key, icase)
+ Regexp.new('\A' + Regexp.quote(key).gsub(/\w+\b/, '\&\w*'), icase)
+ end
+
+ def self.candidate(key, icase = false, pat = nil, &block)
+ pat ||= Completion.regexp(key, icase)
+ candidates = []
+ block.call do |k, *v|
+ (if Regexp === k
+ kn = ""
+ k === key
+ else
+ kn = defined?(k.id2name) ? k.id2name : k
+ pat === kn
+ end) or next
+ v << k if v.empty?
+ candidates << [k, v, kn]
+ end
+ candidates
+ end
+
+ def self.completable?(key)
+ String.try_convert(key) or defined?(key.id2name)
+ end
+
+ def candidate(key, icase = false, pat = nil, &_)
+ Completion.candidate(key, icase, pat, &method(:each))
+ end
+
+ public
+ def complete(key, icase = false, pat = nil)
+ candidates = candidate(key, icase, pat, &method(:each)).sort_by {|k, v, kn| kn.size}
+ if candidates.size == 1
+ canon, sw, * = candidates[0]
+ elsif candidates.size > 1
+ canon, sw, cn = candidates.shift
+ candidates.each do |k, v, kn|
+ next if sw == v
+ if String === cn and String === kn
+ if cn.rindex(kn, 0)
+ canon, sw, cn = k, v, kn
+ next
+ elsif kn.rindex(cn, 0)
+ next
+ end
+ end
+ throw :ambiguous, key
+ end
+ end
+ if canon
+ block_given? or return key, *sw
+ yield(key, *sw)
+ end
+ end
+
+ def convert(opt = nil, val = nil, *)
+ val
+ end
+ end
+
+ #
+ # Map from option/keyword string to object with completion.
+ #
+ class OptionMap < Hash
+ include Completion
+ end
+
+ #
+ # Individual switch class. Not important to the user.
+ #
+ # Defined within Switch are several Switch-derived classes: NoArgument,
+ # RequiredArgument, etc.
+ #
+ class Switch
+ # :nodoc:
+
+ attr_reader :pattern, :conv, :short, :long, :arg, :desc, :block
+
+ #
+ # Guesses argument style from +arg+. Returns corresponding
+ # Gem::OptionParser::Switch class (OptionalArgument, etc.).
+ #
+ def self.guess(arg)
+ case arg
+ when ""
+ t = self
+ when /\A=?\[/
+ t = Switch::OptionalArgument
+ when /\A\s+\[/
+ t = Switch::PlacedArgument
+ else
+ t = Switch::RequiredArgument
+ end
+ self >= t or incompatible_argument_styles(arg, t)
+ t
+ end
+
+ def self.incompatible_argument_styles(arg, t)
+ raise(ArgumentError, "#{arg}: incompatible argument styles\n #{self}, #{t}",
+ ParseError.filter_backtrace(caller(2)))
+ end
+
+ def self.pattern
+ NilClass
+ end
+
+ def initialize(pattern = nil, conv = nil,
+ short = nil, long = nil, arg = nil,
+ desc = ([] if short or long), block = nil, values = nil, &_block)
+ raise if Array === pattern
+ block ||= _block
+ @pattern, @conv, @short, @long, @arg, @desc, @block, @values =
+ pattern, conv, short, long, arg, desc, block, values
+ end
+
+ #
+ # Parses +arg+ and returns rest of +arg+ and matched portion to the
+ # argument pattern. Yields when the pattern doesn't match substring.
+ #
+ def parse_arg(arg) # :nodoc:
+ pattern or return nil, [arg]
+ unless m = pattern.match(arg)
+ yield(InvalidArgument, arg)
+ return arg, []
+ end
+ if String === m
+ m = [s = m]
+ else
+ m = m.to_a
+ s = m[0]
+ return nil, m unless String === s
+ end
+ raise InvalidArgument, arg unless arg.rindex(s, 0)
+ return nil, m if s.length == arg.length
+ yield(InvalidArgument, arg) # didn't match whole arg
+ return arg[s.length..-1], m
+ end
+ private :parse_arg
+
+ #
+ # Parses argument, converts and returns +arg+, +block+ and result of
+ # conversion. Yields at semi-error condition instead of raising an
+ # exception.
+ #
+ def conv_arg(arg, val = []) # :nodoc:
+ v, = *val
+ if conv
+ val = conv.call(*val)
+ else
+ val = proc {|v| v}.call(*val)
+ end
+ if @values
+ @values.include?(val) or raise InvalidArgument, v
+ end
+ return arg, block, val
+ end
+ private :conv_arg
+
+ #
+ # Produces the summary text. Each line of the summary is yielded to the
+ # block (without newline).
+ #
+ # +sdone+:: Already summarized short style options keyed hash.
+ # +ldone+:: Already summarized long style options keyed hash.
+ # +width+:: Width of left side (option part). In other words, the right
+ # side (description part) starts after +width+ columns.
+ # +max+:: Maximum width of left side -> the options are filled within
+ # +max+ columns.
+ # +indent+:: Prefix string indents all summarized lines.
+ #
+ def summarize(sdone = {}, ldone = {}, width = 1, max = width - 1, indent = "")
+ sopts, lopts = [], [], nil
+ @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
+ @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
+ return if sopts.empty? and lopts.empty? # completely hidden
+
+ left = [sopts.join(', ')]
+ right = desc.dup
+
+ while s = lopts.shift
+ l = left[-1].length + s.length
+ l += arg.length if left.size == 1 && arg
+ l < max or sopts.empty? or left << +''
+ left[-1] << (left[-1].empty? ? ' ' * 4 : ', ') << s
+ end
+
+ if arg
+ left[0] << (left[1] ? arg.sub(/\A(\[?)=/, '\1') + ',' : arg)
+ end
+ mlen = left.collect {|ss| ss.length}.max.to_i
+ while mlen > width and l = left.shift
+ mlen = left.collect {|ss| ss.length}.max.to_i if l.length == mlen
+ if l.length < width and (r = right[0]) and !r.empty?
+ l = l.to_s.ljust(width) + ' ' + r
+ right.shift
+ end
+ yield(indent + l)
+ end
+
+ while begin l = left.shift; r = right.shift; l or r end
+ l = l.to_s.ljust(width) + ' ' + r if r and !r.empty?
+ yield(indent + l)
+ end
+
+ self
+ end
+
+ def add_banner(to) # :nodoc:
+ unless @short or @long
+ s = desc.join
+ to << " [" + s + "]..." unless s.empty?
+ end
+ to
+ end
+
+ def match_nonswitch?(str) # :nodoc:
+ @pattern =~ str unless @short or @long
+ end
+
+ #
+ # Main name of the switch.
+ #
+ def switch_name
+ (long.first || short.first).sub(/\A-+(?:\[no-\])?/, '')
+ end
+
+ def compsys(sdone, ldone) # :nodoc:
+ sopts, lopts = [], []
+ @short.each {|s| sdone.fetch(s) {sopts << s}; sdone[s] = true} if @short
+ @long.each {|s| ldone.fetch(s) {lopts << s}; ldone[s] = true} if @long
+ return if sopts.empty? and lopts.empty? # completely hidden
+
+ (sopts+lopts).each do |opt|
+ # "(-x -c -r)-l[left justify]"
+ if /\A--\[no-\](.+)$/ =~ opt
+ o = $1
+ yield("--#{o}", desc.join(""))
+ yield("--no-#{o}", desc.join(""))
+ else
+ yield("#{opt}", desc.join(""))
+ end
+ end
+ end
+
+ def pretty_print_contents(q) # :nodoc:
+ if @block
+ q.text ":" + @block.source_location.join(":") + ":"
+ first = false
+ else
+ first = true
+ end
+ [@short, @long].each do |list|
+ list.each do |opt|
+ if first
+ q.text ":"
+ first = false
+ end
+ q.breakable
+ q.text opt
+ end
+ end
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) {pretty_print_contents(q)}
+ end
+
+ def omitted_argument(val) # :nodoc:
+ val.pop if val.size == 3 and val.last.nil?
+ val
+ end
+
+ #
+ # Switch that takes no arguments.
+ #
+ class NoArgument < self
+
+ #
+ # Raises an exception if any arguments given.
+ #
+ def parse(arg, argv)
+ yield(NeedlessArgument, arg) if arg
+ conv_arg(arg)
+ end
+
+ def self.incompatible_argument_styles(*) # :nodoc:
+ end
+
+ def self.pattern # :nodoc:
+ Object
+ end
+
+ def pretty_head # :nodoc:
+ "NoArgument"
+ end
+ end
+
+ #
+ # Switch that takes an argument.
+ #
+ class RequiredArgument < self
+
+ #
+ # Raises an exception if argument is not present.
+ #
+ def parse(arg, argv, &_)
+ unless arg
+ raise MissingArgument if argv.empty?
+ arg = argv.shift
+ end
+ conv_arg(*parse_arg(arg, &method(:raise)))
+ end
+
+ def pretty_head # :nodoc:
+ "Required"
+ end
+ end
+
+ #
+ # Switch that can omit argument.
+ #
+ class OptionalArgument < self
+
+ #
+ # Parses argument if given, or uses default value.
+ #
+ def parse(arg, argv, &error)
+ if arg
+ conv_arg(*parse_arg(arg, &error))
+ else
+ omitted_argument conv_arg(arg)
+ end
+ end
+
+ def pretty_head # :nodoc:
+ "Optional"
+ end
+ end
+
+ #
+ # Switch that takes an argument, which does not begin with '-' or is '-'.
+ #
+ class PlacedArgument < self
+
+ #
+ # Returns nil if argument is not present or begins with '-' and is not '-'.
+ #
+ def parse(arg, argv, &error)
+ if !(val = arg) and (argv.empty? or /\A-./ =~ (val = argv[0]))
+ return nil, block
+ end
+ opt = (val = parse_arg(val, &error))[1]
+ val = conv_arg(*val)
+ if opt and !arg
+ argv.shift
+ else
+ omitted_argument val
+ val[0] = nil
+ end
+ val
+ end
+
+ def pretty_head # :nodoc:
+ "Placed"
+ end
+ end
+ end
+
+ #
+ # Simple option list providing mapping from short and/or long option
+ # string to Gem::OptionParser::Switch and mapping from acceptable argument to
+ # matching pattern and converter pair. Also provides summary feature.
+ #
+ class List
+ # :nodoc:
+
+ # Map from acceptable argument types to pattern and converter pairs.
+ attr_reader :atype
+
+ # Map from short style option switches to actual switch objects.
+ attr_reader :short
+
+ # Map from long style option switches to actual switch objects.
+ attr_reader :long
+
+ # List of all switches and summary string.
+ attr_reader :list
+
+ #
+ # Just initializes all instance variables.
+ #
+ def initialize
+ @atype = {}
+ @short = OptionMap.new
+ @long = OptionMap.new
+ @list = []
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.group(1, "(", ")") do
+ @list.each do |sw|
+ next unless Switch === sw
+ q.group(1, "(" + sw.pretty_head, ")") do
+ sw.pretty_print_contents(q)
+ end
+ end
+ end
+ end
+
+ #
+ # See Gem::OptionParser.accept.
+ #
+ def accept(t, pat = /.*/m, &block)
+ if pat
+ pat.respond_to?(:match) or
+ raise TypeError, "has no 'match'", ParseError.filter_backtrace(caller(2))
+ else
+ pat = t if t.respond_to?(:match)
+ end
+ unless block
+ block = pat.method(:convert).to_proc if pat.respond_to?(:convert)
+ end
+ @atype[t] = [pat, block]
+ end
+
+ #
+ # See Gem::OptionParser.reject.
+ #
+ def reject(t)
+ @atype.delete(t)
+ end
+
+ #
+ # Adds +sw+ according to +sopts+, +lopts+ and +nlopts+.
+ #
+ # +sw+:: Gem::OptionParser::Switch instance to be added.
+ # +sopts+:: Short style option list.
+ # +lopts+:: Long style option list.
+ # +nlopts+:: Negated long style options list.
+ #
+ def update(sw, sopts, lopts, nsw = nil, nlopts = nil) # :nodoc:
+ sopts.each {|o| @short[o] = sw} if sopts
+ lopts.each {|o| @long[o] = sw} if lopts
+ nlopts.each {|o| @long[o] = nsw} if nsw and nlopts
+ used = @short.invert.update(@long.invert)
+ @list.delete_if {|o| Switch === o and !used[o]}
+ end
+ private :update
+
+ #
+ # Inserts +switch+ at the head of the list, and associates short, long
+ # and negated long options. Arguments are:
+ #
+ # +switch+:: Gem::OptionParser::Switch instance to be inserted.
+ # +short_opts+:: List of short style options.
+ # +long_opts+:: List of long style options.
+ # +nolong_opts+:: List of long style options with "no-" prefix.
+ #
+ # prepend(switch, short_opts, long_opts, nolong_opts)
+ #
+ def prepend(*args)
+ update(*args)
+ @list.unshift(args[0])
+ end
+
+ #
+ # Appends +switch+ at the tail of the list, and associates short, long
+ # and negated long options. Arguments are:
+ #
+ # +switch+:: Gem::OptionParser::Switch instance to be inserted.
+ # +short_opts+:: List of short style options.
+ # +long_opts+:: List of long style options.
+ # +nolong_opts+:: List of long style options with "no-" prefix.
+ #
+ # append(switch, short_opts, long_opts, nolong_opts)
+ #
+ def append(*args)
+ update(*args)
+ @list.push(args[0])
+ end
+
+ #
+ # Searches +key+ in +id+ list. The result is returned or yielded if a
+ # block is given. If it isn't found, nil is returned.
+ #
+ def search(id, key)
+ if list = __send__(id)
+ val = list.fetch(key) {return nil}
+ block_given? ? yield(val) : val
+ end
+ end
+
+ #
+ # Searches list +id+ for +opt+ and the optional patterns for completion
+ # +pat+. If +icase+ is true, the search is case insensitive. The result
+ # is returned or yielded if a block is given. If it isn't found, nil is
+ # returned.
+ #
+ def complete(id, opt, icase = false, *pat, &block)
+ __send__(id).complete(opt, icase, *pat, &block)
+ end
+
+ def get_candidates(id)
+ yield __send__(id).keys
+ end
+
+ #
+ # Iterates over each option, passing the option to the +block+.
+ #
+ def each_option(&block)
+ list.each(&block)
+ end
+
+ #
+ # Creates the summary table, passing each line to the +block+ (without
+ # newline). The arguments +args+ are passed along to the summarize
+ # method which is called on every option.
+ #
+ def summarize(*args, &block)
+ sum = []
+ list.reverse_each do |opt|
+ if opt.respond_to?(:summarize) # perhaps Gem::OptionParser::Switch
+ s = []
+ opt.summarize(*args) {|l| s << l}
+ sum.concat(s.reverse)
+ elsif !opt or opt.empty?
+ sum << ""
+ elsif opt.respond_to?(:each_line)
+ sum.concat([*opt.each_line].reverse)
+ else
+ sum.concat([*opt.each].reverse)
+ end
+ end
+ sum.reverse_each(&block)
+ end
+
+ def add_banner(to) # :nodoc:
+ list.each do |opt|
+ if opt.respond_to?(:add_banner)
+ opt.add_banner(to)
+ end
+ end
+ to
+ end
+
+ def compsys(*args, &block) # :nodoc:
+ list.each do |opt|
+ if opt.respond_to?(:compsys)
+ opt.compsys(*args, &block)
+ end
+ end
+ end
+ end
+
+ #
+ # Hash with completion search feature. See Gem::OptionParser::Completion.
+ #
+ class CompletingHash < Hash
+ include Completion
+
+ #
+ # Completion for hash key.
+ #
+ def match(key)
+ *values = fetch(key) {
+ raise AmbiguousArgument, catch(:ambiguous) {return complete(key)}
+ }
+ return key, *values
+ end
+ end
+
+ # :stopdoc:
+
+ #
+ # Enumeration of acceptable argument styles. Possible values are:
+ #
+ # NO_ARGUMENT:: The switch takes no arguments. (:NONE)
+ # REQUIRED_ARGUMENT:: The switch requires an argument. (:REQUIRED)
+ # OPTIONAL_ARGUMENT:: The switch requires an optional argument. (:OPTIONAL)
+ #
+ # Use like --switch=argument (long style) or -Xargument (short style). For
+ # short style, only portion matched to argument pattern is treated as
+ # argument.
+ #
+ ArgumentStyle = {}
+ NoArgument.each {|el| ArgumentStyle[el] = Switch::NoArgument}
+ RequiredArgument.each {|el| ArgumentStyle[el] = Switch::RequiredArgument}
+ OptionalArgument.each {|el| ArgumentStyle[el] = Switch::OptionalArgument}
+ ArgumentStyle.freeze
+
+ #
+ # Switches common used such as '--', and also provides default
+ # argument classes
+ #
+ DefaultList = List.new
+ DefaultList.short['-'] = Switch::NoArgument.new {}
+ DefaultList.long[''] = Switch::NoArgument.new {throw :terminate}
+
+ COMPSYS_HEADER = <<'XXX' # :nodoc:
+
+typeset -A opt_args
+local context state line
+
+_arguments -s -S \
+XXX
+
+ def compsys(to, name = File.basename($0)) # :nodoc:
+ to << "#compdef #{name}\n"
+ to << COMPSYS_HEADER
+ visit(:compsys, {}, {}) {|o, d|
+ to << %Q[ "#{o}[#{d.gsub(/[\\\"\[\]]/, '\\\\\&')}]" \\\n]
+ }
+ to << " '*:file:_files' && return 0\n"
+ end
+
+ def help_exit
+ if $stdout.tty? && (pager = ENV.values_at(*%w[RUBY_PAGER PAGER]).find {|e| e && !e.empty?})
+ less = ENV["LESS"]
+ args = [{"LESS" => "#{less} -Fe"}, pager, "w"]
+ print = proc do |f|
+ f.puts help
+ rescue Errno::EPIPE
+ # pager terminated
+ end
+ if Process.respond_to?(:fork) and false
+ IO.popen("-") {|f| f ? Process.exec(*args, in: f) : print.call($stdout)}
+ # unreachable
+ end
+ IO.popen(*args, &print)
+ else
+ puts help
+ end
+ exit
+ end
+
+ #
+ # Default options for ARGV, which never appear in option summary.
+ #
+ Officious = {}
+
+ #
+ # --help
+ # Shows option summary.
+ #
+ Officious['help'] = proc do |parser|
+ Switch::NoArgument.new do |arg|
+ parser.help_exit
+ end
+ end
+
+ #
+ # --*-completion-bash=WORD
+ # Shows candidates for command line completion.
+ #
+ Officious['*-completion-bash'] = proc do |parser|
+ Switch::RequiredArgument.new do |arg|
+ puts parser.candidate(arg)
+ exit
+ end
+ end
+
+ #
+ # --*-completion-zsh[=NAME:FILE]
+ # Creates zsh completion file.
+ #
+ Officious['*-completion-zsh'] = proc do |parser|
+ Switch::OptionalArgument.new do |arg|
+ parser.compsys($stdout, arg)
+ exit
+ end
+ end
+
+ #
+ # --version
+ # Shows version string if Version is defined.
+ #
+ Officious['version'] = proc do |parser|
+ Switch::OptionalArgument.new do |pkg|
+ if pkg
+ begin
+ require_relative 'optparse/version'
+ rescue LoadError
+ else
+ show_version(*pkg.split(/,/)) or
+ abort("#{parser.program_name}: no version found in package #{pkg}")
+ exit
+ end
+ end
+ v = parser.ver or abort("#{parser.program_name}: version unknown")
+ puts v
+ exit
+ end
+ end
+
+ # :startdoc:
+
+ #
+ # Class methods
+ #
+
+ #
+ # Initializes a new instance and evaluates the optional block in context
+ # of the instance. Arguments +args+ are passed to #new, see there for
+ # description of parameters.
+ #
+ # This method is *deprecated*, its behavior corresponds to the older #new
+ # method.
+ #
+ def self.with(*args, &block)
+ opts = new(*args)
+ opts.instance_eval(&block)
+ opts
+ end
+
+ #
+ # Returns an incremented value of +default+ according to +arg+.
+ #
+ def self.inc(arg, default = nil)
+ case arg
+ when Integer
+ arg.nonzero?
+ when nil
+ default.to_i + 1
+ end
+ end
+
+ #
+ # See self.inc
+ #
+ def inc(*args)
+ self.class.inc(*args)
+ end
+
+ #
+ # Initializes the instance and yields itself if called with a block.
+ #
+ # +banner+:: Banner message.
+ # +width+:: Summary width.
+ # +indent+:: Summary indent.
+ #
+ def initialize(banner = nil, width = 32, indent = ' ' * 4)
+ @stack = [DefaultList, List.new, List.new]
+ @program_name = nil
+ @banner = banner
+ @summary_width = width
+ @summary_indent = indent
+ @default_argv = ARGV
+ @require_exact = false
+ @raise_unknown = true
+ add_officious
+ yield self if block_given?
+ end
+
+ def add_officious # :nodoc:
+ list = base()
+ Officious.each do |opt, block|
+ list.long[opt] ||= block.call(self)
+ end
+ end
+
+ #
+ # Terminates option parsing. Optional parameter +arg+ is a string pushed
+ # back to be the first non-option argument.
+ #
+ def terminate(arg = nil)
+ self.class.terminate(arg)
+ end
+ #
+ # See #terminate.
+ #
+ def self.terminate(arg = nil)
+ throw :terminate, arg
+ end
+
+ @stack = [DefaultList]
+ #
+ # Returns the global top option list.
+ #
+ # Do not use directly.
+ #
+ def self.top() DefaultList end
+
+ #
+ # Directs to accept specified class +t+. The argument string is passed to
+ # the block in which it should be converted to the desired class.
+ #
+ # +t+:: Argument class specifier, any object including Class.
+ # +pat+:: Pattern for argument, defaults to +t+ if it responds to match.
+ #
+ # accept(t, pat, &block)
+ #
+ def accept(*args, &blk) top.accept(*args, &blk) end
+ #
+ # See #accept.
+ #
+ def self.accept(*args, &blk) top.accept(*args, &blk) end
+
+ #
+ # Directs to reject specified class argument.
+ #
+ # +type+:: Argument class specifier, any object including Class.
+ #
+ # reject(type)
+ #
+ def reject(*args, &blk) top.reject(*args, &blk) end
+ #
+ # See #reject.
+ #
+ def self.reject(*args, &blk) top.reject(*args, &blk) end
+
+ #
+ # Instance methods
+ #
+
+ # Heading banner preceding summary.
+ attr_writer :banner
+
+ # Program name to be emitted in error message and default banner,
+ # defaults to $0.
+ attr_writer :program_name
+
+ # Width for option list portion of summary. Must be Numeric.
+ attr_accessor :summary_width
+
+ # Indentation for summary. Must be String (or have + String method).
+ attr_accessor :summary_indent
+
+ # Strings to be parsed in default.
+ attr_accessor :default_argv
+
+ # Whether to require that options match exactly (disallows providing
+ # abbreviated long option as short option).
+ attr_accessor :require_exact
+
+ # Whether to raise at unknown option.
+ attr_accessor :raise_unknown
+
+ #
+ # Heading banner preceding summary.
+ #
+ def banner
+ unless @banner
+ @banner = +"Usage: #{program_name} [options]"
+ visit(:add_banner, @banner)
+ end
+ @banner
+ end
+
+ #
+ # Program name to be emitted in error message and default banner, defaults
+ # to $0.
+ #
+ def program_name
+ @program_name || strip_ext(File.basename($0))
+ end
+
+ private def strip_ext(name) # :nodoc:
+ exts = /#{
+ require "rbconfig"
+ Regexp.union(*RbConfig::CONFIG["EXECUTABLE_EXTS"]&.split(" "))
+ }\z/o
+ name.sub(exts, "")
+ end
+
+ # for experimental cascading :-)
+ alias set_banner banner=
+ alias set_program_name program_name=
+ alias set_summary_width summary_width=
+ alias set_summary_indent summary_indent=
+
+ # Version
+ attr_writer :version
+ # Release code
+ attr_writer :release
+
+ #
+ # Version
+ #
+ def version
+ (defined?(@version) && @version) || (defined?(::Version) && ::Version)
+ end
+
+ #
+ # Release code
+ #
+ def release
+ (defined?(@release) && @release) || (defined?(::Release) && ::Release) || (defined?(::RELEASE) && ::RELEASE)
+ end
+
+ #
+ # Returns version string from program_name, version and release.
+ #
+ def ver
+ if v = version
+ str = +"#{program_name} #{[v].join('.')}"
+ str << " (#{v})" if v = release
+ str
+ end
+ end
+
+ #
+ # Shows warning message with the program name
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#warn.
+ #
+ def warn(mesg = $!)
+ super("#{program_name}: #{mesg}")
+ end
+
+ #
+ # Shows message with the program name then aborts.
+ #
+ # +mesg+:: Message, defaulted to +$!+.
+ #
+ # See Kernel#abort.
+ #
+ def abort(mesg = $!)
+ super("#{program_name}: #{mesg}")
+ end
+
+ #
+ # Subject of #on / #on_head, #accept / #reject
+ #
+ def top
+ @stack[-1]
+ end
+
+ #
+ # Subject of #on_tail.
+ #
+ def base
+ @stack[1]
+ end
+
+ #
+ # Pushes a new List.
+ #
+ # If a block is given, yields +self+ and returns the result of the
+ # block, otherwise returns +self+.
+ #
+ def new
+ @stack.push(List.new)
+ if block_given?
+ yield self
+ else
+ self
+ end
+ end
+
+ #
+ # Removes the last List.
+ #
+ def remove
+ @stack.pop
+ end
+
+ #
+ # Puts option summary into +to+ and returns +to+. Yields each line if
+ # a block is given.
+ #
+ # +to+:: Output destination, which must have method <<. Defaults to [].
+ # +width+:: Width of left side, defaults to @summary_width.
+ # +max+:: Maximum length allowed for left side, defaults to +width+ - 1.
+ # +indent+:: Indentation, defaults to @summary_indent.
+ #
+ def summarize(to = [], width = @summary_width, max = width - 1, indent = @summary_indent, &blk)
+ nl = "\n"
+ blk ||= proc {|l| to << (l.index(nl, -1) ? l : l + nl)}
+ visit(:summarize, {}, {}, width, max, indent, &blk)
+ to
+ end
+
+ #
+ # Returns option summary string.
+ #
+ def help; summarize("#{banner}".sub(/\n?\z/, "\n")) end
+ alias to_s help
+
+ def pretty_print(q) # :nodoc:
+ q.object_group(self) do
+ first = true
+ if @stack.size > 2
+ @stack.each_with_index do |s, i|
+ next if i < 2
+ next if s.list.empty?
+ if first
+ first = false
+ q.text ":"
+ end
+ q.breakable
+ s.pretty_print(q)
+ end
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ require 'pp'
+ pretty_print_inspect
+ end
+
+ #
+ # Returns option summary list.
+ #
+ def to_a; summarize("#{banner}".split(/^/)) end
+
+ #
+ # Checks if an argument is given twice, in which case an ArgumentError is
+ # raised. Called from Gem::OptionParser#switch only.
+ #
+ # +obj+:: New argument.
+ # +prv+:: Previously specified argument.
+ # +msg+:: Exception message.
+ #
+ def notwice(obj, prv, msg) # :nodoc:
+ unless !prv or prv == obj
+ raise(ArgumentError, "argument #{msg} given twice: #{obj}",
+ ParseError.filter_backtrace(caller(2)))
+ end
+ obj
+ end
+ private :notwice
+
+ SPLAT_PROC = proc {|*a| a.length <= 1 ? a.first : a} # :nodoc:
+
+ # :call-seq:
+ # make_switch(params, block = nil)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def make_switch(opts, block = nil)
+ short, long, nolong, style, pattern, conv, not_pattern, not_conv, not_style = [], [], []
+ ldesc, sdesc, desc, arg = [], [], []
+ default_style = Switch::NoArgument
+ default_pattern = nil
+ klass = nil
+ q, a = nil
+ has_arg = false
+ values = nil
+
+ opts.each do |o|
+ # argument class
+ next if search(:atype, o) do |pat, c|
+ klass = notwice(o, klass, 'type')
+ if not_style and not_style != Switch::NoArgument
+ not_pattern, not_conv = pat, c
+ else
+ default_pattern, conv = pat, c
+ end
+ end
+
+ # directly specified pattern(any object possible to match)
+ if !Completion.completable?(o) and o.respond_to?(:match)
+ pattern = notwice(o, pattern, 'pattern')
+ if pattern.respond_to?(:convert)
+ conv = pattern.method(:convert).to_proc
+ else
+ conv = SPLAT_PROC
+ end
+ next
+ end
+
+ # anything others
+ case o
+ when Proc, Method
+ block = notwice(o, block, 'block')
+ when Array, Hash, Set
+ if Array === o
+ o, v = o.partition {|v,| Completion.completable?(v)}
+ values = notwice(v, values, 'values') unless v.empty?
+ next if o.empty?
+ end
+ case pattern
+ when CompletingHash
+ when nil
+ pattern = CompletingHash.new
+ conv = pattern.method(:convert).to_proc if pattern.respond_to?(:convert)
+ else
+ raise ArgumentError, "argument pattern given twice"
+ end
+ o.each {|pat, *v| pattern[pat] = v.fetch(0) {pat}}
+ when Range
+ values = notwice(o, values, 'values')
+ when Module
+ raise ArgumentError, "unsupported argument type: #{o}", ParseError.filter_backtrace(caller(4))
+ when *ArgumentStyle.keys
+ style = notwice(ArgumentStyle[o], style, 'style')
+ when /\A--no-([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ o = notwice(a ? Object : TrueClass, klass, 'type')
+ not_pattern, not_conv = search(:atype, o) unless not_style
+ not_style = (not_style || default_style).guess(arg = a) if a
+ default_style = Switch::NoArgument
+ default_pattern, conv = search(:atype, FalseClass) unless default_pattern
+ ldesc << "--no-#{q}"
+ (q = q.downcase).tr!('_', '-')
+ long << "no-#{q}"
+ nolong << q
+ when /\A--\[no-\]([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ o = notwice(a ? Object : TrueClass, klass, 'type')
+ if a
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ ldesc << "--[no-]#{q}"
+ (o = q.downcase).tr!('_', '-')
+ long << o
+ not_pattern, not_conv = search(:atype, FalseClass) unless not_style
+ not_style = Switch::NoArgument
+ nolong << "no-#{o}"
+ when /\A--([^\[\]=\s]*)(.+)?/
+ q, a = $1, $2
+ if a
+ o = notwice(NilClass, klass, 'type')
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ ldesc << "--#{q}"
+ (o = q.downcase).tr!('_', '-')
+ long << o
+ when /\A-(\[\^?\]?(?:[^\\\]]|\\.)*\])(.+)?/
+ q, a = $1, $2
+ o = notwice(Object, klass, 'type')
+ if a
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ else
+ has_arg = true
+ end
+ sdesc << "-#{q}"
+ short << Regexp.new(q)
+ when /\A-(.)(.+)?/
+ q, a = $1, $2
+ if a
+ o = notwice(NilClass, klass, 'type')
+ default_style = default_style.guess(arg = a)
+ default_pattern, conv = search(:atype, o) unless default_pattern
+ end
+ sdesc << "-#{q}"
+ short << q
+ when /\A=/
+ style = notwice(default_style.guess(arg = o), style, 'style')
+ default_pattern, conv = search(:atype, Object) unless default_pattern
+ else
+ desc.push(o) if o && !o.empty?
+ end
+ end
+
+ default_pattern, conv = search(:atype, default_style.pattern) unless default_pattern
+ if Range === values and klass
+ unless (!values.begin or klass === values.begin) and
+ (!values.end or klass === values.end)
+ raise ArgumentError, "range does not match class"
+ end
+ end
+ if !(short.empty? and long.empty?)
+ if has_arg and default_style == Switch::NoArgument
+ default_style = Switch::RequiredArgument
+ end
+ s = (style || default_style).new(pattern || default_pattern,
+ conv, sdesc, ldesc, arg, desc, block, values)
+ elsif !block
+ if style or pattern
+ raise ArgumentError, "no switch given", ParseError.filter_backtrace(caller)
+ end
+ s = desc
+ else
+ short << pattern
+ s = (style || default_style).new(pattern,
+ conv, nil, nil, arg, desc, block, values)
+ end
+ return s, short, long,
+ (not_style.new(not_pattern, not_conv, sdesc, ldesc, nil, desc, block) if not_style),
+ nolong
+ end
+
+ # ----
+ # Option definition phase methods
+ #
+ # These methods are used to define options, or to construct an
+ # Gem::OptionParser instance in other words.
+
+ # :call-seq:
+ # define(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define(*opts, &block)
+ top.append(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ # :call-seq:
+ # on(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def on(*opts, &block)
+ define(*opts, &block)
+ self
+ end
+ alias def_option define
+
+ # :call-seq:
+ # define_head(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define_head(*opts, &block)
+ top.prepend(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ # :call-seq:
+ # on_head(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the head of the summary.
+ #
+ def on_head(*opts, &block)
+ define_head(*opts, &block)
+ self
+ end
+ alias def_head_option define_head
+
+ # :call-seq:
+ # define_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ def define_tail(*opts, &block)
+ base.append(*(sw = make_switch(opts, block)))
+ sw[0]
+ end
+
+ #
+ # :call-seq:
+ # on_tail(*params, &block)
+ #
+ # :include: ../doc/optparse/creates_option.rdoc
+ #
+ # The new option is added at the tail of the summary.
+ #
+ def on_tail(*opts, &block)
+ define_tail(*opts, &block)
+ self
+ end
+ alias def_tail_option define_tail
+
+ #
+ # Add separator in summary.
+ #
+ def separator(string)
+ top.append(string, nil, nil)
+ end
+
+ # ----
+ # Arguments parse phase methods
+ #
+ # These methods parse +argv+, convert, and store the results by
+ # calling handlers. As these methods do not modify +self+, +self+
+ # can be frozen.
+
+ #
+ # Parses command line arguments +argv+ in order. When a block is given,
+ # each non-option argument is yielded. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
+ #
+ # Returns the rest of +argv+ left unparsed.
+ #
+ def order(*argv, **keywords, &nonopt)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ order!(argv, **keywords, &nonopt)
+ end
+
+ #
+ # Same as #order, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def order!(argv = default_argv, into: nil, **keywords, &nonopt)
+ setter = ->(name, val) {into[name.to_sym] = val} if into
+ parse_in_order(argv, setter, **keywords, &nonopt)
+ end
+
+ def parse_in_order(argv = default_argv, setter = nil, exact: require_exact, **, &nonopt) # :nodoc:
+ opt, arg, val, rest = nil
+ nonopt ||= proc {|a| throw :terminate, a}
+ argv.unshift(arg) if arg = catch(:terminate) {
+ while arg = argv.shift
+ case arg
+ # long option
+ when /\A--([^=]*)(?:=(.*))?/m
+ opt, rest = $1, $2
+ opt.tr!('_', '-')
+ begin
+ if exact
+ sw, = search(:long, opt)
+ else
+ sw, = complete(:long, opt, true)
+ end
+ rescue ParseError
+ throw :terminate, arg unless raise_unknown
+ raise $!.set_option(arg, true)
+ else
+ unless sw
+ throw :terminate, arg unless raise_unknown
+ raise InvalidOption, arg
+ end
+ end
+ begin
+ opt, cb, val = sw.parse(rest, argv) {|*exc| raise(*exc)}
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
+ rescue ParseError
+ raise $!.set_option(arg, rest)
+ end
+
+ # short option
+ when /\A-(.)((=).*|.+)?/m
+ eq, rest, opt = $3, $2, $1
+ has_arg, val = eq, rest
+ begin
+ sw, = search(:short, opt)
+ unless sw
+ begin
+ sw, = complete(:short, opt)
+ # short option matched.
+ val = arg.delete_prefix('-')
+ has_arg = true
+ rescue InvalidOption
+ raise if exact
+ # if no short options match, try completion with long
+ # options.
+ sw, = complete(:long, opt)
+ eq ||= !rest
+ end
+ end
+ rescue ParseError
+ throw :terminate, arg unless raise_unknown
+ raise $!.set_option(arg, true)
+ end
+ begin
+ opt, cb, val = sw.parse(val, argv) {|*exc| raise(*exc) if eq}
+ rescue ParseError
+ raise $!.set_option(arg, arg.length > 2)
+ else
+ raise InvalidOption, arg if has_arg and !eq and arg == "-#{opt}"
+ end
+ begin
+ argv.unshift(opt) if opt and (!rest or (opt = opt.sub(/\A-*/, '-')) != '-')
+ val = callback!(cb, 1, val) if cb
+ callback!(setter, 2, sw.switch_name, val) if setter
+ rescue ParseError
+ raise $!.set_option(arg, arg.length > 2)
+ end
+
+ # non-option argument
+ else
+ catch(:prune) do
+ visit(:each_option) do |sw0|
+ sw = sw0
+ sw.block.call(arg) if Switch === sw and sw.match_nonswitch?(arg)
+ end
+ nonopt.call(arg)
+ end
+ end
+ end
+
+ nil
+ }
+
+ visit(:search, :short, nil) {|sw| sw.block.call(*argv) if !sw.pattern}
+
+ argv
+ end
+ private :parse_in_order
+
+ # Calls callback with _val_.
+ def callback!(cb, max_arity, *args) # :nodoc:
+ args.compact!
+
+ if (size = args.size) < max_arity and cb.to_proc.lambda?
+ (arity = cb.arity) < 0 and arity = (1-arity)
+ arity = max_arity if arity > max_arity
+ args[arity - 1] = nil if arity > size
+ end
+ cb.call(*args)
+ end
+ private :callback!
+
+ #
+ # Parses command line arguments +argv+ in permutation mode and returns
+ # list of non-option arguments. When optional +into+ keyword
+ # argument is provided, the parsed option values are stored there via
+ # <code>[]=</code> method (so it can be Hash, or OpenStruct, or other
+ # similar object).
+ #
+ def permute(*argv, **keywords)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ permute!(argv, **keywords)
+ end
+
+ #
+ # Same as #permute, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def permute!(argv = default_argv, **keywords)
+ nonopts = []
+ order!(argv, **keywords) {|nonopt| nonopts << nonopt}
+ argv[0, 0] = nonopts
+ argv
+ end
+
+ #
+ # Parses command line arguments +argv+ in order when environment variable
+ # POSIXLY_CORRECT is set, and in permutation mode otherwise.
+ # When optional +into+ keyword argument is provided, the parsed option
+ # values are stored there via <code>[]=</code> method (so it can be Hash,
+ # or OpenStruct, or other similar object).
+ #
+ def parse(*argv, **keywords)
+ argv = argv[0].dup if argv.size == 1 and Array === argv[0]
+ parse!(argv, **keywords)
+ end
+
+ #
+ # Same as #parse, but removes switches destructively.
+ # Non-option arguments remain in +argv+.
+ #
+ def parse!(argv = default_argv, **keywords)
+ if ENV.include?('POSIXLY_CORRECT')
+ order!(argv, **keywords)
+ else
+ permute!(argv, **keywords)
+ end
+ end
+
+ #
+ # Wrapper method for getopts.rb.
+ #
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option")
+ # # params["a"] = true # -a
+ # # params["b"] = "1" # -b1
+ # # params["foo"] = "1" # --foo
+ # # params["bar"] = "x" # --bar x
+ # # params["zot"] = "z" # --zot Z
+ #
+ # Option +symbolize_names+ (boolean) specifies whether returned Hash keys should be Symbols; defaults to +false+ (use Strings).
+ #
+ # params = ARGV.getopts("ab:", "foo", "bar:", "zot:Z;zot option", symbolize_names: true)
+ # # params[:a] = true # -a
+ # # params[:b] = "1" # -b1
+ # # params[:foo] = "1" # --foo
+ # # params[:bar] = "x" # --bar x
+ # # params[:zot] = "z" # --zot Z
+ #
+ def getopts(*args, symbolize_names: false, **keywords)
+ argv = Array === args.first ? args.shift : default_argv
+ single_options, *long_options = *args
+
+ result = {}
+ setter = (symbolize_names ?
+ ->(name, val) {result[name.to_sym] = val}
+ : ->(name, val) {result[name] = val})
+
+ single_options.scan(/(.)(:)?/) do |opt, val|
+ if val
+ setter[opt, nil]
+ define("-#{opt} VAL")
+ else
+ setter[opt, false]
+ define("-#{opt}")
+ end
+ end if single_options
+
+ long_options.each do |arg|
+ arg, desc = arg.split(';', 2)
+ opt, val = arg.split(':', 2)
+ if val
+ setter[opt, (val unless val.empty?)]
+ define("--#{opt}=#{result[opt] || "VAL"}", *[desc].compact)
+ else
+ setter[opt, false]
+ define("--#{opt}", *[desc].compact)
+ end
+ end
+
+ parse_in_order(argv, setter, **keywords)
+ result
+ end
+
+ #
+ # See #getopts.
+ #
+ def self.getopts(*args, symbolize_names: false)
+ new.getopts(*args, symbolize_names: symbolize_names)
+ end
+
+ #
+ # Traverses @stack, sending each element method +id+ with +args+ and
+ # +block+.
+ #
+ def visit(id, *args, &block) # :nodoc:
+ @stack.reverse_each do |el|
+ el.__send__(id, *args, &block)
+ end
+ nil
+ end
+ private :visit
+
+ #
+ # Searches +key+ in @stack for +id+ hash and returns or yields the result.
+ #
+ def search(id, key) # :nodoc:
+ block_given = block_given?
+ visit(:search, id, key) do |k|
+ return block_given ? yield(k) : k
+ end
+ end
+ private :search
+
+ #
+ # Completes shortened long style option switch and returns pair of
+ # canonical switch and switch descriptor Gem::OptionParser::Switch.
+ #
+ # +typ+:: Searching table.
+ # +opt+:: Searching key.
+ # +icase+:: Search case insensitive if true.
+ # +pat+:: Optional pattern for completion.
+ #
+ def complete(typ, opt, icase = false, *pat) # :nodoc:
+ if pat.empty?
+ search(typ, opt) {|sw| return [sw, opt]} # exact match or...
+ end
+ ambiguous = catch(:ambiguous) {
+ visit(:complete, typ, opt, icase, *pat) {|o, *sw| return sw}
+ }
+ exc = ambiguous ? AmbiguousOption : InvalidOption
+ raise exc.new(opt, additional: proc {|o| additional_message(typ, o)})
+ end
+ private :complete
+
+ #
+ # Returns additional info.
+ #
+ def additional_message(typ, opt)
+ return unless typ and opt and defined?(DidYouMean::SpellChecker)
+ all_candidates = []
+ visit(:get_candidates, typ) do |candidates|
+ all_candidates.concat(candidates)
+ end
+ all_candidates.select! {|cand| cand.is_a?(String) }
+ checker = DidYouMean::SpellChecker.new(dictionary: all_candidates)
+ DidYouMean.formatter.message_for(all_candidates & checker.correct(opt))
+ end
+
+ #
+ # Return candidates for +word+.
+ #
+ def candidate(word)
+ list = []
+ case word
+ when '-'
+ long = short = true
+ when /\A--/
+ word, arg = word.split(/=/, 2)
+ argpat = Completion.regexp(arg, false) if arg and !arg.empty?
+ long = true
+ when /\A-/
+ short = true
+ end
+ pat = Completion.regexp(word, long)
+ visit(:each_option) do |opt|
+ next unless Switch === opt
+ opts = (long ? opt.long : []) + (short ? opt.short : [])
+ opts = Completion.candidate(word, true, pat, &opts.method(:each)).map(&:first) if pat
+ if /\A=/ =~ opt.arg
+ opts.map! {|sw| sw + "="}
+ if arg and CompletingHash === opt.pattern
+ if opts = opt.pattern.candidate(arg, false, argpat)
+ opts.map!(&:last)
+ end
+ end
+ end
+ list.concat(opts)
+ end
+ list
+ end
+
+ #
+ # Loads options from file names as +filename+. Does nothing when the file
+ # is not present. Returns whether successfully loaded.
+ #
+ # +filename+ defaults to basename of the program without suffix in a
+ # directory ~/.options, then the basename with '.options' suffix
+ # under XDG and Haiku standard places.
+ #
+ # The optional +into+ keyword argument works exactly like that accepted in
+ # method #parse.
+ #
+ def load(filename = nil, **keywords)
+ unless filename
+ basename = File.basename($0, '.*')
+ return true if load(File.expand_path("~/.options/#{basename}"), **keywords) rescue nil
+ basename << ".options"
+ if !(xdg = ENV['XDG_CONFIG_HOME']) or xdg.empty?
+ # https://specifications.freedesktop.org/basedir-spec/latest/#variables
+ #
+ # If $XDG_CONFIG_HOME is either not set or empty, a default
+ # equal to $HOME/.config should be used.
+ xdg = ['~/.config', true]
+ end
+ return [
+ xdg,
+
+ *ENV['XDG_CONFIG_DIRS']&.split(File::PATH_SEPARATOR),
+
+ # Haiku
+ ['~/config/settings', true],
+ ].any? {|dir, expand|
+ next if !dir or dir.empty?
+ filename = File.join(dir, basename)
+ filename = File.expand_path(filename) if expand
+ load(filename, **keywords) rescue nil
+ }
+ end
+ begin
+ parse(*File.readlines(filename, chomp: true), **keywords)
+ true
+ rescue Errno::ENOENT, Errno::ENOTDIR
+ false
+ end
+ end
+
+ #
+ # Parses environment variable +env+ or its uppercase with splitting like a
+ # shell.
+ #
+ # +env+ defaults to the basename of the program.
+ #
+ def environment(env = File.basename($0, '.*'), **keywords)
+ env = ENV[env] || ENV[env.upcase] or return
+ require 'shellwords'
+ parse(*Shellwords.shellwords(env), **keywords)
+ end
+
+ #
+ # Acceptable argument classes
+ #
+
+ #
+ # Any string and no conversion. This is fall-back.
+ #
+ accept(Object) {|s,|s or s.nil?}
+
+ accept(NilClass) {|s,|s}
+
+ #
+ # Any non-empty string, and no conversion.
+ #
+ accept(String, /.+/m) {|s,*|s}
+
+ #
+ # Ruby/C-like integer, octal for 0-7 sequence, binary for 0b, hexadecimal
+ # for 0x, and decimal for others; with optional sign prefix. Converts to
+ # Integer.
+ #
+ decimal = '\d+(?:_\d+)*'
+ binary = 'b[01]+(?:_[01]+)*'
+ hex = 'x[\da-f]+(?:_[\da-f]+)*'
+ octal = "0(?:[0-7]+(?:_[0-7]+)*|#{binary}|#{hex})?"
+ integer = "#{octal}|#{decimal}"
+
+ accept(Integer, %r"\A[-+]?(?:#{integer})\z"io) {|s,|
+ begin
+ Integer(s)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Float number format, and converts to Float.
+ #
+ float = "(?:#{decimal}(?=(.)?)(?:\\.(?:#{decimal})?)?|\\.#{decimal})(?:E[-+]?#{decimal})?"
+ floatpat = %r"\A[-+]?#{float}\z"io
+ accept(Float, floatpat) {|s,| s.to_f if s}
+
+ #
+ # Generic numeric format, converts to Integer for integer format, Float
+ # for float format, and Rational for rational format.
+ #
+ real = "[-+]?(?:#{octal}|#{float})"
+ accept(Numeric, /\A(#{real})(?:\/(#{real}))?\z/io) {|s, d, f, n,|
+ if n
+ Rational(d, n)
+ elsif f
+ Float(s)
+ else
+ Integer(s)
+ end
+ }
+
+ #
+ # Decimal integer format, to be converted to Integer.
+ #
+ DecimalInteger = /\A[-+]?#{decimal}\z/io
+ accept(DecimalInteger, DecimalInteger) {|s,|
+ begin
+ Integer(s, 10)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Ruby/C like octal/hexadecimal/binary integer format, to be converted to
+ # Integer.
+ #
+ OctalInteger = /\A[-+]?(?:[0-7]+(?:_[0-7]+)*|0(?:#{binary}|#{hex}))\z/io
+ accept(OctalInteger, OctalInteger) {|s,|
+ begin
+ Integer(s, 8)
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Decimal integer/float number format, to be converted to Integer for
+ # integer format, Float for float format.
+ #
+ DecimalNumeric = floatpat # decimal integer is allowed as float also.
+ accept(DecimalNumeric, floatpat) {|s, f|
+ begin
+ if f
+ Float(s)
+ else
+ Integer(s)
+ end
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end if s
+ }
+
+ #
+ # Boolean switch, which means whether it is present or not, whether it is
+ # absent or not with prefix no-, or it takes an argument
+ # yes/no/true/false/+/-.
+ #
+ yesno = CompletingHash.new
+ %w[- no false].each {|el| yesno[el] = false}
+ %w[+ yes true].each {|el| yesno[el] = true}
+ yesno['nil'] = false # should be nil?
+ accept(TrueClass, yesno) {|arg, val| val == nil or val}
+ #
+ # Similar to TrueClass, but defaults to false.
+ #
+ accept(FalseClass, yesno) {|arg, val| val != nil and val}
+
+ #
+ # List of strings separated by ",".
+ #
+ accept(Array) do |s, |
+ if s
+ s = s.split(',').collect {|ss| ss unless ss.empty?}
+ end
+ s
+ end
+
+ #
+ # Regular expression with options.
+ #
+ accept(Regexp, %r"\A/((?:\\.|[^\\])*)/([[:alpha:]]+)?\z|.*") do |all, s, o|
+ f = 0
+ if o
+ f |= Regexp::IGNORECASE if /i/ =~ o
+ f |= Regexp::MULTILINE if /m/ =~ o
+ f |= Regexp::EXTENDED if /x/ =~ o
+ case o = o.delete("imx")
+ when ""
+ when "u"
+ s = s.encode(Encoding::UTF_8)
+ when "e"
+ s = s.encode(Encoding::EUC_JP)
+ when "s"
+ s = s.encode(Encoding::SJIS)
+ when "n"
+ f |= Regexp::NOENCODING
+ else
+ raise Gem::OptionParser::InvalidArgument, "unknown regexp option - #{o}"
+ end
+ else
+ s ||= all
+ end
+ Regexp.new(s, f)
+ end
+
+ #
+ # Exceptions
+ #
+
+ #
+ # Base class of exceptions from Gem::OptionParser.
+ #
+ class ParseError < RuntimeError
+ # Reason which caused the error.
+ Reason = 'parse error'
+
+ # :nodoc:
+ def initialize(*args, additional: nil)
+ @additional = additional
+ @arg0, = args
+ @args = args
+ @reason = nil
+ end
+
+ attr_reader :args
+ attr_writer :reason
+ attr_accessor :additional
+
+ #
+ # Pushes back erred argument(s) to +argv+.
+ #
+ def recover(argv)
+ argv[0, 0] = @args
+ argv
+ end
+
+ DIR = File.join(__dir__, '')
+ def self.filter_backtrace(array)
+ unless $DEBUG
+ array.delete_if {|bt| bt.start_with?(DIR)}
+ end
+ array
+ end
+
+ def set_backtrace(array)
+ super(self.class.filter_backtrace(array))
+ end
+
+ def set_option(opt, eq)
+ if eq
+ @args[0] = opt
+ else
+ @args.unshift(opt)
+ end
+ self
+ end
+
+ #
+ # Returns error reason. Override this for I18N.
+ #
+ def reason
+ @reason || self.class::Reason
+ end
+
+ def inspect
+ "#<#{self.class}: #{args.join(' ')}>"
+ end
+
+ #
+ # Default stringizing method to emit standard error message.
+ #
+ def message
+ "#{reason}: #{args.join(' ')}#{additional[@arg0] if additional}"
+ end
+
+ alias to_s message
+ end
+
+ #
+ # Raises when ambiguously completable string is encountered.
+ #
+ class AmbiguousOption < ParseError
+ const_set(:Reason, 'ambiguous option')
+ end
+
+ #
+ # Raises when there is an argument for a switch which takes no argument.
+ #
+ class NeedlessArgument < ParseError
+ const_set(:Reason, 'needless argument')
+ end
+
+ #
+ # Raises when a switch with mandatory argument has no argument.
+ #
+ class MissingArgument < ParseError
+ const_set(:Reason, 'missing argument')
+ end
+
+ #
+ # Raises when switch is undefined.
+ #
+ class InvalidOption < ParseError
+ const_set(:Reason, 'invalid option')
+ end
+
+ #
+ # Raises when the given argument does not match required format.
+ #
+ class InvalidArgument < ParseError
+ const_set(:Reason, 'invalid argument')
+ end
+
+ #
+ # Raises when the given argument word can't be completed uniquely.
+ #
+ class AmbiguousArgument < InvalidArgument
+ const_set(:Reason, 'ambiguous argument')
+ end
+
+ #
+ # Miscellaneous
+ #
+
+ #
+ # Extends command line arguments array (ARGV) to parse itself.
+ #
+ module Arguable
+
+ #
+ # Sets Gem::OptionParser object, when +opt+ is +false+ or +nil+, methods
+ # Gem::OptionParser::Arguable#options and Gem::OptionParser::Arguable#options= are
+ # undefined. Thus, there is no ways to access the Gem::OptionParser object
+ # via the receiver object.
+ #
+ def options=(opt)
+ unless @optparse = opt
+ class << self
+ undef_method(:options)
+ undef_method(:options=)
+ end
+ end
+ end
+
+ #
+ # Actual Gem::OptionParser object, automatically created if nonexistent.
+ #
+ # If called with a block, yields the Gem::OptionParser object and returns the
+ # result of the block. If an Gem::OptionParser::ParseError exception occurs
+ # in the block, it is rescued, a error message printed to STDERR and
+ # +nil+ returned.
+ #
+ def options
+ @optparse ||= Gem::OptionParser.new
+ @optparse.default_argv = self
+ block_given? or return @optparse
+ begin
+ yield @optparse
+ rescue ParseError
+ @optparse.warn $!
+ nil
+ end
+ end
+
+ #
+ # Parses +self+ destructively in order and returns +self+ containing the
+ # rest arguments left unparsed.
+ #
+ def order!(**keywords, &blk) options.order!(self, **keywords, &blk) end
+
+ #
+ # Parses +self+ destructively in permutation mode and returns +self+
+ # containing the rest arguments left unparsed.
+ #
+ def permute!(**keywords) options.permute!(self, **keywords) end
+
+ #
+ # Parses +self+ destructively and returns +self+ containing the
+ # rest arguments left unparsed.
+ #
+ def parse!(**keywords) options.parse!(self, **keywords) end
+
+ #
+ # Substitution of getopts is possible as follows. Also see
+ # Gem::OptionParser#getopts.
+ #
+ # def getopts(*args)
+ # ($OPT = ARGV.getopts(*args)).each do |opt, val|
+ # eval "$OPT_#{opt.gsub(/[^A-Za-z0-9_]/, '_')} = val"
+ # end
+ # rescue Gem::OptionParser::ParseError
+ # end
+ #
+ def getopts(*args, symbolize_names: false, **keywords)
+ options.getopts(self, *args, symbolize_names: symbolize_names, **keywords)
+ end
+
+ #
+ # Initializes instance variable.
+ #
+ def self.extend_object(obj)
+ super
+ obj.instance_eval {@optparse = nil}
+ end
+
+ def initialize(*args) # :nodoc:
+ super
+ @optparse = nil
+ end
+ end
+
+ #
+ # Acceptable argument classes. Now contains DecimalInteger, OctalInteger
+ # and DecimalNumeric. See Acceptable argument classes (in source code).
+ #
+ module Acceptables
+ const_set(:DecimalInteger, Gem::OptionParser::DecimalInteger)
+ const_set(:OctalInteger, Gem::OptionParser::OctalInteger)
+ const_set(:DecimalNumeric, Gem::OptionParser::DecimalNumeric)
+ end
+end
+
+# ARGV is arguable by Gem::OptionParser
+ARGV.extend(Gem::OptionParser::Arguable)
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/ac.rb b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb
new file mode 100644
index 0000000000..28a5b1b33e
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/ac.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+
+#
+# autoconf-like options.
+#
+class Gem::OptionParser::AC < Gem::OptionParser
+ # :stopdoc:
+ private
+
+ def _check_ac_args(name, block)
+ unless /\A\w[-\w]*\z/ =~ name
+ raise ArgumentError, name
+ end
+ unless block
+ raise ArgumentError, "no block given", ParseError.filter_backtrace(caller)
+ end
+ end
+
+ ARG_CONV = proc {|val| val.nil? ? true : val}
+ private_constant :ARG_CONV
+
+ def _ac_arg_enable(prefix, name, help_string, block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--#{prefix}-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ ac_block = proc {|val| block.call(ARG_CONV.call(val))}
+ enable = Switch::PlacedArgument.new(nil, ARG_CONV, sdesc, ldesc, nil, desc, ac_block)
+ disable = Switch::NoArgument.new(nil, proc {false}, sdesc, ldesc, nil, desc, ac_block)
+ top.append(enable, [], ["enable-" + q], disable, ['disable-' + q])
+ enable
+ end
+
+ # :startdoc:
+
+ public
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--enable-<i>name</i></tt> in help message.
+ def ac_arg_enable(name, help_string, &block)
+ _ac_arg_enable("enable", name, help_string, block)
+ end
+
+ # Define <tt>--enable</tt> / <tt>--disable</tt> style option
+ #
+ # Appears as <tt>--disable-<i>name</i></tt> in help message.
+ def ac_arg_disable(name, help_string, &block)
+ _ac_arg_enable("disable", name, help_string, block)
+ end
+
+ # Define <tt>--with</tt> / <tt>--without</tt> style option
+ #
+ # Appears as <tt>--with-<i>name</i></tt> in help message.
+ def ac_arg_with(name, help_string, &block)
+ _check_ac_args(name, block)
+
+ sdesc = []
+ ldesc = ["--with-#{name}"]
+ desc = [help_string]
+ q = name.downcase
+ with = Switch::PlacedArgument.new(*search(:atype, String), sdesc, ldesc, nil, desc, block)
+ without = Switch::NoArgument.new(nil, proc {}, sdesc, ldesc, nil, desc, block)
+ top.append(with, [], ["with-" + q], without, ['without-' + q])
+ with
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/date.rb b/lib/rubygems/vendor/optparse/lib/optparse/date.rb
new file mode 100644
index 0000000000..d9a9f4f48a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/date.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+require 'date'
+
+Gem::OptionParser.accept(DateTime) do |s,|
+ begin
+ DateTime.parse(s) if s
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
+Gem::OptionParser.accept(Date) do |s,|
+ begin
+ Date.parse(s) if s
+ rescue ArgumentError
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb
new file mode 100644
index 0000000000..70762f033b
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/kwargs.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require_relative '../optparse'
+
+class Gem::OptionParser
+ # :call-seq:
+ # define_by_keywords(options, method, **params)
+ #
+ # :include: ../../doc/optparse/creates_option.rdoc
+ #
+ # Defines options which set in to _options_ for keyword parameters
+ # of _method_.
+ #
+ # Parameters for each keywords are given as elements of _params_.
+ #
+ def define_by_keywords(options, method, **params)
+ method.parameters.each do |type, name|
+ case type
+ when :key, :keyreq
+ op, cl = *(type == :key ? %w"[ ]" : ["", ""])
+ define("--#{name}=#{op}#{name.upcase}#{cl}", *params[name]) do |o|
+ options[name] = o
+ end
+ end
+ end
+ options
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb
new file mode 100644
index 0000000000..d47ad60255
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/shellwords.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: false
+# -*- ruby -*-
+
+require 'shellwords'
+require_relative '../optparse'
+
+Gem::OptionParser.accept(Shellwords) {|s,| Shellwords.shellwords(s)}
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/time.rb b/lib/rubygems/vendor/optparse/lib/optparse/time.rb
new file mode 100644
index 0000000000..c59e1e4ced
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/time.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: false
+require_relative '../optparse'
+require 'time'
+
+Gem::OptionParser.accept(Time) do |s,|
+ begin
+ (Time.httpdate(s) rescue Time.parse(s)) if s
+ rescue
+ raise Gem::OptionParser::InvalidArgument, s
+ end
+end
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/uri.rb b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb
new file mode 100644
index 0000000000..398127479a
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/uri.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: false
+# -*- ruby -*-
+
+require_relative '../optparse'
+require_relative '../../../uri/lib/uri'
+
+Gem::OptionParser.accept(Gem::URI) {|s,| Gem::URI.parse(s) if s}
diff --git a/lib/rubygems/vendor/optparse/lib/optparse/version.rb b/lib/rubygems/vendor/optparse/lib/optparse/version.rb
new file mode 100644
index 0000000000..e39889ae87
--- /dev/null
+++ b/lib/rubygems/vendor/optparse/lib/optparse/version.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: false
+# Gem::OptionParser internal utility
+
+class << Gem::OptionParser
+ #
+ # Shows version string in packages if Version is defined.
+ #
+ # +pkgs+:: package list
+ #
+ def show_version(*pkgs)
+ progname = ARGV.options.program_name
+ result = false
+ show = proc do |klass, cname, version|
+ str = "#{progname}"
+ unless klass == ::Object and cname == :VERSION
+ version = version.join(".") if Array === version
+ str << ": #{klass}" unless klass == Object
+ str << " version #{version}"
+ end
+ [:Release, :RELEASE].find do |rel|
+ if klass.const_defined?(rel)
+ str << " (#{klass.const_get(rel)})"
+ end
+ end
+ puts str
+ result = true
+ end
+ if pkgs.size == 1 and pkgs[0] == "all"
+ self.search_const(::Object, /\AV(?:ERSION|ersion)\z/) do |klass, cname, version|
+ unless cname[1] == ?e and klass.const_defined?(:Version)
+ show.call(klass, cname.intern, version)
+ end
+ end
+ else
+ pkgs.each do |pkg|
+ begin
+ pkg = pkg.split(/::|\//).inject(::Object) {|m, c| m.const_get(c)}
+ v = case
+ when pkg.const_defined?(:Version)
+ pkg.const_get(n = :Version)
+ when pkg.const_defined?(:VERSION)
+ pkg.const_get(n = :VERSION)
+ else
+ n = nil
+ "unknown"
+ end
+ show.call(pkg, n, v)
+ rescue NameError
+ end
+ end
+ end
+ result
+ end
+
+ # :stopdoc:
+
+ def each_const(path, base = ::Object)
+ path.split(/::|\//).inject(base) do |klass, name|
+ raise NameError, path unless Module === klass
+ klass.constants.grep(/#{name}/i) do |c|
+ klass.const_defined?(c) or next
+ klass.const_get(c)
+ end
+ end
+ end
+
+ def search_const(klass, name)
+ klasses = [klass]
+ while klass = klasses.shift
+ klass.constants.each do |cname|
+ klass.const_defined?(cname) or next
+ const = klass.const_get(cname)
+ yield klass, cname, const if name === cname
+ klasses << const if Module === const and const != ::Object
+ end
+ end
+ end
+
+ # :startdoc:
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb
new file mode 100644
index 0000000000..818e947477
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb
@@ -0,0 +1,53 @@
+require_relative "pub_grub/package"
+require_relative "pub_grub/static_package_source"
+require_relative "pub_grub/term"
+require_relative "pub_grub/version_range"
+require_relative "pub_grub/version_constraint"
+require_relative "pub_grub/version_union"
+require_relative "pub_grub/version_solver"
+require_relative "pub_grub/incompatibility"
+require_relative 'pub_grub/solve_failure'
+require_relative 'pub_grub/failure_writer'
+require_relative 'pub_grub/version'
+
+module Gem::PubGrub
+ # Minimal logger that doesn't require the 'logger' gem
+ class NullLogger
+ def info(&block); end
+ def debug(&block); end
+ def warn(&block); end
+ def error(&block); end
+ end
+
+ class StderrLogger
+ def info(&block)
+ $stderr.puts "INFO: #{block.call}" if block
+ end
+
+ def debug(&block)
+ $stderr.puts "DEBUG: #{block.call}" if block
+ end
+
+ def warn(&block)
+ $stderr.puts "WARN: #{block.call}" if block
+ end
+
+ def error(&block)
+ $stderr.puts "ERROR: #{block.call}" if block
+ end
+ end
+
+ class << self
+ attr_writer :logger
+
+ def logger
+ @logger || default_logger
+ end
+
+ private
+
+ def default_logger
+ @logger = $DEBUG ? StderrLogger.new : NullLogger.new
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb
new file mode 100644
index 0000000000..7a11cf0933
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/assignment.rb
@@ -0,0 +1,20 @@
+module Gem::PubGrub
+ class Assignment
+ attr_reader :term, :cause, :decision_level, :index
+ def initialize(term, cause, decision_level, index)
+ @term = term
+ @cause = cause
+ @decision_level = decision_level
+ @index = index
+ end
+
+ def self.decision(package, version, decision_level, index)
+ term = Term.new(VersionConstraint.exact(package, version), true)
+ new(term, :decision, decision_level, index)
+ end
+
+ def decision?
+ cause == :decision
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
new file mode 100644
index 0000000000..c8dbf2a5ab
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/basic_package_source.rb
@@ -0,0 +1,169 @@
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+
+module Gem::PubGrub
+ # Types:
+ #
+ # Where possible, Gem::PubGrub will accept user-defined types, so long as they quack.
+ #
+ # ## "Package":
+ #
+ # This class will be used to represent the various packages being solved for.
+ # .to_s will be called when displaying errors and debugging info, it should
+ # probably return the package's name.
+ # It must also have a reasonable definition of #== and #hash
+ #
+ # Example classes: String ("rails")
+ #
+ #
+ # ## "Version":
+ #
+ # This class will be used to represent a single version number.
+ #
+ # Versions don't need to store their associated package, however they will
+ # only be compared against other versions of the same package.
+ #
+ # It must be Comparible (and implement <=> reasonably)
+ #
+ # Example classes: Gem::Version, Integer
+ #
+ #
+ # ## "Dependency"
+ #
+ # This class represents the requirement one package has on another. It is
+ # returned by dependencies_for(package, version) and will be passed to
+ # parse_dependency to convert it to a format Gem::PubGrub understands.
+ #
+ # It must also have a reasonable definition of #==
+ #
+ # Example classes: String ("~> 1.0"), Gem::Requirement
+ #
+ class BasicPackageSource
+ # Override me!
+ #
+ # This is called per package to find all possible versions of a package.
+ #
+ # It is called at most once per-package
+ #
+ # Returns: Array of versions for a package, in preferred order of selection
+ def all_versions_for(package)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Returns: Hash in the form of { package => requirement, ... }
+ def dependencies_for(package, version)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # Convert a (user-defined) dependency into a format Gem::PubGrub understands.
+ #
+ # Package is passed to this method but for many implementations is not
+ # needed.
+ #
+ # Returns: either a Gem::PubGrub::VersionRange, Gem::PubGrub::VersionUnion, or a
+ # Gem::PubGrub::VersionConstraint
+ def parse_dependency(package, dependency)
+ raise NotImplementedError
+ end
+
+ # Override me!
+ #
+ # If not overridden, this will call dependencies_for with the root package.
+ #
+ # Returns: Hash in the form of { package => requirement, ... } (see dependencies_for)
+ def root_dependencies
+ dependencies_for(@root_package, @root_version)
+ end
+
+ def initialize
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @sorted_versions = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = [@root_version]
+ else
+ h[k] = all_versions_for(k).sort
+ end
+ end
+
+ @cached_dependencies = Hash.new do |packages, package|
+ if package == @root_package
+ packages[package] = {
+ @root_version => root_dependencies
+ }
+ else
+ packages[package] = Hash.new do |versions, version|
+ versions[version] = dependencies_for(package, version)
+ end
+ end
+ end
+ end
+
+ def versions_for(package, range=VersionRange.any)
+ range.select_versions(@sorted_versions[package])
+ end
+
+ def no_versions_incompatibility_for(_package, unsatisfied_term)
+ cause = Incompatibility::NoVersions.new(unsatisfied_term)
+
+ Incompatibility.new([unsatisfied_term], cause: cause)
+ end
+
+ def incompatibilities_for(package, version)
+ package_deps = @cached_dependencies[package]
+ sorted_versions = @sorted_versions[package]
+ package_deps[version].map do |dep_package, dep_constraint_name|
+ low = high = sorted_versions.index(version)
+
+ # find version low such that all >= low share the same dep
+ while low > 0 &&
+ package_deps[sorted_versions[low - 1]][dep_package] == dep_constraint_name
+ low -= 1
+ end
+ low =
+ if low == 0
+ nil
+ else
+ sorted_versions[low]
+ end
+
+ # find version high such that all < high share the same dep
+ while high < sorted_versions.length &&
+ package_deps[sorted_versions[high]][dep_package] == dep_constraint_name
+ high += 1
+ end
+ high =
+ if high == sorted_versions.length
+ nil
+ else
+ sorted_versions[high]
+ end
+
+ range = VersionRange.new(min: low, max: high, include_min: !low.nil?)
+
+ self_constraint = VersionConstraint.new(package, range: range)
+
+ if !@packages.include?(dep_package)
+ # no such package -> this version is invalid
+ end
+
+ dep_constraint = parse_dependency(dep_package, dep_constraint_name)
+ if !dep_constraint
+ # falsey indicates this dependency was invalid
+ cause = Gem::PubGrub::Incompatibility::InvalidDependency.new(dep_package, dep_constraint_name)
+ return [Incompatibility.new([Term.new(self_constraint, true)], cause: cause)]
+ elsif !dep_constraint.is_a?(VersionConstraint)
+ # Upgrade range/union to VersionConstraint
+ dep_constraint = VersionConstraint.new(dep_package, range: dep_constraint)
+ end
+
+ Incompatibility.new([Term.new(self_constraint, true), Term.new(dep_constraint, false)], cause: :dependency)
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb
new file mode 100644
index 0000000000..d8bfde0286
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/failure_writer.rb
@@ -0,0 +1,182 @@
+module Gem::PubGrub
+ class FailureWriter
+ def initialize(root)
+ @root = root
+
+ # { Incompatibility => Integer }
+ @derivations = {}
+
+ # [ [ String, Integer or nil ] ]
+ @lines = []
+
+ # { Incompatibility => Integer }
+ @line_numbers = {}
+
+ count_derivations(root)
+ end
+
+ def write
+ return @root.to_s unless @root.conflict?
+
+ visit(@root)
+
+ padding = @line_numbers.empty? ? 0 : "(#{@line_numbers.values.last}) ".length
+
+ @lines.map do |message, number|
+ next "" if message.empty?
+
+ lead = number ? "(#{number}) " : ""
+ lead = lead.ljust(padding)
+ message = message.gsub("\n", "\n" + " " * (padding + 2))
+ "#{lead}#{message}"
+ end.join("\n")
+ end
+
+ private
+
+ def write_line(incompatibility, message, numbered:)
+ if numbered
+ number = @line_numbers.length + 1
+ @line_numbers[incompatibility] = number
+ end
+
+ @lines << [message, number]
+ end
+
+ def visit(incompatibility, conclusion: false)
+ raise unless incompatibility.conflict?
+
+ numbered = conclusion || @derivations[incompatibility] > 1;
+ conjunction = conclusion || incompatibility == @root ? "So," : "And"
+
+ cause = incompatibility.cause
+
+ if cause.conflict.conflict? && cause.other.conflict?
+ conflict_line = @line_numbers[cause.conflict]
+ other_line = @line_numbers[cause.other]
+
+ if conflict_line && other_line
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict} (#{conflict_line})\nand #{cause.other} (#{other_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif conflict_line || other_line
+ with_line = conflict_line ? cause.conflict : cause.other
+ without_line = conflict_line ? cause.other : cause.conflict
+ line = @line_numbers[with_line]
+
+ visit(without_line);
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{with_line} (#{line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ single_line_conflict = single_line?(cause.conflict.cause)
+ single_line_other = single_line?(cause.other.cause)
+
+ if single_line_conflict || single_line_other
+ first = single_line_other ? cause.conflict : cause.other
+ second = single_line_other ? cause.other : cause.conflict
+ visit(first)
+ visit(second)
+ write_line(
+ incompatibility,
+ "Thus, #{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(cause.conflict, conclusion: true)
+ @lines << ["", nil]
+ visit(cause.other)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{cause.conflict} (#{@line_numbers[cause.conflict]}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+ elsif cause.conflict.conflict? || cause.other.conflict?
+ derived = cause.conflict.conflict? ? cause.conflict : cause.other
+ ext = cause.conflict.conflict? ? cause.other : cause.conflict
+
+ derived_line = @line_numbers[derived]
+ if derived_line
+ write_line(
+ incompatibility,
+ "Because #{ext}\nand #{derived} (#{derived_line}),\n#{incompatibility}.",
+ numbered: numbered
+ )
+ elsif collapsible?(derived)
+ derived_cause = derived.cause
+ if derived_cause.conflict.conflict?
+ collapsed_derived = derived_cause.conflict
+ collapsed_ext = derived_cause.other
+ else
+ collapsed_derived = derived_cause.other
+ collapsed_ext = derived_cause.conflict
+ end
+
+ visit(collapsed_derived)
+
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{collapsed_ext}\nand #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ else
+ visit(derived)
+ write_line(
+ incompatibility,
+ "#{conjunction} because #{ext},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ else
+ write_line(
+ incompatibility,
+ "Because #{cause.conflict}\nand #{cause.other},\n#{incompatibility}.",
+ numbered: numbered
+ )
+ end
+ end
+
+ def single_line?(cause)
+ !cause.conflict.conflict? && !cause.other.conflict?
+ end
+
+ def collapsible?(incompatibility)
+ return false if @derivations[incompatibility] > 1
+
+ cause = incompatibility.cause
+ # If incompatibility is derived from two derived incompatibilities,
+ # there are too many transitive causes to display concisely.
+ return false if cause.conflict.conflict? && cause.other.conflict?
+
+ # If incompatibility is derived from two external incompatibilities, it
+ # tends to be confusing to collapse it.
+ return false unless cause.conflict.conflict? || cause.other.conflict?
+
+ # If incompatibility's internal cause is numbered, collapsing it would
+ # get too noisy.
+ complex = cause.conflict.conflict? ? cause.conflict : cause.other
+
+ !@line_numbers.has_key?(complex)
+ end
+
+ def count_derivations(incompatibility)
+ if @derivations.has_key?(incompatibility)
+ @derivations[incompatibility] += 1
+ else
+ @derivations[incompatibility] = 1
+ if incompatibility.conflict?
+ cause = incompatibility.cause
+ count_derivations(cause.conflict)
+ count_derivations(cause.other)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb
new file mode 100644
index 0000000000..b5652b5e01
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/incompatibility.rb
@@ -0,0 +1,150 @@
+module Gem::PubGrub
+ class Incompatibility
+ ConflictCause = Struct.new(:incompatibility, :satisfier) do
+ alias_method :conflict, :incompatibility
+ alias_method :other, :satisfier
+ end
+
+ InvalidDependency = Struct.new(:package, :constraint) do
+ end
+
+ NoVersions = Struct.new(:constraint) do
+ end
+
+ attr_reader :terms, :cause
+
+ def initialize(terms, cause:, custom_explanation: nil)
+ @cause = cause
+ @terms = cleanup_terms(terms)
+ @custom_explanation = custom_explanation
+
+ if cause == :dependency && @terms.length != 2
+ raise ArgumentError, "a dependency Incompatibility must have exactly two terms. Got #{@terms.inspect}"
+ end
+ end
+
+ def hash
+ cause.hash ^ terms.hash
+ end
+
+ def eql?(other)
+ cause.eql?(other.cause) &&
+ terms.eql?(other.terms)
+ end
+
+ def failure?
+ terms.empty? || (terms.length == 1 && Package.root?(terms[0].package) && terms[0].positive?)
+ end
+
+ def conflict?
+ ConflictCause === cause
+ end
+
+ # Returns all external incompatibilities in this incompatibility's
+ # derivation graph
+ def external_incompatibilities
+ if conflict?
+ [
+ cause.conflict,
+ cause.other
+ ].flat_map(&:external_incompatibilities)
+ else
+ [this]
+ end
+ end
+
+ def to_s
+ return @custom_explanation if @custom_explanation
+
+ case cause
+ when :root
+ "(root dependency)"
+ when :dependency
+ "#{terms[0].to_s(allow_every: true)} depends on #{terms[1].invert}"
+ when Gem::PubGrub::Incompatibility::InvalidDependency
+ "#{terms[0].to_s(allow_every: true)} depends on unknown package #{cause.package}"
+ when Gem::PubGrub::Incompatibility::NoVersions
+ "no versions satisfy #{cause.constraint}"
+ when Gem::PubGrub::Incompatibility::ConflictCause
+ if failure?
+ "version solving has failed"
+ elsif terms.length == 1
+ term = terms[0]
+ if term.positive?
+ if term.constraint.any?
+ "#{term.package} cannot be used"
+ else
+ "#{term.to_s(allow_every: true)} cannot be used"
+ end
+ else
+ "#{term.invert} is required"
+ end
+ else
+ if terms.all?(&:positive?)
+ if terms.length == 2
+ "#{terms[0].to_s(allow_every: true)} is incompatible with #{terms[1]}"
+ else
+ "one of #{terms.map(&:to_s).join(" or ")} must be false"
+ end
+ elsif terms.all?(&:negative?)
+ if terms.length == 2
+ "either #{terms[0].invert} or #{terms[1].invert}"
+ else
+ "one of #{terms.map(&:invert).join(" or ")} must be true";
+ end
+ else
+ positive = terms.select(&:positive?)
+ negative = terms.select(&:negative?).map(&:invert)
+
+ if positive.length == 1
+ "#{positive[0].to_s(allow_every: true)} requires #{negative.join(" or ")}"
+ else
+ "if #{positive.join(" and ")} then #{negative.join(" or ")}"
+ end
+ end
+ end
+ else
+ raise "unhandled cause: #{cause.inspect}"
+ end
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def pretty_print(q)
+ q.group 2, "#<#{self.class}", ">" do
+ q.breakable
+ q.text to_s
+
+ q.breakable
+ q.text " caused by "
+ q.pp @cause
+ end
+ end
+
+ private
+
+ def cleanup_terms(terms)
+ terms.each do |term|
+ raise "#{term.inspect} must be a term" unless term.is_a?(Term)
+ end
+
+ if terms.length != 1 && ConflictCause === cause
+ terms = terms.reject do |term|
+ term.positive? && Package.root?(term.package)
+ end
+ end
+
+ # Optimized simple cases
+ return terms if terms.length <= 1
+ return terms if terms.length == 2 && terms[0].package != terms[1].package
+
+ terms.group_by(&:package).map do |package, common_terms|
+ common_terms.inject do |acc, term|
+ acc.intersect(term)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb
new file mode 100644
index 0000000000..6baa908f60
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/package.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class Package
+
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def inspect
+ "#<#{self.class} #{name.inspect}>"
+ end
+
+ def <=>(other)
+ name <=> other.name
+ end
+
+ ROOT = Package.new(:root)
+ ROOT_VERSION = 0
+
+ def self.root
+ ROOT
+ end
+
+ def self.root_version
+ ROOT_VERSION
+ end
+
+ def self.root?(package)
+ if package.respond_to?(:root?)
+ package.root?
+ else
+ package == root
+ end
+ end
+
+ def to_s
+ name.to_s
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb
new file mode 100644
index 0000000000..f6a6ae6964
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/partial_solution.rb
@@ -0,0 +1,121 @@
+require_relative 'assignment'
+
+module Gem::PubGrub
+ class PartialSolution
+ attr_reader :assignments, :decisions
+ attr_reader :attempted_solutions
+
+ def initialize
+ reset!
+
+ @attempted_solutions = 1
+ @backtracking = false
+ end
+
+ def decision_level
+ @decisions.length
+ end
+
+ def relation(term)
+ package = term.package
+ return :overlap if !@terms.key?(package)
+
+ @relation_cache[package][term] ||=
+ @terms[package].relation(term)
+ end
+
+ def satisfies?(term)
+ relation(term) == :subset
+ end
+
+ def derive(term, cause)
+ add_assignment(Assignment.new(term, cause, decision_level, assignments.length))
+ end
+
+ def satisfier(term)
+ assignment =
+ @assignments_by[term.package].bsearch do |assignment_by|
+ @cumulative_assignments[assignment_by].satisfies?(term)
+ end
+
+ assignment || raise("#{term} unsatisfied")
+ end
+
+ # A list of unsatisfied terms
+ def unsatisfied
+ @required.keys.reject do |package|
+ @decisions.key?(package)
+ end.map do |package|
+ @terms[package]
+ end
+ end
+
+ def decide(package, version)
+ @attempted_solutions += 1 if @backtracking
+ @backtracking = false;
+
+ decisions[package] = version
+ assignment = Assignment.decision(package, version, decision_level, assignments.length)
+ add_assignment(assignment)
+ end
+
+ def backtrack(previous_level)
+ @backtracking = true
+
+ new_assignments = assignments.select do |assignment|
+ assignment.decision_level <= previous_level
+ end
+
+ new_decisions = Hash[decisions.first(previous_level)]
+
+ reset!
+
+ @decisions = new_decisions
+
+ new_assignments.each do |assignment|
+ add_assignment(assignment)
+ end
+ end
+
+ private
+
+ def reset!
+ # { Array<Assignment> }
+ @assignments = []
+
+ # { Package => Array<Assignment> }
+ @assignments_by = Hash.new { |h,k| h[k] = [] }
+ @cumulative_assignments = {}.compare_by_identity
+
+ # { Package => Package::Version }
+ @decisions = {}
+
+ # { Package => Term }
+ @terms = {}
+ @relation_cache = Hash.new { |h,k| h[k] = {} }
+
+ # { Package => Boolean }
+ @required = {}
+ end
+
+ def add_assignment(assignment)
+ term = assignment.term
+ package = term.package
+
+ @assignments << assignment
+ @assignments_by[package] << assignment
+
+ @required[package] = true if term.positive?
+
+ if @terms.key?(package)
+ old_term = @terms[package]
+ @terms[package] = old_term.intersect(term)
+ else
+ @terms[package] = term
+ end
+ @relation_cache[package].clear
+
+ @cumulative_assignments[assignment] = @terms[package]
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb
new file mode 100644
index 0000000000..60ca3ca2ea
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/rubygems.rb
@@ -0,0 +1,45 @@
+module Gem::PubGrub
+ module RubyGems
+ extend self
+
+ def requirement_to_range(requirement)
+ ranges = requirement.requirements.map do |(op, ver)|
+ case op
+ when "~>"
+ name = "~> #{ver}"
+ bump = ver.class.new(ver.bump.to_s + ".A")
+ VersionRange.new(name: name, min: ver, max: bump, include_min: true)
+ when ">"
+ VersionRange.new(min: ver)
+ when ">="
+ VersionRange.new(min: ver, include_min: true)
+ when "<"
+ VersionRange.new(max: ver)
+ when "<="
+ VersionRange.new(max: ver, include_max: true)
+ when "="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true)
+ when "!="
+ VersionRange.new(min: ver, max: ver, include_min: true, include_max: true).invert
+ else
+ raise "bad version specifier: #{op}"
+ end
+ end
+
+ ranges.inject(&:intersect)
+ end
+
+ def requirement_to_constraint(package, requirement)
+ Gem::PubGrub::VersionConstraint.new(package, range: requirement_to_range(requirement))
+ end
+
+ def parse_range(dep)
+ requirement_to_range(Gem::Requirement.new(dep))
+ end
+
+ def parse_constraint(package, dep)
+ range = parse_range(dep)
+ Gem::PubGrub::VersionConstraint.new(package, range: range)
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb
new file mode 100644
index 0000000000..c4181d2b25
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/solve_failure.rb
@@ -0,0 +1,19 @@
+require_relative 'failure_writer'
+
+module Gem::PubGrub
+ class SolveFailure < StandardError
+ attr_reader :incompatibility
+
+ def initialize(incompatibility)
+ @incompatibility = incompatibility
+ end
+
+ def to_s
+ "Could not find compatible versions\n\n#{explanation}"
+ end
+
+ def explanation
+ @explanation ||= FailureWriter.new(@incompatibility).write
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb
new file mode 100644
index 0000000000..9e1de7d7a1
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/static_package_source.rb
@@ -0,0 +1,61 @@
+require_relative 'package'
+require_relative 'rubygems'
+require_relative 'version_constraint'
+require_relative 'incompatibility'
+require_relative 'basic_package_source'
+
+module Gem::PubGrub
+ class StaticPackageSource < BasicPackageSource
+ class DSL
+ def initialize(packages, root_deps)
+ @packages = packages
+ @root_deps = root_deps
+ end
+
+ def root(deps:)
+ @root_deps.update(deps)
+ end
+
+ def add(name, version, deps: {})
+ version = Gem::Version.new(version)
+ @packages[name] ||= {}
+ raise ArgumentError, "#{name} #{version} declared twice" if @packages[name].key?(version)
+ @packages[name][version] = clean_deps(name, version, deps)
+ end
+
+ private
+
+ # Exclude redundant self-referencing dependencies
+ def clean_deps(name, version, deps)
+ deps.reject {|dep_name, req| name == dep_name && Gem::PubGrub::RubyGems.parse_range(req).include?(version) }
+ end
+ end
+
+ def initialize
+ @root_deps = {}
+ @packages = {}
+
+ yield DSL.new(@packages, @root_deps)
+
+ super()
+ end
+
+ def all_versions_for(package)
+ @packages[package].keys
+ end
+
+ def root_dependencies
+ @root_deps
+ end
+
+ def dependencies_for(package, version)
+ @packages[package][version]
+ end
+
+ def parse_dependency(package, dependency)
+ return false unless @packages.key?(package)
+
+ Gem::PubGrub::RubyGems.parse_constraint(package, dependency)
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb
new file mode 100644
index 0000000000..b9874cdece
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/strategy.rb
@@ -0,0 +1,42 @@
+module Gem::PubGrub
+ class Strategy
+ def initialize(source)
+ @source = source
+
+ @root_package = Package.root
+ @root_version = Package.root_version
+
+ @version_indexes = Hash.new do |h,k|
+ if k == @root_package
+ h[k] = { @root_version => 0 }
+ else
+ h[k] = @source.all_versions_for(k).each.with_index.to_h
+ end
+ end
+ end
+
+ def next_package_and_version(unsatisfied)
+ package, range = next_term_to_try_from(unsatisfied)
+
+ [package, most_preferred_version_of(package, range)]
+ end
+
+ private
+
+ def most_preferred_version_of(package, range)
+ versions = @source.versions_for(package, range)
+
+ indexes = @version_indexes[package]
+ versions.min_by { |version| indexes[version] || Float::INFINITY }
+ end
+
+ def next_term_to_try_from(unsatisfied)
+ unsatisfied.min_by do |package, range|
+ matching_versions = @source.versions_for(package, range)
+ higher_versions = @source.versions_for(package, range.upper_invert)
+
+ [matching_versions.count <= 1 ? 0 : 1, higher_versions.count]
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb
new file mode 100644
index 0000000000..bb26bdc911
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/term.rb
@@ -0,0 +1,105 @@
+module Gem::PubGrub
+ class Term
+ attr_reader :package, :constraint, :positive
+
+ def initialize(constraint, positive)
+ @constraint = constraint
+ @package = @constraint.package
+ @positive = positive
+ end
+
+ def to_s(allow_every: false)
+ if positive
+ @constraint.to_s(allow_every: allow_every)
+ else
+ "not #{@constraint}"
+ end
+ end
+
+ def hash
+ constraint.hash ^ positive.hash
+ end
+
+ def eql?(other)
+ positive == other.positive &&
+ constraint.eql?(other.constraint)
+ end
+
+ def invert
+ self.class.new(@constraint, !@positive)
+ end
+ alias_method :inverse, :invert
+
+ def intersect(other)
+ raise ArgumentError, "packages must match" if package != other.package
+
+ if positive? && other.positive?
+ self.class.new(constraint.intersect(other.constraint), true)
+ elsif negative? && other.negative?
+ self.class.new(constraint.union(other.constraint), false)
+ else
+ positive = positive? ? self : other
+ negative = negative? ? self : other
+ self.class.new(positive.constraint.intersect(negative.constraint.invert), true)
+ end
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def relation(other)
+ if positive? && other.positive?
+ constraint.relation(other.constraint)
+ elsif negative? && other.positive?
+ if constraint.allows_all?(other.constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif positive? && other.negative?
+ if !other.constraint.allows_any?(constraint)
+ :subset
+ elsif other.constraint.allows_all?(constraint)
+ :disjoint
+ else
+ :overlap
+ end
+ elsif negative? && other.negative?
+ if constraint.allows_all?(other.constraint)
+ :subset
+ else
+ :overlap
+ end
+ else
+ raise
+ end
+ end
+
+ def normalized_constraint
+ @normalized_constraint ||= positive ? constraint : constraint.invert
+ end
+
+ def satisfies?(other)
+ raise ArgumentError, "packages must match" unless package == other.package
+
+ relation(other) == :subset
+ end
+
+ def positive?
+ @positive
+ end
+
+ def negative?
+ !positive?
+ end
+
+ def empty?
+ @empty ||= normalized_constraint.empty?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb
new file mode 100644
index 0000000000..5701bf0656
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version.rb
@@ -0,0 +1,3 @@
+module Gem::PubGrub
+ VERSION = "0.5.0"
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb
new file mode 100644
index 0000000000..ee998b3271
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_constraint.rb
@@ -0,0 +1,129 @@
+require_relative 'version_range'
+
+module Gem::PubGrub
+ class VersionConstraint
+ attr_reader :package, :range
+
+ # @param package [Gem::PubGrub::Package]
+ # @param range [Gem::PubGrub::VersionRange]
+ def initialize(package, range: nil)
+ @package = package
+ @range = range
+ end
+
+ def hash
+ package.hash ^ range.hash
+ end
+
+ def ==(other)
+ package == other.package &&
+ range == other.range
+ end
+
+ def eql?(other)
+ package.eql?(other.package) &&
+ range.eql?(other.range)
+ end
+
+ class << self
+ def exact(package, version)
+ range = VersionRange.new(min: version, max: version, include_min: true, include_max: true)
+ new(package, range: range)
+ end
+
+ def any(package)
+ new(package, range: VersionRange.any)
+ end
+
+ def empty(package)
+ new(package, range: VersionRange.empty)
+ end
+ end
+
+ def intersect(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.intersect(other.range))
+ end
+
+ def union(other)
+ unless package == other.package
+ raise ArgumentError, "Can only intersect between VersionConstraint of the same package"
+ end
+
+ self.class.new(package, range: range.union(other.range))
+ end
+
+ def invert
+ new_range = range.invert
+ self.class.new(package, range: new_range)
+ end
+
+ def difference(other)
+ intersect(other.invert)
+ end
+
+ def allows_all?(other)
+ range.allows_all?(other.range)
+ end
+
+ def allows_any?(other)
+ range.intersects?(other.range)
+ end
+
+ def subset?(other)
+ other.allows_all?(self)
+ end
+
+ def overlap?(other)
+ other.allows_any?(self)
+ end
+
+ def disjoint?(other)
+ !overlap?(other)
+ end
+
+ def relation(other)
+ if subset?(other)
+ :subset
+ elsif overlap?(other)
+ :overlap
+ else
+ :disjoint
+ end
+ end
+
+ def to_s(allow_every: false)
+ if Package.root?(package)
+ package.to_s
+ elsif allow_every && any?
+ "every version of #{package}"
+ else
+ "#{package} #{constraint_string}"
+ end
+ end
+
+ def constraint_string
+ if any?
+ ">= 0"
+ else
+ range.to_s
+ end
+ end
+
+ def empty?
+ range.empty?
+ end
+
+ # Does this match every version of the package
+ def any?
+ range.any?
+ end
+
+ def inspect
+ "#<#{self.class} #{self}>"
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb
new file mode 100644
index 0000000000..fa0e2d5742
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_range.rb
@@ -0,0 +1,423 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class VersionRange
+ attr_reader :min, :max, :include_min, :include_max
+
+ alias_method :include_min?, :include_min
+ alias_method :include_max?, :include_max
+
+ class Empty < VersionRange
+ undef_method :min, :max
+ undef_method :include_min, :include_min?
+ undef_method :include_max, :include_max?
+
+ def initialize
+ end
+
+ def empty?
+ true
+ end
+
+ def eql?(other)
+ other.empty?
+ end
+
+ def hash
+ [].hash
+ end
+
+ def intersects?(_)
+ false
+ end
+
+ def intersect(other)
+ self
+ end
+
+ def allows_all?(other)
+ other.empty?
+ end
+
+ def include?(_)
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def to_s
+ "(no versions)"
+ end
+
+ def ==(other)
+ other.class == self.class
+ end
+
+ def invert
+ VersionRange.any
+ end
+
+ def select_versions(_)
+ []
+ end
+ end
+
+ EMPTY = Empty.new
+ Empty.singleton_class.undef_method(:new)
+
+ def self.empty
+ EMPTY
+ end
+
+ def self.any
+ new
+ end
+
+ def initialize(min: nil, max: nil, include_min: false, include_max: false, name: nil)
+ raise ArgumentError, "Ranges without a lower bound cannot have include_min == true" if !min && include_min == true
+ raise ArgumentError, "Ranges without an upper bound cannot have include_max == true" if !max && include_max == true
+
+ @min = min
+ @max = max
+ @include_min = include_min
+ @include_max = include_max
+ @name = name
+ end
+
+ def hash
+ @hash ||= min.hash ^ max.hash ^ include_min.hash ^ include_max.hash
+ end
+
+ def eql?(other)
+ if other.is_a?(VersionRange)
+ !other.empty? &&
+ min.eql?(other.min) &&
+ max.eql?(other.max) &&
+ include_min.eql?(other.include_min) &&
+ include_max.eql?(other.include_max)
+ else
+ ranges.eql?(other.ranges)
+ end
+ end
+
+ def ranges
+ [self]
+ end
+
+ def include?(version)
+ compare_version(version) == 0
+ end
+
+ # Partitions passed versions into [lower, within, higher]
+ #
+ # versions must be sorted
+ def partition_versions(versions)
+ min_index =
+ if !min || versions.empty?
+ 0
+ elsif include_min?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= min }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > min }
+ end
+
+ lower = versions.slice(0, min_index)
+ versions = versions.slice(min_index, versions.size)
+
+ max_index =
+ if !max || versions.empty?
+ versions.size
+ elsif include_max?
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] > max }
+ else
+ (0..versions.size).bsearch { |i| versions[i].nil? || versions[i] >= max }
+ end
+
+ [
+ lower,
+ versions.slice(0, max_index),
+ versions.slice(max_index, versions.size)
+ ]
+ end
+
+ # Returns versions which are included by this range.
+ #
+ # versions must be sorted
+ def select_versions(versions)
+ return versions if any?
+
+ partition_versions(versions)[1]
+ end
+
+ def compare_version(version)
+ if min
+ case version <=> min
+ when -1
+ return -1
+ when 0
+ return -1 if !include_min
+ when 1
+ end
+ end
+
+ if max
+ case version <=> max
+ when -1
+ when 0
+ return 1 if !include_max
+ when 1
+ return 1
+ end
+ end
+
+ 0
+ end
+
+ def strictly_lower?(other)
+ return false if !max || !other.min
+
+ case max <=> other.min
+ when 0
+ !include_max || !other.include_min
+ when -1
+ true
+ when 1
+ false
+ end
+ end
+
+ def strictly_higher?(other)
+ other.strictly_lower?(self)
+ end
+
+ def intersects?(other)
+ return false if other.empty?
+ return other.intersects?(self) if other.is_a?(VersionUnion)
+ !strictly_lower?(other) && !strictly_higher?(other)
+ end
+ alias_method :allows_any?, :intersects?
+
+ def intersect(other)
+ return other if other.empty?
+ return other.intersect(self) if other.is_a?(VersionUnion)
+
+ min_range =
+ if !min
+ other
+ elsif !other.min
+ self
+ else
+ case min <=> other.min
+ when 0
+ include_min ? other : self
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ max_range =
+ if !max
+ other
+ elsif !other.max
+ self
+ else
+ case max <=> other.max
+ when 0
+ include_max ? other : self
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ if !min_range.equal?(max_range) && min_range.min && max_range.max
+ case min_range.min <=> max_range.max
+ when -1
+ when 0
+ if !min_range.include_min || !max_range.include_max
+ return EMPTY
+ end
+ when 1
+ return EMPTY
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ # The span covered by two ranges
+ #
+ # If self and other are contiguous, this builds a union of the two ranges.
+ # (if they aren't you are probably calling the wrong method)
+ def span(other)
+ return self if other.empty?
+
+ min_range =
+ if !min
+ self
+ elsif !other.min
+ other
+ else
+ case min <=> other.min
+ when 0
+ include_min ? self : other
+ when -1
+ self
+ when 1
+ other
+ end
+ end
+
+ max_range =
+ if !max
+ self
+ elsif !other.max
+ other
+ else
+ case max <=> other.max
+ when 0
+ include_max ? self : other
+ when -1
+ other
+ when 1
+ self
+ end
+ end
+
+ VersionRange.new(
+ min: min_range.min,
+ include_min: min_range.include_min,
+ max: max_range.max,
+ include_max: max_range.include_max
+ )
+ end
+
+ def union(other)
+ return other.union(self) if other.is_a?(VersionUnion)
+
+ if contiguous_to?(other)
+ span(other)
+ else
+ VersionUnion.union([self, other])
+ end
+ end
+
+ def contiguous_to?(other)
+ return false if other.empty?
+ return true if any?
+
+ intersects?(other) || contiguous_below?(other) || contiguous_above?(other)
+ end
+
+ def contiguous_below?(other)
+ return false if !max || !other.min
+
+ max == other.min && (include_max || other.include_min)
+ end
+
+ def contiguous_above?(other)
+ other.contiguous_below?(self)
+ end
+
+ def allows_all?(other)
+ return true if other.empty?
+
+ if other.is_a?(VersionUnion)
+ return VersionUnion.new([self]).allows_all?(other)
+ end
+
+ return false if max && !other.max
+ return false if min && !other.min
+
+ if min
+ case min <=> other.min
+ when -1
+ when 0
+ return false if !include_min && other.include_min
+ when 1
+ return false
+ end
+ end
+
+ if max
+ case max <=> other.max
+ when -1
+ return false
+ when 0
+ return false if !include_max && other.include_max
+ when 1
+ end
+ end
+
+ true
+ end
+
+ def any?
+ !min && !max
+ end
+
+ def empty?
+ false
+ end
+
+ def to_s
+ @name ||= constraints.join(", ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def upper_invert
+ return self.class.empty unless max
+
+ VersionRange.new(min: max, include_min: !include_max)
+ end
+
+ def invert
+ return self.class.empty if any?
+
+ low = -> { VersionRange.new(max: min, include_max: !include_min) }
+ high = -> { VersionRange.new(min: max, include_min: !include_max) }
+
+ if !min
+ high.call
+ elsif !max
+ low.call
+ else
+ low.call.union(high.call)
+ end
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ min == other.min &&
+ max == other.max &&
+ include_min == other.include_min &&
+ include_max == other.include_max
+ end
+
+ private
+
+ def constraints
+ return ["any"] if any?
+ return ["= #{min}"] if min.to_s == max.to_s
+
+ c = []
+ c << "#{include_min ? ">=" : ">"} #{min}" if min
+ c << "#{include_max ? "<=" : "<"} #{max}" if max
+ c
+ end
+
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb
new file mode 100644
index 0000000000..3341d8fe3b
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_solver.rb
@@ -0,0 +1,236 @@
+require_relative 'partial_solution'
+require_relative 'term'
+require_relative 'incompatibility'
+require_relative 'solve_failure'
+require_relative 'strategy'
+
+module Gem::PubGrub
+ class VersionSolver
+ attr_reader :logger
+ attr_reader :source
+ attr_reader :solution
+ attr_reader :strategy
+
+ def initialize(source:, root: Package.root, strategy: Strategy.new(source), logger: Gem::PubGrub.logger)
+ @logger = logger
+
+ @source = source
+ @strategy = strategy
+
+ # { package => [incompatibility, ...]}
+ @incompatibilities = Hash.new do |h, k|
+ h[k] = []
+ end
+
+ @seen_incompatibilities = {}
+
+ @solution = PartialSolution.new
+
+ add_incompatibility Incompatibility.new([
+ Term.new(VersionConstraint.any(root), false)
+ ], cause: :root)
+
+ propagate(root)
+ end
+
+ def solved?
+ solution.unsatisfied.empty?
+ end
+
+ # Returns true if there is more work to be done, false otherwise
+ def work
+ unsatisfied_terms = solution.unsatisfied
+ if unsatisfied_terms.empty?
+ logger.info { "Solution found after #{solution.attempted_solutions} attempts:" }
+ solution.decisions.each do |package, version|
+ next if Package.root?(package)
+ logger.info { "* #{package} #{version}" }
+ end
+
+ return false
+ end
+
+ next_package = choose_package_version_from(unsatisfied_terms)
+ propagate(next_package)
+
+ true
+ end
+
+ def solve
+ while work; end
+
+ solution.decisions
+ end
+
+ alias_method :result, :solve
+
+ private
+
+ def propagate(initial_package)
+ changed = [initial_package]
+ while package = changed.shift
+ @incompatibilities[package].reverse_each do |incompatibility|
+ result = propagate_incompatibility(incompatibility)
+ if result == :conflict
+ root_cause = resolve_conflict(incompatibility)
+ changed.clear
+ changed << propagate_incompatibility(root_cause)
+ elsif result # should be a Package
+ changed << result
+ end
+ end
+ changed.uniq!
+ end
+ end
+
+ def propagate_incompatibility(incompatibility)
+ unsatisfied = nil
+ incompatibility.terms.each do |term|
+ relation = solution.relation(term)
+ if relation == :disjoint
+ return nil
+ elsif relation == :overlap
+ # If more than one term is inconclusive, we can't deduce anything
+ return nil if unsatisfied
+ unsatisfied = term
+ end
+ end
+
+ if !unsatisfied
+ return :conflict
+ end
+
+ logger.debug { "derived: #{unsatisfied.invert}" }
+
+ solution.derive(unsatisfied.invert, incompatibility)
+
+ unsatisfied.package
+ end
+
+ def choose_package_version_from(unsatisfied_terms)
+ remaining = unsatisfied_terms.map { |t| [t.package, t.constraint.range] }.to_h
+
+ package, version = strategy.next_package_and_version(remaining)
+
+ logger.debug { "attempting #{package} #{version}" }
+
+ if version.nil?
+ unsatisfied_term = unsatisfied_terms.find { |t| t.package == package }
+ add_incompatibility source.no_versions_incompatibility_for(package, unsatisfied_term)
+ return package
+ end
+
+ conflict = false
+
+ source.incompatibilities_for(package, version).each do |incompatibility|
+ if @seen_incompatibilities.include?(incompatibility)
+ logger.debug { "knew: #{incompatibility}" }
+ next
+ end
+ @seen_incompatibilities[incompatibility] = true
+
+ add_incompatibility incompatibility
+
+ conflict ||= incompatibility.terms.all? do |term|
+ term.package == package || solution.satisfies?(term)
+ end
+ end
+
+ unless conflict
+ logger.info { "selected #{package} #{version}" }
+
+ solution.decide(package, version)
+ else
+ logger.info { "conflict: #{conflict.inspect}" }
+ end
+
+ package
+ end
+
+ def resolve_conflict(incompatibility)
+ logger.info { "conflict: #{incompatibility}" }
+
+ new_incompatibility = nil
+
+ while !incompatibility.failure?
+ most_recent_term = nil
+ most_recent_satisfier = nil
+ difference = nil
+
+ previous_level = 1
+
+ incompatibility.terms.each do |term|
+ satisfier = solution.satisfier(term)
+
+ if most_recent_satisfier.nil?
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ elsif most_recent_satisfier.index < satisfier.index
+ previous_level = [previous_level, most_recent_satisfier.decision_level].max
+ most_recent_term = term
+ most_recent_satisfier = satisfier
+ difference = nil
+ else
+ previous_level = [previous_level, satisfier.decision_level].max
+ end
+
+ if most_recent_term == term
+ difference = most_recent_satisfier.term.difference(most_recent_term)
+ if difference.empty?
+ difference = nil
+ else
+ difference_satisfier = solution.satisfier(difference.inverse)
+ previous_level = [previous_level, difference_satisfier.decision_level].max
+ end
+ end
+ end
+
+ if previous_level < most_recent_satisfier.decision_level ||
+ most_recent_satisfier.decision?
+
+ logger.info { "backtracking to #{previous_level}" }
+ solution.backtrack(previous_level)
+
+ if new_incompatibility
+ add_incompatibility(new_incompatibility)
+ end
+
+ return incompatibility
+ end
+
+ new_terms = []
+ new_terms += incompatibility.terms - [most_recent_term]
+ new_terms += most_recent_satisfier.cause.terms.reject { |term|
+ term.package == most_recent_satisfier.term.package
+ }
+ if difference
+ new_terms << difference.invert
+ end
+
+ new_incompatibility = Incompatibility.new(new_terms, cause: Incompatibility::ConflictCause.new(incompatibility, most_recent_satisfier.cause))
+
+ if incompatibility.to_s == new_incompatibility.to_s
+ logger.info { "!! failed to resolve conflicts, this shouldn't have happened" }
+ break
+ end
+
+ incompatibility = new_incompatibility
+
+ partially = difference ? " partially" : ""
+ logger.info { "! #{most_recent_term} is#{partially} satisfied by #{most_recent_satisfier.term}" }
+ logger.info { "! which is caused by #{most_recent_satisfier.cause}" }
+ logger.info { "! thus #{incompatibility}" }
+ end
+
+ raise SolveFailure.new(incompatibility)
+ end
+
+ def add_incompatibility(incompatibility)
+ logger.debug { "fact: #{incompatibility}" }
+ incompatibility.terms.each do |term|
+ package = term.package
+ @incompatibilities[package] << incompatibility
+ end
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb
new file mode 100644
index 0000000000..4166318a98
--- /dev/null
+++ b/lib/rubygems/vendor/pub_grub/lib/pub_grub/version_union.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+module Gem::PubGrub
+ class VersionUnion
+ attr_reader :ranges
+
+ def self.normalize_ranges(ranges)
+ ranges = ranges.flat_map do |range|
+ range.ranges
+ end
+
+ ranges.reject!(&:empty?)
+
+ return [] if ranges.empty?
+
+ mins, ranges = ranges.partition { |r| !r.min }
+ original_ranges = mins + ranges.sort_by { |r| [r.min, r.include_min ? 0 : 1] }
+ ranges = [original_ranges.shift]
+ original_ranges.each do |range|
+ if ranges.last.contiguous_to?(range)
+ ranges << ranges.pop.span(range)
+ else
+ ranges << range
+ end
+ end
+
+ ranges
+ end
+
+ def self.union(ranges, normalize: true)
+ ranges = normalize_ranges(ranges) if normalize
+
+ if ranges.size == 0
+ VersionRange.empty
+ elsif ranges.size == 1
+ ranges[0]
+ else
+ new(ranges)
+ end
+ end
+
+ def initialize(ranges)
+ raise ArgumentError unless ranges.all? { |r| r.instance_of?(VersionRange) }
+ @ranges = ranges
+ end
+
+ def hash
+ ranges.hash
+ end
+
+ def eql?(other)
+ ranges.eql?(other.ranges)
+ end
+
+ def include?(version)
+ !!ranges.bsearch {|r| r.compare_version(version) }
+ end
+
+ def select_versions(all_versions)
+ versions = []
+ ranges.inject(all_versions) do |acc, range|
+ _, matching, higher = range.partition_versions(acc)
+ versions.concat matching
+ higher
+ end
+ versions
+ end
+
+ def intersects?(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ if my_range.intersects?(other_range)
+ return true
+ end
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ end
+ alias_method :allows_any?, :intersects?
+
+ def allows_all?(other)
+ my_ranges = ranges.dup
+
+ my_range = my_ranges.shift
+
+ other.ranges.all? do |other_range|
+ while my_range
+ break if my_range.allows_all?(other_range)
+ my_range = my_ranges.shift
+ end
+
+ !!my_range
+ end
+ end
+
+ def empty?
+ false
+ end
+
+ def any?
+ false
+ end
+
+ def intersect(other)
+ my_ranges = ranges.dup
+ other_ranges = other.ranges.dup
+ new_ranges = []
+
+ my_range = my_ranges.shift
+ other_range = other_ranges.shift
+ while my_range && other_range
+ new_ranges << my_range.intersect(other_range)
+
+ if !my_range.max || other_range.empty? || (other_range.max && other_range.max < my_range.max)
+ other_range = other_ranges.shift
+ else
+ my_range = my_ranges.shift
+ end
+ end
+ new_ranges.reject!(&:empty?)
+ VersionUnion.union(new_ranges, normalize: false)
+ end
+
+ def upper_invert
+ ranges.last.upper_invert
+ end
+
+ def invert
+ ranges.map(&:invert).inject(:intersect)
+ end
+
+ def union(other)
+ VersionUnion.union([self, other])
+ end
+
+ def to_s
+ output = []
+
+ ranges = self.ranges.dup
+ while !ranges.empty?
+ ne = []
+ range = ranges.shift
+ while !ranges.empty? && ranges[0].min.to_s == range.max.to_s
+ ne << range.max
+ range = range.span(ranges.shift)
+ end
+
+ ne.map! {|x| "!= #{x}" }
+ if ne.empty?
+ output << range.to_s
+ elsif range.any?
+ output << ne.join(', ')
+ else
+ output << "#{range}, #{ne.join(', ')}"
+ end
+ end
+
+ output.join(" OR ")
+ end
+
+ def inspect
+ "#<#{self.class} #{to_s}>"
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ self.ranges == other.ranges
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/resolv/lib/resolv.rb b/lib/rubygems/vendor/resolv/lib/resolv.rb
new file mode 100644
index 0000000000..4f48e0642b
--- /dev/null
+++ b/lib/rubygems/vendor/resolv/lib/resolv.rb
@@ -0,0 +1,3499 @@
+# frozen_string_literal: true
+
+require 'socket'
+require_relative '../../../vendored_timeout'
+require 'io/wait'
+require_relative '../../../vendored_securerandom'
+require 'rbconfig'
+
+# Gem::Resolv is a thread-aware DNS resolver library written in Ruby. Gem::Resolv can
+# handle multiple DNS requests concurrently without blocking the entire Ruby
+# interpreter.
+#
+# See also resolv-replace.rb to replace the libc resolver with Gem::Resolv.
+#
+# Gem::Resolv can look up various DNS resources using the DNS module directly.
+#
+# Examples:
+#
+# p Gem::Resolv.getaddress "www.ruby-lang.org"
+# p Gem::Resolv.getname "210.251.121.214"
+#
+# Gem::Resolv::DNS.open do |dns|
+# ress = dns.getresources "www.ruby-lang.org", Gem::Resolv::DNS::Resource::IN::A
+# p ress.map(&:address)
+# ress = dns.getresources "ruby-lang.org", Gem::Resolv::DNS::Resource::IN::MX
+# p ress.map { |r| [r.exchange.to_s, r.preference] }
+# end
+#
+#
+# == Bugs
+#
+# * NIS is not supported.
+# * /etc/nsswitch.conf is not supported.
+
+class Gem::Resolv
+
+ # The version string
+ VERSION = "0.7.0"
+
+ ##
+ # Looks up the first IP address for +name+.
+
+ def self.getaddress(name)
+ DefaultResolver.getaddress(name)
+ end
+
+ ##
+ # Looks up all IP address for +name+.
+
+ def self.getaddresses(name)
+ DefaultResolver.getaddresses(name)
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+.
+
+ def self.each_address(name, &block)
+ DefaultResolver.each_address(name, &block)
+ end
+
+ ##
+ # Looks up the hostname of +address+.
+
+ def self.getname(address)
+ DefaultResolver.getname(address)
+ end
+
+ ##
+ # Looks up all hostnames for +address+.
+
+ def self.getnames(address)
+ DefaultResolver.getnames(address)
+ end
+
+ ##
+ # Iterates over all hostnames for +address+.
+
+ def self.each_name(address, &proc)
+ DefaultResolver.each_name(address, &proc)
+ end
+
+ ##
+ # Creates a new Gem::Resolv using +resolvers+.
+ #
+ # If +resolvers+ is not given, a hash, or +nil+, uses a Hosts resolver and
+ # and a DNS resolver. If +resolvers+ is a hash, uses the hash as
+ # configuration for the DNS resolver.
+
+ def initialize(resolvers=(arg_not_set = true; nil), use_ipv6: (keyword_not_set = true; nil))
+ if !keyword_not_set && !arg_not_set
+ warn "Support for separate use_ipv6 keyword is deprecated, as it is ignored if an argument is provided. Do not provide a positional argument if using the use_ipv6 keyword argument.", uplevel: 1
+ end
+
+ @resolvers = case resolvers
+ when Hash, nil
+ [Hosts.new, DNS.new(DNS::Config.default_config_hash.merge(resolvers || {}))]
+ else
+ resolvers
+ end
+ end
+
+ ##
+ # Looks up the first IP address for +name+.
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("no address for #{name}")
+ end
+
+ ##
+ # Looks up all IP address for +name+.
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+.
+
+ def each_address(name)
+ if AddressRegex =~ name
+ yield name
+ return
+ end
+ yielded = false
+ @resolvers.each {|r|
+ r.each_address(name) {|address|
+ yield address.to_s
+ yielded = true
+ }
+ return if yielded
+ }
+ end
+
+ ##
+ # Looks up the hostname of +address+.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("no name for #{address}")
+ end
+
+ ##
+ # Looks up all hostnames for +address+.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+.
+
+ def each_name(address)
+ yielded = false
+ @resolvers.each {|r|
+ r.each_name(address) {|name|
+ yield name.to_s
+ yielded = true
+ }
+ return if yielded
+ }
+ end
+
+ ##
+ # Indicates a failure to resolve a name or address.
+
+ class ResolvError < StandardError; end
+
+ ##
+ # Indicates a timeout resolving a name or address.
+
+ class ResolvTimeout < Gem::Timeout::Error; end
+
+ ##
+ # Gem::Resolv::Hosts is a hostname resolver that uses the system hosts file.
+
+ class Hosts
+ if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM || ::RbConfig::CONFIG['host_os'] =~ /mswin/
+ begin
+ require 'win32/resolv' unless defined?(Win32::Resolv)
+ hosts = Win32::Resolv.get_hosts_path || IO::NULL
+ rescue LoadError
+ end
+ end
+ # The default file name for host names
+ DefaultFileName = hosts || '/etc/hosts'
+
+ ##
+ # Creates a new Gem::Resolv::Hosts, using +filename+ for its data source.
+
+ def initialize(filename = DefaultFileName)
+ @filename = filename
+ @mutex = Thread::Mutex.new
+ @initialized = nil
+ end
+
+ def lazy_initialize # :nodoc:
+ @mutex.synchronize {
+ unless @initialized
+ @name2addr = {}
+ @addr2name = {}
+ File.open(@filename, 'rb') {|f|
+ f.each {|line|
+ line.sub!(/#.*/, '')
+ addr, *hostnames = line.split(/\s+/)
+ next unless addr
+ (@addr2name[addr] ||= []).concat(hostnames)
+ hostnames.each {|hostname| (@name2addr[hostname] ||= []) << addr}
+ }
+ }
+ @name2addr.each {|name, arr| arr.reverse!}
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ ##
+ # Gets the IP address of +name+ from the hosts file.
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("#{@filename} has no name: #{name}")
+ end
+
+ ##
+ # Gets all IP addresses for +name+ from the hosts file.
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the hosts file.
+
+ def each_address(name, &proc)
+ lazy_initialize
+ @name2addr[name]&.each(&proc)
+ end
+
+ ##
+ # Gets the hostname of +address+ from the hosts file.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("#{@filename} has no address: #{address}")
+ end
+
+ ##
+ # Gets all hostnames for +address+ from the hosts file.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the hosts file.
+
+ def each_name(address, &proc)
+ lazy_initialize
+ @addr2name[address]&.each(&proc)
+ end
+ end
+
+ ##
+ # Gem::Resolv::DNS is a DNS stub resolver.
+ #
+ # Information taken from the following places:
+ #
+ # * STD0013
+ # * RFC 1035
+ # * ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
+ # * etc.
+
+ class DNS
+
+ ##
+ # Default DNS Port
+
+ Port = 53
+
+ ##
+ # Default DNS UDP packet size
+
+ UDPSize = 512
+
+ ##
+ # Creates a new DNS resolver. See Gem::Resolv::DNS.new for argument details.
+ #
+ # Yields the created DNS resolver to the block, if given, otherwise
+ # returns it.
+
+ def self.open(*args)
+ dns = new(*args)
+ return dns unless block_given?
+ begin
+ yield dns
+ ensure
+ dns.close
+ end
+ end
+
+ ##
+ # Creates a new DNS resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil:: Uses /etc/resolv.conf.
+ # String:: Path to a file using /etc/resolv.conf's format.
+ # Hash:: Must contain :nameserver, :search and :ndots keys.
+ # :nameserver_port can be used to specify port number of nameserver address.
+ # :raise_timeout_errors can be used to raise timeout errors
+ # as exceptions instead of treating the same as an NXDOMAIN response.
+ #
+ # The value of :nameserver should be an address string or
+ # an array of address strings.
+ # - :nameserver => '8.8.8.8'
+ # - :nameserver => ['8.8.8.8', '8.8.4.4']
+ #
+ # The value of :nameserver_port should be an array of
+ # pair of nameserver address and port number.
+ # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]]
+ #
+ # Example:
+ #
+ # Gem::Resolv::DNS.new(:nameserver => ['210.251.121.21'],
+ # :search => ['ruby-lang.org'],
+ # :ndots => 1)
+
+ def initialize(config_info=nil)
+ @mutex = Thread::Mutex.new
+ @config = Config.new(config_info)
+ @initialized = nil
+ end
+
+ # Sets the resolver timeouts. This may be a single positive number
+ # or an array of positive numbers representing timeouts in seconds.
+ # If an array is specified, a DNS request will retry and wait for
+ # each successive interval in the array until a successful response
+ # is received. Specifying +nil+ reverts to the default timeouts:
+ # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ]
+ #
+ # Example:
+ #
+ # dns.timeouts = 3
+ #
+ def timeouts=(values)
+ @config.timeouts = values
+ end
+
+ def lazy_initialize # :nodoc:
+ @mutex.synchronize {
+ unless @initialized
+ @config.lazy_initialize
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ ##
+ # Closes the DNS resolver.
+
+ def close
+ @mutex.synchronize {
+ if @initialized
+ @initialized = false
+ end
+ }
+ end
+
+ ##
+ # Gets the IP address of +name+ from the DNS resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved address will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def getaddress(name)
+ each_address(name) {|address| return address}
+ raise ResolvError.new("DNS result has no information for #{name}")
+ end
+
+ ##
+ # Gets all IP addresses for +name+ from the DNS resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def getaddresses(name)
+ ret = []
+ each_address(name) {|address| ret << address}
+ return ret
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the DNS
+ # resolver.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def each_address(name)
+ if use_ipv6?
+ each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address}
+ end
+ each_resource(name, Resource::IN::A) {|resource| yield resource.address}
+ end
+
+ def use_ipv6? # :nodoc:
+ @config.lazy_initialize unless @config.instance_variable_get(:@initialized)
+
+ use_ipv6 = @config.use_ipv6?
+ unless use_ipv6.nil?
+ return use_ipv6
+ end
+
+ begin
+ list = Socket.ip_address_list
+ rescue NotImplementedError
+ return true
+ end
+ list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? }
+ end
+ private :use_ipv6?
+
+ ##
+ # Gets the hostname for +address+ from the DNS resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # name will be a Gem::Resolv::DNS::Name.
+
+ def getname(address)
+ each_name(address) {|name| return name}
+ raise ResolvError.new("DNS result has no information for #{address}")
+ end
+
+ ##
+ # Gets all hostnames for +address+ from the DNS resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # names will be Gem::Resolv::DNS::Name instances.
+
+ def getnames(address)
+ ret = []
+ each_name(address) {|name| ret << name}
+ return ret
+ end
+
+ ##
+ # Iterates over all hostnames for +address+ retrieved from the DNS
+ # resolver.
+ #
+ # +address+ must be a Gem::Resolv::IPv4, Gem::Resolv::IPv6 or a String. Retrieved
+ # names will be Gem::Resolv::DNS::Name instances.
+
+ def each_name(address)
+ case address
+ when Name
+ ptr = address
+ when IPv4, IPv6
+ ptr = address.to_name
+ when IPv4::Regex
+ ptr = IPv4.create(address).to_name
+ when IPv6::Regex
+ ptr = IPv6.create(address).to_name
+ else
+ raise ResolvError.new("cannot interpret as address: #{address}")
+ end
+ each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name}
+ end
+
+ ##
+ # Look up the +typeclass+ DNS resource of +name+.
+ #
+ # +name+ must be a Gem::Resolv::DNS::Name or a String.
+ #
+ # +typeclass+ should be one of the following:
+ #
+ # * Gem::Resolv::DNS::Resource::IN::A
+ # * Gem::Resolv::DNS::Resource::IN::AAAA
+ # * Gem::Resolv::DNS::Resource::IN::ANY
+ # * Gem::Resolv::DNS::Resource::IN::CNAME
+ # * Gem::Resolv::DNS::Resource::IN::HINFO
+ # * Gem::Resolv::DNS::Resource::IN::MINFO
+ # * Gem::Resolv::DNS::Resource::IN::MX
+ # * Gem::Resolv::DNS::Resource::IN::NS
+ # * Gem::Resolv::DNS::Resource::IN::PTR
+ # * Gem::Resolv::DNS::Resource::IN::SOA
+ # * Gem::Resolv::DNS::Resource::IN::TXT
+ # * Gem::Resolv::DNS::Resource::IN::WKS
+ #
+ # Returned resource is represented as a Gem::Resolv::DNS::Resource instance,
+ # i.e. Gem::Resolv::DNS::Resource::IN::A.
+
+ def getresource(name, typeclass)
+ each_resource(name, typeclass) {|resource| return resource}
+ raise ResolvError.new("DNS result has no information for #{name}")
+ end
+
+ ##
+ # Looks up all +typeclass+ DNS resources for +name+. See #getresource for
+ # argument details.
+
+ def getresources(name, typeclass)
+ ret = []
+ each_resource(name, typeclass) {|resource| ret << resource}
+ return ret
+ end
+
+ ##
+ # Iterates over all +typeclass+ DNS resources for +name+. See
+ # #getresource for argument details.
+
+ def each_resource(name, typeclass, &proc)
+ fetch_resource(name, typeclass) {|reply, reply_name|
+ extract_resources(reply, reply_name, typeclass, &proc)
+ }
+ end
+
+ # :stopdoc:
+
+ def fetch_resource(name, typeclass)
+ lazy_initialize
+ truncated = {}
+ requesters = {}
+ udp_requester = begin
+ make_udp_requester
+ rescue Errno::EACCES
+ # fall back to TCP
+ end
+ senders = {}
+
+ begin
+ @config.resolv(name) do |candidate, tout, nameserver, port|
+ msg = Message.new
+ msg.rd = 1
+ msg.add_question(candidate, typeclass)
+
+ requester = requesters.fetch([nameserver, port]) do
+ if !truncated[candidate] && udp_requester
+ udp_requester
+ else
+ requesters[[nameserver, port]] = make_tcp_requester(nameserver, port)
+ end
+ end
+
+ unless sender = senders[[candidate, requester, nameserver, port]]
+ sender = requester.sender(msg, candidate, nameserver, port)
+ next if !sender
+ senders[[candidate, requester, nameserver, port]] = sender
+ end
+ reply, reply_name = requester.request(sender, tout)
+ case reply.rcode
+ when RCode::NoError
+ if reply.tc == 1 and not Requester::TCP === requester
+ # Retry via TCP:
+ truncated[candidate] = true
+ redo
+ else
+ yield(reply, reply_name)
+ end
+ return
+ when RCode::NXDomain
+ raise Config::NXDomain.new(reply_name.to_s)
+ else
+ raise Config::OtherResolvError.new(reply_name.to_s)
+ end
+ end
+ ensure
+ udp_requester&.close
+ requesters.each_value { |requester| requester&.close }
+ end
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ if nameserver_port.length == 1
+ Requester::ConnectedUDP.new(*nameserver_port[0])
+ else
+ Requester::UnconnectedUDP.new(*nameserver_port)
+ end
+ end
+
+ def make_tcp_requester(host, port) # :nodoc:
+ return Requester::TCP.new(host, port)
+ rescue Errno::ECONNREFUSED
+ # Treat a refused TCP connection attempt to a nameserver like a timeout,
+ # as Gem::Resolv::DNS::Config#resolv considers ResolvTimeout exceptions as a
+ # hint to try the next nameserver:
+ raise ResolvTimeout
+ end
+
+ def extract_resources(msg, name, typeclass) # :nodoc:
+ if typeclass < Resource::ANY
+ n0 = Name.create(name)
+ msg.each_resource {|n, ttl, data|
+ yield data if n0 == n
+ }
+ end
+ yielded = false
+ n0 = Name.create(name)
+ msg.each_resource {|n, ttl, data|
+ if n0 == n
+ case data
+ when typeclass
+ yield data
+ yielded = true
+ when Resource::CNAME
+ n0 = data.name
+ end
+ end
+ }
+ return if yielded
+ msg.each_resource {|n, ttl, data|
+ if n0 == n
+ case data
+ when typeclass
+ yield data
+ end
+ end
+ }
+ end
+
+ def self.random(arg) # :nodoc:
+ begin
+ Gem::SecureRandom.random_number(arg)
+ rescue NotImplementedError
+ rand(arg)
+ end
+ end
+
+ RequestID = {} # :nodoc:
+ RequestIDMutex = Thread::Mutex.new # :nodoc:
+
+ def self.allocate_request_id(host, port) # :nodoc:
+ id = nil
+ RequestIDMutex.synchronize {
+ h = (RequestID[[host, port]] ||= {})
+ begin
+ id = random(0x0000..0xffff)
+ end while h[id]
+ h[id] = true
+ }
+ id
+ end
+
+ def self.free_request_id(host, port, id) # :nodoc:
+ RequestIDMutex.synchronize {
+ key = [host, port]
+ if h = RequestID[key]
+ h.delete id
+ if h.empty?
+ RequestID.delete key
+ end
+ end
+ }
+ end
+
+ case RUBY_PLATFORM
+ when *[
+ # https://www.rfc-editor.org/rfc/rfc6056.txt
+ # Appendix A. Survey of the Algorithms in Use by Some Popular Implementations
+ /freebsd/, /linux/, /netbsd/, /openbsd/, /solaris/,
+ /darwin/, # the same as FreeBSD
+ ] then
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ udpsock.bind(bind_host, 0)
+ end
+ else
+ # Sequential port assignment
+ def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc:
+ # Ephemeral port number range recommended by RFC 6056
+ port = random(1024..65535)
+ udpsock.bind(bind_host, port)
+ rescue Errno::EADDRINUSE, # POSIX
+ Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5)
+ Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4).
+ retry
+ end
+ end
+
+ class Requester # :nodoc:
+ def initialize
+ @senders = {}
+ @socks = nil
+ end
+
+ def request(sender, tout)
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timelimit = start + tout
+ begin
+ sender.send
+ rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this
+ Errno::ENETUNREACH
+ raise ResolvTimeout
+ end
+ while true
+ before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ timeout = timelimit - before_select
+ if timeout <= 0
+ raise ResolvTimeout
+ end
+ if @socks.size == 1
+ select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil
+ else
+ select_result = IO.select(@socks, nil, nil, timeout)
+ end
+ if !select_result
+ after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC)
+ next if after_select < timelimit
+ raise ResolvTimeout
+ end
+ begin
+ reply, from = recv_reply(select_result[0])
+ rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD
+ Errno::ECONNRESET # Windows
+ # No name server running on the server?
+ # Don't wait anymore.
+ raise ResolvTimeout
+ end
+ begin
+ msg = Message.decode(reply)
+ rescue DecodeError
+ next # broken DNS message ignored
+ end
+ if sender == sender_for(from, msg)
+ break
+ else
+ # unexpected DNS message ignored
+ end
+ end
+ return msg, sender.data
+ end
+
+ def sender_for(addr, msg)
+ @senders[[addr,msg.id]]
+ end
+
+ def close
+ socks = @socks
+ @socks = nil
+ socks&.each(&:close)
+ end
+
+ class Sender # :nodoc:
+ def initialize(msg, data, sock)
+ @msg = msg
+ @data = data
+ @sock = sock
+ end
+ end
+
+ class UnconnectedUDP < Requester # :nodoc:
+ def initialize(*nameserver_port)
+ super()
+ @nameserver_port = nameserver_port
+ @initialized = false
+ @mutex = Thread::Mutex.new
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ @socks_hash = {}
+ @socks = []
+ @nameserver_port.each {|host, port|
+ if host.index(':')
+ bind_host = "::"
+ af = Socket::AF_INET6
+ else
+ bind_host = "0.0.0.0"
+ af = Socket::AF_INET
+ end
+ next if @socks_hash[bind_host]
+ begin
+ sock = UDPSocket.new(af)
+ rescue Errno::EAFNOSUPPORT, Errno::EPROTONOSUPPORT
+ next # The kernel doesn't support the address family.
+ end
+ @socks << sock
+ @socks_hash[bind_host] = sock
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, bind_host)
+ }
+ }
+ self
+ end
+
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply, from = readable_socks[0].recvfrom(UDPSize)
+ return reply, [from[3],from[1]]
+ end
+
+ def sender(msg, data, host, port=Port)
+ host = Addrinfo.ip(host).ip_address
+ lazy_initialize
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return nil if !sock
+ service = [host, port]
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ return @senders[[service, id]] =
+ Sender.new(request, data, sock, host, port)
+ end
+
+ def close
+ @mutex.synchronize {
+ if @initialized
+ super
+ @senders.each_key {|service, id|
+ DNS.free_request_id(service[0], service[1], id)
+ }
+ @initialized = false
+ end
+ }
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def initialize(msg, data, sock, host, port)
+ super(msg, data, sock)
+ @host = host
+ @port = port
+ end
+ attr_reader :data
+
+ def send
+ raise "@sock is nil." if @sock.nil?
+ @sock.send(@msg, 0, @host, @port)
+ end
+ end
+ end
+
+ class ConnectedUDP < Requester # :nodoc:
+ def initialize(host, port=Port)
+ super()
+ @host = host
+ @port = port
+ @mutex = Thread::Mutex.new
+ @initialized = false
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ next if @initialized
+ @initialized = true
+ is_ipv6 = @host.index(':')
+ sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET)
+ @socks = [sock]
+ sock.do_not_reverse_lookup = true
+ DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0")
+ sock.connect(@host, @port)
+ }
+ self
+ end
+
+ def recv_reply(readable_socks)
+ lazy_initialize
+ reply = readable_socks[0].recv(UDPSize)
+ return reply, nil
+ end
+
+ def sender(msg, data, host=@host, port=@port)
+ lazy_initialize
+ unless host == @host && port == @port
+ raise RequestError.new("host/port don't match: #{host}:#{port}")
+ end
+ id = DNS.allocate_request_id(@host, @port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
+ end
+
+ def close
+ @mutex.synchronize do
+ if @initialized
+ super
+ @senders.each_key {|from, id|
+ DNS.free_request_id(@host, @port, id)
+ }
+ @initialized = false
+ end
+ end
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def send
+ raise "@sock is nil." if @sock.nil?
+ @sock.send(@msg, 0)
+ end
+ attr_reader :data
+ end
+ end
+
+ class MDNSOneShot < UnconnectedUDP # :nodoc:
+ def sender(msg, data, host, port=Port)
+ lazy_initialize
+ id = DNS.allocate_request_id(host, port)
+ request = msg.encode
+ request[0,2] = [id].pack('n')
+ sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"]
+ return @senders[id] =
+ UnconnectedUDP::Sender.new(request, data, sock, host, port)
+ end
+
+ def sender_for(addr, msg)
+ lazy_initialize
+ @senders[msg.id]
+ end
+ end
+
+ class TCP < Requester # :nodoc:
+ def initialize(host, port=Port)
+ super()
+ @host = host
+ @port = port
+ sock = TCPSocket.new(@host, @port)
+ @socks = [sock]
+ @senders = {}
+ end
+
+ def recv_reply(readable_socks)
+ len = readable_socks[0].read(2).unpack('n')[0]
+ reply = @socks[0].read(len)
+ return reply, nil
+ end
+
+ def sender(msg, data, host=@host, port=@port)
+ unless host == @host && port == @port
+ raise RequestError.new("host/port don't match: #{host}:#{port}")
+ end
+ id = DNS.allocate_request_id(@host, @port)
+ request = msg.encode
+ request[0,2] = [request.length, id].pack('nn')
+ return @senders[[nil,id]] = Sender.new(request, data, @socks[0])
+ end
+
+ class Sender < Requester::Sender # :nodoc:
+ def send
+ @sock.print(@msg)
+ @sock.flush
+ end
+ attr_reader :data
+ end
+
+ def close
+ super
+ @senders.each_key {|from,id|
+ DNS.free_request_id(@host, @port, id)
+ }
+ end
+ end
+
+ ##
+ # Indicates a problem with the DNS request.
+
+ class RequestError < StandardError
+ end
+ end
+
+ class Config # :nodoc:
+ def initialize(config_info=nil)
+ @mutex = Thread::Mutex.new
+ @config_info = config_info
+ @initialized = nil
+ @timeouts = nil
+ end
+
+ def timeouts=(values)
+ if values
+ values = Array(values)
+ values.each do |t|
+ Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric"
+ t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive"
+ end
+ @timeouts = values
+ else
+ @timeouts = nil
+ end
+ end
+
+ def Config.parse_resolv_conf(filename)
+ nameserver = []
+ search = nil
+ ndots = 1
+ File.open(filename, 'rb') {|f|
+ f.each {|line|
+ line.sub!(/[#;].*/, '')
+ keyword, *args = line.split(/\s+/)
+ next unless keyword
+ case keyword
+ when 'nameserver'
+ nameserver.concat(args.each(&:freeze))
+ when 'domain'
+ next if args.empty?
+ search = [args[0].freeze]
+ when 'search'
+ next if args.empty?
+ search = args.each(&:freeze)
+ when 'options'
+ args.each {|arg|
+ case arg
+ when /\Andots:(\d+)\z/
+ ndots = $1.to_i
+ end
+ }
+ end
+ }
+ }
+ return { :nameserver => nameserver.freeze, :search => search.freeze, :ndots => ndots.freeze }.freeze
+ end
+
+ def Config.default_config_hash(filename="/etc/resolv.conf")
+ if File.exist? filename
+ Config.parse_resolv_conf(filename)
+ elsif defined?(Win32::Resolv)
+ search, nameserver = Win32::Resolv.get_resolv_info
+ config_hash = {}
+ config_hash[:nameserver] = nameserver if nameserver
+ config_hash[:search] = [search].flatten if search
+ config_hash
+ else
+ {}
+ end
+ end
+
+ def lazy_initialize
+ @mutex.synchronize {
+ unless @initialized
+ @nameserver_port = []
+ @use_ipv6 = nil
+ @search = nil
+ @ndots = 1
+ case @config_info
+ when nil
+ config_hash = Config.default_config_hash
+ when String
+ config_hash = Config.parse_resolv_conf(@config_info)
+ when Hash
+ config_hash = @config_info.dup
+ if String === config_hash[:nameserver]
+ config_hash[:nameserver] = [config_hash[:nameserver]]
+ end
+ if String === config_hash[:search]
+ config_hash[:search] = [config_hash[:search]]
+ end
+ else
+ raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
+ end
+ if config_hash.include? :nameserver
+ @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] }
+ end
+ if config_hash.include? :nameserver_port
+ @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] }
+ end
+ if config_hash.include? :use_ipv6
+ @use_ipv6 = config_hash[:use_ipv6]
+ end
+ @search = config_hash[:search] if config_hash.include? :search
+ @ndots = config_hash[:ndots] if config_hash.include? :ndots
+ @raise_timeout_errors = config_hash[:raise_timeout_errors]
+
+ if @nameserver_port.empty?
+ @nameserver_port << ['0.0.0.0', Port]
+ end
+ if @search
+ @search = @search.map {|arg| Label.split(arg) }
+ else
+ hostname = Socket.gethostname
+ if /\./ =~ hostname
+ @search = [Label.split($')]
+ else
+ @search = [[]]
+ end
+ end
+
+ if !@nameserver_port.kind_of?(Array) ||
+ @nameserver_port.any? {|ns_port|
+ !(Array === ns_port) ||
+ ns_port.length != 2
+ !(String === ns_port[0]) ||
+ !(Integer === ns_port[1])
+ }
+ raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}")
+ end
+
+ if !@search.kind_of?(Array) ||
+ !@search.all? {|ls| ls.all? {|l| Label::Str === l } }
+ raise ArgumentError.new("invalid search config: #{@search.inspect}")
+ end
+
+ if !@ndots.kind_of?(Integer)
+ raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}")
+ end
+
+ @initialized = true
+ end
+ }
+ self
+ end
+
+ def single?
+ lazy_initialize
+ if @nameserver_port.length == 1
+ return @nameserver_port[0]
+ else
+ return nil
+ end
+ end
+
+ def nameserver_port
+ @nameserver_port
+ end
+
+ def use_ipv6?
+ @use_ipv6
+ end
+
+ def generate_candidates(name)
+ candidates = nil
+ name = Name.create(name)
+ if name.absolute?
+ candidates = [name]
+ else
+ if @ndots <= name.length - 1
+ candidates = [Name.new(name.to_a)]
+ else
+ candidates = []
+ end
+ candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)})
+ fname = Name.create("#{name}.")
+ if !candidates.include?(fname)
+ candidates << fname
+ end
+ end
+ return candidates
+ end
+
+ InitialTimeout = 5
+
+ def generate_timeouts
+ ts = [InitialTimeout]
+ ts << ts[-1] * 2 / @nameserver_port.length
+ ts << ts[-1] * 2
+ ts << ts[-1] * 2
+ return ts
+ end
+
+ def resolv(name)
+ candidates = generate_candidates(name)
+ timeouts = @timeouts || generate_timeouts
+ timeout_error = false
+ begin
+ candidates.each {|candidate|
+ begin
+ timeouts.each {|tout|
+ @nameserver_port.each {|nameserver, port|
+ begin
+ yield candidate, tout, nameserver, port
+ rescue ResolvTimeout
+ end
+ }
+ }
+ timeout_error = true
+ raise ResolvError.new("DNS resolv timeout: #{name}")
+ rescue NXDomain
+ end
+ }
+ rescue ResolvError
+ raise if @raise_timeout_errors && timeout_error
+ end
+ end
+
+ ##
+ # Indicates no such domain was found.
+
+ class NXDomain < ResolvError
+ end
+
+ ##
+ # Indicates some other unhandled resolver error was encountered.
+
+ class OtherResolvError < ResolvError
+ end
+ end
+
+ module OpCode # :nodoc:
+ Query = 0
+ IQuery = 1
+ Status = 2
+ Notify = 4
+ Update = 5
+ end
+
+ module RCode # :nodoc:
+ NoError = 0
+ FormErr = 1
+ ServFail = 2
+ NXDomain = 3
+ NotImp = 4
+ Refused = 5
+ YXDomain = 6
+ YXRRSet = 7
+ NXRRSet = 8
+ NotAuth = 9
+ NotZone = 10
+ BADVERS = 16
+ BADSIG = 16
+ BADKEY = 17
+ BADTIME = 18
+ BADMODE = 19
+ BADNAME = 20
+ BADALG = 21
+ end
+
+ ##
+ # Indicates that the DNS response was unable to be decoded.
+
+ class DecodeError < StandardError
+ end
+
+ ##
+ # Indicates that the DNS request was unable to be encoded.
+
+ class EncodeError < StandardError
+ end
+
+ module Label # :nodoc:
+ def self.split(arg)
+ labels = []
+ arg.scan(/[^\.]+/) {labels << Str.new($&)}
+ return labels
+ end
+
+ class Str # :nodoc:
+ def initialize(string)
+ @string = string
+ # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343]
+ # This assumes @string is given in ASCII compatible encoding.
+ @downcase = string.b.downcase
+ end
+ attr_reader :string, :downcase
+
+ def to_s
+ return @string
+ end
+
+ def inspect
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other)
+ return self.class == other.class && @downcase == other.downcase
+ end
+
+ def eql?(other)
+ return self == other
+ end
+
+ def hash
+ return @downcase.hash
+ end
+ end
+ end
+
+ ##
+ # A representation of a DNS name.
+
+ class Name
+
+ ##
+ # Creates a new DNS name from +arg+. +arg+ can be:
+ #
+ # Name:: returns +arg+.
+ # String:: Creates a new Name.
+
+ def self.create(arg)
+ case arg
+ when Name
+ return arg
+ when String
+ return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
+ else
+ raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
+ end
+ end
+
+ def initialize(labels, absolute=true) # :nodoc:
+ labels = labels.map {|label|
+ case label
+ when String then Label::Str.new(label)
+ when Label::Str then label
+ else
+ raise ArgumentError, "unexpected label: #{label.inspect}"
+ end
+ }
+ @labels = labels
+ @absolute = absolute
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>"
+ end
+
+ ##
+ # True if this name is absolute.
+
+ def absolute?
+ return @absolute
+ end
+
+ def ==(other) # :nodoc:
+ return false unless Name === other
+ return false unless @absolute == other.absolute?
+ return @labels == other.to_a
+ end
+
+ alias eql? == # :nodoc:
+
+ ##
+ # Returns true if +other+ is a subdomain.
+ #
+ # Example:
+ #
+ # domain = Gem::Resolv::DNS::Name.create("y.z")
+ # p Gem::Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
+ # p Gem::Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
+ # p Gem::Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
+ # p Gem::Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
+ #
+
+ def subdomain_of?(other)
+ raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other
+ return false if @absolute != other.absolute?
+ other_len = other.length
+ return false if @labels.length <= other_len
+ return @labels[-other_len, other_len] == other.to_a
+ end
+
+ def hash # :nodoc:
+ return @labels.hash ^ @absolute.hash
+ end
+
+ def to_a # :nodoc:
+ return @labels
+ end
+
+ def length # :nodoc:
+ return @labels.length
+ end
+
+ def [](i) # :nodoc:
+ return @labels[i]
+ end
+
+ ##
+ # returns the domain name as a string.
+ #
+ # The domain name doesn't have a trailing dot even if the name object is
+ # absolute.
+ #
+ # Example:
+ #
+ # p Gem::Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
+ # p Gem::Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
+
+ def to_s
+ return @labels.join('.')
+ end
+ end
+
+ class Message # :nodoc:
+ @@identifier = -1
+
+ def initialize(id = (@@identifier += 1) & 0xffff)
+ @id = id
+ @qr = 0
+ @opcode = 0
+ @aa = 0
+ @tc = 0
+ @rd = 0 # recursion desired
+ @ra = 0 # recursion available
+ @rcode = 0
+ @question = []
+ @answer = []
+ @authority = []
+ @additional = []
+ end
+
+ attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode
+ attr_reader :question, :answer, :authority, :additional
+
+ def ==(other)
+ return @id == other.id &&
+ @qr == other.qr &&
+ @opcode == other.opcode &&
+ @aa == other.aa &&
+ @tc == other.tc &&
+ @rd == other.rd &&
+ @ra == other.ra &&
+ @rcode == other.rcode &&
+ @question == other.question &&
+ @answer == other.answer &&
+ @authority == other.authority &&
+ @additional == other.additional
+ end
+
+ def add_question(name, typeclass)
+ @question << [Name.create(name), typeclass]
+ end
+
+ def each_question
+ @question.each {|name, typeclass|
+ yield name, typeclass
+ }
+ end
+
+ def add_answer(name, ttl, data)
+ @answer << [Name.create(name), ttl, data]
+ end
+
+ def each_answer
+ @answer.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def add_authority(name, ttl, data)
+ @authority << [Name.create(name), ttl, data]
+ end
+
+ def each_authority
+ @authority.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def add_additional(name, ttl, data)
+ @additional << [Name.create(name), ttl, data]
+ end
+
+ def each_additional
+ @additional.each {|name, ttl, data|
+ yield name, ttl, data
+ }
+ end
+
+ def each_resource
+ each_answer {|name, ttl, data| yield name, ttl, data}
+ each_authority {|name, ttl, data| yield name, ttl, data}
+ each_additional {|name, ttl, data| yield name, ttl, data}
+ end
+
+ def encode
+ return MessageEncoder.new {|msg|
+ msg.put_pack('nnnnnn',
+ @id,
+ (@qr & 1) << 15 |
+ (@opcode & 15) << 11 |
+ (@aa & 1) << 10 |
+ (@tc & 1) << 9 |
+ (@rd & 1) << 8 |
+ (@ra & 1) << 7 |
+ (@rcode & 15),
+ @question.length,
+ @answer.length,
+ @authority.length,
+ @additional.length)
+ @question.each {|q|
+ name, typeclass = q
+ msg.put_name(name)
+ msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue)
+ }
+ [@answer, @authority, @additional].each {|rr|
+ rr.each {|r|
+ name, ttl, data = r
+ msg.put_name(name)
+ msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl)
+ msg.put_length16 {data.encode_rdata(msg)}
+ }
+ }
+ }.to_s
+ end
+
+ class MessageEncoder # :nodoc:
+ def initialize
+ @data = ''.dup
+ @names = {}
+ yield self
+ end
+
+ def to_s
+ return @data
+ end
+
+ def put_bytes(d)
+ @data << d
+ end
+
+ def put_pack(template, *d)
+ @data << d.pack(template)
+ end
+
+ def put_length16
+ length_index = @data.length
+ @data << "\0\0"
+ data_start = @data.length
+ yield
+ data_end = @data.length
+ @data[length_index, 2] = [data_end - data_start].pack("n")
+ end
+
+ def put_string(d)
+ self.put_pack("C", d.length)
+ @data << d
+ end
+
+ def put_string_list(ds)
+ ds.each {|d|
+ self.put_string(d)
+ }
+ end
+
+ def put_name(d, compress: true)
+ put_labels(d.to_a, compress: compress)
+ end
+
+ def put_labels(d, compress: true)
+ d.each_index {|i|
+ domain = d[i..-1]
+ if compress && idx = @names[domain]
+ self.put_pack("n", 0xc000 | idx)
+ return
+ else
+ if @data.length < 0x4000
+ @names[domain] = @data.length
+ end
+ self.put_label(d[i])
+ end
+ }
+ @data << "\0"
+ end
+
+ def put_label(d)
+ self.put_string(d.to_s)
+ end
+ end
+
+ def Message.decode(m)
+ o = Message.new(0)
+ MessageDecoder.new(m) {|msg|
+ id, flag, qdcount, ancount, nscount, arcount =
+ msg.get_unpack('nnnnnn')
+ o.id = id
+ o.tc = (flag >> 9) & 1
+ o.rcode = flag & 15
+ return o unless o.tc.zero?
+
+ o.qr = (flag >> 15) & 1
+ o.opcode = (flag >> 11) & 15
+ o.aa = (flag >> 10) & 1
+ o.rd = (flag >> 8) & 1
+ o.ra = (flag >> 7) & 1
+ (1..qdcount).each {
+ name, typeclass = msg.get_question
+ o.add_question(name, typeclass)
+ }
+ (1..ancount).each {
+ name, ttl, data = msg.get_rr
+ o.add_answer(name, ttl, data)
+ }
+ (1..nscount).each {
+ name, ttl, data = msg.get_rr
+ o.add_authority(name, ttl, data)
+ }
+ (1..arcount).each {
+ name, ttl, data = msg.get_rr
+ o.add_additional(name, ttl, data)
+ }
+ }
+ return o
+ end
+
+ class MessageDecoder # :nodoc:
+ def initialize(data)
+ @data = data
+ @index = 0
+ @limit = data.bytesize
+ yield self
+ end
+
+ def inspect
+ "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>"
+ end
+
+ def get_length16
+ len, = self.get_unpack('n')
+ save_limit = @limit
+ @limit = @index + len
+ d = yield(len)
+ if @index < @limit
+ raise DecodeError.new("junk exists")
+ elsif @limit < @index
+ raise DecodeError.new("limit exceeded")
+ end
+ @limit = save_limit
+ return d
+ end
+
+ def get_bytes(len = @limit - @index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
+ d = @data.byteslice(@index, len)
+ @index += len
+ return d
+ end
+
+ def get_unpack(template)
+ len = 0
+ template.each_byte {|byte|
+ byte = "%c" % byte
+ case byte
+ when ?c, ?C
+ len += 1
+ when ?n
+ len += 2
+ when ?N
+ len += 4
+ else
+ raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
+ end
+ }
+ raise DecodeError.new("limit exceeded") if @limit < @index + len
+ arr = @data.unpack("@#{@index}#{template}")
+ @index += len
+ return arr
+ end
+
+ def get_string
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ len = @data.getbyte(@index)
+ raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
+ d = @data.byteslice(@index + 1, len)
+ @index += 1 + len
+ return d
+ end
+
+ def get_string_list
+ strings = []
+ while @index < @limit
+ strings << self.get_string
+ end
+ strings
+ end
+
+ def get_list
+ [].tap do |values|
+ while @index < @limit
+ values << yield
+ end
+ end
+ end
+
+ def get_name
+ return Name.new(self.get_labels)
+ end
+
+ def get_labels
+ prev_index = @index
+ save_index = nil
+ d = []
+ size = -1
+ while true
+ raise DecodeError.new("limit exceeded") if @limit <= @index
+ case @data.getbyte(@index)
+ when 0
+ @index += 1
+ if save_index
+ @index = save_index
+ end
+ return d
+ when 192..255
+ idx = self.get_unpack('n')[0] & 0x3fff
+ if prev_index <= idx
+ raise DecodeError.new("non-backward name pointer")
+ end
+ prev_index = idx
+ if !save_index
+ save_index = @index
+ end
+ @index = idx
+ else
+ l = self.get_label
+ d << l
+ size += 1 + l.string.bytesize
+ raise DecodeError.new("name label data exceed 255 octets") if size > 255
+ end
+ end
+ end
+
+ def get_label
+ return Label::Str.new(self.get_string)
+ end
+
+ def get_question
+ name = self.get_name
+ type, klass = self.get_unpack("nn")
+ return name, Resource.get_class(type, klass)
+ end
+
+ def get_rr
+ name = self.get_name
+ type, klass, ttl = self.get_unpack('nnN')
+ typeclass = Resource.get_class(type, klass)
+ res = self.get_length16 do
+ begin
+ typeclass.decode_rdata self
+ rescue => e
+ raise DecodeError, e.message, e.backtrace
+ end
+ end
+ res.instance_variable_set :@ttl, ttl
+ return name, ttl, res
+ end
+ end
+ end
+
+ ##
+ # SvcParams for service binding RRs. [RFC9460]
+
+ class SvcParams
+ include Enumerable
+
+ ##
+ # Create a list of SvcParams with the given initial content.
+ #
+ # +params+ has to be an enumerable of +SvcParam+s.
+ # If its content has +SvcParam+s with the duplicate key,
+ # the one appears last takes precedence.
+
+ def initialize(params = [])
+ @params = {}
+
+ params.each do |param|
+ add param
+ end
+ end
+
+ ##
+ # Get SvcParam for the given +key+ in this list.
+
+ def [](key)
+ @params[canonical_key(key)]
+ end
+
+ ##
+ # Get the number of SvcParams in this list.
+
+ def count
+ @params.count
+ end
+
+ ##
+ # Get whether this list is empty.
+
+ def empty?
+ @params.empty?
+ end
+
+ ##
+ # Add the SvcParam +param+ to this list, overwriting the existing one with the same key.
+
+ def add(param)
+ @params[param.class.key_number] = param
+ end
+
+ ##
+ # Remove the +SvcParam+ with the given +key+ and return it.
+
+ def delete(key)
+ @params.delete(canonical_key(key))
+ end
+
+ ##
+ # Enumerate the +SvcParam+s in this list.
+
+ def each(&block)
+ return enum_for(:each) unless block
+ @params.each_value(&block)
+ end
+
+ def encode(msg) # :nodoc:
+ @params.keys.sort.each do |key|
+ msg.put_pack('n', key)
+ msg.put_length16 do
+ @params.fetch(key).encode(msg)
+ end
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ params = msg.get_list do
+ key, = msg.get_unpack('n')
+ msg.get_length16 do
+ SvcParam::ClassHash[key].decode(msg)
+ end
+ end
+
+ return self.new(params)
+ end
+
+ private
+
+ def canonical_key(key) # :nodoc:
+ case key
+ when Integer
+ key
+ when /\Akey(\d+)\z/
+ Integer($1)
+ when Symbol
+ SvcParam::ClassHash[key].key_number
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+ end
+
+ ##
+ # Base class for SvcParam. [RFC9460]
+
+ class SvcParam
+
+ ##
+ # Get the presentation name of the SvcParamKey.
+
+ def self.key_name
+ const_get(:KeyName)
+ end
+
+ ##
+ # Get the registered number of the SvcParamKey.
+
+ def self.key_number
+ const_get(:KeyNumber)
+ end
+
+ ClassHash = Hash.new do |h, key| # :nodoc:
+ case key
+ when Integer
+ Generic.create(key)
+ when /\Akey(?<key>\d+)\z/
+ Generic.create(key.to_int)
+ when Symbol
+ raise KeyError, "unknown key #{key}"
+ else
+ raise TypeError, 'key must be either String or Symbol'
+ end
+ end
+
+ ##
+ # Generic SvcParam abstract class.
+
+ class Generic < SvcParam
+
+ ##
+ # SvcParamValue in wire-format byte string.
+
+ attr_reader :value
+
+ ##
+ # Create generic SvcParam
+
+ def initialize(value)
+ @value = value
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@value)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_bytes)
+ end
+
+ def self.create(key_number)
+ c = Class.new(Generic)
+ key_name = :"key#{key_number}"
+ c.const_set(:KeyName, key_name)
+ c.const_set(:KeyNumber, key_number)
+ self.const_set(:"Key#{key_number}", c)
+ ClassHash[key_name] = ClassHash[key_number] = c
+ return c
+ end
+ end
+
+ ##
+ # "mandatory" SvcParam -- Mandatory keys in service binding RR
+
+ class Mandatory < SvcParam
+ KeyName = :mandatory
+ KeyNumber = 0
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Mandatory keys.
+
+ attr_reader :keys
+
+ ##
+ # Initialize "mandatory" ScvParam.
+
+ def initialize(keys)
+ @keys = keys.map(&:to_int)
+ end
+
+ def encode(msg) # :nodoc:
+ @keys.sort.each do |key|
+ msg.put_pack('n', key)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ keys = msg.get_list { msg.get_unpack('n')[0] }
+ return self.new(keys)
+ end
+ end
+
+ ##
+ # "alpn" SvcParam -- Additional supported protocols
+
+ class ALPN < SvcParam
+ KeyName = :alpn
+ KeyNumber = 1
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Supported protocol IDs.
+
+ attr_reader :protocol_ids
+
+ ##
+ # Initialize "alpn" ScvParam.
+
+ def initialize(protocol_ids)
+ @protocol_ids = protocol_ids.map(&:to_str)
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_string_list(@protocol_ids)
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new(msg.get_string_list)
+ end
+ end
+
+ ##
+ # "no-default-alpn" SvcParam -- No support for default protocol
+
+ class NoDefaultALPN < SvcParam
+ KeyName = :'no-default-alpn'
+ KeyNumber = 2
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ def encode(msg) # :nodoc:
+ # no payload
+ end
+
+ def self.decode(msg) # :nodoc:
+ return self.new
+ end
+ end
+
+ ##
+ # "port" SvcParam -- Port for alternative endpoint
+
+ class Port < SvcParam
+ KeyName = :port
+ KeyNumber = 3
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Port number.
+
+ attr_reader :port
+
+ ##
+ # Initialize "port" ScvParam.
+
+ def initialize(port)
+ @port = port.to_int
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_pack('n', @port)
+ end
+
+ def self.decode(msg) # :nodoc:
+ port, = msg.get_unpack('n')
+ return self.new(port)
+ end
+ end
+
+ ##
+ # "ipv4hint" SvcParam -- IPv4 address hints
+
+ class IPv4Hint < SvcParam
+ KeyName = :ipv4hint
+ KeyNumber = 4
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv4 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv4hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv4.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv4.new(msg.get_bytes(4)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "ipv6hint" SvcParam -- IPv6 address hints
+
+ class IPv6Hint < SvcParam
+ KeyName = :ipv6hint
+ KeyNumber = 6
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # Set of IPv6 addresses.
+
+ attr_reader :addresses
+
+ ##
+ # Initialize "ipv6hint" ScvParam.
+
+ def initialize(addresses)
+ @addresses = addresses.map {|address| IPv6.create(address) }
+ end
+
+ def encode(msg) # :nodoc:
+ @addresses.each do |address|
+ msg.put_bytes(address.address)
+ end
+ end
+
+ def self.decode(msg) # :nodoc:
+ addresses = msg.get_list { IPv6.new(msg.get_bytes(16)) }
+ return self.new(addresses)
+ end
+ end
+
+ ##
+ # "dohpath" SvcParam -- DNS over HTTPS path template [RFC9461]
+
+ class DoHPath < SvcParam
+ KeyName = :dohpath
+ KeyNumber = 7
+ ClassHash[KeyName] = ClassHash[KeyNumber] = self # :nodoc:
+
+ ##
+ # URI template for DoH queries.
+
+ attr_reader :template
+
+ ##
+ # Initialize "dohpath" ScvParam.
+
+ def initialize(template)
+ @template = template.encode('utf-8')
+ end
+
+ def encode(msg) # :nodoc:
+ msg.put_bytes(@template)
+ end
+
+ def self.decode(msg) # :nodoc:
+ template = msg.get_bytes.force_encoding('utf-8')
+ return self.new(template)
+ end
+ end
+ end
+
+ ##
+ # A DNS query abstract class.
+
+ class Query
+ def encode_rdata(msg) # :nodoc:
+ raise EncodeError.new("#{self.class} is query.")
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ raise DecodeError.new("#{self.class} is query.")
+ end
+ end
+
+ ##
+ # A DNS resource abstract class.
+
+ class Resource < Query
+
+ ##
+ # Remaining Time To Live for this Resource.
+
+ attr_reader :ttl
+
+ ClassHash = Module.new do
+ module_function
+
+ def []=(type_class_value, klass)
+ type_value, class_value = type_class_value
+ Resource.const_set(:"Type#{type_value}_Class#{class_value}", klass)
+ end
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ raise NotImplementedError.new
+ end
+
+ def ==(other) # :nodoc:
+ return false unless self.class == other.class
+ s_ivars = self.instance_variables
+ s_ivars.sort!
+ s_ivars.delete :@ttl
+ o_ivars = other.instance_variables
+ o_ivars.sort!
+ o_ivars.delete :@ttl
+ return s_ivars == o_ivars &&
+ s_ivars.collect {|name| self.instance_variable_get name} ==
+ o_ivars.collect {|name| other.instance_variable_get name}
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ h = 0
+ vars = self.instance_variables
+ vars.delete :@ttl
+ vars.each {|name|
+ h ^= self.instance_variable_get(name).hash
+ }
+ return h
+ end
+
+ def self.get_class(type_value, class_value) # :nodoc:
+ cache = :"Type#{type_value}_Class#{class_value}"
+
+ return (const_defined?(cache) && const_get(cache)) ||
+ Generic.create(type_value, class_value)
+ end
+
+ ##
+ # A generic resource abstract class.
+
+ class Generic < Resource
+
+ ##
+ # Creates a new generic resource.
+
+ def initialize(data)
+ @data = data
+ end
+
+ ##
+ # Data for this generic resource.
+
+ attr_reader :data
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(data)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(msg.get_bytes)
+ end
+
+ def self.create(type_value, class_value) # :nodoc:
+ c = Class.new(Generic)
+ c.const_set(:TypeValue, type_value)
+ c.const_set(:ClassValue, class_value)
+ Generic.const_set("Type#{type_value}_Class#{class_value}", c)
+ ClassHash[[type_value, class_value]] = c
+ return c
+ end
+ end
+
+ ##
+ # Domain Name resource abstract class.
+
+ class DomainName < Resource
+
+ ##
+ # Creates a new DomainName from +name+.
+
+ def initialize(name)
+ @name = name
+ end
+
+ ##
+ # The name of this DomainName.
+
+ attr_reader :name
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@name)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(msg.get_name)
+ end
+ end
+
+ # Standard (class generic) RRs
+
+ ClassValue = nil # :nodoc:
+
+ ##
+ # An authoritative name server.
+
+ class NS < DomainName
+ TypeValue = 2 # :nodoc:
+ end
+
+ ##
+ # The canonical name for an alias.
+
+ class CNAME < DomainName
+ TypeValue = 5 # :nodoc:
+ end
+
+ ##
+ # Start Of Authority resource.
+
+ class SOA < Resource
+
+ TypeValue = 6 # :nodoc:
+
+ ##
+ # Creates a new SOA record. See the attr documentation for the
+ # details of each argument.
+
+ def initialize(mname, rname, serial, refresh, retry_, expire, minimum)
+ @mname = mname
+ @rname = rname
+ @serial = serial
+ @refresh = refresh
+ @retry = retry_
+ @expire = expire
+ @minimum = minimum
+ end
+
+ ##
+ # Name of the host where the master zone file for this zone resides.
+
+ attr_reader :mname
+
+ ##
+ # The person responsible for this domain name.
+
+ attr_reader :rname
+
+ ##
+ # The version number of the zone file.
+
+ attr_reader :serial
+
+ ##
+ # How often, in seconds, a secondary name server is to check for
+ # updates from the primary name server.
+
+ attr_reader :refresh
+
+ ##
+ # How often, in seconds, a secondary name server is to retry after a
+ # failure to check for a refresh.
+
+ attr_reader :retry
+
+ ##
+ # Time in seconds that a secondary name server is to use the data
+ # before refreshing from the primary name server.
+
+ attr_reader :expire
+
+ ##
+ # The minimum number of seconds to be used for TTL values in RRs.
+
+ attr_reader :minimum
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@mname)
+ msg.put_name(@rname)
+ msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ mname = msg.get_name
+ rname = msg.get_name
+ serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN')
+ return self.new(
+ mname, rname, serial, refresh, retry_, expire, minimum)
+ end
+ end
+
+ ##
+ # A Pointer to another DNS name.
+
+ class PTR < DomainName
+ TypeValue = 12 # :nodoc:
+ end
+
+ ##
+ # Host Information resource.
+
+ class HINFO < Resource
+
+ TypeValue = 13 # :nodoc:
+
+ ##
+ # Creates a new HINFO running +os+ on +cpu+.
+
+ def initialize(cpu, os)
+ @cpu = cpu
+ @os = os
+ end
+
+ ##
+ # CPU architecture for this resource.
+
+ attr_reader :cpu
+
+ ##
+ # Operating system for this resource.
+
+ attr_reader :os
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_string(@cpu)
+ msg.put_string(@os)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ cpu = msg.get_string
+ os = msg.get_string
+ return self.new(cpu, os)
+ end
+ end
+
+ ##
+ # Mailing list or mailbox information.
+
+ class MINFO < Resource
+
+ TypeValue = 14 # :nodoc:
+
+ def initialize(rmailbx, emailbx)
+ @rmailbx = rmailbx
+ @emailbx = emailbx
+ end
+
+ ##
+ # Domain name responsible for this mail list or mailbox.
+
+ attr_reader :rmailbx
+
+ ##
+ # Mailbox to use for error messages related to the mail list or mailbox.
+
+ attr_reader :emailbx
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_name(@rmailbx)
+ msg.put_name(@emailbx)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ rmailbx = msg.get_string
+ emailbx = msg.get_string
+ return self.new(rmailbx, emailbx)
+ end
+ end
+
+ ##
+ # Mail Exchanger resource.
+
+ class MX < Resource
+
+ TypeValue= 15 # :nodoc:
+
+ ##
+ # Creates a new MX record with +preference+, accepting mail at
+ # +exchange+.
+
+ def initialize(preference, exchange)
+ @preference = preference
+ @exchange = exchange
+ end
+
+ ##
+ # The preference for this MX.
+
+ attr_reader :preference
+
+ ##
+ # The host of this MX.
+
+ attr_reader :exchange
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack('n', @preference)
+ msg.put_name(@exchange)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ preference, = msg.get_unpack('n')
+ exchange = msg.get_name
+ return self.new(preference, exchange)
+ end
+ end
+
+ ##
+ # Unstructured text resource.
+
+ class TXT < Resource
+
+ TypeValue = 16 # :nodoc:
+
+ def initialize(first_string, *rest_strings)
+ @strings = [first_string, *rest_strings]
+ end
+
+ ##
+ # Returns an Array of Strings for this TXT record.
+
+ attr_reader :strings
+
+ ##
+ # Returns the concatenated string from +strings+.
+
+ def data
+ @strings.join("")
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_string_list(@strings)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ strings = msg.get_string_list
+ return self.new(*strings)
+ end
+ end
+
+ ##
+ # Location resource
+
+ class LOC < Resource
+
+ TypeValue = 29 # :nodoc:
+
+ def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude)
+ @version = version
+ @ssize = Gem::Resolv::LOC::Size.create(ssize)
+ @hprecision = Gem::Resolv::LOC::Size.create(hprecision)
+ @vprecision = Gem::Resolv::LOC::Size.create(vprecision)
+ @latitude = Gem::Resolv::LOC::Coord.create(latitude)
+ @longitude = Gem::Resolv::LOC::Coord.create(longitude)
+ @altitude = Gem::Resolv::LOC::Alt.create(altitude)
+ end
+
+ ##
+ # Returns the version value for this LOC record which should always be 00
+
+ attr_reader :version
+
+ ##
+ # The spherical size of this LOC
+ # in meters using scientific notation as 2 integers of XeY
+
+ attr_reader :ssize
+
+ ##
+ # The horizontal precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :hprecision
+
+ ##
+ # The vertical precision using ssize type values
+ # in meters using scientific notation as 2 integers of XeY
+ # for precision use value/2 e.g. 2m = +/-1m
+
+ attr_reader :vprecision
+
+ ##
+ # The latitude for this LOC where 2**31 is the equator
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :latitude
+
+ ##
+ # The longitude for this LOC where 2**31 is the prime meridian
+ # in thousandths of an arc second as an unsigned 32bit integer
+
+ attr_reader :longitude
+
+ ##
+ # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid
+ # in centimeters as an unsigned 32bit integer
+
+ attr_reader :altitude
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@version)
+ msg.put_bytes(@ssize.scalar)
+ msg.put_bytes(@hprecision.scalar)
+ msg.put_bytes(@vprecision.scalar)
+ msg.put_bytes(@latitude.coordinates)
+ msg.put_bytes(@longitude.coordinates)
+ msg.put_bytes(@altitude.altitude)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ version = msg.get_bytes(1)
+ ssize = msg.get_bytes(1)
+ hprecision = msg.get_bytes(1)
+ vprecision = msg.get_bytes(1)
+ latitude = msg.get_bytes(4)
+ longitude = msg.get_bytes(4)
+ altitude = msg.get_bytes(4)
+ return self.new(
+ version,
+ Gem::Resolv::LOC::Size.new(ssize),
+ Gem::Resolv::LOC::Size.new(hprecision),
+ Gem::Resolv::LOC::Size.new(vprecision),
+ Gem::Resolv::LOC::Coord.new(latitude,"lat"),
+ Gem::Resolv::LOC::Coord.new(longitude,"lon"),
+ Gem::Resolv::LOC::Alt.new(altitude)
+ )
+ end
+ end
+
+ ##
+ # A Query type requesting any RR.
+
+ class ANY < Query
+ TypeValue = 255 # :nodoc:
+ end
+
+ ##
+ # CAA resource record defined in RFC 8659
+ #
+ # These records identify certificate authority allowed to issue
+ # certificates for the given domain.
+
+ class CAA < Resource
+ TypeValue = 257
+
+ ##
+ # Creates a new CAA for +flags+, +tag+ and +value+.
+
+ def initialize(flags, tag, value)
+ unless (0..255) === flags
+ raise ArgumentError.new('flags must be an Integer between 0 and 255')
+ end
+ unless (1..15) === tag.bytesize
+ raise ArgumentError.new('length of tag must be between 1 and 15')
+ end
+
+ @flags = flags
+ @tag = tag
+ @value = value
+ end
+
+ ##
+ # Flags for this property:
+ # - Bit 0 : 0 = not critical, 1 = critical
+
+ attr_reader :flags
+
+ ##
+ # Property tag ("issue", "issuewild", "iodef"...).
+
+ attr_reader :tag
+
+ ##
+ # Property value.
+
+ attr_reader :value
+
+ ##
+ # Whether the critical flag is set on this property.
+
+ def critical?
+ flags & 0x80 != 0
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack('C', @flags)
+ msg.put_string(@tag)
+ msg.put_bytes(@value)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ flags, = msg.get_unpack('C')
+ tag = msg.get_string
+ value = msg.get_bytes
+ self.new flags, tag, value
+ end
+ end
+
+ ClassInsensitiveTypes = [ # :nodoc:
+ NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY, CAA
+ ]
+
+ ##
+ # module IN contains ARPA Internet specific RRs.
+
+ module IN
+
+ ClassValue = 1 # :nodoc:
+
+ ClassInsensitiveTypes.each {|s|
+ c = Class.new(s)
+ c.const_set(:TypeValue, s::TypeValue)
+ c.const_set(:ClassValue, ClassValue)
+ ClassHash[[s::TypeValue, ClassValue]] = c
+ self.const_set(s.name.sub(/.*::/, ''), c)
+ }
+
+ ##
+ # IPv4 Address resource
+
+ class A < Resource
+ TypeValue = 1
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new A for +address+.
+
+ def initialize(address)
+ @address = IPv4.create(address)
+ end
+
+ ##
+ # The Gem::Resolv::IPv4 address for this A.
+
+ attr_reader :address
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(IPv4.new(msg.get_bytes(4)))
+ end
+ end
+
+ ##
+ # Well Known Service resource.
+
+ class WKS < Resource
+ TypeValue = 11
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ def initialize(address, protocol, bitmap)
+ @address = IPv4.create(address)
+ @protocol = protocol
+ @bitmap = bitmap
+ end
+
+ ##
+ # The host these services run on.
+
+ attr_reader :address
+
+ ##
+ # IP protocol number for these services.
+
+ attr_reader :protocol
+
+ ##
+ # A bit map of enabled services on this host.
+ #
+ # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP
+ # service (port 25). If this bit is set, then an SMTP server should
+ # be listening on TCP port 25; if zero, SMTP service is not
+ # supported.
+
+ attr_reader :bitmap
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ msg.put_pack("n", @protocol)
+ msg.put_bytes(@bitmap)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ address = IPv4.new(msg.get_bytes(4))
+ protocol, = msg.get_unpack("n")
+ bitmap = msg.get_bytes
+ return self.new(address, protocol, bitmap)
+ end
+ end
+
+ ##
+ # An IPv6 address record.
+
+ class AAAA < Resource
+ TypeValue = 28
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ ##
+ # Creates a new AAAA for +address+.
+
+ def initialize(address)
+ @address = IPv6.create(address)
+ end
+
+ ##
+ # The Gem::Resolv::IPv6 address for this AAAA.
+
+ attr_reader :address
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_bytes(@address.address)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ return self.new(IPv6.new(msg.get_bytes(16)))
+ end
+ end
+
+ ##
+ # SRV resource record defined in RFC 2782
+ #
+ # These records identify the hostname and port that a service is
+ # available at.
+
+ class SRV < Resource
+ TypeValue = 33
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+
+ # Create a SRV resource record.
+ #
+ # See the documentation for #priority, #weight, #port and #target
+ # for +priority+, +weight+, +port and +target+ respectively.
+
+ def initialize(priority, weight, port, target)
+ @priority = priority.to_int
+ @weight = weight.to_int
+ @port = port.to_int
+ @target = Name.create(target)
+ end
+
+ # The priority of this target host.
+ #
+ # A client MUST attempt to contact the target host with the
+ # lowest-numbered priority it can reach; target hosts with the same
+ # priority SHOULD be tried in an order defined by the weight field.
+ # The range is 0-65535. Note that it is not widely implemented and
+ # should be set to zero.
+
+ attr_reader :priority
+
+ # A server selection mechanism.
+ #
+ # The weight field specifies a relative weight for entries with the
+ # same priority. Larger weights SHOULD be given a proportionately
+ # higher probability of being selected. The range of this number is
+ # 0-65535. Domain administrators SHOULD use Weight 0 when there
+ # isn't any server selection to do, to make the RR easier to read
+ # for humans (less noisy). Note that it is not widely implemented
+ # and should be set to zero.
+
+ attr_reader :weight
+
+ # The port on this target host of this service.
+ #
+ # The range is 0-65535.
+
+ attr_reader :port
+
+ # The domain name of the target host.
+ #
+ # A target of "." means that the service is decidedly not available
+ # at this domain.
+
+ attr_reader :target
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_pack("n", @weight)
+ msg.put_pack("n", @port)
+ msg.put_name(@target, compress: false)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ weight, = msg.get_unpack("n")
+ port, = msg.get_unpack("n")
+ target = msg.get_name
+ return self.new(priority, weight, port, target)
+ end
+ end
+
+ ##
+ # Common implementation for SVCB-compatible resource records.
+
+ class ServiceBinding
+
+ ##
+ # Create a service binding resource record.
+
+ def initialize(priority, target, params = [])
+ @priority = priority.to_int
+ @target = Name.create(target)
+ @params = SvcParams.new(params)
+ end
+
+ ##
+ # The priority of this target host.
+ #
+ # The range is 0-65535.
+ # If set to 0, this RR is in AliasMode. Otherwise, it is in ServiceMode.
+
+ attr_reader :priority
+
+ ##
+ # The domain name of the target host.
+
+ attr_reader :target
+
+ ##
+ # The service parameters for the target host.
+
+ attr_reader :params
+
+ ##
+ # Whether this RR is in AliasMode.
+
+ def alias_mode?
+ self.priority == 0
+ end
+
+ ##
+ # Whether this RR is in ServiceMode.
+
+ def service_mode?
+ !alias_mode?
+ end
+
+ def encode_rdata(msg) # :nodoc:
+ msg.put_pack("n", @priority)
+ msg.put_name(@target, compress: false)
+ @params.encode(msg)
+ end
+
+ def self.decode_rdata(msg) # :nodoc:
+ priority, = msg.get_unpack("n")
+ target = msg.get_name
+ params = SvcParams.decode(msg)
+ return self.new(priority, target, params)
+ end
+ end
+
+ ##
+ # SVCB resource record [RFC9460]
+
+ class SVCB < ServiceBinding
+ TypeValue = 64
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
+
+ ##
+ # HTTPS resource record [RFC9460]
+
+ class HTTPS < ServiceBinding
+ TypeValue = 65
+ ClassValue = IN::ClassValue
+ ClassHash[[TypeValue, ClassValue]] = self # :nodoc:
+ end
+ end
+ end
+ end
+
+ ##
+ # A Gem::Resolv::DNS IPv4 address.
+
+ class IPv4
+
+ Regex256 = /0
+ |1(?:[0-9][0-9]?)?
+ |2(?:[0-4][0-9]?|5[0-5]?|[6-9])?
+ |[3-9][0-9]?/x # :nodoc:
+
+ ##
+ # Regular expression IPv4 addresses must match.
+ Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/
+
+ ##
+ # Creates a new IPv4 address from +arg+ which may be:
+ #
+ # IPv4:: returns +arg+.
+ # String:: +arg+ must match the IPv4::Regex constant
+
+ def self.create(arg)
+ case arg
+ when IPv4
+ return arg
+ when Regex
+ if (0..255) === (a = $1.to_i) &&
+ (0..255) === (b = $2.to_i) &&
+ (0..255) === (c = $3.to_i) &&
+ (0..255) === (d = $4.to_i)
+ return self.new([a, b, c, d].pack("CCCC"))
+ else
+ raise ArgumentError.new("IPv4 address with invalid value: " + arg)
+ end
+ else
+ raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}")
+ end
+ end
+
+ def initialize(address) # :nodoc:
+ unless address.kind_of?(String)
+ raise ArgumentError, 'IPv4 address must be a string'
+ end
+ unless address.length == 4
+ raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes"
+ end
+ @address = address
+ end
+
+ ##
+ # A String representation of this IPv4 address.
+
+ ##
+ # The raw IPv4 address as a String.
+
+ attr_reader :address
+
+ def to_s # :nodoc:
+ return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC"))
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ ##
+ # Turns this IPv4 address into a Gem::Resolv::DNS::Name.
+
+ def to_name
+ return DNS::Name.create(
+ '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse)
+ end
+
+ def ==(other) # :nodoc:
+ return @address == other.address
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @address.hash
+ end
+ end
+
+ ##
+ # A Gem::Resolv::DNS IPv6 address.
+
+ class IPv6
+
+ ##
+ # IPv6 address format a:b:c:d:e:f:g:h
+ Regex_8Hex = /\A
+ (?:[0-9A-Fa-f]{1,4}:){7}
+ [0-9A-Fa-f]{1,4}
+ \z/x
+
+ ##
+ # Compressed IPv6 address format a::b
+
+ Regex_CompressedHex = /\A
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ \z/x
+
+ ##
+ # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z
+
+ Regex_6Hex4Dec = /\A
+ ((?:[0-9A-Fa-f]{1,4}:){6,6})
+ (\d+)\.(\d+)\.(\d+)\.(\d+)
+ \z/x
+
+ ##
+ # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z
+
+ Regex_CompressedHex4Dec = /\A
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}:)*)
+ (\d+)\.(\d+)\.(\d+)\.(\d+)
+ \z/x
+
+ ##
+ # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1
+ Regex_8HexLinkLocal = /\A
+ [Ff][Ee]80
+ (?::[0-9A-Fa-f]{1,4}){7}
+ %[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
+ # Compressed IPv6 link local address format fe80::b%em1
+
+ Regex_CompressedHexLinkLocal = /\A
+ [Ff][Ee]80:
+ (?:
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
+ ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ |
+ :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
+ )?
+ :[0-9A-Fa-f]{1,4}%[-0-9A-Za-z._~]+
+ \z/x
+
+ ##
+ # A composite IPv6 address Regexp.
+
+ Regex = /
+ (?:#{Regex_8Hex}) |
+ (?:#{Regex_CompressedHex}) |
+ (?:#{Regex_6Hex4Dec}) |
+ (?:#{Regex_CompressedHex4Dec}) |
+ (?:#{Regex_8HexLinkLocal}) |
+ (?:#{Regex_CompressedHexLinkLocal})
+ /x
+
+ ##
+ # Creates a new IPv6 address from +arg+ which may be:
+ #
+ # IPv6:: returns +arg+.
+ # String:: +arg+ must match one of the IPv6::Regex* constants
+
+ def self.create(arg)
+ case arg
+ when IPv6
+ return arg
+ when String
+ address = ''.b
+ if Regex_8Hex =~ arg
+ arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
+ elsif Regex_CompressedHex =~ arg
+ prefix = $1
+ suffix = $2
+ a1 = ''.b
+ a2 = ''.b
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
+ suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
+ omitlen = 16 - a1.length - a2.length
+ address << a1 << "\0" * omitlen << a2
+ elsif Regex_6Hex4Dec =~ arg
+ prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i
+ if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
+ address << [a, b, c, d].pack('CCCC')
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ elsif Regex_CompressedHex4Dec =~ arg
+ prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
+ if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
+ a1 = ''.b
+ a2 = ''.b
+ prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
+ suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
+ omitlen = 12 - a1.length - a2.length
+ address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC')
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ else
+ raise ArgumentError.new("not numeric IPv6 address: " + arg)
+ end
+ return IPv6.new(address)
+ else
+ raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
+ end
+ end
+
+ def initialize(address) # :nodoc:
+ unless address.kind_of?(String) && address.length == 16
+ raise ArgumentError.new('IPv6 address must be 16 bytes')
+ end
+ @address = address
+ end
+
+ ##
+ # The raw IPv6 address as a String.
+
+ attr_reader :address
+
+ def to_s # :nodoc:
+ sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")).sub(/(^|:)0(:0)+(:|$)/, '::')
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ ##
+ # Turns this IPv6 address into a Gem::Resolv::DNS::Name.
+ #--
+ # ip6.arpa should be searched too. [RFC3152]
+
+ def to_name
+ return DNS::Name.new(
+ @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa'])
+ end
+
+ def ==(other) # :nodoc:
+ return @address == other.address
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @address.hash
+ end
+ end
+
+ ##
+ # Gem::Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly
+ # makes queries to the mDNS addresses without understanding anything about
+ # multicast ports.
+ #
+ # Information taken form the following places:
+ #
+ # * RFC 6762
+
+ class MDNS < DNS
+
+ ##
+ # Default mDNS Port
+
+ Port = 5353
+
+ ##
+ # Default IPv4 mDNS address
+
+ AddressV4 = '224.0.0.251'
+
+ ##
+ # Default IPv6 mDNS address
+
+ AddressV6 = 'ff02::fb'
+
+ ##
+ # Default mDNS addresses
+
+ Addresses = [
+ [AddressV4, Port],
+ [AddressV6, Port],
+ ]
+
+ ##
+ # Creates a new one-shot Multicast DNS (mDNS) resolver.
+ #
+ # +config_info+ can be:
+ #
+ # nil::
+ # Uses the default mDNS addresses
+ #
+ # Hash::
+ # Must contain :nameserver or :nameserver_port like
+ # Gem::Resolv::DNS#initialize.
+
+ def initialize(config_info=nil)
+ if config_info then
+ super({ nameserver_port: Addresses }.merge(config_info))
+ else
+ super(nameserver_port: Addresses)
+ end
+ end
+
+ ##
+ # Iterates over all IP addresses for +name+ retrieved from the mDNS
+ # resolver, provided name ends with "local". If the name does not end in
+ # "local" no records will be returned.
+ #
+ # +name+ can be a Gem::Resolv::DNS::Name or a String. Retrieved addresses will
+ # be a Gem::Resolv::IPv4 or Gem::Resolv::IPv6
+
+ def each_address(name)
+ name = Gem::Resolv::DNS::Name.create(name)
+
+ return unless name[-1].to_s == 'local'
+
+ super(name)
+ end
+
+ def make_udp_requester # :nodoc:
+ nameserver_port = @config.nameserver_port
+ Requester::MDNSOneShot.new(*nameserver_port)
+ end
+
+ end
+
+ module LOC # :nodoc:
+
+ ##
+ # A Gem::Resolv::LOC::Size
+
+ class Size
+
+ # Regular expression LOC size must match.
+
+ Regex = /^(\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Size from +arg+ which may be:
+ #
+ # LOC::Size:: returns +arg+.
+ # String:: +arg+ must match the LOC::Size::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Size
+ return arg
+ when String
+ scalar = ''
+ if Regex =~ arg
+ scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C")
+ else
+ raise ArgumentError.new("not a properly formed Size string: " + arg)
+ end
+ return Size.new(scalar)
+ else
+ raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(scalar)
+ @scalar = scalar
+ end
+
+ ##
+ # The raw size
+
+ attr_reader :scalar
+
+ def to_s # :nodoc:
+ s = @scalar.unpack("H2").join.to_s
+ return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @scalar == other.scalar
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @scalar.hash
+ end
+
+ end
+
+ ##
+ # A Gem::Resolv::LOC::Coord
+
+ class Coord
+
+ # Regular expression LOC Coord must match.
+
+ Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/
+
+ ##
+ # Creates a new LOC::Coord from +arg+ which may be:
+ #
+ # LOC::Coord:: returns +arg+.
+ # String:: +arg+ must match the LOC::Coord::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Coord
+ return arg
+ when String
+ coordinates = ''
+ if Regex =~ arg && $1.to_f < 180
+ m = $~
+ hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1
+ coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) +
+ (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N")
+ orientation = m[4][/[NS]/] ? 'lat' : 'lon'
+ else
+ raise ArgumentError.new("not a properly formed Coord string: " + arg)
+ end
+ return Coord.new(coordinates,orientation)
+ else
+ raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(coordinates,orientation)
+ unless coordinates.kind_of?(String)
+ raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}")
+ end
+ unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/]
+ raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"')
+ end
+ @coordinates = coordinates
+ @orientation = orientation
+ end
+
+ ##
+ # The raw coordinates
+
+ attr_reader :coordinates
+
+ ## The orientation of the hemisphere as 'lat' or 'lon'
+
+ attr_reader :orientation
+
+ def to_s # :nodoc:
+ c = @coordinates.unpack("N").join.to_i
+ val = (c - (2**31)).abs
+ fracsecs = (val % 1e3).to_i.to_s
+ val = val / 1e3
+ secs = (val % 60).to_i.to_s
+ val = val / 60
+ mins = (val % 60).to_i.to_s
+ degs = (val / 60).to_i.to_s
+ posi = (c >= 2**31)
+ case posi
+ when true
+ hemi = @orientation[/^lat$/] ? "N" : "E"
+ else
+ hemi = @orientation[/^lon$/] ? "W" : "S"
+ end
+ return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @coordinates == other.coordinates
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @coordinates.hash
+ end
+
+ end
+
+ ##
+ # A Gem::Resolv::LOC::Alt
+
+ class Alt
+
+ # Regular expression LOC Alt must match.
+
+ Regex = /^([+-]*\d+\.*\d*)[m]$/
+
+ ##
+ # Creates a new LOC::Alt from +arg+ which may be:
+ #
+ # LOC::Alt:: returns +arg+.
+ # String:: +arg+ must match the LOC::Alt::Regex constant
+
+ def self.create(arg)
+ case arg
+ when Alt
+ return arg
+ when String
+ altitude = ''
+ if Regex =~ arg
+ altitude = [($1.to_f*(1e2))+(1e7)].pack("N")
+ else
+ raise ArgumentError.new("not a properly formed Alt string: " + arg)
+ end
+ return Alt.new(altitude)
+ else
+ raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}")
+ end
+ end
+
+ # Internal use; use self.create.
+ def initialize(altitude)
+ @altitude = altitude
+ end
+
+ ##
+ # The raw altitude
+
+ attr_reader :altitude
+
+ def to_s # :nodoc:
+ a = @altitude.unpack("N").join.to_i
+ return ((a.to_f/1e2)-1e5).to_s + "m"
+ end
+
+ def inspect # :nodoc:
+ return "#<#{self.class} #{self}>"
+ end
+
+ def ==(other) # :nodoc:
+ return @altitude == other.altitude
+ end
+
+ def eql?(other) # :nodoc:
+ return self == other
+ end
+
+ def hash # :nodoc:
+ return @altitude.hash
+ end
+
+ end
+
+ end
+
+ ##
+ # Default resolver to use for Gem::Resolv class methods.
+
+ DefaultResolver = self.new
+
+ ##
+ # Replaces the resolvers in the default resolver with +new_resolvers+. This
+ # allows resolvers to be changed for resolv-replace.
+
+ def DefaultResolver.replace_resolvers new_resolvers
+ @resolvers = new_resolvers
+ end
+
+ ##
+ # Address Regexp to use for matching IP addresses.
+
+ AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
+
+end
diff --git a/lib/rubygems/vendor/securerandom/lib/securerandom.rb b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
new file mode 100644
index 0000000000..b6f1d71ad3
--- /dev/null
+++ b/lib/rubygems/vendor/securerandom/lib/securerandom.rb
@@ -0,0 +1,102 @@
+# -*- coding: us-ascii -*-
+# frozen_string_literal: true
+
+require 'random/formatter'
+
+# == Secure random number generator interface.
+#
+# This library is an interface to secure random number generators which are
+# suitable for generating session keys in HTTP cookies, etc.
+#
+# You can use this library in your application by requiring it:
+#
+# require 'rubygems/vendor/securerandom/lib/securerandom'
+#
+# It supports the following secure random number generators:
+#
+# * openssl
+# * /dev/urandom
+# * Win32
+#
+# Gem::SecureRandom is extended by the Random::Formatter module which
+# defines the following methods:
+#
+# * alphanumeric
+# * base64
+# * choose
+# * gen_random
+# * hex
+# * rand
+# * random_bytes
+# * random_number
+# * urlsafe_base64
+# * uuid
+#
+# These methods are usable as class methods of Gem::SecureRandom such as
+# +Gem::SecureRandom.hex+.
+#
+# If a secure random number generator is not available,
+# +NotImplementedError+ is raised.
+
+module Gem::SecureRandom
+
+ # The version
+ VERSION = "0.4.1"
+
+ class << self
+ # Returns a random binary string containing +size+ bytes.
+ #
+ # See Random.bytes
+ def bytes(n)
+ return gen_random(n)
+ end
+
+ # Compatibility methods for Ruby 3.2, we can remove this after dropping to support Ruby 3.2
+ def alphanumeric(n = nil, chars: ALPHANUMERIC)
+ n = 16 if n.nil?
+ choose(chars, n)
+ end if RUBY_VERSION < '3.3'
+
+ private
+
+ # :stopdoc:
+
+ # Implementation using OpenSSL
+ def gen_random_openssl(n)
+ return OpenSSL::Random.random_bytes(n)
+ end
+
+ # Implementation using system random device
+ def gen_random_urandom(n)
+ ret = Random.urandom(n)
+ unless ret
+ raise NotImplementedError, "No random device"
+ end
+ unless ret.length == n
+ raise NotImplementedError, "Unexpected partial read from random device: only #{ret.length} for #{n} bytes"
+ end
+ ret
+ end
+
+ begin
+ # Check if Random.urandom is available
+ Random.urandom(1)
+ alias gen_random gen_random_urandom
+ rescue RuntimeError
+ begin
+ require 'openssl'
+ rescue NoMethodError
+ raise NotImplementedError, "No random device"
+ else
+ alias gen_random gen_random_openssl
+ end
+ end
+
+ # :startdoc:
+
+ # Generate random data bytes for Random::Formatter
+ public :gen_random
+ end
+end
+
+Gem::SecureRandom.extend(Random::Formatter)
diff --git a/lib/rubygems/vendor/timeout/lib/timeout.rb b/lib/rubygems/vendor/timeout/lib/timeout.rb
new file mode 100644
index 0000000000..376b8c0e2b
--- /dev/null
+++ b/lib/rubygems/vendor/timeout/lib/timeout.rb
@@ -0,0 +1,201 @@
+# frozen_string_literal: true
+# Timeout long-running blocks
+#
+# == Synopsis
+#
+# require 'rubygems/vendor/timeout/lib/timeout'
+# status = Gem::Timeout.timeout(5) {
+# # Something that should be interrupted if it takes more than 5 seconds...
+# }
+#
+# == Description
+#
+# Gem::Timeout provides a way to auto-terminate a potentially long-running
+# operation if it hasn't finished in a fixed amount of time.
+#
+# == Copyright
+#
+# Copyright:: (C) 2000 Network Applied Communication Laboratory, Inc.
+# Copyright:: (C) 2000 Information-technology Promotion Agency, Japan
+
+module Gem::Timeout
+ # The version
+ VERSION = "0.4.4"
+
+ # Internal error raised to when a timeout is triggered.
+ class ExitException < Exception
+ def exception(*) # :nodoc:
+ self
+ end
+ end
+
+ # Raised by Gem::Timeout.timeout when the block times out.
+ class Error < RuntimeError
+ def self.handle_timeout(message) # :nodoc:
+ exc = ExitException.new(message)
+
+ begin
+ yield exc
+ rescue ExitException => e
+ raise new(message) if exc.equal?(e)
+ raise
+ end
+ end
+ end
+
+ # :stopdoc:
+ CONDVAR = ConditionVariable.new
+ QUEUE = Queue.new
+ QUEUE_MUTEX = Mutex.new
+ TIMEOUT_THREAD_MUTEX = Mutex.new
+ @timeout_thread = nil
+ private_constant :CONDVAR, :QUEUE, :QUEUE_MUTEX, :TIMEOUT_THREAD_MUTEX
+
+ class Request
+ attr_reader :deadline
+
+ def initialize(thread, timeout, exception_class, message)
+ @thread = thread
+ @deadline = GET_TIME.call(Process::CLOCK_MONOTONIC) + timeout
+ @exception_class = exception_class
+ @message = message
+
+ @mutex = Mutex.new
+ @done = false # protected by @mutex
+ end
+
+ def done?
+ @mutex.synchronize do
+ @done
+ end
+ end
+
+ def expired?(now)
+ now >= @deadline
+ end
+
+ def interrupt
+ @mutex.synchronize do
+ unless @done
+ @thread.raise @exception_class, @message
+ @done = true
+ end
+ end
+ end
+
+ def finished
+ @mutex.synchronize do
+ @done = true
+ end
+ end
+ end
+ private_constant :Request
+
+ def self.create_timeout_thread
+ watcher = Thread.new do
+ requests = []
+ while true
+ until QUEUE.empty? and !requests.empty? # wait to have at least one request
+ req = QUEUE.pop
+ requests << req unless req.done?
+ end
+ closest_deadline = requests.min_by(&:deadline).deadline
+
+ now = 0.0
+ QUEUE_MUTEX.synchronize do
+ while (now = GET_TIME.call(Process::CLOCK_MONOTONIC)) < closest_deadline and QUEUE.empty?
+ CONDVAR.wait(QUEUE_MUTEX, closest_deadline - now)
+ end
+ end
+
+ requests.each do |req|
+ req.interrupt if req.expired?(now)
+ end
+ requests.reject!(&:done?)
+ end
+ end
+ ThreadGroup::Default.add(watcher) unless watcher.group.enclosed?
+ watcher.name = "Gem::Timeout stdlib thread"
+ watcher.thread_variable_set(:"\0__detached_thread__", true)
+ watcher
+ end
+ private_class_method :create_timeout_thread
+
+ def self.ensure_timeout_thread_created
+ unless @timeout_thread and @timeout_thread.alive?
+ # If the Mutex is already owned we are in a signal handler.
+ # In that case, just return and let the main thread create the @timeout_thread.
+ return if TIMEOUT_THREAD_MUTEX.owned?
+ TIMEOUT_THREAD_MUTEX.synchronize do
+ unless @timeout_thread and @timeout_thread.alive?
+ @timeout_thread = create_timeout_thread
+ end
+ end
+ end
+ end
+
+ # We keep a private reference so that time mocking libraries won't break
+ # Gem::Timeout.
+ GET_TIME = Process.method(:clock_gettime)
+ private_constant :GET_TIME
+
+ # :startdoc:
+
+ # Perform an operation in a block, raising an error if it takes longer than
+ # +sec+ seconds to complete.
+ #
+ # +sec+:: Number of seconds to wait for the block to terminate. Any non-negative number
+ # or nil may be used, including Floats to specify fractional seconds. A
+ # value of 0 or +nil+ will execute the block without any timeout.
+ # Any negative number will raise an ArgumentError.
+ # +klass+:: Exception Class to raise if the block fails to terminate
+ # in +sec+ seconds. Omitting will use the default, Gem::Timeout::Error
+ # +message+:: Error message to raise with Exception Class.
+ # Omitting will use the default, "execution expired"
+ #
+ # Returns the result of the block *if* the block completed before
+ # +sec+ seconds, otherwise throws an exception, based on the value of +klass+.
+ #
+ # The exception thrown to terminate the given block cannot be rescued inside
+ # the block unless +klass+ is given explicitly. However, the block can use
+ # ensure to prevent the handling of the exception. For that reason, this
+ # method cannot be relied on to enforce timeouts for untrusted blocks.
+ #
+ # If a scheduler is defined, it will be used to handle the timeout by invoking
+ # Scheduler#timeout_after.
+ #
+ # Note that this is both a method of module Gem::Timeout, so you can <tt>include
+ # Gem::Timeout</tt> into your classes so they have a #timeout method, as well as
+ # a module method, so you can call it directly as Gem::Timeout.timeout().
+ def timeout(sec, klass = nil, message = nil, &block) #:yield: +sec+
+ return yield(sec) if sec == nil or sec.zero?
+ raise ArgumentError, "Timeout sec must be a non-negative number" if 0 > sec
+
+ message ||= "execution expired"
+
+ if Fiber.respond_to?(:current_scheduler) && (scheduler = Fiber.current_scheduler)&.respond_to?(:timeout_after)
+ return scheduler.timeout_after(sec, klass || Error, message, &block)
+ end
+
+ Gem::Timeout.ensure_timeout_thread_created
+ perform = Proc.new do |exc|
+ request = Request.new(Thread.current, sec, exc, message)
+ QUEUE_MUTEX.synchronize do
+ QUEUE << request
+ CONDVAR.signal
+ end
+ begin
+ return yield(sec)
+ ensure
+ request.finished
+ end
+ end
+
+ if klass
+ perform.call(klass)
+ else
+ Error.handle_timeout(message, &perform)
+ end
+ end
+ module_function :timeout
+end
diff --git a/lib/rubygems/vendor/tsort/lib/tsort.rb b/lib/rubygems/vendor/tsort/lib/tsort.rb
new file mode 100644
index 0000000000..9dd7c09521
--- /dev/null
+++ b/lib/rubygems/vendor/tsort/lib/tsort.rb
@@ -0,0 +1,455 @@
+# frozen_string_literal: true
+
+#--
+# tsort.rb - provides a module for topological sorting and strongly connected components.
+#++
+#
+
+#
+# Gem::TSort implements topological sorting using Tarjan's algorithm for
+# strongly connected components.
+#
+# Gem::TSort is designed to be able to be used with any object which can be
+# interpreted as a directed graph.
+#
+# Gem::TSort requires two methods to interpret an object as a graph,
+# tsort_each_node and tsort_each_child.
+#
+# * tsort_each_node is used to iterate for all nodes over a graph.
+# * tsort_each_child is used to iterate for child nodes of a given node.
+#
+# The equality of nodes are defined by eql? and hash since
+# Gem::TSort uses Hash internally.
+#
+# == A Simple Example
+#
+# The following example demonstrates how to mix the Gem::TSort module into an
+# existing class (in this case, Hash). Here, we're treating each key in
+# the hash as a node in the graph, and so we simply alias the required
+# #tsort_each_node method to Hash's #each_key method. For each key in the
+# hash, the associated value is an array of the node's child nodes. This
+# choice in turn leads to our implementation of the required #tsort_each_child
+# method, which fetches the array of child nodes and then iterates over that
+# array using the user-supplied block.
+#
+# require 'rubygems/vendor/tsort/lib/tsort'
+#
+# class Hash
+# include Gem::TSort
+# alias tsort_each_node each_key
+# def tsort_each_child(node, &block)
+# fetch(node).each(&block)
+# end
+# end
+#
+# {1=>[2, 3], 2=>[3], 3=>[], 4=>[]}.tsort
+# #=> [3, 2, 1, 4]
+#
+# {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}.strongly_connected_components
+# #=> [[4], [2, 3], [1]]
+#
+# == A More Realistic Example
+#
+# A very simple `make' like tool can be implemented as follows:
+#
+# require 'rubygems/vendor/tsort/lib/tsort'
+#
+# class Make
+# def initialize
+# @dep = {}
+# @dep.default = []
+# end
+#
+# def rule(outputs, inputs=[], &block)
+# triple = [outputs, inputs, block]
+# outputs.each {|f| @dep[f] = [triple]}
+# @dep[triple] = inputs
+# end
+#
+# def build(target)
+# each_strongly_connected_component_from(target) {|ns|
+# if ns.length != 1
+# fs = ns.delete_if {|n| Array === n}
+# raise Gem::TSort::Cyclic.new("cyclic dependencies: #{fs.join ', '}")
+# end
+# n = ns.first
+# if Array === n
+# outputs, inputs, block = n
+# inputs_time = inputs.map {|f| File.mtime f}.max
+# begin
+# outputs_time = outputs.map {|f| File.mtime f}.min
+# rescue Errno::ENOENT
+# outputs_time = nil
+# end
+# if outputs_time == nil ||
+# inputs_time != nil && outputs_time <= inputs_time
+# sleep 1 if inputs_time != nil && inputs_time.to_i == Time.now.to_i
+# block.call
+# end
+# end
+# }
+# end
+#
+# def tsort_each_child(node, &block)
+# @dep[node].each(&block)
+# end
+# include Gem::TSort
+# end
+#
+# def command(arg)
+# print arg, "\n"
+# system arg
+# end
+#
+# m = Make.new
+# m.rule(%w[t1]) { command 'date > t1' }
+# m.rule(%w[t2]) { command 'date > t2' }
+# m.rule(%w[t3]) { command 'date > t3' }
+# m.rule(%w[t4], %w[t1 t3]) { command 'cat t1 t3 > t4' }
+# m.rule(%w[t5], %w[t4 t2]) { command 'cat t4 t2 > t5' }
+# m.build('t5')
+#
+# == Bugs
+#
+# * 'tsort.rb' is wrong name because this library uses
+# Tarjan's algorithm for strongly connected components.
+# Although 'strongly_connected_components.rb' is correct but too long.
+#
+# == References
+#
+# R. E. Tarjan, "Depth First Search and Linear Graph Algorithms",
+# <em>SIAM Journal on Computing</em>, Vol. 1, No. 2, pp. 146-160, June 1972.
+#
+
+module Gem::TSort
+
+ VERSION = "0.2.0"
+
+ class Cyclic < StandardError
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.tsort #=> [4, 2, 3, 1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.tsort # raises Gem::TSort::Cyclic
+ #
+ def tsort
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.tsort(each_node, each_child)
+ end
+
+ # Returns a topologically sorted array of nodes.
+ # The array is sorted from children to parents, i.e.
+ # the first element has no child and the last node has no parent.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.tsort(each_node, each_child) #=> [4, 2, 3, 1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.tsort(each_node, each_child) # raises Gem::TSort::Cyclic
+ #
+ def self.tsort(each_node, each_child)
+ tsort_each(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #tsort method.
+ # <tt><em>obj</em>.tsort_each</tt> is similar to <tt><em>obj</em>.tsort.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #tsort_each returns +nil+.
+ # If there is a cycle, Gem::TSort::Cyclic is raised.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.tsort_each {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def tsort_each(&block) # :yields: node
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.tsort_each(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Gem::TSort.tsort method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.tsort_each(each_node, each_child) {|n| p n }
+ # #=> 4
+ # # 2
+ # # 3
+ # # 1
+ #
+ def self.tsort_each(each_node, each_child) # :yields: node
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ each_strongly_connected_component(each_node, each_child) {|component|
+ if component.size == 1
+ yield component.first
+ else
+ raise Cyclic.new("topological sort failed: #{component.inspect}")
+ end
+ }
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2], [3], [1]]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # p graph.strongly_connected_components #=> [[4], [2, 3], [1]]
+ #
+ def strongly_connected_components
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.strongly_connected_components(each_node, each_child)
+ end
+
+ # Returns strongly connected components as an array of arrays of nodes.
+ # The array is sorted from children to parents.
+ # Each elements of the array represents a strongly connected component.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2], [3], [1]]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # p Gem::TSort.strongly_connected_components(each_node, each_child)
+ # #=> [[4], [2, 3], [1]]
+ #
+ def self.strongly_connected_components(each_node, each_child)
+ each_strongly_connected_component(each_node, each_child).to_a
+ end
+
+ # The iterator version of the #strongly_connected_components method.
+ # <tt><em>obj</em>.each_strongly_connected_component</tt> is similar to
+ # <tt><em>obj</em>.strongly_connected_components.each</tt>, but
+ # modification of _obj_ during the iteration may lead to unexpected results.
+ #
+ # #each_strongly_connected_component returns +nil+.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def each_strongly_connected_component(&block) # :yields: nodes
+ each_node = method(:tsort_each_node)
+ each_child = method(:tsort_each_child)
+ Gem::TSort.each_strongly_connected_component(each_node, each_child, &block)
+ end
+
+ # The iterator version of the Gem::TSort.strongly_connected_components method.
+ #
+ # The graph is represented by _each_node_ and _each_child_.
+ # _each_node_ should have +call+ method which yields for each node in the graph.
+ # _each_child_ should have +call+ method which takes a node argument and yields for each child node.
+ #
+ # g = {1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ # # [3]
+ # # [1]
+ #
+ # g = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_node = lambda {|&b| g.each_key(&b) }
+ # each_child = lambda {|n, &b| g[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component(each_node, each_child) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component(each_node, each_child) # :yields: nodes
+ return to_enum(__method__, each_node, each_child) unless block_given?
+
+ id_map = {}
+ stack = []
+ each_node.call {|node|
+ unless id_map.include? node
+ each_strongly_connected_component_from(node, each_child, id_map, stack) {|c|
+ yield c
+ }
+ end
+ }
+ nil
+ end
+
+ # Iterates over strongly connected component in the subgraph reachable from
+ # _node_.
+ #
+ # Return value is unspecified.
+ #
+ # #each_strongly_connected_component_from doesn't call #tsort_each_node.
+ #
+ # class G
+ # include Gem::TSort
+ # def initialize(g)
+ # @g = g
+ # end
+ # def tsort_each_child(n, &b) @g[n].each(&b) end
+ # def tsort_each_node(&b) @g.each_key(&b) end
+ # end
+ #
+ # graph = G.new({1=>[2, 3], 2=>[4], 3=>[2, 4], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2]
+ #
+ # graph = G.new({1=>[2], 2=>[3, 4], 3=>[2], 4=>[]})
+ # graph.each_strongly_connected_component_from(2) {|scc| p scc }
+ # #=> [4]
+ # # [2, 3]
+ #
+ def each_strongly_connected_component_from(node, id_map={}, stack=[], &block) # :yields: nodes
+ Gem::TSort.each_strongly_connected_component_from(node, method(:tsort_each_child), id_map, stack, &block)
+ end
+
+ # Iterates over strongly connected components in a graph.
+ # The graph is represented by _node_ and _each_child_.
+ #
+ # _node_ is the first node.
+ # _each_child_ should have +call+ method which takes a node argument
+ # and yields for each child node.
+ #
+ # Return value is unspecified.
+ #
+ # #Gem::TSort.each_strongly_connected_component_from is a class method and
+ # it doesn't need a class to represent a graph which includes Gem::TSort.
+ #
+ # graph = {1=>[2], 2=>[3, 4], 3=>[2], 4=>[]}
+ # each_child = lambda {|n, &b| graph[n].each(&b) }
+ # Gem::TSort.each_strongly_connected_component_from(1, each_child) {|scc|
+ # p scc
+ # }
+ # #=> [4]
+ # # [2, 3]
+ # # [1]
+ #
+ def self.each_strongly_connected_component_from(node, each_child, id_map={}, stack=[]) # :yields: nodes
+ return to_enum(__method__, node, each_child, id_map, stack) unless block_given?
+
+ minimum_id = node_id = id_map[node] = id_map.size
+ stack_length = stack.length
+ stack << node
+
+ each_child.call(node) {|child|
+ if id_map.include? child
+ child_id = id_map[child]
+ minimum_id = child_id if child_id && child_id < minimum_id
+ else
+ sub_minimum_id =
+ each_strongly_connected_component_from(child, each_child, id_map, stack) {|c|
+ yield c
+ }
+ minimum_id = sub_minimum_id if sub_minimum_id < minimum_id
+ end
+ }
+
+ if node_id == minimum_id
+ component = stack.slice!(stack_length .. -1)
+ component.each {|n| id_map[n] = nil}
+ yield component
+ end
+
+ minimum_id
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_node is used to iterate for all nodes over a graph.
+ #
+ def tsort_each_node # :yields: node
+ raise NotImplementedError.new
+ end
+
+ # Should be implemented by a extended class.
+ #
+ # #tsort_each_child is used to iterate for child nodes of _node_.
+ #
+ def tsort_each_child(node) # :yields: child
+ raise NotImplementedError.new
+ end
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri.rb b/lib/rubygems/vendor/uri/lib/uri.rb
new file mode 100644
index 0000000000..4691b122b2
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: false
+# Gem::URI is a module providing classes to handle Uniform Resource Identifiers
+# (RFC2396[https://www.rfc-editor.org/rfc/rfc2396]).
+#
+# == Features
+#
+# * Uniform way of handling URIs.
+# * Flexibility to introduce custom Gem::URI schemes.
+# * Flexibility to have an alternate Gem::URI::Parser (or just different patterns
+# and regexp's).
+#
+# == Basic example
+#
+# require 'rubygems/vendor/uri/lib/uri'
+#
+# uri = Gem::URI("http://foo.com/posts?id=30&limit=5#time=1305298413")
+# #=> #<Gem::URI::HTTP http://foo.com/posts?id=30&limit=5#time=1305298413>
+#
+# uri.scheme #=> "http"
+# uri.host #=> "foo.com"
+# uri.path #=> "/posts"
+# uri.query #=> "id=30&limit=5"
+# uri.fragment #=> "time=1305298413"
+#
+# uri.to_s #=> "http://foo.com/posts?id=30&limit=5#time=1305298413"
+#
+# == Adding custom URIs
+#
+# module Gem::URI
+# class RSYNC < Generic
+# DEFAULT_PORT = 873
+# end
+# register_scheme 'RSYNC', RSYNC
+# end
+# #=> Gem::URI::RSYNC
+#
+# Gem::URI.scheme_list
+# #=> {"FILE"=>Gem::URI::File, "FTP"=>Gem::URI::FTP, "HTTP"=>Gem::URI::HTTP,
+# # "HTTPS"=>Gem::URI::HTTPS, "LDAP"=>Gem::URI::LDAP, "LDAPS"=>Gem::URI::LDAPS,
+# # "MAILTO"=>Gem::URI::MailTo, "RSYNC"=>Gem::URI::RSYNC}
+#
+# uri = Gem::URI("rsync://rsync.foo.com")
+# #=> #<Gem::URI::RSYNC rsync://rsync.foo.com>
+#
+# == RFC References
+#
+# A good place to view an RFC spec is http://www.ietf.org/rfc.html.
+#
+# Here is a list of all related RFC's:
+# - RFC822[https://www.rfc-editor.org/rfc/rfc822]
+# - RFC1738[https://www.rfc-editor.org/rfc/rfc1738]
+# - RFC2255[https://www.rfc-editor.org/rfc/rfc2255]
+# - RFC2368[https://www.rfc-editor.org/rfc/rfc2368]
+# - RFC2373[https://www.rfc-editor.org/rfc/rfc2373]
+# - RFC2396[https://www.rfc-editor.org/rfc/rfc2396]
+# - RFC2732[https://www.rfc-editor.org/rfc/rfc2732]
+# - RFC3986[https://www.rfc-editor.org/rfc/rfc3986]
+#
+# == Class tree
+#
+# - Gem::URI::Generic (in uri/generic.rb)
+# - Gem::URI::File - (in uri/file.rb)
+# - Gem::URI::FTP - (in uri/ftp.rb)
+# - Gem::URI::HTTP - (in uri/http.rb)
+# - Gem::URI::HTTPS - (in uri/https.rb)
+# - Gem::URI::LDAP - (in uri/ldap.rb)
+# - Gem::URI::LDAPS - (in uri/ldaps.rb)
+# - Gem::URI::MailTo - (in uri/mailto.rb)
+# - Gem::URI::Parser - (in uri/common.rb)
+# - Gem::URI::REGEXP - (in uri/common.rb)
+# - Gem::URI::REGEXP::PATTERN - (in uri/common.rb)
+# - Gem::URI::Util - (in uri/common.rb)
+# - Gem::URI::Error - (in uri/common.rb)
+# - Gem::URI::InvalidURIError - (in uri/common.rb)
+# - Gem::URI::InvalidComponentError - (in uri/common.rb)
+# - Gem::URI::BadURIError - (in uri/common.rb)
+#
+# == Copyright Info
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# Documentation::
+# Akira Yamada <akira@ruby-lang.org>
+# Dmitry V. Sabanin <sdmitry@lrn.ru>
+# Vincent Batts <vbatts@hashbangbash.com>
+# License::
+# Copyright (c) 2001 akira yamada <akira@ruby-lang.org>
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+
+module Gem::URI
+end
+
+require_relative 'uri/version'
+require_relative 'uri/common'
+require_relative 'uri/generic'
+require_relative 'uri/file'
+require_relative 'uri/ftp'
+require_relative 'uri/http'
+require_relative 'uri/https'
+require_relative 'uri/ldap'
+require_relative 'uri/ldaps'
+require_relative 'uri/mailto'
+require_relative 'uri/ws'
+require_relative 'uri/wss'
diff --git a/lib/rubygems/vendor/uri/lib/uri/common.rb b/lib/rubygems/vendor/uri/lib/uri/common.rb
new file mode 100644
index 0000000000..e9bdfa6a07
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/common.rb
@@ -0,0 +1,922 @@
+# frozen_string_literal: true
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative "rfc2396_parser"
+require_relative "rfc3986_parser"
+
+module Gem::URI
+ # The default parser instance for RFC 2396.
+ RFC2396_PARSER = RFC2396_Parser.new
+ Ractor.make_shareable(RFC2396_PARSER) if defined?(Ractor)
+
+ # The default parser instance for RFC 3986.
+ RFC3986_PARSER = RFC3986_Parser.new
+ Ractor.make_shareable(RFC3986_PARSER) if defined?(Ractor)
+
+ # The default parser instance.
+ DEFAULT_PARSER = RFC3986_PARSER
+ Ractor.make_shareable(DEFAULT_PARSER) if defined?(Ractor)
+
+ # Set the default parser instance.
+ def self.parser=(parser = RFC3986_PARSER)
+ remove_const(:Parser) if defined?(::Gem::URI::Parser)
+ const_set("Parser", parser.class)
+
+ remove_const(:PARSER) if defined?(::Gem::URI::PARSER)
+ const_set("PARSER", parser)
+
+ remove_const(:REGEXP) if defined?(::Gem::URI::REGEXP)
+ remove_const(:PATTERN) if defined?(::Gem::URI::PATTERN)
+ if Parser == RFC2396_Parser
+ const_set("REGEXP", Gem::URI::RFC2396_REGEXP)
+ const_set("PATTERN", Gem::URI::RFC2396_REGEXP::PATTERN)
+ end
+
+ Parser.new.regexp.each_pair do |sym, str|
+ remove_const(sym) if const_defined?(sym, false)
+ const_set(sym, str)
+ end
+ end
+ self.parser = RFC3986_PARSER
+
+ def self.const_missing(const) # :nodoc:
+ if const == :REGEXP
+ warn "Gem::URI::REGEXP is obsolete. Use Gem::URI::RFC2396_REGEXP explicitly.", uplevel: 1 if $VERBOSE
+ Gem::URI::RFC2396_REGEXP
+ elsif value = RFC2396_PARSER.regexp[const]
+ warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_PARSER.regexp[#{const.inspect}] explicitly.", uplevel: 1 if $VERBOSE
+ value
+ elsif value = RFC2396_Parser.const_get(const)
+ warn "Gem::URI::#{const} is obsolete. Use Gem::URI::RFC2396_Parser::#{const} explicitly.", uplevel: 1 if $VERBOSE
+ value
+ else
+ super
+ end
+ end
+
+ module Util # :nodoc:
+ def make_components_hash(klass, array_hash)
+ tmp = {}
+ if array_hash.kind_of?(Array) &&
+ array_hash.size == klass.component.size - 1
+ klass.component[1..-1].each_index do |i|
+ begin
+ tmp[klass.component[i + 1]] = array_hash[i].clone
+ rescue TypeError
+ tmp[klass.component[i + 1]] = array_hash[i]
+ end
+ end
+
+ elsif array_hash.kind_of?(Hash)
+ array_hash.each do |key, value|
+ begin
+ tmp[key] = value.clone
+ rescue TypeError
+ tmp[key] = value
+ end
+ end
+ else
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{klass} (#{klass.component[1..-1].join(', ')})"
+ end
+ tmp[:scheme] = klass.to_s.sub(/\A.*::/, '').downcase
+
+ return tmp
+ end
+ module_function :make_components_hash
+ end
+
+ module Schemes # :nodoc:
+ class << self
+ ReservedChars = ".+-"
+ EscapedChars = "\u01C0\u01C1\u01C2"
+ # Use Lo category chars as escaped chars for TruffleRuby, which
+ # does not allow Symbol categories as identifiers.
+
+ def escape(name)
+ unless name and name.ascii_only?
+ return nil
+ end
+ name.upcase.tr(ReservedChars, EscapedChars)
+ end
+
+ def unescape(name)
+ name.tr(EscapedChars, ReservedChars).encode(Encoding::US_ASCII).upcase
+ end
+
+ def find(name)
+ const_get(name, false) if name and const_defined?(name, false)
+ end
+
+ def register(name, klass)
+ unless scheme = escape(name)
+ raise ArgumentError, "invalid character as scheme - #{name}"
+ end
+ const_set(scheme, klass)
+ end
+
+ def list
+ constants.map { |name|
+ [unescape(name.to_s), const_get(name)]
+ }.to_h
+ end
+ end
+ end
+ private_constant :Schemes
+
+ # Registers the given +klass+ as the class to be instantiated
+ # when parsing a \Gem::URI with the given +scheme+:
+ #
+ # Gem::URI.register_scheme('MS_SEARCH', Gem::URI::Generic) # => Gem::URI::Generic
+ # Gem::URI.scheme_list['MS_SEARCH'] # => Gem::URI::Generic
+ #
+ # Note that after calling String#upcase on +scheme+, it must be a valid
+ # constant name.
+ def self.register_scheme(scheme, klass)
+ Schemes.register(scheme, klass)
+ end
+
+ # Returns a hash of the defined schemes:
+ #
+ # Gem::URI.scheme_list
+ # # =>
+ # {"MAILTO"=>Gem::URI::MailTo,
+ # "LDAPS"=>Gem::URI::LDAPS,
+ # "WS"=>Gem::URI::WS,
+ # "HTTP"=>Gem::URI::HTTP,
+ # "HTTPS"=>Gem::URI::HTTPS,
+ # "LDAP"=>Gem::URI::LDAP,
+ # "FILE"=>Gem::URI::File,
+ # "FTP"=>Gem::URI::FTP}
+ #
+ # Related: Gem::URI.register_scheme.
+ def self.scheme_list
+ Schemes.list
+ end
+
+ # :stopdoc:
+ INITIAL_SCHEMES = scheme_list
+ private_constant :INITIAL_SCHEMES
+ Ractor.make_shareable(INITIAL_SCHEMES) if defined?(Ractor)
+ # :startdoc:
+
+ # Returns a new object constructed from the given +scheme+, +arguments+,
+ # and +default+:
+ #
+ # - The new object is an instance of <tt>Gem::URI.scheme_list[scheme.upcase]</tt>.
+ # - The object is initialized by calling the class initializer
+ # using +scheme+ and +arguments+.
+ # See Gem::URI::Generic.new.
+ #
+ # Examples:
+ #
+ # values = ['john.doe', 'www.example.com', '123', nil, '/forum/questions/', nil, 'tag=networking&order=newest', 'top']
+ # Gem::URI.for('https', *values)
+ # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Gem::URI.for('foo', *values, default: Gem::URI::HTTP)
+ # # => #<Gem::URI::HTTP foo://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ def self.for(scheme, *arguments, default: Generic)
+ const_name = Schemes.escape(scheme)
+
+ uri_class = INITIAL_SCHEMES[const_name]
+ uri_class ||= Schemes.find(const_name)
+ uri_class ||= default
+
+ return uri_class.new(scheme, *arguments)
+ end
+
+ #
+ # Base class for all Gem::URI exceptions.
+ #
+ class Error < StandardError; end
+ #
+ # Not a Gem::URI.
+ #
+ class InvalidURIError < Error; end
+ #
+ # Not a Gem::URI component.
+ #
+ class InvalidComponentError < Error; end
+ #
+ # Gem::URI is valid, bad usage is not.
+ #
+ class BadURIError < Error; end
+
+ # Returns a 9-element array representing the parts of the \Gem::URI
+ # formed from the string +uri+;
+ # each array element is a string or +nil+:
+ #
+ # names = %w[scheme userinfo host port registry path opaque query fragment]
+ # values = Gem::URI.split('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # names.zip(values)
+ # # =>
+ # [["scheme", "https"],
+ # ["userinfo", "john.doe"],
+ # ["host", "www.example.com"],
+ # ["port", "123"],
+ # ["registry", nil],
+ # ["path", "/forum/questions/"],
+ # ["opaque", nil],
+ # ["query", "tag=networking&order=newest"],
+ # ["fragment", "top"]]
+ #
+ def self.split(uri)
+ PARSER.split(uri)
+ end
+
+ # Returns a new \Gem::URI object constructed from the given string +uri+:
+ #
+ # Gem::URI.parse('https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Gem::URI::HTTPS https://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ # Gem::URI.parse('http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top')
+ # # => #<Gem::URI::HTTP http://john.doe@www.example.com:123/forum/questions/?tag=networking&order=newest#top>
+ #
+ # It's recommended to first Gem::URI::RFC2396_PARSER.escape string +uri+
+ # if it may contain invalid Gem::URI characters.
+ #
+ def self.parse(uri)
+ PARSER.parse(uri)
+ end
+
+ # Merges the given Gem::URI strings +str+
+ # per {RFC 2396}[https://www.rfc-editor.org/rfc/rfc2396.html].
+ #
+ # Each string in +str+ is converted to an
+ # {RFC3986 Gem::URI}[https://www.rfc-editor.org/rfc/rfc3986.html] before being merged.
+ #
+ # Examples:
+ #
+ # Gem::URI.join("http://example.com/","main.rbx")
+ # # => #<Gem::URI::HTTP http://example.com/main.rbx>
+ #
+ # Gem::URI.join('http://example.com', 'foo')
+ # # => #<Gem::URI::HTTP http://example.com/foo>
+ #
+ # Gem::URI.join('http://example.com', '/foo', '/bar')
+ # # => #<Gem::URI::HTTP http://example.com/bar>
+ #
+ # Gem::URI.join('http://example.com', '/foo', 'bar')
+ # # => #<Gem::URI::HTTP http://example.com/bar>
+ #
+ # Gem::URI.join('http://example.com', '/foo/', 'bar')
+ # # => #<Gem::URI::HTTP http://example.com/foo/bar>
+ #
+ def self.join(*str)
+ DEFAULT_PARSER.join(*str)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::extract(str[, schemes][,&blk])
+ #
+ # == Args
+ #
+ # +str+::
+ # String to extract URIs from.
+ # +schemes+::
+ # Limit Gem::URI matching to specific schemes.
+ #
+ # == Description
+ #
+ # Extracts URIs from a string. If block given, iterates through all matched URIs.
+ # Returns nil if block given or array with matches.
+ #
+ # == Usage
+ #
+ # require "rubygems/vendor/uri/lib/uri"
+ #
+ # Gem::URI.extract("text here http://foo.example.org/bla and here mailto:test@example.com and here also.")
+ # # => ["http://foo.example.com/bla", "mailto:test@example.com"]
+ #
+ def self.extract(str, schemes = nil, &block) # :nodoc:
+ warn "Gem::URI.extract is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.extract(str, schemes, &block)
+ end
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::regexp([match_schemes])
+ #
+ # == Args
+ #
+ # +match_schemes+::
+ # Array of schemes. If given, resulting regexp matches to URIs
+ # whose scheme is one of the match_schemes.
+ #
+ # == Description
+ #
+ # Returns a Regexp object which matches to Gem::URI-like strings.
+ # The Regexp object returned by this method includes arbitrary
+ # number of capture group (parentheses). Never rely on its number.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # # extract first Gem::URI from html_string
+ # html_string.slice(Gem::URI.regexp)
+ #
+ # # remove ftp URIs
+ # html_string.sub(Gem::URI.regexp(['ftp']), '')
+ #
+ # # You should not rely on the number of parentheses
+ # html_string.scan(Gem::URI.regexp) do |*matches|
+ # p $&
+ # end
+ #
+ def self.regexp(schemes = nil)# :nodoc:
+ warn "Gem::URI.regexp is obsolete", uplevel: 1 if $VERBOSE
+ PARSER.make_regexp(schemes)
+ end
+
+ TBLENCWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ TBLENCWWWCOMP_[-i.chr] = -('%%%02X' % i)
+ end
+ TBLENCURICOMP_ = TBLENCWWWCOMP_.dup.freeze # :nodoc:
+ TBLENCWWWCOMP_[' '] = '+'
+ TBLENCWWWCOMP_.freeze
+ TBLDECWWWCOMP_ = {} # :nodoc:
+ 256.times do |i|
+ h, l = i>>4, i&15
+ TBLDECWWWCOMP_[-('%%%X%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%X' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%X%x' % [h, l])] = -i.chr
+ TBLDECWWWCOMP_[-('%%%x%x' % [h, l])] = -i.chr
+ end
+ TBLDECWWWCOMP_['+'] = ' '
+ TBLDECWWWCOMP_.freeze
+
+ # Returns a URL-encoded string derived from the given string +str+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>' '</tt> to character <tt>'+'</tt>.
+ # - Any other character to "percent notation";
+ # the percent notation for character <i>c</i> is <tt>'%%%X' % c.ord</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form_component('Here are some punctuation characters: ,;?:')
+ # # => "Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A"
+ #
+ # Encoding:
+ #
+ # - If +str+ has encoding Encoding::ASCII_8BIT, argument +enc+ is ignored.
+ # - Otherwise +str+ is converted first to Encoding::UTF_8
+ # (with suitable character replacements),
+ # and then to encoding +enc+.
+ #
+ # In either case, the returned string has forced encoding Encoding::US_ASCII.
+ #
+ # Related: Gem::URI.encode_uri_component (encodes <tt>' '</tt> as <tt>'%20'</tt>).
+ def self.encode_www_form_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCWWWCOMP_, str, enc)
+ end
+
+ # Returns a string decoded from the given \URL-encoded string +str+.
+ #
+ # The given string is first encoded as Encoding::ASCII-8BIT (using String#b),
+ # then decoded (as below), and finally force-encoded to the given encoding +enc+.
+ #
+ # The returned string:
+ #
+ # - Preserves:
+ #
+ # - Characters <tt>'*'</tt>, <tt>'.'</tt>, <tt>'-'</tt>, and <tt>'_'</tt>.
+ # - Character in ranges <tt>'a'..'z'</tt>, <tt>'A'..'Z'</tt>,
+ # and <tt>'0'..'9'</tt>.
+ #
+ # Example:
+ #
+ # Gem::URI.decode_www_form_component('*.-_azAZ09')
+ # # => "*.-_azAZ09"
+ #
+ # - Converts:
+ #
+ # - Character <tt>'+'</tt> to character <tt>' '</tt>.
+ # - Each "percent notation" to an ASCII character.
+ #
+ # Example:
+ #
+ # Gem::URI.decode_www_form_component('Here+are+some+punctuation+characters%3A+%2C%3B%3F%3A')
+ # # => "Here are some punctuation characters: ,;?:"
+ #
+ # Related: Gem::URI.decode_uri_component (preserves <tt>'+'</tt>).
+ def self.decode_www_form_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/\+|%\h\h/, str, enc)
+ end
+
+ # Like Gem::URI.encode_www_form_component, except that <tt>' '</tt> (space)
+ # is encoded as <tt>'%20'</tt> (instead of <tt>'+'</tt>).
+ def self.encode_uri_component(str, enc=nil)
+ _encode_uri_component(/[^*\-.0-9A-Z_a-z]/, TBLENCURICOMP_, str, enc)
+ end
+
+ # Like Gem::URI.decode_www_form_component, except that <tt>'+'</tt> is preserved.
+ def self.decode_uri_component(str, enc=Encoding::UTF_8)
+ _decode_uri_component(/%\h\h/, str, enc)
+ end
+
+ # Returns a string derived from the given string +str+ with
+ # Gem::URI-encoded characters matching +regexp+ according to +table+.
+ def self._encode_uri_component(regexp, table, str, enc)
+ str = str.to_s.dup
+ if str.encoding != Encoding::ASCII_8BIT
+ if enc && enc != Encoding::ASCII_8BIT
+ str.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace)
+ str.encode!(enc, fallback: ->(x){"&##{x.ord};"})
+ end
+ str.force_encoding(Encoding::ASCII_8BIT)
+ end
+ str.gsub!(regexp, table)
+ str.force_encoding(Encoding::US_ASCII)
+ end
+ private_class_method :_encode_uri_component
+
+ # Returns a string decoding characters matching +regexp+ from the
+ # given \URL-encoded string +str+.
+ def self._decode_uri_component(regexp, str, enc)
+ raise ArgumentError, "invalid %-encoding (#{str})" if /%(?!\h\h)/.match?(str)
+ str.b.gsub(regexp, TBLDECWWWCOMP_).force_encoding(enc)
+ end
+ private_class_method :_decode_uri_component
+
+ # Returns a URL-encoded string derived from the given
+ # {Enumerable}[rdoc-ref:Enumerable@Enumerable+in+Ruby+Classes]
+ # +enum+.
+ #
+ # The result is suitable for use as form data
+ # for an \HTTP request whose <tt>Content-Type</tt> is
+ # <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned string consists of the elements of +enum+,
+ # each converted to one or more URL-encoded strings,
+ # and all joined with character <tt>'&'</tt>.
+ #
+ # Simple examples:
+ #
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', 1], ['baz', 2]])
+ # # => "foo=0&bar=1&baz=2"
+ # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The returned string is formed using method Gem::URI.encode_www_form_component,
+ # which converts certain characters:
+ #
+ # Gem::URI.encode_www_form('f#o': '/', 'b-r': '$', 'b z': '@')
+ # # => "f%23o=%2F&b-r=%24&b+z=%40"
+ #
+ # When +enum+ is Array-like, each element +ele+ is converted to a field:
+ #
+ # - If +ele+ is an array of two or more elements,
+ # the field is formed from its first two elements
+ # (and any additional elements are ignored):
+ #
+ # name = Gem::URI.encode_www_form_component(ele[0], enc)
+ # value = Gem::URI.encode_www_form_component(ele[1], enc)
+ # "#{name}=#{value}"
+ #
+ # Examples:
+ #
+ # Gem::URI.encode_www_form([%w[foo bar], %w[baz bat bah]])
+ # # => "foo=bar&baz=bat"
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', :baz, 'bat']])
+ # # => "foo=0&bar=baz"
+ #
+ # - If +ele+ is an array of one element,
+ # the field is formed from <tt>ele[0]</tt>:
+ #
+ # Gem::URI.encode_www_form_component(ele[0])
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form([['foo'], [:bar], [0]])
+ # # => "foo&bar&0"
+ #
+ # - Otherwise the field is formed from +ele+:
+ #
+ # Gem::URI.encode_www_form_component(ele)
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form(['foo', :bar, 0])
+ # # => "foo&bar&0"
+ #
+ # The elements of an Array-like +enum+ may be mixture:
+ #
+ # Gem::URI.encode_www_form([['foo', 0], ['bar', 1, 2], ['baz'], :bat])
+ # # => "foo=0&bar=1&baz&bat"
+ #
+ # When +enum+ is Hash-like,
+ # each +key+/+value+ pair is converted to one or more fields:
+ #
+ # - If +value+ is
+ # {Array-convertible}[rdoc-ref:implicit_conversion.rdoc@Array-Convertible+Objects],
+ # each element +ele+ in +value+ is paired with +key+ to form a field:
+ #
+ # name = Gem::URI.encode_www_form_component(key, enc)
+ # value = Gem::URI.encode_www_form_component(ele, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form({foo: [:bar, 1], baz: [:bat, :bam, 2]})
+ # # => "foo=bar&foo=1&baz=bat&baz=bam&baz=2"
+ #
+ # - Otherwise, +key+ and +value+ are paired to form a field:
+ #
+ # name = Gem::URI.encode_www_form_component(key, enc)
+ # value = Gem::URI.encode_www_form_component(value, enc)
+ # "#{name}=#{value}"
+ #
+ # Example:
+ #
+ # Gem::URI.encode_www_form({foo: 0, bar: 1, baz: 2})
+ # # => "foo=0&bar=1&baz=2"
+ #
+ # The elements of a Hash-like +enum+ may be mixture:
+ #
+ # Gem::URI.encode_www_form({foo: [0, 1], bar: 2})
+ # # => "foo=0&foo=1&bar=2"
+ #
+ def self.encode_www_form(enum, enc=nil)
+ enum.map do |k,v|
+ if v.nil?
+ encode_www_form_component(k, enc)
+ elsif v.respond_to?(:to_ary)
+ v.to_ary.map do |w|
+ str = encode_www_form_component(k, enc)
+ unless w.nil?
+ str << '='
+ str << encode_www_form_component(w, enc)
+ end
+ end.join('&')
+ else
+ str = encode_www_form_component(k, enc)
+ str << '='
+ str << encode_www_form_component(v, enc)
+ end
+ end.join('&')
+ end
+
+ # Returns name/value pairs derived from the given string +str+,
+ # which must be an ASCII string.
+ #
+ # The method may be used to decode the body of Net::HTTPResponse object +res+
+ # for which <tt>res['Content-Type']</tt> is <tt>'application/x-www-form-urlencoded'</tt>.
+ #
+ # The returned data is an array of 2-element subarrays;
+ # each subarray is a name/value pair (both are strings).
+ # Each returned string has encoding +enc+,
+ # and has had invalid characters removed via
+ # {String#scrub}[rdoc-ref:String#scrub].
+ #
+ # A simple example:
+ #
+ # Gem::URI.decode_www_form('foo=0&bar=1&baz')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ # The returned strings have certain conversions,
+ # similar to those performed in Gem::URI.decode_www_form_component:
+ #
+ # Gem::URI.decode_www_form('f%23o=%2F&b-r=%24&b+z=%40')
+ # # => [["f#o", "/"], ["b-r", "$"], ["b z", "@"]]
+ #
+ # The given string may contain consecutive separators:
+ #
+ # Gem::URI.decode_www_form('foo=0&&bar=1&&baz=2')
+ # # => [["foo", "0"], ["", ""], ["bar", "1"], ["", ""], ["baz", "2"]]
+ #
+ # A different separator may be specified:
+ #
+ # Gem::URI.decode_www_form('foo=0--bar=1--baz', separator: '--')
+ # # => [["foo", "0"], ["bar", "1"], ["baz", ""]]
+ #
+ def self.decode_www_form(str, enc=Encoding::UTF_8, separator: '&', use__charset_: false, isindex: false)
+ raise ArgumentError, "the input of #{self.name}.#{__method__} must be ASCII only string" unless str.ascii_only?
+ ary = []
+ return ary if str.empty?
+ enc = Encoding.find(enc)
+ str.b.each_line(separator) do |string|
+ string.chomp!(separator)
+ key, sep, val = string.partition('=')
+ if isindex
+ if sep.empty?
+ val = key
+ key = +''
+ end
+ isindex = false
+ end
+
+ if use__charset_ and key == '_charset_' and e = get_encoding(val)
+ enc = e
+ use__charset_ = false
+ end
+
+ key.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ if val
+ val.gsub!(/\+|%\h\h/, TBLDECWWWCOMP_)
+ else
+ val = +''
+ end
+
+ ary << [key, val]
+ end
+ ary.each do |k, v|
+ k.force_encoding(enc)
+ k.scrub!
+ v.force_encoding(enc)
+ v.scrub!
+ end
+ ary
+ end
+
+ private
+=begin command for WEB_ENCODINGS_
+ curl https://encoding.spec.whatwg.org/encodings.json|
+ ruby -rjson -e 'H={}
+ h={
+ "shift_jis"=>"Windows-31J",
+ "euc-jp"=>"cp51932",
+ "iso-2022-jp"=>"cp50221",
+ "x-mac-cyrillic"=>"macCyrillic",
+ }
+ JSON($<.read).map{|x|x["encodings"]}.flatten.each{|x|
+ Encoding.find(n=h.fetch(n=x["name"].downcase,n))rescue next
+ x["labels"].each{|y|H[y]=n}
+ }
+ puts "{"
+ H.each{|k,v|puts %[ #{k.dump}=>#{v.dump},]}
+ puts "}"
+'
+=end
+ WEB_ENCODINGS_ = {
+ "unicode-1-1-utf-8"=>"utf-8",
+ "utf-8"=>"utf-8",
+ "utf8"=>"utf-8",
+ "866"=>"ibm866",
+ "cp866"=>"ibm866",
+ "csibm866"=>"ibm866",
+ "ibm866"=>"ibm866",
+ "csisolatin2"=>"iso-8859-2",
+ "iso-8859-2"=>"iso-8859-2",
+ "iso-ir-101"=>"iso-8859-2",
+ "iso8859-2"=>"iso-8859-2",
+ "iso88592"=>"iso-8859-2",
+ "iso_8859-2"=>"iso-8859-2",
+ "iso_8859-2:1987"=>"iso-8859-2",
+ "l2"=>"iso-8859-2",
+ "latin2"=>"iso-8859-2",
+ "csisolatin3"=>"iso-8859-3",
+ "iso-8859-3"=>"iso-8859-3",
+ "iso-ir-109"=>"iso-8859-3",
+ "iso8859-3"=>"iso-8859-3",
+ "iso88593"=>"iso-8859-3",
+ "iso_8859-3"=>"iso-8859-3",
+ "iso_8859-3:1988"=>"iso-8859-3",
+ "l3"=>"iso-8859-3",
+ "latin3"=>"iso-8859-3",
+ "csisolatin4"=>"iso-8859-4",
+ "iso-8859-4"=>"iso-8859-4",
+ "iso-ir-110"=>"iso-8859-4",
+ "iso8859-4"=>"iso-8859-4",
+ "iso88594"=>"iso-8859-4",
+ "iso_8859-4"=>"iso-8859-4",
+ "iso_8859-4:1988"=>"iso-8859-4",
+ "l4"=>"iso-8859-4",
+ "latin4"=>"iso-8859-4",
+ "csisolatincyrillic"=>"iso-8859-5",
+ "cyrillic"=>"iso-8859-5",
+ "iso-8859-5"=>"iso-8859-5",
+ "iso-ir-144"=>"iso-8859-5",
+ "iso8859-5"=>"iso-8859-5",
+ "iso88595"=>"iso-8859-5",
+ "iso_8859-5"=>"iso-8859-5",
+ "iso_8859-5:1988"=>"iso-8859-5",
+ "arabic"=>"iso-8859-6",
+ "asmo-708"=>"iso-8859-6",
+ "csiso88596e"=>"iso-8859-6",
+ "csiso88596i"=>"iso-8859-6",
+ "csisolatinarabic"=>"iso-8859-6",
+ "ecma-114"=>"iso-8859-6",
+ "iso-8859-6"=>"iso-8859-6",
+ "iso-8859-6-e"=>"iso-8859-6",
+ "iso-8859-6-i"=>"iso-8859-6",
+ "iso-ir-127"=>"iso-8859-6",
+ "iso8859-6"=>"iso-8859-6",
+ "iso88596"=>"iso-8859-6",
+ "iso_8859-6"=>"iso-8859-6",
+ "iso_8859-6:1987"=>"iso-8859-6",
+ "csisolatingreek"=>"iso-8859-7",
+ "ecma-118"=>"iso-8859-7",
+ "elot_928"=>"iso-8859-7",
+ "greek"=>"iso-8859-7",
+ "greek8"=>"iso-8859-7",
+ "iso-8859-7"=>"iso-8859-7",
+ "iso-ir-126"=>"iso-8859-7",
+ "iso8859-7"=>"iso-8859-7",
+ "iso88597"=>"iso-8859-7",
+ "iso_8859-7"=>"iso-8859-7",
+ "iso_8859-7:1987"=>"iso-8859-7",
+ "sun_eu_greek"=>"iso-8859-7",
+ "csiso88598e"=>"iso-8859-8",
+ "csisolatinhebrew"=>"iso-8859-8",
+ "hebrew"=>"iso-8859-8",
+ "iso-8859-8"=>"iso-8859-8",
+ "iso-8859-8-e"=>"iso-8859-8",
+ "iso-ir-138"=>"iso-8859-8",
+ "iso8859-8"=>"iso-8859-8",
+ "iso88598"=>"iso-8859-8",
+ "iso_8859-8"=>"iso-8859-8",
+ "iso_8859-8:1988"=>"iso-8859-8",
+ "visual"=>"iso-8859-8",
+ "csisolatin6"=>"iso-8859-10",
+ "iso-8859-10"=>"iso-8859-10",
+ "iso-ir-157"=>"iso-8859-10",
+ "iso8859-10"=>"iso-8859-10",
+ "iso885910"=>"iso-8859-10",
+ "l6"=>"iso-8859-10",
+ "latin6"=>"iso-8859-10",
+ "iso-8859-13"=>"iso-8859-13",
+ "iso8859-13"=>"iso-8859-13",
+ "iso885913"=>"iso-8859-13",
+ "iso-8859-14"=>"iso-8859-14",
+ "iso8859-14"=>"iso-8859-14",
+ "iso885914"=>"iso-8859-14",
+ "csisolatin9"=>"iso-8859-15",
+ "iso-8859-15"=>"iso-8859-15",
+ "iso8859-15"=>"iso-8859-15",
+ "iso885915"=>"iso-8859-15",
+ "iso_8859-15"=>"iso-8859-15",
+ "l9"=>"iso-8859-15",
+ "iso-8859-16"=>"iso-8859-16",
+ "cskoi8r"=>"koi8-r",
+ "koi"=>"koi8-r",
+ "koi8"=>"koi8-r",
+ "koi8-r"=>"koi8-r",
+ "koi8_r"=>"koi8-r",
+ "koi8-ru"=>"koi8-u",
+ "koi8-u"=>"koi8-u",
+ "dos-874"=>"windows-874",
+ "iso-8859-11"=>"windows-874",
+ "iso8859-11"=>"windows-874",
+ "iso885911"=>"windows-874",
+ "tis-620"=>"windows-874",
+ "windows-874"=>"windows-874",
+ "cp1250"=>"windows-1250",
+ "windows-1250"=>"windows-1250",
+ "x-cp1250"=>"windows-1250",
+ "cp1251"=>"windows-1251",
+ "windows-1251"=>"windows-1251",
+ "x-cp1251"=>"windows-1251",
+ "ansi_x3.4-1968"=>"windows-1252",
+ "ascii"=>"windows-1252",
+ "cp1252"=>"windows-1252",
+ "cp819"=>"windows-1252",
+ "csisolatin1"=>"windows-1252",
+ "ibm819"=>"windows-1252",
+ "iso-8859-1"=>"windows-1252",
+ "iso-ir-100"=>"windows-1252",
+ "iso8859-1"=>"windows-1252",
+ "iso88591"=>"windows-1252",
+ "iso_8859-1"=>"windows-1252",
+ "iso_8859-1:1987"=>"windows-1252",
+ "l1"=>"windows-1252",
+ "latin1"=>"windows-1252",
+ "us-ascii"=>"windows-1252",
+ "windows-1252"=>"windows-1252",
+ "x-cp1252"=>"windows-1252",
+ "cp1253"=>"windows-1253",
+ "windows-1253"=>"windows-1253",
+ "x-cp1253"=>"windows-1253",
+ "cp1254"=>"windows-1254",
+ "csisolatin5"=>"windows-1254",
+ "iso-8859-9"=>"windows-1254",
+ "iso-ir-148"=>"windows-1254",
+ "iso8859-9"=>"windows-1254",
+ "iso88599"=>"windows-1254",
+ "iso_8859-9"=>"windows-1254",
+ "iso_8859-9:1989"=>"windows-1254",
+ "l5"=>"windows-1254",
+ "latin5"=>"windows-1254",
+ "windows-1254"=>"windows-1254",
+ "x-cp1254"=>"windows-1254",
+ "cp1255"=>"windows-1255",
+ "windows-1255"=>"windows-1255",
+ "x-cp1255"=>"windows-1255",
+ "cp1256"=>"windows-1256",
+ "windows-1256"=>"windows-1256",
+ "x-cp1256"=>"windows-1256",
+ "cp1257"=>"windows-1257",
+ "windows-1257"=>"windows-1257",
+ "x-cp1257"=>"windows-1257",
+ "cp1258"=>"windows-1258",
+ "windows-1258"=>"windows-1258",
+ "x-cp1258"=>"windows-1258",
+ "x-mac-cyrillic"=>"macCyrillic",
+ "x-mac-ukrainian"=>"macCyrillic",
+ "chinese"=>"gbk",
+ "csgb2312"=>"gbk",
+ "csiso58gb231280"=>"gbk",
+ "gb2312"=>"gbk",
+ "gb_2312"=>"gbk",
+ "gb_2312-80"=>"gbk",
+ "gbk"=>"gbk",
+ "iso-ir-58"=>"gbk",
+ "x-gbk"=>"gbk",
+ "gb18030"=>"gb18030",
+ "big5"=>"big5",
+ "big5-hkscs"=>"big5",
+ "cn-big5"=>"big5",
+ "csbig5"=>"big5",
+ "x-x-big5"=>"big5",
+ "cseucpkdfmtjapanese"=>"cp51932",
+ "euc-jp"=>"cp51932",
+ "x-euc-jp"=>"cp51932",
+ "csiso2022jp"=>"cp50221",
+ "iso-2022-jp"=>"cp50221",
+ "csshiftjis"=>"Windows-31J",
+ "ms932"=>"Windows-31J",
+ "ms_kanji"=>"Windows-31J",
+ "shift-jis"=>"Windows-31J",
+ "shift_jis"=>"Windows-31J",
+ "sjis"=>"Windows-31J",
+ "windows-31j"=>"Windows-31J",
+ "x-sjis"=>"Windows-31J",
+ "cseuckr"=>"euc-kr",
+ "csksc56011987"=>"euc-kr",
+ "euc-kr"=>"euc-kr",
+ "iso-ir-149"=>"euc-kr",
+ "korean"=>"euc-kr",
+ "ks_c_5601-1987"=>"euc-kr",
+ "ks_c_5601-1989"=>"euc-kr",
+ "ksc5601"=>"euc-kr",
+ "ksc_5601"=>"euc-kr",
+ "windows-949"=>"euc-kr",
+ "utf-16be"=>"utf-16be",
+ "utf-16"=>"utf-16le",
+ "utf-16le"=>"utf-16le",
+ } # :nodoc:
+ Ractor.make_shareable(WEB_ENCODINGS_) if defined?(Ractor)
+
+ # :nodoc:
+ # return encoding or nil
+ # http://encoding.spec.whatwg.org/#concept-encoding-get
+ def self.get_encoding(label)
+ Encoding.find(WEB_ENCODINGS_[label.to_str.strip.downcase]) rescue nil
+ end
+end # module Gem::URI
+
+module Gem
+
+ #
+ # Returns a \Gem::URI object derived from the given +uri+,
+ # which may be a \Gem::URI string or an existing \Gem::URI object:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ # # Returns a new Gem::URI.
+ # uri = Gem::URI('http://github.com/ruby/ruby')
+ # # => #<Gem::URI::HTTP http://github.com/ruby/ruby>
+ # # Returns the given Gem::URI.
+ # Gem::URI(uri)
+ # # => #<Gem::URI::HTTP http://github.com/ruby/ruby>
+ #
+ # You must require 'rubygems/vendor/uri/lib/uri' to use this method.
+ #
+ def URI(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ Gem::URI.parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+ module_function :URI
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/file.rb b/lib/rubygems/vendor/uri/lib/uri/file.rb
new file mode 100644
index 0000000000..391c499716
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/file.rb
@@ -0,0 +1,100 @@
+# frozen_string_literal: true
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The "file" Gem::URI is defined by RFC8089.
+ #
+ class File < Generic
+ # A Default port of nil for Gem::URI::File.
+ DEFAULT_PORT = nil
+
+ #
+ # An Array of the available components for Gem::URI::File.
+ #
+ COMPONENT = [
+ :scheme,
+ :host,
+ :path
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::File object from components, with syntax checking.
+ #
+ # The components accepted are +host+ and +path+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, path]</code>.
+ #
+ # A path from e.g. the File class should be escaped before
+ # being passed.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri1 = Gem::URI::File.build(['host.example.com', '/path/file.zip'])
+ # uri1.to_s # => "file://host.example.com/path/file.zip"
+ #
+ # uri2 = Gem::URI::File.build({:host => 'host.example.com',
+ # :path => '/ruby/src'})
+ # uri2.to_s # => "file://host.example.com/ruby/src"
+ #
+ # uri3 = Gem::URI::File.build({:path => Gem::URI::RFC2396_PARSER.escape('/path/my file.txt')})
+ # uri3.to_s # => "file:///path/my%20file.txt"
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Gem::URI::Generic.host=.
+ #
+ def set_host(v)
+ v = "" if v.nil? || v == "localhost"
+ @host = v
+ end
+
+ # do nothing
+ def set_port(v)
+ end
+
+ # raise InvalidURIError
+ def check_userinfo(user)
+ raise Gem::URI::InvalidURIError, "cannot set userinfo for file Gem::URI"
+ end
+
+ # raise InvalidURIError
+ def check_user(user)
+ raise Gem::URI::InvalidURIError, "cannot set user for file Gem::URI"
+ end
+
+ # raise InvalidURIError
+ def check_password(user)
+ raise Gem::URI::InvalidURIError, "cannot set password for file Gem::URI"
+ end
+
+ # do nothing
+ def set_userinfo(v)
+ end
+
+ # do nothing
+ def set_user(v)
+ end
+
+ # do nothing
+ def set_password(v)
+ end
+ end
+
+ register_scheme 'FILE', File
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ftp.rb b/lib/rubygems/vendor/uri/lib/uri/ftp.rb
new file mode 100644
index 0000000000..7517813029
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ftp.rb
@@ -0,0 +1,267 @@
+# frozen_string_literal: false
+# = uri/ftp.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # FTP Gem::URI syntax is defined by RFC1738 section 3.2.
+ #
+ # This class will be redesigned because of difference of implementations;
+ # the structure of its path. draft-hoffman-ftp-uri-04 is a draft but it
+ # is a good summary about the de facto spec.
+ # https://datatracker.ietf.org/doc/html/draft-hoffman-ftp-uri-04
+ #
+ class FTP < Generic
+ # A Default port of 21 for Gem::URI::FTP.
+ DEFAULT_PORT = 21
+
+ #
+ # An Array of the available components for Gem::URI::FTP.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port,
+ :path, :typecode
+ ].freeze
+
+ #
+ # Typecode is "a", "i", or "d".
+ #
+ # * "a" indicates a text file (the FTP command was ASCII)
+ # * "i" indicates a binary file (FTP command IMAGE)
+ # * "d" indicates the contents of a directory should be displayed
+ #
+ TYPECODE = ['a', 'i', 'd'].freeze
+
+ # Typecode prefix ";type=".
+ TYPECODE_PREFIX = ';type='.freeze
+
+ def self.new2(user, password, host, port, path,
+ typecode = nil, arg_check = true) # :nodoc:
+ # Do not use this method! Not tested. [Bug #7301]
+ # This methods remains just for compatibility,
+ # Keep it undocumented until the active maintainer is assigned.
+ typecode = nil if typecode.size == 0
+ if typecode && !TYPECODE.include?(typecode)
+ raise ArgumentError,
+ "bad typecode is specified: #{typecode}"
+ end
+
+ # do escape
+
+ self.new('ftp',
+ [user, password],
+ host, port, nil,
+ typecode ? path + TYPECODE_PREFIX + typecode : path,
+ nil, nil, nil, arg_check)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::FTP object from components, with syntax checking.
+ #
+ # The components accepted are +userinfo+, +host+, +port+, +path+, and
+ # +typecode+.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, typecode]</code>.
+ #
+ # If the path supplied is absolute, it will be escaped in order to
+ # make it absolute in the Gem::URI.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri1 = Gem::URI::FTP.build(['user:password', 'ftp.example.com', nil,
+ # '/path/file.zip', 'i'])
+ # uri1.to_s # => "ftp://user:password@ftp.example.com/%2Fpath/file.zip;type=i"
+ #
+ # uri2 = Gem::URI::FTP.build({:host => 'ftp.example.com',
+ # :path => 'ruby/src'})
+ # uri2.to_s # => "ftp://ftp.example.com/ruby/src"
+ #
+ def self.build(args)
+
+ # Fix the incoming path to be generic URL syntax
+ # FTP path -> URL path
+ # foo/bar /foo/bar
+ # /foo/bar /%2Ffoo/bar
+ #
+ if args.kind_of?(Array)
+ args[3] = '/' + args[3].sub(/^\//, '%2F')
+ else
+ args[:path] = '/' + args[:path].sub(/^\//, '%2F')
+ end
+
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:typecode]
+ if tmp[:typecode].size == 1
+ tmp[:typecode] = TYPECODE_PREFIX + tmp[:typecode]
+ end
+ tmp[:path] << tmp[:typecode]
+ end
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::FTP object from generic URL components with no
+ # syntax checking.
+ #
+ # Unlike build(), this method does not escape the path component as
+ # required by RFC1738; instead it is treated as per RFC2396.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = nil,
+ arg_check = false)
+ raise InvalidURIError unless path
+ path = path.sub(/^\//,'')
+ path.sub!(/^%2F/,'/')
+ super(scheme, userinfo, host, port, registry, path, opaque,
+ query, fragment, parser, arg_check)
+ @typecode = nil
+ if tmp = @path.index(TYPECODE_PREFIX)
+ typecode = @path[tmp + TYPECODE_PREFIX.size..-1]
+ @path = @path[0..tmp - 1]
+
+ if arg_check
+ self.typecode = typecode
+ else
+ self.set_typecode(typecode)
+ end
+ end
+ end
+
+ # typecode accessor.
+ #
+ # See Gem::URI::FTP::COMPONENT.
+ attr_reader :typecode
+
+ # Validates typecode +v+,
+ # returns +true+ or +false+.
+ #
+ def check_typecode(v)
+ if TYPECODE.include?(v)
+ return true
+ else
+ raise InvalidComponentError,
+ "bad typecode(expected #{TYPECODE.join(', ')}): #{v}"
+ end
+ end
+ private :check_typecode
+
+ # Private setter for the typecode +v+.
+ #
+ # See also Gem::URI::FTP.typecode=.
+ #
+ def set_typecode(v)
+ @typecode = v
+ end
+ protected :set_typecode
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the typecode +v+
+ # (with validation).
+ #
+ # See also Gem::URI::FTP.check_typecode.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("ftp://john@ftp.example.com/my_file.img")
+ # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img>
+ # uri.typecode = "i"
+ # uri
+ # #=> #<Gem::URI::FTP ftp://john@ftp.example.com/my_file.img;type=i>
+ #
+ def typecode=(typecode)
+ check_typecode(typecode)
+ set_typecode(typecode)
+ typecode
+ end
+
+ def merge(oth) # :nodoc:
+ tmp = super(oth)
+ if self != tmp
+ tmp.set_typecode(oth.typecode)
+ end
+
+ return tmp
+ end
+
+ # Returns the path from an FTP Gem::URI.
+ #
+ # RFC 1738 specifically states that the path for an FTP Gem::URI does not
+ # include the / which separates the Gem::URI path from the Gem::URI host. Example:
+ #
+ # <code>ftp://ftp.example.com/pub/ruby</code>
+ #
+ # The above Gem::URI indicates that the client should connect to
+ # ftp.example.com then cd to pub/ruby from the initial login directory.
+ #
+ # If you want to cd to an absolute directory, you must include an
+ # escaped / (%2F) in the path. Example:
+ #
+ # <code>ftp://ftp.example.com/%2Fpub/ruby</code>
+ #
+ # This method will then return "/pub/ruby".
+ #
+ def path
+ return @path.sub(/^\//,'').sub(/^%2F/,'/')
+ end
+
+ # Private setter for the path of the Gem::URI::FTP.
+ def set_path(v)
+ super("/" + v.sub(/^\//, "%2F"))
+ end
+ protected :set_path
+
+ # Returns a String representation of the Gem::URI::FTP.
+ def to_s
+ save_path = nil
+ if @typecode
+ save_path = @path
+ @path = @path + TYPECODE_PREFIX + @typecode
+ end
+ str = super
+ if @typecode
+ @path = save_path
+ end
+
+ return str
+ end
+ end
+
+ register_scheme 'FTP', FTP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/generic.rb b/lib/rubygems/vendor/uri/lib/uri/generic.rb
new file mode 100644
index 0000000000..d0bc77dfda
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/generic.rb
@@ -0,0 +1,1592 @@
+# frozen_string_literal: true
+
+# = uri/generic.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'common'
+autoload :IPSocket, 'socket'
+autoload :IPAddr, 'ipaddr'
+
+module Gem::URI
+
+ #
+ # Base class for all Gem::URI classes.
+ # Implements generic Gem::URI syntax as per RFC 2396.
+ #
+ class Generic
+ include Gem::URI
+
+ #
+ # A Default port of nil for Gem::URI::Generic.
+ #
+ DEFAULT_PORT = nil
+
+ #
+ # Returns default port.
+ #
+ def self.default_port
+ self::DEFAULT_PORT
+ end
+
+ #
+ # Returns default port.
+ #
+ def default_port
+ self.class.default_port
+ end
+
+ #
+ # An Array of the available components for Gem::URI::Generic.
+ #
+ COMPONENT = [
+ :scheme,
+ :userinfo, :host, :port, :registry,
+ :path, :opaque,
+ :query,
+ :fragment
+ ].freeze
+
+ #
+ # Components of the Gem::URI in the order.
+ #
+ def self.component
+ self::COMPONENT
+ end
+
+ USE_REGISTRY = false # :nodoc:
+
+ def self.use_registry # :nodoc:
+ self::USE_REGISTRY
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # At first, tries to create a new Gem::URI::Generic instance using
+ # Gem::URI::Generic::build. But, if exception Gem::URI::InvalidComponentError is raised,
+ # then it does Gem::URI::RFC2396_PARSER.escape all Gem::URI components and tries again.
+ #
+ def self.build2(args)
+ begin
+ return self.build(args)
+ rescue InvalidComponentError
+ if args.kind_of?(Array)
+ return self.build(args.collect{|x|
+ if x.is_a?(String)
+ Gem::URI::RFC2396_PARSER.escape(x)
+ else
+ x
+ end
+ })
+ elsif args.kind_of?(Hash)
+ tmp = {}
+ args.each do |key, value|
+ tmp[key] = if value
+ Gem::URI::RFC2396_PARSER.escape(value)
+ else
+ value
+ end
+ end
+ return self.build(tmp)
+ end
+ end
+ end
+
+ #
+ # == Synopsis
+ #
+ # See ::new.
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::Generic instance from components of Gem::URI::Generic
+ # with check. Components are: scheme, userinfo, host, port, registry, path,
+ # opaque, query, and fragment. You can provide arguments either by an Array or a Hash.
+ # See ::new for hash keys to use or for order of array items.
+ #
+ def self.build(args)
+ if args.kind_of?(Array) &&
+ args.size == ::Gem::URI::Generic::COMPONENT.size
+ tmp = args.dup
+ elsif args.kind_of?(Hash)
+ tmp = ::Gem::URI::Generic::COMPONENT.collect do |c|
+ if args.include?(c)
+ args[c]
+ else
+ nil
+ end
+ end
+ else
+ component = self.component rescue ::Gem::URI::Generic::COMPONENT
+ raise ArgumentError,
+ "expected Array of or Hash of components of #{self} (#{component.join(', ')})"
+ end
+
+ tmp << nil
+ tmp << true
+ return self.new(*tmp)
+ end
+
+ #
+ # == Args
+ #
+ # +scheme+::
+ # Protocol scheme, i.e. 'http','ftp','mailto' and so on.
+ # +userinfo+::
+ # User name and password, i.e. 'sdmitry:bla'.
+ # +host+::
+ # Server host name.
+ # +port+::
+ # Server port.
+ # +registry+::
+ # Registry of naming authorities.
+ # +path+::
+ # Path on server.
+ # +opaque+::
+ # Opaque part.
+ # +query+::
+ # Query data.
+ # +fragment+::
+ # Part of the Gem::URI after '#' character.
+ # +parser+::
+ # Parser for internal use [Gem::URI::DEFAULT_PARSER by default].
+ # +arg_check+::
+ # Check arguments [false by default].
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::Generic instance from ``generic'' components without check.
+ #
+ def initialize(scheme,
+ userinfo, host, port, registry,
+ path, opaque,
+ query,
+ fragment,
+ parser = DEFAULT_PARSER,
+ arg_check = false)
+ @scheme = nil
+ @user = nil
+ @password = nil
+ @host = nil
+ @port = nil
+ @path = nil
+ @query = nil
+ @opaque = nil
+ @fragment = nil
+ @parser = parser == DEFAULT_PARSER ? nil : parser
+
+ if arg_check
+ self.scheme = scheme
+ self.hostname = host
+ self.port = port
+ self.userinfo = userinfo
+ self.path = path
+ self.query = query
+ self.opaque = opaque
+ self.fragment = fragment
+ else
+ self.set_scheme(scheme)
+ self.set_host(host)
+ self.set_port(port)
+ self.set_userinfo(userinfo)
+ self.set_path(path)
+ self.query = query
+ self.set_opaque(opaque)
+ self.fragment=(fragment)
+ end
+ if registry
+ raise InvalidURIError,
+ "the scheme #{@scheme} does not accept registry part: #{registry} (or bad hostname?)"
+ end
+
+ @scheme&.freeze
+ self.set_path('') if !@path && !@opaque # (see RFC2396 Section 5.2)
+ self.set_port(self.default_port) if self.default_port && !@port
+ end
+
+ #
+ # Returns the scheme component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").scheme #=> "http"
+ #
+ attr_reader :scheme
+
+ # Returns the host component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").host #=> "foo"
+ #
+ # It returns nil if no host component exists.
+ #
+ # Gem::URI("mailto:foo@example.org").host #=> nil
+ #
+ # The component does not contain the port number.
+ #
+ # Gem::URI("http://foo:8080/bar/baz").host #=> "foo"
+ #
+ # Since IPv6 addresses are wrapped with brackets in URIs,
+ # this method returns IPv6 addresses wrapped with brackets.
+ # This form is not appropriate to pass to socket methods such as TCPSocket.open.
+ # If unwrapped host names are required, use the #hostname method.
+ #
+ # Gem::URI("http://[::1]/bar/baz").host #=> "[::1]"
+ # Gem::URI("http://[::1]/bar/baz").hostname #=> "::1"
+ #
+ attr_reader :host
+
+ # Returns the port component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").port #=> 80
+ # Gem::URI("http://foo:8080/bar/baz").port #=> 8080
+ #
+ attr_reader :port
+
+ def registry # :nodoc:
+ nil
+ end
+
+ # Returns the path component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz").path #=> "/bar/baz"
+ #
+ attr_reader :path
+
+ # Returns the query component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz?search=FooBar").query #=> "search=FooBar"
+ #
+ attr_reader :query
+
+ # Returns the opaque part of the Gem::URI.
+ #
+ # Gem::URI("mailto:foo@example.org").opaque #=> "foo@example.org"
+ # Gem::URI("http://foo/bar/baz").opaque #=> nil
+ #
+ # The portion of the path that does not make use of the slash '/'.
+ # The path typically refers to an absolute path or an opaque part.
+ # (See RFC2396 Section 3 and 5.2.)
+ #
+ attr_reader :opaque
+
+ # Returns the fragment component of the Gem::URI.
+ #
+ # Gem::URI("http://foo/bar/baz?search=FooBar#ponies").fragment #=> "ponies"
+ #
+ attr_reader :fragment
+
+ # Returns the parser to be used.
+ #
+ # Unless the +parser+ is defined, DEFAULT_PARSER is used.
+ #
+ def parser
+ if !defined?(@parser) || !@parser
+ DEFAULT_PARSER
+ else
+ @parser || DEFAULT_PARSER
+ end
+ end
+
+ # Replaces self by other Gem::URI object.
+ #
+ def replace!(oth)
+ if self.class != oth.class
+ raise ArgumentError, "expected #{self.class} object"
+ end
+
+ component.each do |c|
+ self.__send__("#{c}=", oth.__send__(c))
+ end
+ end
+ private :replace!
+
+ #
+ # Components of the Gem::URI in the order.
+ #
+ def component
+ self.class.component
+ end
+
+ #
+ # Checks the scheme +v+ component against the +parser+ Regexp for :SCHEME.
+ #
+ def check_scheme(v)
+ if v && parser.regexp[:SCHEME] !~ v
+ raise InvalidComponentError,
+ "bad component(expected scheme component): #{v}"
+ end
+
+ return true
+ end
+ private :check_scheme
+
+ # Protected setter for the scheme component +v+.
+ #
+ # See also Gem::URI::Generic.scheme=.
+ #
+ def set_scheme(v)
+ @scheme = v&.downcase
+ end
+ protected :set_scheme
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the scheme component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_scheme.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.scheme = "https"
+ # uri.to_s #=> "https://my.example.com"
+ #
+ def scheme=(v)
+ check_scheme(v)
+ set_scheme(v)
+ v
+ end
+
+ #
+ # Checks the +user+ and +password+.
+ #
+ # If +password+ is not provided, then +user+ is
+ # split, using Gem::URI::Generic.split_userinfo, to
+ # pull +user+ and +password.
+ #
+ # See also Gem::URI::Generic.check_user, Gem::URI::Generic.check_password.
+ #
+ def check_userinfo(user, password = nil)
+ if !password
+ user, password = split_userinfo(user)
+ end
+ check_user(user)
+ check_password(password, user)
+
+ return true
+ end
+ private :check_userinfo
+
+ #
+ # Checks the user +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_user(v)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set user with opaque"
+ end
+
+ return v unless v
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad component(expected userinfo component or user component): #{v}"
+ end
+
+ return true
+ end
+ private :check_user
+
+ #
+ # Checks the password +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :USERINFO.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a user component defined.
+ #
+ def check_password(v, user = @user)
+ if @opaque
+ raise InvalidURIError,
+ "cannot set password with opaque"
+ end
+ return v unless v
+
+ if !user
+ raise InvalidURIError,
+ "password component depends user component"
+ end
+
+ if parser.regexp[:USERINFO] !~ v
+ raise InvalidComponentError,
+ "bad password component"
+ end
+
+ return true
+ end
+ private :check_password
+
+ #
+ # Sets userinfo, argument is string like 'name:pass'.
+ #
+ def userinfo=(userinfo)
+ if userinfo.nil?
+ return nil
+ end
+ check_userinfo(*userinfo)
+ set_userinfo(*userinfo)
+ # returns userinfo
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +user+ component
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_user.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.user = "sam"
+ # uri.to_s #=> "http://sam:V3ry_S3nsit1ve@my.example.com"
+ #
+ def user=(user)
+ check_user(user)
+ set_user(user)
+ # returns user
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the +password+ component
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_password.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://john:S3nsit1ve@my.example.com")
+ # uri.password = "V3ry_S3nsit1ve"
+ # uri.to_s #=> "http://john:V3ry_S3nsit1ve@my.example.com"
+ #
+ def password=(password)
+ check_password(password)
+ set_password(password)
+ # returns password
+ end
+
+ # Protected setter for the +user+ component, and +password+ if available
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.userinfo=.
+ #
+ def set_userinfo(user, password = nil)
+ unless password
+ user, password = split_userinfo(user)
+ end
+ @user = user
+ @password = password
+
+ [@user, @password]
+ end
+ protected :set_userinfo
+
+ # Protected setter for the user component +v+.
+ #
+ # See also Gem::URI::Generic.user=.
+ #
+ def set_user(v)
+ set_userinfo(v, nil)
+ v
+ end
+ protected :set_user
+
+ # Protected setter for the password component +v+.
+ #
+ # See also Gem::URI::Generic.password=.
+ #
+ def set_password(v)
+ @password = v
+ # returns v
+ end
+ protected :set_password
+
+ # Returns the userinfo +ui+ as <code>[user, password]</code>
+ # if properly formatted as 'user:password'.
+ def split_userinfo(ui)
+ return nil, nil unless ui
+ user, password = ui.split(':', 2)
+
+ return user, password
+ end
+ private :split_userinfo
+
+ # Escapes 'user:password' +v+ based on RFC 1738 section 3.1.
+ def escape_userpass(v)
+ parser.escape(v, /[@:\/]/o) # RFC 1738 section 3.1 #/
+ end
+ private :escape_userpass
+
+ # Returns the userinfo, either as 'user' or 'user:password'.
+ def userinfo
+ if @user.nil?
+ nil
+ elsif @password.nil?
+ @user
+ else
+ @user + ':' + @password
+ end
+ end
+
+ # Returns the user component (without Gem::URI decoding).
+ def user
+ @user
+ end
+
+ # Returns the password component (without Gem::URI decoding).
+ def password
+ @password
+ end
+
+ # Returns the authority info (array of user, password, host and
+ # port), if any is set. Or returns +nil+.
+ def authority
+ return @user, @password, @host, @port if @user || @password || @host || @port
+ end
+
+ # Returns the user component after Gem::URI decoding.
+ def decoded_user
+ Gem::URI.decode_uri_component(@user) if @user
+ end
+
+ # Returns the password component after Gem::URI decoding.
+ def decoded_password
+ Gem::URI.decode_uri_component(@password) if @password
+ end
+
+ #
+ # Checks the host +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :HOST.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a host component defined.
+ #
+ def check_host(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set host with registry or opaque"
+ elsif parser.regexp[:HOST] !~ v
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ return true
+ end
+ private :check_host
+
+ # Protected setter for the host component +v+.
+ #
+ # See also Gem::URI::Generic.host=.
+ #
+ def set_host(v)
+ @host = v
+ end
+ protected :set_host
+
+ # Protected setter for the authority info (+user+, +password+, +host+
+ # and +port+). If +port+ is +nil+, +default_port+ will be set.
+ #
+ protected def set_authority(user, password, host, port = nil)
+ @user, @password, @host, @port = user, password, host, port || self.default_port
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the host component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_host.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.host = "foo.com"
+ # uri.to_s #=> "http://foo.com"
+ #
+ def host=(v)
+ check_host(v)
+ set_host(v)
+ set_userinfo(nil)
+ v
+ end
+
+ # Extract the host part of the Gem::URI and unwrap brackets for IPv6 addresses.
+ #
+ # This method is the same as Gem::URI::Generic#host except
+ # brackets for IPv6 (and future IP) addresses are removed.
+ #
+ # uri = Gem::URI("http://[::1]/bar")
+ # uri.hostname #=> "::1"
+ # uri.host #=> "[::1]"
+ #
+ def hostname
+ v = self.host
+ v&.start_with?('[') && v.end_with?(']') ? v[1..-2] : v
+ end
+
+ # Sets the host part of the Gem::URI as the argument with brackets for IPv6 addresses.
+ #
+ # This method is the same as Gem::URI::Generic#host= except
+ # the argument can be a bare IPv6 address.
+ #
+ # uri = Gem::URI("http://foo/bar")
+ # uri.hostname = "::1"
+ # uri.to_s #=> "http://[::1]/bar"
+ #
+ # If the argument seems to be an IPv6 address,
+ # it is wrapped with brackets.
+ #
+ def hostname=(v)
+ v = "[#{v}]" if !(v&.start_with?('[') && v&.end_with?(']')) && v&.index(':')
+ self.host = v
+ end
+
+ #
+ # Checks the port +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp for :PORT.
+ #
+ # Can not have a registry or opaque component defined,
+ # with a port component defined.
+ #
+ def check_port(v)
+ return v unless v
+
+ if @opaque
+ raise InvalidURIError,
+ "cannot set port with registry or opaque"
+ elsif !v.kind_of?(Integer) && parser.regexp[:PORT] !~ v
+ raise InvalidComponentError,
+ "bad component(expected port component): #{v.inspect}"
+ end
+
+ return true
+ end
+ private :check_port
+
+ # Protected setter for the port component +v+.
+ #
+ # See also Gem::URI::Generic.port=.
+ #
+ def set_port(v)
+ v = v.empty? ? nil : v.to_i unless !v || v.kind_of?(Integer)
+ @port = v
+ end
+ protected :set_port
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the port component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_port.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.port = 8080
+ # uri.to_s #=> "http://my.example.com:8080"
+ #
+ def port=(v)
+ check_port(v)
+ set_port(v)
+ set_userinfo(nil)
+ port
+ end
+
+ def check_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ private :check_registry
+
+ def set_registry(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+ protected :set_registry
+
+ def registry=(v) # :nodoc:
+ raise InvalidURIError, "cannot set registry"
+ end
+
+ #
+ # Checks the path +v+ component for RFC2396 compliance
+ # and against the +parser+ Regexp
+ # for :ABS_PATH and :REL_PATH.
+ #
+ # Can not have a opaque component defined,
+ # with a path component defined.
+ #
+ def check_path(v)
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if v && @opaque
+ raise InvalidURIError,
+ "path conflicts with opaque"
+ end
+
+ # If scheme is ftp, path may be relative.
+ # See RFC 1738 section 3.2.2, and RFC 2396.
+ if @scheme && @scheme != "ftp"
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected absolute path component): #{v}"
+ end
+ else
+ if v && v != '' && parser.regexp[:ABS_PATH] !~ v &&
+ parser.regexp[:REL_PATH] !~ v
+ raise InvalidComponentError,
+ "bad component(expected relative path component): #{v}"
+ end
+ end
+
+ return true
+ end
+ private :check_path
+
+ # Protected setter for the path component +v+.
+ #
+ # See also Gem::URI::Generic.path=.
+ #
+ def set_path(v)
+ @path = v
+ end
+ protected :set_path
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the path component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_path.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/pub/files")
+ # uri.path = "/faq/"
+ # uri.to_s #=> "http://my.example.com/faq/"
+ #
+ def path=(v)
+ check_path(v)
+ set_path(v)
+ v
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the query component +v+.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/?id=25")
+ # uri.query = "id=1"
+ # uri.to_s #=> "http://my.example.com/?id=1"
+ #
+ def query=(v)
+ return @query = nil unless v
+ raise InvalidURIError, "query conflicts with opaque" if @opaque
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ raise InvalidURIError, "invalid percent escape: #{$1}" if /(%\H\H)/n.match(v)
+ v.gsub!(/(?!%\h\h|[!$-&(-;=?-_a-~])./n.freeze){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @query = v
+ end
+
+ #
+ # Checks the opaque +v+ component for RFC2396 compliance and
+ # against the +parser+ Regexp for :OPAQUE.
+ #
+ # Can not have a host, port, user, or path component defined,
+ # with an opaque component defined.
+ #
+ def check_opaque(v)
+ return v unless v
+
+ # raise if both hier and opaque are not nil, because:
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ if @host || @port || @user || @path # userinfo = @user + ':' + @password
+ raise InvalidURIError,
+ "cannot set opaque with host, port, userinfo or path"
+ elsif v && parser.regexp[:OPAQUE] !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ return true
+ end
+ private :check_opaque
+
+ # Protected setter for the opaque component +v+.
+ #
+ # See also Gem::URI::Generic.opaque=.
+ #
+ def set_opaque(v)
+ @opaque = v
+ end
+ protected :set_opaque
+
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the opaque component +v+
+ # (with validation).
+ #
+ # See also Gem::URI::Generic.check_opaque.
+ #
+ def opaque=(v)
+ check_opaque(v)
+ set_opaque(v)
+ v
+ end
+
+ #
+ # Checks the fragment +v+ component against the +parser+ Regexp for :FRAGMENT.
+ #
+ #
+ # == Args
+ #
+ # +v+::
+ # String
+ #
+ # == Description
+ #
+ # Public setter for the fragment component +v+
+ # (with validation).
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/?id=25#time=1305212049")
+ # uri.fragment = "time=1305212086"
+ # uri.to_s #=> "http://my.example.com/?id=25#time=1305212086"
+ #
+ def fragment=(v)
+ return @fragment = nil unless v
+
+ x = v.to_str
+ v = x.dup if x.equal? v
+ v.encode!(Encoding::UTF_8) rescue nil
+ v.delete!("\t\r\n")
+ v.force_encoding(Encoding::ASCII_8BIT)
+ v.gsub!(/(?!%\h\h|[!-~])./n){'%%%02X' % $&.ord}
+ v.force_encoding(Encoding::US_ASCII)
+ @fragment = v
+ end
+
+ #
+ # Returns true if Gem::URI is hierarchical.
+ #
+ # == Description
+ #
+ # Gem::URI has components listed in order of decreasing significance from left to right,
+ # see RFC3986 https://www.rfc-editor.org/rfc/rfc3986 1.2.3.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com/")
+ # uri.hierarchical?
+ # #=> true
+ # uri = Gem::URI.parse("mailto:joe@example.com")
+ # uri.hierarchical?
+ # #=> false
+ #
+ def hierarchical?
+ if @path
+ true
+ else
+ false
+ end
+ end
+
+ #
+ # Returns true if Gem::URI has a scheme (e.g. http:// or https://) specified.
+ #
+ def absolute?
+ if @scheme
+ true
+ else
+ false
+ end
+ end
+ alias absolute absolute?
+
+ #
+ # Returns true if Gem::URI does not have a scheme (e.g. http:// or https://) specified.
+ #
+ def relative?
+ !absolute?
+ end
+
+ #
+ # Returns an Array of the path split on '/'.
+ #
+ def split_path(path)
+ path.split("/", -1)
+ end
+ private :split_path
+
+ #
+ # Merges a base path +base+, with relative path +rel+,
+ # returns a modified base path.
+ #
+ def merge_path(base, rel)
+
+ # RFC2396, Section 5.2, 5)
+ # RFC2396, Section 5.2, 6)
+ base_path = split_path(base)
+ rel_path = split_path(rel)
+
+ # RFC2396, Section 5.2, 6), a)
+ base_path << '' if base_path.last == '..'
+ while i = base_path.index('..')
+ base_path.slice!(i - 1, 2)
+ end
+
+ if (first = rel_path.first) and first.empty?
+ base_path.clear
+ rel_path.shift
+ end
+
+ # RFC2396, Section 5.2, 6), c)
+ # RFC2396, Section 5.2, 6), d)
+ rel_path.push('') if rel_path.last == '.' || rel_path.last == '..'
+ rel_path.delete('.')
+
+ # RFC2396, Section 5.2, 6), e)
+ tmp = []
+ rel_path.each do |x|
+ if x == '..' &&
+ !(tmp.empty? || tmp.last == '..')
+ tmp.pop
+ else
+ tmp << x
+ end
+ end
+
+ add_trailer_slash = !tmp.empty?
+ if base_path.empty?
+ base_path = [''] # keep '/' for root directory
+ elsif add_trailer_slash
+ base_path.pop
+ end
+ while x = tmp.shift
+ if x == '..'
+ # RFC2396, Section 4
+ # a .. or . in an absolute path has no special meaning
+ base_path.pop if base_path.size > 1
+ else
+ # if x == '..'
+ # valid absolute (but abnormal) path "/../..."
+ # else
+ # valid absolute path
+ # end
+ base_path << x
+ tmp.each {|t| base_path << t}
+ add_trailer_slash = false
+ break
+ end
+ end
+ base_path.push('') if add_trailer_slash
+
+ return base_path.join('/')
+ end
+ private :merge_path
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Destructive form of #merge.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.merge!("/main.rbx?page=1")
+ # uri.to_s # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge!(oth)
+ t = merge(oth)
+ if self == t
+ nil
+ else
+ replace!(t)
+ self
+ end
+ end
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Merges two URIs.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.merge("/main.rbx?page=1")
+ # # => "http://my.example.com/main.rbx?page=1"
+ #
+ def merge(oth)
+ rel = parser.__send__(:convert_to_uri, oth)
+
+ if rel.absolute?
+ #raise BadURIError, "both Gem::URI are absolute" if absolute?
+ # hmm... should return oth for usability?
+ return rel
+ end
+
+ unless self.absolute?
+ raise BadURIError, "both Gem::URI are relative"
+ end
+
+ base = self.dup
+
+ authority = rel.authority
+
+ # RFC2396, Section 5.2, 2)
+ if (rel.path.nil? || rel.path.empty?) && !authority && !rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+ return base
+ end
+
+ base.query = nil
+ base.fragment=(nil)
+
+ # RFC2396, Section 5.2, 4)
+ if authority
+ base.set_authority(*authority)
+ base.set_path(rel.path)
+ elsif base.path && rel.path
+ base.set_path(merge_path(base.path, rel.path))
+ end
+
+ # RFC2396, Section 5.2, 7)
+ base.query = rel.query if rel.query
+ base.fragment=(rel.fragment) if rel.fragment
+
+ return base
+ end # merge
+ alias + merge
+
+ # :stopdoc:
+ def route_from_path(src, dst)
+ case dst
+ when src
+ # RFC2396, Section 4.2
+ return ''
+ when %r{(?:\A|/)\.\.?(?:/|\z)}
+ # dst has abnormal absolute path,
+ # like "/./", "/../", "/x/../", ...
+ return dst.dup
+ end
+
+ src_path = src.scan(%r{[^/]*/})
+ dst_path = dst.scan(%r{[^/]*/?})
+
+ # discard same parts
+ while !dst_path.empty? && dst_path.first == src_path.first
+ src_path.shift
+ dst_path.shift
+ end
+
+ tmp = dst_path.join
+
+ # calculate
+ if src_path.empty?
+ if tmp.empty?
+ return './'
+ elsif dst_path.first.include?(':') # (see RFC2396 Section 5)
+ return './' + tmp
+ else
+ return tmp
+ end
+ end
+
+ return '../' * src_path.size + tmp
+ end
+ private :route_from_path
+ # :startdoc:
+
+ # :stopdoc:
+ def route_from0(oth)
+ oth = parser.__send__(:convert_to_uri, oth)
+ if self.relative?
+ raise BadURIError,
+ "relative Gem::URI: #{self}"
+ end
+ if oth.relative?
+ raise BadURIError,
+ "relative Gem::URI: #{oth}"
+ end
+
+ if self.scheme != oth.scheme
+ return self, self.dup
+ end
+ rel = Gem::URI::Generic.new(nil, # it is relative Gem::URI
+ self.userinfo, self.host, self.port,
+ nil, self.path, self.opaque,
+ self.query, self.fragment, parser)
+
+ if rel.userinfo != oth.userinfo ||
+ rel.host.to_s.downcase != oth.host.to_s.downcase ||
+ rel.port != oth.port
+
+ if self.userinfo.nil? && self.host.nil?
+ return self, self.dup
+ end
+
+ rel.set_port(nil) if rel.port == oth.default_port
+ return rel, rel
+ end
+ rel.set_userinfo(nil)
+ rel.set_host(nil)
+ rel.set_port(nil)
+
+ if rel.path && rel.path == oth.path
+ rel.set_path('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ elsif rel.opaque && rel.opaque == oth.opaque
+ rel.set_opaque('')
+ rel.query = nil if rel.query == oth.query
+ return rel, rel
+ end
+
+ # you can modify `rel', but cannot `oth'.
+ return oth, rel
+ end
+ private :route_from0
+ # :startdoc:
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path from oth to self.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://my.example.com/main.rbx?page=1')
+ # uri.route_from('http://my.example.com')
+ # #=> #<Gem::URI::Generic /main.rbx?page=1>
+ #
+ def route_from(oth)
+ # you can modify `rel', but cannot `oth'.
+ begin
+ oth, rel = route_from0(oth)
+ rescue
+ raise $!.class, $!.message
+ end
+ if oth == rel
+ return rel
+ end
+
+ rel.set_path(route_from_path(oth.path, self.path))
+ if rel.path == './' && self.query
+ # "./?foo" -> "?foo"
+ rel.set_path('')
+ end
+
+ return rel
+ end
+
+ alias - route_from
+
+ #
+ # == Args
+ #
+ # +oth+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Calculates relative path to oth from self.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://my.example.com')
+ # uri.route_to('http://my.example.com/main.rbx?page=1')
+ # #=> #<Gem::URI::Generic /main.rbx?page=1>
+ #
+ def route_to(oth)
+ parser.__send__(:convert_to_uri, oth).route_from(self)
+ end
+
+ #
+ # Returns normalized Gem::URI.
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # Gem::URI("HTTP://my.EXAMPLE.com").normalize
+ # #=> #<Gem::URI::HTTP http://my.example.com/>
+ #
+ # Normalization here means:
+ #
+ # * scheme and host are converted to lowercase,
+ # * an empty path component is set to "/".
+ #
+ def normalize
+ uri = dup
+ uri.normalize!
+ uri
+ end
+
+ #
+ # Destructive version of #normalize.
+ #
+ def normalize!
+ if path&.empty?
+ set_path('/')
+ end
+ if scheme && scheme != scheme.downcase
+ set_scheme(self.scheme.downcase)
+ end
+ if host && host != host.downcase
+ set_host(self.host.downcase)
+ end
+ end
+
+ #
+ # Constructs String from Gem::URI.
+ #
+ def to_s
+ str = ''.dup
+ if @scheme
+ str << @scheme
+ str << ':'
+ end
+
+ if @opaque
+ str << @opaque
+ else
+ if @host || %w[file postgres].include?(@scheme)
+ str << '//'
+ end
+ if self.userinfo
+ str << self.userinfo
+ str << '@'
+ end
+ if @host
+ str << @host
+ end
+ if @port && @port != self.default_port
+ str << ':'
+ str << @port.to_s
+ end
+ if (@host || @port) && !@path.empty? && !@path.start_with?('/')
+ str << '/'
+ end
+ str << @path
+ if @query
+ str << '?'
+ str << @query
+ end
+ end
+ if @fragment
+ str << '#'
+ str << @fragment
+ end
+ str
+ end
+ alias to_str to_s
+
+ #
+ # Compares two URIs.
+ #
+ def ==(oth)
+ if self.class == oth.class
+ self.normalize.component_ary == oth.normalize.component_ary
+ else
+ false
+ end
+ end
+
+ # Returns the hash value.
+ def hash
+ self.component_ary.hash
+ end
+
+ # Compares with _oth_ for Hash.
+ def eql?(oth)
+ self.class == oth.class &&
+ parser == oth.parser &&
+ self.component_ary.eql?(oth.component_ary)
+ end
+
+ # Returns an Array of the components defined from the COMPONENT Array.
+ def component_ary
+ component.collect do |x|
+ self.__send__(x)
+ end
+ end
+ protected :component_ary
+
+ # == Args
+ #
+ # +components+::
+ # Multiple Symbol arguments defined in Gem::URI::HTTP.
+ #
+ # == Description
+ #
+ # Selects specified components from Gem::URI.
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse('http://myuser:mypass@my.example.com/test.rbx')
+ # uri.select(:userinfo, :host, :path)
+ # # => ["myuser:mypass", "my.example.com", "/test.rbx"]
+ #
+ def select(*components)
+ components.collect do |c|
+ if component.include?(c)
+ self.__send__(c)
+ else
+ raise ArgumentError,
+ "expected of components of #{self.class} (#{self.class.component.join(', ')})"
+ end
+ end
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{self}>"
+ end
+
+ #
+ # == Args
+ #
+ # +v+::
+ # Gem::URI or String
+ #
+ # == Description
+ #
+ # Attempts to parse other Gem::URI +oth+,
+ # returns [parsed_oth, self].
+ #
+ # == Usage
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("http://my.example.com")
+ # uri.coerce("http://foo.com")
+ # #=> [#<Gem::URI::HTTP http://foo.com>, #<Gem::URI::HTTP http://my.example.com>]
+ #
+ def coerce(oth)
+ case oth
+ when String
+ oth = parser.parse(oth)
+ else
+ super
+ end
+
+ return oth, self
+ end
+
+ # Returns a proxy Gem::URI.
+ # The proxy Gem::URI is obtained from environment variables such as http_proxy,
+ # ftp_proxy, no_proxy, etc.
+ # If there is no proper proxy, nil is returned.
+ #
+ # If the optional parameter +env+ is specified, it is used instead of ENV.
+ #
+ # Note that capitalized variables (HTTP_PROXY, FTP_PROXY, NO_PROXY, etc.)
+ # are examined, too.
+ #
+ # But http_proxy and HTTP_PROXY is treated specially under CGI environment.
+ # It's because HTTP_PROXY may be set by Proxy: header.
+ # So HTTP_PROXY is not used.
+ # http_proxy is not used too if the variable is case insensitive.
+ # CGI_HTTP_PROXY can be used instead.
+ def find_proxy(env=ENV)
+ raise BadURIError, "relative Gem::URI: #{self}" if self.relative?
+ name = self.scheme.downcase + '_proxy'
+ proxy_uri = nil
+ if name == 'http_proxy' && env.include?('REQUEST_METHOD') # CGI?
+ # HTTP_PROXY conflicts with *_proxy for proxy settings and
+ # HTTP_* for header information in CGI.
+ # So it should be careful to use it.
+ pairs = env.reject {|k, v| /\Ahttp_proxy\z/i !~ k }
+ case pairs.length
+ when 0 # no proxy setting anyway.
+ proxy_uri = nil
+ when 1
+ k, _ = pairs.shift
+ if k == 'http_proxy' && env[k.upcase] == nil
+ # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env[name]
+ else
+ proxy_uri = nil
+ end
+ else # http_proxy is safe to use because ENV is case sensitive.
+ proxy_uri = env.to_hash[name]
+ end
+ if !proxy_uri
+ # Use CGI_HTTP_PROXY. cf. libwww-perl.
+ proxy_uri = env["CGI_#{name.upcase}"]
+ end
+ elsif name == 'http_proxy'
+ if RUBY_ENGINE == 'jruby' && p_addr = ENV_JAVA['http.proxyHost']
+ p_port = ENV_JAVA['http.proxyPort']
+ if p_user = ENV_JAVA['http.proxyUser']
+ p_pass = ENV_JAVA['http.proxyPass']
+ proxy_uri = "http://#{p_user}:#{p_pass}@#{p_addr}:#{p_port}"
+ else
+ proxy_uri = "http://#{p_addr}:#{p_port}"
+ end
+ else
+ unless proxy_uri = env[name]
+ if proxy_uri = env[name.upcase]
+ warn 'The environment variable HTTP_PROXY is discouraged. Please use http_proxy instead.', uplevel: 1
+ end
+ end
+ end
+ else
+ proxy_uri = env[name] || env[name.upcase]
+ end
+
+ if proxy_uri.nil? || proxy_uri.empty?
+ return nil
+ end
+
+ if self.hostname
+ begin
+ addr = IPSocket.getaddress(self.hostname)
+ return nil if /\A127\.|\A::1\z/ =~ addr
+ rescue SocketError
+ end
+ end
+
+ name = 'no_proxy'
+ if no_proxy = env[name] || env[name.upcase]
+ return nil unless Gem::URI::Generic.use_proxy?(self.hostname, addr, self.port, no_proxy)
+ end
+ Gem::URI.parse(proxy_uri)
+ end
+
+ def self.use_proxy?(hostname, addr, port, no_proxy) # :nodoc:
+ hostname = hostname.downcase
+ dothostname = ".#{hostname}"
+ no_proxy.scan(/([^:,\s]+)(?::(\d+))?/) {|p_host, p_port|
+ if !p_port || port == p_port.to_i
+ if p_host.start_with?('.')
+ return false if hostname.end_with?(p_host.downcase)
+ else
+ return false if dothostname.end_with?(".#{p_host.downcase}")
+ end
+ if addr
+ begin
+ return false if IPAddr.new(p_host).include?(addr)
+ rescue IPAddr::InvalidAddressError
+ next
+ end
+ end
+ end
+ }
+ true
+ end
+ end
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/http.rb b/lib/rubygems/vendor/uri/lib/uri/http.rb
new file mode 100644
index 0000000000..99c78358ac
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/http.rb
@@ -0,0 +1,137 @@
+# frozen_string_literal: false
+# = uri/http.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The syntax of HTTP URIs is defined in RFC1738 section 3.3.
+ #
+ # Note that the Ruby Gem::URI library allows HTTP URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class HTTP < Generic
+ # A Default port of 80 for Gem::URI::HTTP.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Gem::URI::HTTP.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ fragment
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::HTTP object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, query, and
+ # fragment.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query, fragment]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Gem::URI::HTTP.build([nil, "www.example.com", nil, "/path",
+ # "query", 'fragment'])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid HTTP URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ # Do not allow empty host names, as they are not allowed by RFC 3986.
+ def check_host(v)
+ ret = super
+
+ if ret && v.empty?
+ raise InvalidComponentError,
+ "bad component(expected host component): #{v}"
+ end
+
+ ret
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for an HTTP request, as required by Net::HTTP::Get.
+ #
+ # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query.
+ # Otherwise, the path is simply Gem::URI#path.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::HTTP.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+
+ #
+ # == Description
+ #
+ # Returns the authority for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc3986#section-3.2.
+ #
+ #
+ # Example:
+ #
+ # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').authority #=> "www.example.com"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').authority #=> "www.example.com:8000"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').authority #=> "www.example.com"
+ #
+ def authority
+ if port == default_port
+ host
+ else
+ "#{host}:#{port}"
+ end
+ end
+
+ #
+ # == Description
+ #
+ # Returns the origin for an HTTP uri, as defined in
+ # https://www.rfc-editor.org/rfc/rfc6454.
+ #
+ #
+ # Example:
+ #
+ # Gem::URI::HTTP.build(host: 'www.example.com', path: '/foo/bar').origin #=> "http://www.example.com"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 8000, path: '/foo/bar').origin #=> "http://www.example.com:8000"
+ # Gem::URI::HTTP.build(host: 'www.example.com', port: 80, path: '/foo/bar').origin #=> "http://www.example.com"
+ # Gem::URI::HTTPS.build(host: 'www.example.com', path: '/foo/bar').origin #=> "https://www.example.com"
+ #
+ def origin
+ "#{scheme}://#{authority}"
+ end
+ end
+
+ register_scheme 'HTTP', HTTP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/https.rb b/lib/rubygems/vendor/uri/lib/uri/https.rb
new file mode 100644
index 0000000000..6e8e732e1d
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/https.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/https.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'http'
+
+module Gem::URI
+
+ # The default port for HTTPS URIs is 443, and the scheme is 'https:' rather
+ # than 'http:'. Other than that, HTTPS URIs are identical to HTTP URIs;
+ # see Gem::URI::HTTP.
+ class HTTPS < HTTP
+ # A Default port of 443 for Gem::URI::HTTPS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'HTTPS', HTTPS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ldap.rb b/lib/rubygems/vendor/uri/lib/uri/ldap.rb
new file mode 100644
index 0000000000..1a08b5ab7e
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ldap.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# Author::
+# Takaaki Tateishi <ttate@jaist.ac.jp>
+# Akira Yamada <akira@ruby-lang.org>
+# License::
+# Gem::URI::LDAP is copyrighted free software by Takaaki Tateishi and Akira Yamada.
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # LDAP Gem::URI SCHEMA (described in RFC2255).
+ #--
+ # ldap://<host>/<dn>[?<attrs>[?<scope>[?<filter>[?<extensions>]]]]
+ #++
+ class LDAP < Generic
+
+ # A Default port of 389 for Gem::URI::LDAP.
+ DEFAULT_PORT = 389
+
+ # An Array of the available components for Gem::URI::LDAP.
+ COMPONENT = [
+ :scheme,
+ :host, :port,
+ :dn,
+ :attributes,
+ :scope,
+ :filter,
+ :extensions,
+ ].freeze
+
+ # Scopes available for the starting point.
+ #
+ # * SCOPE_BASE - the Base DN
+ # * SCOPE_ONE - one level under the Base DN, not including the base DN and
+ # not including any entries under this
+ # * SCOPE_SUB - subtrees, all entries at all levels
+ #
+ SCOPE = [
+ SCOPE_ONE = 'one',
+ SCOPE_SUB = 'sub',
+ SCOPE_BASE = 'base',
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::LDAP object from components, with syntax checking.
+ #
+ # The components accepted are host, port, dn, attributes,
+ # scope, filter, and extensions.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[host, port, dn, attributes, scope, filter, extensions]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::LDAP.build({:host => 'ldap.example.com',
+ # :dn => '/dc=example'})
+ #
+ # uri = Gem::URI::LDAP.build(["ldap.example.com", nil,
+ # "/dc=example;dc=com", "query", nil, nil, nil])
+ #
+ def self.build(args)
+ tmp = Util::make_components_hash(self, args)
+
+ if tmp[:dn]
+ tmp[:path] = tmp[:dn]
+ end
+
+ query = []
+ [:extensions, :filter, :scope, :attributes].collect do |x|
+ next if !tmp[x] && query.size == 0
+ query.unshift(tmp[x])
+ end
+
+ tmp[:query] = query.join('?')
+
+ return super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::LDAP object from generic Gem::URI components as per
+ # RFC 2396. No LDAP-specific syntax checking is performed.
+ #
+ # Arguments are +scheme+, +userinfo+, +host+, +port+, +registry+, +path+,
+ # +opaque+, +query+, and +fragment+, in that order.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::LDAP.new("ldap", nil, "ldap.example.com", nil, nil,
+ # "/dc=example;dc=com", nil, "query", nil)
+ #
+ # See also Gem::URI::Generic.new.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ if @fragment
+ raise InvalidURIError, 'bad LDAP URL'
+ end
+
+ parse_dn
+ parse_query
+ end
+
+ # Private method to cleanup +dn+ from using the +path+ component attribute.
+ def parse_dn
+ raise InvalidURIError, 'bad LDAP URL' unless @path
+ @dn = @path[1..-1]
+ end
+ private :parse_dn
+
+ # Private method to cleanup +attributes+, +scope+, +filter+, and +extensions+
+ # from using the +query+ component attribute.
+ def parse_query
+ @attributes = nil
+ @scope = nil
+ @filter = nil
+ @extensions = nil
+
+ if @query
+ attrs, scope, filter, extensions = @query.split('?')
+
+ @attributes = attrs if attrs && attrs.size > 0
+ @scope = scope if scope && scope.size > 0
+ @filter = filter if filter && filter.size > 0
+ @extensions = extensions if extensions && extensions.size > 0
+ end
+ end
+ private :parse_query
+
+ # Private method to assemble +query+ from +attributes+, +scope+, +filter+, and +extensions+.
+ def build_path_query
+ @path = '/' + @dn
+
+ query = []
+ [@extensions, @filter, @scope, @attributes].each do |x|
+ next if !x && query.size == 0
+ query.unshift(x)
+ end
+ @query = query.join('?')
+ end
+ private :build_path_query
+
+ # Returns dn.
+ def dn
+ @dn
+ end
+
+ # Private setter for dn +val+.
+ def set_dn(val)
+ @dn = val
+ build_path_query
+ @dn
+ end
+ protected :set_dn
+
+ # Setter for dn +val+.
+ def dn=(val)
+ set_dn(val)
+ val
+ end
+
+ # Returns attributes.
+ def attributes
+ @attributes
+ end
+
+ # Private setter for attributes +val+.
+ def set_attributes(val)
+ @attributes = val
+ build_path_query
+ @attributes
+ end
+ protected :set_attributes
+
+ # Setter for attributes +val+.
+ def attributes=(val)
+ set_attributes(val)
+ val
+ end
+
+ # Returns scope.
+ def scope
+ @scope
+ end
+
+ # Private setter for scope +val+.
+ def set_scope(val)
+ @scope = val
+ build_path_query
+ @scope
+ end
+ protected :set_scope
+
+ # Setter for scope +val+.
+ def scope=(val)
+ set_scope(val)
+ val
+ end
+
+ # Returns filter.
+ def filter
+ @filter
+ end
+
+ # Private setter for filter +val+.
+ def set_filter(val)
+ @filter = val
+ build_path_query
+ @filter
+ end
+ protected :set_filter
+
+ # Setter for filter +val+.
+ def filter=(val)
+ set_filter(val)
+ val
+ end
+
+ # Returns extensions.
+ def extensions
+ @extensions
+ end
+
+ # Private setter for extensions +val+.
+ def set_extensions(val)
+ @extensions = val
+ build_path_query
+ @extensions
+ end
+ protected :set_extensions
+
+ # Setter for extensions +val+.
+ def extensions=(val)
+ set_extensions(val)
+ val
+ end
+
+ # Checks if Gem::URI has a path.
+ # For Gem::URI::LDAP this will return +false+.
+ def hierarchical?
+ false
+ end
+ end
+
+ register_scheme 'LDAP', LDAP
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ldaps.rb b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb
new file mode 100644
index 0000000000..b7a5b50e27
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ldaps.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: false
+# = uri/ldap.rb
+#
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'ldap'
+
+module Gem::URI
+
+ # The default port for LDAPS URIs is 636, and the scheme is 'ldaps:' rather
+ # than 'ldap:'. Other than that, LDAPS URIs are identical to LDAP URIs;
+ # see Gem::URI::LDAP.
+ class LDAPS < LDAP
+ # A Default port of 636 for Gem::URI::LDAPS
+ DEFAULT_PORT = 636
+ end
+
+ register_scheme 'LDAPS', LDAPS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/mailto.rb b/lib/rubygems/vendor/uri/lib/uri/mailto.rb
new file mode 100644
index 0000000000..7ae544d194
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/mailto.rb
@@ -0,0 +1,293 @@
+# frozen_string_literal: false
+# = uri/mailto.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # RFC6068, the mailto URL scheme.
+ #
+ class MailTo < Generic
+ include RFC2396_REGEXP
+
+ # A Default port of nil for Gem::URI::MailTo.
+ DEFAULT_PORT = nil
+
+ # An Array of the available components for Gem::URI::MailTo.
+ COMPONENT = [ :scheme, :to, :headers ].freeze
+
+ # :stopdoc:
+ # "hname" and "hvalue" are encodings of an RFC 822 header name and
+ # value, respectively. As with "to", all URL reserved characters must
+ # be encoded.
+ #
+ # "#mailbox" is as specified in RFC 822 [RFC822]. This means that it
+ # consists of zero or more comma-separated mail addresses, possibly
+ # including "phrase" and "comment" components. Note that all URL
+ # reserved characters in "to" must be encoded: in particular,
+ # parentheses, commas, and the percent sign ("%"), which commonly occur
+ # in the "mailbox" syntax.
+ #
+ # Within mailto URLs, the characters "?", "=", "&" are reserved.
+
+ # ; RFC 6068
+ # hfields = "?" hfield *( "&" hfield )
+ # hfield = hfname "=" hfvalue
+ # hfname = *qchar
+ # hfvalue = *qchar
+ # qchar = unreserved / pct-encoded / some-delims
+ # some-delims = "!" / "$" / "'" / "(" / ")" / "*"
+ # / "+" / "," / ";" / ":" / "@"
+ #
+ # ; RFC3986
+ # unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
+ # pct-encoded = "%" HEXDIG HEXDIG
+ HEADER_REGEXP = /\A(?<hfield>(?:%\h\h|[!$'-.0-;@-Z_a-z~])*=(?:%\h\h|[!$'-.0-;@-Z_a-z~])*)(?:&\g<hfield>)*\z/
+ # practical regexp for email address
+ # https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
+ EMAIL_REGEXP = /\A[a-zA-Z0-9.!\#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*\z/
+ # :startdoc:
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::MailTo object from components, with syntax checking.
+ #
+ # Components can be provided as an Array or Hash. If an Array is used,
+ # the components must be supplied as <code>[to, headers]</code>.
+ #
+ # If a Hash is used, the keys are the component names preceded by colons.
+ #
+ # The headers can be supplied as a pre-encoded string, such as
+ # <code>"subject=subscribe&cc=address"</code>, or as an Array of Arrays
+ # like <code>[['subject', 'subscribe'], ['cc', 'address']]</code>.
+ #
+ # Examples:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # m1 = Gem::URI::MailTo.build(['joe@example.com', 'subject=Ruby'])
+ # m1.to_s # => "mailto:joe@example.com?subject=Ruby"
+ #
+ # m2 = Gem::URI::MailTo.build(['john@example.com', [['Subject', 'Ruby'], ['Cc', 'jack@example.com']]])
+ # m2.to_s # => "mailto:john@example.com?Subject=Ruby&Cc=jack@example.com"
+ #
+ # m3 = Gem::URI::MailTo.build({:to => 'listman@example.com', :headers => [['subject', 'subscribe']]})
+ # m3.to_s # => "mailto:listman@example.com?subject=subscribe"
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+
+ case tmp[:to]
+ when Array
+ tmp[:opaque] = tmp[:to].join(',')
+ when String
+ tmp[:opaque] = tmp[:to].dup
+ else
+ tmp[:opaque] = ''
+ end
+
+ if tmp[:headers]
+ query =
+ case tmp[:headers]
+ when Array
+ tmp[:headers].collect { |x|
+ if x.kind_of?(Array)
+ x[0] + '=' + x[1..-1].join
+ else
+ x.to_s
+ end
+ }.join('&')
+ when Hash
+ tmp[:headers].collect { |h,v|
+ h + '=' + v
+ }.join('&')
+ else
+ tmp[:headers].to_s
+ end
+ unless query.empty?
+ tmp[:opaque] << '?' << query
+ end
+ end
+
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::MailTo object from generic URL components with
+ # no syntax checking.
+ #
+ # This method is usually called from Gem::URI::parse, which checks
+ # the validity of each component.
+ #
+ def initialize(*arg)
+ super(*arg)
+
+ @to = nil
+ @headers = []
+
+ # The RFC3986 parser does not normally populate opaque
+ @opaque = "?#{@query}" if @query && !@opaque
+
+ unless @opaque
+ raise InvalidComponentError,
+ "missing opaque part for mailto URL"
+ end
+ to, header = @opaque.split('?', 2)
+ # allow semicolon as a addr-spec separator
+ # http://support.microsoft.com/kb/820868
+ unless /\A(?:[^@,;]+@[^@,;]+(?:\z|[,;]))*\z/ =~ to
+ raise InvalidComponentError,
+ "unrecognised opaque part for mailtoURL: #{@opaque}"
+ end
+
+ if arg[10] # arg_check
+ self.to = to
+ self.headers = header
+ else
+ set_to(to)
+ set_headers(header)
+ end
+ end
+
+ # The primary e-mail address of the URL, as a String.
+ attr_reader :to
+
+ # E-mail headers set by the URL, as an Array of Arrays.
+ attr_reader :headers
+
+ # Checks the to +v+ component.
+ def check_to(v)
+ return true unless v
+ return true if v.size == 0
+
+ v.split(/[,;]/).each do |addr|
+ # check url safety as path-rootless
+ if /\A(?:%\h\h|[!$&-.0-;=@-Z_a-z~])*\z/ !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as Gem::URI #{addr.dump}"
+ end
+
+ # check addr-spec
+ # don't s/\+/ /g
+ addr.gsub!(/%\h\h/, Gem::URI::TBLDECWWWCOMP_)
+ if EMAIL_REGEXP !~ addr
+ raise InvalidComponentError,
+ "an address in 'to' is invalid as uri-escaped addr-spec #{addr.dump}"
+ end
+ end
+
+ true
+ end
+ private :check_to
+
+ # Private setter for to +v+.
+ def set_to(v)
+ @to = v
+ end
+ protected :set_to
+
+ # Setter for to +v+.
+ def to=(v)
+ check_to(v)
+ set_to(v)
+ v
+ end
+
+ # Checks the headers +v+ component against either
+ # * HEADER_REGEXP
+ def check_headers(v)
+ return true unless v
+ return true if v.size == 0
+ if HEADER_REGEXP !~ v
+ raise InvalidComponentError,
+ "bad component(expected opaque component): #{v}"
+ end
+
+ true
+ end
+ private :check_headers
+
+ # Private setter for headers +v+.
+ def set_headers(v)
+ @headers = []
+ if v
+ v.split('&').each do |x|
+ @headers << x.split(/=/, 2)
+ end
+ end
+ end
+ protected :set_headers
+
+ # Setter for headers +v+.
+ def headers=(v)
+ check_headers(v)
+ set_headers(v)
+ v
+ end
+
+ # Constructs String from Gem::URI.
+ def to_s
+ @scheme + ':' +
+ if @to
+ @to
+ else
+ ''
+ end +
+ if @headers.size > 0
+ '?' + @headers.collect{|x| x.join('=')}.join('&')
+ else
+ ''
+ end +
+ if @fragment
+ '#' + @fragment
+ else
+ ''
+ end
+ end
+
+ # Returns the RFC822 e-mail text equivalent of the URL, as a String.
+ #
+ # Example:
+ #
+ # require 'rubygems/vendor/uri/lib/uri'
+ #
+ # uri = Gem::URI.parse("mailto:ruby-list@ruby-lang.org?Subject=subscribe&cc=myaddr")
+ # uri.to_mailtext
+ # # => "To: ruby-list@ruby-lang.org\nSubject: subscribe\nCc: myaddr\n\n\n"
+ #
+ def to_mailtext
+ to = Gem::URI.decode_www_form_component(@to)
+ head = ''
+ body = ''
+ @headers.each do |x|
+ case x[0]
+ when 'body'
+ body = Gem::URI.decode_www_form_component(x[1])
+ when 'to'
+ to << ', ' + Gem::URI.decode_www_form_component(x[1])
+ else
+ head << Gem::URI.decode_www_form_component(x[0]).capitalize + ': ' +
+ Gem::URI.decode_www_form_component(x[1]) + "\n"
+ end
+ end
+
+ "To: #{to}
+#{head}
+#{body}
+"
+ end
+ alias to_rfc822text to_mailtext
+ end
+
+ register_scheme 'MAILTO', MailTo
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb
new file mode 100644
index 0000000000..2bb4181649
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/rfc2396_parser.rb
@@ -0,0 +1,547 @@
+# frozen_string_literal: false
+#--
+# = uri/common.rb
+#
+# Author:: Akira Yamada <akira@ruby-lang.org>
+# License::
+# You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+module Gem::URI
+ #
+ # Includes Gem::URI::REGEXP::PATTERN
+ #
+ module RFC2396_REGEXP
+ #
+ # Patterns used to parse Gem::URI's
+ #
+ module PATTERN
+ # :stopdoc:
+
+ # RFC 2396 (Gem::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # alpha = lowalpha | upalpha
+ ALPHA = "a-zA-Z"
+ # alphanum = alpha | digit
+ ALNUM = "#{ALPHA}\\d"
+
+ # hex = digit | "A" | "B" | "C" | "D" | "E" | "F" |
+ # "a" | "b" | "c" | "d" | "e" | "f"
+ HEX = "a-fA-F\\d"
+ # escaped = "%" hex hex
+ ESCAPED = "%[#{HEX}]{2}"
+ # mark = "-" | "_" | "." | "!" | "~" | "*" | "'" |
+ # "(" | ")"
+ # unreserved = alphanum | mark
+ UNRESERVED = "\\-_.!~*'()#{ALNUM}"
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | ","
+ # reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" |
+ # "$" | "," | "[" | "]" (RFC 2732)
+ RESERVED = ";/?:@&=+$,\\[\\]"
+
+ # domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum
+ DOMLABEL = "(?:[#{ALNUM}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # toplabel = alpha | alpha *( alphanum | "-" ) alphanum
+ TOPLABEL = "(?:[#{ALPHA}](?:[-#{ALNUM}]*[#{ALNUM}])?)"
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ HOSTNAME = "(?:#{DOMLABEL}\\.)*#{TOPLABEL}\\.?"
+
+ # :startdoc:
+ end # PATTERN
+
+ # :startdoc:
+ end # REGEXP
+
+ # Class that parses String's into Gem::URI's.
+ #
+ # It contains a Hash set of patterns and Regexp's that match and validate.
+ #
+ class RFC2396_Parser
+ include RFC2396_REGEXP
+
+ #
+ # == Synopsis
+ #
+ # Gem::URI::RFC2396_Parser.new([opts])
+ #
+ # == Args
+ #
+ # The constructor accepts a hash as options for parser.
+ # Keys of options are pattern names of Gem::URI components
+ # and values of options are pattern strings.
+ # The constructor generates set of regexps for parsing URIs.
+ #
+ # You can use the following keys:
+ #
+ # * :ESCAPED (Gem::URI::PATTERN::ESCAPED in default)
+ # * :UNRESERVED (Gem::URI::PATTERN::UNRESERVED in default)
+ # * :DOMLABEL (Gem::URI::PATTERN::DOMLABEL in default)
+ # * :TOPLABEL (Gem::URI::PATTERN::TOPLABEL in default)
+ # * :HOSTNAME (Gem::URI::PATTERN::HOSTNAME in default)
+ #
+ # == Examples
+ #
+ # p = Gem::URI::RFC2396_Parser.new(:ESCAPED => "(?:%[a-fA-F0-9]{2}|%u[a-fA-F0-9]{4})")
+ # u = p.parse("http://example.jp/%uABCD") #=> #<Gem::URI::HTTP http://example.jp/%uABCD>
+ # Gem::URI.parse(u.to_s) #=> raises Gem::URI::InvalidURIError
+ #
+ # s = "http://example.com/ABCD"
+ # u1 = p.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD>
+ # u2 = Gem::URI.parse(s) #=> #<Gem::URI::HTTP http://example.com/ABCD>
+ # u1 == u2 #=> true
+ # u1.eql?(u2) #=> false
+ #
+ def initialize(opts = {})
+ @pattern = initialize_pattern(opts)
+ @pattern.each_value(&:freeze)
+ @pattern.freeze
+
+ @regexp = initialize_regexp(@pattern)
+ @regexp.each_value(&:freeze)
+ @regexp.freeze
+ end
+
+ # The Hash of patterns.
+ #
+ # See also #initialize_pattern.
+ attr_reader :pattern
+
+ # The Hash of Regexp.
+ #
+ # See also #initialize_regexp.
+ attr_reader :regexp
+
+ # Returns a split Gem::URI against +regexp[:ABS_URI]+.
+ def split(uri)
+ case uri
+ when ''
+ # null uri
+
+ when @regexp[:ABS_URI]
+ scheme, opaque, userinfo, host, port,
+ registry, path, query, fragment = $~[1..-1]
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ # opaque_part = uric_no_slash *uric
+
+ # abs_path = "/" path_segments
+ # net_path = "//" authority [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ if !scheme
+ raise InvalidURIError,
+ "bad Gem::URI (absolute but no scheme): #{uri}"
+ end
+ if !opaque && (!path && (!host && !registry))
+ raise InvalidURIError,
+ "bad Gem::URI (absolute but no path): #{uri}"
+ end
+
+ when @regexp[:REL_URI]
+ scheme = nil
+ opaque = nil
+
+ userinfo, host, port, registry,
+ rel_segment, abs_path, query, fragment = $~[1..-1]
+ if rel_segment && abs_path
+ path = rel_segment + abs_path
+ elsif rel_segment
+ path = rel_segment
+ elsif abs_path
+ path = abs_path
+ end
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+
+ # net_path = "//" authority [ abs_path ]
+ # abs_path = "/" path_segments
+ # rel_path = rel_segment [ abs_path ]
+
+ # authority = server | reg_name
+ # server = [ [ userinfo "@" ] hostport ]
+
+ else
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri}"
+ end
+
+ path = '' if !path && !opaque # (see RFC2396 Section 5.2)
+ ret = [
+ scheme,
+ userinfo, host, port, # X
+ registry, # X
+ path, # Y
+ opaque, # Y
+ query,
+ fragment
+ ]
+ return ret
+ end
+
+ #
+ # == Args
+ #
+ # +uri+::
+ # String
+ #
+ # == Description
+ #
+ # Parses +uri+ and constructs either matching Gem::URI scheme object
+ # (File, FTP, HTTP, HTTPS, LDAP, LDAPS, or MailTo) or Gem::URI::Generic.
+ #
+ # == Usage
+ #
+ # Gem::URI::RFC2396_PARSER.parse("ldap://ldap.example.com/dc=example?user=john")
+ # #=> #<Gem::URI::LDAP ldap://ldap.example.com/dc=example?user=john>
+ #
+ def parse(uri)
+ Gem::URI.for(*self.split(uri), self)
+ end
+
+ #
+ # == Args
+ #
+ # +uris+::
+ # an Array of Strings
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ #
+ def join(*uris)
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ #
+ # :call-seq:
+ # extract( str )
+ # extract( str, schemes )
+ # extract( str, schemes ) {|item| block }
+ #
+ # == Args
+ #
+ # +str+::
+ # String to search
+ # +schemes+::
+ # Patterns to apply to +str+
+ #
+ # == Description
+ #
+ # Attempts to parse and merge a set of URIs.
+ # If no +block+ given, then returns the result,
+ # else it calls +block+ for each element in result.
+ #
+ # See also #make_regexp.
+ #
+ def extract(str, schemes = nil)
+ if block_given?
+ str.scan(make_regexp(schemes)) { yield $& }
+ nil
+ else
+ result = []
+ str.scan(make_regexp(schemes)) { result.push $& }
+ result
+ end
+ end
+
+ # Returns Regexp that is default +self.regexp[:ABS_URI_REF]+,
+ # unless +schemes+ is provided. Then it is a Regexp.union with +self.pattern[:X_ABS_URI]+.
+ def make_regexp(schemes = nil)
+ unless schemes
+ @regexp[:ABS_URI_REF]
+ else
+ /(?=(?i:#{Regexp.union(*schemes).source}):)#{@pattern[:X_ABS_URI]}/x
+ end
+ end
+
+ #
+ # :call-seq:
+ # escape( str )
+ # escape( str, unsafe )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to make safe
+ # +unsafe+::
+ # Regexp to apply. Defaults to +self.regexp[:UNSAFE]+
+ #
+ # == Description
+ #
+ # Constructs a safe String from +str+, removing unsafe characters,
+ # replacing them with codes.
+ #
+ def escape(str, unsafe = @regexp[:UNSAFE])
+ unless unsafe.kind_of?(Regexp)
+ # perhaps unsafe is String object
+ unsafe = Regexp.new("[#{Regexp.quote(unsafe)}]", false)
+ end
+ str.gsub(unsafe) do
+ us = $&
+ tmp = ''
+ us.each_byte do |uc|
+ tmp << sprintf('%%%02X', uc)
+ end
+ tmp
+ end.force_encoding(Encoding::US_ASCII)
+ end
+
+ #
+ # :call-seq:
+ # unescape( str )
+ # unescape( str, escaped )
+ #
+ # == Args
+ #
+ # +str+::
+ # String to remove escapes from
+ # +escaped+::
+ # Regexp to apply. Defaults to +self.regexp[:ESCAPED]+
+ #
+ # == Description
+ #
+ # Removes escapes from +str+.
+ #
+ def unescape(str, escaped = @regexp[:ESCAPED])
+ enc = str.encoding
+ enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
+ str.gsub(escaped) { [$&[1, 2]].pack('H2').force_encoding(enc) }
+ end
+
+ TO_S = Kernel.instance_method(:to_s) # :nodoc:
+ if TO_S.respond_to?(:bind_call)
+ def inspect # :nodoc:
+ TO_S.bind_call(self)
+ end
+ else
+ def inspect # :nodoc:
+ TO_S.bind(self).call
+ end
+ end
+
+ private
+
+ # Constructs the default Hash of patterns.
+ def initialize_pattern(opts = {})
+ ret = {}
+ ret[:ESCAPED] = escaped = (opts.delete(:ESCAPED) || PATTERN::ESCAPED)
+ ret[:UNRESERVED] = unreserved = opts.delete(:UNRESERVED) || PATTERN::UNRESERVED
+ ret[:RESERVED] = reserved = opts.delete(:RESERVED) || PATTERN::RESERVED
+ ret[:DOMLABEL] = opts.delete(:DOMLABEL) || PATTERN::DOMLABEL
+ ret[:TOPLABEL] = opts.delete(:TOPLABEL) || PATTERN::TOPLABEL
+ ret[:HOSTNAME] = hostname = opts.delete(:HOSTNAME)
+
+ # RFC 2396 (Gem::URI Generic Syntax)
+ # RFC 2732 (IPv6 Literal Addresses in URL's)
+ # RFC 2373 (IPv6 Addressing Architecture)
+
+ # uric = reserved | unreserved | escaped
+ ret[:URIC] = uric = "(?:[#{unreserved}#{reserved}]|#{escaped})"
+ # uric_no_slash = unreserved | escaped | ";" | "?" | ":" | "@" |
+ # "&" | "=" | "+" | "$" | ","
+ ret[:URIC_NO_SLASH] = uric_no_slash = "(?:[#{unreserved};?:@&=+$,]|#{escaped})"
+ # query = *uric
+ ret[:QUERY] = query = "#{uric}*"
+ # fragment = *uric
+ ret[:FRAGMENT] = fragment = "#{uric}*"
+
+ # hostname = *( domainlabel "." ) toplabel [ "." ]
+ # reg-name = *( unreserved / pct-encoded / sub-delims ) # RFC3986
+ unless hostname
+ ret[:HOSTNAME] = hostname = "(?:[a-zA-Z0-9\\-.]|%\\h\\h)+"
+ end
+
+ # RFC 2373, APPENDIX B:
+ # IPv6address = hexpart [ ":" IPv4address ]
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ # hexpart = hexseq | hexseq "::" [ hexseq ] | "::" [ hexseq ]
+ # hexseq = hex4 *( ":" hex4)
+ # hex4 = 1*4HEXDIG
+ #
+ # XXX: This definition has a flaw. "::" + IPv4address must be
+ # allowed too. Here is a replacement.
+ #
+ # IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
+ ret[:IPV4ADDR] = ipv4addr = "\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}"
+ # hex4 = 1*4HEXDIG
+ hex4 = "[#{PATTERN::HEX}]{1,4}"
+ # lastpart = hex4 | IPv4address
+ lastpart = "(?:#{hex4}|#{ipv4addr})"
+ # hexseq1 = *( hex4 ":" ) hex4
+ hexseq1 = "(?:#{hex4}:)*#{hex4}"
+ # hexseq2 = *( hex4 ":" ) lastpart
+ hexseq2 = "(?:#{hex4}:)*#{lastpart}"
+ # IPv6address = hexseq2 | [ hexseq1 ] "::" [ hexseq2 ]
+ ret[:IPV6ADDR] = ipv6addr = "(?:#{hexseq2}|(?:#{hexseq1})?::(?:#{hexseq2})?)"
+
+ # IPv6prefix = ( hexseq1 | [ hexseq1 ] "::" [ hexseq1 ] ) "/" 1*2DIGIT
+ # unused
+
+ # ipv6reference = "[" IPv6address "]" (RFC 2732)
+ ret[:IPV6REF] = ipv6ref = "\\[#{ipv6addr}\\]"
+
+ # host = hostname | IPv4address
+ # host = hostname | IPv4address | IPv6reference (RFC 2732)
+ ret[:HOST] = host = "(?:#{hostname}|#{ipv4addr}|#{ipv6ref})"
+ # port = *digit
+ ret[:PORT] = port = '\d*'
+ # hostport = host [ ":" port ]
+ ret[:HOSTPORT] = hostport = "#{host}(?::#{port})?"
+
+ # userinfo = *( unreserved | escaped |
+ # ";" | ":" | "&" | "=" | "+" | "$" | "," )
+ ret[:USERINFO] = userinfo = "(?:[#{unreserved};:&=+$,]|#{escaped})*"
+
+ # pchar = unreserved | escaped |
+ # ":" | "@" | "&" | "=" | "+" | "$" | ","
+ pchar = "(?:[#{unreserved}:@&=+$,]|#{escaped})"
+ # param = *pchar
+ param = "#{pchar}*"
+ # segment = *pchar *( ";" param )
+ segment = "#{pchar}*(?:;#{param})*"
+ # path_segments = segment *( "/" segment )
+ ret[:PATH_SEGMENTS] = path_segments = "#{segment}(?:/#{segment})*"
+
+ # server = [ [ userinfo "@" ] hostport ]
+ server = "(?:#{userinfo}@)?#{hostport}"
+ # reg_name = 1*( unreserved | escaped | "$" | "," |
+ # ";" | ":" | "@" | "&" | "=" | "+" )
+ ret[:REG_NAME] = reg_name = "(?:[#{unreserved}$,;:@&=+]|#{escaped})+"
+ # authority = server | reg_name
+ authority = "(?:#{server}|#{reg_name})"
+
+ # rel_segment = 1*( unreserved | escaped |
+ # ";" | "@" | "&" | "=" | "+" | "$" | "," )
+ ret[:REL_SEGMENT] = rel_segment = "(?:[#{unreserved};@&=+$,]|#{escaped})+"
+
+ # scheme = alpha *( alpha | digit | "+" | "-" | "." )
+ ret[:SCHEME] = scheme = "[#{PATTERN::ALPHA}][\\-+.#{PATTERN::ALPHA}\\d]*"
+
+ # abs_path = "/" path_segments
+ ret[:ABS_PATH] = abs_path = "/#{path_segments}"
+ # rel_path = rel_segment [ abs_path ]
+ ret[:REL_PATH] = rel_path = "#{rel_segment}(?:#{abs_path})?"
+ # net_path = "//" authority [ abs_path ]
+ ret[:NET_PATH] = net_path = "//#{authority}(?:#{abs_path})?"
+
+ # hier_part = ( net_path | abs_path ) [ "?" query ]
+ ret[:HIER_PART] = hier_part = "(?:#{net_path}|#{abs_path})(?:\\?(?:#{query}))?"
+ # opaque_part = uric_no_slash *uric
+ ret[:OPAQUE_PART] = opaque_part = "#{uric_no_slash}#{uric}*"
+
+ # absoluteURI = scheme ":" ( hier_part | opaque_part )
+ ret[:ABS_URI] = abs_uri = "#{scheme}:(?:#{hier_part}|#{opaque_part})"
+ # relativeURI = ( net_path | abs_path | rel_path ) [ "?" query ]
+ ret[:REL_URI] = rel_uri = "(?:#{net_path}|#{abs_path}|#{rel_path})(?:\\?#{query})?"
+
+ # Gem::URI-reference = [ absoluteURI | relativeURI ] [ "#" fragment ]
+ ret[:URI_REF] = "(?:#{abs_uri}|#{rel_uri})?(?:##{fragment})?"
+
+ ret[:X_ABS_URI] = "
+ (#{scheme}): (?# 1: scheme)
+ (?:
+ (#{opaque_part}) (?# 2: opaque)
+ |
+ (?:(?:
+ //(?:
+ (?:(?:(#{userinfo})@)? (?# 3: userinfo)
+ (?:(#{host})(?::(\\d*))?))? (?# 4: host, 5: port)
+ |
+ (#{reg_name}) (?# 6: registry)
+ )
+ |
+ (?!//)) (?# XXX: '//' is the mark for hostport)
+ (#{abs_path})? (?# 7: path)
+ )(?:\\?(#{query}))? (?# 8: query)
+ )
+ (?:\\#(#{fragment}))? (?# 9: fragment)
+ "
+
+ ret[:X_REL_URI] = "
+ (?:
+ (?:
+ //
+ (?:
+ (?:(#{userinfo})@)? (?# 1: userinfo)
+ (#{host})?(?::(\\d*))? (?# 2: host, 3: port)
+ |
+ (#{reg_name}) (?# 4: registry)
+ )
+ )
+ |
+ (#{rel_segment}) (?# 5: rel_segment)
+ )?
+ (#{abs_path})? (?# 6: abs_path)
+ (?:\\?(#{query}))? (?# 7: query)
+ (?:\\#(#{fragment}))? (?# 8: fragment)
+ "
+
+ ret
+ end
+
+ # Constructs the default Hash of Regexp's.
+ def initialize_regexp(pattern)
+ ret = {}
+
+ # for Gem::URI::split
+ ret[:ABS_URI] = Regexp.new('\A\s*+' + pattern[:X_ABS_URI] + '\s*\z', Regexp::EXTENDED)
+ ret[:REL_URI] = Regexp.new('\A\s*+' + pattern[:X_REL_URI] + '\s*\z', Regexp::EXTENDED)
+
+ # for Gem::URI::extract
+ ret[:URI_REF] = Regexp.new(pattern[:URI_REF])
+ ret[:ABS_URI_REF] = Regexp.new(pattern[:X_ABS_URI], Regexp::EXTENDED)
+ ret[:REL_URI_REF] = Regexp.new(pattern[:X_REL_URI], Regexp::EXTENDED)
+
+ # for Gem::URI::escape/unescape
+ ret[:ESCAPED] = Regexp.new(pattern[:ESCAPED])
+ ret[:UNSAFE] = Regexp.new("[^#{pattern[:UNRESERVED]}#{pattern[:RESERVED]}]")
+
+ # for Generic#initialize
+ ret[:SCHEME] = Regexp.new("\\A#{pattern[:SCHEME]}\\z")
+ ret[:USERINFO] = Regexp.new("\\A#{pattern[:USERINFO]}\\z")
+ ret[:HOST] = Regexp.new("\\A#{pattern[:HOST]}\\z")
+ ret[:PORT] = Regexp.new("\\A#{pattern[:PORT]}\\z")
+ ret[:OPAQUE] = Regexp.new("\\A#{pattern[:OPAQUE_PART]}\\z")
+ ret[:REGISTRY] = Regexp.new("\\A#{pattern[:REG_NAME]}\\z")
+ ret[:ABS_PATH] = Regexp.new("\\A#{pattern[:ABS_PATH]}\\z")
+ ret[:REL_PATH] = Regexp.new("\\A#{pattern[:REL_PATH]}\\z")
+ ret[:QUERY] = Regexp.new("\\A#{pattern[:QUERY]}\\z")
+ ret[:FRAGMENT] = Regexp.new("\\A#{pattern[:FRAGMENT]}\\z")
+
+ ret
+ end
+
+ # Returns +uri+ as-is if it is Gem::URI, or convert it to Gem::URI if it is
+ # a String.
+ def convert_to_uri(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+
+ end # class Parser
+
+ # Backward compatibility for Gem::URI::REGEXP::PATTERN::*
+ RFC2396_Parser.new.pattern.each_pair do |sym, str|
+ unless RFC2396_REGEXP::PATTERN.const_defined?(sym, false)
+ RFC2396_REGEXP::PATTERN.const_set(sym, str)
+ end
+ end
+end # module Gem::URI
diff --git a/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb
new file mode 100644
index 0000000000..3b6961abf6
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/rfc3986_parser.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+module Gem::URI
+ class RFC3986_Parser # :nodoc:
+ # Gem::URI defined in RFC3986
+ HOST = %r[
+ (?<IP-literal>\[(?:
+ (?<IPv6address>
+ (?:\h{1,4}:){6}
+ (?<ls32>\h{1,4}:\h{1,4}
+ | (?<IPv4address>(?<dec-octet>[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]|\d)
+ \.\g<dec-octet>\.\g<dec-octet>\.\g<dec-octet>)
+ )
+ | ::(?:\h{1,4}:){5}\g<ls32>
+ | \h{1,4}?::(?:\h{1,4}:){4}\g<ls32>
+ | (?:(?:\h{1,4}:)?\h{1,4})?::(?:\h{1,4}:){3}\g<ls32>
+ | (?:(?:\h{1,4}:){,2}\h{1,4})?::(?:\h{1,4}:){2}\g<ls32>
+ | (?:(?:\h{1,4}:){,3}\h{1,4})?::\h{1,4}:\g<ls32>
+ | (?:(?:\h{1,4}:){,4}\h{1,4})?::\g<ls32>
+ | (?:(?:\h{1,4}:){,5}\h{1,4})?::\h{1,4}
+ | (?:(?:\h{1,4}:){,6}\h{1,4})?::
+ )
+ | (?<IPvFuture>v\h++\.[!$&-.0-9:;=A-Z_a-z~]++)
+ )\])
+ | \g<IPv4address>
+ | (?<reg-name>(?:%\h\h|[!$&-.0-9;=A-Z_a-z~])*+)
+ ]x
+
+ USERINFO = /(?:%\h\h|[!$&-.0-9:;=A-Z_a-z~])*+/
+
+ SCHEME = %r[[A-Za-z][+\-.0-9A-Za-z]*+].source
+ SEG = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/])].source
+ SEG_NC = %r[(?:%\h\h|[!$&-.0-9;=@A-Z_a-z~])].source
+ FRAGMENT = %r[(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+].source
+
+ RFC3986_URI = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<Gem::URI>
+ (?<scheme>#{SCHEME}):
+ (?<hier-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")})
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/((?!/)\g<seg>++)?)
+ | (?<path-rootless>(?!/)\g<seg>++)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^\#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+
+ RFC3986_relative_ref = %r[\A
+ (?<seg>#{SEG}){0}
+ (?<relative-ref>
+ (?<relative-part>//
+ (?<authority>
+ (?:(?<userinfo>#{USERINFO.source})@)?
+ (?<host>#{HOST.source.delete(" \n")}(?<!/))?
+ (?::(?<port>\d*+))?
+ )
+ (?<path-abempty>(?:/\g<seg>*+)?)
+ | (?<path-absolute>/\g<seg>*+)
+ | (?<path-noscheme>#{SEG_NC}++(?:/\g<seg>*+)?)
+ | (?<path-empty>)
+ )
+ (?:\?(?<query>[^#]*+))?
+ (?:\#(?<fragment>#{FRAGMENT}))?
+ )\z]x
+ attr_reader :regexp
+
+ def initialize
+ @regexp = default_regexp.each_value(&:freeze).freeze
+ end
+
+ def split(uri) #:nodoc:
+ begin
+ uri = uri.to_str
+ rescue NoMethodError
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}"
+ end
+ uri.ascii_only? or
+ raise InvalidURIError, "Gem::URI must be ascii only #{uri.dump}"
+ if m = RFC3986_URI.match(uri)
+ query = m["query"]
+ scheme = m["scheme"]
+ opaque = m["path-rootless"]
+ if opaque
+ opaque << "?#{query}" if query
+ [ scheme,
+ nil, # userinfo
+ nil, # host
+ nil, # port
+ nil, # registry
+ nil, # path
+ opaque,
+ nil, # query
+ m["fragment"]
+ ]
+ else # normal
+ [ scheme,
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-empty"]),
+ nil, # opaque
+ query,
+ m["fragment"]
+ ]
+ end
+ elsif m = RFC3986_relative_ref.match(uri)
+ [ nil, # scheme
+ m["userinfo"],
+ m["host"],
+ m["port"],
+ nil, # registry,
+ (m["path-abempty"] ||
+ m["path-absolute"] ||
+ m["path-noscheme"] ||
+ m["path-empty"]),
+ nil, # opaque
+ m["query"],
+ m["fragment"]
+ ]
+ else
+ raise InvalidURIError, "bad Gem::URI (is not Gem::URI?): #{uri.inspect}"
+ end
+ end
+
+ def parse(uri) # :nodoc:
+ Gem::URI.for(*self.split(uri), self)
+ end
+
+ def join(*uris) # :nodoc:
+ uris[0] = convert_to_uri(uris[0])
+ uris.inject :merge
+ end
+
+ # Compatibility for RFC2396 parser
+ def extract(str, schemes = nil, &block) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.extract is obsolete. Use Gem::URI::RFC2396_PARSER.extract explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.extract(str, schemes, &block)
+ end
+
+ # Compatibility for RFC2396 parser
+ def make_regexp(schemes = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.make_regexp is obsolete. Use Gem::URI::RFC2396_PARSER.make_regexp explicitly.", uplevel: 1 if $VERBOSE
+ RFC2396_PARSER.make_regexp(schemes)
+ end
+
+ # Compatibility for RFC2396 parser
+ def escape(str, unsafe = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.escape is obsolete. Use Gem::URI::RFC2396_PARSER.escape explicitly.", uplevel: 1 if $VERBOSE
+ unsafe ? RFC2396_PARSER.escape(str, unsafe) : RFC2396_PARSER.escape(str)
+ end
+
+ # Compatibility for RFC2396 parser
+ def unescape(str, escaped = nil) # :nodoc:
+ warn "Gem::URI::RFC3986_PARSER.unescape is obsolete. Use Gem::URI::RFC2396_PARSER.unescape explicitly.", uplevel: 1 if $VERBOSE
+ escaped ? RFC2396_PARSER.unescape(str, escaped) : RFC2396_PARSER.unescape(str)
+ end
+
+ @@to_s = Kernel.instance_method(:to_s)
+ if @@to_s.respond_to?(:bind_call)
+ def inspect
+ @@to_s.bind_call(self)
+ end
+ else
+ def inspect
+ @@to_s.bind(self).call
+ end
+ end
+
+ private
+
+ def default_regexp # :nodoc:
+ {
+ SCHEME: %r[\A#{SCHEME}\z]o,
+ USERINFO: %r[\A#{USERINFO}\z]o,
+ HOST: %r[\A#{HOST}\z]o,
+ ABS_PATH: %r[\A/#{SEG}*+\z]o,
+ REL_PATH: %r[\A(?!/)#{SEG}++\z]o,
+ QUERY: %r[\A(?:%\h\h|[!$&-.0-9:;=@A-Z_a-z~/?])*+\z],
+ FRAGMENT: %r[\A#{FRAGMENT}\z]o,
+ OPAQUE: %r[\A(?:[^/].*)?\z],
+ PORT: /\A[\x09\x0a\x0c\x0d ]*+\d*[\x09\x0a\x0c\x0d ]*\z/,
+ }
+ end
+
+ def convert_to_uri(uri)
+ if uri.is_a?(Gem::URI::Generic)
+ uri
+ elsif uri = String.try_convert(uri)
+ parse(uri)
+ else
+ raise ArgumentError,
+ "bad argument (expected Gem::URI object or Gem::URI string)"
+ end
+ end
+
+ end # class Parser
+end # module Gem::URI
diff --git a/lib/rubygems/vendor/uri/lib/uri/version.rb b/lib/rubygems/vendor/uri/lib/uri/version.rb
new file mode 100644
index 0000000000..7ee577887b
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/version.rb
@@ -0,0 +1,6 @@
+module Gem::URI
+ # :stopdoc:
+ VERSION = '1.1.1'.freeze
+ VERSION_CODE = VERSION.split('.').map{|s| s.rjust(2, '0')}.join.freeze
+ # :startdoc:
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/ws.rb b/lib/rubygems/vendor/uri/lib/uri/ws.rb
new file mode 100644
index 0000000000..0dd2a7a1bb
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/ws.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: false
+# = uri/ws.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'generic'
+
+module Gem::URI
+
+ #
+ # The syntax of WS URIs is defined in RFC6455 section 3.
+ #
+ # Note that the Ruby Gem::URI library allows WS URLs containing usernames and
+ # passwords. This is not legal as per the RFC, but used to be
+ # supported in Internet Explorer 5 and 6, before the MS04-004 security
+ # update. See <URL:http://support.microsoft.com/kb/834489>.
+ #
+ class WS < Generic
+ # A Default port of 80 for Gem::URI::WS.
+ DEFAULT_PORT = 80
+
+ # An Array of the available components for Gem::URI::WS.
+ COMPONENT = %i[
+ scheme
+ userinfo host port
+ path
+ query
+ ].freeze
+
+ #
+ # == Description
+ #
+ # Creates a new Gem::URI::WS object from components, with syntax checking.
+ #
+ # The components accepted are userinfo, host, port, path, and query.
+ #
+ # The components should be provided either as an Array, or as a Hash
+ # with keys formed by preceding the component names with a colon.
+ #
+ # If an Array is used, the components must be passed in the
+ # order <code>[userinfo, host, port, path, query]</code>.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::WS.build(host: 'www.example.com', path: '/foo/bar')
+ #
+ # uri = Gem::URI::WS.build([nil, "www.example.com", nil, "/path", "query"])
+ #
+ # Currently, if passed userinfo components this method generates
+ # invalid WS URIs as per RFC 1738.
+ #
+ def self.build(args)
+ tmp = Util.make_components_hash(self, args)
+ super(tmp)
+ end
+
+ #
+ # == Description
+ #
+ # Returns the full path for a WS Gem::URI, as required by Net::HTTP::Get.
+ #
+ # If the Gem::URI contains a query, the full path is Gem::URI#path + '?' + Gem::URI#query.
+ # Otherwise, the path is simply Gem::URI#path.
+ #
+ # Example:
+ #
+ # uri = Gem::URI::WS.build(path: '/foo/bar', query: 'test=true')
+ # uri.request_uri # => "/foo/bar?test=true"
+ #
+ def request_uri
+ return unless @path
+
+ url = @query ? "#@path?#@query" : @path.dup
+ url.start_with?(?/.freeze) ? url : ?/ + url
+ end
+ end
+
+ register_scheme 'WS', WS
+end
diff --git a/lib/rubygems/vendor/uri/lib/uri/wss.rb b/lib/rubygems/vendor/uri/lib/uri/wss.rb
new file mode 100644
index 0000000000..0b91d334bb
--- /dev/null
+++ b/lib/rubygems/vendor/uri/lib/uri/wss.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: false
+# = uri/wss.rb
+#
+# Author:: Matt Muller <mamuller@amazon.com>
+# License:: You can redistribute it and/or modify it under the same term as Ruby.
+#
+# See Gem::URI for general documentation
+#
+
+require_relative 'ws'
+
+module Gem::URI
+
+ # The default port for WSS URIs is 443, and the scheme is 'wss:' rather
+ # than 'ws:'. Other than that, WSS URIs are identical to WS URIs;
+ # see Gem::URI::WS.
+ class WSS < WS
+ # A Default port of 443 for Gem::URI::WSS
+ DEFAULT_PORT = 443
+ end
+
+ register_scheme 'WSS', WSS
+end
diff --git a/lib/rubygems/vendored_net_http.rb b/lib/rubygems/vendored_net_http.rb
new file mode 100644
index 0000000000..a84c52a947
--- /dev/null
+++ b/lib/rubygems/vendored_net_http.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/net/http.rb
+# We should avoid to load it again
+require_relative "vendor/net-http/lib/net/http" unless defined?(Gem::Net::HTTP)
diff --git a/lib/rubygems/vendored_optparse.rb b/lib/rubygems/vendored_optparse.rb
new file mode 100644
index 0000000000..a5611d32f0
--- /dev/null
+++ b/lib/rubygems/vendored_optparse.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/optparse/lib/optparse"
diff --git a/lib/rubygems/vendored_pub_grub.rb b/lib/rubygems/vendored_pub_grub.rb
new file mode 100644
index 0000000000..844d243ab3
--- /dev/null
+++ b/lib/rubygems/vendored_pub_grub.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/pub_grub/lib/pub_grub"
diff --git a/lib/rubygems/vendored_securerandom.rb b/lib/rubygems/vendored_securerandom.rb
new file mode 100644
index 0000000000..859b6d7d7a
--- /dev/null
+++ b/lib/rubygems/vendored_securerandom.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/securerandom/lib/securerandom"
diff --git a/lib/rubygems/vendored_timeout.rb b/lib/rubygems/vendored_timeout.rb
new file mode 100644
index 0000000000..45541928e6
--- /dev/null
+++ b/lib/rubygems/vendored_timeout.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+# Ruby 3.3 and RubyGems 3.5 is already load Gem::Timeout from lib/rubygems/timeout.rb
+# We should avoid to load it again
+require_relative "vendor/timeout/lib/timeout" unless defined?(Gem::Timeout)
diff --git a/lib/rubygems/vendored_tsort.rb b/lib/rubygems/vendored_tsort.rb
new file mode 100644
index 0000000000..c3d815650d
--- /dev/null
+++ b/lib/rubygems/vendored_tsort.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+require_relative "vendor/tsort/lib/tsort"
diff --git a/lib/rubygems/version.rb b/lib/rubygems/version.rb
new file mode 100644
index 0000000000..306733c1d7
--- /dev/null
+++ b/lib/rubygems/version.rb
@@ -0,0 +1,472 @@
+# frozen_string_literal: true
+
+#--
+# Workaround for directly loading Gem::Version in some cases
+module Gem; end
+#++
+
+##
+# The Version class processes string versions into comparable
+# values. A version string should normally be a series of numbers
+# separated by periods. Each part (digits separated by periods) is
+# considered its own number, and these are used for sorting. So for
+# instance, 3.10 sorts higher than 3.2 because ten is greater than
+# two.
+#
+# If any part contains letters (currently only a-z are supported) then
+# that version is considered prerelease. Versions with a prerelease
+# part in the Nth part sort less than versions with N-1
+# parts. Prerelease parts are sorted alphabetically using the normal
+# Ruby string sorting rules. If a prerelease part contains both
+# letters and numbers, it will be broken into multiple parts to
+# provide expected sort behavior (1.0.a10 becomes 1.0.a.10, and is
+# greater than 1.0.a9).
+#
+# Prereleases sort between real releases (newest to oldest):
+#
+# 1. 1.0
+# 2. 1.0.b1
+# 3. 1.0.a.2
+# 4. 0.9
+#
+# If you want to specify a version restriction that includes both prereleases
+# and regular releases of 1.x or later versions:
+#
+# s.add_dependency 'example', '>= 1.0.0.a'
+#
+# == How Software Changes
+#
+# Libraries generally change in 3 ways:
+#
+# 1. The change is an implementation detail, bug fix, security fix, or
+# optimization, and has no behavioral effect on the software using it.
+#
+# 2. The change adds new features, and software using those new features is
+# not compatible with previous versions of the library, but software using
+# previous versions of the library is compatible with the change.
+#
+# 3. The change modifies the public interface of some part of the library in
+# such a way that software that uses that part of the library must be
+# modified to work.
+#
+# == RubyGems Rational Versioning (the recommended approach)
+#
+# * Versions shall be represented by three non-negative integers, separated
+# by periods (e.g. 3.1.4). The first integer is the "major" version
+# number, the second integer is the "minor" version number, and the third
+# integer is the "patch" version number.
+#
+# * A category 1 change (implementation detail, bug fix, or security fix)
+# will increment the patch number.
+#
+# * A category 2 change (backwards compatible) will increment the minor
+# version number and reset the patch number.
+#
+# * A category 3 change (incompatible) will increment the major version number
+# and reset the minor and patch numbers.
+#
+# * Any "public" release of a gem should have a different version.
+#
+# == Optimistic Vs. Pessimistic Dependency Versioning
+#
+# Users expect to be able to specify a version constraint that gives them
+# a reasonable expectation that new versions of a library will work with
+# their software if the version constraint is true, and not work with their
+# software if the version constraint is false. In other words, the perfect
+# system will accept all compatible versions of the library and reject all
+# incompatible versions. Unfortunately, there is no perfect system, as you
+# cannot predict the future. You can never know whether a future version of
+# a library will contain which type of change.
+#
+# There are two common outlooks on dependency versioning:
+#
+# 1. Optimistic. This does not set an upper bound on a dependency. It is
+# possible that a future version of a dependency will break the software,
+# and in that case, the dependency version will need to be updated and
+# changes will need to be made.
+#
+# 2. Pessimistic. This assumes all major version changes of a dependency will
+# break the software, and that patch or minor changes of a dependency will
+# not break the software. If there is a major version of a dependency
+# released, the dependency version must be updated in order to use it, even
+# if no code changes are actually needed.
+#
+# In general, optimistic versioning is superior to pessimistic versioning.
+# Pessimistic versioning is often wrong in both directions. Dependencies can
+# release patch or minor versions that contain incompatibilities. One
+# common reason is that a security fix may require a backwards-incompatible API
+# change. In this case, even though pessimistic versioning was used, it
+# didn't even save effort, as you still need to make code changes and adjust
+# dependency versions. Similarly, for all but the smallest dependencies, just
+# because the dependency made a backwards incompatible change to one interface
+# doesn't mean the dependency made a backwards incompatible change to an
+# interface that the software is using. It is a common problem that a
+# dependency will release a new major version and the software does not require
+# any changes in order to use it. In this case, being pessimistic results in
+# additional work for no benefit.
+#
+# When a library uses pessimistic versioning of dependencies, it causes
+# significant problems if that library is not diligent about updating
+# dependency versions and any library is depending on that library.
+# For example:
+#
+# * Library A is currently on release 1.2.3.
+#
+# * Library B is at version 2.3.4 and has a pessimistic dependency on
+# library A, using ~> 1.0 (>= 1.0, < 2).
+#
+# * Library C is at version 3.4.5 and has an optimistic dependency on
+# library A, using >= 1.0.
+#
+# * Library D has optimistic dependencies on both libraries B and C.
+#
+# * Library A releases a new major version, 2.0.0, with new features, which
+# is mostly backwards compatible, but does contain some backwards
+# incompatible changes.
+#
+# * Library B would work with A 2.0.0, but cannot use it due to pessimistic
+# versioning.
+#
+# * Library C wants to use the new features in the major release of library
+# A to implement its own new features, so it does so, bumps the
+# dependency version of A to >= 2.0, and releases version 3.5.0.
+#
+# * Library D cannot upgrade to the new version of library C, because it
+# depends on library B, which has a pessimistic dependency on library A.
+#
+# * Library C releases a security fix patch version 3.5.1 to fix a
+# vulnerability present in all previous versions.
+#
+# * Library D is now in a terrible situation. It cannot upgrade to library
+# C 3.5.1, as that requires library A > 2.0, because it depends on library
+# B, which requires library A > 1.0, < 2, even though library B would work
+# fine with library A 2.0.0.
+#
+# This type of situation brought on by pessimistic versioning is unfortunately
+# both common and serious in practice.
+#
+# This is not to say that optimistic versioning never causes a problem.
+# However, with optimistic versioning, if there is a problem, it can be solved
+# with the addition of a single dependency. For example, continuing the
+# previous example:
+#
+# * Library A releases a new major version, 3.0.0, which makes backwards
+# incompatible changes that break library C.
+#
+# * Until library C releases an updated version with new changes, library
+# D only needs to set a specific dependency on library A for > 2.0, < 3,
+# until library C is updated to work with the new version of library A.
+#
+# Both optimistic versioning and pessimistic versioning have problems in
+# certain cases. However, it's significantly easier to fix optimistic
+# versioning problems than to fix pessimistic versioning problems.
+#
+# That is not to say that pessimistic versioning is never appropriate. If the
+# dependency is a library that adds a single method, where any change resulting
+# in a major version bump would probably break a library using it, then using
+# pessimistic versioning may be warranted. Additionally, if a dependency has
+# already announced or committed backwards incompatible changes that would
+# break a library's use of it, then having that library use a pessimistic
+# version constraint would likely be warranted. However, outside of
+# specific situations, you should avoid using pessimistic versioning, as the
+# costs typically exceed the benefits.
+
+class Gem::Version
+ include Comparable
+
+ VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc:
+ ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc:
+ RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc:
+
+ ##
+ # A string representation of this Version.
+
+ def version
+ @version
+ end
+
+ alias_method :to_s, :version
+
+ ##
+ # True if the +version+ string matches RubyGems' requirements.
+
+ def self.correct?(version)
+ version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s)
+ end
+
+ ##
+ # Factory method to create a Version object. Input may be a Version
+ # or a String. Intended to simplify client code.
+ #
+ # ver1 = Version.create('1.3.17') # -> (Version object)
+ # ver2 = Version.create(ver1) # -> (ver1)
+
+ def self.create(input)
+ if self === input # check yourself before you wreck yourself
+ input
+ else
+ new input
+ end
+ end
+
+ @@all = {}
+ @@bump = {}
+ @@release = {}
+
+ def self.new(version) # :nodoc:
+ return super unless self == Gem::Version
+
+ @@all[version] ||= super
+ end
+
+ ##
+ # Constructs a Version from the +version+ string. A version string is a
+ # series of digits or ASCII letters separated by dots.
+
+ def initialize(version)
+ unless self.class.correct?(version)
+ raise ArgumentError, "Malformed version number string #{version}"
+ end
+
+ # If version is an empty string convert it to 0
+ version = 0 if version.nil? || (version.is_a?(String) && /\A\s*\Z/.match?(version))
+
+ @version = version.to_s
+
+ # optimization to avoid allocation when given an integer, since we know
+ # it's to_s won't have any spaces or dashes
+ unless version.is_a?(Integer)
+ @version = @version.strip
+ @version.gsub!("-",".pre.")
+ end
+ @version = -@version
+ @segments = nil
+ @sort_key = compute_sort_key
+ end
+
+ ##
+ # Return a new version object where the next to the last revision
+ # number is one greater (e.g., 5.3.1 => 5.4).
+ #
+ # Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored.
+
+ def bump
+ @@bump[self] ||= begin
+ segments = self.segments
+ segments.pop while segments.any? {|s| String === s }
+ segments.pop if segments.size > 1
+
+ segments[-1] = segments[-1].succ
+ self.class.new segments.join(".")
+ end
+ end
+
+ ##
+ # A Version is only eql? to another version if it's specified to the
+ # same precision. Version "1.0" is not the same as version "1".
+
+ def eql?(other)
+ self.class === other && @version == other.version
+ end
+
+ def hash # :nodoc:
+ canonical_segments.hash
+ end
+
+ def init_with(coder) # :nodoc:
+ yaml_initialize coder.tag, coder.map
+ end
+
+ def inspect # :nodoc:
+ "#<#{self.class} #{version.inspect}>"
+ end
+
+ ##
+ # Dump only the raw version string, not the complete object. It's a
+ # string for backwards (RubyGems 1.3.5 and earlier) compatibility.
+
+ def marshal_dump
+ [@version]
+ end
+
+ ##
+ # Load custom marshal format. It's a string for backwards (RubyGems
+ # 1.3.5 and earlier) compatibility.
+
+ def marshal_load(array)
+ string = array[0]
+ raise TypeError, "wrong version string" unless string.is_a?(String)
+
+ initialize string
+ end
+
+ def yaml_initialize(tag, map) # :nodoc:
+ @version = -map["version"]
+ @segments = nil
+ @hash = nil
+ end
+
+ def encode_with(coder) # :nodoc:
+ coder.add "version", @version
+ end
+
+ ##
+ # A version is considered a prerelease if it contains a letter.
+
+ def prerelease?
+ unless instance_variable_defined? :@prerelease
+ @prerelease = /[a-zA-Z]/.match?(version)
+ end
+ @prerelease
+ end
+
+ def pretty_print(q) # :nodoc:
+ q.text "Gem::Version.new(#{version.inspect})"
+ end
+
+ ##
+ # The release for this version (e.g. 1.2.0.a -> 1.2.0).
+ # Non-prerelease versions return themselves.
+
+ def release
+ @@release[self] ||= if prerelease?
+ segments = self.segments
+ segments.pop while segments.any? {|s| String === s }
+ self.class.new segments.join(".")
+ else
+ self
+ end
+ end
+
+ def segments # :nodoc:
+ _segments.dup
+ end
+
+ ##
+ # A recommended version for use with a >= Requirement.
+
+ def approximate_recommendation
+ segments = self.segments
+
+ segments.pop while segments.any? {|s| String === s }
+ segments.pop while segments.size > 2
+ segments.push 0 while segments.size < 2
+
+ recommendation = ">= #{segments.join(".")}"
+ recommendation += ".a" if prerelease?
+ recommendation
+ end
+
+ ##
+ # Compares this version with +other+ returning -1, 0, or 1 if the
+ # other version is larger, the same, or smaller than this
+ # one. +other+ must be an instance of Gem::Version, comparing with
+ # other types may raise an exception.
+
+ def <=>(other)
+ if Gem::Version === other
+ # Fast path for comparison when available.
+ if @sort_key && other.sort_key
+ return @sort_key <=> other.sort_key
+ end
+
+ return 0 if @version == other.version || canonical_segments == other.canonical_segments
+
+ lhsegments = canonical_segments
+ rhsegments = other.canonical_segments
+
+ lhsize = lhsegments.size
+ rhsize = rhsegments.size
+ limit = (lhsize > rhsize ? rhsize : lhsize)
+
+ i = 0
+
+ while i < limit
+ lhs = lhsegments[i]
+ rhs = rhsegments[i]
+ i += 1
+
+ next if lhs == rhs
+ return -1 if String === lhs && Numeric === rhs
+ return 1 if Numeric === lhs && String === rhs
+
+ return lhs <=> rhs
+ end
+
+ lhs = lhsegments[i]
+
+ if lhs.nil?
+ rhs = rhsegments[i]
+
+ while i < rhsize
+ return 1 if String === rhs
+ return -1 unless rhs.zero?
+ rhs = rhsegments[i += 1]
+ end
+ else
+ while i < lhsize
+ return -1 if String === lhs
+ return 1 unless lhs.zero?
+ lhs = lhsegments[i += 1]
+ end
+ end
+
+ 0
+ elsif String === other
+ return unless self.class.correct?(other)
+ self <=> self.class.new(other)
+ end
+ end
+
+ # remove trailing zeros segments before first letter or at the end of the version
+ def canonical_segments
+ @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
+
+ attr_reader :sort_key # :nodoc:
+
+ def compute_sort_key
+ return if prerelease?
+
+ segments = canonical_segments
+ return if segments.size > 5
+
+ key = 0
+ RADIX_OPT.each_with_index do |radix, i|
+ seg = segments.fetch(i, 0)
+ return nil if seg >= radix
+ key = key * radix + seg
+ end
+
+ key
+ end
+
+ def _segments
+ # segments is lazy so it can pick up version values that come from
+ # old marshaled versions, which don't go through marshal_load.
+ # since this version object is cached in @@all, its @segments should be frozen
+ @segments ||= partition_segments(@version)
+ end
+
+ 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
new file mode 100644
index 0000000000..7910fd3d1b
--- /dev/null
+++ b/lib/rubygems/version_option.rb
@@ -0,0 +1,80 @@
+# 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 "../rubygems"
+
+##
+# Mixin methods for --version and --platform Gem::Command options.
+
+module Gem::VersionOption
+ ##
+ # Add the --platform option to the option parser.
+
+ def add_platform_option(task = command, *wrap)
+ Gem::OptionParser.accept Gem::Platform do |value|
+ if value == Gem::Platform::RUBY
+ value
+ else
+ Gem::Platform.new value
+ end
+ end
+
+ add_option("--platform PLATFORM", Gem::Platform,
+ "Specify the platform of gem to #{task}", *wrap) do |value, options|
+ unless options[:added_platform]
+ Gem.platforms = [Gem::Platform::RUBY]
+ options[:added_platform] = true
+ end
+
+ Gem.platforms << value unless Gem.platforms.include? value
+ end
+ end
+
+ ##
+ # Add the --prerelease option to the option parser.
+
+ def add_prerelease_option(*wrap)
+ add_option("--[no-]prerelease",
+ "Allow prerelease versions of a gem", *wrap) do |value, options|
+ options[:prerelease] = value
+ options[:explicit_prerelease] = true
+ end
+ end
+
+ ##
+ # Add the --version option to the option parser.
+
+ def add_version_option(task = command, *wrap)
+ Gem::OptionParser.accept Gem::Requirement do |value|
+ Gem::Requirement.new(*value.split(/\s*,\s*/))
+ end
+
+ add_option("-v", "--version VERSION", Gem::Requirement,
+ "Specify version of gem to #{task}", *wrap) do |value, options|
+ # Allow handling for multiple --version operators
+ if options[:version] && !options[:version].none?
+ options[:version].concat([value])
+ else
+ options[:version] = value
+ end
+
+ explicit_prerelease_set = !options[:explicit_prerelease].nil?
+ options[:explicit_prerelease] = false unless explicit_prerelease_set
+
+ options[:prerelease] = value.prerelease? unless
+ options[:explicit_prerelease]
+ end
+ end
+
+ ##
+ # Extract platform given on the command line
+
+ def get_platform_from_requirements(requirements)
+ Gem.platforms[1].to_s if requirements.key? :added_platform
+ end
+end
diff --git a/lib/rubygems/win_platform.rb b/lib/rubygems/win_platform.rb
new file mode 100644
index 0000000000..10556871b2
--- /dev/null
+++ b/lib/rubygems/win_platform.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require "rbconfig"
+
+module Gem
+ ##
+ # An Array of Regexps that match windows Ruby platforms.
+
+ WIN_PATTERNS = [
+ /bccwin/i,
+ /djgpp/i,
+ /mingw/i,
+ /mswin/i,
+ /wince/i,
+ ].freeze
+
+ @@win_platform = nil
+
+ ##
+ # Is this a windows platform?
+
+ def self.win_platform?
+ if @@win_platform.nil?
+ ruby_platform = RbConfig::CONFIG["host_os"]
+ @@win_platform = !WIN_PATTERNS.find {|r| ruby_platform =~ r }.nil?
+ end
+
+ @@win_platform
+ end
+end
diff --git a/lib/rubygems/yaml_serializer.rb b/lib/rubygems/yaml_serializer.rb
new file mode 100644
index 0000000000..b2547b136b
--- /dev/null
+++ b/lib/rubygems/yaml_serializer.rb
@@ -0,0 +1,845 @@
+# frozen_string_literal: true
+
+unless defined?(Psych::VERSION)
+ module Psych
+ class Exception < ::RuntimeError; end
+ class SyntaxError < Exception; end
+ class DisallowedClass < Exception; end
+ class BadAlias < Exception; end
+ class AliasesNotEnabled < BadAlias; end
+ end
+end
+
+module Gem
+ module YAMLSerializer
+ Scalar = Struct.new(:value, :tag, :anchor, keyword_init: true)
+
+ Mapping = Struct.new(:pairs, :tag, :anchor, keyword_init: true) do
+ def initialize(pairs: [], tag: nil, anchor: nil)
+ super
+ end
+ end
+
+ Sequence = Struct.new(:items, :tag, :anchor, keyword_init: true) do
+ def initialize(items: [], tag: nil, anchor: nil)
+ super
+ end
+ end
+
+ AliasRef = Struct.new(:name, keyword_init: true)
+
+ class Parser
+ MAPPING_KEY_RE = /^((?:[^#:]|:[^ ])+):(?:[ ]+(.*))?$/
+ MAX_NESTING_DEPTH = 1_000
+
+ def initialize(source)
+ @lines = source.split("\n")
+ @anchors = {}
+ @depth = 0
+ strip_document_prefix
+ end
+
+ def parse
+ return nil if @lines.empty?
+
+ root = nil
+ while @lines.any?
+ before = @lines.size
+ node = parse_node(-1)
+ @lines.shift if @lines.size == before && @lines.any?
+
+ if root.is_a?(Mapping) && node.is_a?(Mapping)
+ root.pairs.concat(node.pairs)
+ elsif root.nil?
+ root = node
+ end
+ end
+ root
+ end
+
+ private
+
+ def strip_document_prefix
+ return if @lines.empty?
+ return unless @lines[0]&.start_with?("---")
+
+ if @lines[0].strip == "---"
+ @lines.shift
+ else
+ @lines[0] = @lines[0].sub(/^---\s*/, "")
+ end
+ end
+
+ def parse_node(base_indent)
+ @depth += 1
+ raise_max_nesting! if @depth > MAX_NESTING_DEPTH
+
+ skip_blank_and_comments
+ return nil if @lines.empty?
+
+ line = @lines[0]
+ stripped = line.lstrip
+ indent = line.size - stripped.size
+ return nil if indent < base_indent
+
+ return parse_alias_ref if stripped.start_with?("*")
+
+ anchor = consume_anchor
+
+ if anchor
+ line = @lines[0]
+ stripped = line.lstrip
+ end
+
+ if stripped.start_with?("- ") || stripped == "-"
+ parse_sequence(indent, anchor)
+ elsif stripped.start_with?("\"") && stripped.end_with?("\"")
+ # We don't need to care about the following case here:
+ # 1. "value with comment" # ...
+ # 2. "key": "value"
+ #
+ # 1. must not happen because YAMLSerializer doesn't emit any
+ # comment. YAMLSerializer parses only YAML that is generated
+ # by YAMLSerializer.
+ #
+ # 2. must not happen because #parse_node isn't used non
+ # top-level mapping. Non top-level mapping always uses
+ # #parse_mapping. Top-level mapping never use the '"key":
+ # "value"' form because all top-level keys
+ # ("!ruby/object:Gem::Specification"'s keys) are known and
+ # #emit_specification doesn't quote anything.
+ parse_plain_scalar(indent, anchor)
+ elsif stripped.start_with?("'") && stripped.end_with?("'")
+ # See also the above note for double quotation.
+ parse_plain_scalar(indent, anchor)
+ elsif stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:")
+ parse_mapping(indent, anchor)
+ elsif stripped.start_with?("!ruby/object:")
+ parse_tagged_node(indent, anchor)
+ elsif stripped.start_with?("|")
+ modifier = stripped[1..].to_s.strip
+ @lines.shift
+ register_anchor(anchor, Scalar.new(value: parse_block_scalar(indent, modifier)))
+ else
+ parse_plain_scalar(indent, anchor)
+ end
+ ensure
+ @depth -= 1
+ end
+
+ def parse_sequence(indent, anchor)
+ items = []
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless line.size - stripped.size == indent &&
+ (stripped.start_with?("- ") || stripped == "-")
+ content = @lines.shift.lstrip[1..].strip
+ item_anchor, content = extract_item_anchor(content)
+ item = parse_sequence_item(content, indent)
+ items << register_anchor(item_anchor, item)
+ end
+ register_anchor(anchor, Sequence.new(items: items))
+ end
+
+ def parse_sequence_item(content, indent)
+ if content.start_with?("*")
+ parse_inline_alias(content)
+ elsif content.empty?
+ @lines.any? && current_indent > indent ? parse_node(indent) : nil
+ elsif content.start_with?("!ruby/object:")
+ parse_tagged_content(content.strip, indent)
+ elsif content.start_with?("!binary ")
+ parse_binary_value(content, indent)
+ elsif content.start_with?("-")
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
+ parse_node(indent)
+ elsif content =~ MAPPING_KEY_RE && !content.start_with?("!ruby/object:")
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
+ parse_node(indent)
+ elsif content.start_with?("|")
+ Scalar.new(value: parse_block_scalar(indent, content[1..].to_s.strip))
+ else
+ parse_inline_scalar(content, indent)
+ end
+ end
+
+ def parse_mapping(indent, anchor)
+ pairs = []
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless line.size - stripped.size == indent &&
+ stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:")
+ key = $1.strip
+ @lines.shift
+ val = strip_comment($2.to_s.strip)
+
+ key = decode_binary_tag(key) if key.start_with?("!binary ")
+
+ val_anchor, val = consume_value_anchor(val)
+ value = parse_mapping_value(val, indent)
+ value = register_anchor(val_anchor, value) if val_anchor
+
+ pairs << [Scalar.new(value: key), value]
+ end
+ register_anchor(anchor, Mapping.new(pairs: pairs))
+ end
+
+ def parse_mapping_value(val, indent)
+ if val.start_with?("*")
+ parse_inline_alias(val)
+ elsif val.start_with?("!ruby/object:")
+ parse_tagged_content(val.strip, indent)
+ elsif val.start_with?("!binary ")
+ parse_binary_value(val, indent)
+ elsif val.empty?
+ next_stripped = nil
+ next_indent = nil
+ if @lines.any?
+ next_stripped = @lines[0].lstrip
+ next_indent = @lines[0].size - next_stripped.size
+ end
+ if next_stripped &&
+ (next_stripped.start_with?("- ") || next_stripped == "-") &&
+ next_indent == indent
+ parse_node(indent)
+ else
+ parse_node(indent + 1)
+ end
+ elsif val == "[]"
+ Sequence.new
+ elsif val == "{}"
+ Mapping.new
+ elsif val.start_with?("|")
+ Scalar.new(value: parse_block_scalar(indent, val[1..].to_s.strip))
+ else
+ parse_inline_scalar(val, indent)
+ end
+ end
+
+ def parse_tagged_node(indent, anchor)
+ tag = @lines.shift.strip
+ nested = parse_node(indent)
+ apply_tag(nested, tag, anchor)
+ end
+
+ def parse_tagged_content(tag, indent)
+ nested = parse_node(indent)
+ apply_tag(nested, tag, nil)
+ end
+
+ def apply_tag(node, tag, anchor)
+ if node.is_a?(Mapping)
+ node.tag = tag
+ node.anchor = anchor
+ node
+ else
+ Mapping.new(pairs: [[Scalar.new(value: "value"), node]], tag: tag, anchor: anchor)
+ end
+ end
+
+ def parse_block_scalar(base_indent, modifier)
+ parts = []
+ block_indent = nil
+
+ while @lines.any?
+ line = @lines[0]
+ if line.strip.empty?
+ parts << "\n"
+ @lines.shift
+ else
+ line_indent = line.size - line.lstrip.size
+ break if line_indent <= base_indent
+ block_indent ||= line_indent
+ parts << @lines.shift[block_indent..].to_s << "\n"
+ end
+ end
+
+ res = parts.join
+ res.chomp! if modifier == "-" && res.end_with?("\n")
+ res
+ end
+
+ def parse_plain_scalar(indent, anchor)
+ result = coerce(@lines.shift.strip)
+ return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence)
+
+ while result.is_a?(String) && @lines.any? &&
+ !@lines[0].strip.empty? && current_indent > indent
+ result << " " << @lines.shift.strip
+ end
+ register_anchor(anchor, Scalar.new(value: result))
+ end
+
+ def parse_inline_scalar(val, indent)
+ result = coerce(val)
+ return result if result.is_a?(Mapping) || result.is_a?(Sequence)
+
+ while result.is_a?(String) && @lines.any? &&
+ !@lines[0].strip.empty? && current_indent > indent
+ result << " " << @lines.shift.strip
+ end
+ Scalar.new(value: result)
+ end
+
+ def coerce(val, depth = 0)
+ raise_max_nesting! if depth > MAX_NESTING_DEPTH
+
+ val = val.sub(/^! /, "") if val.start_with?("! ")
+
+ if val =~ /^"(.*)"$/
+ $1.gsub(/\\["nrt\\]/) do |m|
+ case m
+ when '\\"' then '"'
+ when "\\n" then "\n"
+ when "\\r" then "\r"
+ when "\\t" then "\t"
+ when "\\\\" then "\\"
+ end
+ end
+ elsif val =~ /^'(.*)'$/
+ $1.gsub(/''/, "'")
+ elsif val == "true"
+ true
+ elsif val == "false"
+ false
+ elsif ["~", "null"].include?(val)
+ nil
+ elsif val == "{}"
+ Mapping.new
+ elsif val =~ /^\[(.*)\]$/
+ inner = $1.strip
+ return Sequence.new if inner.empty?
+ items = inner.split(/\s*,\s*/).reject(&:empty?).map {|e| Scalar.new(value: coerce(e, depth + 1)) }
+ Sequence.new(items: items)
+ elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val)
+ begin
+ Time.new(val)
+ rescue ArgumentError
+ # date-only format like "2024-06-15" is not supported by Time.new
+ if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val)
+ Time.utc($1.to_i, $2.to_i, $3.to_i)
+ else
+ val
+ end
+ end
+ elsif /^-?\d+$/.match?(val)
+ val.to_i
+ else
+ val
+ end
+ end
+
+ def decode_binary_tag(str)
+ content = str.sub(/\A!binary\s+/, "")
+ content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/
+ content.unpack1("m")
+ end
+
+ def parse_binary_value(val, indent)
+ rest = val.sub(/\A!binary\s+/, "")
+ if rest.start_with?("|")
+ content = parse_block_scalar(indent, rest[1..].to_s.strip)
+ Scalar.new(value: content.unpack1("m"))
+ else
+ Scalar.new(value: decode_binary_tag(val))
+ end
+ end
+
+ def parse_alias_ref
+ AliasRef.new(name: @lines.shift.lstrip[1..].strip)
+ end
+
+ def parse_inline_alias(content)
+ AliasRef.new(name: content[1..].strip)
+ end
+
+ def current_indent
+ line = @lines[0]
+ line.size - line.lstrip.size
+ end
+
+ def consume_anchor
+ line = @lines[0]
+ stripped = line.lstrip
+ return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/
+
+ anchor = $1
+ @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "")
+ anchor
+ end
+
+ def extract_item_anchor(content)
+ return [nil, content] unless content =~ /^&(\S+)/
+
+ anchor = $1
+ [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")]
+ end
+
+ def consume_value_anchor(val)
+ return [nil, val] unless val =~ /^&(\S+)\s+/
+
+ anchor = $1
+ [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")]
+ end
+
+ def register_anchor(name, node)
+ if name
+ @anchors[name] = node
+ node.anchor = name if node.respond_to?(:anchor=)
+ end
+ node
+ end
+
+ def raise_max_nesting!
+ message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})"
+ if defined?(Psych::VERSION)
+ raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil)
+ else
+ raise Psych::SyntaxError, message
+ end
+ end
+
+ def skip_blank_and_comments
+ while @lines.any?
+ line = @lines[0]
+ stripped = line.lstrip
+ break unless stripped.empty? || stripped.start_with?("#")
+ @lines.shift
+ end
+ end
+
+ def strip_comment(val)
+ return val unless val.include?("#")
+ return val if val.lstrip.start_with?("#")
+
+ in_single = false
+ in_double = false
+ escape = false
+
+ val.each_char.with_index do |ch, i|
+ if escape
+ escape = false
+ next
+ end
+
+ if in_single
+ in_single = false if ch == "'"
+ elsif in_double
+ if ch == "\\"
+ escape = true
+ elsif ch == '"'
+ in_double = false
+ end
+ else
+ case ch
+ when "'" then in_single = true
+ when '"' then in_double = true
+ when "#" then return val[0...i].rstrip
+ end
+ end
+ end
+
+ val
+ end
+ end
+
+ class Builder
+ VALID_OPS = %w[= != > < >= <= ~>].freeze
+ ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze
+ MAX_ALIAS_RESOLUTIONS = 1_000
+
+ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true)
+ @permitted_classes = permitted_classes.map {|c| "!ruby/object:#{c}" }
+ @permitted_symbols = permitted_symbols
+ @aliases = aliases
+ @anchor_values = {}
+ @alias_count = 0
+ end
+
+ def build(node)
+ return nil if node.nil?
+
+ result = build_node(node)
+
+ if result.is_a?(Hash) && result[:tag] == "!ruby/object:Gem::Specification"
+ build_specification(result)
+ else
+ result
+ end
+ end
+
+ private
+
+ def build_node(node)
+ case node
+ when nil then nil
+ when AliasRef then resolve_alias(node)
+ when Scalar then store_anchor(node.anchor, node.value)
+ when Mapping then build_mapping(node)
+ when Sequence then store_anchor(node.anchor, node.items.map {|item| build_node(item) })
+ else node # already a Ruby object
+ end
+ end
+
+ def resolve_alias(node)
+ raise Psych::AliasesNotEnabled unless @aliases
+ @alias_count += 1
+ if @alias_count > MAX_ALIAS_RESOLUTIONS
+ raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})"
+ end
+ unless @anchor_values.key?(node.name)
+ klass = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias
+ raise klass, "An alias referenced an unknown anchor: #{node.name}"
+ end
+ @anchor_values.fetch(node.name)
+ end
+
+ def store_anchor(name, value)
+ @anchor_values[name] = value if name
+ value
+ end
+
+ def build_mapping(node)
+ validate_tag!(node.tag) if node.tag
+
+ result = case node.tag
+ when "!ruby/object:Gem::Version"
+ build_version(node)
+ when "!ruby/object:Gem::Platform"
+ build_platform(node)
+ when "!ruby/object:Gem::Requirement", "!ruby/object:Gem::Version::Requirement"
+ build_requirement(node)
+ when "!ruby/object:Gem::Dependency"
+ build_dependency(node)
+ when nil
+ build_hash(node)
+ when "!ruby/object:Gem::Specification"
+ hash = build_hash(node)
+ hash[:tag] = node.tag
+ hash
+ else
+ raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}"
+ end
+
+ store_anchor(node.anchor, result)
+ end
+
+ def build_hash(node)
+ result = {}
+ node.pairs.each do |key_node, value_node|
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
+ value = build_node(value_node)
+
+ if ARRAY_FIELDS.include?(key)
+ value = normalize_array_field(value)
+ end
+
+ result[key] = value
+ end
+ result
+ end
+
+ def build_version(node)
+ hash = pairs_to_hash(node)
+ Gem::Version.new((hash["version"] || hash["value"]).to_s)
+ end
+
+ PLATFORM_FIELDS = %w[cpu os version].freeze
+ PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze
+
+ def build_platform(node)
+ hash = pairs_to_hash(node)
+ if (hash.keys & PLATFORM_FIELDS).any?
+ Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]])
+ elsif hash["value"].is_a?(Array)
+ # Malformed platform (e.g. sequence instead of mapping).
+ # Return the raw value so yaml_initialize handles it like Psych does.
+ hash["value"]
+ else
+ plat = Gem::Platform.allocate
+ hash.each do |k, v|
+ plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k)
+ end
+ plat
+ end
+ end
+
+ def build_requirement(node)
+ r = Gem::Requirement.allocate
+ hash = pairs_to_hash(node)
+ reqs = hash["requirements"] || hash["value"]
+
+ if reqs.is_a?(Array) && !reqs.empty?
+ safe_reqs = []
+ reqs.each do |item|
+ if item.is_a?(Array) && item.size == 2
+ op = item[0].to_s
+ ver = item[1]
+ if VALID_OPS.include?(op)
+ version_obj = ver.is_a?(Gem::Version) ? ver : Gem::Version.new(ver.to_s)
+ safe_reqs << [op, version_obj]
+ end
+ elsif item.is_a?(String)
+ parsed = Gem::Requirement.parse(item)
+ safe_reqs << parsed
+ end
+ rescue Gem::Requirement::BadRequirementError, Gem::Version::BadVersionError
+ # Skip malformed items silently
+ end
+ reqs = safe_reqs unless safe_reqs.empty?
+ end
+
+ r.instance_variable_set(:@requirements, reqs)
+ r
+ end
+
+ def build_dependency(node)
+ hash = pairs_to_hash(node)
+ d = Gem::Dependency.allocate
+ d.instance_variable_set(:@name, hash["name"])
+
+ d.instance_variable_set(:@requirement, hash["requirement"] || hash["version_requirements"])
+
+ raw_type = hash["type"]
+ if raw_type
+ name = raw_type.to_s.sub(/^:/, "")
+ validate_symbol!(name)
+ type = name.to_sym
+ else
+ type = :runtime
+ end
+ d.instance_variable_set(:@type, type)
+
+ d.instance_variable_set(:@prerelease, ["true", true].include?(hash["prerelease"]))
+ d.instance_variable_set(:@version_requirements, d.instance_variable_get(:@requirement))
+ d
+ end
+
+ def build_specification(hash)
+ spec = Gem::Specification.allocate
+
+ normalize_specification_version!(hash)
+ normalize_array_fields!(hash)
+
+ spec.yaml_initialize("!ruby/object:Gem::Specification", hash)
+ spec
+ end
+
+ def pairs_to_hash(node)
+ result = {}
+ node.pairs.each do |key_node, value_node|
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
+ result[key] = build_node(value_node)
+ end
+ result
+ end
+
+ def validate_tag!(tag)
+ return if @permitted_classes.include?(tag)
+ raise_disallowed_class!(tag)
+ end
+
+ def raise_disallowed_class!(tag)
+ if defined?(Psych::VERSION)
+ raise Psych::DisallowedClass.new("load", tag)
+ else
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{tag}"
+ end
+ end
+
+ def validate_symbol!(name)
+ return if @permitted_symbols.empty? || @permitted_symbols.include?(name)
+
+ label = ":#{name}"
+ if defined?(Psych::VERSION)
+ raise Psych::DisallowedClass.new("load", label)
+ else
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{label}"
+ end
+ end
+
+ def normalize_specification_version!(hash)
+ val = hash["specification_version"]
+ return unless val && !val.is_a?(Integer)
+ hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val)
+ end
+
+ def normalize_array_fields!(hash)
+ ARRAY_FIELDS.each do |field|
+ hash[field] = normalize_array_field(hash[field]) if hash[field]
+ end
+ end
+
+ def normalize_array_field(value)
+ if value.is_a?(Hash)
+ value.values.flatten.compact
+ elsif !value.is_a?(Array) && value
+ [value].flatten.compact
+ else
+ value
+ end
+ end
+ end
+
+ class Emitter
+ def emit(obj)
+ "---#{emit_node(obj, 0)}"
+ end
+
+ private
+
+ def emit_node(obj, indent, quote: false)
+ case obj
+ when Gem::Specification then emit_specification(obj, indent)
+ when Gem::Version then emit_version(obj, indent)
+ when Gem::Platform then emit_platform(obj, indent)
+ when Gem::Requirement then emit_requirement(obj, indent)
+ when Gem::Dependency then emit_dependency(obj, indent)
+ when Hash then emit_hash(obj, indent)
+ when Array then emit_array(obj, indent)
+ when Time then emit_time(obj)
+ when String then emit_string(obj, indent, quote: quote)
+ when NilClass
+ "\n"
+ when Numeric, Symbol, TrueClass, FalseClass
+ " #{obj.inspect}\n"
+ else
+ " #{obj.to_s.inspect}\n"
+ end
+ end
+
+ def emit_specification(spec, indent)
+ parts = [" !ruby/object:Gem::Specification\n"]
+ parts << "#{pad(indent)}name:#{emit_node(spec.name, indent + 2)}"
+ parts << "#{pad(indent)}version:#{emit_node(spec.version, indent + 2)}"
+ parts << "#{pad(indent)}platform: #{spec.platform}\n"
+ if spec.platform.to_s != spec.original_platform.to_s
+ parts << "#{pad(indent)}original_platform: #{spec.original_platform}\n"
+ end
+
+ attributes = Gem::Specification.attribute_names.map(&:to_s).sort - %w[name version platform]
+ attributes.each do |name|
+ val = spec.instance_variable_get("@#{name}")
+ next if val.nil?
+ parts << "#{pad(indent)}#{name}:#{emit_node(val, indent + 2)}"
+ end
+
+ res = parts.join
+ res << "\n" unless res.end_with?("\n")
+ res
+ end
+
+ def emit_version(ver, indent)
+ " !ruby/object:Gem::Version\n" \
+ "#{pad(indent)}version: #{emit_node(ver.version.to_s, indent + 2).lstrip}"
+ end
+
+ def emit_platform(plat, indent)
+ " !ruby/object:Gem::Platform\n" \
+ "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \
+ "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \
+ "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}"
+ end
+
+ def emit_requirement(req, indent)
+ " !ruby/object:Gem::Requirement\n" \
+ "#{pad(indent)}requirements:#{emit_node(req.requirements, indent + 2)}"
+ end
+
+ def emit_dependency(dep, indent)
+ [
+ " !ruby/object:Gem::Dependency\n",
+ "#{pad(indent)}name: #{emit_node(dep.name, indent + 2).lstrip}",
+ "#{pad(indent)}requirement:#{emit_node(dep.requirement, indent + 2)}",
+ "#{pad(indent)}type: #{emit_node(dep.type, indent + 2).lstrip}",
+ "#{pad(indent)}prerelease: #{emit_node(dep.prerelease?, indent + 2).lstrip}",
+ "#{pad(indent)}version_requirements:#{emit_node(dep.requirement, indent + 2)}",
+ ].join
+ end
+
+ def emit_hash(hash, indent)
+ if hash.empty?
+ " {}\n"
+ else
+ parts = ["\n"]
+ hash.each do |k, v|
+ is_symbol = k.is_a?(Symbol) || (k.is_a?(String) && k.start_with?(":"))
+ key_str = k.is_a?(Symbol) ? k.inspect : k.to_s
+ parts << "#{pad(indent)}#{key_str}:#{emit_node(v, indent + 2, quote: is_symbol)}"
+ end
+ parts.join
+ end
+ end
+
+ def emit_array(arr, indent)
+ if arr.empty?
+ " []\n"
+ else
+ parts = ["\n"]
+ arr.each do |v|
+ parts << "#{pad(indent)}-#{emit_node(v, indent + 2)}"
+ end
+ parts.join
+ end
+ end
+
+ def emit_time(time)
+ " #{time.utc.strftime("%Y-%m-%d %H:%M:%S.%N Z")}\n"
+ end
+
+ def emit_string(str, indent, quote: false)
+ if str.include?("\n")
+ emit_block_scalar(str, indent)
+ elsif needs_quoting?(str, quote)
+ " #{str.to_s.inspect}\n"
+ else
+ " #{str}\n"
+ end
+ end
+
+ def emit_block_scalar(str, indent)
+ parts = [str.end_with?("\n") ? " |\n" : " |-\n"]
+ str.each_line do |line|
+ parts << "#{pad(indent + 2)}#{line}"
+ end
+ res = parts.join
+ res << "\n" unless res.end_with?("\n")
+ res
+ end
+
+ def needs_quoting?(str, quote)
+ quote || str.empty? ||
+ str =~ /^[!*&:@%$]/ || str =~ /^-?\d+(\.\d+)?$/ || str =~ /^[<>=-]/ ||
+ str == "true" || str == "false" || str == "nil" ||
+ str.include?(":") || str.include?("#") || str.include?("[") || str.include?("]") ||
+ str.include?("{") || str.include?("}") || str.include?(",")
+ end
+
+ def pad(indent)
+ " " * indent
+ end
+ end
+
+ module_function
+
+ def dump(obj)
+ Emitter.new.emit(obj)
+ end
+
+ def load(str, permitted_classes: [], permitted_symbols: [], aliases: true)
+ raise TypeError, "no implicit conversion of nil into String" if str.nil?
+ return nil if str.empty?
+
+ ast = Parser.new(str).parse
+ return nil if ast.nil?
+
+ Builder.new(
+ permitted_classes: permitted_classes,
+ permitted_symbols: permitted_symbols,
+ aliases: aliases
+ ).build(ast)
+ end
+ end
+end