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/builder.rb99
-rw-r--r--lib/rubygems/bundler_version_finder.rb135
-rw-r--r--lib/rubygems/ci_detector.rb75
-rw-r--r--lib/rubygems/command.rb328
-rw-r--r--lib/rubygems/command_manager.rb226
-rw-r--r--lib/rubygems/commands/build_command.rb119
-rw-r--r--lib/rubygems/commands/cert_command.rb391
-rw-r--r--lib/rubygems/commands/check_command.rb122
-rw-r--r--lib/rubygems/commands/cleanup_command.rb184
-rw-r--r--lib/rubygems/commands/contents_command.rb193
-rw-r--r--lib/rubygems/commands/dependency_command.rb214
-rw-r--r--lib/rubygems/commands/environment_command.rb190
-rw-r--r--lib/rubygems/commands/exec_command.rb259
-rw-r--r--lib/rubygems/commands/fetch_command.rb88
-rw-r--r--lib/rubygems/commands/generate_index_command.rb151
-rw-r--r--lib/rubygems/commands/help_command.rb338
-rw-r--r--lib/rubygems/commands/info_command.rb38
-rw-r--r--lib/rubygems/commands/install_command.rb209
-rw-r--r--lib/rubygems/commands/list_command.rb39
-rw-r--r--lib/rubygems/commands/lock_command.rb29
-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.rb31
-rw-r--r--lib/rubygems/commands/owner_command.rb91
-rw-r--r--lib/rubygems/commands/pristine_command.rb227
-rw-r--r--lib/rubygems/commands/push_command.rb169
-rw-r--r--lib/rubygems/commands/query_command.rb263
-rw-r--r--lib/rubygems/commands/rdoc_command.rb81
-rw-r--r--lib/rubygems/commands/rebuild_command.rb261
-rw-r--r--lib/rubygems/commands/search_command.rb40
-rw-r--r--lib/rubygems/commands/server_command.rb96
-rw-r--r--lib/rubygems/commands/setup_command.rb642
-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.rb356
-rw-r--r--lib/rubygems/commands/specification_command.rb115
-rw-r--r--lib/rubygems/commands/stale_command.rb22
-rw-r--r--lib/rubygems/commands/uninstall_command.rb190
-rw-r--r--lib/rubygems/commands/unpack_command.rb124
-rw-r--r--lib/rubygems/commands/update_command.rb362
-rw-r--r--lib/rubygems/commands/which_command.rb52
-rw-r--r--lib/rubygems/commands/yank_command.rb99
-rw-r--r--lib/rubygems/config_file.rb561
-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/custom_require.rb68
-rw-r--r--lib/rubygems/defaults.rb308
-rw-r--r--lib/rubygems/dependency.rb190
-rw-r--r--lib/rubygems/dependency_installer.rb394
-rw-r--r--lib/rubygems/dependency_list.rb104
-rw-r--r--lib/rubygems/deprecate.rb167
-rw-r--r--lib/rubygems/doc_manager.rb243
-rw-r--r--lib/rubygems/doctor.rb132
-rw-r--r--lib/rubygems/errors.rb186
-rw-r--r--lib/rubygems/exceptions.rb180
-rw-r--r--lib/rubygems/ext.rb16
-rw-r--r--lib/rubygems/ext/build_error.rb9
-rw-r--r--lib/rubygems/ext/builder.rb267
-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.rb21
-rw-r--r--lib/rubygems/ext/ext_conf_builder.rb75
-rw-r--r--lib/rubygems/ext/rake_builder.rb44
-rw-r--r--lib/rubygems/format.rb82
-rw-r--r--lib/rubygems/gem_openssl.rb90
-rw-r--r--lib/rubygems/gem_path_searcher.rb172
-rw-r--r--lib/rubygems/gem_runner.rb80
-rw-r--r--lib/rubygems/gemcutter_utilities.rb392
-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/indexer.rb644
-rw-r--r--lib/rubygems/install_message.rb13
-rw-r--r--lib/rubygems/install_update_options.rb226
-rw-r--r--lib/rubygems/installer.rb1025
-rw-r--r--lib/rubygems/installer_test_case.rb142
-rw-r--r--lib/rubygems/installer_uninstaller_utils.rb27
-rw-r--r--lib/rubygems/local_remote_options.rb70
-rw-r--r--lib/rubygems/mock_gem_ui.rb71
-rw-r--r--lib/rubygems/name_tuple.rb125
-rw-r--r--lib/rubygems/old_format.rb153
-rw-r--r--lib/rubygems/openssl.rb7
-rw-r--r--lib/rubygems/package.rb794
-rw-r--r--lib/rubygems/package/digest_io.rb63
-rw-r--r--lib/rubygems/package/f_sync_dir.rb23
-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.rb275
-rw-r--r--lib/rubygems/package/tar_input.rb231
-rw-r--r--lib/rubygems/package/tar_output.rb146
-rw-r--r--lib/rubygems/package/tar_reader.rb79
-rw-r--r--lib/rubygems/package/tar_reader/entry.rb151
-rw-r--r--lib/rubygems/package/tar_test_case.rb137
-rw-r--r--lib/rubygems/package/tar_writer.rb179
-rw-r--r--lib/rubygems/package_task.rb33
-rw-r--r--lib/rubygems/path_support.rb73
-rw-r--r--lib/rubygems/platform.rb369
-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.rb471
-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/require_paths_builder.rb18
-rw-r--r--lib/rubygems/requirement.rb275
-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.rb899
-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/server.rb832
-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_index.rb406
-rw-r--r--lib/rubygems/source_list.rb182
-rw-r--r--lib/rubygems/spec_fetcher.rb387
-rw-r--r--lib/rubygems/specification.rb2520
-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/test_case.rb877
-rw-r--r--lib/rubygems/test_utilities.rb160
-rw-r--r--lib/rubygems/text.rb89
-rw-r--r--lib/rubygems/uninstaller.rb353
-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.rb439
-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.rb139
-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.rb487
-rw-r--r--lib/rubygems/version_option.rb47
-rw-r--r--lib/rubygems/win_platform.rb30
-rw-r--r--lib/rubygems/yaml_serializer.rb845
258 files changed, 47570 insertions, 10785 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/builder.rb b/lib/rubygems/builder.rb
deleted file mode 100644
index a8e96dd90c..0000000000
--- a/lib/rubygems/builder.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-require 'rubygems/user_interaction'
-
-Gem.load_yaml
-
-require 'rubygems/package'
-
-##
-# The Builder class processes RubyGem specification files
-# to produce a .gem file.
-
-class Gem::Builder
-
- include Gem::UserInteraction
-
- ##
- # Constructs a builder instance for the provided specification
- #
- # spec:: [Gem::Specification] The specification instance
-
- def initialize(spec)
- @spec = spec
- end
-
- ##
- # Builds the gem from the specification. Returns the name of the file
- # written.
-
- def build
- @spec.mark_version
- @spec.validate
- @signer = sign
- write_package
- say success if Gem.configuration.verbose
- File.basename @spec.cache_file
- end
-
- def success
- <<-EOM
- Successfully built RubyGem
- Name: #{@spec.name}
- Version: #{@spec.version}
- File: #{File.basename @spec.cache_file}
-EOM
- end
-
- private
-
- ##
- # If the signing key was specified, then load the file, and swap to the
- # public key (TODO: we should probably just omit the signing key in favor of
- # the signing certificate, but that's for the future, also the signature
- # algorithm should be configurable)
-
- def sign
- signer = nil
-
- if @spec.respond_to?(:signing_key) and @spec.signing_key then
- require 'rubygems/security'
-
- signer = Gem::Security::Signer.new @spec.signing_key, @spec.cert_chain
- @spec.signing_key = nil
- @spec.cert_chain = signer.cert_chain.map { |cert| cert.to_s }
- end
-
- signer
- end
-
- def write_package
- file_name = File.basename @spec.cache_file
- open file_name, 'wb' do |gem_io|
- Gem::Package.open gem_io, 'w', @signer do |pkg|
- yaml = @spec.to_yaml
- pkg.metadata = yaml
-
- @spec.files.each do |file|
- next if File.directory?(file)
- next if file == file_name # Don't add gem onto itself
-
- stat = File.stat(file)
- mode = stat.mode & 0777
- size = stat.size
-
- pkg.add_file_simple file, mode, size do |tar_io|
- tar_io.write open(file, "rb") { |f| f.read }
- end
- end
- end
- 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
index 49253ef56b..d38363f293 100644
--- a/lib/rubygems/command.rb
+++ b/lib/rubygems/command.rb
@@ -1,23 +1,27 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'optparse'
-require 'rubygems/user_interaction'
+require_relative "vendored_optparse"
+require_relative "requirement"
+require_relative "user_interaction"
##
# Base class for all Gem commands. When creating a new gem command, define
-# #new, #execute, #arguments, #defaults_str, #description and #usage
+# #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.
@@ -71,7 +75,7 @@ class Gem::Command
when Array
@extra_args = value
when String
- @extra_args = value.split
+ @extra_args = value.split(" ")
end
end
@@ -88,7 +92,7 @@ class Gem::Command
# array or a string to be split on white space.
def self.add_specific_extra_args(cmd,args)
- args = args.split(/\s+/) if args.kind_of? String
+ args = args.split(/\s+/) if args.is_a? String
specific_extra_args_hash[cmd] = args
end
@@ -113,13 +117,14 @@ class Gem::Command
# Unhandled arguments (gem names, files, etc.) are left in
# <tt>options[:args]</tt>.
- def initialize(command, summary=nil, defaults={})
+ def initialize(command, summary = nil, defaults = {})
@command = command
@summary = summary
@program_name = "gem #{command}"
@defaults = defaults
@options = defaults.dup
- @option_groups = Hash.new { |h,k| h[k] = [] }
+ @option_groups = Hash.new {|h,k| h[k] = [] }
+ @deprecated_options = { command => {} }
@parser = nil
@when_invoked = nil
end
@@ -147,18 +152,27 @@ class Gem::Command
##
# 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}"
- def show_lookup_failure(gem_name, version, errors, domain)
- if errors and !errors.empty?
- alert_error "Could not find a valid gem '#{gem_name}' (#{version}), here is why:"
- errors.each { |x| say " #{x.wordy}" }
+ if errors && !errors.empty?
+ msg << ", here is why:\n"
+ errors.each {|x| msg << " #{x.wordy}\n" }
else
- alert_error "Could not find a valid gem '#{gem_name}' (#{version}) in any repository"
+ if required_by && gem != required_by
+ msg << " (required by #{required_by}) in any repository"
+ else
+ msg << " in any repository"
+ end
end
- unless domain == :local then # HACK
- suggestions = Gem::SpecFetcher.fetcher.suggest_gems_from_name gem_name
+ 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
@@ -171,12 +185,31 @@ class Gem::Command
def get_all_gem_names
args = options[:args]
- if args.nil? or args.empty? then
+ if args.nil? || args.empty?
raise Gem::CommandLineError,
"Please specify at least one gem name (e.g. gem build GEMNAME)"
end
- args.select { |arg| arg !~ /^-/ }
+ args.reject {|arg| arg.start_with?("-") }
+ end
+
+ ##
+ # 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
##
@@ -186,14 +219,14 @@ class Gem::Command
def get_one_gem_name
args = options[:args]
- if args.nil? or args.empty? then
+ 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 then
+ if args.size > 1
raise Gem::CommandLineError,
- "Too many gem names (#{args.join(', ')}); please specify only one"
+ "Too many gem names (#{args.join(", ")}); please specify only one"
end
args.first
@@ -268,15 +301,35 @@ class Gem::Command
# 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
- if options[:help] then
+ 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 then
+ elsif @when_invoked
@when_invoked.call options
else
execute
end
+ ensure
+ if ui
+ self.ui = old_ui
+ ui.close
+ end
end
##
@@ -294,7 +347,7 @@ class Gem::Command
##
# Add a command-line option and handler to the command.
#
- # See OptionParser#make_switch for an explanation of +opts+.
+ # See Gem::OptionParser#make_switch for an explanation of +opts+.
#
# +handler+ will be called with two values, the value of the argument and
# the options hash.
@@ -305,6 +358,8 @@ class Gem::Command
def add_option(*opts, &handler) # :yields: value, options
group_name = Symbol === opts.first ? opts.shift : :options
+ raise "Do not pass an empty string in opts" if opts.include?("")
+
@option_groups[group_name] << [opts, handler]
end
@@ -313,7 +368,49 @@ class Gem::Command
def remove_option(name)
@option_groups.each do |_, option_list|
- option_list.reject! { |args, _| args.any? { |x| x =~ /^#{name}/ } }
+ 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
@@ -323,19 +420,17 @@ class Gem::Command
def merge_options(new_options)
@options = @defaults.clone
- new_options.each do |k,v| @options[k] = v end
+ new_options.each {|k,v| @options[k] = v }
end
##
# True if the command handles the given argument list.
def handles?(args)
- begin
- parser.parse!(args.dup)
- return true
- rescue
- return false
- end
+ parser.parse!(args.dup)
+ true
+ rescue StandardError
+ false
end
##
@@ -344,7 +439,8 @@ class Gem::Command
def handle_options(args)
args = add_extra_args(args)
- @options = @defaults.clone
+ check_deprecated_options(args)
+ @options = Marshal.load Marshal.dump @defaults # deep copy
parser.parse!(args)
@options[:args] = args
end
@@ -361,7 +457,7 @@ class Gem::Command
until extra.empty? do
ex = []
ex << extra.shift
- ex << extra.shift if extra.first.to_s =~ /^[^-]/
+ ex << extra.shift if /^[^-]/.match?(extra.first.to_s)
result << ex if handles?(ex)
end
@@ -370,83 +466,106 @@ class Gem::Command
result
end
- private
+ def deprecated?
+ false
+ end
- ##
- # Create on demand parser.
+ private
- def parser
- create_option_parser if @parser.nil?
- @parser
+ def option_is_deprecated?(option)
+ @deprecated_options[command].key?(option)
end
- def create_option_parser
- @parser = OptionParser.new
+ 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|
+ @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
- @parser.separator nil
- configure_options "Common", Gem::Command.common_options
+ ##
+ # Adds a section with +title+ and +content+ to the parser help view. Used
+ # for adding command arguments and default arguments.
- unless arguments.empty?
- @parser.separator nil
- @parser.separator " Arguments:"
- arguments.split(/\n/).each do |arg_desc|
- @parser.separator " #{arg_desc}"
- end
- end
+ def add_parser_run_info(title, content)
+ return if content.empty?
- if @summary then
- @parser.separator nil
- @parser.separator " Summary:"
- wrap(@summary, 80 - 4).split("\n").each do |line|
- @parser.separator " #{line.strip}"
- end
+ @parser.separator nil
+ @parser.separator " #{title}:"
+ content.each_line do |line|
+ @parser.separator " #{line.rstrip}"
end
+ end
- if description then
- formatted = description.split("\n\n").map do |chunk|
- wrap chunk, 80 - 4
- end.join "\n"
+ def add_parser_summary # :nodoc:
+ return unless @summary
- @parser.separator nil
- @parser.separator " Description:"
- formatted.split("\n").each do |line|
- @parser.separator " #{line.rstrip}"
- end
+ @parser.separator nil
+ @parser.separator " Summary:"
+ wrap(@summary, 80 - 4).each_line do |line|
+ @parser.separator " #{line.strip}"
end
+ end
- unless defaults_str.empty?
- @parser.separator nil
- @parser.separator " Defaults:"
- defaults_str.split(/\n/).each do |line|
- @parser.separator " #{line}"
- 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? or option_list.empty?
+ return if option_list.nil? || option_list.empty?
- header = header.to_s.empty? ? '' : "#{header} "
+ header = header.to_s.empty? ? "" : "#{header} "
@parser.separator " #{header}Options:"
option_list.each do |args, handler|
- args.select { |arg| arg =~ /^-/ }
@parser.on(*args) do |value|
handler.call(value, @options)
end
end
- @parser.separator ''
+ @parser.separator ""
end
##
@@ -459,78 +578,87 @@ class Gem::Command
# ----------------------------------------------------------------
# Add the options common to all commands.
- add_common_option('-h', '--help',
- 'Get help on this command') do |value, options|
+ add_common_option("-h", "--help",
+ "Get help on this command") do |_value, options|
options[:help] = true
end
- add_common_option('-V', '--[no-]verbose',
- 'Set the verbose level of output') do |value, options|
+ add_common_option("-V", "--[no-]verbose",
+ "Set the verbose level of output") do |value, _options|
# Set us to "really verbose" so the progress meter works
- if Gem.configuration.verbose and value then
+ if Gem.configuration.verbose && value
Gem.configuration.verbose = 1
else
Gem.configuration.verbose = value
end
end
- add_common_option('-q', '--quiet', 'Silence commands') do |value, options|
+ add_common_option("-q", "--quiet", "Silence command progress meter") do |_value, _options|
Gem.configuration.verbose = false
end
+ add_common_option("--silent",
+ "Silence RubyGems output") do |_value, options|
+ 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
+ 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('--backtrace',
- 'Show stack backtrace on errors') do
+ add_common_option("--debug",
+ "Turn on Ruby debugging") do
end
- add_common_option('--debug',
- 'Turn on Ruby debugging') do
+ add_common_option("--norc",
+ "Avoid loading any .gemrc file") do
end
# :stopdoc:
HELP = <<-HELP
-RubyGems is a sophisticated package manager for Ruby. This is a
-basic help message containing pointers to more information.
+RubyGems is a package manager for Ruby.
Usage:
gem -h/--help
gem -v/--version
- gem command [arguments...] [options...]
+ gem [global options...] command [arguments...] [options...]
+
+ Global options:
+ -C PATH run as if gem was started in <PATH>
+ instead of the current working directory
Examples:
gem install rake
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 platforms show information about platforms
+ 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')
- gem server present a web page at
- http://localhost:8808/
- with info about installed gems
Further information:
- http://rubygems.rubyforge.org
+ https://guides.rubygems.org
HELP
# :startdoc:
-
end
##
-# This is where Commands will be placed in the namespace
+# \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
index 9edd550136..76b2fba835 100644
--- a/lib/rubygems/command_manager.rb
+++ b/lib/rubygems/command_manager.rb
@@ -1,11 +1,14 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/command'
-require 'rubygems/user_interaction'
+require_relative "command"
+require_relative "user_interaction"
+require_relative "text"
##
# The command manager registers and installs all the individual sub-commands
@@ -18,73 +21,104 @@ require 'rubygems/user_interaction'
# # 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
#
-# Gem::CommandManager.instance.register_command :edit
-#
# 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
- @command_manager ||= new
+ @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
- @command_manager = nil
+ @instance = nil
end
##
# Register all the subcommands supported by the gem command.
def initialize
- require 'timeout'
+ require_relative "vendored_timeout"
@commands = {}
- register_command :build
- register_command :cert
- register_command :check
- register_command :cleanup
- register_command :contents
- register_command :dependency
- register_command :environment
- register_command :fetch
- register_command :generate_index
- register_command :help
- register_command :install
- register_command :list
- register_command :lock
- register_command :outdated
- register_command :owner
- register_command :pristine
- register_command :push
- register_command :query
- register_command :rdoc
- register_command :search
- register_command :server
- register_command :sources
- register_command :specification
- register_command :stale
- register_command :uninstall
- register_command :unpack
- register_command :update
- register_command :which
+
+ BUILTIN_COMMANDS.each do |name|
+ register_command name
+ end
end
##
# Register the Symbol +command+ as a gem command.
- def register_command(command)
- @commands[command] = false
+ def register_command(command, obj = false)
+ @commands[command] = obj
end
##
@@ -95,7 +129,7 @@ class Gem::CommandManager
end
##
- # Return the registered command from the command name.
+ # Returns a Command instance for +command_name+
def [](command_name)
command_name = command_name.intern
@@ -104,65 +138,90 @@ class Gem::CommandManager
end
##
- # Return a sorted list of all command names (as strings).
+ # Return a sorted list of all command names as strings.
def command_names
- @commands.keys.collect {|key| key.to_s}.sort
+ @commands.keys.collect(&:to_s).sort
end
##
- # Run the config specified by +args+.
-
- def run(args)
- process_args(args)
- rescue StandardError, Timeout::Error => ex
- alert_error "While executing gem ... (#{ex.class})\n #{ex.to_s}"
- ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if
- Gem.configuration.backtrace
+ # 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 "Interrupted"
+ alert_error clean_text("Interrupted")
terminate_interaction(1)
end
- def process_args(args)
- args = args.to_str.split(/\s+/) if args.respond_to?(:to_str)
- if args.size == 0
+ def process_args(args, build_args = nil)
+ if args.empty?
say Gem::Command::HELP
- terminate_interaction(1)
+ terminate_interaction 1
end
- case args[0]
- when '-h', '--help'
+
+ case args.first
+ when "-h", "--help" then
say Gem::Command::HELP
- terminate_interaction(0)
- when '-v', '--version'
+ terminate_interaction 0
+ when "-v", "--version" then
say Gem::VERSION
- terminate_interaction(0)
- when /^-/
- alert_error "Invalid option: #{args[0]}. See 'gem --help'."
- terminate_interaction(1)
+ terminate_interaction 0
+ when "-C" then
+ args.shift
+ start_point = args.shift
+ if Dir.exist?(start_point)
+ Dir.chdir(start_point) { invoke_command(args, build_args) }
+ else
+ alert_error clean_text("#{start_point} isn't a directory.")
+ terminate_interaction 1
+ end
+ when /^-/ then
+ alert_error clean_text("Invalid option: #{args.first}. See 'gem --help'.")
+ terminate_interaction 1
else
- cmd_name = args.shift.downcase
- cmd = find_command(cmd_name)
- cmd.invoke(*args)
+ 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 then
- raise "Ambiguous command #{cmd_name} matches [#{possibilities.join(', ')}]"
- elsif possibilities.size < 1 then
- raise "Unknown command #{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
- command_names.select { |n| cmd_name == n[0, len] }
+ found = command_names.select {|name| cmd_name == name[0, len] }
+
+ exact = found.find {|name| name == cmd_name }
+
+ exact ? [exact] : found
end
private
@@ -170,25 +229,26 @@ class Gem::CommandManager
def load_and_instantiate(command_name)
command_name = command_name.to_s
const_name = command_name.capitalize.gsub(/_(.)/) { $1.upcase } << "Command"
- commands = Gem::Commands
- retried = false
begin
- commands.const_get(const_name).new
- rescue NameError
- raise if retried
-
- retried = true
begin
require "rubygems/commands/#{command_name}_command"
- rescue Exception => e
- alert_error "Loading command: #{command_name} (#{e.class})\n #{e}"
- ui.errs.puts "\t#{e.backtrace.join "\n\t"}" if
- Gem.configuration.backtrace
+ rescue LoadError
+ # it may have been defined from a rubygems_plugin.rb file
end
- retry
+
+ 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
index 572a5c36ec..cfe1f8ec3c 100644
--- a/lib/rubygems/commands/build_command.rb
+++ b/lib/rubygems/commands/build_command.rb
@@ -1,55 +1,120 @@
-require 'rubygems/command'
-require 'rubygems/builder'
+# 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')
+ 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
- gemspec = get_one_gem_name
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem }
+ return
+ end
+
+ build_gem
+ end
- if File.exist? gemspec
- spec = load_gemspec gemspec
+ private
+
+ def build_gem
+ gemspec = resolve_gem_name
+
+ if gemspec
+ build_package(gemspec)
+ else
+ alert_error error_message
+ terminate_interaction(1)
+ end
+ end
- if spec then
- Gem::Builder.new(spec).build
- else
- alert_error "Error loading gemspec. Aborting."
- terminate_interaction 1
- 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 "Gemspec file not found: #{gemspec}"
+ alert_error "Error loading gemspec. Aborting."
terminate_interaction 1
end
end
- def load_gemspec filename
- if yaml?(filename)
- open(filename) do |f|
- begin
- Gem::Specification.from_yaml(f)
- rescue Gem::EndOfYAMLException
- nil
- 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
- Gem::Specification.load(filename) # can return nil
+ "Couldn't find a gemspec file in #{Dir.pwd}"
end
end
- def yaml?(filename)
- line = open(filename) { |f| line = f.gets }
- result = line =~ %r{!ruby/object:Gem::Specification}
- result
+ 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
index b416b3863d..fe03841ddb 100644
--- a/lib/rubygems/commands/cert_command.rb
+++ b/lib/rubygems/commands/cert_command.rb
@@ -1,86 +1,325 @@
-require 'rubygems/command'
-require 'rubygems/security'
+# frozen_string_literal: true
-class Gem::Commands::CertCommand < Gem::Command
+require_relative "../command"
+require_relative "../security"
+class Gem::Commands::CertCommand < Gem::Command
def initialize
- super 'cert', 'Manage RubyGems certificates and signing settings'
-
- add_option('-a', '--add CERT',
- 'Add a trusted certificate.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- Gem::Security.add_trusted_cert(cert)
- say "Added '#{cert.subject.to_s}'"
- end
-
- add_option('-l', '--list',
- 'List trusted certificates.') do |value, options|
- glob_str = File::join(Gem::Security::OPT[:trust_dir], '*.pem')
- Dir::glob(glob_str) do |path|
- begin
- cert = OpenSSL::X509::Certificate.new(File.read(path))
- # this could probably be formatted more gracefully
- say cert.subject.to_s
- rescue OpenSSL::X509::CertificateError
- next
- end
- end
- end
-
- add_option('-r', '--remove STRING',
- 'Remove trusted certificates containing',
- 'STRING.') do |value, options|
- trust_dir = Gem::Security::OPT[:trust_dir]
- glob_str = File::join(trust_dir, '*.pem')
-
- Dir::glob(glob_str) do |path|
- begin
- cert = OpenSSL::X509::Certificate.new(File.read(path))
- if cert.subject.to_s.downcase.index(value)
- say "Removed '#{cert.subject.to_s}'"
- File.unlink(path)
- end
- rescue OpenSSL::X509::CertificateError
- next
- end
- end
- end
-
- add_option('-b', '--build EMAIL_ADDR',
- 'Build private key and self-signed',
- 'certificate for EMAIL_ADDR.') do |value, options|
- vals = Gem::Security.build_self_signed_cert(value)
- FileUtils.chmod 0600, vals[:key_path]
- say "Public Cert: #{vals[:cert_path]}"
- say "Private Key: #{vals[:key_path]}"
- say "Don't forget to move the key file to somewhere private..."
- end
-
- add_option('-C', '--certificate CERT',
- 'Certificate for --sign command.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- options[:issuer_cert] = cert
- end
-
- add_option('-K', '--private-key KEY',
- 'Private key for --sign command.') do |value, options|
- key = OpenSSL::PKey::RSA.new(File.read(value))
- options[:issuer_key] = key
- end
-
- add_option('-s', '--sign NEWCERT',
- 'Sign a certificate with my key and',
- 'certificate.') do |value, options|
- cert = OpenSSL::X509::Certificate.new(File.read(value))
- my_cert = options[:issuer_cert]
- my_key = options[:issuer_key]
- cert = Gem::Security.sign_cert(cert, my_key, my_cert)
- File.open(value, 'wb') { |file| file.write(cert.to_pem) }
+ 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
-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
index 5a1bfd4f12..fb23dd9cb4 100644
--- a/lib/rubygems/commands/check_command.rb
+++ b/lib/rubygems/commands/check_command.rb
@@ -1,65 +1,97 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/validator'
+# frozen_string_literal: true
-class Gem::Commands::CheckCommand < Gem::Command
+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 installed gems',
- :verify => false, :alien => false
+ super "check", "Check a gem repository for added or missing files",
+ alien: true, doctor: false, dry_run: false, gems: true
- add_option( '--verify FILE',
- 'Verify gem file against its internal',
- 'checksum') do |value, options|
- options[:verify] = value
+ add_option("-a", "--[no-]alien",
+ 'Report "unmanaged" or rogue files in the',
+ "gem repository") do |value, options|
+ options[:alien] = value
end
- add_option('-a', '--alien', "Report 'unmanaged' or rogue files in the",
- "gem repository") do |value, options|
- options[:alien] = true
+ 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'
+ add_version_option "check"
end
- def execute
- if options[:alien]
- say "Performing the 'alien' operation"
- say
- gems = get_all_gem_names rescue []
- Gem::Validator.new.alien(gems).sort.each do |key, val|
- unless val.empty? then
- say "#{key} has #{val.size} problems"
- val.each do |error_entry|
- say " #{error_entry.path}:"
- say " #{error_entry.problem}"
- end
- else
- say "#{key} is error-free" if Gem.configuration.verbose
+ 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
- say
end
+ say
end
+ end
- if options[:verify]
- gem_name = options[:verify]
- unless gem_name
- alert_error "Must specify a .gem file with --verify NAME"
- return
- end
- unless File.exist?(gem_name)
- alert_error "Unknown file: #{gem_name}."
- return
- end
- say "Verifying gem: '#{gem_name}'"
- begin
- Gem::Validator.new.verify_gem_file(gem_name)
- rescue Exception
- alert_error "#{gem_name} is invalid."
- 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
index 124c4c203a..c89a24eee9 100644
--- a/lib/rubygems/commands/cleanup_command.rb
+++ b/lib/rubygems/commands/cleanup_command.rb
@@ -1,17 +1,44 @@
-require 'rubygems/command'
-require 'rubygems/dependency_list'
-require 'rubygems/uninstaller'
+# frozen_string_literal: true
-class Gem::Commands::CleanupCommand < Gem::Command
+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 in the local repository',
- :force => false, :install_dir => Gem.dir
+ 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('-d', '--dryrun', "") do |value, options|
+ add_option(:Deprecated, "--dryrun",
+ "Do not uninstall gems") do |_value, options|
options[:dryrun] = true
end
+ deprecate_option("--dryrun", extra_msg: "Use --dry-run instead")
+
+ 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:
@@ -19,13 +46,16 @@ class Gem::Commands::CleanupCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--no-dryrun"
+ "--no-dry-run"
end
def description # :nodoc:
<<-EOF
-The cleanup command removes old gems from GEM_HOME. If an older version is
-installed elsewhere in GEM_PATH the cleanup command won't touch it.
+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
@@ -35,66 +65,114 @@ installed elsewhere in GEM_PATH the cleanup command won't touch it.
def execute
say "Cleaning up installed gems..."
- primary_gems = {}
- Gem::Specification.each do |spec|
- if primary_gems[spec.name].nil? or
- primary_gems[spec.name].version < spec.version then
- primary_gems[spec.name] = spec
+ 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
- gems_to_cleanup = unless options[:args].empty? then
- options[:args].map do |gem_name|
- Gem::Specification.find_all_by_name gem_name
- end.flatten
- else
- Gem::Specification.to_a
- end
+ say "Clean up complete"
- gems_to_cleanup = gems_to_cleanup.select { |spec|
- primary_gems[spec.name].version != spec.version
- }
+ 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.uniq.each do |spec| deplist.add spec end
+ @gems_to_cleanup.each {|spec| deplist.add spec }
- deps = deplist.strongly_connected_components.flatten.reverse
+ deps = deplist.strongly_connected_components.flatten
- original_path = Gem.path
+ deps.reverse_each do |spec|
+ uninstall_dep spec
+ end
+ end
- deps.each do |spec|
- if options[:dryrun] then
- say "Dry Run Mode: Would uninstall #{spec.full_name}"
- else
- say "Attempting to uninstall #{spec.full_name}"
+ 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
- options[:args] = [spec.name]
+ def get_gems_to_cleanup
+ gems_to_cleanup = @candidate_gems.select do |spec|
+ @primary_gems[spec.name].version != spec.version
+ end
- uninstall_options = {
- :executables => false,
- :version => "= #{spec.version}",
- }
+ default_gems, gems_to_cleanup = gems_to_cleanup.partition(&:default_gem?)
- uninstall_options[:user_install] = Gem.user_dir == spec.base_dir
+ uninstall_from = options[:user_install] ? Gem.user_dir : Gem.dir
- uninstaller = Gem::Uninstaller.new spec.name, uninstall_options
+ 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
- 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
+ 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])
- # Restore path Gem::Uninstaller may have change
- Gem.use_paths(*original_path)
+ if options[:dryrun]
+ say "Dry Run Mode: Would uninstall #{spec.full_name}"
+ return
end
- say "Clean Up Complete"
- end
+ say "Attempting to uninstall #{spec.full_name}"
-end
+ 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
index e483484615..d4f9871868 100644
--- a/lib/rubygems/commands/contents_command.rb
+++ b/lib/rubygems/commands/contents_command.rb
@@ -1,35 +1,46 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::ContentsCommand < Gem::Command
+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
+ super "contents", "Display the contents of the installed gems",
+ specdirs: [], lib_only: false, prefix: true,
+ show_install_dir: false
add_version_option
- add_option( '--all',
+ add_option("--all",
"Contents for all gems") do |all, options|
options[:all] = all
end
- add_option('-s', '--spec-dir a,b,c', Array,
+ add_option("-s", "--spec-dir a,b,c", Array,
"Search for gems under specific paths") do |spec_dirs, options|
options[:specdirs] = spec_dirs
end
- add_option('-l', '--[no-]lib-only',
+ add_option("-l", "--[no-]lib-only",
"Only return files in the Gem's lib_dirs") do |lib_only, options|
options[:lib_only] = lib_only
end
- add_option( '--[no-]prefix',
+ add_option("--[no-]prefix",
"Don't include installed path prefix") do |prefix, options|
options[:prefix] = prefix
end
+
+ add_option("--[no-]show-install-dir",
+ "Show only the gem install dir") do |show, options|
+ options[:show_install_dir] = show
+ end
+
+ @path_kind = nil
+ @spec_dirs = nil
+ @version = nil
end
def arguments # :nodoc:
@@ -40,62 +51,146 @@ class Gem::Commands::ContentsCommand < Gem::Command
"--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
+ @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
- spec_dirs = options[:specdirs].map do |i|
- [i, File.join(i, "specifications")]
- end.flatten
-
- path_kind = if spec_dirs.empty? then
- spec_dirs = Gem::Specification.dirs
- "default gem paths"
- else
- "specified path"
- end
-
- gem_names = if options[:all] then
- Gem::Specification.map(&:name)
- else
- get_all_gem_names
- end
-
- gem_names.each do |name|
- # HACK: find_by_name fails for some reason... ARGH
- # How many places must we embed our resolve logic?
- spec = Gem::Specification.find_all_by_name(name, version).last
-
- unless spec then
- say "Unable to find gem '#{name}' in #{path_kind}"
-
- if Gem.configuration.verbose then
- say "\nDirectories searched:"
- spec_dirs.each { |dir| say dir }
+ 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
- terminate_interaction 1 if gem_names.length == 1
+ 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
- gem_path = spec.full_gem_path
- extra = "/{#{spec.require_paths.join ','}}" if options[:lib_only]
- glob = "#{gem_path}#{extra}/**/*"
- files = Dir[glob]
+ def gem_contents(name)
+ spec = spec_for name
- gem_path = File.join gem_path, '' # add trailing / if missing
+ return false unless spec
- files.sort.each do |file|
- next if File.directory? file
+ files = files_in spec
- file = file.sub gem_path, '' unless options[:prefix]
+ show_files files
- say file
+ 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
-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
index 67cbbc1d5e..9aaefae999 100644
--- a/lib/rubygems/commands/dependency_command.rb
+++ b/lib/rubygems/commands/dependency_command.rb
@@ -1,28 +1,28 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::DependencyCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+class Gem::Commands::DependencyCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'dependency',
- 'Show the dependencies of an installed gem',
- :version => Gem::Requirement.default, :domain => :local
+ super "dependency",
+ "Show the dependencies of an installed gem",
+ version: Gem::Requirement.default, domain: :local
add_version_option
add_platform_option
add_prerelease_option
- add_option('-R', '--[no-]reverse-dependencies',
- 'Include reverse dependencies in the output') do
- |value, options|
+ add_option("-R", "--[no-]reverse-dependencies",
+ "Include reverse dependencies in the output") do |value, options|
options[:reverse_dependencies] = value
end
- add_option('-p', '--pipe',
+ add_option("-p", "--pipe",
"Pipe Format (name --version ver)") do |value, options|
options[:pipe_format] = value
end
@@ -31,123 +31,158 @@ class Gem::Commands::DependencyCommand < Gem::Command
end
def arguments # :nodoc:
- "GEMNAME name of gem to show dependencies for"
+ "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} GEMNAME"
+ "#{program_name} REGEXP"
end
- def execute
- if options[:reverse_dependencies] and remote? and not local? then
- alert_error 'Only reverse dependencies for local gems are supported.'
- terminate_interaction 1
- end
+ def fetch_remote_specs(name, requirement, prerelease) # :nodoc:
+ fetcher = Gem::SpecFetcher.fetcher
- options[:args] << '' if options[:args].empty?
+ specs_type = prerelease ? :complete : :released
- pattern = if options[:args].length == 1 and
- options[:args].first =~ /\A\/(.*)\/(i)?\z/m then
- flags = $2 ? Regexp::IGNORECASE : nil
- Regexp.new $1, flags
- else
- /\A#{Regexp.union(*options[:args])}/
- end
+ 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
- # TODO: deprecate for real damnit
- dependency = Gem::Deprecate.skip_during {
- Gem::Dependency.new pattern, options[:version]
- }
- dependency.prerelease = options[:prerelease]
+ ss.map {|tuple, source| source.fetch_spec(tuple) }
+ end
+ def fetch_specs(name_pattern, requirement, prerelease) # :nodoc:
specs = []
- specs.concat dependency.matching_specs if local?
-
- if remote? and not options[:reverse_dependencies] then
- fetcher = Gem::SpecFetcher.fetcher
+ 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)
- # REFACTOR: fetcher.find_specs_matching => specs
- specs_and_sources = fetcher.find_matching(dependency,
- dependency.specific?, true,
- dependency.prerelease?)
-
- specs.concat specs_and_sources.map { |spec_tuple, source_uri|
- fetcher.fetch_spec spec_tuple, URI.parse(source_uri)
- }
+ name_matches && version_matches
+ }.map(&:to_spec)
end
- if specs.empty? then
- patterns = options[:args].join ','
- say "No gems found matching #{patterns} (#{options[:version]})" if
- Gem.configuration.verbose
-
- terminate_interaction 1
- end
+ specs.concat fetch_remote_specs name_pattern, requirement, prerelease if remote?
- specs = specs.uniq.sort
+ ensure_specs specs
- reverse = Hash.new { |h, k| h[k] = [] }
+ specs.uniq.sort
+ end
- if options[:reverse_dependencies] then
- specs.each do |spec|
- reverse[spec.full_name] = find_reverse_dependencies spec
+ 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
- if options[:pipe_format] then
- specs.each do |spec|
- unless spec.dependencies.empty?
- spec.dependencies.sort_by { |dep| dep.name }.each do |dep|
- say "#{dep.name} --version '#{dep.requirement}'"
- end
+ 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
- response = ''
-
- specs.each do |spec|
- response << print_dependencies(spec)
- unless reverse[spec.full_name].empty? then
- response << " Used by\n"
- reverse[spec.full_name].each do |sp, dep|
- response << " #{sp} (#{dep})\n"
- end
- end
- response << "\n"
- end
+ display_readable specs, reverse
+ end
+ end
- say response
+ 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 print_dependencies(spec, level = 0)
- response = ''
- response << ' ' * level + "Gem #{spec.full_name}\n"
- unless spec.dependencies.empty? then
- spec.dependencies.sort_by { |dep| dep.name }.each do |dep|
- response << ' ' * level + " #{dep}\n"
+ 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)
+ 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 and
- dep.requirement.satisfied_by?(spec.version) then
+ if spec.name == dep.name &&
+ dep.requirement.satisfied_by?(spec.version)
result << [sp.full_name, dep]
end
end
@@ -156,5 +191,16 @@ class Gem::Commands::DependencyCommand < Gem::Command
result
end
-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
index 9585c71250..a5eb521a53 100644
--- a/lib/rubygems/commands/environment_command.rb
+++ b/lib/rubygems/commands/environment_command.rb
@@ -1,56 +1,68 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::EnvironmentCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::EnvironmentCommand < Gem::Command
def initialize
- super 'environment', 'Display information about the RubyGems environment'
+ super "environment", "Display information about the RubyGems environment"
end
def arguments # :nodoc:
args = <<-EOF
- packageversion display the package version
- gemdir display the path where gems are installed
- gempath display path used to search for gems
+ home display the path where gems are installed. Aliases: gemhome, gemdir, GEM_HOME
+ path display path used to search for gems. Aliases: gempath, GEM_PATH
+ user_gemhome display the path where gems are installed when `--user-install` is given. Aliases: user_gemdir
version display the gem format version
remotesources display the remote gem servers
platform display the supported gem platforms
+ credentials display the path where credentials are stored
<omitted> display everything
EOF
- return args.gsub(/^\s+/, '')
+ 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
-~/.gemrc file for individual users and a /etc/gemrc for all users. A gemrc
-is a YAML file with the following YAML keys:
+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
+ :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
- gem_command: A string containing arguments for the specified gem command
+ :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
+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
@@ -62,69 +74,109 @@ lib/rubygems/defaults/operating_system.rb
end
def execute
- out = ''
+ out = String.new
arg = options[:args][0]
- case arg
- when /^packageversion/ then
- out << Gem::RubyGemsPackageVersion
- when /^version/ then
- out << Gem::VERSION
- when /^gemdir/, /^gemhome/, /^home/, /^GEM_HOME/ then
- out << Gem.dir
- when /^gempath/, /^path/, /^GEM_PATH/ then
- out << Gem.path.join(File::PATH_SEPARATOR)
- when /^remotesources/ then
- out << Gem.sources.join("\n")
- when /^platform/ then
- out << Gem.platforms.join(File::PATH_SEPARATOR)
- when nil then
- out = "RubyGems Environment:\n"
-
- out << " - RUBYGEMS VERSION: #{Gem::VERSION}\n"
-
- out << " - RUBY VERSION: #{RUBY_VERSION} (#{RUBY_RELEASE_DATE}"
- out << " patchlevel #{RUBY_PATCHLEVEL}" if defined? RUBY_PATCHLEVEL
- out << ") [#{RUBY_PLATFORM}]\n"
-
- out << " - INSTALLATION DIRECTORY: #{Gem.dir}\n"
-
- out << " - RUBYGEMS PREFIX: #{Gem.prefix}\n" unless Gem.prefix.nil?
-
- out << " - RUBY EXECUTABLE: #{Gem.ruby}\n"
-
- out << " - EXECUTABLE DIRECTORY: #{Gem.bindir}\n"
-
- out << " - RUBYGEMS PLATFORMS:\n"
- Gem.platforms.each do |platform|
- out << " - #{platform}\n"
+ 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
- out << " - GEM PATHS:\n"
- out << " - #{Gem.dir}\n"
+ def add_path(out, path)
+ path.each do |component|
+ out << " - #{component}\n"
+ end
+ end
- path = Gem.path.dup
- path.delete Gem.dir
- path.each do |p|
- out << " - #{p}\n"
- end
+ def show_environment # :nodoc:
+ out = "RubyGems Environment:\n".dup
- 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 << " - RUBYGEMS VERSION: #{Gem::VERSION}\n"
- out << " - REMOTE SOURCES:\n"
- Gem.sources.each do |s|
- out << " - #{s}\n"
- end
+ 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"
- else
- raise Gem::CommandLineError, "Unknown environment option [#{arg}]"
+ out << " - SYSTEM CONFIGURATION DIRECTORY: #{Gem::ConfigFile::SYSTEM_CONFIG_PATH}\n"
+
+ out << " - RUBYGEMS PLATFORMS:\n"
+ Gem.platforms.each do |platform|
+ out << " - #{platform}\n"
end
- say out
- true
+
+ 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
-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
index 666d83e730..8e64a18cee 100644
--- a/lib/rubygems/commands/fetch_command.rb
+++ b/lib/rubygems/commands/fetch_command.rb
@@ -1,69 +1,109 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::FetchCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../version_option"
+class Gem::Commands::FetchCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'fetch', 'Download a gem and place it in the current directory'
+ defaults = {
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ }
+
+ super "fetch", "Download a gem and place it in the current directory", defaults
add_bulk_threshold_option
add_proxy_option
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'
+ "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
- version = options[:version] || Gem::Requirement.default
- all = Gem::Requirement.default != version
+ 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
+ gem_names = get_all_gem_names_and_versions
- gem_names.each do |gem_name|
- dep = Gem::Dependency.new gem_name, version
+ gem_names.each do |gem_name, gem_version|
+ gem_version ||= version
+ dep = Gem::Dependency.new gem_name, gem_version
dep.prerelease = options[:prerelease]
+ suppress_suggestions = !options[:suggest_alternate]
specs_and_sources, errors =
- Gem::SpecFetcher.fetcher.fetch_with_errors(dep, all, true,
- dep.prerelease?)
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
- if platform then
- filtered = specs_and_sources.select { |s,| s.platform == platform }
+ if platform
+ filtered = specs_and_sources.select {|s,| s.platform == platform }
specs_and_sources = filtered unless filtered.empty?
end
- spec, source_uri = specs_and_sources.sort_by { |s,| s.version }.last
+ spec, source = specs_and_sources.max_by {|s,| s }
- if spec.nil? then
- show_lookup_failure gem_name, version, errors, options[:domain]
+ if spec.nil?
+ show_lookup_failure gem_name, gem_version, errors, suppress_suggestions, options[:domain]
+ exit_code |= 2
next
end
-
- path = Gem::RemoteFetcher.fetcher.download spec, source_uri
- FileUtils.mv path, File.basename(spec.cache_file)
-
+ source.download spec
say "Downloaded #{spec.full_name}"
end
- end
+ exit_code
+ end
end
-
diff --git a/lib/rubygems/commands/generate_index_command.rb b/lib/rubygems/commands/generate_index_command.rb
index d4b4790649..13be92593b 100644
--- a/lib/rubygems/commands/generate_index_command.rb
+++ b/lib/rubygems/commands/generate_index_command.rb
@@ -1,124 +1,51 @@
-require 'rubygems/command'
-require 'rubygems/indexer'
+# frozen_string_literal: true
-##
-# Generates a index files for use as a gem server.
-#
-# See `gem help generate_index`
+require_relative "../command"
-class Gem::Commands::GenerateIndexCommand < Gem::Command
-
- def initialize
- super 'generate_index',
- 'Generates the index files for a gem server directory',
- :directory => '.', :build_legacy => true, :build_modern => true
-
- add_option '-d', '--directory=DIRNAME',
- 'repository base dir containing gems subdir' do |dir, options|
- options[:directory] = File.expand_path dir
- end
-
- add_option '--[no-]legacy',
- 'Generate Marshal.4.8' do |value, options|
- unless options[:build_modern] or value then
- raise OptionParser::InvalidOption, 'no indicies will be built'
+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
- options[:build_legacy] = value
- end
-
- add_option '--[no-]modern',
- 'Generate indexes for RubyGems newer',
- 'than 1.2.0' do |value, options|
- unless options[:build_legacy] or value then
- raise OptionParser::InvalidOption, 'no indicies will be built'
+ def execute
+ alert_error "Install the rubygems-generate_index gem for the generate_index command"
end
- options[:build_modern] = value
- end
-
- add_option '--update',
- 'Update modern indexes with gems added',
- 'since the last update' do |value, options|
- options[:update] = value
- end
-
- add_option :RSS, '--rss-gems-host=GEM_HOST',
- 'Host name where gems are served from,',
- 'used for GUID and enclosure values' do |value, options|
- options[:rss_gems_host] = value
- end
-
- add_option :RSS, '--rss-host=HOST',
- 'Host name for more gems information,',
- 'used for RSS feed link' do |value, options|
- options[:rss_host] = value
- end
-
- add_option :RSS, '--rss-title=TITLE',
- 'Set title for RSS feed' do |value, options|
- options[:rss_title] = value
+ def invoke_with_build_args(args, build_args)
+ name = "rubygems-generate_index"
+ spec = begin
+ Gem::Specification.find_by_name(name)
+ rescue Gem::LoadError
+ require "rubygems/dependency_installer"
+ Gem.install(name, Gem::Requirement.default, Gem::DependencyInstaller::DEFAULT_OPTIONS).find {|s| s.name == name }
+ end
+
+ # remove the methods defined in this file so that the methods defined in the gem are used instead,
+ # and without a method redefinition warning
+ %w[description execute invoke_with_build_args].each do |method|
+ RubygemsTrampoline.remove_method(method)
+ end
+ self.class.singleton_class.remove_method(:new)
+
+ spec.activate
+ Gem.load_plugin_files spec.matches_for_glob("rubygems_plugin#{Gem.suffix_pattern}")
+
+ self.class.new.invoke_with_build_args(args, build_args)
+ end
end
- end
+ private_constant :RubygemsTrampoline
- def defaults_str # :nodoc:
- "--directory . --legacy --modern"
- end
-
- def description # :nodoc:
- <<-EOF
-The generate_index command creates a set of indexes for serving gems
-statically. The command expects a 'gems' directory under the path given to
-the --directory option. The given directory will be the directory you serve
-as the gem repository.
-
-For `gem generate_index --directory /path/to/repo`, expose /path/to/repo via
-your HTTP server configuration (not /path/to/repo/gems).
-
-When done, it will generate a set of files like this:
-
- gems/*.gem # .gem files you want to
- # index
-
- specs.<version>.gz # specs index
- latest_specs.<version>.gz # latest specs index
- prerelease_specs.<version>.gz # prerelease specs index
- quick/Marshal.<version>/<gemname>.gemspec.rz # Marshal quick index file
-
- # these files support legacy RubyGems
- Marshal.<version>
- Marshal.<version>.Z # Marshal full index
-
-The .Z and .rz extension files are compressed with the inflate algorithm.
-The Marshal version number comes from ruby's Marshal::MAJOR_VERSION and
-Marshal::MINOR_VERSION constants. It is used to ensure compatibility.
-
-If --rss-host and --rss-gem-host are given an RSS feed will be generated at
-index.rss containing gems released in the last two days.
- EOF
- end
-
- def execute
- if options[:update] and
- (options[:rss_host] or options[:rss_gems_host]) then
- alert_error '--update not compatible with RSS generation'
- terminate_interaction 1
+ # 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
- if not File.exist?(options[:directory]) or
- not File.directory?(options[:directory]) then
- alert_error "unknown directory name #{directory}."
- terminate_interaction 1
- else
- indexer = Gem::Indexer.new options.delete(:directory), options
-
- if options[:update] then
- indexer.update_index
- else
- indexer.generate_index
- end
- end
+ prepend(RubygemsTrampoline)
end
-
end
-
diff --git a/lib/rubygems/commands/help_command.rb b/lib/rubygems/commands/help_command.rb
index 20b52429b2..664f400561 100644
--- a/lib/rubygems/commands/help_command.rb
+++ b/lib/rubygems/commands/help_command.rb
@@ -1,7 +1,8 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::HelpCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::HelpCommand < Gem::Command
# :stopdoc:
EXAMPLES = <<-EOF
Some examples of 'gem' usage.
@@ -37,7 +38,7 @@ Some examples of 'gem' usage.
* Create a gem:
- See http://rubygems.rubyforge.org/wiki/wiki.pl?CreateAGemInTenMinutes
+ See https://guides.rubygems.org/make-your-own-gem/
* See information about RubyGems:
@@ -46,6 +47,189 @@ Some examples of 'gem' usage.
* 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'
@@ -55,8 +239,9 @@ 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.
+ * 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.
@@ -66,29 +251,41 @@ 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"
- end
+ super "help", "Provide help on the 'gem' command"
- def arguments # :nodoc:
- args = <<-EOF
- commands List all 'gem' commands
- examples Show examples of 'gem' usage
- <command> Show specific help for <command>
- EOF
- return args.gsub(/^\s+/, '')
+ @command_manager = Gem::CommandManager.instance
end
def usage # :nodoc:
@@ -96,72 +293,85 @@ platform.
end
def execute
- command_manager = Gem::CommandManager.instance
arg = options[:args][0]
- if begins? "commands", arg then
- out = []
- out << "GEM commands are:"
- out << nil
+ _, help = SUBCOMMANDS.find do |command,|
+ begins? command, arg
+ end
- margin_width = 4
+ if help
+ if Symbol === help
+ send help
+ else
+ say help
+ end
+ return
+ end
- desc_width = command_manager.command_names.map { |n| n.size }.max + 4
+ if options[:help]
+ show_help
- summary_width = 80 - margin_width - desc_width
- wrap_indent = ' ' * (margin_width + desc_width)
- format = "#{' ' * margin_width}%-#{desc_width}s%s"
+ elsif arg
+ show_command_help arg
- command_manager.command_names.each do |cmd_name|
- summary = command_manager[cmd_name].summary
- summary = wrap(summary, summary_width).split "\n"
- out << sprintf(format, cmd_name, summary.shift)
- until summary.empty? do
- out << "#{wrap_indent}#{summary.shift}"
- end
- end
+ else
+ say Gem::Command::HELP
+ 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'."
+ def show_commands # :nodoc:
+ out = []
+ out << "GEM commands are:"
+ out << nil
- say out.join("\n")
+ margin_width = 4
- elsif begins? "options", arg then
- say Gem::Command::HELP
+ desc_width = @command_manager.command_names.map(&:size).max + 4
- elsif begins? "examples", arg then
- say EXAMPLES
+ summary_width = 80 - margin_width - desc_width
+ wrap_indent = " " * (margin_width + desc_width)
+ format = "#{" " * margin_width}%-#{desc_width}s%s"
- elsif begins? "platforms", arg then
- say PLATFORMS
+ @command_manager.command_names.each do |cmd_name|
+ command = @command_manager[cmd_name]
- elsif options[:help] then
- command = command_manager[options[:help]]
- if command
- # help with provided command
- command.invoke("--help")
- else
- alert_error "Unknown command #{options[:help]}. Try 'gem help commands'"
- end
+ next if command&.deprecated?
- elsif arg then
- possibilities = command_manager.find_command_possibilities(arg.downcase)
- if possibilities.size == 1
- command = command_manager[possibilities.first]
- command.invoke("--help")
- elsif possibilities.size > 1
- alert_warning "Ambiguous command #{arg} (#{possibilities.join(', ')})"
- else
- alert_warning "Unknown command #{arg}. Try gem help commands"
+ 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
- say Gem::Command::HELP
+ 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
index 003ba8601c..6d3beec0b4 100644
--- a/lib/rubygems/commands/install_command.rb
+++ b/lib/rubygems/commands/install_command.rb
@@ -1,10 +1,12 @@
-require 'rubygems/command'
-require 'rubygems/doc_manager'
-require 'rubygems/install_update_options'
-require 'rubygems/dependency_installer'
-require 'rubygems/local_remote_options'
-require 'rubygems/validator'
-require 'rubygems/version_option'
+# 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
@@ -12,26 +14,33 @@ require 'rubygems/version_option'
# 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({
- :generate_rdoc => true,
- :generate_ri => true,
- :format_executable => false,
- :version => Gem::Requirement.default,
+ format_executable: false,
+ lock: true,
+ suggest_alternate: true,
+ version: Gem::Requirement.default,
+ without_groups: [],
})
- super 'install', 'Install a gem into the local repository', defaults
+ 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:
@@ -39,8 +48,9 @@ class Gem::Commands::InstallCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--both --version '#{Gem::Requirement.default}' --rdoc --ri --no-force\n" \
- "--install-dir #{Gem.dir}"
+ "--both --version '#{Gem::Requirement.default}' --no-force\n" \
+ "--install-dir #{Gem.dir} --lock\n" +
+ install_update_defaults_str
end
def description # :nodoc:
@@ -54,6 +64,25 @@ 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
@@ -93,73 +122,147 @@ to write the specification by hand. For example:
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} GEMNAME [GEMNAME ...] [options] -- --build-flags"
+ "#{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_dependencies] then
- alert "`gem install -y` is now default and will be removed"
- alert "use --ignore-dependencies to install only the gems you list"
+ if options.include? :gemdeps
+ install_from_gemdeps
+ return # not reached
end
- installed_gems = []
+ @installed_specs = []
- ENV.delete 'GEM_PATH' if options[:install_dir].nil? and RUBY_VERSION > '1.9'
+ ENV.delete "GEM_PATH" if options[:install_dir].nil?
- exit_code = 0
+ check_version
- get_all_gem_names.each do |gem_name|
- begin
- next if options[:conservative] and
- not Gem::Dependency.new(gem_name, options[:version]).matching_specs.empty?
+ load_hooks
+
+ exit_code = install_gems
+
+ show_installed
+
+ say update_suggestion if eligible_for_update?
+
+ terminate_interaction exit_code
+ end
- inst = Gem::DependencyInstaller.new options
- inst.install gem_name, options[:version]
+ def install_from_gemdeps # :nodoc:
+ require_relative "../request_set"
+ rs = Gem::RequestSet.new
- inst.installed_gems.each do |spec|
- say "Successfully installed #{spec.full_name}"
- end
+ 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
- installed_gems.push(*inst.installed_gems)
+ 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::GemNotFoundException => e
- show_lookup_failure e.name, e.version, e.errors, options[:domain]
+ 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
- unless installed_gems.empty? then
- gems = installed_gems.length == 1 ? 'gem' : 'gems'
- say "#{installed_gems.length} #{gems} installed"
+ exit_code
+ end
- # NOTE: *All* of the RI documents must be generated first. For some
- # reason, RI docs cannot be generated after any RDoc documents are
- # generated.
+ ##
+ # Loads post-install hooks
- if options[:generate_ri] then
- installed_gems.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri
- end
+ def load_hooks # :nodoc:
+ require_relative "../install_message"
+ require_relative "../rdoc"
+ end
- Gem::DocManager.update_ri_cache
- end
+ def show_install_errors(errors) # :nodoc:
+ return unless errors
- if options[:generate_rdoc] then
- installed_gems.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc
- end
- end
- end
+ 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}"
- raise Gem::SystemExitException, exit_code
+ alert_warning msg
+ end
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
index f3e5da9551..fab4b73814 100644
--- a/lib/rubygems/commands/list_command.rb
+++ b/lib/rubygems/commands/list_command.rb
@@ -1,35 +1,42 @@
-require 'rubygems/command'
-require 'rubygems/commands/query_command'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../query_utils"
##
-# An alternate to Gem::Commands::QueryCommand that searches for gems starting
-# with the the supplied argument.
+# Searches for gems starting with the supplied argument.
-class Gem::Commands::ListCommand < Gem::Commands::QueryCommand
+class Gem::Commands::ListCommand < Gem::Command
+ include Gem::QueryUtils
def initialize
- super 'list', 'Display gems whose name starts with STRING'
+ super "list", "Display local gems whose name matches REGEXP",
+ domain: :local, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
- remove_option('--name-matches')
+ add_query_options
end
def arguments # :nodoc:
- "STRING start of gem name to look for"
+ "REGEXP regexp to look for in gem name"
end
def defaults_str # :nodoc:
"--local --no-details"
end
- def usage # :nodoc:
- "#{program_name} [STRING]"
- 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.
- def execute
- string = get_one_optional_argument || ''
- options[:name] = /^#{string}/i
- super
+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
index a6dca320ef..f7fd5ada16 100644
--- a/lib/rubygems/commands/lock_command.rb
+++ b/lib/rubygems/commands/lock_command.rb
@@ -1,13 +1,14 @@
-require 'rubygems/command'
+# frozen_string_literal: true
-class Gem::Commands::LockCommand < Gem::Command
+require_relative "../command"
+class Gem::Commands::LockCommand < Gem::Command
def initialize
- super 'lock', 'Generate a lockdown list of gems',
- :strict => false
+ super "lock", "Generate a lockdown list of gems",
+ strict: false
- add_option '-s', '--[no-]strict',
- 'fail if unable to satisfy a dependency' do |strict, options|
+ add_option "-s", "--[no-]strict",
+ "fail if unable to satisfy a dependency" do |strict, options|
options[:strict] = strict
end
end
@@ -30,7 +31,7 @@ generated.
Example:
- gemlock rails-1.0.0 > lockdown.rb
+ gem lock rails-1.0.0 > lockdown.rb
will produce in lockdown.rb:
@@ -58,7 +59,7 @@ lock it down to the exact version.
end
def complain(message)
- if options[:strict] then
+ if options[:strict]
raise Gem::Exception, message
else
say "# #{message}"
@@ -77,7 +78,7 @@ lock it down to the exact version.
spec = Gem::Specification.load spec_path(full_name)
- if spec.nil? then
+ if spec.nil?
complain "Could not find gem #{full_name}, try using the full name"
next
end
@@ -89,7 +90,7 @@ lock it down to the exact version.
next if locked[dep.name]
candidates = dep.matching_specs
- if candidates.empty? then
+ if candidates.empty?
complain "Unable to satisfy '#{dep}' from currently installed gems"
else
pending << candidates.last.full_name
@@ -99,12 +100,10 @@ lock it down to the exact version.
end
def spec_path(gem_full_name)
- gemspecs = Gem.path.map { |path|
+ gemspecs = Gem.path.map do |path|
File.join path, "specifications", "#{gem_full_name}.gemspec"
- }
+ end
- gemspecs.find { |path| File.exist? path }
+ 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
index ea6b9f0abf..08a9221a26 100644
--- a/lib/rubygems/commands/outdated_command.rb
+++ b/lib/rubygems/commands/outdated_command.rb
@@ -1,30 +1,33 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::OutdatedCommand < Gem::Command
+require_relative "../command"
+require_relative "../local_remote_options"
+require_relative "../spec_fetcher"
+require_relative "../version_option"
+class Gem::Commands::OutdatedCommand < Gem::Command
include Gem::LocalRemoteOptions
include Gem::VersionOption
def initialize
- super 'outdated', 'Display all gems that need updates'
+ super "outdated", "Display all gems that need updates"
add_local_remote_options
add_platform_option
end
- def execute
- Gem::Specification.outdated.sort.each do |name|
- local = Gem::Specification.find_all_by_name(name).max
- dep = Gem::Dependency.new local.name, ">= #{local.version}"
- remotes = Gem::SpecFetcher.fetcher.fetch dep
+ def description # :nodoc:
+ <<-EOF
+The outdated command lists gems you may wish to upgrade to a newer version.
- next if remotes.empty?
+You can check for dependency mismatches using the dependency command and
+update the gems with the update or install commands.
+ EOF
+ end
- remote = remotes.last.first
- say "#{local.name} (#{local.version} < #{remote.version})"
+ 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
index 6ebf9aa1aa..675e866734 100644
--- a/lib/rubygems/commands/owner_command.rb
+++ b/lib/rubygems/commands/owner_command.rb
@@ -1,36 +1,65 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/gemcutter_utilities'
+# 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:
- 'Manage gem owners on RubyGems.org.'
+ <<-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', description
+ super "owner", "Manage gem owners of a gem on the push server"
add_proxy_option
add_key_option
- defaults.merge! :add => [], :remove => []
+ add_otp_option
+ defaults.merge! add: [], remove: []
- add_option '-a', '--add EMAIL', 'Add an owner' do |value, options|
+ add_option "-a", "--add NEW_OWNER", "Add an owner by user identifier" do |value, options|
options[:add] << value
end
- add_option '-r', '--remove EMAIL', 'Remove an owner' do |value, options|
+ add_option "-r", "--remove OLD_OWNER", "Remove an owner by user identifier" do |value, options|
options[:remove] << value
end
+
+ add_option "-h", "--host HOST",
+ "Use another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)" do |value, options|
+ options[:host] = value
+ end
end
def execute
- sign_in
+ @host = options[:host]
+
+ sign_in(scope: get_owner_scope)
name = get_one_gem_name
add_owners name, options[:add]
@@ -38,39 +67,59 @@ class Gem::Commands::OwnerCommand < Gem::Command
show_owners name
end
- def show_owners name
+ 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 = YAML.load resp.body
+ owners = Gem::SafeYAML.safe_load clean_text(resp.body)
say "Owners for gem: #{name}"
owners.each do |owner|
- say "- #{owner['email']}"
+ identifier = owner["email"] || owner["handle"] || owner["id"]
+ say "- #{identifier} (#{owner["role"]})"
end
end
end
- def add_owners name, owners
+ def add_owners(name, owners)
manage_owners :post, name, owners
end
- def remove_owners name, owners
+ def remove_owners(name, owners)
manage_owners :delete, name, owners
end
- def manage_owners method, name, owners
+ def manage_owners(method, name, owners)
owners.each do |owner|
- response = rubygems_api_request method, "api/v1/gems/#{name}/owners" do |request|
- request.set_form_data 'email' => owner
- request.add_field "Authorization", api_key
- end
+ response = send_owner_request(method, name, owner)
+ action = method == :delete ? "Removing" : "Adding"
+
+ with_response response, "#{action} #{owner}"
+ rescue Gem::WebauthnVerificationError => e
+ raise e
+ rescue StandardError
+ # ignore early exits to allow for completing the iteration of all owners
+ end
+ end
- with_response response
+ 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
index 272a4dd18e..10978c2af7 100644
--- a/lib/rubygems/commands/pristine_command.rb
+++ b/lib/rubygems/commands/pristine_command.rb
@@ -1,30 +1,73 @@
-require 'rubygems/command'
-require 'rubygems/format'
-require 'rubygems/installer'
-require 'rubygems/version_option'
+# frozen_string_literal: true
-class Gem::Commands::PristineCommand < Gem::Command
+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,
- :all => false
-
- add_option('--all',
- 'Restore all installed gems to pristine',
- 'condition') do |value, options|
+ super "pristine",
+ "Restores installed gems to pristine condition from files located in the gem cache",
+ version: Gem::Requirement.default,
+ extensions: true,
+ extensions_set: false,
+ all: false
+
+ add_option("--all",
+ "Restore all installed gems to pristine",
+ "condition") do |value, options|
options[:all] = value
end
- add_option('--[no-]extensions',
- 'Restore gems with extensions') do |value, options|
- options[:extensions] = value
+ 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_version_option('restore to', 'pristine condition')
+ 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:
@@ -32,75 +75,149 @@ class Gem::Commands::PristineCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--all --extensions"
+ "--extensions"
end
def description # :nodoc:
<<-EOF
-The pristine command compares the installed gems with the contents of the
-cached gem and restores any files that don't match the cached gem's copy.
+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.
-If you have made modifications to your installed gems, the pristine command
-will revert them. After all the gem's files have been checked all bin stubs
-for the gem are regenerated.
+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, you will need to use `gem install` to
-revert the gem.
+If the cached gem cannot be found it will be downloaded.
-If --no-extensions is provided pristine will not attempt to restore gems with
-extensions.
+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} [args]"
+ "#{program_name} [GEMNAME ...]"
end
def execute
- specs = if options[:all] then
- Gem::Specification.map
- else
- get_all_gem_names.map do |gem_name|
- Gem::Specification.find_all_by_name gem_name, options[:version]
- end.flatten
- end
-
- if specs.to_a.empty? then
+ 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
- install_dir = Gem.dir # TODO use installer option
+ say "Restoring gems to pristine condition..."
- raise Gem::FilePermissionError.new(install_dir) unless
- File.writable?(install_dir)
+ specs.group_by(&:full_name_with_location).values.each do |grouped_specs|
+ spec = grouped_specs.find {|s| !s.default_gem? } || grouped_specs.first
- say "Restoring gems to pristine condition..."
+ 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
- specs.each do |spec|
- unless spec.extensions.empty? or options[:extensions] then
- say "Skipped #{spec.full_name}, it needs to compile an extension"
+ unless spec.extensions.empty? || options[:extensions] || only_executables || only_plugins
+ say "Skipped #{spec.full_name_with_location}, it needs to compile an extension"
next
end
gem = spec.cache_file
- unless File.exist? gem then
- require 'rubygems/remote_fetcher'
+ 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..."
- say "Cached gem for #{spec.full_name} not found, attempting to fetch..."
dep = Gem::Dependency.new spec.name, spec.version
- Gem::RemoteFetcher.fetcher.download_to_cache dep
+ 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
- # TODO use installer options
- installer = Gem::Installer.new(gem,
- :wrappers => true,
- :force => true,
- :install_dir => spec.base_dir)
- installer.install
+ 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}"
+ say "Restored #{spec.full_name_with_location}"
end
end
end
diff --git a/lib/rubygems/commands/push_command.rb b/lib/rubygems/commands/push_command.rb
index a7663edf4a..02931b3025 100644
--- a/lib/rubygems/commands/push_command.rb
+++ b/lib/rubygems/commands/push_command.rb
@@ -1,13 +1,24 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/gemcutter_utilities'
+# 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:
- 'Push a gem up to RubyGems.org'
+ <<-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:
@@ -19,42 +30,156 @@ class Gem::Commands::PushCommand < Gem::Command
end
def initialize
- super 'push', description
+ 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'
- ) do |value, options|
+ add_option("--host HOST",
+ "Push to another gemcutter-compatible host",
+ " (e.g. https://rubygems.org)") do |value, options|
options[:host] = value
+ @user_defined_host = true
end
+
+ add_option("--attestation FILE",
+ "Push with sigstore attestations") do |value, options|
+ options[:attestations] << value
+ end
+
+ @host = nil
end
def execute
- sign_in
- send_gem get_one_gem_name
+ 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
+ def send_gem(name)
args = [:post, "api/v1/gems"]
- args << options[:host] if options[:host]
+ _, 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)
- if Gem.latest_rubygems_version < Gem::Version.new(Gem::VERSION) then
- alert_error "Using beta/unreleased version of rubygems. Not pushing."
- terminate_interaction 1
+ 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
- response = rubygems_api_request(*args) do |request|
- request.body = Gem.read_binary name
- request.add_field "Content-Length", request.body.size
+ def send_push_request_without_attestation(name, args)
+ scope = get_push_scope
+ rubygems_api_request(*args, scope: scope) do |request|
+ body = Gem.read_binary name
+ request.body = body
request.add_field "Content-Type", "application/octet-stream"
- request.add_field "Authorization", api_key
+ request.add_field "Content-Length", request.body.size
+ request.add_field "Authorization", api_key
end
+ end
- with_response response
+ 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
-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/query_command.rb b/lib/rubygems/commands/query_command.rb
deleted file mode 100644
index 725da8787b..0000000000
--- a/lib/rubygems/commands/query_command.rb
+++ /dev/null
@@ -1,263 +0,0 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
-require 'rubygems/text'
-
-class Gem::Commands::QueryCommand < Gem::Command
-
- include Gem::Text
- include Gem::LocalRemoteOptions
- include Gem::VersionOption
-
- def initialize(name = 'query',
- summary = 'Query gem information in local or remote repositories')
- super name, summary,
- :name => //, :domain => :local, :details => false, :versions => true,
- :installed => false, :version => Gem::Requirement.default
-
- add_option('-i', '--[no-]installed',
- 'Check for installed gem') do |value, options|
- options[:installed] = value
- end
-
- add_version_option command, "for use with --installed"
-
- add_option('-n', '--name-matches REGEXP',
- 'Name of gem(s) to query on matches the',
- 'provided REGEXP') do |value, options|
- options[:name] = /#{value}/i
- end
-
- add_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( '--[no-]prerelease',
- 'Display prerelease versions') do |value, options|
- options[:prerelease] = value
- end
-
- add_local_remote_options
- end
-
- def defaults_str # :nodoc:
- "--local --name-matches // --no-details --versions --no-installed"
- end
-
- def execute
- exit_code = 0
-
- name = options[:name]
- prerelease = options[:prerelease]
-
- if options[:installed] then
- if name.source.empty? then
- alert_error "You must specify a gem name"
- exit_code |= 4
- elsif installed? name, options[:version] then
- say "true"
- else
- say "false"
- exit_code |= 1
- end
-
- terminate_interaction exit_code
- end
-
- req = Gem::Requirement.default
- # TODO: deprecate for real
- dep = Gem::Deprecate.skip_during { Gem::Dependency.new name, req }
-
- if local? then
- if prerelease and not both? then
- alert_warning "prereleases are always shown locally"
- end
-
- if ui.outs.tty? or both? then
- say
- say "*** LOCAL GEMS ***"
- say
- end
-
- specs = Gem::Specification.find_all { |s|
- s.name =~ name and req =~ s.version
- }
-
- spec_tuples = specs.map do |spec|
- [[spec.name, spec.version, spec.original_platform, spec], :local]
- end
-
- output_query_results spec_tuples
- end
-
- if remote? then
- if ui.outs.tty? or both? then
- say
- say "*** REMOTE GEMS ***"
- say
- end
-
- all = options[:all]
-
- fetcher = Gem::SpecFetcher.fetcher
- spec_tuples = fetcher.find_matching dep, all, false, prerelease
-
- spec_tuples += fetcher.find_matching dep, false, false, true if
- prerelease and all
-
- output_query_results spec_tuples
- end
- end
-
- private
-
- ##
- # Check if gem +name+ version +version+ is installed.
-
- def installed?(name, req = Gem::Requirement.default)
- Gem::Specification.any? { |s| s.name =~ name and req =~ s.version }
- end
-
- def output_query_results(spec_tuples)
- output = []
- versions = Hash.new { |h,name| h[name] = [] }
-
- spec_tuples.each do |spec_tuple, source_uri|
- versions[spec_tuple.first] << [spec_tuple, source_uri]
- end
-
- versions = versions.sort_by do |(name,_),_|
- name.downcase
- end
-
- versions.each do |gem_name, matching_tuples|
- matching_tuples = matching_tuples.sort_by do |(_, version,_),_|
- version
- end.reverse
-
- platforms = Hash.new { |h,version| h[version] = [] }
-
- matching_tuples.map do |(_, version, platform,_),_|
- platforms[version] << platform if platform
- end
-
- seen = {}
-
- matching_tuples.delete_if do |(_, version,_),_|
- if seen[version] then
- true
- else
- seen[version] = true
- false
- end
- end
-
- entry = gem_name.dup
-
- if options[:versions] then
- list = if platforms.empty? or options[:details] then
- matching_tuples.map { |(_, version,_),_| version }.uniq
- else
- platforms.sort.reverse.map do |version, pls|
- if pls == [Gem::Platform::RUBY] then
- version
- else
- ruby = pls.delete Gem::Platform::RUBY
- platform_list = [ruby, *pls.sort].compact
- "#{version} #{platform_list.join ' '}"
- end
- end
- end.join ', '
-
- entry << " (#{list})"
- end
-
- if options[:details] then
- detail_tuple = matching_tuples.first
-
- spec = if detail_tuple.first.length == 4 then
- detail_tuple.first.last
- else
- uri = URI.parse detail_tuple.last
- Gem::SpecFetcher.fetcher.fetch_spec detail_tuple.first, uri
- end
-
- entry << "\n"
-
- non_ruby = platforms.any? do |_, pls|
- pls.any? { |pl| pl != Gem::Platform::RUBY }
- end
-
- if non_ruby then
- if platforms.length == 1 then
- title = platforms.values.length == 1 ? 'Platform' : 'Platforms'
- entry << " #{title}: #{platforms.values.sort.join ', '}\n"
- else
- entry << " Platforms:\n"
- platforms.sort_by do |version,|
- version
- end.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
-
- authors = "Author#{spec.authors.length > 1 ? 's' : ''}: "
- authors << spec.authors.join(', ')
- entry << format_text(authors, 68, 4)
-
- if spec.rubyforge_project and not spec.rubyforge_project.empty? then
- rubyforge = "Rubyforge: http://rubyforge.org/projects/#{spec.rubyforge_project}"
- entry << "\n" << format_text(rubyforge, 68, 4)
- end
-
- if spec.homepage and not spec.homepage.empty? then
- entry << "\n" << format_text("Homepage: #{spec.homepage}", 68, 4)
- end
-
- if spec.license and not spec.license.empty? then
- licenses = "License#{spec.licenses.length > 1 ? 's' : ''}: "
- licenses << spec.licenses.join(', ')
- entry << "\n" << format_text(licenses, 68, 4)
- end
-
- if spec.loaded_from then
- if matching_tuples.length == 1 then
- loaded_from = File.dirname File.dirname(spec.loaded_from)
- entry << "\n" << " Installed at: #{loaded_from}"
- else
- label = 'Installed at'
- matching_tuples.each do |(_,version,_,s),|
- loaded_from = File.dirname File.dirname(s.loaded_from)
- entry << "\n" << " #{label} (#{version}): #{loaded_from}"
- label = ' ' * label.length
- end
- end
- end
-
- entry << "\n\n" << format_text(spec.summary, 68, 4)
- end
- output << entry
- end
-
- say output.join(options[:details] ? "\n\n" : "\n")
- end
-
-end
-
diff --git a/lib/rubygems/commands/rdoc_command.rb b/lib/rubygems/commands/rdoc_command.rb
index ea0f3ad592..62c4bf8ce9 100644
--- a/lib/rubygems/commands/rdoc_command.rb
+++ b/lib/rubygems/commands/rdoc_command.rb
@@ -1,33 +1,36 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/doc_manager'
+# 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 => true, :include_ri => true, :overwrite => false
+ super "rdoc", "Generates RDoc for pre-installed gems",
+ version: Gem::Requirement.default,
+ include_rdoc: false, include_ri: true, overwrite: false
- add_option('--all',
- 'Generate RDoc/RI documentation for all',
- 'installed gems') do |value, options|
+ add_option("--all",
+ "Generate RDoc/RI documentation for all",
+ "installed gems") do |value, options|
options[:all] = value
end
- add_option('--[no-]rdoc',
- 'Generate RDoc HTML') do |value, options|
+ add_option("--[no-]rdoc",
+ "Generate RDoc HTML") do |value, options|
options[:include_rdoc] = value
end
- add_option('--[no-]ri',
- 'Generate RI data') do |value, options|
+ add_option("--[no-]ri",
+ "Generate RI data") do |value, options|
options[:include_ri] = value
end
- add_option('--[no-]overwrite',
- 'Overwrite installed documents') do |value, options|
+ add_option("--[no-]overwrite",
+ "Overwrite installed documents") do |value, options|
options[:overwrite] = value
end
@@ -39,13 +42,17 @@ class Gem::Commands::RdocCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--version '#{Gem::Requirement.default}' --rdoc --ri --no-overwrite"
+ "--version '#{Gem::Requirement.default}' --ri --no-overwrite"
end
def description # :nodoc:
<<-DESC
-The rdoc command builds RDoc and RI documentation for installed gems. Use
---overwrite to force rebuilding of documentation.
+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
@@ -54,38 +61,30 @@ The rdoc command builds RDoc and RI documentation for installed gems. Use
end
def execute
- if options[:all] then
- specs = Gem::SourceIndex.from_installed_gems.collect { |name, spec|
- spec
- }
+ specs = if options[:all]
+ Gem::Specification.to_a
else
- gem_name = get_one_gem_name
- dep = Gem::Dependency.new gem_name, options[:version]
- specs = Gem::SourceIndex.from_installed_gems.search dep
+ get_all_gem_names.flat_map do |name|
+ Gem::Specification.find_by_name name, options[:version]
+ end.uniq
end
if specs.empty?
- raise "Failed to find gem #{gem_name} to generate RDoc for #{options[:version]}"
+ alert_error "No matching gems found"
+ terminate_interaction 1
end
- if options[:include_ri]
- specs.sort.each do |spec|
- doc = Gem::DocManager.new(spec)
- doc.generate_ri if options[:overwrite] || !doc.ri_installed?
- end
+ specs.each do |spec|
+ doc = Gem::RDoc.new spec, options[:include_rdoc], options[:include_ri]
- Gem::DocManager.update_ri_cache
- end
+ doc.force = options[:overwrite]
- if options[:include_rdoc]
- specs.sort.each do |spec|
- doc = Gem::DocManager.new(spec)
- doc.generate_rdoc if options[:overwrite] || !doc.rdoc_installed?
+ if options[:overwrite]
+ FileUtils.rm_rf File.join(spec.doc_dir, "ri")
+ FileUtils.rm_rf File.join(spec.doc_dir, "rdoc")
end
- end
- true
+ doc.generate
+ end
end
-
end
-
diff --git a/lib/rubygems/commands/rebuild_command.rb b/lib/rubygems/commands/rebuild_command.rb
new file mode 100644
index 0000000000..23b9d7b3ba
--- /dev/null
+++ b/lib/rubygems/commands/rebuild_command.rb
@@ -0,0 +1,261 @@
+# frozen_string_literal: true
+
+require "digest"
+require "fileutils"
+require "tmpdir"
+require_relative "../gemspec_helpers"
+require_relative "../package"
+
+class Gem::Commands::RebuildCommand < Gem::Command
+ include Gem::GemspecHelpers
+
+ def initialize
+ super "rebuild", "Attempt to reproduce a build of a gem."
+
+ add_option "--diff", "If the files don't match, compare them using diffoscope." do |_value, options|
+ options[:diff] = true
+ end
+
+ add_option "--force", "Skip validation of the spec." do |_value, options|
+ options[:force] = true
+ end
+
+ add_option "--strict", "Consider warnings as errors when validating the spec." do |_value, options|
+ options[:strict] = true
+ end
+
+ add_option "--source GEM_SOURCE", "Specify the source to download the gem from." do |value, options|
+ options[:source] = value
+ end
+
+ add_option "--original GEM_FILE", "Specify a local file to compare against (instead of downloading it)." do |value, options|
+ options[:original_gem_file] = value
+ end
+
+ add_option "--gemspec GEMSPEC_FILE", "Specify the name of the gemspec file." do |value, options|
+ options[:gemspec_file] = value
+ end
+
+ add_option "-C PATH", "Run as if gem build was started in <PATH> instead of the current working directory." do |value, options|
+ options[:build_path] = value
+ end
+ end
+
+ def arguments # :nodoc:
+ "GEM_NAME gem name on gem server\n" \
+ "GEM_VERSION gem version you are attempting to rebuild"
+ end
+
+ def description # :nodoc:
+ <<-EOF
+The rebuild command allows you to (attempt to) reproduce a build of a gem
+from a ruby gemspec.
+
+This command assumes the gemspec can be built with the `gem build` command.
+If you use any of `gem build`, `rake build`, or`rake release` in the
+build/release process for a gem, it is a potential candidate.
+
+You will need to match the RubyGems version used, since this is included in
+the Gem metadata.
+
+If the gem includes lockfiles (e.g. Gemfile.lock) and similar, it will
+require more effort to reproduce a build. For example, it might require
+more precisely matched versions of Ruby and/or Bundler to be used.
+ EOF
+ end
+
+ def usage # :nodoc:
+ "#{program_name} GEM_NAME GEM_VERSION"
+ end
+
+ def execute
+ gem_name, gem_version = get_gem_name_and_version
+
+ old_dir, new_dir = prep_dirs
+
+ gem_filename = "#{gem_name}-#{gem_version}.gem"
+ old_file = File.join(old_dir, gem_filename)
+ new_file = File.join(new_dir, gem_filename)
+
+ if options[:original_gem_file]
+ FileUtils.copy_file(options[:original_gem_file], old_file)
+ else
+ download_gem(gem_name, gem_version, old_file)
+ end
+
+ rg_version = rubygems_version(old_file)
+ unless rg_version == Gem::VERSION
+ alert_error <<-EOF
+You need to use the same RubyGems version #{gem_name} v#{gem_version} was built with.
+
+#{gem_name} v#{gem_version} was built using RubyGems v#{rg_version}.
+Gem files include the version of RubyGems used to build them.
+This means in order to reproduce #{gem_filename}, you must also use RubyGems v#{rg_version}.
+
+You're using RubyGems v#{Gem::VERSION}.
+
+Please install RubyGems v#{rg_version} and try again.
+ EOF
+ terminate_interaction 1
+ end
+
+ source_date_epoch = get_timestamp(old_file).to_s
+
+ if build_path = options[:build_path]
+ Dir.chdir(build_path) { build_gem(gem_name, source_date_epoch, new_file) }
+ else
+ build_gem(gem_name, source_date_epoch, new_file)
+ end
+
+ compare(source_date_epoch, old_file, new_file)
+ end
+
+ private
+
+ def sha256(file)
+ Digest::SHA256.hexdigest(Gem.read_binary(file))
+ end
+
+ def get_timestamp(file)
+ mtime = nil
+ File.open(file, Gem.binary_mode) do |f|
+ Gem::Package::TarReader.new(f) do |tar|
+ mtime = tar.seek("metadata.gz") {|tf| tf.header.mtime }
+ end
+ end
+
+ mtime
+ end
+
+ def compare(source_date_epoch, old_file, new_file)
+ date = Time.at(source_date_epoch.to_i).strftime("%F %T %Z")
+
+ old_hash = sha256(old_file)
+ new_hash = sha256(new_file)
+
+ say
+ say "Built at: #{date} (#{source_date_epoch})"
+ say "Original build saved to: #{old_file}"
+ say "Reproduced build saved to: #{new_file}"
+ say "Working directory: #{options[:build_path] || Dir.pwd}"
+ say
+ say "Hash comparison:"
+ say " #{old_hash}\t#{old_file}"
+ say " #{new_hash}\t#{new_file}"
+ say
+
+ if old_hash == new_hash
+ say "SUCCESS - original and rebuild hashes matched"
+ else
+ say "FAILURE - original and rebuild hashes did not match"
+ say
+
+ if options[:diff]
+ if system("diffoscope", old_file, new_file).nil?
+ alert_error "error: could not find `diffoscope` executable"
+ end
+ else
+ say "Pass --diff for more details (requires diffoscope to be installed)."
+ end
+
+ terminate_interaction 1
+ end
+ end
+
+ def prep_dirs
+ rebuild_dir = Dir.mktmpdir("gem_rebuild")
+ old_dir = File.join(rebuild_dir, "old")
+ new_dir = File.join(rebuild_dir, "new")
+
+ FileUtils.mkdir_p(old_dir)
+ FileUtils.mkdir_p(new_dir)
+
+ [old_dir, new_dir]
+ end
+
+ def get_gem_name_and_version
+ args = options[:args] || []
+ if args.length == 2
+ gem_name, gem_version = args
+ elsif args.length > 2
+ raise Gem::CommandLineError, "Too many arguments"
+ else
+ raise Gem::CommandLineError, "Expected GEM_NAME and GEM_VERSION arguments (gem rebuild GEM_NAME GEM_VERSION)"
+ end
+
+ [gem_name, gem_version]
+ end
+
+ def build_gem(gem_name, source_date_epoch, output_file)
+ gemspec = options[:gemspec_file] || find_gemspec("#{gem_name}.gemspec")
+
+ if gemspec
+ build_package(gemspec, source_date_epoch, output_file)
+ else
+ alert_error error_message(gem_name)
+ terminate_interaction(1)
+ end
+ end
+
+ def build_package(gemspec, source_date_epoch, output_file)
+ with_source_date_epoch(source_date_epoch) do
+ spec = Gem::Specification.load(gemspec)
+ if spec
+ Gem::Package.build(
+ spec,
+ options[:force],
+ options[:strict],
+ output_file
+ )
+ else
+ alert_error "Error loading gemspec. Aborting."
+ terminate_interaction 1
+ end
+ end
+ end
+
+ def with_source_date_epoch(source_date_epoch)
+ old_sde = ENV["SOURCE_DATE_EPOCH"]
+ ENV["SOURCE_DATE_EPOCH"] = source_date_epoch.to_s
+
+ yield
+ ensure
+ ENV["SOURCE_DATE_EPOCH"] = old_sde
+ end
+
+ def error_message(gem_name)
+ if gem_name
+ "Couldn't find a gemspec file matching '#{gem_name}' in #{Dir.pwd}"
+ else
+ "Couldn't find a gemspec file in #{Dir.pwd}"
+ end
+ end
+
+ def download_gem(gem_name, gem_version, old_file)
+ # This code was based loosely off the `gem fetch` command.
+ version = "= #{gem_version}"
+ dep = Gem::Dependency.new gem_name, version
+
+ specs_and_sources, errors =
+ Gem::SpecFetcher.fetcher.spec_for_dependency dep
+
+ # There should never be more than one item in specs_and_sources,
+ # since we search for an exact version.
+ spec, source = specs_and_sources[0]
+
+ if spec.nil?
+ show_lookup_failure gem_name, version, errors, options[:domain]
+ terminate_interaction 1
+ end
+
+ download_path = source.download spec
+
+ FileUtils.move(download_path, old_file)
+
+ say "Downloaded #{gem_name} version #{gem_version} as #{old_file}."
+ end
+
+ def rubygems_version(gem_file)
+ Gem::Package.new(gem_file).spec.rubygems_version
+ end
+end
diff --git a/lib/rubygems/commands/search_command.rb b/lib/rubygems/commands/search_command.rb
index 52e96fd1ef..50e161ac9b 100644
--- a/lib/rubygems/commands/search_command.rb
+++ b/lib/rubygems/commands/search_command.rb
@@ -1,31 +1,41 @@
-require 'rubygems/command'
-require 'rubygems/commands/query_command'
+# frozen_string_literal: true
-class Gem::Commands::SearchCommand < Gem::Commands::QueryCommand
+require_relative "../command"
+require_relative "../query_utils"
+
+class Gem::Commands::SearchCommand < Gem::Command
+ include Gem::QueryUtils
def initialize
- super 'search', 'Display all gems whose name contains STRING'
+ super "search", "Display remote gems whose name matches REGEXP",
+ domain: :remote, details: false, versions: true,
+ installed: nil, version: Gem::Requirement.default
- remove_option '--name-matches'
+ add_query_options
end
def arguments # :nodoc:
- "STRING fragment of gem name to search for"
+ "REGEXP regexp to search for in gem name"
end
def defaults_str # :nodoc:
- "--local --no-details"
+ "--remote --no-details"
end
- def usage # :nodoc:
- "#{program_name} [STRING]"
- 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.
- def execute
- string = get_one_optional_argument
- options[:name] = /#{string}/i
- super
+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
index b65d48c4fc..f1dde4aa02 100644
--- a/lib/rubygems/commands/server_command.rb
+++ b/lib/rubygems/commands/server_command.rb
@@ -1,86 +1,26 @@
-require 'rubygems/command'
-require 'rubygems/server'
-
-class Gem::Commands::ServerCommand < Gem::Command
-
- def initialize
- super 'server', 'Documentation and gem repository HTTP server',
- :port => 8808, :gemdir => [], :daemon => false
-
- OptionParser.accept :Port do |port|
- if port =~ /\A\d+\z/ then
- port = Integer port
- raise OptionParser::InvalidArgument, "#{port}: not a port number" if
- port > 65535
-
- port
- else
- begin
- Socket.getservbyname port
- rescue SocketError
- raise OptionParser::InvalidArgument, "#{port}: no such named service"
- end
+# 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
- add_option '-p', '--port=PORT', :Port,
- 'port to listen on' do |port, options|
- options[:port] = port
- end
-
- add_option '-d', '--dir=GEMDIR',
- 'directories from which to serve gems',
- 'multiple directories may be provided' do |gemdir, options|
- options[:gemdir] << File.expand_path(gemdir)
+ def description # :nodoc:
+ <<-EOF
+The server command has been moved to the rubygems-server gem.
+ EOF
end
- add_option '--[no-]daemon', 'run as a daemon' do |daemon, options|
- options[:daemon] = daemon
+ def execute
+ alert_error "Install the rubygems-server gem for the server command"
end
-
- add_option '-b', '--bind=HOST,HOST',
- 'addresses to bind', Array do |address, options|
- options[:addresses] ||= []
- options[:addresses].push(*address)
- end
-
- add_option '-l', '--launch[=COMMAND]',
- 'launches a browser window',
- "COMMAND defaults to 'start' on Windows",
- "and 'open' on all other platforms" do |launch, options|
- launch ||= Gem.win_platform? ? 'start' : 'open'
- options[:launch] = launch
- end
- end
-
- def defaults_str # :nodoc:
- "--port 8808 --dir #{Gem.dir} --no-daemon"
end
-
- def description # :nodoc:
- <<-EOF
-The server command starts up a web server that hosts the RDoc for your
-installed gems and can operate as a server for installation of gems on other
-machines.
-
-The cache files for installed gems must exist to use the server as a source
-for gem installation.
-
-To install gems from a running server, use `gem install GEMNAME --source
-http://gem_server_host:8808`
-
-You can set up a shortcut to gem server documentation using the URL:
-
- http://localhost:8808/rdoc?q=%s - Firefox
- http://localhost:8808/rdoc?q=* - LaunchBar
-
- EOF
- end
-
- def execute
- options[:gemdir] << Gem.dir if options[:gemdir].empty?
- Gem::Server.run options
- end
-
end
-
diff --git a/lib/rubygems/commands/setup_command.rb b/lib/rubygems/commands/setup_command.rb
index 52a3b88fe3..175599967c 100644
--- a/lib/rubygems/commands/setup_command.rb
+++ b/lib/rubygems/commands/setup_command.rb
@@ -1,64 +1,114 @@
-require 'rubygems/command'
+# 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*$}
- def initialize
- require 'tmpdir'
+ ENV_PATHS = %w[/usr/bin/env /bin/env].freeze
- super 'setup', 'Install RubyGems',
- :format_executable => true, :rdoc => true, :ri => true,
- :site_or_vendor => :sitelibdir,
- :destdir => '', :prefix => ''
+ 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|
+ add_option "--prefix=PREFIX",
+ "Prefix path for installing RubyGems",
+ "Will not affect gem repository location" do |prefix, options|
options[:prefix] = File.expand_path prefix
end
- add_option '--destdir=DESTDIR',
- 'Root directory to install RubyGems into',
- 'Mainly used for packaging RubyGems' do |destdir, options|
+ add_option "--destdir=DESTDIR",
+ "Root directory to install RubyGems into",
+ "Mainly used for packaging RubyGems" do |destdir, options|
options[:destdir] = File.expand_path destdir
end
- add_option '--[no-]vendor',
- 'Install into vendorlibdir not sitelibdir' do |vendor, options|
- options[:site_or_vendor] = vendor ? :vendorlibdir : :sitelibdir
+ add_option "--[no-]vendor",
+ "Install into vendorlibdir not sitelibdir" do |vendor, options|
+ options[:site_or_vendor] = vendor ? "vendorlibdir" : "sitelibdir"
end
- add_option '--[no-]format-executable',
- 'Makes `gem` match ruby',
- 'If ruby is ruby18, gem will be gem18' do |value, options|
+ add_option "--[no-]format-executable",
+ "Makes `gem` match ruby",
+ "If Ruby is ruby18, gem will be gem18" do |value, options|
options[:format_executable] = value
end
- add_option '--[no-]rdoc',
- 'Generate RDoc documentation for RubyGems' do |value, options|
- options[:rdoc] = value
+ 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-]ri',
- 'Generate RI documentation for RubyGems' do |value, options|
- options[:ri] = value
+ 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
- end
- def check_ruby_version
- required_version = Gem::Requirement.new '>= 1.8.7'
+ add_option "--[no-]regenerate-plugins",
+ "Regenerate gem plugins" do |value, options|
+ options[:regenerate_plugins] = value
+ end
- unless required_version.satisfied_by? Gem.ruby_version then
- alert_error "Expected Ruby version #{required_version}, is #{Gem.ruby_version}"
- terminate_interaction 1
+ 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 --rdoc --ri"
+ "--format-executable --document ri --regenerate-binstubs"
end
def description # :nodoc:
@@ -75,30 +125,30 @@ prefix and suffix. If ruby was installed as `ruby18`, gem will be
installed as `gem18`.
By default, this RubyGems will install gem as:
- #{Gem.default_exec_format % 'gem'}
+ #{Gem.default_exec_format % "gem"}
EOF
end
- def execute
- @verbose = Gem.configuration.really_verbose
-
- install_destdir = options[:destdir]
-
- unless install_destdir.empty? then
- ENV['GEM_HOME'] ||= File.join(install_destdir,
- Gem.default_dir.gsub(/^[a-zA-Z]:/, ''))
+ module MakeDirs
+ def mkdir_p(path, **opts)
+ super
+ (@mkdirs ||= []) << path
end
+ end
- check_ruby_version
+ def execute
+ @verbose = Gem.configuration.really_verbose
- require 'fileutils'
- if Gem.configuration.really_verbose then
+ require "fileutils"
+ if Gem.configuration.really_verbose
extend FileUtils::Verbose
else
extend FileUtils
end
+ extend MakeDirs
- lib_dir, bin_dir = make_destination_dirs install_destdir
+ lib_dir, bin_dir = make_destination_dirs
+ man_dir = generate_default_man_dir
install_lib lib_dir
@@ -106,77 +156,106 @@ By default, this RubyGems will install gem as:
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
- install_rdoc
+ documentation_success = install_rdoc
say
- if @verbose then
+ if @verbose
say "-" * 78
say
end
- release_notes = File.join Dir.pwd, 'History.txt'
+ if options[:previous_version].empty?
+ options[:previous_version] = Gem::VERSION.sub(/[0-9]+$/, "0")
+ end
- release_notes = if File.exist? release_notes then
- open release_notes do |io|
- text = io.gets '==='
- text << io.gets('===')
- text[0...-3].sub(/^# coding:.*?^=/m, '')
- end
- else
- "Oh-no! Unable to find release notes!"
- end
+ options[:previous_version] = Gem::Version.new(options[:previous_version])
- say release_notes
+ show_release_notes
say
say "-" * 78
say
say "RubyGems installed the following executables:"
- say @bin_file_names.map { |name| "\t#{name}\n" }
+ say bin_file_names.map {|name| "\t#{name}\n" }
say
- unless @bin_file_names.grep(/#{File::SEPARATOR}gem$/) then
+ 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)
- say "Installing gem executable" if @verbose
-
- @bin_file_names = []
+ prog_mode = options[:prog_mode] || 0o755
- Dir.chdir 'bin' do
- bin_files = Dir['*']
+ executables = { "gem" => "exe" }
+ executables.each do |tool, path|
+ say "Installing #{tool} executable" if @verbose
- bin_files.delete 'update_rubygems'
+ Dir.chdir path do
+ bin_file = "gem"
- bin_files.each do |bin_file|
- bin_file_formatted = if options[:format_executable] then
- Gem.default_exec_format % bin_file
- else
- bin_file
- end
+ require "tmpdir"
- dest_file = File.join bin_dir, bin_file_formatted
- bin_tmp_file = File.join Dir.tmpdir, bin_file
+ dest_file = target_bin_path(bin_dir, bin_file)
+ bin_tmp_file = File.join Dir.tmpdir, "#{bin_file}.#{$$}"
begin
bin = File.readlines bin_file
- bin[0] = "#!#{Gem.ruby}\n"
+ bin[0] = shebang
- File.open bin_tmp_file, 'w' do |fp|
+ File.open bin_tmp_file, "w" do |fp|
fp.puts bin.join
end
- install bin_tmp_file, dest_file, :mode => 0755
- @bin_file_names << dest_file
+ install bin_tmp_file, dest_file, mode: prog_mode
+ bin_file_names << dest_file
ensure
rm bin_tmp_file
end
@@ -186,18 +265,14 @@ By default, this RubyGems will install gem as:
begin
bin_cmd_file = File.join Dir.tmpdir, "#{bin_file}.bat"
- File.open bin_cmd_file, 'w' do |file|
+ File.open bin_cmd_file, "w" do |file|
file.puts <<-TEXT
-@ECHO OFF
-IF NOT "%~f0" == "~f0" GOTO :WinNT
-@"#{File.basename(Gem.ruby).chomp('"')}" "#{dest_file}" %1 %2 %3 %4 %5 %6 %7 %8 %9
-GOTO :EOF
-:WinNT
-@"#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
-TEXT
+ @ECHO OFF
+ @"%~dp0#{File.basename(Gem.ruby).chomp('"')}" "%~dpn0" %*
+ TEXT
end
- install bin_cmd_file, "#{dest_file}.bat", :mode => 0755
+ install bin_cmd_file, "#{dest_file}.bat", mode: prog_mode
ensure
rm bin_cmd_file
end
@@ -205,107 +280,208 @@ TEXT
end
end
- def install_lib(lib_dir)
- say "Installing RubyGems" if @verbose
+ 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
- Dir.chdir 'lib' do
- lib_files = Dir[File.join('**', '*rb')]
+ def install_lib(lib_dir)
+ libs = { "RubyGems" => "lib" }
+ libs["Bundler"] = "bundler/lib"
+ libs.each do |tool, path|
+ say "Installing #{tool}" if @verbose
- lib_files.each do |lib_file|
- dest_file = File.join lib_dir, lib_file
- dest_dir = File.dirname dest_file
- mkdir_p dest_dir unless File.directory? dest_dir
+ lib_files = files_in path
- install lib_file, dest_file, :mode => 0644
+ 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'
+ gem_doc_dir = File.join Gem.dir, "doc"
rubygems_name = "rubygems-#{Gem::VERSION}"
rubygems_doc_dir = File.join gem_doc_dir, rubygems_name
- if File.writable? gem_doc_dir and
- (not File.exist? rubygems_doc_dir or
- File.writable? rubygems_doc_dir) then
+ 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|
+ Dir[File.join(Gem.dir, "doc", "rubygems-[0-9]*")].each do |dir|
rm_rf dir
end
- if options[:ri] then
- ri_dir = File.join rubygems_doc_dir, 'ri'
- say "Installing #{rubygems_name} ri into #{ri_dir}" if @verbose
- run_rdoc '--ri', '--op', ri_dir
- end
+ require_relative "../rdoc"
+
+ return false unless defined?(Gem::RDoc)
- if options[:rdoc] then
- rdoc_dir = File.join rubygems_doc_dir, 'rdoc'
- say "Installing #{rubygems_name} rdoc into #{rdoc_dir}" if @verbose
- run_rdoc '--op', rdoc_dir
+ fake_spec = Gem::Specification.new "rubygems", Gem::VERSION
+ def fake_spec.full_gem_path
+ File.expand_path "../../..", __dir__
end
- elsif @verbose then
+
+ 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 make_destination_dirs(install_destdir)
- lib_dir = nil
- bin_dir = nil
+ 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 }
- prefix = options[:prefix]
- site_or_vendor = options[:site_or_vendor]
+ Gem::Specification.remove_spec current_default_spec
+ loaded_from = current_default_spec.loaded_from
+ File.delete(loaded_from)
- if prefix.empty? then
- lib_dir = Gem::ConfigMap[site_or_vendor]
- bin_dir = Gem::ConfigMap[:bindir]
+ # 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
- # Apple installed RubyGems into libdir, and RubyGems <= 1.1.0 gets
- # confused about installation location, so switch back to
- # sitelibdir/vendorlibdir.
- if defined?(APPLE_GEM_HOME) and
- # just in case Apple and RubyGems don't get this patched up proper.
- (prefix == Gem::ConfigMap[:libdir] or
- # this one is important
- prefix == File.join(Gem::ConfigMap[:libdir], 'ruby')) then
- lib_dir = Gem::ConfigMap[site_or_vendor]
- bin_dir = Gem::ConfigMap[:bindir]
- else
- lib_dir = File.join prefix, 'lib'
- bin_dir = File.join prefix, 'bin'
+ 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
- unless install_destdir.empty? then
- lib_dir = File.join install_destdir, lib_dir.gsub(/^[a-zA-Z]:/, '')
- bin_dir = File.join install_destdir, bin_dir.gsub(/^[a-zA-Z]:/, '')
+ 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
- mkdir_p lib_dir
- mkdir_p bin_dir
+ [prepend_destdir_if_present(lib_dir), prepend_destdir_if_present(bin_dir)]
+ end
- return lib_dir, bin_dir
+ 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',
+ "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."
+ deprecation_message = "`#{old_bin_file}` has been deprecated. Use `#{new_name}` instead."
- File.open old_bin_path, 'w' do |fp|
+ File.open old_bin_path, "w" do |fp|
fp.write <<-EOF
#!#{Gem.ruby}
@@ -315,37 +491,177 @@ abort "#{deprecation_message}"
next unless Gem.win_platform?
- File.open "#{old_bin_path}.bat", 'w' do |fp|
- fp.puts %{@ECHO.#{deprecation_message}}
+ File.open "#{old_bin_path}.bat", "w" do |fp|
+ fp.puts %(@ECHO.#{deprecation_message})
end
end
end
- def run_rdoc(*args)
- begin
- gem 'rdoc'
- rescue Gem::LoadError
+ 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
- require 'rdoc/rdoc'
+ def remove_old_man_files(old_man_dir)
+ old_man1_dir = "#{old_man_dir}/man1"
- args << '--main' << 'README.rdoc' << '--quiet'
- args << '.'
- args << 'README.rdoc' << 'UPGRADING.rdoc'
- args << 'LICENSE.txt' << 'MIT.txt' << 'History.txt'
+ if File.exist?(old_man1_dir)
+ man1_to_remove = Dir.chdir(old_man1_dir) { Dir["bundle*.1{,.txt,.ronn}"] }
- r = RDoc::RDoc.new
- r.document args
+ 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 'rubygems/uninstaller'
+ require_relative "../uninstaller"
- ui = Gem::Uninstaller.new('gemcutter', :all => true, :ignore => true,
- :version => '< 0.4')
+ ui = Gem::Uninstaller.new("gemcutter", all: true, ignore: true,
+ version: "< 0.4")
ui.uninstall
rescue Gem::InstallError
end
-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
index ac14313e9d..b399af2bd3 100644
--- a/lib/rubygems/commands/sources_command.rb
+++ b/lib/rubygems/commands/sources_command.rb
@@ -1,136 +1,348 @@
-require 'rubygems/command'
-require 'rubygems/remote_fetcher'
-require 'rubygems/spec_fetcher'
-require 'rubygems/local_remote_options'
+# frozen_string_literal: true
-class Gem::Commands::SourcesCommand < Gem::Command
+require_relative "../command"
+require_relative "../remote_fetcher"
+require_relative "../spec_fetcher"
+require_relative "../local_remote_options"
+class Gem::Commands::SourcesCommand < Gem::Command
include Gem::LocalRemoteOptions
def initialize
- require 'fileutils'
+ require "fileutils"
- super 'sources',
- 'Manage the sources and cache file RubyGems uses to search for gems'
+ super "sources",
+ "Manage the sources and cache file RubyGems uses to search for gems"
- add_option '-a', '--add SOURCE_URI', 'Add source' do |value, options|
+ add_option "-a", "--add SOURCE_URI", "Add source" do |value, options|
options[:add] = value
end
- add_option '-l', '--list', 'List sources' do |value, options|
+ add_option "--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|
+ add_option "-r", "--remove SOURCE_URI", "Remove source" do |value, options|
options[:remove] = value
end
- add_option '-c', '--clear-all',
- 'Remove all sources (clear the cache)' do |value, options|
+ add_option "-c", "--clear-all", "Remove all sources (clear the cache)" do |value, options|
options[:clear_all] = value
end
- add_option '-u', '--update', 'Update source cache' do |value, options|
+ add_option "-u", "--update", "Update source cache" do |value, options|
options[:update] = value
end
+ add_option "-f", "--[no-]force", "Do not show any confirmation prompts and behave as if 'yes' was always answered" do |value, options|
+ options[:force] = value
+ end
+
add_proxy_option
end
- def defaults_str
- '--list'
+ 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 execute
- options[:list] = !(options[:add] ||
- options[:clear_all] ||
- options[:remove] ||
- options[:update])
+ def append_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
- if options[:clear_all] then
- path = Gem::SpecFetcher.fetcher.dir
- FileUtils.rm_rf path
+ begin
+ source.load_specs :released
+ was_present = Gem.sources.include?(source)
+ Gem.sources.append source
+ Gem.configuration.write
- unless File.exist? path then
- say "*** Removed specs cache ***"
+ if was_present
+ say "#{source_uri} moved to end of sources"
else
- unless File.writable? path then
- say "*** Unable to remove source cache (write protected) ***"
- else
- say "*** Unable to remove source cache ***"
- end
-
- terminate_interaction 1
+ 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
- if options[:add] then
- source_uri = options[:add]
- uri = URI.parse source_uri
+ def prepend_source(source_uri) # :nodoc:
+ source = build_new_source(source_uri)
+ source_uri = source.uri.to_s
- begin
- Gem::SpecFetcher.fetcher.load_specs uri, 'specs'
- Gem.sources << source_uri
- Gem.configuration.write
+ 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"
- rescue URI::Error, ArgumentError
- say "#{source_uri} is not a URI"
- terminate_interaction 1
- rescue Gem::RemoteFetcher::FetchError => e
- say "Error fetching #{source_uri}:\n\t#{e.message}"
- terminate_interaction 1
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
- if options[:remove] then
- source_uri = options[:remove]
+ def check_typo_squatting(source)
+ if source.typo_squatting?("rubygems.org")
+ question = <<-QUESTION.chomp
+#{source.uri} is too similar to https://rubygems.org
- unless Gem.sources.include? source_uri then
- say "source #{source_uri} not present in cache"
- else
- Gem.sources.delete source_uri
- Gem.configuration.write
+Do you want to add this source?
+ QUESTION
- say "#{source_uri} removed from sources"
- end
+ 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
- if options[:update] then
- fetcher = Gem::SpecFetcher.fetcher
+ def clear_all # :nodoc:
+ path = Gem.spec_cache_dir
+ FileUtils.rm_rf path
- Gem.sources.each do |update_uri|
- update_uri = URI.parse update_uri
- fetcher.load_specs update_uri, 'specs'
- fetcher.load_specs update_uri, 'latest_specs'
+ if File.exist? path
+ if File.writable? path
+ say "*** Unable to remove source cache ***"
+ else
+ say "*** Unable to remove source cache (write protected) ***"
end
- say "source cache successfully updated"
+ 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
- if options[:list] then
- say "*** CURRENT SOURCES ***"
- say
+ 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]
- Gem.sources.each do |source|
- say source
+ 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
- private
+ def update # :nodoc:
+ Gem.sources.each_source do |src|
+ src.load_specs :released
+ src.load_specs :latest
+ end
- def remove_cache_file(desc, path)
+ say "source cache successfully updated"
+ end
+
+ def remove_cache_file(desc, path) # :nodoc:
FileUtils.rm_rf path
- if not File.exist?(path) then
+ if !File.exist?(path)
say "*** Removed #{desc} source cache ***"
- elsif not File.writable?(path) then
+ elsif !File.writable?(path)
say "*** Unable to remove #{desc} source cache (write protected) ***"
else
say "*** Unable to remove #{desc} source cache ***"
end
end
-end
+ private
+
+ def default_sources
+ Gem::SourceList.from(Gem.default_sources)
+ end
+
+ def configured_sources
+ return @configured_sources if defined?(@configured_sources)
+
+ configuration_sources = Gem.configuration.sources
+ @configured_sources = Gem::SourceList.from(configuration_sources) if configuration_sources
+ end
+ def config_file_name
+ Gem.configuration.config_file_name
+ end
+
+ def build_source(source_uri)
+ source_uri = normalize_source_uri(source_uri)
+ Gem::Source.new(source_uri)
+ end
+
+ def build_new_source(source_uri)
+ source = build_source(source_uri)
+ check_rubygems_https(source.uri.to_s)
+ check_typo_squatting(source)
+ source
+ end
+end
diff --git a/lib/rubygems/commands/specification_command.rb b/lib/rubygems/commands/specification_command.rb
index 921fd519e9..15e543f1a6 100644
--- a/lib/rubygems/commands/specification_command.rb
+++ b/lib/rubygems/commands/specification_command.rb
@@ -1,37 +1,39 @@
-require 'rubygems/command'
-require 'rubygems/local_remote_options'
-require 'rubygems/version_option'
-require 'rubygems/format'
+# frozen_string_literal: true
-class Gem::Commands::SpecificationCommand < Gem::Command
+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
+ super "specification", "Display gem specification (in yaml)",
+ domain: :local, version: Gem::Requirement.default,
+ format: :yaml
- add_version_option('examine')
+ add_version_option("examine")
add_platform_option
+ add_prerelease_option
- add_option('--all', 'Output specifications for all versions of',
- 'the gem') do |value, options|
+ add_option("--all", "Output specifications for all versions of",
+ "the gem") do |_value, options|
options[:all] = true
end
- add_option('--ruby', 'Output ruby format') do |value, options|
+ add_option("--ruby", "Output ruby format") do |_value, options|
options[:format] = :ruby
end
- add_option('--yaml', 'Output RUBY format') do |value, options|
+ add_option("--yaml", "Output YAML format") do |_value, options|
options[:format] = :yaml
end
- add_option('--marshal', 'Output Marshal format') do |value, options|
+ add_option("--marshal", "Output Marshal format") do |_value, options|
options[:format] = :marshal
end
@@ -40,7 +42,7 @@ class Gem::Commands::SpecificationCommand < Gem::Command
def arguments # :nodoc:
<<-ARGS
-GEMFILE name of gem to show the gemspec for
+GEM_OR_FILE gem name or a .gem file to show the gemspec for
FIELD name of gemspec field to show
ARGS
end
@@ -49,49 +51,94 @@ FIELD name of gemspec field to show
"--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} [GEMFILE] [FIELD]"
+ "#{program_name} [GEM_OR_FILE] [FIELD]"
end
def execute
specs = []
gem = options[:args].shift
- unless gem then
+ unless gem
raise Gem::CommandLineError,
- "Please specify a gem name or file on the command line"
+ "Please specify a gem name or a .gem file on the command line"
end
- dep = Gem::Dependency.new gem, options[:version]
+ 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 and options[:format] == :ruby
-
- if local? then
- if File.exist? gem then
- specs << Gem::Format.from_file_by_path(gem).spec rescue nil
+ 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? then
+ if specs.empty?
specs.push(*dep.matching_specs)
end
end
- if remote? then
- found = Gem::SpecFetcher.fetcher.fetch dep
+ if remote?
+ dep.prerelease = options[:prerelease]
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dep
- specs.push(*found.map { |spec,| spec })
+ specs.push(*found.map {|spec,| spec })
end
- if specs.empty? then
- alert_error "Unknown gem '#{gem}'"
+ if specs.empty?
+ alert_error "No gem matching '#{dep}' found"
terminate_interaction 1
end
- unless options[:all] then
- specs = [specs.sort_by { |s| s.version }.last]
+ 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|
@@ -100,8 +147,8 @@ FIELD name of gemspec field to show
say case options[:format]
when :ruby then s.to_ruby
when :marshal then Marshal.dump s
- else s.to_yaml
- end
+ else Gem.use_psych? ? s.to_yaml : Gem::YAMLSerializer.dump(s)
+ end
say "\n"
end
diff --git a/lib/rubygems/commands/stale_command.rb b/lib/rubygems/commands/stale_command.rb
index 36c517e27c..0be2b85159 100644
--- a/lib/rubygems/commands/stale_command.rb
+++ b/lib/rubygems/commands/stale_command.rb
@@ -1,12 +1,24 @@
-require 'rubygems/command'
+# frozen_string_literal: true
+
+require_relative "../command"
class Gem::Commands::StaleCommand < Gem::Command
def initialize
- super('stale', 'List gems along with access times')
+ super("stale", "List gems along with access times")
+ end
+
+ def description # :nodoc:
+ <<-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}"
+ program_name.to_s
end
def execute
@@ -21,8 +33,8 @@ class Gem::Commands::StaleCommand < Gem::Command
end
end
- gem_to_atime.sort_by { |_, atime| atime }.each do |name, atime|
- say "#{name} at #{atime.strftime '%c'}"
+ 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
index aaadb762b5..3c26074f93 100644
--- a/lib/rubygems/commands/uninstall_command.rb
+++ b/lib/rubygems/commands/uninstall_command.rb
@@ -1,6 +1,9 @@
-require 'rubygems/command'
-require 'rubygems/version_option'
-require 'rubygems/uninstaller'
+# frozen_string_literal: true
+
+require_relative "../command"
+require_relative "../version_option"
+require_relative "../uninstaller"
+require "fileutils"
##
# Gem uninstaller command line tool
@@ -8,54 +11,83 @@ require 'rubygems/uninstaller'
# 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
+ super "uninstall", "Uninstall gems from the local repository",
+ version: Gem::Requirement.default, user_install: true,
+ check_dev: false, vendor: false
- add_option('-a', '--[no-]all',
- 'Uninstall all matching versions'
- ) do |value, options|
+ add_option("-a", "--[no-]all",
+ "Uninstall all matching versions") do |value, options|
options[:all] = value
end
- add_option('-I', '--[no-]ignore-dependencies',
- 'Ignore dependency requirements while',
- 'uninstalling') do |value, options|
+ add_option("-I", "--[no-]ignore-dependencies",
+ "Ignore dependency requirements while",
+ "uninstalling") do |value, options|
options[:ignore] = value
end
- add_option('-x', '--[no-]executables',
- 'Uninstall applicable executables without',
- 'confirmation') do |value, options|
+ add_option("-D", "--[no-]check-development",
+ "Check development dependencies while uninstalling",
+ "(default: false)") do |value, options|
+ options[:check_dev] = value
+ end
+
+ add_option("-x", "--[no-]executables",
+ "Uninstall applicable executables without",
+ "confirmation") do |value, options|
options[:executables] = value
end
- add_option('-i', '--install-dir DIR',
- 'Directory to uninstall gem from') do |value, options|
+ add_option("-i", "--install-dir DIR",
+ "Directory to uninstall gem from") do |value, options|
options[:install_dir] = File.expand_path(value)
end
- add_option('-n', '--bindir DIR',
- 'Directory to remove binaries from') do |value, options|
+ add_option("-n", "--bindir DIR",
+ "Directory to remove executables from") do |value, options|
options[:bin_dir] = File.expand_path(value)
end
- add_option('--[no-]user-install',
- 'Uninstall from user\'s home directory',
- 'in addition to GEM_HOME.') do |value, options|
+ add_option("--[no-]user-install",
+ "Uninstall from user's home directory",
+ "in addition to GEM_HOME.") do |value, options|
options[:user_install] = value
end
- add_option('--[no-]format-executable',
- 'Assume executable names match Ruby\'s prefix and suffix.') do |value, options|
+ add_option("--[no-]format-executable",
+ "Assume executable names match Ruby's prefix and suffix.") do |value, options|
options[:format_executable] = value
end
+ add_option("--[no-]force",
+ "Uninstall all versions of the named gems",
+ "ignoring dependencies") do |value, options|
+ 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:
@@ -64,31 +96,109 @@ class Gem::Commands::UninstallCommand < Gem::Command
def defaults_str # :nodoc:
"--version '#{Gem::Requirement.default}' --no-force " \
- "--install-dir #{Gem.dir}\n" \
- "--user-install"
+ "--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
- original_path = Gem.path
-
- get_all_gem_names.each do |gem_name|
- begin
- Gem::Uninstaller.new(gem_name, options).uninstall
- rescue Gem::InstallError => e
- alert e.message
- rescue Gem::GemNotInHomeException => e
- spec = e.spec
- alert("In order to remove #{spec.name}, please execute:\n" \
- "\tgem uninstall #{spec.name} --install-dir=#{spec.installation_path}")
- ensure
- Gem.use_paths(*original_path)
+ 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
-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
index 64b8ad64f8..c2fc720297 100644
--- a/lib/rubygems/commands/unpack_command.rb
+++ b/lib/rubygems/commands/unpack_command.rb
@@ -1,28 +1,39 @@
-require 'rubygems/command'
-require 'rubygems/installer'
-require 'rubygems/version_option'
-require 'rubygems/remote_fetcher'
+# frozen_string_literal: true
-class Gem::Commands::UnpackCommand < Gem::Command
+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'
+ require "fileutils"
- super 'unpack', 'Unpack an installed gem to the current directory',
- :version => Gem::Requirement.default,
- :target => Dir.pwd
+ super "unpack", "Unpack an installed gem to the current directory",
+ version: Gem::Requirement.default,
+ target: Dir.pwd
- add_option('--target=DIR',
- 'target directory for unpacking') do |value, options|
+ add_option("--target=DIR",
+ "target directory for unpacking") do |value, options|
options[:target] = value
end
- add_option('--spec', 'unpack the gem specification') do |value, options|
+ add_option("--spec", "unpack the gem specification") do |_value, options|
options[:spec] = true
end
+ add_security_option
add_version_option
end
@@ -34,6 +45,24 @@ class Gem::Commands::UnpackCommand < Gem::Command
"--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
@@ -44,33 +73,45 @@ class Gem::Commands::UnpackCommand < Gem::Command
# 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 then
+ unless path
alert_error "Gem '#{name}' not installed nor fetchable."
next
end
- if @options[:spec] then
- spec, metadata = get_metadata path
+ if @options[:spec]
+ spec, metadata = Gem::Package.raw_spec(path, security_policy)
- if metadata.nil? then
+ if metadata.nil?
alert_error "--spec is unsupported on '#{name}' (old format gem)"
next
end
spec_file = File.basename spec.spec_file
- open spec_file, 'w' do |io|
+ 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'
+ basename = File.basename path, ".gem"
target_dir = File.expand_path basename, options[:target]
- FileUtils.mkdir_p target_dir
- Gem::Installer.new(path, :unpack => true).unpack target_dir
+
+ package = Gem::Package.new path, security_policy
+ package.extract_files target_dir
+
say "Unpacked gem: '#{target_dir}'"
end
end
@@ -89,7 +130,7 @@ class Gem::Commands::UnpackCommand < Gem::Command
return this_path if File.exist? this_path
end
- return nil
+ nil
end
##
@@ -102,24 +143,18 @@ class Gem::Commands::UnpackCommand < Gem::Command
# get_path 'rake', '< 0.1' # nil
# get_path 'rak' # nil (exact name required)
#--
- # TODO: This should be refactored so that it's a general service. I don't
- # think any of our existing classes are the right place though. Just maybe
- # 'Cache'?
- #
- # TODO: It just uses Gem.dir for now. What's an easy way to get the list of
- # source directories?
- def get_path dependency
- return dependency.name if dependency.name =~ /\.gem$/i
+ def get_path(dependency)
+ return dependency.name if /\.gem$/i.match?(dependency.name)
specs = dependency.matching_specs
- selected = specs.sort_by { |s| s.version }.last # HACK: hunt last down
+ selected = specs.max_by(&:version)
return Gem::RemoteFetcher.fetcher.download_to_cache(dependency) unless
selected
- return unless dependency.name =~ /^#{selected.name}$/i
+ return unless /^#{selected.name}$/i.match?(dependency.name)
# We expect to find (basename).gem in the 'cache' directory. Furthermore,
# the name match must be exact (ignoring case).
@@ -130,31 +165,4 @@ class Gem::Commands::UnpackCommand < Gem::Command
path
end
-
- ##
- # Extracts the Gem::Specification and raw metadata from the .gem file at
- # +path+.
-
- def get_metadata path
- format = Gem::Format.from_file_by_path path
- spec = format.spec
-
- metadata = nil
-
- 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.gunzip entry.read
- end
- end
- end
-
- return spec, metadata
- end
-
end
-
diff --git a/lib/rubygems/commands/update_command.rb b/lib/rubygems/commands/update_command.rb
index d63b943c56..d9740d814a 100644
--- a/lib/rubygems/commands/update_command.rb
+++ b/lib/rubygems/commands/update_command.rb
@@ -1,42 +1,54 @@
-require 'rubygems/command'
-require 'rubygems/command_manager'
-require 'rubygems/install_update_options'
-require 'rubygems/local_remote_options'
-require 'rubygems/spec_fetcher'
-require 'rubygems/version_option'
-require 'rubygems/commands/install_command'
+# 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
- super 'update',
- 'Update the named gems (or all installed gems) in the local repository',
- :generate_rdoc => true,
- :generate_ri => true,
- :force => false
+ options = {
+ force: false,
+ }
+
+ options.merge!(install_update_options)
+
+ super "update", "Update installed gems to the latest version", options
add_install_update_options
- OptionParser.accept Gem::Version do |value|
+ Gem::OptionParser.accept Gem::Version do |value|
Gem::Version.new value
value
end
- add_option('--system [VERSION]', Gem::Version,
- 'Update the RubyGems system software') do |value, options|
- value = true unless value
+ add_option("--system [VERSION]", Gem::Version,
+ "Update the RubyGems system software") do |value, opts|
+ value ||= true
- options[:system] = value
+ opts[:system] = value
end
add_local_remote_options
add_platform_option
add_prerelease_option "as update targets"
+
+ @updated = []
+ @installer = nil
end
def arguments # :nodoc:
@@ -44,189 +56,271 @@ class Gem::Commands::UpdateCommand < Gem::Command
end
def defaults_str # :nodoc:
- "--rdoc --ri --no-force --install-dir #{Gem.dir}"
+ "--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 execute
- @installer = Gem::DependencyInstaller.new options
- @updated = []
+ def check_latest_rubygems(version) # :nodoc:
+ if Gem.rubygems_version == version
+ say "Latest version already installed. Done."
+ terminate_interaction
+ end
+ end
- hig = {}
+ 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
- if options[:system] then
+ def execute
+ if options[:system]
update_rubygems
return
- else
- say "Updating installed gems"
+ end
- hig = {} # highest installed gems
+ gems_to_update = which_to_update(
+ highest_installed_gems,
+ options[:args].uniq
+ )
- Gem::Specification.each do |spec|
- if hig[spec.name].nil? or hig[spec.name].version < spec.version then
- hig[spec.name] = spec
- end
+ if options[:explain]
+ say "Gems to update:"
+
+ gems_to_update.each do |name_tuple|
+ say " #{name_tuple.full_name}"
end
+
+ return
end
- gems_to_update = which_to_update hig, options[:args].uniq
+ say "Updating installed gems"
updated = update_gems gems_to_update
- if updated.empty? then
+ 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.map { |spec| spec.name }.join ', '}"
+ say "Gems updated: #{updated_names.join(" ")}"
+ end
+ say "Gems already up-to-date: #{up_to_date_names.join(" ")}" unless up_to_date_names.empty?
+ say "Gems not currently installed: #{not_installed_names.join(" ")}" unless not_installed_names.empty?
+ end
- if options[:generate_ri] then
- updated.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_ri
- end
+ def fetch_remote_gems(spec) # :nodoc:
+ dependency = Gem::Dependency.new spec.name, "> #{spec.version}"
+ dependency.prerelease = options[:prerelease]
- Gem::DocManager.update_ri_cache
- end
+ fetcher = Gem::SpecFetcher.fetcher
- if options[:generate_rdoc] then
- updated.each do |gem|
- Gem::DocManager.new(gem, options[:rdoc_args]).generate_rdoc
- end
- end
- end
+ spec_tuples, errors = fetcher.search_for_dependency dependency
+
+ error = errors.find {|e| e.respond_to? :exception }
+
+ raise error if error
+
+ spec_tuples
end
- def update_gem name, version = Gem::Requirement.default
- return if @updated.any? { |spec| spec.name == name }
- success = false
+ def highest_installed_gems # :nodoc:
+ hig = {} # highest installed gems
- say "Updating #{name}"
- begin
- @installer.install name, version
- success = true
- rescue Gem::InstallError => e
- alert_error "Error installing #{name}:\n\t#{e.message}"
- success = false
- end
+ # Get only gem specifications installed as --user-install
+ Gem::Specification.dirs = Gem.user_dir if options[:user_install]
- @installer.installed_gems.each do |spec|
- @updated << spec
- say "Successfully installed #{spec.full_name}" if success
+ 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 update_gems gems_to_update
- gems_to_update.uniq.sort.each do |(name, version)|
- update_gem name, version
- end
+ def highest_remote_name_tuple(spec) # :nodoc:
+ spec_tuples = fetch_remote_gems spec
- @updated
+ highest_remote_gem = spec_tuples.max
+ return unless highest_remote_gem
+
+ highest_remote_gem.first
end
- ##
- # Update RubyGems software to the latest version.
+ def install_rubygems(spec) # :nodoc:
+ args = update_rubygems_arguments
+ version = spec.version
- def update_rubygems
- unless options[:args].empty? then
- alert_error "Gem names are not allowed with the --system option"
- terminate_interaction 1
+ 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
- options[:user_install] = false
+ status = yield
- # TODO: rename version and other variable name conflicts
- # TODO: get rid of all this indirection on name and other BS
+ unless status
+ FileUtils.mv File.join(tmpdir, "plugins"), Gem.plugindir
+ end
+
+ status
+ end
+ end
+ end
+ def rubygems_target_version
version = options[:system]
- if version == true then
- version = Gem::Version.new Gem::VERSION
- requirement = Gem::Requirement.new ">= #{Gem::VERSION}"
- else
+ 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.name = "rubygems-update"
rubygems_update.version = version
- hig = {
- 'rubygems-update' => rubygems_update
- }
+ highest_remote_tup = highest_remote_name_tuple(rubygems_update)
+ target = highest_remote_tup ? highest_remote_tup.version : version
- gems_to_update = which_to_update hig, options[:args], :system
- name, up_ver = gems_to_update.first
- current_ver = Gem::Version.new Gem::VERSION
+ [target, requirement]
+ end
- target = if options[:system] == true then
- up_ver
- else
- version
- end
+ def update_gem(name, version = Gem::Requirement.default)
+ return if @updated.any? {|spec| spec.name == name }
- if current_ver == target then
- # if options[:system] != true and version == current_ver then
- say "Latest version currently installed. Aborting."
- terminate_interaction
- end
+ update_options = options.dup
+ update_options[:prerelease] = version.prerelease?
- update_gem name, target
+ @installer = Gem::DependencyInstaller.new update_options
- installed_gems = Gem::Specification.find_all_by_name 'rubygems-update', requirement
- version = installed_gems.last.version
+ 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
- args = []
- args << '--prefix' << Gem.prefix if Gem.prefix
- args << '--no-rdoc' unless options[:generate_rdoc]
- args << '--no-ri' unless options[:generate_ri]
- args << '--no-format-executable' if options[:no_format_executable]
+ @installer.installed_gems.each do |spec|
+ @updated << spec
+ end
+ end
- update_dir = File.join Gem.dir, 'gems', "rubygems-update-#{version}"
+ def update_gems(gems_to_update)
+ gems_to_update.uniq.sort.each do |name_tuple|
+ update_gem name_tuple.name, name_tuple.version
+ end
- Dir.chdir update_dir do
- say "Installing RubyGems #{version}"
- setup_cmd = "#{Gem.ruby} setup.rb #{args.join ' '}"
+ @updated
+ end
+
+ ##
+ # Update RubyGems software to the latest version.
- # Make sure old rubygems isn't loaded
- old = ENV["RUBYOPT"]
- ENV.delete("RUBYOPT") if old
- installed = system setup_cmd
- say "RubyGems system software updated" if installed
- ENV["RUBYOPT"] = old if old
+ def update_rubygems
+ if Gem.disable_system_update_message
+ alert_error Gem.disable_system_update_message
+ terminate_interaction 1
end
- end
- def which_to_update highest_installed_gems, gem_names, system = false
- result = []
+ check_update_arguments
- highest_installed_gems.each do |l_name, l_spec|
- next if not gem_names.empty? and
- gem_names.all? { |name| /#{name}/ !~ l_spec.name }
+ version, requirement = rubygems_target_version
- dependency = Gem::Dependency.new l_spec.name, "> #{l_spec.version}"
+ check_latest_rubygems version
- fetcher = Gem::SpecFetcher.fetcher
- spec_tuples = fetcher.find_matching dependency
+ check_oldest_rubygems version
- matching_gems = spec_tuples.select do |(name, _, platform),|
- name == l_name and Gem::Platform.match platform
- end
+ 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?
- highest_remote_gem = matching_gems.sort_by do |(_, version),|
- version
- end.last
+ install_rubygems installed_gems.first
+ end
- highest_remote_gem ||= [[nil, Gem::Version.new(0), nil]] # "null" object
- highest_remote_ver = highest_remote_gem.first[1]
+ 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
- if system or (l_spec.version < highest_remote_ver) then
- result << [l_spec.name, [l_spec.version, highest_remote_ver].max]
- 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
-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
index 6495278a87..5ed4d9d142 100644
--- a/lib/rubygems/commands/which_command.rb
+++ b/lib/rubygems/commands/which_command.rb
@@ -1,16 +1,18 @@
-require 'rubygems/command'
+# 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
+ super "which", "Find the location of a library file you can require",
+ search_gems_first: false, show_all: false
- add_option '-a', '--[no-]all', 'show all matching files' do |show_all, options|
+ add_option "-a", "--[no-]all", "show all matching files" do |show_all, options|
options[:show_all] = show_all
end
- add_option '-g', '--[no-]gems-first',
- 'search gems before non-gems' do |gems_first, options|
+ add_option "-g", "--[no-]gems-first",
+ "search gems before non-gems" do |gems_first, options|
options[:search_gems_first] = gems_first
end
end
@@ -23,31 +25,41 @@ class Gem::Commands::WhichCommand < Gem::Command
"--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 = false
+ found = true
options[:args].each do |arg|
- arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, '')
+ arg = arg.sub(/#{Regexp.union(*Gem.suffixes)}$/, "")
dirs = $LOAD_PATH
spec = Gem::Specification.find_by_path arg
- if spec then
- if options[:search_gems_first] then
- dirs = gem_paths(spec) + $LOAD_PATH
+ if spec
+ if options[:search_gems_first]
+ dirs = spec.full_require_paths + $LOAD_PATH
else
- dirs = $LOAD_PATH + gem_paths(spec)
+ dirs = $LOAD_PATH + spec.full_require_paths
end
end
- # TODO: this is totally redundant and stupid
paths = find_paths arg, dirs
- if paths.empty? then
- alert_error "Can't find ruby library file or shared library #{arg}"
+ if paths.empty?
+ alert_error "Can't find Ruby library file or shared library #{arg}"
+ found = false
else
say paths
- found = true
end
end
@@ -60,7 +72,7 @@ class Gem::Commands::WhichCommand < Gem::Command
dirs.each do |dir|
Gem.suffixes.each do |ext|
full_path = File.join dir, "#{package_name}#{ext}"
- if File.exist? full_path and not File.directory? full_path then
+ if File.exist?(full_path) && !File.directory?(full_path)
result << full_path
return result unless options[:show_all]
end
@@ -70,13 +82,7 @@ class Gem::Commands::WhichCommand < Gem::Command
result
end
- def gem_paths(spec)
- spec.require_paths.collect { |d| File.join spec.full_gem_path, d }
- 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
index a4237e143f..d5e9eb4e33 100644
--- a/lib/rubygems/config_file.rb
+++ b/lib/rubygems/config_file.rb
@@ -1,13 +1,18 @@
+# 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.
+# Gem::ConfigFile RubyGems options and gem command options from gemrc.
#
-# ~/.gemrc is a YAML file that uses strings to match gem command arguments and
+# 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
@@ -21,58 +26,69 @@
# RubyGems options use symbol keys. Valid options are:
#
# +:backtrace+:: See #backtrace
-# +:benchmark+:: See #benchmark
-# +:sources+:: Sets Gem::sources
+# +:bulk_threshold+:: See #bulk_threshold
# +:verbose+:: See #verbose
+# +:update_sources+:: See #update_sources
+# +:concurrent_downloads+:: See #concurrent_downloads
+# +:cert_expiration_length_days+:: See #cert_expiration_length_days
+# +:install_extension_in_lib+:: See #install_extension_in_lib
+# +:ipv4_fallback_enabled+:: See #ipv4_fallback_enabled
+# +:global_gem_cache+:: See #global_gem_cache
+# +:use_psych+:: See #use_psych
+# +:gemhome+:: See #home
+# +:gempath+:: See #path
+# +:sources+:: Sets Gem::sources
+# +:disable_default_gem_server+:: See #disable_default_gem_server
+# +:ssl_verify_mode+:: See #ssl_verify_mode
+# +:ssl_ca_cert+:: See #ssl_ca_cert
+# +:ssl_client_cert+:: See #ssl_client_cert
+#
+# gemrc files may exist in various locations and are read and merged in
+# the following order:
+#
+# - 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 = false
- DEFAULT_BENCHMARK = false
+ DEFAULT_BACKTRACE = true
DEFAULT_BULK_THRESHOLD = 1000
DEFAULT_VERBOSITY = true
DEFAULT_UPDATE_SOURCES = true
+ DEFAULT_CONCURRENT_DOWNLOADS = 8
+ DEFAULT_CERT_EXPIRATION_LENGTH_DAYS = 365
+ DEFAULT_IPV4_FALLBACK_ENABLED = false
+ 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 = {}
+ OPERATING_SYSTEM_DEFAULTS = Gem.operating_system_defaults
##
# For Ruby implementers to set configuration defaults. Set in
# rubygems/defaults/#{RUBY_ENGINE}.rb
- PLATFORM_DEFAULTS = {}
+ PLATFORM_DEFAULTS = Gem.platform_defaults
- system_config_path =
+ # :stopdoc:
+
+ SYSTEM_CONFIG_PATH =
begin
require "etc"
Etc.sysconfdir
rescue LoadError, NoMethodError
- begin
- # TODO: remove after we drop 1.8.7 and 1.9.1
- require 'Win32API'
-
- CSIDL_COMMON_APPDATA = 0x0023
- path = 0.chr * 260
- if RUBY_VERSION > '1.9' then
- SHGetFolderPath = Win32API.new 'shell32', 'SHGetFolderPath', 'PLPLP',
- 'L', :stdcall
- SHGetFolderPath.call nil, CSIDL_COMMON_APPDATA, nil, 1, path
- else
- SHGetFolderPath = Win32API.new 'shell32', 'SHGetFolderPath', 'LLLLP',
- 'L'
- SHGetFolderPath.call 0, CSIDL_COMMON_APPDATA, 0, 1, path
- end
-
- path.strip
- rescue LoadError
- "/etc"
- end
+ RbConfig::CONFIG["sysconfdir"] || "/etc"
end
- SYSTEM_WIDE_CONFIG_FILE = File.join system_config_path, 'gemrc'
+ # :startdoc:
+
+ SYSTEM_WIDE_CONFIG_FILE = File.join SYSTEM_CONFIG_PATH, "gemrc"
##
# List of arguments supplied to the config file object.
@@ -95,11 +111,6 @@ class Gem::ConfigFile
attr_writer :backtrace
##
- # True if we are benchmarking this run.
-
- attr_accessor :benchmark
-
- ##
# Bulk threshold value. If the number of missing gems are above this
# threshold value, then a bulk download technique is used. (deprecated)
@@ -114,19 +125,65 @@ class Gem::ConfigFile
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
##
- # API key for RubyGems.org
+ # True if we want to force specification of gem server when pushing a gem
+
+ attr_accessor :disable_default_gem_server
- attr_reader :rubygems_api_key
+ # openssl verify mode value, used for remote https connection
+
+ attr_reader :ssl_verify_mode
##
- # Hash of RubyGems.org and alternate API keys
+ # 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 :api_keys
+ attr_reader :ssl_client_cert
##
# Create the config file object. +args+ is the list of arguments
@@ -146,107 +203,236 @@ class Gem::ConfigFile
# <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(arg_list)
- @config_file_name = nil
- need_config_file_name = false
-
- arg_list = arg_list.map do |arg|
- if need_config_file_name then
- @config_file_name = arg
- need_config_file_name = false
- nil
- elsif arg =~ /^--config-file=(.*)/ then
- @config_file_name = $1
- nil
- elsif arg =~ /^--config-file$/ then
- need_config_file_name = true
- nil
- else
- arg
- end
- end.compact
+ def initialize(args)
+ set_config_file_name(args)
@backtrace = DEFAULT_BACKTRACE
- @benchmark = DEFAULT_BENCHMARK
@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.dup.untaint
+ 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
- @hash = @hash.merge system_config
- @hash = @hash.merge user_config
-
- # HACK these override command-line args, which is bad
- @backtrace = @hash[:backtrace] if @hash.key? :backtrace
- @benchmark = @hash[:benchmark] if @hash.key? :benchmark
- @bulk_threshold = @hash[:bulk_threshold] if @hash.key? :bulk_threshold
- @home = @hash[:gemhome] if @hash.key? :gemhome
- @path = @hash[:gempath] if @hash.key? :gempath
- @update_sources = @hash[:update_sources] if @hash.key? :update_sources
- @verbose = @hash[:verbose] if @hash.key? :verbose
-
- load_api_keys
-
- Gem.sources = @hash[:sources] if @hash.key? :sources
- handle_arguments arg_list
+ 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
- File.join Gem.user_home, '.gem', 'credentials'
+ 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
- @api_keys = if File.exist? credentials_path then
- load_file(credentials_path)
- else
- @hash
- end
- if @api_keys.key? :rubygems_api_key then
- @rubygems_api_key = @api_keys[:rubygems_api_key]
- @api_keys[:rubygems] = @api_keys.delete :rubygems_api_key unless @api_keys.key? :rubygems
+ 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)
- config = load_file(credentials_path).merge(:rubygems_api_key => api_key)
+ set_api_key :rubygems_api_key, api_key
- dirname = File.dirname credentials_path
- Dir.mkdir(dirname) unless File.exist? dirname
+ @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
- Gem.load_yaml
+ config = load_file(credentials_path).merge(host => api_key)
- File.open(credentials_path, 'w') do |f|
- f.write config.to_yaml
+ 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
- @rubygems_api_key = api_key
+ 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)
- Gem.load_yaml
+ yaml_errors = [ArgumentError]
+
+ return {} unless filename && !filename.empty? && File.exist?(filename)
- return {} unless filename and File.exist? filename
begin
- YAML.load(File.read(filename))
- rescue ArgumentError
- warn "Failed to load #{config_file_name}"
+ 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 #{config_file_name} due to permissions problem."
- end or {}
+ 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 or $DEBUG
+ @backtrace || $DEBUG
+ end
+
+ # Check state file is writable. Creates empty file if not present to ensure we can write to it.
+ def state_file_writable?
+ if File.exist?(state_file_name)
+ File.writable?(state_file_name)
+ else
+ require "fileutils"
+ FileUtils.mkdir_p File.dirname(state_file_name)
+ File.open(state_file_name, "w") {}
+ true
+ end
+ rescue Errno::EACCES
+ false
end
# The name of the configuration file.
@@ -254,22 +440,39 @@ class Gem::ConfigFile
@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 :benchmark
hash.delete :backtrace
hash.delete :bulk_threshold
yield :update_sources, @update_sources
yield :verbose, @verbose
- yield :benchmark, @benchmark
yield :backtrace, @backtrace
yield :bulk_threshold, @bulk_threshold
- yield 'config_file_name', @config_file_name if @config_file_name
+ yield "config_file_name", @config_file_name if @config_file_name
hash.each(&block)
end
@@ -282,10 +485,10 @@ class Gem::ConfigFile
case arg
when /^--(backtrace|traceback)$/ then
@backtrace = true
- when /^--bench(mark)?$/ then
- @benchmark = true
when /^--debug$/ then
$DEBUG = true
+
+ warn "NOTE: Debugging mode prints all exceptions even when rescued"
else
@args << arg
end
@@ -295,67 +498,155 @@ class Gem::ConfigFile
# Really verbose mode gives you extra output.
def really_verbose
case verbose
- when true, false, nil then false
- else true
+ 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.key?(:backtrace) ? @hash[:backtrace] :
- DEFAULT_BACKTRACE
- yaml_hash[:benchmark] = @hash.key?(:benchmark) ? @hash[:benchmark] :
- DEFAULT_BENCHMARK
- yaml_hash[:bulk_threshold] = @hash.key?(:bulk_threshold) ?
- @hash[:bulk_threshold] : DEFAULT_BULK_THRESHOLD
- yaml_hash[:sources] = Gem.sources
- yaml_hash[:update_sources] = @hash.key?(:update_sources) ?
- @hash[:update_sources] : DEFAULT_UPDATE_SOURCES
- yaml_hash[:verbose] = @hash.key?(:verbose) ? @hash[:verbose] :
- DEFAULT_VERBOSITY
-
- keys = yaml_hash.keys.map { |key| key.to_s }
- keys << 'debug'
+ 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 =~ re
+ next if key&.match?(re)
yaml_hash[key.to_s] = value
end
- yaml_hash.to_yaml
+ self.class.dump_with_rubygems_yaml(yaml_hash)
end
# Writes out this config file, replacing its source.
def write
- open config_file_name, 'w' do |io|
+ 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.to_s]
+ @hash[key] || @hash[key.to_s]
end
# Set configuration option +key+ to +value+.
def []=(key, value)
- @hash[key.to_s] = value
+ @hash[key] = value
end
def ==(other) # :nodoc:
- self.class === other and
- @backtrace == other.backtrace and
- @benchmark == other.benchmark and
- @bulk_threshold == other.bulk_threshold and
- @verbose == other.verbose and
- @update_sources == other.update_sources and
- @hash == other.hash
+ self.class === other &&
+ @backtrace == other.backtrace &&
+ @bulk_threshold == other.bulk_threshold &&
+ @verbose == other.verbose &&
+ @update_sources == other.update_sources &&
+ @hash == other.hash
end
- protected
-
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/custom_require.rb b/lib/rubygems/custom_require.rb
deleted file mode 100644
index 641db842ac..0000000000
--- a/lib/rubygems/custom_require.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-module Kernel
-
- if defined?(gem_original_require) then
- # Ruby ships with a custom_require, override its require
- remove_method :require
- else
- ##
- # The Kernel#require from before RubyGems was loaded.
-
- alias gem_original_require require
- private :gem_original_require
- 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
- if Gem.unresolved_deps.empty? or Gem.loaded_path? path then
- gem_original_require path
- else
- spec = Gem::Specification.find { |s|
- s.activated? and s.contains_requirable_file? path
- }
-
- unless spec then
- found_specs = Gem::Specification.find_in_unresolved path
- unless found_specs.empty? then
- found_specs = [found_specs.last]
- else
- found_specs = Gem::Specification.find_in_unresolved_tree path
- end
-
- found_specs.each do |found_spec|
- found_spec.activate
- end
- end
-
- return gem_original_require path
- end
- rescue LoadError => load_error
- if load_error.message.end_with?(path) and Gem.try_activate(path) then
- return gem_original_require(path)
- end
-
- raise load_error
- end
-
- private :require
-
-end
-
diff --git a/lib/rubygems/defaults.rb b/lib/rubygems/defaults.rb
index 20b4198bfa..2247c49c81 100644
--- a/lib/rubygems/defaults.rb
+++ b/lib/rubygems/defaults.rb
@@ -1,8 +1,10 @@
-module Gem
+# frozen_string_literal: true
- # TODO: move this whole file back into rubygems.rb
+module Gem
+ DEFAULT_HOST = "https://rubygems.org"
- @post_install_hooks ||= []
+ @post_install_hooks ||= []
+ @done_installing_hooks ||= []
@post_uninstall_hooks ||= []
@pre_uninstall_hooks ||= []
@pre_install_hooks ||= []
@@ -11,7 +13,21 @@ module Gem
# An Array of the default sources that come with RubyGems
def self.default_sources
- %w[http://rubygems.org/]
+ @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
##
@@ -19,55 +35,171 @@ module Gem
# specified in the environment
def self.default_dir
- path = if defined? RUBY_FRAMEWORK_VERSION then
- [
- File.dirname(ConfigMap[:sitedir]),
- 'Gems',
- ConfigMap[:ruby_version]
- ]
- elsif ConfigMap[:rubylibprefix] then
- [
- ConfigMap[:rubylibprefix],
- 'gems',
- ConfigMap[:ruby_version]
- ]
- else
- [
- ConfigMap[:libdir],
- ruby_engine,
- 'gems',
- ConfigMap[:ruby_version]
- ]
- end
-
- @default_dir ||= File.join(*path)
+ @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
- File.join Gem.user_home, '.gem', ruby_engine, ConfigMap[:ruby_version]
+ 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
##
- # Default gem load path
+ # The path to standard location of the user's configuration directory.
- def self.default_path
- if File.exist? Gem.user_home then
- [user_dir, default_dir]
+ 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
- [default_dir]
+ 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 = ConfigMap[:ruby_install_name].sub('ruby', '%s') rescue '%s'
+ exec_format = begin
+ RbConfig::CONFIG["ruby_install_name"].sub("ruby", "%s")
+ rescue StandardError
+ "%s"
+ end
- unless exec_format =~ /%s/ then
+ unless exec_format.include?("%s")
raise Gem::Exception,
"[BUG] invalid exec_format #{exec_format.inspect}, no %s"
end
@@ -79,39 +211,107 @@ module Gem
# The default directory for binaries
def self.default_bindir
- if defined? RUBY_FRAMEWORK_VERSION then # mac framework support
- '/usr/bin'
- else # generic install
- ConfigMap[: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 system-wide source info cache directory
+ # 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
- def self.default_system_source_cache_dir
- File.join(Gem.dir, 'source_cache')
+ default_cert_path
end
##
- # The default user-specific source info cache directory
+ # Enables automatic installation into user directory
- def self.default_user_source_cache_dir
- #
- # NOTE Probably an argument for moving this to per-ruby supported dirs like
- # user_dir
- #
- File.join(Gem.user_home, '.gem', 'source_cache')
+ 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
##
- # A wrapper around RUBY_ENGINE const that may not be defined
+ # Install extensions into lib as well as into the extension directory.
- def self.ruby_engine
- if defined? RUBY_ENGINE then
- RUBY_ENGINE
- else
- 'ruby'
+ 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
index 0caf65c6c4..1e91f493a6 100644
--- a/lib/rubygems/dependency.rb
+++ b/lib/rubygems/dependency.rb
@@ -1,20 +1,22 @@
-require "rubygems/requirement"
+# 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,
- ]
+ :development,
+ :runtime,
+ ].freeze
##
# Dependency name or regular expression.
@@ -31,19 +33,24 @@ class Gem::Dependency
# argument can optionally be the dependency type, which defaults to
# <tt>:runtime</tt>.
- def initialize name, *requirements
- if Regexp === name then
+ 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 1 == requirements.length # unpack
+ requirements = requirements.first if requirements.length == 1 # unpack
unless TYPES.include? type
- raise ArgumentError, "Valid types are #{TYPES.inspect}, "
- + "not #{type.inspect}"
+ raise ArgumentError, "Valid types are #{TYPES.inspect}, " \
+ "not #{type.inspect}"
end
@name = name
@@ -66,8 +73,11 @@ class Gem::Dependency
end
def inspect # :nodoc:
- "<%s type=%p name=%p requirements=%p>" %
- [self.class, self.type, self.name, requirement.to_s]
+ 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
##
@@ -77,15 +87,23 @@ class Gem::Dependency
@prerelease || requirement.prerelease?
end
- def pretty_print q # :nodoc:
- q.group 1, 'Gem::Dependency.new(', ')' do
+ ##
+ # 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.text ","
q.breakable
q.pp requirement
- q.text ','
+ q.text ","
q.breakable
q.pp type
@@ -96,7 +114,7 @@ class Gem::Dependency
# What does this dependency require?
def requirement
- return @requirement if defined?(@requirement) and @requirement
+ return @requirement if defined?(@requirement) && @requirement
# @version_requirements and @version_requirement are legacy ivar
# names, and supported here because older gems need to keep
@@ -113,9 +131,11 @@ class Gem::Dependency
# 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_requirement = nil
@version_requirements = Gem::Requirement.new version
end
@@ -127,7 +147,7 @@ class Gem::Dependency
end
def to_s # :nodoc:
- if type != :runtime then
+ if type != :runtime
"#{name} (#{requirement}, #{type})"
else
"#{name} (#{requirement})"
@@ -141,18 +161,22 @@ class Gem::Dependency
@type ||= :runtime
end
- def == other # :nodoc:
+ def runtime?
+ @type == :runtime || !@type
+ end
+
+ def ==(other) # :nodoc:
Gem::Dependency === other &&
- self.name == other.name &&
- self.type == other.type &&
- self.requirement == other.requirement
+ name == other.name &&
+ type == other.type &&
+ requirement == other.requirement
end
##
# Dependencies are ordered by name.
- def <=> other
- self.name <=> other.name
+ def <=>(other)
+ name <=> other.name
end
##
@@ -161,7 +185,7 @@ class Gem::Dependency
# other has only an equal version requirement that satisfies this
# dependency.
- def =~ other
+ def =~(other)
unless Gem::Dependency === other
return unless other.respond_to?(:name) && other.respond_to?(:version)
other = Gem::Dependency.new other.name, other.version
@@ -172,21 +196,55 @@ class Gem::Dependency
reqs = other.requirement.requirements
return false unless reqs.length == 1
- return false unless reqs.first.first == '='
+ return false unless reqs.first.first == "="
version = reqs.first.last
requirement.satisfied_by? version
end
- def match? name, version
+ 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
- return true if requirement.none?
- requirement.satisfied_by? Gem::Version.new(version)
+ 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
- def matches_spec? spec
+ ##
+ # 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?
@@ -196,14 +254,14 @@ class Gem::Dependency
##
# Merges the requirements of +other+ into this dependency
- def merge other
- unless name == other.name then
+ def merge(other)
+ unless name == other.name
raise ArgumentError,
"#{self} and #{other} have different names"
end
default = Gem::Requirement.default
- self_req = self.requirement
+ self_req = requirement
other_req = other.requirement
return self.class.new name, self_req if other_req == default
@@ -212,19 +270,16 @@ class Gem::Dependency
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 { |spec|
- self.name === spec.name and # TODO: == instead of ===
- requirement.satisfied_by? spec.version
- }
+ def matching_specs(platform_only = false)
+ matches = Gem::Specification.find_all_by_name(name, requirement)
if platform_only
- matches.reject! { |spec|
- not Gem::Platform.match spec.platform
- }
+ matches.reject! do |spec|
+ spec.nil? || !Gem::Platform.match_spec?(spec)
+ end
end
- matches = matches.sort_by { |s| s.sort_obj } # HACK: shouldn't be needed
+ matches.reject(&:ignored?)
end
##
@@ -239,12 +294,14 @@ class Gem::Dependency
# TODO: check Gem.activated_spec[self.name] in case matches falls outside
- if matches.empty? then
- specs = Gem::Specification.all_names.join ", "
- error = Gem::LoadError.new "Could not find #{name} (#{requirement}) amongst [#{specs}]"
- error.name = self.name
- error.requirement = self.requirement
- raise error
+ 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
@@ -253,8 +310,39 @@ class Gem::Dependency
end
def to_spec
- matches = self.to_specs
+ 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
- matches.find { |spec| spec.activated? } or matches.last
+ def encode_with(coder) # :nodoc:
+ coder.add "name", @name
+ coder.add "requirement", @requirement
+ coder.add "type", @type
+ coder.add "prerelease", @prerelease
+ coder.add "version_requirements", @version_requirements
end
end
diff --git a/lib/rubygems/dependency_installer.rb b/lib/rubygems/dependency_installer.rb
index 6303e8e9ac..c842714d95 100644
--- a/lib/rubygems/dependency_installer.rb
+++ b/lib/rubygems/dependency_installer.rb
@@ -1,29 +1,47 @@
-require 'rubygems'
-require 'rubygems/dependency_list'
-require 'rubygems/installer'
-require 'rubygems/spec_fetcher'
-require 'rubygems/user_interaction'
+# 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
- attr_reader :gems_to_install
- attr_reader :installed_gems
+ 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
- DEFAULT_OPTIONS = {
- :env_shebang => false,
- :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,
- }
+ ##
+ # 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.
@@ -42,20 +60,19 @@ class Gem::DependencyInstaller
# :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 = {})
- if options[:install_dir] then
- @gem_home = options[:install_dir]
-
- Gem::Specification.dirs = @gem_home
- Gem.ensure_gem_subdirectories @gem_home
- options[:install_dir] = @gem_home # FIX: because we suck and reuse below
- end
+ @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]
@@ -65,179 +82,57 @@ class Gem::DependencyInstaller
@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
- @install_dir = options[:install_dir] || Gem.dir
@cache_dir = options[:cache_dir] || @install_dir
- # Set with any errors that SpecFetcher finds while search through
- # gemspecs for a dep
- @errors = nil
+ @errors = []
end
##
- # Returns a list of pairs of gemspecs and source_uris that match
- # Gem::Dependency +dep+ from both local (Dir.pwd) and remote (Gem.sources)
- # sources. Gems are sorted with newer gems preferred over older gems, and
- # local gems preferred over remote gems.
-
- def find_gems_with_sources(dep)
- # Reset the errors
- @errors = nil
- gems_and_sources = []
-
- if @domain == :both or @domain == :local then
- Dir[File.join(Dir.pwd, "#{dep.name}-[0-9]*.gem")].each do |gem_file|
- spec = Gem::Format.from_file_by_path(gem_file).spec
- gems_and_sources << [spec, gem_file] if spec.name == dep.name
- end
- end
-
- if @domain == :both or @domain == :remote then
- begin
- # REFACTOR: all = dep.requirement.needs_all?
- requirements = dep.requirement.requirements.map do |req, ver|
- req
- end
-
- all = !dep.prerelease? &&
- # we only need latest if there's one requirement and it is
- # guaranteed to match the newest specs
- (requirements.length > 1 or
- (requirements.first != ">=" and requirements.first != ">"))
+ # Indicated, based on the requested domain, if local
+ # gems should be considered.
- found, @errors = Gem::SpecFetcher.fetcher.fetch_with_errors dep, all, true, dep.prerelease?
-
- gems_and_sources.push(*found)
-
- rescue Gem::RemoteFetcher::FetchError => e
- if Gem.configuration.really_verbose then
- say "Error fetching remote data:\t\t#{e.message}"
- say "Falling back to local-only install"
- end
- @domain = :local
- end
- end
-
- gems_and_sources.sort_by do |gem, source|
- [gem, source =~ /^http:\/\// ? 0 : 1] # local gems win
- end
+ def consider_local?
+ @domain == :both || @domain == :local
end
##
- # Gathers all dependencies necessary for the installation from local and
- # remote sources unless the ignore_dependencies was given.
-
- def gather_dependencies
- specs = @specs_and_sources.map { |spec,_| spec }
-
- # these gems were listed by the user, always install them
- keep_names = specs.map { |spec| spec.full_name }
-
- dependency_list = Gem::DependencyList.new @development
- dependency_list.add(*specs)
- to_do = specs.dup
-
- add_found_dependencies to_do, dependency_list unless @ignore_dependencies
+ # Indicated, based on the requested domain, if remote
+ # gems should be considered.
- dependency_list.specs.reject! { |spec|
- not keep_names.include?(spec.full_name) and
- Gem::Specification.include?(spec)
- }
-
- unless dependency_list.ok? or @ignore_dependencies or @force then
- reason = dependency_list.why_not_ok?.map { |k,v|
- "#{k} requires #{v.join(", ")}"
- }.join("; ")
- raise Gem::DependencyError, "Unable to resolve dependencies: #{reason}"
- end
-
- @gems_to_install = dependency_list.dependency_order.reverse
+ def consider_remote?
+ @domain == :both || @domain == :remote
end
- def add_found_dependencies to_do, dependency_list
- seen = {}
- dependencies = Hash.new { |h, name| h[name] = Gem::Dependency.new name }
-
- until to_do.empty? do
- spec = to_do.shift
- next if spec.nil? or seen[spec.name]
- seen[spec.name] = true
-
- deps = spec.runtime_dependencies
- deps |= spec.development_dependencies if @development
-
- deps.each do |dep|
- dependencies[dep.name] = dependencies[dep.name].merge dep
-
- results = find_gems_with_sources(dep).reverse
-
- results.reject! do |dep_spec,|
- to_do.push dep_spec
-
- # already locally installed
- Gem::Specification.any? do |installed_spec|
- dep.name == installed_spec.name and
- dep.requirement.satisfied_by? installed_spec.version
- end
- end
-
- results.each do |dep_spec, source_uri|
- @specs_and_sources << [dep_spec, source_uri]
-
- dependency_list.add dep_spec
- end
- end
- end
-
- dependency_list.remove_specs_unsatisfied_by dependencies
- end
-
- ##
- # Finds a spec and the source_uri it came from for gem +gem_name+ and
- # +version+. Returns an Array of specs and sources required for
- # installation of the gem.
-
- def find_spec_by_name_and_version(gem_name,
- version = Gem::Requirement.default,
- prerelease = false)
- spec_and_source = nil
-
- glob = if File::ALT_SEPARATOR then
- gem_name.gsub File::ALT_SEPARATOR, File::SEPARATOR
- else
- gem_name
- end
-
- local_gems = Dir["#{glob}*"].sort.reverse
-
- local_gems.each do |gem_file|
- next unless gem_file =~ /gem$/
+ def in_background(what) # :nodoc:
+ fork_happened = false
+ if @build_docs_in_background && Process.respond_to?(:fork)
begin
- spec = Gem::Format.from_file_by_path(gem_file).spec
- spec_and_source = [spec, gem_file]
- break
- rescue SystemCallError, Gem::Package::FormatError
+ Process.fork do
+ yield
+ end
+ fork_happened = true
+ say "#{what} in a background process."
+ rescue NotImplementedError
end
end
-
- unless spec_and_source then
- dep = Gem::Dependency.new gem_name, version
- dep.prerelease = true if prerelease
- spec_and_sources = find_gems_with_sources(dep).reverse
- spec_and_source = spec_and_sources.find { |spec, source|
- Gem::Platform.match spec.platform
- }
- end
-
- if spec_and_source.nil? then
- raise Gem::GemNotFoundException.new(
- "Could not find a valid gem '#{gem_name}' (#{version}) locally or in a repository",
- gem_name, version, @errors)
- end
-
- @specs_and_sources = [spec_and_source]
+ yield unless fork_happened
end
##
@@ -254,51 +149,116 @@ class Gem::DependencyInstaller
# 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
- if String === dep_or_name then
- find_spec_by_name_and_version dep_or_name, version, @prerelease
- else
- dep_or_name.prerelease = @prerelease
- @specs_and_sources = [find_gems_with_sources(dep_or_name).last]
- end
+ def install(dep_or_name, version = Gem::Requirement.default)
+ request_set = resolve_dependencies dep_or_name, version
@installed_gems = []
- gather_dependencies
+ 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
- last = @gems_to_install.size - 1
- @gems_to_install.each_with_index do |spec, index|
- next if Gem::Specification.include?(spec) and index != last
+ @installed_gems.sort!
- # TODO: make this sorta_verbose so other users can benefit from it
- say "Installing gem #{spec.full_name}" if Gem.configuration.really_verbose
+ # 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?
- _, source_uri = @specs_and_sources.assoc spec
- begin
- local_gem_path = Gem::RemoteFetcher.fetcher.download spec, source_uri,
- @cache_dir
- rescue Gem::RemoteFetcher::FetchError
- next if @force
- raise
+ @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
- inst = Gem::Installer.new local_gem_path,
- :bin_dir => @bin_dir,
- :development => @development,
- :env_shebang => @env_shebang,
- :force => @force,
- :format_executable => @format_executable,
- :ignore_dependencies => @ignore_dependencies,
- :install_dir => @install_dir,
- :security_policy => @security_policy,
- :user_install => @user_install,
- :wrappers => @wrappers
-
- spec = inst.install
-
- @installed_gems << spec
+ 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
- @installed_gems
+ 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
index 9f1da9166c..d50cfe2d54 100644
--- a/lib/rubygems/dependency_list.rb
+++ b/lib/rubygems/dependency_list.rb
@@ -1,21 +1,26 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'tsort'
-require 'rubygems/deprecate'
+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 TSort
+ include Gem::TSort
##
# Allows enabling/disabling use of development dependencies
@@ -27,24 +32,15 @@ class Gem::DependencyList
def self.from_specs
list = new
- list.add(*Gem::Specification.map)
+ list.add(*Gem::Specification.to_a)
list
end
##
- # Creates a DependencyList from a Gem::SourceIndex +source_index+
-
- def self.from_source_index(ignored=nil)
- warn "NOTE: DependencyList.from_source_index ignores it's arg" if ignored
-
- from_specs
- end
-
- ##
# Creates a new DependencyList. If +development+ is true, development
# dependencies will be included.
- def initialize development = false
+ def initialize(development = false)
@specs = []
@development = development
@@ -54,7 +50,7 @@ class Gem::DependencyList
# Adds +gemspecs+ to the dependency list.
def add(*gemspecs)
- @specs.push(*gemspecs)
+ @specs.concat gemspecs
end
def clear
@@ -83,8 +79,8 @@ class Gem::DependencyList
seen = {}
sorted.each do |spec|
- if index = seen[spec.name] then
- if result[index].version < spec.version then
+ if index = seen[spec.name]
+ if result[index].version < spec.version
result[index] = spec
end
else
@@ -104,11 +100,11 @@ class Gem::DependencyList
end
def find_name(full_name)
- @specs.find { |spec| spec.full_name == full_name }
+ @specs.find {|spec| spec.full_name == full_name }
end
def inspect # :nodoc:
- "#<%s:0x%x %p>" % [self.class, object_id, map { |s| s.full_name }]
+ format("%s %p>", super[0..-2], map(&:full_name))
end
##
@@ -118,16 +114,16 @@ class Gem::DependencyList
why_not_ok?(:quick).empty?
end
- def why_not_ok? quick = false
- unsatisfied = Hash.new { |h,k| h[k] = [] }
+ 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? { |installed_spec|
- dep.name == installed_spec.name and
- dep.requirement.satisfied_by? installed_spec.version
- }
+ inst = Gem::Specification.any? do |installed_spec|
+ dep.name == installed_spec.name &&
+ dep.requirement.satisfied_by?(installed_spec.version)
+ end
- unless inst or @specs.find { |s| s.satisfies_requirement? dep } then
+ unless inst || @specs.find {|s| s.satisfies_requirement? dep }
unsatisfied[spec.name] << dep
return unsatisfied if quick
end
@@ -138,32 +134,37 @@ class Gem::DependencyList
end
##
- # Is is ok to remove a gemspec from the dependency list?
+ # 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)
+ def ok_to_remove?(full_name, check_dev = true)
gem_to_remove = find_name full_name
- siblings = @specs.find_all { |s|
+ # 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|
- spec.dependencies.each do |dep|
+ 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? { |dep|
- siblings.any? { |s|
+ deps.all? do |dep|
+ siblings.any? do |s|
s.satisfies_requirement? dep
- }
- }
+ end
+ end
end
##
@@ -171,18 +172,18 @@ class Gem::DependencyList
# satisfy items in +dependencies+ (a hash of gem names to arrays of
# dependencies).
- def remove_specs_unsatisfied_by dependencies
- specs.reject! { |spec|
+ def remove_specs_unsatisfied_by(dependencies)
+ specs.reject! do |spec|
dep = dependencies[spec.name]
- dep and not dep.requirement.satisfied_by? spec.version
- }
+ dep && !dep.requirement.satisfied_by?(spec.version)
+ end
end
##
# 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 }
+ @specs.delete_if {|spec| spec.full_name == full_name }
end
##
@@ -190,7 +191,7 @@ class Gem::DependencyList
# gemspecs that have a dependency satisfied by the named gemspec.
def spec_predecessors
- result = Hash.new { |h,k| h[k] = [] }
+ result = Hash.new {|h,k| h[k] = [] }
specs = @specs.sort.reverse
@@ -199,7 +200,7 @@ class Gem::DependencyList
next if spec == other
other.dependencies.each do |dep|
- if spec.satisfies_requirement? dep then
+ if spec.satisfies_requirement? dep
result[spec] << other
end
end
@@ -213,7 +214,7 @@ class Gem::DependencyList
@specs.each(&block)
end
- def tsort_each_child(node, &block)
+ def tsort_each_child(node)
specs = @specs.sort.reverse
dependencies = node.runtime_dependencies
@@ -221,12 +222,8 @@ class Gem::DependencyList
dependencies.each do |dep|
specs.each do |spec|
- if spec.satisfies_requirement? dep then
- begin
- yield spec
- rescue TSort::Cyclic
- # do nothing
- end
+ if spec.satisfies_requirement? dep
+ yield spec
break
end
end
@@ -240,13 +237,6 @@ class Gem::DependencyList
# +ignored+.
def active_count(specs, ignored)
- specs.count { |spec| ignored[spec.full_name].nil? }
- end
-end
-
-class Gem::DependencyList
- class << self
- extend Gem::Deprecate
- deprecate :from_source_index, "from_specs", 2011, 11
+ specs.count {|spec| ignored[spec.full_name].nil? }
end
end
diff --git a/lib/rubygems/deprecate.rb b/lib/rubygems/deprecate.rb
index a78208ec24..eb503bb269 100644
--- a/lib/rubygems/deprecate.rb
+++ b/lib/rubygems/deprecate.rb
@@ -1,33 +1,81 @@
-##
-# Provides a single method +deprecate+ to be used to declare when
-# something is going away.
-#
-# class Legacy
-# def self.klass_method
-# # ...
-# end
-#
-# def instance_method
-# # ...
-# end
-#
-# extend Gem::Deprecate
-# deprecate :instance_method, "X.z", 2011, 4
-#
-# class << self
-# extend Gem::Deprecate
-# deprecate :klass_method, :none, 2011, 4
-# end
-# end
+# frozen_string_literal: true
module Gem
- module Deprecate
+ ##
+ # 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:
+ def self.skip=(v) # :nodoc:
@skip = v
end
@@ -35,36 +83,89 @@ module Gem
# Temporarily turn off warnings. Intended for tests only.
def skip_during
- Gem::Deprecate.skip, original = true, Gem::Deprecate.skip
+ original = Gem::Deprecate.skip
+ Gem::Deprecate.skip = true
yield
ensure
Gem::Deprecate.skip = original
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 {
+ 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| # TODO: really works on 1.8.7?
- klass = self.kind_of? Module
+ 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}",
- ". It will be removed on or after %4d-%02d-01." % [year, month],
+ 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 :deprecate, :skip_during
+ module_function :rubygems_deprecate, :rubygems_deprecate_command, :skip_during
end
end
diff --git a/lib/rubygems/doc_manager.rb b/lib/rubygems/doc_manager.rb
deleted file mode 100644
index 826f57d9dd..0000000000
--- a/lib/rubygems/doc_manager.rb
+++ /dev/null
@@ -1,243 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-
-##
-# The documentation manager generates RDoc and RI for RubyGems.
-
-class Gem::DocManager
-
- include Gem::UserInteraction
-
- @configured_args = []
-
- def self.configured_args
- @configured_args ||= []
- end
-
- def self.configured_args=(args)
- case args
- when Array
- @configured_args = args
- when String
- @configured_args = args.split
- end
- end
-
- ##
- # Load RDoc from a gem if it is available, otherwise from Ruby's stdlib
-
- def self.load_rdoc
- begin
- gem 'rdoc'
- rescue Gem::LoadError
- # use built-in RDoc
- end
-
- begin
- require 'rdoc/rdoc'
-
- @rdoc_version = if defined? RDoc::VERSION then
- Gem::Version.new RDoc::VERSION
- else
- Gem::Version.new '1.0.1' # HACK parsing is hard
- end
-
- rescue LoadError => e
- raise Gem::DocumentError,
- "ERROR: RDoc documentation generator not installed: #{e}"
- end
- end
-
- def self.rdoc_version
- @rdoc_version
- end
-
- ##
- # Updates the RI cache for RDoc 2 if it is installed
-
- def self.update_ri_cache
- load_rdoc rescue return
-
- return unless defined? RDoc::VERSION # RDoc 1 does not have VERSION
-
- require 'rdoc/ri/driver'
-
- options = {
- :use_cache => true,
- :use_system => true,
- :use_site => true,
- :use_home => true,
- :use_gems => true,
- :formatter => RDoc::RI::Formatter,
- }
-
- RDoc::RI::Driver.new(options).class_cache
- end
-
- ##
- # Create a document manager for +spec+. +rdoc_args+ contains arguments for
- # RDoc (template etc.) as a String.
-
- def initialize(spec, rdoc_args="")
- require 'fileutils'
- @spec = spec
- @doc_dir = spec.doc_dir
- @rdoc_args = rdoc_args.nil? ? [] : rdoc_args.split
- end
-
- ##
- # Is the RDoc documentation installed?
-
- def rdoc_installed?
- File.exist?(File.join(@doc_dir, "rdoc"))
- end
-
- ##
- # Is the RI documentation installed?
-
- def ri_installed?
- File.exist?(File.join(@doc_dir, "ri"))
- end
-
- ##
- # Generate the RI documents for this gem spec.
- #
- # Note that if both RI and RDoc documents are generated from the same
- # process, the RI docs should be done first (a likely bug in RDoc will cause
- # RI docs generation to fail if run after RDoc).
-
- def generate_ri
- setup_rdoc
- install_ri # RDoc bug, ri goes first
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
- end
-
- ##
- # Generate the RDoc documents for this gem spec.
- #
- # Note that if both RI and RDoc documents are generated from the same
- # process, the RI docs should be done first (a likely bug in RDoc will cause
- # RI docs generation to fail if run after RDoc).
-
- def generate_rdoc
- setup_rdoc
- install_rdoc
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
- end
-
- ##
- # Generate and install RDoc into the documentation directory
-
- def install_rdoc
- rdoc_dir = File.join @doc_dir, 'rdoc'
-
- FileUtils.rm_rf rdoc_dir
-
- say "Installing RDoc documentation for #{@spec.full_name}..."
- run_rdoc '--op', rdoc_dir
- end
-
- ##
- # Generate and install RI into the documentation directory
-
- def install_ri
- ri_dir = File.join @doc_dir, 'ri'
-
- FileUtils.rm_rf ri_dir
-
- say "Installing ri documentation for #{@spec.full_name}..."
- run_rdoc '--ri', '--op', ri_dir
- end
-
- ##
- # Run RDoc with +args+, which is an ARGV style argument list
-
- def run_rdoc(*args)
- args << @spec.rdoc_options
- args << self.class.configured_args
- args << @spec.require_paths.clone
- args << @spec.extra_rdoc_files
- args << '--title' << "#{@spec.full_name} Documentation"
- args << '--quiet'
- args = args.flatten.map do |arg| arg.to_s end
-
- if self.class.rdoc_version >= Gem::Version.new('2.4.0') then
- args.delete '--inline-source'
- args.delete '--promiscuous'
- args.delete '-p'
- args.delete '--one-file'
- # HACK more
- end
-
- debug_args = args.dup
-
- r = RDoc::RDoc.new
-
- old_pwd = Dir.pwd
- Dir.chdir @spec.full_gem_path
-
- say "rdoc #{args.join ' '}" if Gem.configuration.really_verbose
-
- begin
- r.document args
- rescue Errno::EACCES => e
- dirname = File.dirname e.message.split("-")[1].strip
- raise Gem::FilePermissionError.new(dirname)
- rescue Interrupt => e
- raise e
- rescue Exception => ex
- alert_error "While generating documentation for #{@spec.full_name}"
- ui.errs.puts "... MESSAGE: #{ex}"
- ui.errs.puts "... RDOC args: #{debug_args.join(' ')}"
- ui.errs.puts "\t#{ex.backtrace.join "\n\t"}" if
- Gem.configuration.backtrace
- terminate_interaction 1
- ensure
- Dir.chdir old_pwd
- end
- end
-
- def setup_rdoc
- if File.exist?(@doc_dir) && !File.writable?(@doc_dir) then
- raise Gem::FilePermissionError.new(@doc_dir)
- end
-
- FileUtils.mkdir_p @doc_dir unless File.exist?(@doc_dir)
-
- self.class.load_rdoc
- end
-
- ##
- # Remove RDoc and RI documentation
-
- def uninstall_doc
- base_dir = @spec.base_dir
- raise Gem::FilePermissionError.new base_dir unless File.writable? base_dir
-
- # TODO: ok... that's twice... ugh
- old_name = [
- @spec.name, @spec.version, @spec.original_platform].join '-'
-
- doc_dir = @spec.doc_dir
- unless File.directory? doc_dir then
- doc_dir = File.join File.dirname(doc_dir), old_name
- end
-
- ri_dir = @spec.ri_dir
- unless File.directory? ri_dir then
- ri_dir = File.join File.dirname(ri_dir), old_name
- end
-
- FileUtils.rm_rf doc_dir
- FileUtils.rm_rf ri_dir
- 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
index 950b34d744..4bbc5217e0 100644
--- a/lib/rubygems/errors.rb
+++ b/lib/rubygems/errors.rb
@@ -1,35 +1,177 @@
-class Gem::ErrorReason; end
+# frozen_string_literal: true
-# Generated when trying to lookup a gem to indicate that the gem
-# was found, but that it isn't usable on the current platform.
+#--
+# This file contains all the various exceptions and other errors that are used
+# inside of RubyGems.
#
-# fetch and install read these and report them to the user to aid
-# in figuring out why a gem couldn't be installed.
-#
-class Gem::PlatformMismatch < Gem::ErrorReason
+# 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
- attr_reader :name
- attr_reader :version
- attr_reader :platforms
+ private
- def initialize(name, version)
- @name = name
- @version = version
- @platforms = []
+ def build_message
+ total = Gem::Specification.stubs.size
+ "Could not find '#{name}' (#{requirement}) among #{total} total gem(s)\n"
+ end
end
- def add_platform(platform)
- @platforms << platform
+ ##
+ # 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
- def wordy
- prefix = "Found #{@name} (#{@version})"
+ 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
- if @platforms.size == 1
- "#{prefix}, but was for platform #{@platforms[0]}"
- else
- "#{prefix}, but was for platforms #{@platforms.join(' ,')}"
+ 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
index 55d67f9125..e00a70c662 100644
--- a/lib/rubygems/exceptions.rb
+++ b/lib/rubygems/exceptions.rb
@@ -1,3 +1,7 @@
+# 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.
@@ -5,17 +9,65 @@ 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
##
@@ -24,10 +76,15 @@ class Gem::EndOfYAMLException < Gem::Exception; end
##
# Signals that a file permission error is preventing the user from
-# installing in the requested directories.
+# operating on the given directory.
+
class Gem::FilePermissionError < Gem::Exception
- def initialize(path)
- super("You don't have write permissions into the #{path} directory.")
+ attr_reader :directory
+
+ def initialize(directory)
+ @directory = directory
+
+ super "You don't have write permissions for the #{directory} directory."
end
end
@@ -37,19 +94,49 @@ class Gem::FormatException < Gem::Exception
attr_accessor :file_path
end
-class Gem::GemNotFoundException < Gem::Exception
- def initialize(msg, name=nil, version=nil, errors=nil)
- super msg
+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
- attr_reader :name, :version, :errors
+ ##
+ # 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
@@ -73,19 +160,92 @@ 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
- attr_accessor :exit_code
+ ##
+ # The exit code for the process
+
+ alias_method :exit_code, :status
+
+ ##
+ # Creates a new SystemExitException with the given +exit_code+
def initialize(exit_code)
- @exit_code = exit_code
+ super exit_code, "Exiting RubyGems with exit_code #{exit_code}"
+ end
+end
- super "Exiting RubyGems with exit_code #{exit_code}"
+##
+# 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
index 97ee762a4a..b5ca126a08 100644
--- a/lib/rubygems/ext.rb
+++ b/lib/rubygems/ext.rb
@@ -1,18 +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.
#++
-require 'rubygems'
-
##
# Classes for building C extensions live here.
module Gem::Ext; end
-require 'rubygems/ext/builder'
-require 'rubygems/ext/configure_builder'
-require 'rubygems/ext/ext_conf_builder'
-require 'rubygems/ext/rake_builder'
-
+require_relative "ext/build_error"
+require_relative "ext/builder"
+require_relative "ext/configure_builder"
+require_relative "ext/ext_conf_builder"
+require_relative "ext/rake_builder"
+require_relative "ext/cmake_builder"
+require_relative "ext/cargo_builder"
diff --git a/lib/rubygems/ext/build_error.rb b/lib/rubygems/ext/build_error.rb
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
index 5e518962ce..e00cf159da 100644
--- a/lib/rubygems/ext/builder.rb
+++ b/lib/rubygems/ext/builder.rb
@@ -1,56 +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)
- unless File.exist? 'Makefile' then
- raise Gem::InstallError, "Makefile not found:\n\n#{results.join "\n"}"
+ 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
- mf = File.read('Makefile')
- mf = mf.gsub(/^RUBYARCHDIR\s*=\s*\$[^$]*/, "RUBYARCHDIR = #{dest_path}")
- mf = mf.gsub(/^RUBYLIBDIR\s*=\s*\$[^$]*/, "RUBYLIBDIR = #{dest_path}")
+ # 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)
- File.open('Makefile', 'wb') {|f| f.print mf}
+ 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"]) : ""
- # try to find make program from Ruby configure arguments first
- RbConfig::CONFIG['configure_args'] =~ /with-make-prog\=(\w+)/
- make_program = $1 || ENV['make']
- unless make_program then
- make_program = (/mswin/ =~ RUBY_PLATFORM) ? 'nmake' : 'make'
+ # 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
- ['', ' install'].each do |target|
- cmd = "#{make_program}#{target}"
- results << cmd
- results << `#{cmd} #{redirector}`
+ env = [destdir]
- raise Gem::InstallError, "make#{target} failed:\n\n#{results}" unless
- $?.success?
+ 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.redirector
- '2>&1'
+ 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)
- results << command
- results << `#{command} #{redirector}`
+ 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)
- unless $?.success? then
- raise Gem::InstallError, "#{class_name} failed:\n\n#{results.join "\n"}"
+ 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
-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
index c2087eb5ad..230b214b3c 100644
--- a/lib/rubygems/ext/configure_builder.rb
+++ b/lib/rubygems/ext/configure_builder.rb
@@ -1,25 +1,26 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/ext/builder'
-
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
- def self.build(extension, directory, dest_path, results)
- unless File.exist?('Makefile') then
- cmd = "sh ./configure --prefix=#{dest_path}"
- cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty?
+ unless File.exist?(File.join(configure_dir, "Makefile"))
+ cmd = ["sh", "./configure", "--prefix=#{dest_path}", *args]
- run cmd, results
+ run cmd, results, class_name, configure_dir
end
- make dest_path, results
+ 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
index b3d588dc9c..822454355d 100644
--- a/lib/rubygems/ext/ext_conf_builder.rb
+++ b/lib/rubygems/ext/ext_conf_builder.rb
@@ -1,24 +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.
#++
-require 'rubygems/ext/builder'
-require 'rubygems/command'
-
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)
- def self.build(extension, directory, dest_path, results)
- cmd = "#{Gem.ruby} #{File.basename extension}"
- cmd << " #{Gem::Command.build_args.join ' '}" unless Gem::Command.build_args.empty?
+ 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
- run cmd, results
+ ENV["DESTDIR"] = nil
- make dest_path, results
+ 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
index a1df694366..d702d7f339 100644
--- a/lib/rubygems/ext/rake_builder.rb
+++ b/lib/rubygems/ext/rake_builder.rb
@@ -1,39 +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.
#++
-require 'rubygems/ext/builder'
-require 'rubygems/command'
-
class Gem::Ext::RakeBuilder < Gem::Ext::Builder
-
- def self.build(extension, directory, dest_path, results)
- if File.basename(extension) =~ /mkrf_conf/i then
- cmd = "#{Gem.ruby} #{File.basename extension}"
- cmd << " #{Gem::Command.build_args.join " "}" unless Gem::Command.build_args.empty?
- run cmd, results
+ 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
- # Deal with possible spaces in the path, e.g. C:/Program Files
- dest_path = '"' + dest_path.to_s + '"' if dest_path.to_s.include?(' ')
-
- rake = ENV['rake']
-
- rake ||= begin
- "\"#{Gem.ruby}\" -rubygems #{Gem.bin_path('rake', 'rake')}"
- rescue Gem::Exception
- end
+ if /mkrf_conf/i.match?(File.basename(extension))
+ run([Gem.ruby, File.basename(extension), *args], results, class_name, extension_dir)
+ end
- rake ||= Gem.default_exec_format % 'rake'
+ rake = ENV["rake"]
- cmd = "#{rake} RUBYARCHDIR=#{dest_path} RUBYLIBDIR=#{dest_path}" # ENV is frozen
+ 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
- run cmd, results
+ 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/format.rb b/lib/rubygems/format.rb
deleted file mode 100644
index 246c599316..0000000000
--- a/lib/rubygems/format.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems/package'
-
-##
-# Gem::Format knows the guts of the RubyGem .gem file format and provides the
-# capability to read gem files
-
-class Gem::Format
-
- attr_accessor :spec
- attr_accessor :file_entries
- attr_accessor :gem_path
-
- ##
- # Constructs a Format representing the gem's data which came from +gem_path+
-
- def initialize(gem_path)
- @gem_path = gem_path
- end
-
- ##
- # Reads the gem +file_path+ using +security_policy+ and returns a Format
- # representing the data in the gem
-
- def self.from_file_by_path(file_path, security_policy = nil)
- unless File.exist?(file_path)
- raise Gem::Exception, "Cannot load gem at [#{file_path}] in #{Dir.pwd}"
- end
-
- start = File.read file_path, 20
-
- if start.nil? or start.length < 20 then
- nil
- elsif start.include?("MD5SUM =") # old version gems
- require 'rubygems/old_format'
-
- Gem::OldFormat.from_file_by_path file_path
- else
- begin
- open file_path, Gem.binary_mode do |io|
- from_io io, file_path, security_policy
- end
- rescue Gem::Package::TarInvalidError => e
- message = "corrupt gem (#{e.class}: #{e.message})"
- raise Gem::Package::FormatError.new(message, file_path)
- end
- end
- end
-
- ##
- # Reads a gem from +io+ at +gem_path+ using +security_policy+ and returns a
- # Format representing the data from the gem
-
- def self.from_io(io, gem_path="(io)", security_policy = nil)
- format = new gem_path
-
- Gem::Package.open io, 'r', security_policy do |pkg|
- format.spec = pkg.metadata
- format.file_entries = []
-
- pkg.each do |entry|
- size = entry.header.size
- mode = entry.header.mode
-
- format.file_entries << [{
- "size" => size, "mode" => mode, "path" => entry.full_name,
- },
- entry.read
- ]
- end
- end
-
- format
- end
-
-end
-
diff --git a/lib/rubygems/gem_openssl.rb b/lib/rubygems/gem_openssl.rb
deleted file mode 100644
index 682058f2c1..0000000000
--- a/lib/rubygems/gem_openssl.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-#--
-# Some system might not have OpenSSL installed, therefore the core
-# library file openssl might not be available. We localize testing
-# for the presence of OpenSSL in this file.
-#++
-
-module Gem
- class << self
- ##
- # Is SSL (used by the signing commands) available on this
- # platform?
-
- def ssl_available?
- @ssl_available
- end
-
- ##
- # Is SSL available?
-
- attr_writer :ssl_available
-
- ##
- # Ensure that SSL is available. Throw an exception if it is not.
-
- def ensure_ssl_available
- unless ssl_available?
- raise Gem::Exception, "SSL is not installed on this system"
- end
- end
- end
-end
-
-# :stopdoc:
-
-begin
- require 'openssl'
-
- # Reference a constant defined in the .rb portion of ssl (just to
- # make sure that part is loaded too).
-
- Gem.ssl_available = !!OpenSSL::Digest::SHA1
-
- class OpenSSL::X509::Certificate
- # Check the validity of this certificate.
- def check_validity(issuer_cert = nil, time = Time.now)
- ret = if @not_before && @not_before > time
- [false, :expired, "not valid before '#@not_before'"]
- elsif @not_after && @not_after < time
- [false, :expired, "not valid after '#@not_after'"]
- elsif issuer_cert && !verify(issuer_cert.public_key)
- [false, :issuer, "#{issuer_cert.subject} is not issuer"]
- else
- [true, :ok, 'Valid certificate']
- end
-
- # return hash
- { :is_valid => ret[0], :error => ret[1], :desc => ret[2] }
- end
- end
-
-rescue LoadError, StandardError
- Gem.ssl_available = false
-end
-
-module Gem::SSL
-
- # We make our own versions of the constants here. This allows us
- # to reference the constants, even though some systems might not
- # have SSL installed in the Ruby core package.
- #
- # These constants are only used during load time. At runtime, any
- # method that makes a direct reference to SSL software must be
- # protected with a Gem.ensure_ssl_available call.
-
- if Gem.ssl_available? then
- PKEY_RSA = OpenSSL::PKey::RSA
- DIGEST_SHA1 = OpenSSL::Digest::SHA1
- else
- PKEY_RSA = :rsa
- DIGEST_SHA1 = :sha1
- end
-
-end
-
diff --git a/lib/rubygems/gem_path_searcher.rb b/lib/rubygems/gem_path_searcher.rb
deleted file mode 100644
index 814b5fb0e5..0000000000
--- a/lib/rubygems/gem_path_searcher.rb
+++ /dev/null
@@ -1,172 +0,0 @@
-require "rubygems"
-require "rubygems/deprecate"
-
-##
-# GemPathSearcher has the capability to find loadable files inside
-# gems. It generates data up front to speed up searches later.
-
-class Gem::GemPathSearcher
-
- ##
- # Initialise the data we need to make searches later.
-
- def initialize
- # We want a record of all the installed gemspecs, in the order we wish to
- # examine them.
- # TODO: remove this stupid method
- @gemspecs = init_gemspecs
-
- # Map gem spec to glob of full require_path directories. Preparing this
- # information may speed up searches later.
- @lib_dirs = {}
-
- @gemspecs.each do |spec|
- @lib_dirs[spec.object_id] = lib_dirs_for spec
- end
- end
-
- ##
- # Look in all the installed gems until a matching +glob+ is found.
- # Return the _gemspec_ of the gem where it was found. If no match
- # is found, return nil.
- #
- # The gems are searched in alphabetical order, and in reverse
- # version order.
- #
- # For example:
- #
- # find('log4r') # -> (log4r-1.1 spec)
- # find('log4r.rb') # -> (log4r-1.1 spec)
- # find('rake/rdoctask') # -> (rake-0.4.12 spec)
- # find('foobarbaz') # -> nil
- #
- # Matching paths can have various suffixes ('.rb', '.so', and
- # others), which may or may not already be attached to _file_.
- # This method doesn't care about the full filename that matches;
- # only that there is a match.
-
- def find(glob)
- # HACK violation of encapsulation
- @gemspecs.find do |spec|
- # TODO: inverted responsibility
- matching_file? spec, glob
- end
- end
-
- # Looks through the available gemspecs and finds the first
- # one that contains +file+ as a requirable file.
-
- def find_spec_for_file(file)
- @gemspecs.find do |spec|
- return spec if spec.contains_requirable_file?(file)
- end
- end
-
- def find_active(glob)
- # HACK violation of encapsulation
- @gemspecs.find do |spec|
- # TODO: inverted responsibility
- spec.loaded? and matching_file? spec, glob
- end
- end
-
- ##
- # Works like #find, but finds all gemspecs matching +glob+.
-
- def find_all(glob)
- # HACK violation of encapsulation
- @gemspecs.select do |spec|
- # TODO: inverted responsibility
- matching_file? spec, glob
- end || []
- end
-
- def find_in_unresolved(glob)
- # HACK violation
- specs = Gem.unresolved_deps.values.map { |dep|
- Gem.source_index.search dep, true
- }.flatten
-
- specs.select do |spec|
- # TODO: inverted responsibility
- matching_file? spec, glob
- end || []
- end
-
- def find_in_unresolved_tree glob
- # HACK violation
- # TODO: inverted responsibility
- specs = Gem.unresolved_deps.values.map { |dep|
- Gem.source_index.search dep, true
- }.flatten
-
- specs.reverse_each do |spec|
- trails = matching_paths(spec, glob)
- next if trails.empty?
- return trails.map(&:reverse).sort.first.reverse
- end
-
- []
- end
-
- ##
- # Attempts to find a matching path using the require_paths of the given
- # +spec+.
-
- def matching_file?(spec, path)
- not matching_files(spec, path).empty?
- end
-
- def matching_paths(spec, path)
- trails = []
-
- spec.traverse do |from_spec, dep, to_spec, trail|
- next unless to_spec.conflicts.empty?
- trails << trail unless matching_files(to_spec, path).empty?
- end
-
- trails
- end
-
- ##
- # Returns files matching +path+ in +spec+.
- #--
- # Some of the intermediate results are cached in @lib_dirs for speed.
-
- def matching_files(spec, path)
- return [] unless @lib_dirs[spec.object_id] # case no paths
- glob = File.join @lib_dirs[spec.object_id], "#{path}#{Gem.suffix_pattern}"
- Dir[glob].select { |f| File.file? f.untaint }
- end
-
- ##
- # Return a list of all installed gemspecs, sorted by alphabetical order and
- # in reverse version order. (bar-2, bar-1, foo-2)
-
- def init_gemspecs
- Gem::Specification.sort { |a, b|
- names = a.name <=> b.name
- next names if names.nonzero?
- b.version <=> a.version
- }
- end
-
- ##
- # Returns library directories glob for a gemspec. For example,
- # '/usr/local/lib/ruby/gems/1.8/gems/foobar-1.0/{lib,ext}'
-
- def lib_dirs_for(spec)
- "#{spec.full_gem_path}/{#{spec.require_paths.join(',')}}" if
- spec.require_paths
- end
-
- extend Gem::Deprecate
-
- deprecate :initialize, :none, 2011, 10
- deprecate :find, :none, 2011, 10
- deprecate :find_active, :none, 2011, 10
- deprecate :find_all, :none, 2011, 10
- deprecate :find_in_unresolved, :none, 2011, 10
- deprecate :find_in_unresolved_tree, :none, 2011, 10
- deprecate :find_spec_for_file, :none, 2011, 10
-end
diff --git a/lib/rubygems/gem_runner.rb b/lib/rubygems/gem_runner.rb
index 6197036f81..e60cebd0cb 100644
--- a/lib/rubygems/gem_runner.rb
+++ b/lib/rubygems/gem_runner.rb
@@ -1,18 +1,13 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require "rubygems"
-require 'rubygems/command_manager'
-require 'rubygems/config_file'
-require 'rubygems/doc_manager'
-
-##
-# Load additional plugins from $LOAD_PATH
-
-Gem.load_env_plugins rescue nil
+require_relative "../rubygems"
+require_relative "command_manager"
##
# Run an instance of the gem program.
@@ -24,63 +19,70 @@ Gem.load_env_plugins rescue nil
# classes they call directly.
class Gem::GemRunner
-
- def initialize(options={})
- # TODO: nuke these options
- @command_manager_class = options[:command_manager] || Gem::CommandManager
- @config_file_class = options[:config_file] || Gem::ConfigFile
- @doc_manager_class = options[:doc_manager] || Gem::DocManager
+ def initialize
+ @command_manager_class = Gem::CommandManager
+ @config_file_class = Gem::ConfigFile
end
##
# Run the gem command with the following arguments.
def run(args)
- start_time = Time.now
-
- if args.include?('--')
- # We need to preserve the original ARGV to use for passing gem options
- # to source gems. If there is a -- in the line, strip all options after
- # it...its for the source building process.
- build_args = args[args.index("--") + 1...args.length]
- args = args[0...args.index("--")]
- end
-
- Gem::Command.build_args = build_args if build_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 ' '
+ config_args.split " "
else
Array(config_args)
- end
+ end
Gem::Command.add_specific_extra_args command_name, config_args
end
- cmd.run Gem.configuration.args
- end_time = Time.now
+ cmd.run Gem.configuration.args, build_args
+ end
- if Gem.configuration.benchmark then
- printf "\nExecution time: %0.2f seconds.\n", end_time - start_time
- puts "Press Enter to finish"
- STDIN.gets
- 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]
- @doc_manager_class.configured_args = Gem.configuration[:rdoc]
end
-
end
-
-Gem.load_plugins
diff --git a/lib/rubygems/gemcutter_utilities.rb b/lib/rubygems/gemcutter_utilities.rb
index c0e7ee99e9..9c22c14fad 100644
--- a/lib/rubygems/gemcutter_utilities.rb
+++ b/lib/rubygems/gemcutter_utilities.rb
@@ -1,82 +1,398 @@
-require 'rubygems/remote_fetcher'
+# 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
- OptionParser.accept Symbol do |value|
- value.to_sym
- end
+ 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/credentials') do |value,options|
+ 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 options[:key] then
+ 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
- def sign_in
- return if Gem.configuration.rubygems_api_key
+ ##
+ # 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 "Enter your RubyGems.org credentials."
- say "Don't have an account yet? Create one at http://rubygems.org/sign_up"
+ say "The existing key doesn't have access of #{scope} on #{pretty_host}. Please sign in to update access."
- email = ask " Email: "
- password = ask_for_password "Password: "
+ identifier = ask "Username/email: "
+ password = ask_for_password " Password: "
+
+ response = rubygems_api_request(:put, "api/v1/api_key",
+ sign_in_host, scope: scope) do |request|
+ request.basic_auth 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"
- response = rubygems_api_request :get, "api/v1/api_key" do |request|
- request.basic_auth email, password
+ 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."
- Gem.configuration.rubygems_api_key = resp.body
+ 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
- def rubygems_api_request(method, path, host = Gem.host, &block)
- require 'net/http'
- host = ENV['RUBYGEMS_HOST'] if ENV['RUBYGEMS_HOST']
- uri = URI.parse "#{host}/#{path}"
+ ##
+ # 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
- say "Pushing gem to #{host}..."
+ private
- request_method = Net::HTTP.const_get method.to_s.capitalize
+ 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, &block)
+ 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 with_response(resp)
- case resp
- when Net::HTTPSuccess then
- if block_given? then
- yield resp
+ 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
- say resp.body
+ 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
- say resp.body
- terminate_interaction 1
+ host
end
end
- def verify_api_key(key)
- if Gem.configuration.api_keys.key? key then
- Gem.configuration.api_keys[key]
+ def get_scope_params(scope)
+ scope_params = { index_rubygems: true, push_rubygem: true }
+
+ if scope
+ scope_params = { scope => true }
else
- alert_error "No such API key. You can add it with gem keys --add #{key}"
- terminate_interaction 1
+ 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/indexer.rb b/lib/rubygems/indexer.rb
deleted file mode 100644
index e87e5a3632..0000000000
--- a/lib/rubygems/indexer.rb
+++ /dev/null
@@ -1,644 +0,0 @@
-require 'rubygems'
-require 'rubygems/format'
-require 'time'
-
-begin
- gem 'builder'
- require 'builder/xchar'
-rescue LoadError
-end
-
-##
-# Top level class for building the gem repository index.
-
-class Gem::Indexer
-
- include Gem::UserInteraction
-
- ##
- # Build indexes for RubyGems older than 1.2.0 when true
-
- attr_accessor :build_legacy
-
- ##
- # Build indexes for RubyGems 1.2.0 and newer when true
-
- attr_accessor :build_modern
-
- ##
- # Index install location
-
- attr_reader :dest_directory
-
- ##
- # Specs index install location
-
- attr_reader :dest_specs_index
-
- ##
- # Latest specs index install location
-
- attr_reader :dest_latest_specs_index
-
- ##
- # Prerelease specs index install location
-
- attr_reader :dest_prerelease_specs_index
-
- ##
- # Index build directory
-
- attr_reader :directory
-
- ##
- # Create an indexer that will index the gems in +directory+.
-
- def initialize(directory, options = {})
- require 'fileutils'
- require 'tmpdir'
- require 'zlib'
-
- unless defined?(Builder::XChar) then
- raise "Gem::Indexer requires that the XML Builder library be installed:" \
- "\n\tgem install builder"
- end
-
- options = { :build_legacy => true, :build_modern => true }.merge options
-
- @build_legacy = options[:build_legacy]
- @build_modern = options[:build_modern]
-
- @rss_title = options[:rss_title]
- @rss_host = options[:rss_host]
- @rss_gems_host = options[:rss_gems_host]
-
- @dest_directory = directory
- @directory = File.join(Dir.tmpdir, "gem_generate_index_#{$$}")
-
- marshal_name = "Marshal.#{Gem.marshal_version}"
-
- @master_index = File.join @directory, 'yaml'
- @marshal_index = File.join @directory, marshal_name
-
- @quick_dir = File.join @directory, 'quick'
- @quick_marshal_dir = File.join @quick_dir, marshal_name
- @quick_marshal_dir_base = File.join "quick", marshal_name # FIX: UGH
-
- @quick_index = File.join @quick_dir, 'index'
- @latest_index = File.join @quick_dir, 'latest_index'
-
- @specs_index = File.join @directory, "specs.#{Gem.marshal_version}"
- @latest_specs_index =
- File.join(@directory, "latest_specs.#{Gem.marshal_version}")
- @prerelease_specs_index =
- File.join(@directory, "prerelease_specs.#{Gem.marshal_version}")
- @dest_specs_index =
- File.join(@dest_directory, "specs.#{Gem.marshal_version}")
- @dest_latest_specs_index =
- File.join(@dest_directory, "latest_specs.#{Gem.marshal_version}")
- @dest_prerelease_specs_index =
- File.join(@dest_directory, "prerelease_specs.#{Gem.marshal_version}")
-
- @rss_index = File.join @directory, 'index.rss'
-
- @files = []
- 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(spec)
- spec.files = []
- spec.test_files = []
- spec.rdoc_options = []
- spec.extra_rdoc_files = []
- spec.cert_chain = []
- spec
- end
-
- ##
- # Build various indicies
-
- def build_indicies
- # Marshal gemspecs are used by both modern and legacy RubyGems
-
- Gem::Specification.dirs = []
- Gem::Specification.add_specs(*map_gems_to_specs(gem_file_list))
-
- build_marshal_gemspecs
- build_legacy_indicies if @build_legacy
- build_modern_indicies if @build_modern
- build_rss
-
- compress_indicies
- end
-
- ##
- # Builds indicies for RubyGems older than 1.2.x
-
- def build_legacy_indicies
- index = collect_specs
-
- say "Generating Marshal master index"
-
- Gem.time 'Generated Marshal master index' do
- open @marshal_index, 'wb' do |io|
- io.write index.dump
- end
- end
-
- @files << @marshal_index
- @files << "#{@marshal_index}.Z"
- end
-
- ##
- # Builds Marshal quick index gemspecs.
-
- def build_marshal_gemspecs
- count = Gem::Specification.count
- progress = ui.progress_reporter count,
- "Generating Marshal quick index gemspecs for #{count} gems",
- "Complete"
-
- files = []
-
- Gem.time 'Generated Marshal quick index gemspecs' do
- Gem::Specification.each do |spec|
- spec_file_name = "#{spec.original_name}.gemspec.rz"
- marshal_name = File.join @quick_marshal_dir, spec_file_name
-
- marshal_zipped = Gem.deflate Marshal.dump(spec)
- open marshal_name, 'wb' do |io| io.write marshal_zipped end
-
- files << marshal_name
-
- progress.updated spec.original_name
- end
-
- progress.done
- end
-
- @files << @quick_marshal_dir
-
- files
- end
-
- ##
- # Build a single index for RubyGems 1.2 and newer
-
- def build_modern_index(index, file, name)
- say "Generating #{name} index"
-
- Gem.time "Generated #{name} index" do
- open(file, 'wb') do |io|
- specs = index.map do |*spec|
- # We have to splat here because latest_specs is an array, while the
- # others are hashes.
- spec = spec.flatten.last
- platform = spec.original_platform
-
- # win32-api-1.0.4-x86-mswin32-60
- unless String === platform then
- alert_warning "Skipping invalid platform in gem: #{spec.full_name}"
- next
- end
-
- platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
- [spec.name, spec.version, platform]
- end
-
- specs = compact_specs(specs)
- Marshal.dump(specs, io)
- end
- end
- end
-
- ##
- # Builds indicies for RubyGems 1.2 and newer. Handles full, latest, prerelease
-
- def build_modern_indicies
- prerelease, released = Gem::Specification.partition { |s|
- s.version.prerelease?
- }
- latest_specs = Gem::Specification.latest_specs
-
- build_modern_index(released.sort, @specs_index, 'specs')
- build_modern_index(latest_specs.sort, @latest_specs_index, 'latest specs')
- build_modern_index(prerelease.sort, @prerelease_specs_index,
- 'prerelease specs')
-
- @files += [@specs_index,
- "#{@specs_index}.gz",
- @latest_specs_index,
- "#{@latest_specs_index}.gz",
- @prerelease_specs_index,
- "#{@prerelease_specs_index}.gz"]
- end
-
- ##
- # Builds an RSS feed for past two days gem releases according to the gem's
- # date.
-
- def build_rss
- if @rss_host.nil? or @rss_gems_host.nil? then
- if Gem.configuration.really_verbose then
- alert_warning "no --rss-host or --rss-gems-host, RSS generation disabled"
- end
- return
- end
-
- require 'cgi'
- require 'rubygems/text'
-
- extend Gem::Text
-
- Gem.time 'Generated rss' do
- open @rss_index, 'wb' do |io|
- rss_host = CGI.escapeHTML @rss_host
- rss_title = CGI.escapeHTML(@rss_title || 'gems')
-
- io.puts <<-HEADER
-<?xml version="1.0"?>
-<rss version="2.0">
- <channel>
- <title>#{rss_title}</title>
- <link>http://#{rss_host}</link>
- <description>Recently released gems from http://#{rss_host}</description>
- <generator>RubyGems v#{Gem::VERSION}</generator>
- <docs>http://cyber.law.harvard.edu/rss/rss.html</docs>
- HEADER
-
- today = Gem::Specification::TODAY
- yesterday = today - 86400
-
- index = Gem::Specification.select do |spec|
- spec_date = spec.date
- # TODO: remove this and make YAML based specs properly normalized
- spec_date = Time.parse(spec_date.to_s) if Date === spec_date
-
- spec_date >= yesterday && spec_date <= today
- end
-
- index.sort_by { |spec| [-spec.date.to_i, spec] }.each do |spec|
- file_name = File.basename spec.cache_file
- gem_path = CGI.escapeHTML "http://#{@rss_gems_host}/gems/#{file_name}"
- size = File.stat(spec.loaded_from).size # rescue next
-
- description = spec.description || spec.summary || ''
- authors = Array spec.authors
- emails = Array spec.email
- authors = emails.zip(authors).map do |email, author|
- email += " (#{author})" if author and not author.empty?
- end.join ', '
-
- description = description.split(/\n\n+/).map do |chunk|
- format_text chunk, 78
- end
-
- description = description.join "\n\n"
-
- item = ''
-
- item << <<-ITEM
- <item>
- <title>#{CGI.escapeHTML spec.full_name}</title>
- <description>
-&lt;pre&gt;#{CGI.escapeHTML description.chomp}&lt;/pre&gt;
- </description>
- <author>#{CGI.escapeHTML authors}</author>
- <guid>#{CGI.escapeHTML spec.full_name}</guid>
- <enclosure url=\"#{gem_path}\"
- length=\"#{size}\" type=\"application/octet-stream\" />
- <pubDate>#{spec.date.rfc2822}</pubDate>
- ITEM
-
- item << <<-ITEM if spec.homepage
- <link>#{CGI.escapeHTML spec.homepage}</link>
- ITEM
-
- item << <<-ITEM
- </item>
- ITEM
-
- io.puts item
- end
-
- io.puts <<-FOOTER
- </channel>
-</rss>
- FOOTER
- end
- end
-
- @files << @rss_index
- end
-
- def map_gems_to_specs gems
- gems.map { |gemfile|
- if File.size(gemfile) == 0 then
- alert_warning "Skipping zero-length gem: #{gemfile}"
- next
- end
-
- begin
- spec = Gem::Format.from_file_by_path(gemfile).spec
- spec.loaded_from = gemfile
-
- # HACK: fuck this shit - borks all tests that use pl1
- # if File.basename(gemfile, ".gem") != spec.original_name then
- # exp = spec.full_name
- # exp << " (#{spec.original_name})" if
- # spec.original_name != spec.full_name
- # msg = "Skipping misnamed gem: #{gemfile} should be named #{exp}"
- # alert_warning msg
- # next
- # end
-
- abbreviate spec
- sanitize spec
-
- spec
- rescue SignalException => e
- alert_error "Received signal, exiting"
- raise
- rescue Exception => e
- msg = ["Unable to process #{gemfile}",
- "#{e.message} (#{e.class})",
- "\t#{e.backtrace.join "\n\t"}"].join("\n")
- alert_error msg
- end
- }.compact
- end
-
- ##
- # Collect specifications from .gem files from the gem directory.
-
- def collect_specs(gems = gem_file_list)
- Gem::Deprecate.skip_during do
- index = Gem::SourceIndex.new
-
- map_gems_to_specs(gems).each do |spec|
- index.add_spec spec, spec.original_name
- end
-
- index
- end
- end
-
- ##
- # Compresses indicies on disk
- #--
- # All future files should be compressed using gzip, not deflate
-
- def compress_indicies
- say "Compressing indicies"
-
- Gem.time 'Compressed indicies' do
- if @build_legacy then
- compress @marshal_index, 'Z'
- paranoid @marshal_index, 'Z'
- end
-
- if @build_modern then
- gzip @specs_index
- gzip @latest_specs_index
- gzip @prerelease_specs_index
- end
- end
- end
-
- ##
- # Compacts Marshal output for the specs index data source by using identical
- # objects as much as possible.
-
- def compact_specs(specs)
- names = {}
- versions = {}
- platforms = {}
-
- specs.map do |(name, version, platform)|
- names[name] = name unless names.include? name
- versions[version] = version unless versions.include? version
- platforms[platform] = platform unless platforms.include? platform
-
- [names[name], versions[version], platforms[platform]]
- end
- end
-
- ##
- # Compress +filename+ with +extension+.
-
- def compress(filename, extension)
- data = Gem.read_binary filename
-
- zipped = Gem.deflate data
-
- open "#{filename}.#{extension}", 'wb' do |io|
- io.write zipped
- end
- end
-
- ##
- # List of gem file names to index.
-
- def gem_file_list
- Dir[File.join(@dest_directory, "gems", '*.gem')]
- end
-
- ##
- # Builds and installs indicies.
-
- def generate_index
- make_temp_directories
- build_indicies
- install_indicies
- rescue SignalException
- ensure
- FileUtils.rm_rf @directory
- end
-
- ##
- # Zlib::GzipWriter wrapper that gzips +filename+ on disk.
-
- def gzip(filename)
- Zlib::GzipWriter.open "#{filename}.gz" do |io|
- io.write Gem.read_binary(filename)
- end
- end
-
- ##
- # Install generated indicies into the destination directory.
-
- def install_indicies
- verbose = Gem.configuration.really_verbose
-
- say "Moving index into production dir #{@dest_directory}" if verbose
-
- files = @files
- files.delete @quick_marshal_dir if files.include? @quick_dir
-
- if files.include? @quick_marshal_dir and not files.include? @quick_dir then
- files.delete @quick_marshal_dir
-
- dst_name = File.join(@dest_directory, @quick_marshal_dir_base)
-
- FileUtils.mkdir_p File.dirname(dst_name), :verbose => verbose
- FileUtils.rm_rf dst_name, :verbose => verbose
- FileUtils.mv(@quick_marshal_dir, dst_name,
- :verbose => verbose, :force => true)
- end
-
- files = files.map do |path|
- path.sub(/^#{Regexp.escape @directory}\/?/, '') # HACK?
- end
-
- files.each do |file|
- src_name = File.join @directory, file
- dst_name = File.join @dest_directory, file
-
- FileUtils.rm_rf dst_name, :verbose => verbose
- FileUtils.mv(src_name, @dest_directory,
- :verbose => verbose, :force => true)
- end
- end
-
- ##
- # Make directories for index generation
-
- def make_temp_directories
- FileUtils.rm_rf @directory
- FileUtils.mkdir_p @directory, :mode => 0700
- FileUtils.mkdir_p @quick_marshal_dir
- end
-
- ##
- # Ensure +path+ and path with +extension+ are identical.
-
- def paranoid(path, extension)
- data = Gem.read_binary path
- compressed_data = Gem.read_binary "#{path}.#{extension}"
-
- unless data == Gem.inflate(compressed_data) then
- raise "Compressed file #{compressed_path} does not match uncompressed file #{path}"
- end
- 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(spec)
- spec.summary = sanitize_string(spec.summary)
- spec.description = sanitize_string(spec.description)
- spec.post_install_message = sanitize_string(spec.post_install_message)
- spec.authors = spec.authors.collect { |a| sanitize_string(a) }
-
- spec
- 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 = string.to_s
-
- begin
- Builder::XChar.encode string
- rescue NameError, NoMethodError
- string.to_xs
- end
- end
-
- ##
- # Perform an in-place update of the repository from newly added gems. Only
- # works for modern indicies, and sets #build_legacy to false when run.
-
- def update_index
- @build_legacy = false
-
- make_temp_directories
-
- specs_mtime = File.stat(@dest_specs_index).mtime
- newest_mtime = Time.at 0
-
- updated_gems = gem_file_list.select do |gem|
- gem_mtime = File.stat(gem).mtime
- newest_mtime = gem_mtime if gem_mtime > newest_mtime
- gem_mtime >= specs_mtime
- end
-
- if updated_gems.empty? then
- say 'No new gems'
- terminate_interaction 0
- end
-
- specs = map_gems_to_specs updated_gems
- prerelease, released = specs.partition { |s| s.version.prerelease? }
-
- files = build_marshal_gemspecs
-
- Gem.time 'Updated indexes' do
- update_specs_index released, @dest_specs_index, @specs_index
- update_specs_index released, @dest_latest_specs_index, @latest_specs_index
- update_specs_index(prerelease,
- @dest_prerelease_specs_index,
- @prerelease_specs_index)
- end
-
- compress_indicies
-
- verbose = Gem.configuration.really_verbose
-
- say "Updating production dir #{@dest_directory}" if verbose
-
- files << @specs_index
- files << "#{@specs_index}.gz"
- files << @latest_specs_index
- files << "#{@latest_specs_index}.gz"
- files << @prerelease_specs_index
- files << "#{@prerelease_specs_index}.gz"
-
- files = files.map do |path|
- path.sub(/^#{Regexp.escape @directory}\/?/, '') # HACK?
- end
-
- files.each do |file|
- src_name = File.join @directory, file
- dst_name = File.join @dest_directory, file # REFACTOR: duped above
-
- FileUtils.mv src_name, dst_name, :verbose => verbose,
- :force => true
-
- File.utime newest_mtime, newest_mtime, dst_name
- end
- end
-
- ##
- # Combines specs in +index+ and +source+ then writes out a new copy to
- # +dest+. For a latest index, does not ensure the new file is minimal.
-
- def update_specs_index(index, source, dest)
- specs_index = Marshal.load Gem.read_binary(source)
-
- index.each do |spec|
- platform = spec.original_platform
- platform = Gem::Platform::RUBY if platform.nil? or platform.empty?
- specs_index << [spec.name, spec.version, platform]
- end
-
- specs_index = compact_specs specs_index.uniq.sort
-
- open dest, 'wb' do |io|
- Marshal.dump specs_index, io
- end
- 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
index 3ee6432b4c..e8859cadaf 100644
--- a/lib/rubygems/install_update_options.rb
+++ b/lib/rubygems/install_update_options.rb
@@ -1,128 +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 'rubygems'
-
-# forward-declare
-
-module Gem::Security # :nodoc:
- class Policy # :nodoc:
- end
-end
+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
- OptionParser.accept Gem::Security::Policy do |value|
- require 'rubygems/security'
+ 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
- value = Gem::Security::Policies[value]
- valid = Gem::Security::Policies.keys.sort
- message = "#{value} (#{valid.join ', '} are valid)"
- raise OptionParser::InvalidArgument, message if value.nil?
- value
+ 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", '-i', '--install-dir DIR',
- 'Gem repository directory to get installed',
- 'gems') do |value, options|
- options[:install_dir] = File.expand_path(value)
+ 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", '-n', '--bindir DIR',
- 'Directory where binary files are',
- 'located') do |value, options|
- options[:bin_dir] = File.expand_path(value)
+ 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", '-d', '--[no-]rdoc',
- 'Generate RDoc documentation for the gem on',
- 'install') do |value, options|
- options[:generate_rdoc] = value
+ 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", '--[no-]ri',
- 'Generate RI documentation for the gem on',
- 'install') do |value, options|
- options[:generate_ri] = value
+ add_option(:"Install/Update", "-N", "--no-document",
+ "Disable documentation generation") do |_value, options|
+ options[:document] = []
end
- add_option(:"Install/Update", '-E', '--[no-]env-shebang',
+ add_option(:"Install/Update", "-E", "--[no-]env-shebang",
"Rewrite the shebang line on installed",
"scripts to use /usr/bin/env") do |value, options|
options[:env_shebang] = value
end
- add_option(:"Install/Update", '-f', '--[no-]force',
- 'Force gem to install, bypassing dependency',
- 'checks') do |value, options|
+ add_option(:"Install/Update", "-f", "--[no-]force",
+ "Force gem to install, bypassing dependency",
+ "checks") do |value, options|
options[:force] = value
end
- add_option(:"Install/Update", '-w', '--[no-]wrappers',
- 'Use bin wrappers for executables',
- 'Not available on dosish platforms') do |value, options|
+ add_option(:"Install/Update", "-w", "--[no-]wrappers",
+ "Use bin wrappers for executables",
+ "Not available on dosish platforms") do |value, options|
options[:wrappers] = value
end
- add_option(:"Install/Update", '-P', '--trust-policy POLICY',
- Gem::Security::Policy,
- 'Specify gem trust policy') do |value, options|
- options[:security_policy] = value
- end
+ add_security_option
- add_option(:"Install/Update", '--ignore-dependencies',
- 'Do not install any required dependent gems') do |value, options|
+ add_option(:"Install/Update", "--ignore-dependencies",
+ "Do not install any required dependent gems") do |value, options|
options[:ignore_dependencies] = value
end
- add_option(:"Install/Update", '-y', '--include-dependencies',
- 'Unconditionally install the required',
- 'dependent gems') do |value, options|
- options[:include_dependencies] = value
- end
-
- add_option(:"Install/Update", '--[no-]format-executable',
- 'Make installed executable names match ruby.',
- 'If ruby is ruby18, foo_exec will be',
- 'foo_exec18') do |value, options|
+ add_option(:"Install/Update", "--[no-]format-executable",
+ "Make installed executable names match Ruby.",
+ "If Ruby is ruby18, foo_exec will be",
+ "foo_exec18") do |value, options|
options[:format_executable] = value
end
- add_option(:"Install/Update", '--[no-]user-install',
- 'Install in user\'s home directory instead',
- 'of GEM_HOME.') do |value, options|
+ add_option(:"Install/Update", "--[no-]user-install",
+ "Install in user's home directory instead",
+ "of GEM_HOME.") do |value, options|
options[:user_install] = value
end
add_option(:"Install/Update", "--development",
- "Install any additional development",
- "dependencies") do |value, options|
+ "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|
+ "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 command.
+ # Default options for the gem install and update commands.
- def install_update_defaults_str
- '--rdoc --no-force --wrappers'
+ def install_update_options
+ {
+ document: %w[ri],
+ }
end
-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
index 74d803d7fa..a6e1dc4730 100644
--- a/lib/rubygems/installer.rb
+++ b/lib/rubygems/installer.rb
@@ -1,18 +1,19 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/format'
-require 'rubygems/exceptions'
-require 'rubygems/ext'
-require 'rubygems/require_paths_builder'
-require 'rubygems/user_interaction'
+require_relative "installer_uninstaller_utils"
+require_relative "exceptions"
+require_relative "package"
+require_relative "ext"
+require_relative "user_interaction"
##
-# The installer class processes RubyGem .gem files and installs the files
-# contained in the .gem into the Gem.path.
+# 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
@@ -25,29 +26,28 @@ require 'rubygems/user_interaction'
# 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]
+ ENV_PATHS = %w[/usr/bin/env /bin/env].freeze
##
- # Raised when there is an error while building extensions.
- #
- class ExtensionBuildError < Gem::InstallError; end
+ # Deprecated in favor of Gem::Ext::BuildError
- include Gem::UserInteraction
+ ExtensionBuildError = Gem::Ext::BuildError # :nodoc:
- include Gem::RequirePathsBuilder if Gem::QUICKLOADER_SUCKAGE
+ include Gem::UserInteraction
- attr_reader :gem
+ 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
@@ -58,14 +58,18 @@ class Gem::Installer
attr_reader :options
- @path_warning = false
+ ##
+ # The gem package instance.
+
+ attr_reader :package
class << self
-
##
- # True if we've warned about PATH not including Gem.bindir
-
- attr_accessor :path_warning
+ # 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
@@ -73,60 +77,181 @@ class Gem::Installer
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 located at
- # +gem+. +options+ is a Hash with the following keys:
+ # 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.
- # :format_executable:: Format the executable the same as the ruby executable.
- # If your ruby is ruby18, foo_exec will be installed as
- # foo_exec18.
# :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(gem, options={})
- require 'fileutils'
+ def initialize(package, options = {})
+ require "fileutils"
- @gem = gem
@options = options
+ @package = package
+
process_options
- if options[:user_install] and not options[:unpack] then
- @gem_home = Gem.user_dir
- check_that_user_bin_dir_is_in_path
- end
+ @package.dir_mode = options[:dir_mode]
+ @package.prog_mode = options[:prog_mode]
+ @package.data_mode = options[:data_mode]
end
##
- # Lazy accessor for the spec's gem directory.
+ # 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 gem_dir
- @gem_dir ||= spec.gem_dir.dup.untaint
+ 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 installer's Gem::Format instance.
+ # Lazy accessor for the spec's gem directory.
- def format
- begin
- @format ||= Gem::Format.from_file_by_path gem, @security_policy
- rescue Gem::Package::FormatError
- raise Gem::InstallError, "invalid gem format for #{gem}"
- end
+ def gem_dir
+ @gem_dir ||= File.join(gem_home, "gems", spec.full_name)
end
##
# Lazy accessor for the installer's spec.
def spec
- @spec ||= format.spec
+ @package.spec
end
##
@@ -141,82 +266,96 @@ class Gem::Installer
# specifications/<gem-version>.gemspec #=> the Gem::Specification
def install
- current_home = Gem.dir
- current_path = Gem.paths.path
-
- verify_gem_home(options[:unpack])
- Gem.use_paths gem_home, current_path # HACK: shouldn't need Gem.paths.path
+ pre_install_checks
- # If we're forcing the install then disable security unless the security
- # policy says that we only install signed gems.
- @security_policy = nil if @force and @security_policy and
- not @security_policy.only_signed
+ run_pre_install_hooks
- unless @force
- ensure_required_ruby_version_met
- ensure_required_rubygems_version_met
- ensure_dependencies_met unless @ignore_dependencies
- end
+ # Set loaded_from to ensure extension_dir is correct
+ spec.loaded_from = spec_file
- Gem.pre_install_hooks.each do |hook|
- result = hook.call self
+ # Completely remove any previous gem files
+ FileUtils.rm_rf gem_dir
+ FileUtils.rm_rf spec.extension_dir
- if result == false then
- location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
+ dir_mode = options[:dir_mode]
+ FileUtils.mkdir_p gem_dir, mode: dir_mode && 0o755
- message = "pre-install hook#{location} failed for #{spec.full_name}"
- raise Gem::InstallError, message
- end
- end
+ extract_files
- Gem.ensure_gem_subdirectories gem_home
+ build_extensions
+ write_build_info_file
+ run_post_build_hooks
- # Completely remove any previous gem files
- FileUtils.rm_rf(gem_dir) if File.exist? gem_dir
+ generate_bin
+ if options[:install_plugin] == false
+ remove_stale_plugins
+ warn_skipped_plugins
+ else
+ generate_plugins
+ end
- FileUtils.mkdir_p gem_dir
+ write_spec
+ write_cache_file
- extract_files
- build_extensions
+ File.chmod(dir_mode, gem_dir) if dir_mode
- Gem.post_build_hooks.each do |hook|
- result = hook.call self
+ say clean_text(spec.post_install_message.to_s) if options[:post_install_message] && !spec.post_install_message.nil?
- if result == false then
- FileUtils.rm_rf gem_dir
+ Gem::Specification.add_spec(spec) unless @install_dir
- location = " at #{$1}" if hook.inspect =~ /@(.*:\d+)/
+ load_plugin unless options[:install_plugin] == false
- message = "post-build hook#{location} failed for #{spec.full_name}"
- raise Gem::InstallError, message
- end
- end
+ run_post_install_hooks
- generate_bin
- write_spec
+ spec
+ rescue Errno::EACCES => e
+ # Permission denied - /path/to/foo
+ raise Gem::FilePermissionError, e.message.split(" - ").last
+ end
- write_require_paths_file_if_needed if Gem::QUICKLOADER_SUCKAGE
+ 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+)/
- cache_file = spec.cache_file
- FileUtils.cp gem, cache_file unless File.exist? cache_file
+ message = "pre-install hook#{location} failed for #{spec.full_name}"
+ raise Gem::InstallError, message
+ end
+ end
- say spec.post_install_message unless spec.post_install_message.nil?
+ def run_post_build_hooks # :nodoc:
+ Gem.post_build_hooks.each do |hook|
+ next unless hook.call(self) == false
+ FileUtils.rm_rf gem_dir
- spec.loaded_from = spec.spec_file
+ location = " at #{$1}" if hook.inspect =~ /[ @](.*:\d+)/
- Gem::Specification.add_spec spec unless Gem::Specification.include? spec
+ 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.
- return spec
- rescue Zlib::GzipFile::Error
- raise Gem::InstallError, "gzip error installing #{gem}"
- ensure
- # conditional since we might be here because we're erroring out early.
- if current_path
- Gem.use_paths current_home, current_path
+ 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
@@ -228,26 +367,42 @@ class Gem::Installer
# dependency :: Gem::Dependency
def ensure_dependency(spec, dependency)
- unless installation_satisfies_dependency? dependency then
+ unless installation_satisfies_dependency? dependency
raise Gem::InstallError, "#{spec.name} requires #{dependency}"
end
true
end
##
- # True if the gems in the source_index satisfy +dependency+.
+ # True if the gems in the system satisfy +dependency+.
def installation_satisfies_dependency?(dependency)
- not dependency.matching_specs.empty?
+ 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
##
- # Unpacks the gem into the given directory.
+ # The location of the spec file that is installed.
+ #
- def unpack(directory)
- @gem_dir = directory
- @format = Gem::Format.from_file_by_path gem, @security_policy
- extract_files
+ 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
##
@@ -255,76 +410,109 @@ class Gem::Installer
# specifications directory.
def write_spec
- file_name = spec.spec_file.untaint
+ spec.installed_by_version = Gem.rubygems_version
- File.open(file_name, "w") do |file|
- file.puts spec.to_ruby_for_cache
- end
+ Gem.write_binary(spec_file, spec.to_ruby_for_cache)
+ end
+
+ ##
+ # Writes the full .gemspec specification (in Ruby) to the gem home's
+ # specifications/default directory.
+ #
+ # In contrast to #write_spec, this keeps file lists, so the `gem contents`
+ # command works.
+
+ def write_default_spec
+ 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? then
- script_name = filename + ".bat"
+ if Gem.win_platform?
+ script_name = formatted_program_filename(filename) + ".bat"
script_path = File.join bindir, File.basename(script_name)
- File.open script_path, 'w' do |file|
+ File.open script_path, "w" do |file|
file.puts windows_stub_script(bindir, filename)
end
- say script_path if Gem.configuration.really_verbose
+ verbose script_path
end
end
- def generate_bin
- return if spec.executables.nil? or spec.executables.empty?
+ def generate_bin # :nodoc:
+ executables = spec.executables
+ return if executables.nil? || executables.empty?
- # 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.
- bindir = @bin_dir || Gem.bindir(gem_home)
+ if @gem_home == Gem.user_dir
+ # If we get here, then one of the following likely happened:
+ # - `--user-install` was specified
+ # - `Gem::PathSupport#home` fell back to `Gem.user_dir`
+ # - GEM_HOME was manually set to `Gem.user_dir`
+
+ check_that_user_bin_dir_is_in_path(executables)
+ end
- Dir.mkdir bindir unless File.exist? bindir
- raise Gem::FilePermissionError.new(bindir) unless File.writable? bindir
+ ensure_writable_dir @bin_dir
- spec.executables.each do |filename|
- filename.untaint
- bin_path = File.expand_path File.join(gem_dir, spec.bindir, filename)
+ executables.each do |filename|
+ bin_path = File.join gem_dir, spec.bindir, filename
+ next unless File.exist? bin_path
- unless File.exist? bin_path
- warn "Hey?!?! Where did #{bin_path} go??"
- next
+ mode = File.stat(bin_path).mode
+ dir_mode = options[:prog_mode] || (mode | 0o111)
+
+ unless dir_mode == mode
+ File.chmod dir_mode, bin_path
end
- mode = File.stat(bin_path).mode | 0111
- FileUtils.chmod mode, bin_path
+ check_executable_overwrite filename
- if @wrappers then
- generate_bin_script filename, bindir
+ if @wrappers
+ generate_bin_script filename, @bin_dir
else
- generate_bin_symlink filename, bindir
+ 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
- # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/193379
+ # https://blade.ruby-lang.org/ruby-talk/193379
def generate_bin_script(filename, bindir)
bin_script_path = File.join bindir, formatted_program_filename(filename)
- FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
+ Gem.open_file_with_lock(bin_script_path) do
+ require "fileutils"
+ FileUtils.rm_f bin_script_path # prior install may have been --no-wrappers
- File.open bin_script_path, 'wb', 0755 do |file|
- file.print app_script_text(filename)
+ File.open(bin_script_path, "wb", 0o755) do |file|
+ file.write app_script_text(filename)
+ file.chmod(options[:prog_mode] || 0o755)
+ end
end
- say bin_script_path if Gem.configuration.really_verbose
+ verbose bin_script_path
generate_windows_script filename, bindir
end
@@ -334,73 +522,93 @@ class Gem::Installer
# the symlink if the gem being installed has a newer version.
def generate_bin_symlink(filename, bindir)
- if Gem.win_platform? then
- alert_warning "Unable to use symlinks on Windows, installing wrapper"
- generate_bin_script filename, bindir
- return
- end
-
src = File.join gem_dir, spec.bindir, filename
dst = File.join bindir, formatted_program_filename(filename)
- if File.exist? dst then
- if File.symlink? dst then
+ if File.exist? dst
+ if File.symlink? dst
link = File.readlink(dst).split File::SEPARATOR
- cur_version = Gem::Version.create(link[-3].sub(/^.*-/, ''))
+ cur_version = Gem::Version.create(link[-3].sub(/^.*-/, ""))
return if spec.version < cur_version
end
File.unlink dst
end
- FileUtils.symlink src, dst, :verbose => Gem.configuration.really_verbose
+ FileUtils.symlink src, dst, verbose: Gem.configuration.really_verbose
+ rescue NotImplementedError, SystemCallError
+ alert_warning "Unable to use symlinks, installing wrapper"
+ generate_bin_script filename, bindir
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)
- ruby_name = Gem::ConfigMap[:ruby_install_name] if @env_shebang
- path = spec.bin_file bin_file_name
- first_line = File.open(path, "rb") {|file| file.gets}
+ path = File.join gem_dir, spec.bindir, bin_file_name
+ first_line = File.open(path, "rb", &:gets) || ""
- if /\A#!/ =~ first_line then
+ 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}")
+ shebang = first_line.sub(/\A\#!.*?ruby\S*((\s+\S+)+)/, "#!#{Gem.ruby}")
opts = $1
shebang.strip! # Avoid nasty ^M issues.
end
- if not ruby_name then
- "#!#{Gem.ruby}#{opts}"
- elsif opts then
- "#!/bin/sh\n'exec' #{ruby_name.dump} '-x' \"$0\" \"$@\"\n#{shebang}"
- else
+ 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_name}"
+ "#!#{@env_path} #{ruby_install_name}"
+ else
+ "#{bash_prolog_script}#!#{Gem.ruby}#{opts}"
end
end
- def ensure_required_ruby_version_met
- if rrv = spec.required_ruby_version then
- unless rrv.satisfied_by? Gem.ruby_version then
- raise Gem::InstallError, "#{spec.name} requires Ruby version #{rrv}."
- end
- end
- end
+ ##
+ # Ensures the Gem::Specification written out for this gem is loadable upon
+ # installation.
- def ensure_required_rubygems_version_met
- if rrgv = spec.required_rubygems_version then
- unless rrgv.satisfied_by? Gem::Version.new(Gem::VERSION) then
- raise Gem::InstallError,
- "#{spec.name} requires RubyGems version #{rrgv}. " +
- "Try 'gem update --system' to update RubyGems itself."
- end
+ 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
+ def ensure_dependencies_met # :nodoc:
deps = spec.runtime_dependencies
deps |= spec.development_dependencies if @development
@@ -409,149 +617,240 @@ class Gem::Installer
end
end
- def process_options
+ def process_options # :nodoc:
@options = {
- :bin_dir => nil,
- :env_shebang => false,
- :exec_format => false,
- :force => false,
- :install_dir => Gem.dir,
+ 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]
- @gem_home = options[:install_dir]
+ @install_dir = options[:install_dir]
+ @user_install = options[:user_install]
@ignore_dependencies = options[:ignore_dependencies]
@format_executable = options[:format_executable]
- @security_policy = options[:security_policy]
@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
- raise "NOTE: Installer option :source_index is dead" if
- options[:source_index]
+ # 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
+ def check_that_user_bin_dir_is_in_path(executables) # :nodoc:
user_bin_dir = @bin_dir || Gem.bindir(gem_home)
- user_bin_dir.gsub!(File::SEPARATOR, File::ALT_SEPARATOR) if File::ALT_SEPARATOR
- unless ENV['PATH'].split(File::PATH_SEPARATOR).include? user_bin_dir then
- unless self.class.path_warning then
- alert_warning "You don't have #{user_bin_dir} in your PATH,\n\t gem executables will not run."
- self.class.path_warning = true
+ 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(unpack = false)
- FileUtils.mkdir_p gem_home
- raise Gem::FilePermissionError, gem_home unless
- unpack or File.writable?(gem_home)
+ 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)
- return <<-TEXT
-#{shebang bin_file_name}
-#
-# This file was generated by RubyGems.
-#
-# The application '#{spec.name}' is installed as part of a gem, and
-# this file is here to facilitate running it.
-#
+ # 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
-require 'rubygems'
+ 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
-version = "#{Gem::Requirement.default}"
+ def gemdeps_load(name)
+ return "" if name == "bundler"
-if ARGV.first =~ /^_(.*)_$/ and Gem::Version.correct? $1 then
- version = $1
- ARGV.shift
-end
+ <<~TEXT
-gem '#{spec.name}', version
-load Gem.bin_path('#{spec.name}', '#{bin_file_name}', version)
-TEXT
+ Gem.use_gemdeps
+ TEXT
end
- ##
- # return the stub script text used to launch the true ruby script
+ def explicit_version_requirement(name)
+ code = "version = str"
+ return code unless name == "bundler"
- def windows_stub_script(bindir, bin_file_name)
- ruby = File.basename(Gem.ruby).chomp('"')
- return <<-TEXT
-@ECHO OFF
-IF NOT "%~f0" == "~f0" GOTO :WinNT
-@"#{ruby}" "#{File.join(bindir, bin_file_name)}" %1 %2 %3 %4 %5 %6 %7 %8 %9
-GOTO :EOF
-:WinNT
-@"#{ruby}" "%~dpn0" %*
-TEXT
+ 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
- return if spec.extensions.empty?
- say "Building native extensions. This could take a while..."
- dest_path = File.join gem_dir, spec.require_paths.first
- ran_rake = false # only run rake once
-
- spec.extensions.each do |extension|
- break if ran_rake
- results = []
-
- builder = case extension
- when /extconf/ then
- Gem::Ext::ExtConfBuilder
- when /configure/ then
- Gem::Ext::ConfigureBuilder
- when /rakefile/i, /mkrf_conf/i then
- ran_rake = true
- Gem::Ext::RakeBuilder
- else
- results = ["No builder for extension '#{extension}'"]
- nil
- end
-
-
- extension_dir = begin
- File.join gem_dir, File.dirname(extension)
- rescue TypeError # extension == nil
- gem_dir
- end
-
-
- begin
- Dir.chdir extension_dir do
- results = builder.build(extension, gem_dir, dest_path, results)
-
- say results.join("\n") if Gem.configuration.really_verbose
- end
- rescue
- results = results.join "\n"
+ if options[:build_extension] == false
+ warn_skipped_extensions
+ return
+ end
- gem_make_out = File.join extension_dir, 'gem_make.out'
+ builder = Gem::Ext::Builder.new spec, build_args, Gem.target_rbconfig, build_jobs
- open gem_make_out, 'wb' do |io| io.puts results end
+ builder.build_extensions
+ end
- message = <<-EOF
-ERROR: Failed to build gem native extension.
+ def warn_skipped_extensions # :nodoc:
+ return if spec.extensions.empty?
- #{results}
+ alert_warning "#{spec.full_name} contains native extensions that were not built.\n" \
+ "To build extensions, run: gem pristine #{spec.name} --extensions"
+ end
-Gem files will remain installed in #{gem_dir} for inspection.
-Results logged to #{gem_make_out}
-EOF
+ def warn_skipped_plugins # :nodoc:
+ return if spec.plugins.empty?
- raise ExtensionBuildError, message
- end
- end
+ 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
##
@@ -560,43 +859,23 @@ EOF
# Ensures that files can't be installed outside the gem directory.
def extract_files
- raise ArgumentError, "format required to extract from" if @format.nil?
-
- @format.file_entries.each do |entry, file_data|
- path = entry['path'].untaint
-
- if path.start_with? "/" then # for extra sanity
- raise Gem::InstallError, "attempt to install file into #{entry['path']}"
- end
-
- path = File.expand_path File.join(gem_dir, path)
-
- unless path.start_with? gem_dir then
- msg = "attempt to install file into %p under %s" %
- [entry['path'], gem_dir]
- raise Gem::InstallError, msg
- end
-
- FileUtils.rm_rf(path) if File.exist? path
-
- dir = File.dirname path
- FileUtils.mkdir_p dir unless File.exist? dir
-
- File.open(path, "wb") do |out|
- out.write file_data
- end
+ @package.extract_files gem_dir
+ end
- FileUtils.chmod entry['mode'], path
+ ##
+ # 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.
- say path if Gem.configuration.really_verbose
- end
+ 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 then
+ if @format_executable
self.class.exec_format % File.basename(filename)
else
filename
@@ -612,5 +891,153 @@ EOF
def dir
gem_dir.to_s
end
-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_test_case.rb b/lib/rubygems/installer_test_case.rb
deleted file mode 100644
index 7c7b3b98af..0000000000
--- a/lib/rubygems/installer_test_case.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-require 'rubygems/test_case'
-require 'rubygems/installer'
-
-class Gem::Installer
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :gem_dir
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :format
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :gem_home
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :env_shebang
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :ignore_dependencies
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :format_executable
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :security_policy
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :spec
-
- ##
- # Available through requiring rubygems/installer_test_case
-
- attr_writer :wrappers
-end
-
-##
-# A test case for Gem::Installer.
-
-class Gem::InstallerTestCase < Gem::TestCase
-
- def setup
- super
-
- @installer_tmp = File.join @tempdir, 'installer'
- FileUtils.mkdir_p @installer_tmp
-
- Gem.use_paths @installer_tmp
- Gem.ensure_gem_subdirectories @installer_tmp
-
- @spec = quick_gem 'a'
- util_make_exec @spec
- util_build_gem @spec
- @gem = @spec.cache_file
-
- @user_spec = quick_gem 'b'
- util_make_exec @user_spec
- util_build_gem @user_spec
- @user_gem = @user_spec.cache_file
-
- Gem.use_paths @gemhome
-
- @installer = util_installer @spec, @gemhome
- @user_installer = util_installer @user_spec, Gem.user_dir, :user
-
- Gem.use_paths @gemhome
- end
-
- def util_gem_bindir spec = @spec
- # TODO: deprecate
- spec.bin_dir
- end
-
- def util_gem_dir spec = @spec
- # TODO: deprecate
- spec.gem_dir
- end
-
- def util_inst_bindir
- File.join @gemhome, "bin"
- end
-
- def util_make_exec(spec = @spec, shebang = "#!/usr/bin/ruby")
- spec.executables = %w[executable]
- spec.files << 'bin/executable'
-
- exec_path = spec.bin_file "executable"
- write_file exec_path do |io|
- io.puts shebang
- end
-
- bin_path = File.join @tempdir, "bin", "executable"
- write_file bin_path do |io|
- io.puts shebang
- end
- end
-
- def util_setup_gem(ui = @ui) # HACK fix use_ui to make this automatic
- @spec.files << File.join('lib', 'code.rb')
- @spec.extensions << File.join('ext', 'a', 'mkrf_conf.rb')
-
- Dir.chdir @tempdir do
- FileUtils.mkdir_p 'bin'
- FileUtils.mkdir_p 'lib'
- FileUtils.mkdir_p File.join('ext', 'a')
- File.open File.join('bin', 'executable'), 'w' do |f| f.puts '1' end
- File.open File.join('lib', 'code.rb'), 'w' do |f| f.puts '1' end
- File.open File.join('ext', 'a', 'mkrf_conf.rb'), 'w' do |f|
- f << <<-EOF
- File.open 'Rakefile', 'w' do |rf| rf.puts "task :default" end
- EOF
- end
-
- use_ui ui do
- FileUtils.rm @gem
-
- @gem = Gem::Builder.new(@spec).build
- end
- end
-
- @installer = Gem::Installer.new @gem
- end
-
- def util_installer(spec, gem_home, user=false)
- Gem::Installer.new spec.cache_file, :user_install => user
- 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
index a1e106d9be..3b88c43149 100644
--- a/lib/rubygems/local_remote_options.rb
+++ b/lib/rubygems/local_remote_options.rb
@@ -1,30 +1,33 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'uri'
-require 'rubygems'
+require_relative "vendor/uri/lib/uri"
+require_relative "../rubygems"
##
# Mixin methods for local and remote Gem::Command options.
module Gem::LocalRemoteOptions
-
##
- # Allows OptionParser to handle HTTP URIs.
+ # Allows Gem::OptionParser to handle HTTP URIs.
def accept_uri_http
- OptionParser.accept URI::HTTP do |value|
+ Gem::OptionParser.accept Gem::URI::HTTP do |value|
begin
- uri = URI.parse value
- rescue URI::InvalidURIError
- raise OptionParser::InvalidArgument, value
+ uri = Gem::URI.parse value
+ rescue Gem::URI::InvalidURIError
+ raise Gem::OptionParser::InvalidArgument, value
end
- unless ['http', 'https', 'file'].include?(uri.scheme)
- raise OptionParser::InvalidArgument, value
+ 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
@@ -35,18 +38,18 @@ module Gem::LocalRemoteOptions
# Add local/remote options to the command line parser.
def add_local_remote_options
- add_option(:"Local/Remote", '-l', '--local',
- 'Restrict operations to the LOCAL domain') do |value, options|
+ add_option(:"Local/Remote", "-l", "--local",
+ "Restrict operations to the LOCAL domain") do |_value, options|
options[:domain] = :local
end
- add_option(:"Local/Remote", '-r', '--remote',
- 'Restrict operations to the REMOTE domain') do |value, options|
+ add_option(:"Local/Remote", "-r", "--remote",
+ "Restrict operations to the REMOTE domain") do |_value, options|
options[:domain] = :remote
end
- add_option(:"Local/Remote", '-b', '--both',
- 'Allow LOCAL and REMOTE operations') do |value, options|
+ add_option(:"Local/Remote", "-b", "--both",
+ "Allow LOCAL and REMOTE operations") do |_value, options|
options[:domain] = :both
end
@@ -61,10 +64,9 @@ module Gem::LocalRemoteOptions
# Add the --bulk-threshold option
def add_bulk_threshold_option
- add_option(:"Local/Remote", '-B', '--bulk-threshold COUNT',
+ add_option(:"Local/Remote", "-B", "--bulk-threshold COUNT",
"Threshold for switching to bulk",
- "synchronization (default #{Gem.configuration.bulk_threshold})") do
- |value, options|
+ "synchronization (default #{Gem.configuration.bulk_threshold})") do |value, _options|
Gem.configuration.bulk_threshold = value.to_i
end
end
@@ -73,9 +75,8 @@ module Gem::LocalRemoteOptions
# Add the --clear-sources option
def add_clear_sources_option
- add_option(:"Local/Remote", '--clear-sources',
- 'Clear the gem sources') do |value, options|
-
+ add_option(:"Local/Remote", "--clear-sources",
+ "Clear the gem sources") do |_value, options|
Gem.sources = nil
options[:sources_cleared] = true
end
@@ -87,9 +88,9 @@ module Gem::LocalRemoteOptions
def add_proxy_option
accept_uri_http
- add_option(:"Local/Remote", '-p', '--[no-]http-proxy [URL]', URI::HTTP,
- 'Use HTTP proxy for remote operations') do |value, options|
- options[:http_proxy] = (value == false) ? :no_proxy : value
+ add_option(:"Local/Remote", "-p", "--[no-]http-proxy [URL]", Gem::URI::HTTP,
+ "Use HTTP proxy for remote operations") do |value, options|
+ options[:http_proxy] = value == false ? :no_proxy : value
Gem.configuration[:http_proxy] = options[:http_proxy]
end
end
@@ -100,12 +101,11 @@ module Gem::LocalRemoteOptions
def add_source_option
accept_uri_http
- add_option(:"Local/Remote", '--source URL', URI::HTTP,
- 'Add URL as a remote source for gems') do |source, options|
+ 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?("/")
- source << '/' if source !~ /\/\z/
-
- if options.delete :sources_cleared then
+ if options.delete :sources_cleared
Gem.sources = [source]
else
Gem.sources << source unless Gem.sources.include?(source)
@@ -117,8 +117,8 @@ module Gem::LocalRemoteOptions
# Add the --update-sources option
def add_update_sources_option
- add_option(:Deprecated, '-u', '--[no-]update-sources',
- 'Update local source cache') do |value, options|
+ add_option(:Deprecated, "-u", "--[no-]update-sources",
+ "Update local source cache") do |value, _options|
Gem.configuration.update_sources = value
end
end
@@ -134,15 +134,13 @@ module Gem::LocalRemoteOptions
# Is local fetching enabled?
def local?
- options[:domain] == :local || options[:domain] == :both
+ [:local, :both].include?(options[:domain])
end
##
# Is remote fetching enabled?
def remote?
- options[:domain] == :remote || options[:domain] == :both
+ [:remote, :both].include?(options[:domain])
end
-
end
-
diff --git a/lib/rubygems/mock_gem_ui.rb b/lib/rubygems/mock_gem_ui.rb
deleted file mode 100644
index 13f0bf564b..0000000000
--- a/lib/rubygems/mock_gem_ui.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-require 'stringio'
-require 'rubygems/user_interaction'
-
-##
-# This Gem::StreamUI subclass records input and output to StringIO for
-# retrieval during tests.
-
-class Gem::MockGemUi < Gem::StreamUI
- class TermError < RuntimeError
- attr_reader :exit_code
-
- def initialize exit_code
- super
- @exit_code = exit_code
- end
- end
- class SystemExitException < RuntimeError; end
-
- module TTY
-
- attr_accessor :tty
-
- def tty?()
- @tty = true unless defined?(@tty)
- @tty
- end
-
- def noecho
- yield self
- end
- end
-
- def initialize(input = "")
- ins = StringIO.new input
- outs = StringIO.new
- errs = StringIO.new
-
- ins.extend TTY
- outs.extend TTY
- errs.extend TTY
-
- super ins, outs, errs, true
-
- @terminated = false
- end
-
- def input
- @ins.string
- end
-
- def output
- @outs.string
- end
-
- def error
- @errs.string
- end
-
- def terminated?
- @terminated
- end
-
- def terminate_interaction(status=0)
- @terminated = true
-
- raise TermError, status if status != 0
- raise SystemExitException
- end
-
-end
-
diff --git a/lib/rubygems/name_tuple.rb b/lib/rubygems/name_tuple.rb
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/old_format.rb b/lib/rubygems/old_format.rb
deleted file mode 100644
index a44fd533a5..0000000000
--- a/lib/rubygems/old_format.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems'
-
-##
-# The format class knows the guts of the RubyGem .gem file format and provides
-# the capability to read gem files
-
-class Gem::OldFormat
-
- attr_accessor :spec, :file_entries, :gem_path
-
- ##
- # Constructs an instance of a Format object, representing the gem's data
- # structure.
- #
- # gem:: [String] The file name of the gem
-
- def initialize(gem_path)
- require 'fileutils'
- require 'zlib'
- Gem.load_yaml
-
- @gem_path = gem_path
- end
-
- ##
- # Reads the named gem file and returns a Format object, representing the
- # data from the gem file
- #
- # file_path:: [String] Path to the gem file
-
- def self.from_file_by_path(file_path)
- unless File.exist?(file_path)
- raise Gem::Exception, "Cannot load gem file [#{file_path}]"
- end
-
- File.open(file_path, 'rb') do |file|
- from_io(file, file_path)
- end
- end
-
- ##
- # Reads a gem from an io stream and returns a Format object, representing
- # the data from the gem file
- #
- # io:: [IO] Stream from which to read the gem
-
- def self.from_io(io, gem_path="(io)")
- format = self.new(gem_path)
- skip_ruby(io)
- format.spec = read_spec(io)
- format.file_entries = []
- read_files_from_gem(io) do |entry, file_data|
- format.file_entries << [entry, file_data]
- end
- format
- end
-
- private
-
- ##
- # Skips the Ruby self-install header. After calling this method, the
- # IO index will be set after the Ruby code.
- #
- # file:: [IO] The IO to process (skip the Ruby code)
-
- def self.skip_ruby(file)
- end_seen = false
- loop {
- line = file.gets
- if(line == nil || line.chomp == "__END__") then
- end_seen = true
- break
- end
- }
-
- if end_seen == false then
- raise Gem::Exception.new("Failed to find end of ruby script while reading gem")
- end
- end
-
- ##
- # Reads the specification YAML from the supplied IO and constructs
- # a Gem::Specification from it. After calling this method, the
- # IO index will be set after the specification header.
- #
- # file:: [IO] The IO to process
-
- def self.read_spec(file)
- yaml = ''
-
- read_until_dashes file do |line|
- yaml << line
- end
-
- Gem::Specification.from_yaml yaml
- rescue YAML::Error => e
- raise Gem::Exception, "Failed to parse gem specification out of gem file"
- rescue ArgumentError => e
- raise Gem::Exception, "Failed to parse gem specification out of gem file"
- end
-
- ##
- # Reads lines from the supplied IO until a end-of-yaml (---) is
- # reached
- #
- # file:: [IO] The IO to process
- # block:: [String] The read line
-
- def self.read_until_dashes(file)
- while((line = file.gets) && line.chomp.strip != "---") do
- yield line
- end
- end
-
- ##
- # Reads the embedded file data from a gem file, yielding an entry
- # containing metadata about the file and the file contents themselves
- # for each file that's archived in the gem.
- # NOTE: Many of these methods should be extracted into some kind of
- # Gem file read/writer
- #
- # gem_file:: [IO] The IO to process
-
- def self.read_files_from_gem(gem_file)
- errstr = "Error reading files from gem"
- header_yaml = ''
- begin
- self.read_until_dashes(gem_file) do |line|
- header_yaml << line
- end
- header = YAML.load(header_yaml)
- raise Gem::Exception, errstr unless header
-
- header.each do |entry|
- file_data = ''
- self.read_until_dashes(gem_file) do |line|
- file_data << line
- end
- yield [entry, Zlib::Inflate.inflate(file_data.strip.unpack("m")[0])]
- end
- rescue Zlib::DataError
- raise Gem::Exception, errstr
- end
- 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
index 2b50c588ee..7e41b18f66 100644
--- a/lib/rubygems/package.rb
+++ b/lib/rubygems/package.rb
@@ -1,83 +1,769 @@
-# -*- coding: utf-8 -*-
-#--
+# frozen_string_literal: true
+
+# rubocop:disable Style/AsciiComments
+
# Copyright (C) 2004 Mauricio Julio Fernández Pradier
# See LICENSE.txt for additional licensing information.
-#++
-require 'rubygems/specification'
+# rubocop:enable Style/AsciiComments
-module Gem::Package
+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 Error < StandardError; end
- class NonSeekableIO < Error; end
- class ClosedIO < Error; end
- class BadCheckSum < Error; end
- class TooLongFileName < Error; end
class FormatError < Error
attr_reader :path
- def initialize message, path = nil
- @path = path
+ def initialize(message, source = nil)
+ if source
+ @path = source.is_a?(String) ? source : source.path
- message << " in #{path}" if 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
- # FIX: zenspider said: does it really take an IO?
- # passed to a method called open?!? that seems stupid.
- def self.open(io, mode = "r", signer = nil, &block)
- tar_type = case mode
- when 'r' then TarInput
- when 'w' then TarOutput
- else
- raise "Unknown Package open mode"
- end
-
- tar_type.open(io, signer, &block)
- end
-
- def self.pack(src, destname, signer = nil)
- TarOutput.open(destname, signer) do |outp|
- dir_class.chdir(src) do
- outp.metadata = (file_class.read("RPA/metadata") rescue nil)
- find_class.find('.') do |entry|
- case
- when file_class.file?(entry)
- entry.sub!(%r{\./}, "")
- next if entry =~ /\ARPA\//
- stat = File.stat(entry)
- outp.add_file_simple(entry, stat.mode, stat.size) do |os|
- file_class.open(entry, "rb") do |f|
- os.write(f.read(4096)) until f.eof?
- end
- end
- when file_class.dir?(entry)
- entry.sub!(%r{\./}, "")
- next if entry == "RPA"
- outp.mkdir(entry, file_class.stat(entry).mode)
+ 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
- raise "Don't know how to pack this yet!"
+ 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
-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
-require 'rubygems/package/f_sync_dir'
-require 'rubygems/package/tar_header'
-require 'rubygems/package/tar_input'
-require 'rubygems/package/tar_output'
-require 'rubygems/package/tar_reader'
-require 'rubygems/package/tar_reader/entry'
-require 'rubygems/package/tar_writer'
+ 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/f_sync_dir.rb b/lib/rubygems/package/f_sync_dir.rb
deleted file mode 100644
index f7eb7f3ce3..0000000000
--- a/lib/rubygems/package/f_sync_dir.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# -*- coding: utf-8 -*-
-#--
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#++
-
-module Gem::Package::FSyncDir
-
- private
-
- ##
- # make sure this hits the disc
-
- def fsync_dir(dirname)
- dir = open dirname, 'r'
- dir.fsync
- rescue # ignore IOError if it's an unpatched (old) Ruby
- ensure
- dir.close if dir rescue nil
- 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
index 4f923b9b5e..dd20d65080 100644
--- a/lib/rubygems/package/tar_header.rb
+++ b/lib/rubygems/package/tar_header.rb
@@ -1,8 +1,11 @@
-# -*- coding: utf-8 -*-
-#--
+# 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
##
#--
@@ -28,7 +31,6 @@
# A header for a tar file
class Gem::Package::TarHeader
-
##
# Fields in the tar header
@@ -49,143 +51,143 @@ class Gem::Package::TarHeader
: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' # prefix
+ PACK_FORMAT = ("a100" + # name
+ "a8" + # mode
+ "a8" + # uid
+ "a8" + # gid
+ "a12" + # size
+ "a12" + # mtime
+ "a7a" + # chksum
+ "a" + # typeflag
+ "a100" + # linkname
+ "a6" + # magic
+ "a2" + # version
+ "a32" + # uname
+ "a32" + # gname
+ "a8" + # devmajor
+ "a8" + # devminor
+ "a155").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' # prefix
+ UNPACK_FORMAT = ("A100" + # name
+ "A8" + # mode
+ "A8" + # uid
+ "A8" + # gid
+ "A12" + # size
+ "A12" + # mtime
+ "A8" + # checksum
+ "A" + # typeflag
+ "A100" + # linkname
+ "A6" + # magic
+ "A2" + # version
+ "A32" + # uname
+ "A32" + # gname
+ "A8" + # devmajor
+ "A8" + # devminor
+ "A155").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
- empty = (header == "\0" * 512)
+ return EMPTY if header == EMPTY_HEADER
fields = header.unpack UNPACK_FORMAT
- name = fields.shift
- mode = fields.shift.oct
- uid = fields.shift.oct
- gid = fields.shift.oct
- size = fields.shift.oct
- mtime = fields.shift.oct
- checksum = fields.shift.oct
- typeflag = fields.shift
- linkname = fields.shift
- magic = fields.shift
- version = fields.shift.oct
- uname = fields.shift
- gname = fields.shift
- devmajor = fields.shift.oct
- devminor = fields.shift.oct
- prefix = fields.shift
-
- new :name => name,
- :mode => mode,
- :uid => uid,
- :gid => gid,
- :size => size,
- :mtime => mtime,
- :checksum => checksum,
- :typeflag => typeflag,
- :linkname => linkname,
- :magic => magic,
- :version => version,
- :uname => uname,
- :gname => gname,
- :devmajor => devmajor,
- :devminor => devminor,
- :prefix => prefix,
-
- :empty => empty
-
- # HACK unfactor for Rubinius
- #new :name => fields.shift,
- # :mode => fields.shift.oct,
- # :uid => fields.shift.oct,
- # :gid => fields.shift.oct,
- # :size => fields.shift.oct,
- # :mtime => fields.shift.oct,
- # :checksum => fields.shift.oct,
- # :typeflag => fields.shift,
- # :linkname => fields.shift,
- # :magic => fields.shift,
- # :version => fields.shift.oct,
- # :uname => fields.shift,
- # :gname => fields.shift,
- # :devmajor => fields.shift.oct,
- # :devminor => fields.shift.oct,
- # :prefix => fields.shift,
-
- # :empty => empty
+ new name: fields.shift,
+ mode: strict_oct(fields.shift),
+ uid: oct_or_256based(fields.shift),
+ gid: oct_or_256based(fields.shift),
+ size: strict_oct(fields.shift),
+ mtime: strict_oct(fields.shift),
+ checksum: strict_oct(fields.shift),
+ typeflag: fields.shift,
+ linkname: fields.shift,
+ magic: fields.shift,
+ version: strict_oct(fields.shift),
+ uname: fields.shift,
+ gname: fields.shift,
+ devmajor: strict_oct(fields.shift),
+ devminor: strict_oct(fields.shift),
+ prefix: fields.shift,
+
+ empty: false
+ end
+
+ def self.strict_oct(str)
+ 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] then
+ unless vals[:name] && vals[:size] && vals[:prefix] && vals[:mode]
raise ArgumentError, ":name, :size, :prefix and :mode required"
end
- vals[:uid] ||= 0
- vals[:gid] ||= 0
- vals[:mtime] ||= 0
- vals[:checksum] ||= ""
- vals[:typeflag] ||= "0"
- vals[:magic] ||= "ustar"
- vals[:version] ||= "00"
- vals[:uname] ||= "wheel"
- vals[:gname] ||= "wheel"
- vals[:devmajor] ||= 0
- vals[:devminor] ||= 0
-
- FIELDS.each do |name|
- instance_variable_set "@#{name}", vals[name]
- end
+ @checksum = vals[:checksum] || ""
+ @devmajor = vals[:devmajor] || 0
+ @devminor = vals[:devminor] || 0
+ @gid = vals[:gid] || 0
+ @gname = vals[:gname] || "wheel"
+ @linkname = vals[:linkname]
+ @magic = vals[:magic] || "ustar"
+ @mode = vals[:mode]
+ @mtime = vals[:mtime] || 0
+ @name = vals[:name]
+ @prefix = vals[:prefix]
+ @size = vals[:size]
+ @typeflag = vals[:typeflag]
+ @typeflag = "0" if @typeflag.nil? || @typeflag.empty?
+ @uid = vals[:uid] || 0
+ @uname = vals[:uname] || "wheel"
+ @version = vals[:version] || "00"
@empty = vals[:empty]
end
+ EMPTY = new({ # :nodoc:
+ checksum: 0,
+ gname: "",
+ linkname: "",
+ magic: "",
+ mode: 0,
+ name: "",
+ prefix: "",
+ size: 0,
+ uname: "",
+ version: 0,
+
+ empty: true,
+ }).freeze
+ private_constant :EMPTY
+
##
# Is the tar entry empty?
@@ -194,23 +196,23 @@ class Gem::Package::TarHeader
end
def ==(other) # :nodoc:
- self.class === other and
- @checksum == other.checksum and
- @devmajor == other.devmajor and
- @devminor == other.devminor and
- @gid == other.gid and
- @gname == other.gname and
- @linkname == other.linkname and
- @magic == other.magic and
- @mode == other.mode and
- @mtime == other.mtime and
- @name == other.name and
- @prefix == other.prefix and
- @size == other.size and
- @typeflag == other.typeflag and
- @uid == other.uid and
- @uname == other.uname and
- @version == other.version
+ self.class === other &&
+ @checksum == other.checksum &&
+ @devmajor == other.devmajor &&
+ @devminor == other.devminor &&
+ @gid == other.gid &&
+ @gname == other.gname &&
+ @linkname == other.linkname &&
+ @magic == other.magic &&
+ @mode == other.mode &&
+ @mtime == other.mtime &&
+ @name == other.name &&
+ @prefix == other.prefix &&
+ @size == other.size &&
+ @typeflag == other.typeflag &&
+ @uid == other.uid &&
+ @uname == other.uname &&
+ @version == other.version
end
def to_s # :nodoc:
@@ -226,10 +228,21 @@ class Gem::Package::TarHeader
@checksum = oct calculate_checksum(header), 6
end
+ ##
+ # Header's full name, including prefix
+
+ def full_name
+ if prefix != ""
+ File.join prefix, name
+ else
+ name
+ end
+ end
+
private
def calculate_checksum(header)
- header.unpack("C*").inject { |a, b| a + b }
+ header.sum(0)
end
def header(checksum = @checksum)
@@ -250,17 +263,15 @@ class Gem::Package::TarHeader
gname,
oct(devmajor, 7),
oct(devminor, 7),
- prefix
+ prefix,
]
header = header.pack PACK_FORMAT
- header << ("\0" * ((512 - header.size) % 512))
+ header.ljust 512, "\0"
end
def oct(num, len)
- "%0#{len}o" % num
+ format("%0#{len}o", num)
end
-
end
-
diff --git a/lib/rubygems/package/tar_input.rb b/lib/rubygems/package/tar_input.rb
deleted file mode 100644
index 5ac93ff336..0000000000
--- a/lib/rubygems/package/tar_input.rb
+++ /dev/null
@@ -1,231 +0,0 @@
-# -*- coding: iso-8859-1 -*-
-#++
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#--
-
-require 'zlib'
-Gem.load_yaml
-
-class Gem::Package::TarInput
-
- include Gem::Package::FSyncDir
- include Enumerable
-
- attr_reader :metadata
-
- private_class_method :new
-
- def self.open(io, security_policy = nil, &block)
- is = new io, security_policy
-
- yield is
- ensure
- is.close if is
- end
-
- def initialize(io, security_policy = nil)
- @io = io
- @tarreader = Gem::Package::TarReader.new @io
- has_meta = false
-
- data_sig, meta_sig, data_dgst, meta_dgst = nil, nil, nil, nil
- dgst_algo = security_policy ? Gem::Security::OPT[:dgst_algo] : nil
-
- @tarreader.each do |entry|
- case entry.full_name
- when "metadata"
- @metadata = load_gemspec entry.read
- has_meta = true
- when "metadata.gz"
- begin
- # if we have a security_policy, then pre-read the metadata file
- # and calculate it's digest
- sio = nil
- if security_policy
- Gem.ensure_ssl_available
- sio = StringIO.new(entry.read)
- meta_dgst = dgst_algo.digest(sio.string)
- sio.rewind
- end
-
- # Ruby 1.8 doesn't have encoding and YAML is UTF-8
- args = [sio || entry]
- args << { :external_encoding => Encoding::UTF_8 } if
- Object.const_defined?(:Encoding)
-
- gzis = Zlib::GzipReader.new(*args)
-
- # YAML wants an instance of IO
- @metadata = load_gemspec(gzis)
- has_meta = true
- ensure
- gzis.close unless gzis.nil?
- end
- when 'metadata.gz.sig'
- meta_sig = entry.read
- when 'data.tar.gz.sig'
- data_sig = entry.read
- when 'data.tar.gz'
- if security_policy
- Gem.ensure_ssl_available
- data_dgst = dgst_algo.digest(entry.read)
- end
- end
- end
-
- if security_policy then
- Gem.ensure_ssl_available
-
- # map trust policy from string to actual class (or a serialized YAML
- # file, if that exists)
- if String === security_policy then
- if Gem::Security::Policies.key? security_policy then
- # load one of the pre-defined security policies
- security_policy = Gem::Security::Policies[security_policy]
- elsif File.exist? security_policy then
- # FIXME: this doesn't work yet
- security_policy = YAML.load File.read(security_policy)
- else
- raise Gem::Exception, "Unknown trust policy '#{security_policy}'"
- end
- end
-
- if data_sig && data_dgst && meta_sig && meta_dgst then
- # the user has a trust policy, and we have a signed gem
- # file, so use the trust policy to verify the gem signature
-
- begin
- security_policy.verify_gem(data_sig, data_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify data signature: #{e}"
- end
-
- begin
- security_policy.verify_gem(meta_sig, meta_dgst, @metadata.cert_chain)
- rescue Exception => e
- raise "Couldn't verify metadata signature: #{e}"
- end
- elsif security_policy.only_signed
- raise Gem::Exception, "Unsigned gem"
- else
- # FIXME: should display warning here (trust policy, but
- # either unsigned or badly signed gem file)
- end
- end
-
- @tarreader.rewind
-
- unless has_meta then
- path = io.path if io.respond_to? :path
- error = Gem::Package::FormatError.new 'no metadata found', path
- raise error
- end
- end
-
- def close
- @io.close
- @tarreader.close
- end
-
- def each(&block)
- @tarreader.each do |entry|
- next unless entry.full_name == "data.tar.gz"
- is = zipped_stream entry
-
- begin
- Gem::Package::TarReader.new is do |inner|
- inner.each(&block)
- end
- ensure
- is.close if is
- end
- end
-
- @tarreader.rewind
- end
-
- def extract_entry(destdir, entry, expected_md5sum = nil)
- if entry.directory? then
- dest = File.join destdir, entry.full_name
-
- if File.directory? dest then
- FileUtils.chmod entry.header.mode, dest, :verbose => false
- else
- FileUtils.mkdir_p dest, :mode => entry.header.mode, :verbose => false
- end
-
- fsync_dir dest
- fsync_dir File.join(dest, "..")
-
- return
- end
-
- # it's a file
- md5 = Digest::MD5.new if expected_md5sum
- destdir = File.join destdir, File.dirname(entry.full_name)
- FileUtils.mkdir_p destdir, :mode => 0755, :verbose => false
- destfile = File.join destdir, File.basename(entry.full_name)
- FileUtils.chmod 0600, destfile, :verbose => false rescue nil # Errno::ENOENT
-
- open destfile, "wb", entry.header.mode do |os|
- loop do
- data = entry.read 4096
- break unless data
- # HACK shouldn't we check the MD5 before writing to disk?
- md5 << data if expected_md5sum
- os.write(data)
- end
-
- os.fsync
- end
-
- FileUtils.chmod entry.header.mode, destfile, :verbose => false
- fsync_dir File.dirname(destfile)
- fsync_dir File.join(File.dirname(destfile), "..")
-
- if expected_md5sum && expected_md5sum != md5.hexdigest then
- raise Gem::Package::BadCheckSum
- end
- end
-
- # Attempt to YAML-load a gemspec from the given _io_ parameter. Return
- # nil if it fails.
- def load_gemspec(io)
- Gem::Specification.from_yaml io
- rescue Gem::Exception
- nil
- end
-
- ##
- # Return an IO stream for the zipped entry.
- #
- # NOTE: Originally this method used two approaches, Return a GZipReader
- # directly, or read the GZipReader into a string and return a StringIO on
- # the string. The string IO approach was used for versions of ZLib before
- # 1.2.1 to avoid buffer errors on windows machines. Then we found that
- # errors happened with 1.2.1 as well, so we changed the condition. Then
- # we discovered errors occurred with versions as late as 1.2.3. At this
- # point (after some benchmarking to show we weren't seriously crippling
- # the unpacking speed) we threw our hands in the air and declared that
- # this method would use the String IO approach on all platforms at all
- # times. And that's the way it is.
-
- def zipped_stream(entry)
- if defined? Rubinius or defined? Maglev then
- # these implementations have working Zlib
- zis = Zlib::GzipReader.new entry
- dis = zis.read
- is = StringIO.new(dis)
- else
- # This is Jamis Buck's Zlib workaround for some unknown issue
- entry.read(10) # skip the gzip header
- zis = Zlib::Inflate.new(-Zlib::MAX_WBITS)
- is = StringIO.new(zis.inflate(entry.read))
- end
- ensure
- zis.finish if zis
- end
-
-end
-
diff --git a/lib/rubygems/package/tar_output.rb b/lib/rubygems/package/tar_output.rb
deleted file mode 100644
index fdc8f4fb7c..0000000000
--- a/lib/rubygems/package/tar_output.rb
+++ /dev/null
@@ -1,146 +0,0 @@
-# -*- coding: utf-8 -*-
-#--
-# Copyright (C) 2004 Mauricio Julio Fernández Pradier
-# See LICENSE.txt for additional licensing information.
-#++
-
-##
-# TarOutput is a wrapper to TarWriter that builds gem-format tar file.
-#
-# Gem-format tar files contain the following files:
-# [data.tar.gz] A gzipped tar file containing the files that compose the gem
-# which will be extracted into the gem/ dir on installation.
-# [metadata.gz] A YAML format Gem::Specification.
-# [data.tar.gz.sig] A signature for the gem's data.tar.gz.
-# [metadata.gz.sig] A signature for the gem's metadata.gz.
-#
-# See TarOutput::open for usage details.
-
-class Gem::Package::TarOutput
-
- ##
- # Creates a new TarOutput which will yield a TarWriter object for the
- # data.tar.gz portion of a gem-format tar file.
- #
- # See #initialize for details on +io+ and +signer+.
- #
- # See #add_gem_contents for details on adding metadata to the tar file.
-
- def self.open(io, signer = nil, &block) # :yield: data_tar_writer
- tar_outputter = new io, signer
- tar_outputter.add_gem_contents(&block)
- tar_outputter.add_metadata
- tar_outputter.add_signatures
-
- ensure
- tar_outputter.close
- end
-
- ##
- # Creates a new TarOutput that will write a gem-format tar file to +io+. If
- # +signer+ is given, the data.tar.gz and metadata.gz will be signed and
- # the signatures will be added to the tar file.
-
- def initialize(io, signer)
- @io = io
- @signer = signer
-
- @tar_writer = Gem::Package::TarWriter.new @io
-
- @metadata = nil
-
- @data_signature = nil
- @meta_signature = nil
- end
-
- ##
- # Yields a TarWriter for the data.tar.gz inside a gem-format tar file.
- # The yielded TarWriter has been extended with a #metadata= method for
- # attaching a YAML format Gem::Specification which will be written by
- # add_metadata.
-
- def add_gem_contents
- @tar_writer.add_file "data.tar.gz", 0644 do |inner|
- sio = @signer ? StringIO.new : nil
- Zlib::GzipWriter.wrap(sio || inner) do |os|
-
- Gem::Package::TarWriter.new os do |data_tar_writer|
- # :stopdoc:
- def data_tar_writer.metadata() @metadata end
- def data_tar_writer.metadata=(metadata) @metadata = metadata end
- # :startdoc:
-
- yield data_tar_writer
-
- @metadata = data_tar_writer.metadata
- end
- end
-
- # if we have a signing key, then sign the data
- # digest and return the signature
- if @signer then
- require 'rubygems/security'
- digest = Gem::Security::OPT[:dgst_algo].digest sio.string
- @data_signature = @signer.sign digest
- inner.write sio.string
- end
- end
-
- self
- end
-
- ##
- # Adds metadata.gz to the gem-format tar file which was saved from a
- # previous #add_gem_contents call.
-
- def add_metadata
- return if @metadata.nil?
-
- @tar_writer.add_file "metadata.gz", 0644 do |io|
- begin
- sio = @signer ? StringIO.new : nil
- gzos = Zlib::GzipWriter.new(sio || io)
- gzos.write @metadata
- ensure
- gzos.flush
- gzos.finish
-
- # if we have a signing key, then sign the metadata digest and return
- # the signature
- if @signer then
- require 'rubygems/security'
- digest = Gem::Security::OPT[:dgst_algo].digest sio.string
- @meta_signature = @signer.sign digest
- io.write sio.string
- end
- end
- end
- end
-
- ##
- # Adds data.tar.gz.sig and metadata.gz.sig to the gem-format tar files if
- # a Gem::Security::Signer was sent to initialize.
-
- def add_signatures
- if @data_signature then
- @tar_writer.add_file 'data.tar.gz.sig', 0644 do |io|
- io.write @data_signature
- end
- end
-
- if @meta_signature then
- @tar_writer.add_file 'metadata.gz.sig', 0644 do |io|
- io.write @meta_signature
- end
- end
- end
-
- ##
- # Closes the TarOutput.
-
- def close
- @tar_writer.close
- end
-
-end
-
diff --git a/lib/rubygems/package/tar_reader.rb b/lib/rubygems/package/tar_reader.rb
index e6a71d386c..b66a8a62bc 100644
--- a/lib/rubygems/package/tar_reader.rb
+++ b/lib/rubygems/package/tar_reader.rb
@@ -1,20 +1,17 @@
-# -*- coding: utf-8 -*-
-#--
+# 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 Gem::Package
-
- ##
- # Raised if the tar IO is not seekable
-
- class UnexpectedEOF < StandardError; end
+ include Enumerable
##
# Creates a new TarReader on +io+ and yields it to the block, if given.
@@ -33,6 +30,8 @@ class Gem::Package::TarReader
nil
end
+ attr_reader :io # :nodoc:
+
##
# Creates a new tar file reader on +io+ which needs to respond to #pos,
# #eof?, #read, #getc and #pos=
@@ -52,55 +51,53 @@ class Gem::Package::TarReader
# Iterates over files in the tarball yielding each entry
def each
- loop do
- return if @io.eof?
-
- header = Gem::Package::TarHeader.from @io
- return if header.empty?
-
- entry = Gem::Package::TarReader::Entry.new header, @io
- size = entry.header.size
-
- yield entry
-
- skip = (512 - (size % 512)) % 512
- pending = size - entry.bytes_read
+ return enum_for __method__ unless block_given?
+ until @io.eof? do
begin
- # avoid reading...
- @io.seek pending, IO::SEEK_CUR
- pending = 0
- rescue Errno::EINVAL, NameError
- while pending > 0 do
- bytes_read = @io.read([pending, 4096].min).size
- raise UnexpectedEOF if @io.eof?
- pending -= bytes_read
- end
+ 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
- @io.read skip # discard trailing zeros
-
- # make sure nobody can use #read, #getc or #rewind anymore
+ return if header.empty?
+ entry = Gem::Package::TarReader::Entry.new header, @io
+ yield entry
entry.close
end
end
- alias each_entry each
+ alias_method :each_entry, :each
##
# NOTE: Do not call #rewind during #each
def rewind
- if @init_pos == 0 then
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :rewind
+ if @init_pos == 0
@io.rewind
else
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
@io.pos = @init_pos
end
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
-require 'rubygems/package/tar_reader/entry'
+ 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
index 7034e59210..f837e86fd6 100644
--- a/lib/rubygems/package/tar_reader/entry.rb
+++ b/lib/rubygems/package/tar_reader/entry.rb
@@ -1,13 +1,29 @@
-# -*- coding: utf-8 -*-
-#++
+# 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
@@ -22,6 +38,7 @@ class Gem::Package::TarReader::Entry
@header = header
@io = io
@orig_pos = @io.pos
+ @end_pos = @orig_pos + @header.size
@read = 0
end
@@ -40,7 +57,14 @@ class Gem::Package::TarReader::Entry
# Closes the tar entry
def close
+ return if closed?
+ # Seek to the end of the entry if it wasn't fully read
+ seek(0, IO::SEEK_END)
+ # discard trailing zeros
+ skip = (512 - (@header.size % 512)) % 512
+ @io.read(skip)
@closed = true
+ nil
end
##
@@ -63,24 +87,18 @@ class Gem::Package::TarReader::Entry
# Full name of the tar entry
def full_name
- if @header.prefix != "" then
- File.join @header.prefix, @header.name
- else
- @header.name
- end
+ @header.full_name.force_encoding(Encoding::UTF_8)
rescue ArgumentError => e
- raise unless e.message == 'string contains null byte'
+ raise unless e.message == "string contains null byte"
raise Gem::Package::TarInvalidError,
- 'tar is corrupt, name contains null byte'
+ "tar is corrupt, name contains null byte"
end
##
# Read one byte from the tar entry
def getc
- check_closed
-
- return nil if @read >= @header.size
+ return nil if eof?
ret = @io.getc
@read += 1 if ret
@@ -103,6 +121,13 @@ class Gem::Package::TarReader::Entry
end
##
+ # Is this tar entry a symlink?
+
+ def symlink?
+ @header.typeflag == "2"
+ end
+
+ ##
# The position in the tar entry
def pos
@@ -112,34 +137,108 @@ class Gem::Package::TarReader::Entry
end
##
- # Reads +len+ bytes from the tar file entry, or the rest of the entry if
- # nil
+ # Seek to the position in the tar entry
- def read(len = nil)
- check_closed
+ def pos=(new_pos)
+ seek(new_pos, IO::SEEK_SET)
+ end
+
+ def size
+ @header.size
+ end
+
+ alias_method :length, :size
- return nil if @read >= @header.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
- len ||= @header.size - @read
- max_read = [len, @header.size - @read].min
+ max_read = [maxlen, @header.size - @read].compact.min
ret = @io.read max_read
+ if ret.nil?
+ return maxlen ? nil : "" # IO.read returns nil on EOF with len argument
+ end
@read += ret.size
ret
end
+ def readpartial(maxlen, 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
+
##
- # Rewinds to the beginning of the tar file entry
+ # Seeks to +offset+ bytes into the tar file entry
+ # +whence+ can be IO::SEEK_SET, IO::SEEK_CUR, or IO::SEEK_END
- def rewind
+ def seek(offset, whence = IO::SEEK_SET)
check_closed
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
+ 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
- @io.pos = @orig_pos
- @read = 0
+ 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
-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_test_case.rb b/lib/rubygems/package/tar_test_case.rb
deleted file mode 100644
index 4601f1328f..0000000000
--- a/lib/rubygems/package/tar_test_case.rb
+++ /dev/null
@@ -1,137 +0,0 @@
-require 'rubygems/test_case'
-require 'rubygems/package'
-
-##
-# A test case for Gem::Package::Tar* classes
-
-class Gem::Package::TarTestCase < Gem::TestCase
-
- def ASCIIZ(str, length)
- str + "\0" * (length - str.length)
- end
-
- def SP(s)
- s + " "
- end
-
- def SP_Z(s)
- s + " \0"
- end
-
- def Z(s)
- s + "\0"
- end
-
- def assert_headers_equal(expected, actual)
- expected = expected.to_s unless String === expected
- actual = actual.to_s unless String === actual
-
- fields = %w[
- name 100
- mode 8
- uid 8
- gid 8
- size 12
- mtime 12
- checksum 8
- typeflag 1
- linkname 100
- magic 6
- version 2
- uname 32
- gname 32
- devmajor 8
- devminor 8
- prefix 155
- ]
-
- offset = 0
-
- until fields.empty? do
- name = fields.shift
- length = fields.shift.to_i
-
- if name == "checksum" then
- chksum_off = offset
- offset += length
- next
- end
-
- assert_equal expected[offset, length], actual[offset, length],
- "Field #{name} of the tar header differs."
-
- offset += length
- end
-
- assert_equal expected[chksum_off, 8], actual[chksum_off, 8]
- end
-
- def calc_checksum(header)
- sum = header.unpack("C*").inject{|s,a| s + a}
- SP(Z(to_oct(sum, 6)))
- end
-
- def header(type, fname, dname, length, mode, checksum = nil)
- checksum ||= " " * 8
-
- arr = [ # struct tarfile_entry_posix
- ASCIIZ(fname, 100), # char name[100]; ASCII + (Z unless filled)
- Z(to_oct(mode, 7)), # char mode[8]; 0 padded, octal null
- Z(to_oct(0, 7)), # char uid[8]; ditto
- Z(to_oct(0, 7)), # char gid[8]; ditto
- Z(to_oct(length, 11)), # char size[12]; 0 padded, octal, null
- Z(to_oct(0, 11)), # char mtime[12]; 0 padded, octal, null
- checksum, # char checksum[8]; 0 padded, octal, null, space
- type, # char typeflag[1]; file: "0" dir: "5"
- "\0" * 100, # char linkname[100]; ASCII + (Z unless filled)
- "ustar\0", # char magic[6]; "ustar\0"
- "00", # char version[2]; "00"
- ASCIIZ("wheel", 32), # char uname[32]; ASCIIZ
- ASCIIZ("wheel", 32), # char gname[32]; ASCIIZ
- Z(to_oct(0, 7)), # char devmajor[8]; 0 padded, octal, null
- Z(to_oct(0, 7)), # char devminor[8]; 0 padded, octal, null
- ASCIIZ(dname, 155) # char prefix[155]; ASCII + (Z unless filled)
- ]
-
- format = "C100C8C8C8C12C12C8CC100C6C2C32C32C8C8C155"
- h = if RUBY_VERSION >= "1.9" then
- arr.join
- else
- arr = arr.join("").split(//).map{|x| x[0]}
- arr.pack format
- end
- ret = h + "\0" * (512 - h.size)
- assert_equal(512, ret.size)
- ret
- end
-
- def tar_dir_header(name, prefix, mode)
- h = header("5", name, prefix, 0, mode)
- checksum = calc_checksum(h)
- header("5", name, prefix, 0, mode, checksum)
- end
-
- def tar_file_header(fname, dname, mode, length)
- h = header("0", fname, dname, length, mode)
- checksum = calc_checksum(h)
- header("0", fname, dname, length, mode, checksum)
- end
-
- def to_oct(n, pad_size)
- "%0#{pad_size}o" % n
- end
-
- def util_entry(tar)
- io = TempIO.new tar
-
- header = Gem::Package::TarHeader.from io
-
- Gem::Package::TarReader::Entry.new header, io
- end
-
- def util_dir_entry
- util_entry tar_dir_header("foo", "bar", 0)
- end
-
-end
-
diff --git a/lib/rubygems/package/tar_writer.rb b/lib/rubygems/package/tar_writer.rb
index a73b5e5cab..39fed9e2af 100644
--- a/lib/rubygems/package/tar_writer.rb
+++ b/lib/rubygems/package/tar_writer.rb
@@ -1,21 +1,22 @@
-# -*- coding: utf-8 -*-
-#--
+# 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
@@ -40,21 +41,19 @@ class Gem::Package::TarWriter
# number of bytes will be more than #limit
def write(data)
- if data.size + @written > @limit
+ if data.bytesize + @written > @limit
raise FileOverflow, "You tried to feed more data than fits in the file."
end
@io.write data
- @written += data.size
- data.size
+ @written += data.bytesize
+ data.bytesize
end
-
end
##
# IO wrapper that provides only #write
class RestrictedStream
-
##
# Creates a new RestrictedStream wrapping +io+
@@ -68,7 +67,6 @@ class Gem::Package::TarWriter
def write(data)
@io.write data
end
-
end
##
@@ -97,18 +95,17 @@ class Gem::Package::TarWriter
end
##
- # Adds file +name+ with permissions +mode+, and yields an IO for writing the
- # file to
+ # Adds file +name+ with permissions +mode+ and mtime +mtime+ (sets
+ # Gem.source_date_epoch if not specified), and yields an IO for
+ # writing the file to
- def add_file(name, mode) # :yields: io
+ def add_file(name, mode, mtime = nil) # :yields: io
check_closed
- raise Gem::Package::NonSeekableIO unless @io.respond_to? :pos=
-
name, prefix = split_name name
init_pos = @io.pos
- @io.write "\0" * 512 # placeholder for the header
+ @io.write Gem::Package::TarHeader::EMPTY_HEADER # placeholder for the header
yield RestrictedStream.new(@io) if block_given?
@@ -120,8 +117,9 @@ class Gem::Package::TarWriter
final_pos = @io.pos
@io.pos = init_pos
- header = Gem::Package::TarHeader.new :name => name, :mode => mode,
- :size => size, :prefix => prefix
+ 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
@@ -130,6 +128,80 @@ class Gem::Package::TarWriter
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.
@@ -138,8 +210,9 @@ class Gem::Package::TarWriter
name, prefix = split_name name
- header = Gem::Package::TarHeader.new(:name => name, :mode => mode,
- :size => size, :prefix => prefix).to_s
+ header = Gem::Package::TarHeader.new(name: name, mode: mode,
+ size: size, prefix: prefix,
+ mtime: Gem.source_date_epoch).to_s
@io.write header
os = BoundedStream.new @io, size
@@ -156,6 +229,25 @@ class Gem::Package::TarWriter
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
@@ -198,9 +290,10 @@ class Gem::Package::TarWriter
name, prefix = split_name(name)
- header = Gem::Package::TarHeader.new :name => name, :mode => mode,
- :typeflag => "5", :size => 0,
- :prefix => prefix
+ header = Gem::Package::TarHeader.new name: name, mode: mode,
+ typeflag: "5", size: 0,
+ prefix: prefix,
+ mtime: Gem.source_date_epoch
@io.write header
@@ -211,31 +304,29 @@ class Gem::Package::TarWriter
# Splits +name+ into a name and prefix that can fit in the TarHeader
def split_name(name) # :nodoc:
- raise Gem::Package::TooLongFileName if name.size > 256
-
- if name.size <= 100 then
- prefix = ""
- else
- parts = name.split(/\//)
- newname = parts.pop
- nxt = ""
-
- loop do
- nxt = parts.pop
- break if newname.size + 1 + nxt.size > 100
- newname = nxt + "/" + newname
+ 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
- prefix = (parts + [nxt]).join "/"
- name = newname
+ 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 name.size > 100 or prefix.size > 155 then
- raise Gem::Package::TooLongFileName
+ 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
- return name, prefix
+ [name, prefix]
end
-
end
-
diff --git a/lib/rubygems/package_task.rb b/lib/rubygems/package_task.rb
index fe32a03b27..d26411684d 100644
--- a/lib/rubygems/package_task.rb
+++ b/lib/rubygems/package_task.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
# Copyright (c) 2003, 2004 Jim Weirich, 2009 Eric Hodel
#
# Permission is hereby granted, free of charge, to any person obtaining
@@ -19,13 +21,9 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-require 'rubygems'
-begin
- gem 'rake'
-rescue Gem::LoadError
-end
-
-require 'rake/packagetask'
+require_relative "../rubygems"
+require_relative "package"
+require "rake/packagetask"
##
# Create a package based upon a Gem::Specification. Gem packages, as well as
@@ -43,13 +41,10 @@ require 'rake/packagetask'
# require 'rubygems/package_task'
#
# spec = Gem::Specification.new do |s|
-# s.platform = Gem::Platform::RUBY
# s.summary = "Ruby based make-like utility."
# s.name = 'rake'
# s.version = PKG_VERSION
# s.requirements << 'none'
-# s.require_path = 'lib'
-# s.autorequire = 'rake'
# s.files = PKG_FILES
# s.description = <<-EOF
# Rake is a Make-like program implemented in Ruby. Tasks
@@ -63,7 +58,6 @@ require 'rake/packagetask'
# 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
@@ -89,6 +83,7 @@ class Gem::PackageTask < Rake::PackageTask
super gem.full_name, :noversion
@gem_spec = gem
@package_files += gem_spec.files if gem_spec.files
+ @fileutils_output = $stdout
end
##
@@ -98,14 +93,17 @@ class Gem::PackageTask < Rake::PackageTask
def define
super
- task :package => [:gem]
-
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]
+ task gem: [gem_path]
trace = Rake.application.options.trace
Gem.configuration.verbose = trace
@@ -113,14 +111,13 @@ class Gem::PackageTask < Rake::PackageTask
file gem_path => [package_dir, gem_dir] + @gem_spec.files do
chdir(gem_dir) do
when_writing "Creating #{gem_spec.file_name}" do
- Gem::Builder.new(gem_spec).build
+ Gem::Package.build gem_spec
+
verbose trace do
- mv gem_file, '..'
+ mv gem_file, ".."
end
end
end
end
end
-
end
-
diff --git a/lib/rubygems/path_support.rb b/lib/rubygems/path_support.rb
index 0aaf2c1bed..13091e29ba 100644
--- a/lib/rubygems/path_support.rb
+++ b/lib/rubygems/path_support.rb
@@ -1,4 +1,7 @@
+# frozen_string_literal: true
+
##
+#
# Gem::PathSupport facilitates the GEM_HOME and GEM_PATH environment settings
# to the rest of RubyGems.
#
@@ -12,59 +15,71 @@ class Gem::PathSupport
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=ENV)
- @env = env
-
- # note 'env' vs 'ENV'...
- @home = env["GEM_HOME"] || ENV["GEM_HOME"] || Gem.default_dir
+ def initialize(env)
+ @home = normalize_home_dir(env["GEM_HOME"] || Gem.default_dir)
+ @path = split_gem_path env["GEM_PATH"], @home
- if File::ALT_SEPARATOR then
- @home = @home.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
- end
-
- self.path = env["GEM_PATH"] || ENV["GEM_PATH"]
+ @spec_cache_dir = env["GEM_SPEC_CACHE"] || Gem.default_spec_cache_dir
end
private
- ##
- # Set the Gem home directory (as reported by Gem.dir).
+ def normalize_home_dir(home)
+ if File::ALT_SEPARATOR
+ home = home.gsub(File::ALT_SEPARATOR, File::SEPARATOR)
+ end
- def home=(home)
- @home = home.to_s
+ expand(home)
end
##
- # Set the Gem search path (as reported by Gem.path).
+ # Split the Gem search path (as reported by Gem.path).
- def path=(gpaths)
- gem_path = [@home]
+ def split_gem_path(gpaths, home)
+ # FIX: it should be [home, *path], not [*path, home]
- # FIX: I can't tell wtf this is doing.
- gpaths ||= (ENV['GEM_PATH'] || "").empty? ? nil : ENV["GEM_PATH"]
+ gem_path = []
- if gpaths then
- if gpaths.kind_of?(Array) then
- gem_path.push(*gpaths)
- else
- gem_path.push(*gpaths.split(File::PATH_SEPARATOR))
+ 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 then
+ if File::ALT_SEPARATOR
gem_path.map! do |this_path|
this_path.gsub File::ALT_SEPARATOR, File::SEPARATOR
end
end
- else
- gem_path.push(*Gem.default_path)
- gem_path << APPLE_GEM_HOME if defined?(APPLE_GEM_HOME)
+ gem_path << home
+ else
+ gem_path = default_path
end
- @path = gem_path.uniq
+ 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
index 4a4e3c1b35..367b00e7e1 100644
--- a/lib/rubygems/platform.rb
+++ b/lib/rubygems/platform.rb
@@ -1,28 +1,63 @@
-require "rubygems/deprecate"
+# 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
+ attr_accessor :cpu, :os, :version
- attr_accessor :os
+ 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
- attr_accessor :version
+ 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.local
- arch = Gem::ConfigMap[:arch]
- arch = "#{arch}_60" if arch =~ /mswin32$/
- @local ||= new(arch)
+ def self.match_spec?(spec)
+ match_gem?(spec.platform, spec.name)
end
- def self.match(platform)
- Gem.platforms.any? do |local_platform|
- platform.nil? or local_platform == platform or
- (local_platform != Gem::Platform::RUBY and local_platform =~ platform)
+ 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
@@ -30,7 +65,7 @@ class Gem::Platform
case arch
when Gem::Platform::CURRENT then
Gem::Platform.local
- when Gem::Platform::RUBY, nil, '' then
+ when Gem::Platform::RUBY, nil, "" then
Gem::Platform::RUBY
else
super
@@ -42,51 +77,47 @@ class Gem::Platform
when Array then
@cpu, @os, @version = arch
when String then
- arch = arch.split '-'
-
- if arch.length > 2 and arch.last !~ /\d/ then # reassemble x86-linux-gnu
- extra = arch.pop
- arch.last << "-#{extra}"
- end
-
- cpu = arch.shift
+ cpu, os = arch.sub(/-+$/, "").split("-", 2)
- @cpu = case cpu
- when /i\d86/ then 'x86'
- else cpu
- end
-
- if arch.length == 2 and arch.last =~ /^\d+(\.\d+)?$/ then # for command-line
- @os, @version = arch
- return
+ @cpu = if cpu&.match?(/i\d86/)
+ "x86"
+ else
+ cpu
end
- os, = arch
- @cpu, os = nil, cpu if os.nil? # legacy jruby
+ if os.nil?
+ @cpu = nil
+ os = cpu
+ end # legacy jruby
@os, @version = case os
- when /aix(\d+)/ then [ 'aix', $1 ]
- when /cygwin/ then [ 'cygwin', nil ]
- when /darwin(\d+)?/ then [ 'darwin', $1 ]
- when /freebsd(\d+)/ then [ 'freebsd', $1 ]
- when /hpux(\d+)/ then [ 'hpux', $1 ]
- when /^java$/, /^jruby$/ then [ 'java', nil ]
- when /^java([\d.]*)/ then [ 'java', $1 ]
- when /^dotnet$/ then [ 'dotnet', nil ]
- when /^dotnet([\d.]*)/ then [ 'dotnet', $1 ]
- when /linux/ then [ 'linux', $1 ]
- when /mingw32/ then [ 'mingw32', nil ]
- when /(mswin\d+)(\_(\d+))?/ then
- os, version = $1, $3
- @cpu = 'x86' if @cpu.nil? and os =~ /32$/
+ 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 /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 /^(\w+_platform)-?(\d+)?/ then [$1, $2]
+ else ["unknown", nil]
+ end
when Gem::Platform then
@cpu = arch.cpu
@os = arch.os
@@ -96,20 +127,43 @@ class Gem::Platform
end
end
- def inspect
- "#<%s:0x%x @cpu=%p, @os=%p, @version=%p>" % [self.class, object_id, *to_a]
- end
-
def to_a
[@cpu, @os, @version]
end
def to_s
- to_a.compact.join '-'
+ to_a.compact.join(@cpu.nil? ? "" : "-")
end
- def empty?
- to_s.empty?
+ ##
+ # 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
##
@@ -117,10 +171,10 @@ class Gem::Platform
# the same CPU, OS and version.
def ==(other)
- self.class === other and to_a == other.to_a
+ self.class === other && to_a == other.to_a
end
- alias :eql? :==
+ alias_method :eql?, :==
def hash # :nodoc:
to_a.hash
@@ -129,19 +183,53 @@ class Gem::Platform
##
# Does +other+ match this platform? Two platforms match if they have the
# same CPU, or either has a CPU of 'universal', they have the same OS, and
- # they have the same version, or either has no version.
+ # they have the same version, or either one has no version
+ #
+ # Additionally, the platform will match if the local CPU is 'arm' and the
+ # other CPU starts with "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
- (@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and
+ ([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`
- # os
- @os == other.os and
+ def normalized_linux_version
+ return nil unless @version
- # version
- (@version.nil? or other.version.nil? or @version == other.version)
+ without_gnu_nor_abi_modifiers = @version.sub(/\Agnu/, "").sub(/eabi(hf)?\Z/, "")
+ return nil if without_gnu_nor_abi_modifiers.empty?
+
+ without_gnu_nor_abi_modifiers
end
##
@@ -154,17 +242,19 @@ class Gem::Platform
when String then
# This data is from http://gems.rubyforge.org/gems/yaml on 19 Aug 2007
other = case other
- when /^i686-darwin(\d)/ then ['x86', 'darwin', $1 ]
- when /^i\d86-linux/ then ['x86', 'linux', nil ]
- when 'java', 'jruby' then [nil, 'java', nil ]
- when /dotnet(\-(\d+\.\d+))?/ then ['universal','dotnet', $2 ]
- when /mswin32(\_(\d+))?/ then ['x86', 'mswin32', $2 ]
- when 'powerpc-darwin' then ['powerpc', 'darwin', nil ]
- when /powerpc-darwin(\d)/ then ['powerpc', 'darwin', $1 ]
- when /sparc-solaris2.8/ then ['sparc', 'solaris', '2.8' ]
- when /universal-darwin(\d)/ then ['universal', 'darwin', $1 ]
- else other
- end
+ when /^i686-darwin(\d)/ then ["x86", "darwin", $1]
+ when /^i\d86-linux/ then ["x86", "linux", nil]
+ when "java", "jruby" then [nil, "java", nil]
+ when /^dalvik(\d+)?$/ then [nil, "dalvik", $1]
+ when /dotnet(\-(\d+\.\d+))?/ then ["universal","dotnet", $2]
+ when /mswin32(\_(\d+))?/ then ["x86", "mswin32", $2]
+ when /mswin64(\_(\d+))?/ then ["x64", "mswin64", $2]
+ when "powerpc-darwin" then ["powerpc", "darwin", nil]
+ when /powerpc-darwin(\d)/ then ["powerpc", "darwin", $1]
+ when /sparc-solaris2.8/ then ["sparc", "solaris", "2.8"]
+ when /universal-darwin(\d)/ then ["universal", "darwin", $1]
+ else other
+ end
other = Gem::Platform.new other
else
@@ -175,19 +265,128 @@ class Gem::Platform
end
##
- # A pure-ruby gem that may use Gem::Specification#extensions to build
+ # A pure-Ruby gem that may use Gem::Specification#extensions to build
# binary files.
- RUBY = 'ruby'
+ RUBY = "ruby"
##
- # A platform-specific gem that is built for the packaging ruby's platform.
+ # A platform-specific gem that is built for the packaging Ruby's platform.
# This will be replaced with Gem::Platform::local.
- CURRENT = 'current'
+ 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
- extend Gem::Deprecate
+ ##
+ # Returns the platform specificity match for the given spec platform and user platform.
- deprecate :empty?, :none, 2011, 11
-end
+ 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
index e0d12987f3..5b83dc6f6f 100644
--- a/lib/rubygems/remote_fetcher.rb
+++ b/lib/rubygems/remote_fetcher.rb
@@ -1,13 +1,18 @@
-require 'rubygems'
-require 'rubygems/user_interaction'
-require 'uri'
+# 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
##
@@ -15,22 +20,32 @@ class Gem::RemoteFetcher
# 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
+ attr_accessor :uri, :original_uri
def initialize(message, uri)
- super message
- @uri = 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
@@ -38,9 +53,11 @@ class Gem::RemoteFetcher
# Cached RemoteFetcher instance.
def self.fetcher
- @fetcher ||= self.new Gem.configuration[:http_proxy]
+ @fetcher ||= new Gem.configuration[:http_proxy]
end
+ attr_accessor :headers
+
##
# Initialize a remote fetcher using the source URI and possible proxy
# information.
@@ -51,25 +68,24 @@ class Gem::RemoteFetcher
# * 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)
- require 'net/http'
- require 'stringio'
- require 'time'
- require 'uri'
+ 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
- @connections = {}
- @requests = Hash.new 0
- @proxy_uri =
- case proxy
- when :no_proxy then nil
- when nil then get_proxy_from_env
- when URI::HTTP then proxy
- else URI.parse(proxy)
- end
- @user_agent = user_agent
+ @proxy = proxy
+ @pools = {}
+ @pool_lock = Thread::Mutex.new
+ @pool_size = 1
+ @cert_files = Gem::Request.get_cert_files
+
+ @headers = headers
end
##
@@ -77,17 +93,16 @@ class Gem::RemoteFetcher
# filename. Returns nil if the gem cannot be located.
#--
# Should probably be integrated with #download below, but that will be a
- # larger, more emcompassing effort. -erikh
+ # larger, more encompassing effort. -erikh
- def download_to_cache dependency
- found = Gem::SpecFetcher.fetcher.fetch dependency, true, true,
- dependency.prerelease?
+ def download_to_cache(dependency)
+ found, _ = Gem::SpecFetcher.fetcher.spec_for_dependency dependency
return if found.empty?
- spec, source_uri = found.sort_by { |(s,_)| s.version }.last
+ spec, source = found.max_by {|(s,_)| s.version }
- download spec, source_uri
+ download spec, source.uri
end
##
@@ -96,93 +111,92 @@ class Gem::RemoteFetcher
# always replaced.
def download(spec, source_uri, install_dir = Gem.dir)
- Gem.ensure_gem_subdirectories(install_dir) rescue nil
+ gem_file_name = File.basename spec.cache_file
- if File.writable?(install_dir)
- cache_dir = File.join install_dir, "cache"
- else
- cache_dir = File.join Gem.user_dir, "cache"
- end
+ 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
- gem_file_name = File.basename spec.cache_file
local_gem_path = File.join cache_dir, gem_file_name
- FileUtils.mkdir_p cache_dir rescue nil unless File.exist? cache_dir
+ require "fileutils"
+ begin
+ FileUtils.mkdir_p cache_dir
+ rescue StandardError
+ nil
+ end unless File.exist? cache_dir
- # Always escape URI's to deal with potential spaces and such
- unless URI::Generic === source_uri
- source_uri = URI.parse(URI.const_defined?(:DEFAULT_PARSER) ?
- URI::DEFAULT_PARSER.escape(source_uri.to_s) :
- URI.escape(source_uri.to_s))
- end
+ source_uri = Gem::Uri.new(source_uri)
scheme = source_uri.scheme
- # URI.parse gets confused by MS Windows paths with forward slashes.
- scheme = nil if scheme =~ /^[a-z]$/i
+ # Gem::URI.parse gets confused by MS Windows paths with forward slashes.
+ scheme = nil if /^[a-z]$/i.match?(scheme)
+ # REFACTOR: split this up and dispatch on scheme (eg download_http)
+ # REFACTOR: be sure to clean up fake fetcher when you do this... cleaner
case scheme
- when 'http', 'https' then
- unless File.exist? local_gem_path then
+ when "http", "https", "s3" then
+ unless File.exist? local_gem_path
begin
- say "Downloading gem #{gem_file_name}" if
- Gem.configuration.really_verbose
+ verbose "Downloading gem #{gem_file_name}"
remote_gem_path = source_uri + "gems/#{gem_file_name}"
- gem = self.fetch_path remote_gem_path
- rescue Gem::RemoteFetcher::FetchError
+ cache_update_path remote_gem_path, local_gem_path
+ rescue FetchError
raise if spec.original_platform == spec.platform
alternate_name = "#{spec.original_name}.gem"
- say "Failed, downloading gem #{alternate_name}" if
- Gem.configuration.really_verbose
+ verbose "Failed, downloading gem #{alternate_name}"
remote_gem_path = source_uri + "gems/#{alternate_name}"
- gem = self.fetch_path remote_gem_path
- end
-
- File.open local_gem_path, 'wb' do |fp|
- fp.write gem
+ cache_update_path remote_gem_path, local_gem_path
end
end
- when 'file' then
+ when "file" then
begin
path = source_uri.path
- path = File.dirname(path) if File.extname(path) == '.gem'
+ path = File.dirname(path) if File.extname(path) == ".gem"
- remote_gem_path = correct_for_windows_path(File.join(path, 'gems', gem_file_name))
+ remote_gem_path = Gem::Util.correct_for_windows_path(File.join(path, "gems", gem_file_name))
FileUtils.cp(remote_gem_path, local_gem_path)
rescue Errno::EACCES
local_gem_path = source_uri.to_s
end
- say "Using local gem #{local_gem_path}" if
- Gem.configuration.really_verbose
- when nil then # TODO test for local overriding cache
+ verbose "Using local gem #{local_gem_path}"
+ when nil then
source_path = if Gem.win_platform? && source_uri.scheme &&
- !source_uri.path.include?(':') then
- "#{source_uri.scheme}:#{source_uri.path}"
- else
- source_uri.path
- end
+ !source_uri.path.include?(":")
+ "#{source_uri.scheme}:#{source_uri.path}"
+ else
+ source_uri.path
+ end
- source_path = unescape source_path
+ source_path = Gem::UriFormatter.new(source_path).unescape
begin
FileUtils.cp source_path, local_gem_path unless
- File.expand_path(source_path) == File.expand_path(local_gem_path)
+ File.identical?(source_path, local_gem_path)
rescue Errno::EACCES
local_gem_path = source_uri.to_s
end
- say "Using local gem #{local_gem_path}" if
- Gem.configuration.really_verbose
+ verbose "Using local gem #{local_gem_path}"
else
- raise Gem::InstallError, "unsupported URI scheme #{source_uri.scheme}"
+ raise ArgumentError, "unsupported URI scheme #{source_uri.scheme}"
end
local_gem_path
@@ -191,279 +205,146 @@ class Gem::RemoteFetcher
##
# File Fetcher. Dispatched by +fetch_path+. Use it instead.
- def fetch_file uri, *_
- Gem.read_binary correct_for_windows_path uri.path
+ 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 ? Net::HTTP::Head : Net::HTTP::Get
- response = request uri, fetch_type, last_modified
+ 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 Net::HTTPOK, Net::HTTPNotModified then
+ when Gem::Net::HTTPOK, Gem::Net::HTTPNotModified then
+ response.uri = uri
head ? response : response.body
- when Net::HTTPMovedPermanently, Net::HTTPFound, Net::HTTPSeeOther,
- Net::HTTPTemporaryRedirect then
- raise FetchError.new('too many redirects', uri) if depth > 10
+ when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther,
+ Gem::Net::HTTPTemporaryRedirect then
+ raise FetchError.new("too many redirects", uri) if depth > 10
+
+ unless location = response["Location"]
+ 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
- location = URI.parse response['Location']
fetch_http(location, last_modified, head, depth + 1)
else
- raise FetchError.new("bad response #{response.message} #{response.code}", uri)
+ custom_error = response["X-Error-Message"]
+ error_detail = custom_error || response.message
+ raise FetchError.new("Bad response #{error_detail} #{response.code}", uri)
end
end
- alias :fetch_https :fetch_http
+ alias_method :fetch_https, :fetch_http
##
# Downloads +uri+ and returns it as a String.
def fetch_path(uri, mtime = nil, head = false)
- uri = URI.parse uri unless URI::Generic === uri
-
- raise ArgumentError, "bad uri: #{uri}" unless uri
- raise ArgumentError, "uri scheme is invalid: #{uri.scheme.inspect}" unless
- uri.scheme
+ uri = Gem::Uri.new uri
- data = send "fetch_#{uri.scheme}", uri, mtime, head
- data = Gem.gunzip data if data and not head and uri.to_s =~ /gz$/
- data
- rescue FetchError
- raise
- rescue Timeout::Error
- raise FetchError.new('timed out', uri.to_s)
- rescue IOError, SocketError, SystemCallError => e
- raise FetchError.new("#{e.class}: #{e}", uri.to_s)
- end
-
- ##
- # Returns the size of +uri+ in bytes.
-
- def fetch_size(uri) # TODO: phase this out
- response = fetch_path(uri, nil, true)
+ 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}" }
- response['content-length'].to_i
- end
+ data = send method, uri, mtime, head
- def escape(str)
- return unless str
- @uri_parser ||= uri_escaper
- @uri_parser.escape str
- end
-
- def unescape(str)
- return unless str
- @uri_parser ||= uri_escaper
- @uri_parser.unescape str
- end
+ 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
- def uri_escaper
- URI::Parser.new
- rescue NameError
- URI
+ data
+ rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
+ *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
+ raise FetchError.new("#{e.class}: #{e}", uri)
end
- ##
- # Returns an HTTP proxy URI if one is set in the environment variables.
-
- def get_proxy_from_env
- env_proxy = ENV['http_proxy'] || ENV['HTTP_PROXY']
-
- return nil if env_proxy.nil? or env_proxy.empty?
-
- uri = URI.parse(normalize_uri(env_proxy))
-
- if uri and uri.user.nil? and uri.password.nil? then
- # Probably we have http_proxy_* variables?
- uri.user = escape(ENV['http_proxy_user'] || ENV['HTTP_PROXY_USER'])
- uri.password = escape(ENV['http_proxy_pass'] || ENV['HTTP_PROXY_PASS'])
+ 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
-
- uri
+ fetch_https public_uri, mtime, head
end
- ##
- # Normalize the URI by adding "http://" if it is missing.
-
- def normalize_uri(uri)
- (uri =~ /^(https?|ftp|file):/) ? uri : "http://#{uri}"
+ # 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
##
- # Creates or an HTTP connection based on +uri+, or retrieves an existing
- # connection, using a proxy if needed.
-
- def connection_for(uri)
- net_http_args = [uri.host, uri.port]
-
- if @proxy_uri then
- net_http_args += [
- @proxy_uri.host,
- @proxy_uri.port,
- @proxy_uri.user,
- @proxy_uri.password
- ]
- 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
- connection_id = [Thread.current.object_id, *net_http_args].join ':'
- @connections[connection_id] ||= Net::HTTP.new(*net_http_args)
- connection = @connections[connection_id]
+ data = fetch_path(uri, mtime)
- if uri.scheme == 'https' and not connection.started? then
- require 'net/https'
- connection.use_ssl = true
- connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
+ if data.nil? # indicates the server returned 304 Not Modified
+ return Gem.read_binary(path)
end
- connection.start unless connection.started?
-
- connection
- rescue Errno::EHOSTDOWN => e
- raise FetchError.new(e.message, uri)
- end
-
- def correct_for_windows_path(path)
- if path[0].chr == '/' && path[1].chr =~ /[a-z]/i && path[2].chr == ':'
- path = path[1..-1]
- else
- path
+ if update && path
+ Gem.write_binary(path, data)
end
- end
- ##
- # Read the data from the (source based) URI, but if it is a file:// URI,
- # read from the filesystem instead.
-
- def open_uri_or_path(uri, last_modified = nil, head = false, depth = 0)
- raise "NO: Use fetch_path instead"
- # TODO: deprecate for fetch_path
+ data
end
##
- # Performs a Net::HTTP request of type +request_class+ on +uri+ returning
- # a Net::HTTP response object. request maintains a table of persistent
+ # Performs a Gem::Net::HTTP request of type +request_class+ on +uri+ returning
+ # a Gem::Net::HTTP response object. request maintains a table of persistent
# connections to reduce connect overhead.
def request(uri, request_class, last_modified = nil)
- request = request_class.new uri.request_uri
-
- unless uri.nil? || uri.user.nil? || uri.user.empty? then
- request.basic_auth uri.user, uri.password
- end
-
- request.add_field 'User-Agent', @user_agent
- request.add_field 'Connection', 'keep-alive'
- request.add_field 'Keep-Alive', '30'
-
- if last_modified then
- last_modified = last_modified.utc
- request.add_field 'If-Modified-Since', last_modified.rfc2822
- end
-
- yield request if block_given?
-
- connection = connection_for uri
-
- retried = false
- bad_response = false
+ proxy = proxy_for @proxy, uri
+ pool = pools_for(proxy).pool_for uri
- begin
- @requests[connection.object_id] += 1
-
- say "#{request.method} #{uri}" if
- Gem.configuration.really_verbose
-
- file_name = File.basename(uri.path)
- # perform download progress reporter only for gems
- if request.response_body_permitted? && file_name =~ /\.gem$/
- reporter = ui.download_reporter
- response = connection.request(request) do |incomplete_response|
- if Net::HTTPOK === incomplete_response
- reporter.fetch(file_name, incomplete_response.content_length)
- downloaded = 0
- data = ''
-
- 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
-
- say "#{response.code} #{response.message}" if
- Gem.configuration.really_verbose
-
- rescue Net::HTTPBadResponse
- say "bad response" if Gem.configuration.really_verbose
-
- reset connection
-
- raise FetchError.new('too many bad responses', uri) if bad_response
-
- bad_response = true
- retry
- # HACK work around EOFError bug in Net::HTTP
- # NOTE Errno::ECONNABORTED raised a lot on Windows, and make impossible
- # to install gems.
- rescue EOFError, Timeout::Error,
- Errno::ECONNABORTED, Errno::ECONNRESET, Errno::EPIPE
-
- requests = @requests[connection.object_id]
- say "connection reset after #{requests} requests, retrying" if
- Gem.configuration.really_verbose
+ request = Gem::Request.new uri, request_class, last_modified, pool
- raise FetchError.new('too many connection resets', uri) if retried
-
- reset connection
-
- retried = true
- retry
+ request.fetch do |req|
+ yield req if block_given?
end
-
- response
end
- ##
- # Resets HTTP connection +connection+.
-
- def reset(connection)
- @requests.delete connection.object_id
+ def https?(uri)
+ uri.scheme.casecmp("https").zero?
+ end
- connection.finish
- connection.start
+ def close_all
+ @pools.each_value(&:close_all)
end
- def user_agent
- ua = "RubyGems/#{Gem::VERSION} #{Gem::Platform.local}"
+ private
- ruby_version = RUBY_VERSION
- ruby_version += 'dev' if RUBY_PATCHLEVEL == -1
+ def proxy_for(proxy, uri)
+ Gem::Request.proxy_uri(proxy || Gem::Request.get_proxy_from_env(uri.scheme))
+ end
- ua << " Ruby/#{ruby_version} (#{RUBY_RELEASE_DATE}"
- if RUBY_PATCHLEVEL >= 0 then
- ua << " patchlevel #{RUBY_PATCHLEVEL}"
- elsif defined?(RUBY_REVISION) then
- ua << " revision #{RUBY_REVISION}"
+ def pools_for(proxy)
+ @pool_lock.synchronize do
+ @pools[proxy] ||= Gem::Request::ConnectionPools.new proxy, @cert_files, @pool_size
end
- ua << ")"
-
- ua << " #{RUBY_ENGINE}" if defined?(RUBY_ENGINE) and RUBY_ENGINE != 'ruby'
-
- ua
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/require_paths_builder.rb b/lib/rubygems/require_paths_builder.rb
deleted file mode 100644
index 23e974639f..0000000000
--- a/lib/rubygems/require_paths_builder.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'rubygems'
-
-# TODO: remove after 1.9.1 dropped
-module Gem::RequirePathsBuilder
- def write_require_paths_file_if_needed(spec = @spec, gem_home = @gem_home)
- return if spec.require_paths == ["lib"] &&
- (spec.bindir.nil? || spec.bindir == "bin")
- file_name = File.join(gem_home, 'gems', "#{@spec.full_name}", ".require_paths")
- file_name.untaint
- File.open(file_name, "w") do |file|
- spec.require_paths.each do |path|
- file.puts path
- end
- file.puts spec.bindir if spec.bindir
- end
- end
-end if Gem::QUICKLOADER_SUCKAGE
-
diff --git a/lib/rubygems/requirement.rb b/lib/rubygems/requirement.rb
index ed5cacc237..0d3f98eb0f 100644
--- a/lib/rubygems/requirement.rb
+++ b/lib/rubygems/requirement.rb
@@ -1,50 +1,49 @@
-require "rubygems/version"
+# frozen_string_literal: true
-# :stopdoc:
-
-# Hack to handle syck's DefaultKey bug with psych
-#
-# Quick note! If/when psych loads in 1.9, it will redefine
-# YAML to point to Psych by removing the YAML constant.
-# Thusly, over in Gem.load_yaml, we define DefaultKey again
-# after proper yaml library has been loaded.
-#
-# All this is so that there is always a YAML::Syck::DefaultKey
-# class no matter if the full yaml library has loaded or not.
-#
-module YAML
- if !defined? Syck
- module Syck
- class DefaultKey
- def to_s
- '='
- end
- end
- end
- end
-end
-
-# :startdoc:
+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
- include Comparable
+ 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
- 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 }
- }
+ ##
+ # Raised when a bad requirement is encountered
- quoted = OPS.keys.map { |k| Regexp.quote k }.join "|"
- PATTERN = /\A\s*(#{quoted})?\s*(#{Gem::Version::VERSION_PATTERN})\s*\z/
+ class BadRequirementError < ArgumentError; end
##
# Factory method to create a Gem::Requirement object. Input may be
@@ -53,14 +52,20 @@ class Gem::Requirement
# If the input is "weird", the default version requirement is
# returned.
- def self.create input
+ 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 then
+ if input.respond_to? :to_str
new [input.to_str]
else
default
@@ -68,15 +73,19 @@ class Gem::Requirement
end
end
- ##
- # A default "version requirement" can surely _only_ be '>= 0'.
- #--
- # This comment once said:
- #
- # "A default "version requirement" can surely _only_ be '> 0'."
-
def self.default
- new '>= 0'
+ 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
##
@@ -87,25 +96,33 @@ class Gem::Requirement
# specification, like <tt>">= 1.2"</tt>, or a simple version number,
# like <tt>"1.2"</tt>.
#
- # parse("> 1.0") # => [">", "1.0"]
- # parse("1.0") # => ["=", "1.0"]
- # parse(Gem::Version.new("1.0")) # => ["=, "1.0"]
+ # 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
+ def self.parse(obj)
return ["=", obj] if Gem::Version === obj
unless PATTERN =~ obj.to_s
- raise ArgumentError, "Illformed requirement [#{obj.inspect}]"
+ raise BadRequirementError, "Illformed requirement [#{obj.inspect}]"
end
+ op = -($1 || "=")
+ version = -$2
- [$1 || "=", Gem::Version.new($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:
+ attr_reader :requirements # :nodoc:
##
# Constructs a requirement from +requirements+. Requirements can be
@@ -113,46 +130,107 @@ class Gem::Requirement
# requirements are ignored. An empty set of +requirements+ is the
# same as <tt>">= 0"</tt>.
- def initialize *requirements
+ def initialize(*requirements)
requirements = requirements.flatten
requirements.compact!
requirements.uniq!
- requirements << ">= 0" if requirements.empty?
- @none = (requirements == ">= 0")
- @requirements = requirements.map! { |r| self.class.parse r }
+ 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?
- @none ||= (to_s == ">= 0")
+ 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}" }.sort
+ requirements.map {|op, version| "#{op} #{version}" }
end
def hash # :nodoc:
- requirements.hash
+ requirements.map {|r| r.first == "~>" ? [r[0], r[1].to_s] : r }.sort.hash
end
def marshal_dump # :nodoc:
- fix_syck_default_key_in_requirements
-
[@requirements]
end
- def marshal_load array # :nodoc:
+ def marshal_load(array) # :nodoc:
@requirements = array[0]
- fix_syck_default_key_in_requirements
+ raise TypeError, "wrong @requirements" unless Array === @requirements &&
+ @requirements.all? {|r| r.size == 2 && (r.first.is_a?(String) || r[0] = "=") && r.last.is_a?(Gem::Version) }
+ end
+
+ def yaml_initialize(tag, vals) # :nodoc:
+ 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? }
+ requirements.any? {|r| r.last.prerelease? }
end
- def pretty_print q # :nodoc:
- q.group 1, 'Gem::Requirement.new(', ')' do
+ def pretty_print(q) # :nodoc:
+ q.group 1, "Gem::Requirement.new(", ")" do
q.pp as_list
end
end
@@ -160,13 +238,14 @@ class Gem::Requirement
##
# True if +version+ satisfies this Requirement.
- def satisfied_by? version
- # #28965: syck has a bug with unquoted '=' YAML.loading as YAML::DefaultKey
- requirements.all? { |op, rv| (OPS[op] || OPS["="]).call version, rv }
+ 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 :=== :satisfied_by?
- alias :=~ :satisfied_by?
+ alias_method :===, :satisfied_by?
+ alias_method :=~, :satisfied_by?
##
# True if the requirement will not always match the latest version.
@@ -174,34 +253,46 @@ class Gem::Requirement
def specific?
return true if @requirements.length > 1 # GIGO, > 1, > 2 is silly
- not %w[> >=].include? @requirements.first.first # grab the operator
+ !%w[> >=].include? @requirements.first.first # grab the operator
end
def to_s # :nodoc:
as_list.join ", "
end
- def <=> other # :nodoc:
- to_s <=> other.to_s
+ 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
- private
+ protected
- def fix_syck_default_key_in_requirements
- # Fixup the Syck DefaultKey bug
- @requirements.each do |r|
- if r[0].kind_of? YAML::Syck::DefaultKey
- r[0] = "="
- end
- end
+ def _sorted_requirements
+ @_sorted_requirements ||= requirements.sort_by(&:to_s)
end
-end
-# :stopdoc:
-# Gem::Version::Requirement is used in a lot of old YAML specs. It's aliased
-# here for backwards compatibility. I'd like to remove this, maybe in RubyGems
-# 2.0.
+ def _tilde_requirements
+ @_tilde_requirements ||= _sorted_requirements.select {|r| r.first == "~>" }
+ end
-::Gem::Version::Requirement = ::Gem::Requirement
-# :startdoc:
+ 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
index f51da65b4b..69ba87b07f 100644
--- a/lib/rubygems/security.rb
+++ b/lib/rubygems/security.rb
@@ -1,84 +1,94 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/exceptions'
-require 'rubygems/gem_openssl'
-require 'fileutils'
+require_relative "exceptions"
+require_relative "openssl"
+##
+# = Signing gems
#
-# = Signed Gems README
-#
-# == Table of Contents
-# * Overview
-# * Walkthrough
-# * Command-Line Options
-# * OpenSSL Reference
-# * Bugs/TODO
-# * About the Author
-#
-# == Overview
-#
-# Gem::Security implements cryptographic signatures in RubyGems. The section
+# 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 gemmaster@example.com
-# $ gem cert --build gemmaster@example.com
+# # build a private key and certificate for yourself:
+# $ gem cert --build you@example.com
#
-# This could take anywhere from 5 seconds to 10 minutes, 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.
+# 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: take the "gem-private_key.pem" file and move it
-# somewhere private, preferably a directory only you have access to, a floppy
-# (yuck!), a CD-ROM, or something comparably secure. 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).
+# 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).
#
-# Now, let's sign an existing gem. I'll be using my Imlib2-Ruby bindings, but
-# you can use whatever gem you'd like. Open up your existing gemspec file and
-# add the following lines:
+# 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 key and certificate chain
-# s.signing_key = '/mnt/floppy/gem-private_key.pem'
-# s.cert_chain = ['gem-public_cert.pem']
+# === Signing Gems
#
-# (Be sure to replace "/mnt/floppy" with the ultra-secret path to your private
-# key).
+# 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.
#
-# After that, go ahead and build your gem as usual. Congratulations, you've
-# just built your first signed gem! If you peek inside your gem file, you'll
-# see a couple of new files have been added:
+# 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.
#
-# $ tar tf tar tf Imlib2-Ruby-0.5.0.gem
-# data.tar.gz
-# data.tar.gz.sig
+# 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.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: "-P HighSecurity", like this:
+# following options: <code>-P HighSecurity</code>, like this:
#
# # install the gem with using the security policy "HighSecurity"
-# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity
+# $ sudo gem install your.gem -P HighSecurity
#
-# The -P option sets your security policy -- we'll talk about that in just a
-# minute. Eh, what's this?
+# The <code>-P</code> option sets your security policy -- we'll talk about
+# that in just a minute. Eh, what's this?
#
-# Attempting local installation of 'Imlib2-Ruby-0.5.0.gem'
-# ERROR: Error installing gem Imlib2-Ruby-0.5.0.gem[.gem]: Couldn't
-# verify data signature: Untrusted Signing Chain Root: cert =
-# '/CN=gemmaster/DC=example/DC=com', error = 'path
-# "/root/.rubygems/trust/cert-15dbb43a6edf6a70a85d4e784e2e45312cff7030.pem"
-# does not exist'
+# $ 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
@@ -105,52 +115,57 @@ require 'fileutils'
# * 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; he can't modify the
-# package contents without invalidating the signature, and he can't
+# 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
-# he'll have better luck causing problems for CPAN users instead :).
+# they'll have better luck causing problems for CPAN users instead :).
#
-# So, the reason RubyGems refused to install our shiny new signed gem was
-# because it was from an untrusted source. Well, my code is infallible
-# (hah!), so I'm going to add myself as a trusted source.
+# 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:
#
-# Here's how:
+# # add trusted certificate
+# gem cert --add ~/.gem/gem-public_cert.pem
#
-# # add trusted certificate
-# gem cert --add gem-public_cert.pem
-#
-# I've added my public certificate as a trusted source. Now I can install
-# packages signed my private key without any hassle. Let's try the install
-# command above again:
+# 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)
-# $ sudo gem install Imlib2-Ruby-0.5.0.gem -P HighSecurity
+# $ gem install -P HighSecurity your-gem-1.0.gem
+# Successfully installed your-gem-1.0
+# 1 gem installed
#
-# This time RubyGems should 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:
+# This time RubyGems will accept your signed package and begin installing.
#
-# Usage: gem cert [options]
+# 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 List trusted certificates.
-# -r, --remove STRING Remove trusted certificates containing STRING.
-# -b, --build EMAIL_ADDR Build private key and self-signed certificate
-# for EMAIL_ADDR.
-# -C, --certificate CERT Certificate for --sign command.
-# -K, --private-key KEY Private key for --sign command.
-# -s, --sign NEWCERT Sign a certificate with my key and certificate.
-#
-# (By the way, you can pull up this list any time you'd like by typing "gem
-# cert --help")
-#
-# Hmm. We've already covered the "--build" option, and the "--add", "--list",
-# and "--remove" commands seem fairly straightforward; they allow you to add,
-# list, and remove the certificates in your trusted certificate list. But
-# what's with this "--sign" option?
+# -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
@@ -172,134 +187,102 @@ require 'fileutils'
# trust. Here's a hypothetical example of a trust hierarchy based (roughly)
# on geography:
#
-#
# --------------------------
-# | rubygems@rubyforge.org |
+# | rubygems@rubygems.org |
# --------------------------
# |
# -----------------------------------
# | |
# ---------------------------- -----------------------------
-# | seattle.rb@zenspider.com | | dcrubyists@richkilmer.com |
+# | seattlerb@seattlerb.org | | dcrubyists@richkilmer.com |
# ---------------------------- -----------------------------
# | | | |
# --------------- ---------------- ----------- --------------
-# | alf@seattle | | bob@portland | | pabs@dc | | tomcope@dc |
+# | drbrain | | zenspider | | pabs@dc | | tomcope@dc |
# --------------- ---------------- ----------- --------------
#
#
-# Now, rather than having 4 trusted certificates (one for alf@seattle,
-# bob@portland, pabs@dc, and tomecope@dc), a user could actually get by with 1
-# certificate: the "rubygems@rubyforge.org" certificate. Here's how it works:
+# 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 "Alf2000-Ruby-0.1.0.gem", a package signed by "alf@seattle". I've
-# never heard of "alf@seattle", but his certificate has a valid signature from
-# the "seattle.rb@zenspider.com" certificate, which in turn has a valid
-# signature from the "rubygems@rubyforge.org" certificate. Voila! At this
-# point, it's much more reasonable for me to trust a package signed by
-# "alf@seattle", because I can establish a chain to "rubygems@rubyforge.org",
-# which I do trust.
+# 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.
#
-# And the "--sign" option allows all this to happen. A developer creates
-# their build certificate with the "--build" 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:
+# === 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, our buddy
-# "alf@seattle"), 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, "alf@seattle" would change his gemspec file to look like this:
-#
-# # signing key (still kept in an undisclosed location!)
-# s.signing_key = '/mnt/floppy/alf-private_key.pem'
-#
-# # certificate chain (includes the issuer certificate now too)
-# s.cert_chain = ['/home/alf/doc/seattlerb-public_cert.pem',
-# '/home/alf/doc/alf_at_seattle-public_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
+# 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...
#
-# I'm sure your new signed gem has finished installing by now (unless you're
-# installing rails and all it's dependencies, that is ;D). At this point you
-# should know how to do all of these new and interesting things:
+# At this point you should know how to do all of these new and interesting
+# things:
#
# * build a gem signing key and certificate
-# * modify your existing gems to support signing
# * adjust your security policy
# * modify your trusted certificate list
# * sign a certificate
#
-# If you've got any questions, feel free to contact me at the email address
-# below. The next couple of sections
-#
-#
-# == Command-Line Options
-#
-# Here's a brief summary of the certificate-related command line options:
-#
-# gem install
-# -P, --trust-policy POLICY Specify gem trust policy.
-#
-# gem cert
-# -a, --add CERT Add a trusted certificate.
-# -l, --list List trusted certificates.
-# -r, --remove STRING Remove trusted certificates containing
-# STRING.
-# -b, --build EMAIL_ADDR Build private key and self-signed
-# certificate for EMAIL_ADDR.
-# -C, --certificate CERT Certificate for --sign command.
-# -K, --private-key KEY Private key for --sign command.
-# -s, --sign NEWCERT Sign a certificate with my key and
-# certificate.
-#
-# A more detailed description of each options is available in the walkthrough
-# above.
-#
# == 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
+# 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 -pe 'sub(/^ +/, "")' > public_key.crt
+# 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
+# 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
+# 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
+# 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 just basic OpenSSL PEM
-# files. Here's a couple of useful commands for manipulating them:
+# 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)
@@ -321,8 +304,8 @@ require 'fileutils'
# * 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 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)
@@ -332,495 +315,301 @@ require 'fileutils'
# MediumSecurity and HighSecurity policies)
# * Better explanation of X509 naming (ie, we don't have to use email
# addresses)
-# * Possible alternate signing mechanisms (eg, via PGP). this could be done
-# pretty easily by adding a :signing_type attribute to the gemspec, then add
-# the necessary support in other places
# * Honor AIA field (see note about OCSP above)
-# * Maybe honor restriction extensions?
+# * 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. ideas?
-# * Possibly embed signature and key algorithms into metadata (right now
-# they're assumed to be the same as what's set in Gem::Security::OPT)
+# file, instead of an array embedded in the metadata.
#
-# == About the Author
+# == Original author
#
# Paul Duncan <pabs@pablotron.org>
-# http://pablotron.org/
+# https://pablotron.org/
module Gem::Security
-
##
# Gem::Security default exception type
class Exception < Gem::Exception; end
##
- # Default options for most of the methods below
-
- OPT = {
- # private key options
- :key_algo => Gem::SSL::PKEY_RSA,
- :key_size => 2048,
-
- # public cert options
- :cert_age => 365 * 24 * 3600, # 1 year
- :dgst_algo => Gem::SSL::DIGEST_SHA1,
-
- # x509 certificate extensions
- :cert_exts => {
- 'basicConstraints' => 'CA:FALSE',
- 'subjectKeyIdentifier' => 'hash',
- 'keyUsage' => 'keyEncipherment,dataEncipherment,digitalSignature',
- },
-
- # save the key and cert to a file in build_self_signed_cert()?
- :save_key => true,
- :save_cert => true,
-
- # if you define either of these, then they'll be used instead of
- # the output_fmt macro below
- :save_key_path => nil,
- :save_cert_path => nil,
-
- # output name format for self-signed certs
- :output_fmt => 'gem-%s.pem',
- :munge_re => Regexp.new(/[^a-z0-9_.-]+/),
-
- # output directory for trusted certificate checksums
- :trust_dir => File.join(Gem.user_home, '.gem', 'trust'),
-
- # default permissions for trust directory and certs
- :perms => {
- :trust_dir => 0700,
- :trusted_cert => 0600,
- :signing_cert => 0600,
- :signing_key => 0600,
- },
- }
-
- ##
- # 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 below.
-
- class Policy
- attr_accessor :verify_data, :verify_signer, :verify_chain,
- :verify_root, :only_trusted, :only_signed
-
- #
- # Create a new Gem::Security::Policy object with the given mode and
- # options.
- #
- def initialize(policy = {}, opt = {})
- # set options
- @opt = Gem::Security::OPT.merge(opt)
-
- # build policy
- 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
-
- #
- # Get the path to the file for this cert.
- #
- def self.trusted_cert_path(cert, opt = {})
- opt = Gem::Security::OPT.merge(opt)
-
- # get digest algorithm, calculate checksum of root.subject
- algo = opt[:dgst_algo]
- dgst = algo.hexdigest(cert.subject.to_s)
+ # Used internally to select the signing digest from all computed digests
- # build path to trusted cert file
- name = "cert-#{dgst}.pem"
-
- # join and return path components
- File::join(opt[:trust_dir], name)
- end
-
- #
- # Verify that the gem data with the given signature and signing chain
- # matched this security policy at the specified time.
- #
- def verify_gem(signature, data, chain, time = Time.now)
- Gem.ensure_ssl_available
- cert_class = OpenSSL::X509::Certificate
- exc = Gem::Security::Exception
- chain ||= []
-
- chain = chain.map{ |str| cert_class.new(str) }
- signer, ch_len = chain[-1], chain.size
-
- # make sure signature is valid
- if @verify_data
- # get digest algorithm (TODO: this should be configurable)
- dgst = @opt[:dgst_algo]
-
- # verify the data signature (this is the most important part, so don't
- # screw it up :D)
- v = signer.public_key.verify(dgst.new, signature, data)
- raise exc, "Invalid Gem Signature" unless v
-
- # make sure the signer is valid
- if @verify_signer
- # make sure the signing cert is valid right now
- v = signer.check_validity(nil, time)
- raise exc, "Invalid Signature: #{v[:desc]}" unless v[:is_valid]
- end
- end
-
- # make sure the certificate chain is valid
- if @verify_chain
- # iterate down over the chain and verify each certificate against it's
- # issuer
- (ch_len - 1).downto(1) do |i|
- issuer, cert = chain[i - 1, 2]
- v = cert.check_validity(issuer, time)
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Invalid Signing Chain', cert.subject, v[:desc]
- ] unless v[:is_valid]
- end
-
- # verify root of chain
- if @verify_root
- # make sure root is self-signed
- root = chain[0]
- raise exc, "%s: %s (subject = '%s', issuer = '%s')" % [
- 'Invalid Signing Chain Root',
- 'Subject does not match Issuer for Gem Signing Chain',
- root.subject.to_s,
- root.issuer.to_s,
- ] unless root.issuer.to_s == root.subject.to_s
-
- # make sure root is valid
- v = root.check_validity(root, time)
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Invalid Signing Chain Root', root.subject, v[:desc]
- ] unless v[:is_valid]
-
- # verify that the chain root is trusted
- if @only_trusted
- # get digest algorithm, calculate checksum of root.subject
- algo = @opt[:dgst_algo]
- path = Gem::Security::Policy.trusted_cert_path(root, @opt)
-
- # check to make sure trusted path exists
- raise exc, "%s: cert = '%s', error = '%s'" % [
- 'Untrusted Signing Chain Root',
- root.subject.to_s,
- "path \"#{path}\" does not exist",
- ] unless File.exist?(path)
-
- # load calculate digest from saved cert file
- save_cert = OpenSSL::X509::Certificate.new(File.read(path))
- save_dgst = algo.digest(save_cert.public_key.to_s)
-
- # create digest of public key
- pkey_str = root.public_key.to_s
- cert_dgst = algo.digest(pkey_str)
-
- # now compare the two digests, raise exception
- # if they don't match
- raise exc, "%s: %s (saved = '%s', root = '%s')" % [
- 'Invalid Signing Chain Root',
- "Saved checksum doesn't match root checksum",
- save_dgst, cert_dgst,
- ] unless save_dgst == cert_dgst
- end
- end
-
- # return the signing chain
- chain.map { |cert| cert.subject }
- end
- end
- end
+ DIGEST_NAME = "SHA256" # :nodoc:
##
- # No security policy: all package signature checks are disabled.
+ # Length of keys created by RSA and DSA keys
- NoSecurity = Policy.new(
- :verify_data => false,
- :verify_signer => false,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ RSA_DSA_KEY_LENGTH = 3072
##
- # 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(
- :verify_data => true,
- :verify_signer => false,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ # Default algorithm to use when building a key pair
- ##
- # 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(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => false,
- :verify_root => false,
- :only_trusted => false,
- :only_signed => false
- )
+ DEFAULT_KEY_ALGORITHM = "RSA"
##
- # 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(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => true,
- :verify_root => true,
- :only_trusted => true,
- :only_signed => false
- )
+ # Named curve used for Elliptic Curve
- ##
- # 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(
- :verify_data => true,
- :verify_signer => true,
- :verify_chain => true,
- :verify_root => true,
- :only_trusted => true,
- :only_signed => true
- )
+ EC_NAME = "secp384r1"
##
- # Hash of configured security policies
+ # Cipher used to encrypt the key pair used to sign gems.
+ # Must be in the list returned by OpenSSL::Cipher.ciphers
- Policies = {
- 'NoSecurity' => NoSecurity,
- 'AlmostNoSecurity' => AlmostNoSecurity,
- 'LowSecurity' => LowSecurity,
- 'MediumSecurity' => MediumSecurity,
- 'HighSecurity' => HighSecurity,
- }
+ KEY_CIPHER = OpenSSL::Cipher.new("AES-256-CBC") if defined?(OpenSSL::Cipher)
##
- # Sign the cert cert with @signing_key and @signing_cert, using the digest
- # algorithm opt[:dgst_algo]. Returns the newly signed certificate.
+ # One day in seconds
- def self.sign_cert(cert, signing_key, signing_cert, opt = {})
- opt = OPT.merge(opt)
+ ONE_DAY = 86_400
- cert.issuer = signing_cert.subject
- cert.sign signing_key, opt[:dgst_algo].new
+ ##
+ # One year in seconds
- cert
- end
+ ONE_YEAR = ONE_DAY * 365
##
- # 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 self.verify_trust_dir(path, perms)
- # if the directory exists, then make sure it is in fact a directory. if
- # it doesn't exist, then create it with the appropriate permissions
- if File.exist?(path)
- # verify that the trust directory is actually a directory
- unless File.directory?(path)
- err = "trust directory #{path} isn't a directory"
- raise Gem::Security::Exception, err
- end
- else
- # trust directory doesn't exist, so create it with permissions
- FileUtils.mkdir_p(path)
- FileUtils.chmod(perms, path)
+ # 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
##
- # Build a certificate from the given DN and private key.
-
- def self.build_cert(name, key, opt = {})
- Gem.ensure_ssl_available
- opt = OPT.merge opt
+ # 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.not_after = Time.now + opt[:cert_age]
- cert.not_before = Time.now
- cert.public_key = key.public_key
- cert.serial = 0
- cert.subject = name
+ 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 = opt[:cert_exts].map do |ext_name, value|
+ cert.extensions = extensions.map do |ext_name, value|
ef.create_extension ext_name, value
end
- i_key = opt[:issuer_key] || key
- i_cert = opt[:issuer_cert] || cert
+ cert
+ end
- cert = sign_cert cert, i_key, i_cert, opt
+ ##
+ # Gets the right public key from a PKey instance
- cert
+ 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
##
- # Build a self-signed certificate for the given email address.
+ # 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.build_self_signed_cert(email_addr, opt = {})
- Gem.ensure_ssl_available
- opt = OPT.merge(opt)
- path = { :key => nil, :cert => nil }
+ def self.create_cert_email(email, key, age = ONE_YEAR, extensions = EXTENSIONS)
+ subject = email_to_name email
- name = email_to_name email_addr, opt[:munge_re]
+ extensions = extensions.merge "subjectAltName" => "email:#{email}"
- key = opt[:key_algo].new opt[:key_size]
+ create_cert_self_signed subject, key, age, extensions
+ end
- verify_trust_dir opt[:trust_dir], opt[:perms][:trust_dir]
+ ##
+ # Creates a self-signed certificate with an issuer and subject of +subject+
+ # and the given +extensions+ for the +key+.
- if opt[:save_key] then
- path[:key] = opt[:save_key_path] || (opt[:output_fmt] % 'private_key')
+ def self.create_cert_self_signed(subject, key, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ certificate = create_cert subject, key, age, extensions
- open path[:key], 'wb' do |io|
- io.chmod opt[:perms][:signing_key]
- io.write key.to_pem
- end
- end
+ sign certificate, key, certificate, age, extensions, serial
+ end
- cert = build_cert name, key, opt
+ ##
+ # Creates a new digest instance using the specified +algorithm+. The default
+ # is SHA256.
- if opt[:save_cert] then
- path[:cert] = opt[:save_cert_path] || (opt[:output_fmt] % 'public_cert')
+ def self.create_digest(algorithm = DIGEST_NAME)
+ OpenSSL::Digest.new(algorithm)
+ end
- open path[:cert], 'wb' do |file|
- file.chmod opt[:perms][:signing_cert]
- file.write cert.to_pem
+ ##
+ # 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
-
- { :key => key, :cert => cert,
- :key_path => path[:key], :cert_path => path[:cert] }
end
##
# Turns +email_address+ into an OpenSSL::X509::Name
- def self.email_to_name email_address, munge_re
- cn, dcs = email_address.split '@'
+ def self.email_to_name(email_address)
+ email_address = email_address.gsub(/[^\w@.-]+/i, "_")
- dcs = dcs.split '.'
+ cn, dcs = email_address.split "@"
- cn = cn.gsub munge_re, '_'
+ dcs = dcs.split "."
- dcs = dcs.map do |dc|
- dc.gsub munge_re, '_'
+ 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
- name = "CN=#{cn}/" << dcs.map { |dc| "DC=#{dc}" }.join('/')
+ serial = expired_certificate.serial + 1
- OpenSSL::X509::Name.parse name
+ create_cert_self_signed(expired_certificate.subject, private_key, age,
+ extensions, serial)
end
##
- # Add certificate to trusted cert list.
+ # 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.
#
- # Note: At the moment these are stored in OPT[:trust_dir], although that
- # directory may change in the future.
+ # Returns the newly signed certificate.
- def self.add_trusted_cert(cert, opt = {})
- opt = OPT.merge(opt)
+ def self.sign(certificate, signing_key, signing_cert, age = ONE_YEAR, extensions = EXTENSIONS, serial = 1)
+ signee_subject = certificate.subject
+ signee_key = certificate.public_key
- # get destination path
- path = Gem::Security::Policy.trusted_cert_path(cert, opt)
+ alt_name = certificate.extensions.find do |extension|
+ extension.oid == "subjectAltName"
+ end
- # verify trust directory (can't write to nowhere, you know)
- verify_trust_dir(opt[:trust_dir], opt[:perms][:trust_dir])
+ extensions = extensions.merge "subjectAltName" => alt_name.value if
+ alt_name
- # write cert to output file
- File.open(path, 'wb') do |file|
- file.chmod(opt[:perms][:trusted_cert])
- file.write(cert.to_pem)
+ issuer_alt_name = signing_cert.extensions.find do |extension|
+ extension.oid == "subjectAltName"
end
- # return nil
- nil
+ 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
##
- # Basic OpenSSL-based package signing class.
-
- class Signer
+ # Returns a Gem::Security::TrustDir which wraps the directory where trusted
+ # certificates live.
- attr_accessor :cert_chain
- attr_accessor :key
+ def self.trust_dir
+ return @trust_dir if @trust_dir
- def initialize(key, cert_chain)
- Gem.ensure_ssl_available
- @algo = Gem::Security::OPT[:dgst_algo]
- @key, @cert_chain = key, cert_chain
+ dir = File.join Gem.user_home, ".gem", "trust"
- # check key, if it's a file, and if it's key, leave it alone
- if @key && !@key.kind_of?(OpenSSL::PKey::PKey)
- @key = OpenSSL::PKey::RSA.new(File.read(@key))
- end
+ @trust_dir ||= Gem::Security::TrustDir.new dir
+ end
- # check cert chain, if it's a file, load it, if it's cert data, convert
- # it into a cert object, and if it's a cert object, leave it alone
- if @cert_chain
- @cert_chain = @cert_chain.map do |cert|
- # check cert, if it's a file, load it, if it's cert data, convert it
- # into a cert object, and if it's a cert object, leave it alone
- if cert && !cert.kind_of?(OpenSSL::X509::Certificate)
- cert = File.read(cert) if File::exist?(cert)
- cert = OpenSSL::X509::Certificate.new(cert)
- end
- cert
- end
- end
- end
+ ##
+ # Enumerates the trusted certificates via Gem::Security::TrustDir.
- ##
- # Sign data with given digest algorithm
+ def self.trusted_certificates(&block)
+ trust_dir.each_certificate(&block)
+ end
- def sign(data)
- @key.sign(@algo.new, data)
+ ##
+ # 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/server.rb b/lib/rubygems/server.rb
deleted file mode 100644
index 47fa7c562d..0000000000
--- a/lib/rubygems/server.rb
+++ /dev/null
@@ -1,832 +0,0 @@
-require 'webrick'
-require 'zlib'
-require 'erb'
-
-require 'rubygems'
-require 'rubygems/doc_manager'
-
-##
-# Gem::Server and allows users to serve gems for consumption by
-# `gem --remote-install`.
-#
-# gem_server starts an HTTP server on the given port and serves the following:
-# * "/" - Browsing of gem spec files for installed gems
-# * "/specs.#{Gem.marshal_version}.gz" - specs name/version/platform index
-# * "/latest_specs.#{Gem.marshal_version}.gz" - latest specs
-# name/version/platform index
-# * "/quick/" - Individual gemspecs
-# * "/gems" - Direct access to download the installable gems
-# * "/rdoc?q=" - Search for installed rdoc documentation
-# * legacy indexes:
-# * "/Marshal.#{Gem.marshal_version}" - Full SourceIndex dump of metadata
-# for installed gems
-#
-# == Usage
-#
-# gem_server = Gem::Server.new Gem.dir, 8089, false
-# gem_server.run
-#
-#--
-# TODO Refactor into a real WEBrick servlet to remove code duplication.
-
-class Gem::Server
-
- attr_reader :spec_dirs
-
- include ERB::Util
- include Gem::UserInteraction
-
- SEARCH = <<-SEARCH
- <form class="headerSearch" name="headerSearchForm" method="get" action="/rdoc">
- <div id="search" style="float:right">
- <label for="q">Filter/Search</label>
- <input id="q" type="text" style="width:10em" name="q">
- <button type="submit" style="display:none"></button>
- </div>
- </form>
- SEARCH
-
- DOC_TEMPLATE = <<-'DOC_TEMPLATE'
- <?xml version="1.0" encoding="iso-8859-1"?>
- <!DOCTYPE html
- PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-
- <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>RubyGems Documentation Index</title>
- <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
- </head>
- <body>
- <div id="fileHeader">
-<%= SEARCH %>
- <h1>RubyGems Documentation Index</h1>
- </div>
- <!-- banner header -->
-
- <div id="bodyContent">
- <div id="contextContent">
- <div id="description">
- <h1>Summary</h1>
- <p>There are <%=values["gem_count"]%> gems installed:</p>
- <p>
- <%= values["specs"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
- <h1>Gems</h1>
-
- <dl>
- <% values["specs"].each do |spec| %>
- <dt>
- <% if spec["first_name_entry"] then %>
- <a name="<%=spec["name"]%>"></a>
- <% end %>
-
- <b><%=spec["name"]%> <%=spec["version"]%></b>
-
- <% if spec["rdoc_installed"] then %>
- <a href="<%=spec["doc_path"]%>">[rdoc]</a>
- <% else %>
- <span title="rdoc not installed">[rdoc]</span>
- <% end %>
-
- <% if spec["homepage"] then %>
- <a href="<%=spec["homepage"]%>" title="<%=spec["homepage"]%>">[www]</a>
- <% else %>
- <span title="no homepage available">[www]</span>
- <% end %>
-
- <% if spec["has_deps"] then %>
- - depends on
- <%= spec["dependencies"].map { |v| "<a href=\"##{v["name"]}\">#{v["name"]}</a>" }.join ', ' %>.
- <% end %>
- </dt>
- <dd>
- <%=spec["summary"]%>
- <% if spec["executables"] then %>
- <br/>
-
- <% if spec["only_one_executable"] then %>
- Executable is
- <% else %>
- Executables are
- <%end%>
-
- <%= spec["executables"].map { |v| "<span class=\"context-item-name\">#{v["executable"]}</span>"}.join ', ' %>.
-
- <%end%>
- <br/>
- <br/>
- </dd>
- <% end %>
- </dl>
-
- </div>
- </div>
- </div>
- <div id="validator-badges">
- <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
- </div>
- </body>
- </html>
- DOC_TEMPLATE
-
- # CSS is copy & paste from rdoc-style.css, RDoc V1.0.1 - 20041108
- RDOC_CSS = <<-RDOC_CSS
-body {
- font-family: Verdana,Arial,Helvetica,sans-serif;
- font-size: 90%;
- margin: 0;
- margin-left: 40px;
- padding: 0;
- background: white;
-}
-
-h1,h2,h3,h4 { margin: 0; color: #efefef; background: transparent; }
-h1 { font-size: 150%; }
-h2,h3,h4 { margin-top: 1em; }
-
-a { background: #eef; color: #039; text-decoration: none; }
-a:hover { background: #039; color: #eef; }
-
-/* Override the base stylesheets Anchor inside a table cell */
-td > a {
- background: transparent;
- color: #039;
- text-decoration: none;
-}
-
-/* and inside a section title */
-.section-title > a {
- background: transparent;
- color: #eee;
- text-decoration: none;
-}
-
-/* === Structural elements =================================== */
-
-div#index {
- margin: 0;
- margin-left: -40px;
- padding: 0;
- font-size: 90%;
-}
-
-
-div#index a {
- margin-left: 0.7em;
-}
-
-div#index .section-bar {
- margin-left: 0px;
- padding-left: 0.7em;
- background: #ccc;
- font-size: small;
-}
-
-
-div#classHeader, div#fileHeader {
- width: auto;
- color: white;
- padding: 0.5em 1.5em 0.5em 1.5em;
- margin: 0;
- margin-left: -40px;
- border-bottom: 3px solid #006;
-}
-
-div#classHeader a, div#fileHeader a {
- background: inherit;
- color: white;
-}
-
-div#classHeader td, div#fileHeader td {
- background: inherit;
- color: white;
-}
-
-
-div#fileHeader {
- background: #057;
-}
-
-div#classHeader {
- background: #048;
-}
-
-
-.class-name-in-header {
- font-size: 180%;
- font-weight: bold;
-}
-
-
-div#bodyContent {
- padding: 0 1.5em 0 1.5em;
-}
-
-div#description {
- padding: 0.5em 1.5em;
- background: #efefef;
- border: 1px dotted #999;
-}
-
-div#description h1,h2,h3,h4,h5,h6 {
- color: #125;;
- background: transparent;
-}
-
-div#validator-badges {
- text-align: center;
-}
-div#validator-badges img { border: 0; }
-
-div#copyright {
- color: #333;
- background: #efefef;
- font: 0.75em sans-serif;
- margin-top: 5em;
- margin-bottom: 0;
- padding: 0.5em 2em;
-}
-
-
-/* === Classes =================================== */
-
-table.header-table {
- color: white;
- font-size: small;
-}
-
-.type-note {
- font-size: small;
- color: #DEDEDE;
-}
-
-.xxsection-bar {
- background: #eee;
- color: #333;
- padding: 3px;
-}
-
-.section-bar {
- color: #333;
- border-bottom: 1px solid #999;
- margin-left: -20px;
-}
-
-
-.section-title {
- background: #79a;
- color: #eee;
- padding: 3px;
- margin-top: 2em;
- margin-left: -30px;
- border: 1px solid #999;
-}
-
-.top-aligned-row { vertical-align: top }
-.bottom-aligned-row { vertical-align: bottom }
-
-/* --- Context section classes ----------------------- */
-
-.context-row { }
-.context-item-name { font-family: monospace; font-weight: bold; color: black; }
-.context-item-value { font-size: small; color: #448; }
-.context-item-desc { color: #333; padding-left: 2em; }
-
-/* --- Method classes -------------------------- */
-.method-detail {
- background: #efefef;
- padding: 0;
- margin-top: 0.5em;
- margin-bottom: 1em;
- border: 1px dotted #ccc;
-}
-.method-heading {
- color: black;
- background: #ccc;
- border-bottom: 1px solid #666;
- padding: 0.2em 0.5em 0 0.5em;
-}
-.method-signature { color: black; background: inherit; }
-.method-name { font-weight: bold; }
-.method-args { font-style: italic; }
-.method-description { padding: 0 0.5em 0 0.5em; }
-
-/* --- Source code sections -------------------- */
-
-a.source-toggle { font-size: 90%; }
-div.method-source-code {
- background: #262626;
- color: #ffdead;
- margin: 1em;
- padding: 0.5em;
- border: 1px dashed #999;
- overflow: hidden;
-}
-
-div.method-source-code pre { color: #ffdead; overflow: hidden; }
-
-/* --- Ruby keyword styles --------------------- */
-
-.standalone-code { background: #221111; color: #ffdead; overflow: hidden; }
-
-.ruby-constant { color: #7fffd4; background: transparent; }
-.ruby-keyword { color: #00ffff; background: transparent; }
-.ruby-ivar { color: #eedd82; background: transparent; }
-.ruby-operator { color: #00ffee; background: transparent; }
-.ruby-identifier { color: #ffdead; background: transparent; }
-.ruby-node { color: #ffa07a; background: transparent; }
-.ruby-comment { color: #b22222; font-weight: bold; background: transparent; }
-.ruby-regexp { color: #ffa07a; background: transparent; }
-.ruby-value { color: #7fffd4; background: transparent; }
- RDOC_CSS
-
- RDOC_NO_DOCUMENTATION = <<-'NO_DOC'
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>Found documentation</title>
- <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
- </head>
- <body>
- <div id="fileHeader">
-<%= SEARCH %>
- <h1>No documentation found</h1>
- </div>
-
- <div id="bodyContent">
- <div id="contextContent">
- <div id="description">
- <p>No gems matched <%= h query.inspect %></p>
-
- <p>
- Back to <a href="/">complete gem index</a>
- </p>
-
- </div>
- </div>
- </div>
- <div id="validator-badges">
- <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
- </div>
- </body>
-</html>
- NO_DOC
-
- RDOC_SEARCH_TEMPLATE = <<-'RDOC_SEARCH'
-<?xml version="1.0" encoding="iso-8859-1"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- <head>
- <title>Found documentation</title>
- <link rel="stylesheet" href="gem-server-rdoc-style.css" type="text/css" media="screen" />
- </head>
- <body>
- <div id="fileHeader">
-<%= SEARCH %>
- <h1>Found documentation</h1>
- </div>
- <!-- banner header -->
-
- <div id="bodyContent">
- <div id="contextContent">
- <div id="description">
- <h1>Summary</h1>
- <p><%=doc_items.length%> documentation topics found.</p>
- <h1>Topics</h1>
-
- <dl>
- <% doc_items.each do |doc_item| %>
- <dt>
- <b><%=doc_item[:name]%></b>
- <a href="<%=doc_item[:url]%>">[rdoc]</a>
- </dt>
- <dd>
- <%=doc_item[:summary]%>
- <br/>
- <br/>
- </dd>
- <% end %>
- </dl>
-
- <p>
- Back to <a href="/">complete gem index</a>
- </p>
-
- </div>
- </div>
- </div>
- <div id="validator-badges">
- <p><small><a href="http://validator.w3.org/check/referer">[Validate]</a></small></p>
- </div>
- </body>
-</html>
- RDOC_SEARCH
-
- def self.run(options)
- new(options[:gemdir], options[:port], options[:daemon],
- options[:launch], options[:addresses]).run
- end
-
- ##
- # Only the first directory in gem_dirs is used for serving gems
-
- def initialize(gem_dirs, port, daemon, launch = nil, addresses = nil)
- Socket.do_not_reverse_lookup = true
-
- @gem_dirs = Array gem_dirs
- @port = port
- @daemon = daemon
- @launch = launch
- @addresses = addresses
- logger = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL
- @server = WEBrick::HTTPServer.new :DoNotListen => true, :Logger => logger
-
- @spec_dirs = @gem_dirs.map do |gem_dir|
- spec_dir = File.join gem_dir, 'specifications'
-
- unless File.directory? spec_dir then
- raise ArgumentError, "#{gem_dir} does not appear to be a gem repository"
- end
-
- spec_dir
- end
-
- Gem::Specification.dirs = @gem_dirs
- end
-
- def Marshal(req, res)
- Gem::Specification.reset
-
- add_date res
-
- index = Gem::Deprecate.skip_during { Marshal.dump Gem.source_index }
-
- if req.request_method == 'HEAD' then
- res['content-length'] = index.length
- return
- end
-
- if req.path =~ /Z$/ then
- res['content-type'] = 'application/x-deflate'
- index = Gem.deflate index
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- res.body << index
- end
-
- def add_date res
- res['date'] = @spec_dirs.map do |spec_dir|
- File.stat(spec_dir).mtime
- end.max
- end
-
- def latest_specs(req, res)
- Gem::Specification.reset
-
- res['content-type'] = 'application/x-gzip'
-
- add_date res
-
- latest_specs = Gem::Specification.latest_specs
-
- specs = latest_specs.sort.map do |spec|
- platform = spec.original_platform || Gem::Platform::RUBY
- [spec.name, spec.version, platform]
- end
-
- specs = Marshal.dump specs
-
- if req.path =~ /\.gz$/ then
- specs = Gem.gzip specs
- res['content-type'] = 'application/x-gzip'
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- if req.request_method == 'HEAD' then
- res['content-length'] = specs.length
- else
- res.body << specs
- end
- end
-
- ##
- # Creates server sockets based on the addresses option. If no addresses
- # were given a server socket for all interfaces is created.
-
- def listen addresses = @addresses
- addresses = [nil] unless addresses
-
- listeners = 0
-
- addresses.each do |address|
- begin
- @server.listen address, @port
- @server.listeners[listeners..-1].each do |listener|
- host, port = listener.addr.values_at 2, 1
- host = "[#{host}]" if host =~ /:/ # we don't reverse lookup
- say "Server started at http://#{host}:#{port}"
- end
-
- listeners = @server.listeners.length
- rescue SystemCallError
- next
- end
- end
-
- if @server.listeners.empty? then
- say "Unable to start a server."
- say "Check for running servers or your --bind and --port arguments"
- terminate_interaction 1
- end
- end
-
- def quick(req, res)
- Gem::Specification.reset
-
- res['content-type'] = 'text/plain'
- add_date res
-
- case req.request_uri.path
- when %r|^/quick/(Marshal.#{Regexp.escape Gem.marshal_version}/)?(.*?)-([0-9.]+)(-.*?)?\.gemspec\.rz$| then
- marshal_format, name, version, platform = $1, $2, $3, $4
- specs = Gem::Specification.find_all_by_name name, version
-
- selector = [name, version, platform].map(&:inspect).join ' '
-
- platform = if platform then
- Gem::Platform.new platform.sub(/^-/, '')
- else
- Gem::Platform::RUBY
- end
-
- specs = specs.select { |s| s.platform == platform }
-
- if specs.empty? then
- res.status = 404
- res.body = "No gems found matching #{selector}"
- elsif specs.length > 1 then
- res.status = 500
- res.body = "Multiple gems found matching #{selector}"
- elsif marshal_format then
- res['content-type'] = 'application/x-deflate'
- res.body << Gem.deflate(Marshal.dump(specs.first))
- end
- else
- raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found."
- end
- end
-
- def root(req, res)
- Gem::Specification.reset
- add_date res
-
- raise WEBrick::HTTPStatus::NotFound, "`#{req.path}' not found." unless
- req.path == '/'
-
- specs = []
- total_file_count = 0
-
- Gem::Specification.each do |spec|
- total_file_count += spec.files.size
- deps = spec.dependencies.map { |dep|
- {
- "name" => dep.name,
- "type" => dep.type,
- "version" => dep.requirement.to_s,
- }
- }
-
- deps = deps.sort_by { |dep| [dep["name"].downcase, dep["version"]] }
- deps.last["is_last"] = true unless deps.empty?
-
- # executables
- executables = spec.executables.sort.collect { |exec| {"executable" => exec} }
- executables = nil if executables.empty?
- executables.last["is_last"] = true if executables
-
- specs << {
- "authors" => spec.authors.sort.join(", "),
- "date" => spec.date.to_s,
- "dependencies" => deps,
- "doc_path" => "/doc_root/#{spec.full_name}/rdoc/index.html",
- "executables" => executables,
- "only_one_executable" => (executables && executables.size == 1),
- "full_name" => spec.full_name,
- "has_deps" => !deps.empty?,
- "homepage" => spec.homepage,
- "name" => spec.name,
- "rdoc_installed" => Gem::DocManager.new(spec).rdoc_installed?,
- "summary" => spec.summary,
- "version" => spec.version.to_s,
- }
- end
-
- specs << {
- "authors" => "Chad Fowler, Rich Kilmer, Jim Weirich, Eric Hodel and others",
- "dependencies" => [],
- "doc_path" => "/doc_root/rubygems-#{Gem::VERSION}/rdoc/index.html",
- "executables" => [{"executable" => 'gem', "is_last" => true}],
- "only_one_executable" => true,
- "full_name" => "rubygems-#{Gem::VERSION}",
- "has_deps" => false,
- "homepage" => "http://docs.rubygems.org/",
- "name" => 'rubygems',
- "rdoc_installed" => true,
- "summary" => "RubyGems itself",
- "version" => Gem::VERSION,
- }
-
- specs = specs.sort_by { |spec| [spec["name"].downcase, spec["version"]] }
- specs.last["is_last"] = true
-
- # tag all specs with first_name_entry
- last_spec = nil
- specs.each do |spec|
- is_first = last_spec.nil? || (last_spec["name"].downcase != spec["name"].downcase)
- spec["first_name_entry"] = is_first
- last_spec = spec
- end
-
- # create page from template
- template = ERB.new(DOC_TEMPLATE)
- res['content-type'] = 'text/html'
-
- values = { "gem_count" => specs.size.to_s, "specs" => specs,
- "total_file_count" => total_file_count.to_s }
-
- # suppress 1.9.3dev warning about unused variable
- values = values
-
- result = template.result binding
- res.body = result
- end
-
- ##
- # Can be used for quick navigation to the rdoc documentation. You can then
- # define a search shortcut for your browser. E.g. in Firefox connect
- # 'shortcut:rdoc' to http://localhost:8808/rdoc?q=%s template. Then you can
- # directly open the ActionPack documentation by typing 'rdoc actionp'. If
- # there are multiple hits for the search term, they are presented as a list
- # with links.
- #
- # Search algorithm aims for an intuitive search:
- # 1. first try to find the gems and documentation folders which name
- # starts with the search term
- # 2. search for entries, that *contain* the search term
- # 3. show all the gems
- #
- # If there is only one search hit, user is immediately redirected to the
- # documentation for the particular gem, otherwise a list with results is
- # shown.
- #
- # === Additional trick - install documentation for ruby core
- #
- # Note: please adjust paths accordingly use for example 'locate yaml.rb' and
- # 'gem environment' to identify directories, that are specific for your
- # local installation
- #
- # 1. install ruby sources
- # cd /usr/src
- # sudo apt-get source ruby
- #
- # 2. generate documentation
- # rdoc -o /usr/lib/ruby/gems/1.8/doc/core/rdoc \
- # /usr/lib/ruby/1.8 ruby1.8-1.8.7.72
- #
- # By typing 'rdoc core' you can now access the core documentation
-
- def rdoc(req, res)
- query = req.query['q']
- show_rdoc_for_pattern("#{query}*", res) && return
- show_rdoc_for_pattern("*#{query}*", res) && return
-
- template = ERB.new RDOC_NO_DOCUMENTATION
-
- res['content-type'] = 'text/html'
- res.body = template.result binding
- end
-
- ##
- # Returns true and prepares http response, if rdoc for the requested gem
- # name pattern was found.
- #
- # The search is based on the file system content, not on the gems metadata.
- # This allows additional documentation folders like 'core' for the ruby core
- # documentation - just put it underneath the main doc folder.
-
- def show_rdoc_for_pattern(pattern, res)
- found_gems = Dir.glob("{#{@gem_dirs.join ','}}/doc/#{pattern}").select {|path|
- File.exist? File.join(path, 'rdoc/index.html')
- }
- case found_gems.length
- when 0
- return false
- when 1
- new_path = File.basename(found_gems[0])
- res.status = 302
- res['Location'] = "/doc_root/#{new_path}/rdoc/index.html"
- return true
- else
- doc_items = []
- found_gems.each do |file_name|
- base_name = File.basename(file_name)
- doc_items << {
- :name => base_name,
- :url => "/doc_root/#{base_name}/rdoc/index.html",
- :summary => ''
- }
- end
-
- template = ERB.new(RDOC_SEARCH_TEMPLATE)
- res['content-type'] = 'text/html'
- result = template.result binding
- res.body = result
- return true
- end
- end
-
- def run
- listen
-
- WEBrick::Daemon.start if @daemon
-
- @server.mount_proc "/Marshal.#{Gem.marshal_version}", method(:Marshal)
- @server.mount_proc "/Marshal.#{Gem.marshal_version}.Z", method(:Marshal)
-
- @server.mount_proc "/specs.#{Gem.marshal_version}", method(:specs)
- @server.mount_proc "/specs.#{Gem.marshal_version}.gz", method(:specs)
-
- @server.mount_proc "/latest_specs.#{Gem.marshal_version}",
- method(:latest_specs)
- @server.mount_proc "/latest_specs.#{Gem.marshal_version}.gz",
- method(:latest_specs)
-
- @server.mount_proc "/quick/", method(:quick)
-
- @server.mount_proc("/gem-server-rdoc-style.css") do |req, res|
- res['content-type'] = 'text/css'
- add_date res
- res.body << RDOC_CSS
- end
-
- @server.mount_proc "/", method(:root)
-
- @server.mount_proc "/rdoc", method(:rdoc)
-
- paths = { "/gems" => "/cache/", "/doc_root" => "/doc/" }
- paths.each do |mount_point, mount_dir|
- @server.mount(mount_point, WEBrick::HTTPServlet::FileHandler,
- File.join(@gem_dirs.first, mount_dir), true)
- end
-
- trap("INT") { @server.shutdown; exit! }
- trap("TERM") { @server.shutdown; exit! }
-
- launch if @launch
-
- @server.start
- end
-
- def specs(req, res)
- Gem::Specification.reset
-
- add_date res
-
- specs = Gem::Specification.sort_by(&:sort_obj).map do |spec|
- platform = spec.original_platform || Gem::Platform::RUBY
- [spec.name, spec.version, platform]
- end
-
- specs = Marshal.dump specs
-
- if req.path =~ /\.gz$/ then
- specs = Gem.gzip specs
- res['content-type'] = 'application/x-gzip'
- else
- res['content-type'] = 'application/octet-stream'
- end
-
- if req.request_method == 'HEAD' then
- res['content-length'] = specs.length
- else
- res.body << specs
- end
- end
-
- def launch
- listeners = @server.listeners.map{|l| l.addr[2] }
-
- # TODO: 0.0.0.0 == any, not localhost.
- host = listeners.any?{|l| l == '0.0.0.0'} ? 'localhost' : listeners.first
-
- say "Launching browser to http://#{host}:#{@port}"
-
- system("#{@launch} http://#{host}:#{@port}")
- end
-end
diff --git a/lib/rubygems/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_index.rb b/lib/rubygems/source_index.rb
deleted file mode 100644
index 1fe92c0a80..0000000000
--- a/lib/rubygems/source_index.rb
+++ /dev/null
@@ -1,406 +0,0 @@
-#--
-# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
-# All rights reserved.
-# See LICENSE.txt for permissions.
-#++
-
-require 'rubygems/specification'
-require 'rubygems/deprecate'
-
-##
-# The SourceIndex object indexes all the gems available from a
-# particular source (e.g. a list of gem directories, or a remote
-# source). A SourceIndex maps a gem full name to a gem
-# specification.
-#
-# NOTE:: The class used to be named Cache, but that became
-# confusing when cached source fetchers where introduced. The
-# constant Gem::Cache is an alias for this class to allow old
-# YAMLized source index objects to load properly.
-
-class Gem::SourceIndex
-
- include Enumerable
-
- attr_reader :gems # :nodoc:
-
- ##
- # Directories to use to refresh this SourceIndex when calling refresh!
-
- attr_accessor :spec_dirs
-
- ##
- # Factory method to construct a source index instance for a given
- # path.
- #
- # deprecated::
- # If supplied, from_installed_gems will act just like
- # +from_gems_in+. This argument is deprecated and is provided
- # just for backwards compatibility, and should not generally
- # be used.
- #
- # return::
- # SourceIndex instance
-
- def self.from_installed_gems(*deprecated)
- if deprecated.empty?
- from_gems_in(*installed_spec_directories)
- else
- warn "NOTE: from_installed_gems(arg) is deprecated. From #{caller.first}"
- from_gems_in(*deprecated) # HACK warn
- end
- end
-
- ##
- # Returns a list of directories from Gem.path that contain specifications.
-
- def self.installed_spec_directories
- # TODO: move to Gem::Utils
- Gem.path.collect { |dir| File.join(dir, "specifications") }
- end
-
- ##
- # Creates a new SourceIndex from the ruby format gem specifications in
- # +spec_dirs+.
-
- def self.from_gems_in(*spec_dirs)
- new spec_dirs
- end
-
- ##
- # Loads a ruby-format specification from +file_name+ and returns the
- # loaded spec.
-
- def self.load_specification(file_name)
- Gem::Deprecate.skip_during do
- Gem::Specification.load Gem::Path.new(file_name)
- end
- end
-
- ##
- # Constructs a source index instance from the provided specifications, which
- # is a Hash of gem full names and Gem::Specifications.
-
- def initialize specs_or_dirs = []
- @gems = {}
- @spec_dirs = nil
-
- case specs_or_dirs
- when Hash then
- specs_or_dirs.each do |full_name, spec|
- add_spec spec
- end
- when Array, String then
- self.spec_dirs = Array(specs_or_dirs)
- refresh!
- else
- arg = specs_or_dirs.inspect
- warn "NOTE: SourceIndex.new(#{arg}) is deprecated; From #{caller.first}."
- end
- end
-
- def all_gems
- gems
- end
-
- def prerelease_gems
- @gems.reject { |name, gem| !gem.version.prerelease? }
- end
-
- def released_gems
- @gems.reject { |name, gem| gem.version.prerelease? }
- end
-
- ##
- # Reconstruct the source index from the specifications in +spec_dirs+.
-
- def load_gems_in(*spec_dirs)
- @gems.clear
-
- spec_dirs.reverse_each do |spec_dir|
- spec_files = Dir[File.join(spec_dir, "*.gemspec")]
-
- spec_files.each do |spec_file|
- gemspec = Gem::Deprecate.skip_during do
- Gem::Specification.load spec_file
- end
- add_spec gemspec if gemspec
- end
- end
-
- self
- end
-
- ##
- # Returns an Array specifications for the latest released versions
- # of each gem in this index.
-
- def latest_specs(include_prerelease=false)
- result = Hash.new { |h,k| h[k] = [] }
- latest = {}
-
- sort.each do |_, spec|
- name = spec.name
- curr_ver = spec.version
- prev_ver = latest.key?(name) ? latest[name].version : nil
-
- next if !include_prerelease && curr_ver.prerelease?
- next unless prev_ver.nil? or curr_ver >= prev_ver or
- latest[name].platform != Gem::Platform::RUBY
-
- if prev_ver.nil? or
- (curr_ver > prev_ver and spec.platform == Gem::Platform::RUBY) then
- result[name].clear
- latest[name] = spec
- end
-
- if spec.platform != Gem::Platform::RUBY then
- result[name].delete_if do |result_spec|
- result_spec.platform == spec.platform
- end
- end
-
- result[name] << spec
- end
-
- result.values.flatten
- end
-
- ##
- # An array including only the prerelease gemspecs
-
- def prerelease_specs
- prerelease_gems.values
- end
-
- ##
- # An array including only the released gemspecs
-
- def released_specs
- released_gems.values
- end
-
- ##
- # Add a gem specification to the source index.
-
- def add_spec(gem_spec, name = gem_spec.full_name)
- # No idea why, but the Indexer wants to insert them using original_name
- # instead of full_name. So we make it an optional arg.
- @gems[name] = gem_spec
- end
-
- ##
- # Add gem specifications to the source index.
-
- def add_specs(*gem_specs)
- Gem::Deprecate.skip_during do
- gem_specs.each do |spec|
- add_spec spec
- end
- end
- end
-
- ##
- # Remove a gem specification named +full_name+.
-
- def remove_spec(full_name)
- @gems.delete full_name
- end
-
- ##
- # Iterate over the specifications in the source index.
-
- def each(&block) # :yields: gem.full_name, gem
- @gems.each(&block)
- end
-
- ##
- # The gem specification given a full gem spec name.
-
- def specification(full_name)
- @gems[full_name]
- end
-
- ##
- # The signature for the source index. Changes in the signature indicate a
- # change in the index.
-
- def index_signature
- require 'digest'
-
- Digest::SHA256.new.hexdigest(@gems.keys.sort.join(',')).to_s
- end
-
- ##
- # The signature for the given gem specification.
-
- def gem_signature(gem_full_name)
- require 'digest'
-
- Digest::SHA256.new.hexdigest(@gems[gem_full_name].to_yaml).to_s
- end
-
- def size
- @gems.size
- end
- alias length size
-
- ##
- # Find a gem by an exact match on the short name.
-
- def find_name(gem_name, requirement = Gem::Requirement.default)
- dep = Gem::Dependency.new gem_name, requirement
-
- Gem::Deprecate.skip_during do
- search dep
- end
- end
-
- ##
- # Search for a gem by Gem::Dependency +gem_pattern+. If +only_platform+
- # is true, only gems matching Gem::Platform.local will be returned. An
- # Array of matching Gem::Specification objects is returned.
- #
- # For backwards compatibility, a String or Regexp pattern may be passed as
- # +gem_pattern+, and a Gem::Requirement for +platform_only+. This
- # behavior is deprecated and will be removed.
-
- def search(gem_pattern, platform_or_requirement = false)
- requirement = nil
- only_platform = false # FIX: WTF is this?!?
-
- # TODO - Remove support and warning for legacy arguments after 2008/11
- unless Gem::Dependency === gem_pattern
- warn "#{Gem.location_of_caller.join ':'}:Warning: Gem::SourceIndex#search support for #{gem_pattern.class} patterns is deprecated, use #find_name"
- end
-
- case gem_pattern
- when Regexp then
- requirement = platform_or_requirement || Gem::Requirement.default
- when Gem::Dependency then
- only_platform = platform_or_requirement
- requirement = gem_pattern.requirement
-
- gem_pattern = if Regexp === gem_pattern.name then
- gem_pattern.name
- elsif gem_pattern.name.empty? then
- //
- else
- /^#{Regexp.escape gem_pattern.name}$/
- end
- else
- requirement = platform_or_requirement || Gem::Requirement.default
- gem_pattern = /#{gem_pattern}/i
- end
-
- unless Gem::Requirement === requirement then
- requirement = Gem::Requirement.create requirement
- end
-
- specs = @gems.values.select do |spec|
- spec.name =~ gem_pattern and
- requirement.satisfied_by? spec.version
- end
-
- if only_platform then
- specs = specs.select do |spec|
- Gem::Platform.match spec.platform
- end
- end
-
- specs.sort_by { |s| s.sort_obj }
- end
-
- ##
- # Replaces the gems in the source index from specifications in the
- # directories this source index was created from. Raises an exception if
- # this source index wasn't created from a directory (via from_gems_in or
- # from_installed_gems, or having spec_dirs set).
-
- def refresh!
- raise 'source index not created from disk' if @spec_dirs.nil?
- load_gems_in(*@spec_dirs)
- end
-
- ##
- # Returns an Array of Gem::Specifications that are not up to date.
-
- def outdated
- outdateds = []
-
- latest_specs.each do |local|
- dependency = Gem::Dependency.new local.name, ">= #{local.version}"
-
- fetcher = Gem::SpecFetcher.fetcher
- remotes = fetcher.find_matching dependency
- remotes = remotes.map { |(_, version, _), _| version }
-
- latest = remotes.sort.last
-
- outdateds << local.name if latest and local.version < latest
- end
-
- outdateds
- end
-
- def ==(other) # :nodoc:
- self.class === other and @gems == other.gems
- end
-
- def dump
- Marshal.dump(self)
- end
-end
-
-# :stopdoc:
-module Gem
-
- ##
- # Cache is an alias for SourceIndex to allow older YAMLized source index
- # objects to load properly.
-
- Cache = SourceIndex
-
-end
-
-class Gem::SourceIndex
- extend Gem::Deprecate
-
- deprecate :all_gems, :none, 2011, 10
-
- deprecate :==, :none, 2011, 11 # noisy
- deprecate :add_specs, :none, 2011, 11 # noisy
- deprecate :each, :none, 2011, 11
- deprecate :gems, :none, 2011, 11
- deprecate :load_gems_in, :none, 2011, 11
- deprecate :refresh!, :none, 2011, 11
- deprecate :spec_dirs=, "Specification.dirs=", 2011, 11 # noisy
- deprecate :add_spec, "Specification.add_spec", 2011, 11
- deprecate :find_name, "Specification.find_by_name", 2011, 11
- deprecate :gem_signature, :none, 2011, 11
- deprecate :index_signature, :none, 2011, 11
- deprecate :initialize, :none, 2011, 11
- deprecate :latest_specs, "Specification.latest_specs", 2011, 11
- deprecate :length, "Specification.all.length", 2011, 11
- deprecate :outdated, :none, 2011, 11
- deprecate :prerelease_gems, :none, 2011, 11
- deprecate :prerelease_specs, :none, 2011, 11
- deprecate :released_gems, :none, 2011, 11
- deprecate :released_specs, :none, 2011, 11
- deprecate :remove_spec, "Specification.remove_spec", 2011, 11
- deprecate :search, :none, 2011, 11
- deprecate :size, "Specification.all.size", 2011, 11
- deprecate :spec_dirs, "Specification.dirs", 2011, 11
- deprecate :specification, "Specification.find", 2011, 11
-
- class << self
- extend Gem::Deprecate
-
- deprecate :from_gems_in, :none, 2011, 10
- deprecate :from_installed_gems, :none, 2011, 10
- deprecate :installed_spec_directories, "Specification.dirs", 2011, 11
- deprecate :load_specification, :none, 2011, 10
- end
-end
-
-# :startdoc:
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
index ad61267cae..835dedf948 100644
--- a/lib/rubygems/spec_fetcher.rb
+++ b/lib/rubygems/spec_fetcher.rb
@@ -1,31 +1,27 @@
-require 'rubygems/remote_fetcher'
-require 'rubygems/user_interaction'
-require 'rubygems/errors'
-require 'rubygems/text'
+# 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
- FILES = {
- :all => 'specs',
- :latest => 'latest_specs',
- :prerelease => 'prerelease_specs',
- }
-
##
- # The SpecFetcher cache dir.
+ # Cache of latest specs
- attr_reader :dir # :nodoc:
+ attr_reader :latest_specs # :nodoc:
##
- # Cache of latest specs
+ # Sources for this SpecFetcher
- attr_reader :latest_specs # :nodoc:
+ attr_reader :sources # :nodoc:
##
# Cache of all released specs
@@ -39,6 +35,10 @@ class Gem::SpecFetcher
@fetcher = nil
+ ##
+ # Default fetcher instance. Use this instead of ::new to reduce object
+ # allocation.
+
def self.fetcher
@fetcher ||= new
end
@@ -47,247 +47,244 @@ class Gem::SpecFetcher
@fetcher = fetcher
end
- def initialize
- require 'fileutils'
+ ##
+ # 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.
- @dir = File.join Gem.user_home, '.gem', 'specs'
- @update_cache = File.stat(Gem.user_home).uid == Process.uid
+ 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,
- :all => @specs
+ latest: @latest_specs,
+ prerelease: @prerelease_specs,
+ released: @specs,
}
@fetcher = Gem::RemoteFetcher.fetcher
end
##
- # Returns the local directory to write +uri+ to.
-
- def cache_dir(uri)
- # Correct for windows paths
- escaped_path = uri.path.sub(/^\/([a-z]):\//i, '/\\1-/')
- File.join @dir, "#{uri.host}%#{uri.port}", File.dirname(escaped_path)
- end
-
- ##
- # Fetch specs matching +dependency+. If +all+ is true, all matching
- # (released) versions are returned. If +matching_platform+ is
- # false, all platforms are returned. If +prerelease+ is true,
- # prerelease versions are included.
-
- def fetch_with_errors(dependency,
- all = false,
- matching_platform = true,
- prerelease = false)
-
- specs_and_sources, errors = find_matching_with_errors(dependency,
- all,
- matching_platform,
- prerelease)
-
- ss = specs_and_sources.map do |spec_tuple, source_uri|
- [fetch_spec(spec_tuple, URI.parse(source_uri)), source_uri]
- end
+ #
+ # Find and fetch gem name tuples that match +dependency+.
+ #
+ # If +matching_platform+ is false, gems for all platforms are returned.
- return [ss, errors]
- end
-
- def fetch(*args)
- fetch_with_errors(*args).first
- end
-
- def fetch_spec(spec, source_uri)
- source_uri = URI.parse source_uri if String === source_uri
- spec = spec - [nil, 'ruby', '']
- spec_file_name = "#{spec.join '-'}.gemspec"
+ def search_for_dependency(dependency, matching_platform = true)
+ found = {}
- uri = source_uri + "#{Gem::MARSHAL_SPEC_DIR}#{spec_file_name}"
+ rejected_specs = {}
- cache_dir = cache_dir uri
+ list, errors = available_specs(dependency.identity)
- local_spec = File.join cache_dir, spec_file_name
+ 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
- if File.exist? local_spec then
- spec = Gem.read_binary local_spec
- else
- uri.path << '.rz'
+ 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
- spec = @fetcher.fetch_path uri
- spec = Gem.inflate spec
+ errors += rejected_specs.values
- if @update_cache then
- FileUtils.mkdir_p cache_dir
+ tuples = []
- open local_spec, 'wb' do |io|
- io.write spec
- end
+ found.each do |source, specs|
+ specs.each do |s|
+ tuples << [s, source]
end
end
- # TODO: Investigate setting Gem::Specification#loaded_from to a URI
- Marshal.load spec
+ tuples = tuples.sort_by {|x| x[0].version }
+
+ [tuples, errors]
end
##
- # Find spec names that match +dependency+. If +all+ is true, all
- # matching released versions are returned. If +matching_platform+
- # is false, gems for all platforms are returned.
-
- def find_matching_with_errors(dependency,
- all = false,
- matching_platform = true,
- prerelease = false)
- found = {}
+ # Return all gem name tuples who's names match +obj+
- rejected_specs = {}
+ def detect(type = :complete)
+ tuples = []
- list(all, prerelease).each do |source_uri, specs|
- found[source_uri] = specs.select do |spec_name, version, spec_platform|
- if dependency.match?(spec_name, version)
- if matching_platform and !Gem::Platform.match(spec_platform)
- pm = (rejected_specs[dependency] ||= Gem::PlatformMismatch.new(spec_name, version))
- pm.add_platform spec_platform
- false
- else
- true
- end
+ list, _ = available_specs(type)
+ list.each do |source, specs|
+ specs.each do |tup|
+ if yield(tup)
+ tuples << [tup, source]
end
end
end
- errors = rejected_specs.values
-
- specs_and_sources = []
+ tuples
+ end
- found.each do |source_uri, specs|
- uri_str = source_uri.to_s
- specs_and_sources.concat(specs.map { |spec| [spec, uri_str] })
+ ##
+ # 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_and_sources, errors]
- end
-
- def find_matching(*args)
- find_matching_with_errors(*args).first
+ [specs, errors]
end
##
- # Suggests a gem based on the supplied +gem_name+. Returns a string
- # of the gem name if an approximate match can be found or nil
- # otherwise. NOTE: for performance reasons only gems which exactly
- # match the first character of +gem_name+ are considered.
+ # Suggests gems based on the supplied +gem_name+. Returns an array of
+ # alternative gem names.
- def suggest_gems_from_name gem_name
- gem_name = gem_name.downcase
- max = gem_name.size / 2
- specs = list.values.flatten 1
+ def suggest_gems_from_name(gem_name, type = :latest, num_results = 5)
+ gem_name = gem_name.downcase.tr("_-", "")
- matches = specs.map { |name, version, platform|
- next unless Gem::Platform.match platform
+ # 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
- distance = levenshtein_distance gem_name, name.downcase
+ max = gem_name.size / 2
+ names = available_specs(type).first.values.flatten(1)
- next if distance >= max
+ min_length = gem_name.length - max
+ max_length = gem_name.length + max
- return [name] if distance == 0
+ gem_name_with_postfix = "#{gem_name}ruby"
+ gem_name_with_prefix = "ruby#{gem_name}"
- [name, distance]
- }.compact
+ 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?
- matches = matches.uniq.sort_by { |name, dist| dist }
+ # 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
- matches.first(5).map { |name, dist| name }
- end
+ # The candidate name, normalized the same as gem_name.
+ normalized_name = n.name.downcase
+ normalized_name.tr!("_-", "")
- ##
- # Returns a list of gems available for each source in Gem::sources. If
- # +all+ is true, all released versions are returned instead of only latest
- # versions. If +prerelease+ is true, include prerelease versions.
-
- def list(all = false, prerelease = false)
- # TODO: make type the only argument
- type = if all
- :all
- elsif prerelease
- :prerelease
- else
- :latest
- end
-
- list = {}
- file = FILES[type]
- cache = @caches[type]
-
- Gem.sources.each do |source_uri|
- source_uri = URI.parse source_uri
-
- unless cache.include? source_uri
- cache[source_uri] = load_specs source_uri, file
- end
+ # 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
- list[source_uri] = cache[source_uri]
- end
+ # 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 type == :all
- list.values.map do |gems|
- gems.reject! { |g| !g[1] || g[1].prerelease? }
- end
- end
-
- list
- end
-
- ##
- # Loads specs in +file+, fetching from +source_uri+ if the on-disk cache is
- # out of date.
+ # 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
- def load_specs(source_uri, file)
- file_name = "#{file}.#{Gem.marshal_version}"
- spec_path = source_uri + "#{file_name}.gz"
- cache_dir = cache_dir spec_path
- local_file = File.join(cache_dir, file_name)
- loaded = false
+ distance = levenshtein_distance gem_name, normalized_name
- if File.exist? local_file then
- spec_dump =
- @fetcher.fetch_path(spec_path, File.mtime(local_file)) rescue nil
+ # Skip current candidate, if the edit distance is greater than allowed.
+ next if distance >= max
- loaded = true if spec_dump
+ # If all else fails, return the name and the calculated distance.
+ [n.name, distance]
+ end
- spec_dump ||= Gem.read_binary local_file
+ matches = if matches.empty? && type != :prerelease
+ suggest_gems_from_name gem_name, :prerelease
else
- spec_dump = @fetcher.fetch_path spec_path
- loaded = true
+ matches.uniq.sort_by {|_name, dist| dist }
end
- specs = begin
- Marshal.load spec_dump
- rescue ArgumentError
- spec_dump = @fetcher.fetch_path spec_path
- loaded = true
-
- Marshal.load spec_dump
- end
-
- if loaded and @update_cache then
- begin
- FileUtils.mkdir_p cache_dir
+ matches.map {|name, _dist| name }.uniq.first(num_results)
+ end
- open local_file, 'wb' do |io|
- io << spec_dump
- end
- rescue
+ ##
+ # 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
- specs
+ [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
index 97db19e69a..51729d755b 100644
--- a/lib/rubygems/specification.rb
+++ b/lib/rubygems/specification.rb
@@ -1,35 +1,56 @@
+# frozen_string_literal: true
+
+#
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/version'
-require 'rubygems/requirement'
-require 'rubygems/platform'
-require "rubygems/deprecate"
+require_relative "basic_specification"
+require_relative "stub_specification"
+require_relative "platform"
+require_relative "specification_record"
-# :stopdoc:
-class Date; end # for ruby_code if date.rb wasn't required
-# :startdoc:
+require "rbconfig"
##
-# The Specification class contains the metadata for a Gem. Typically
+# The Specification class contains the information for a gem. Typically
# defined in a .gemspec file or a Rakefile, and looks like this:
#
-# spec = Gem::Specification.new do |s|
-# s.name = 'example'
-# s.version = '1.0'
-# s.summary = 'Example gem specification'
-# ...
+# 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
#
-# For a great way to package gems, use Hoe.
-class Gem::Specification
+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 the version number of a specification that does not specify one
+ # The version number of a specification that does not specify one
# (i.e. RubyGems 0.7 or earlier).
NONEXISTENT_SPECIFICATION_VERSION = -1
@@ -49,20 +70,51 @@ class Gem::Specification
# 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 = 3
+ 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
- # :stopdoc:
+ today = Time.now.utc
+ TODAY = Time.utc(today.year, today.month, today.day) # :nodoc:
- # version => # of fields
- MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16, 3 => 17 }
+ @load_cache = {} # :nodoc:
+ @load_cache_mutex = Thread::Mutex.new
- today = Time.now.utc
- TODAY = Time.utc(today.year, today.month, today.day)
+ VALID_NAME_PATTERN = /\A[a-zA-Z0-9\.\-\_]+\z/ # :nodoc:
# :startdoc:
@@ -81,97 +133,300 @@ class Gem::Specification
# Map of attribute names to default values.
@@default_value = {
- :authors => [],
- :autorequire => nil,
- :bindir => 'bin',
- :cert_chain => [],
- :date => TODAY,
- :dependencies => [],
- :description => nil,
- :email => nil,
- :executables => [],
- :extensions => [],
- :extra_rdoc_files => [],
- :files => [],
- :homepage => nil,
- :licenses => [],
- :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 => [],
- :rubyforge_project => nil,
- :rubygems_version => Gem::VERSION,
- :signing_key => nil,
- :specification_version => CURRENT_SPECIFICATION_VERSION,
- :summary => nil,
- :test_files => [],
- :version => nil,
- }
-
- @@attributes = @@default_value.keys.sort_by { |s| s.to_s }
- @@array_attributes = @@default_value.reject { |k,v| v != [] }.keys
- @@nil_attributes, @@non_nil_attributes = @@default_value.keys.partition { |k|
+ 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
+ # This gem's name.
+ #
+ # Usage:
+ #
+ # spec.name = 'rake'
attr_accessor :name
##
- # This gem's version
+ # 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
##
- # Paths in the gem to add to $LOAD_PATH when this gem is activated.
+ # A short summary of this gem's description. Displayed in <tt>gem list -d</tt>.
+ #
+ # The #description should be more detailed than the summary.
#
- # The default ['lib'] is typically sufficient.
+ # Usage:
+ #
+ # spec.summary = "This is a small summary of my gem"
- attr_accessor :require_paths
+ attr_reader :summary
##
- # The version of RubyGems used to create this gem.
+ # Files included in this gem. You cannot append to this accessor, you must
+ # assign to it.
#
- # Do not set this, it is set automatically when the gem is packaged.
+ # 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" }
- attr_accessor :rubygems_version
+ 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
##
- # The Gem::Specification version of this gemspec.
+ # A list of authors for this gem.
#
- # Do not set this, it is set automatically when the gem is packaged.
+ # Alternatively, a single author can be specified by assigning a string to
+ # +spec.author+
+ #
+ # Usage:
+ #
+ # spec.authors = ['John Jones', 'Mary Smith']
- attr_accessor :specification_version
+ 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 short summary of this gem's description. Displayed in `gem list -d`.
+ # 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.
#
- # The description should be more detailed than the summary. For example,
- # you might wish to copy the entire README into the description.
+ # 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 :summary
+ 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
##
- # Autorequire was used by old RubyGems to automatically require a file.
+ # Singular (alternative) writer for #authors
#
- # Deprecated: It is neither supported nor functional.
+ # Usage:
+ #
+ # spec.author = 'John Jones'
- attr_accessor :autorequire
+ def author=(o)
+ self.authors = [o]
+ end
##
- # The path in the gem for executable scripts. Usually 'bin'
+ # The path in the gem for executable scripts. Usually 'exe'
+ #
+ # Usage:
+ #
+ # spec.bindir = 'exe'
attr_accessor :bindir
@@ -182,178 +437,452 @@ class Gem::Specification
attr_accessor :cert_chain
##
- # A long description of this gem
+ # A message that gets displayed after the gem is installed.
+ #
+ # Usage:
+ #
+ # spec.post_install_message = "Thanks for installing!"
- attr_reader :description
+ attr_accessor :post_install_message
##
- # Sets the default executable for this gem.
+ # 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
#
- # Deprecated: You must now specify the executable name to Gem.bin_path.
+ # 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
- attr_writer :default_executable
+ # 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
##
- # A contact email for this gem
+ # 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.
#
- # If you are providing multiple authors and multiple emails they should be
- # in the same order such that:
+ # The default value is <code>"lib"</code>
#
- # Hash[*spec.authors.zip(spec.emails).flatten]
+ # Usage:
#
- # Gives a hash of author name to email address.
+ # # If all library files are in the root directory...
+ # spec.require_paths = ['.']
- attr_accessor :email
+ def require_paths=(val)
+ @require_paths = Array(val)
+ end
##
- # The URL of this gem's home page
+ # The RubyGems version required by this gem
- attr_accessor :homepage
+ attr_reader :required_rubygems_version
##
- # True when this gemspec has been activated. This attribute is not persisted.
+ # The key used to sign this gem. See Gem::Security for details.
- attr_accessor :loaded # :nodoc:
+ attr_accessor :signing_key
- alias :loaded? :loaded # :nodoc:
+ ##
+ # 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
##
- # True when this gemspec has been activated. This attribute is not persisted.
+ # Adds a runtime dependency named +gem+ with +requirements+ to this gem.
+ #
+ # Usage:
+ #
+ # spec.add_dependency 'example', '~> 1.1', '>= 1.1.4'
- attr_accessor :activated
+ def add_dependency(gem, *requirements)
+ if requirements.uniq.size != requirements.size
+ warn "WARNING: duplicated #{gem} dependency #{requirements}"
+ end
- alias :activated? :activated
+ add_dependency_with_type(gem, :runtime, requirements)
+ end
##
- # Path this gemspec was loaded from. This attribute is not persisted.
+ # 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'
- attr_reader :loaded_from
+ def executables
+ @executables ||= []
+ end
##
- # Allows deinstallation of gems with legacy platforms.
+ # 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.
- attr_writer :original_platform # :nodoc:
+ def extensions
+ @extensions ||= []
+ end
##
- # A message that gets displayed after the gem is installed
+ # 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']
- attr_accessor :post_install_message
+ def extra_rdoc_files
+ @extra_rdoc_files ||= []
+ end
##
- # The version of ruby required by this gem
+ # 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.
- attr_reader :required_ruby_version
+ 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
- attr_reader :required_rubygems_version
+ def required_rubygems_version=(req)
+ @required_rubygems_version = Gem::Requirement.create req
+ end
##
- # The rubyforge project this gem lives under. i.e. RubyGems'
- # rubyforge_project is "rubygems".
+ # 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'
- attr_accessor :rubyforge_project
+ def requirements
+ @requirements ||= []
+ end
##
- # The key used to sign this gem. See Gem::Security for details.
+ # 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']
- attr_accessor :signing_key
+ 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:
- unless defined?(@@all) && @@all then
- specs = []
-
- self.dirs.reverse_each { |dir|
- Dir[File.join(dir, "*.gemspec")].each { |path|
- spec = Gem::Specification.load path.untaint
- # #load returns nil if the spec is bad, so we just ignore
- # it at this stage
- specs << spec if spec
- }
- }
-
- @@all = specs
- _resort!
+ specification_record.all
+ end
+
+ def self.clear_load_cache # :nodoc:
+ @load_cache_mutex.synchronize do
+ @load_cache.clear
end
- @@all
end
+ private_class_method :clear_load_cache
- def self._resort! # :nodoc:
- @@all.sort! { |a, b|
- names = a.name <=> b.name
- next names if names.nonzero?
- b.version <=> a.version
- }
+ 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
##
- # Adds +spec+ to the known specifications, keeping the collection
- # properly sorted.
+ # Returns a Gem::StubSpecification for every installed gem
- def self.add_spec spec
- # TODO: find all extraneous adds
- # puts
- # p :add_spec => [spec.full_name, caller.reject { |s| s =~ /minitest/ }]
+ def self.stubs
+ specification_record.stubs
+ end
- # TODO: flush the rest of the crap from the tests
- # raise "no dupes #{spec.full_name} in #{all_names.inspect}" if
- # _all.include? spec
+ ##
+ # Returns a Gem::StubSpecification for default gems
- raise "nil spec!" unless spec # TODO: remove once we're happy with tests
+ 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
- return if _all.include? spec
+ ##
+ # Returns a Gem::StubSpecification for installed gem named +name+
+ # only returns stubs that match Gem.platforms
- _all << spec
- _resort!
+ def self.stubs_for(name)
+ specification_record.stubs_for(name)
end
##
- # Adds multiple specs to the known specifications.
+ # 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.add_specs *specs
- raise "nil spec!" if specs.any?(&:nil?) # TODO: remove once we're happy
+ 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
- # TODO: this is much more efficient, but we need the extra checks for now
- # _all.concat specs
- # _resort!
+ ##
+ # Loads the default specifications. It should be called only once.
- specs.each do |spec| # TODO: slow
- add_spec spec
+ 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.first}" unless
+ warn "NOTE: Specification.all called from #{caller(1, 1).first}" unless
Gem::Deprecate.skip
_all
end
##
- # Sets the known specs to +specs+. Not guaranteed to work for you in
- # the future. Use at your own risk. Caveat emptor. Doomy doom doom.
- # Etc etc.
- #
- #--
- # Makes +specs+ the known specs
- # Listen, time is a river
- # Winter comes, code breaks
- #
- # -- wilsonb
+ # Sets the known specs to +specs+.
- def self.all= specs
- @@all = specs
+ def self.all=(specs)
+ specification_record.all = specs
end
##
# Return full names of all specs in sorted order.
def self.all_names
- self._all.map(&:full_name)
+ specification_record.all_names
end
##
@@ -378,23 +907,17 @@ class Gem::Specification
# Return the directories that Specification uses to find specs.
def self.dirs
- @@dirs ||= Gem.path.collect { |dir|
- File.join dir, "specifications"
- }
+ @@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
- # TODO: find extra calls to dir=
- # warn "NOTE: dirs= called from #{caller.first} for #{dirs.inspect}"
+ def self.dirs=(dirs)
+ reset
- self.reset
-
- # ugh
- @@dirs = Array(dirs).map { |dir| File.join dir, "specifications" }
+ @@dirs = Gem::SpecificationRecord.dirs_from(Array(dirs))
end
extend Enumerable
@@ -403,78 +926,102 @@ class Gem::Specification
# Enumerate every known spec. See ::dirs= and ::add_spec to set the list of
# specs.
- def self.each
- return enum_for(:each) unless block_given?
-
- self._all.each do |x|
- yield x
- end
+ def self.each(&block)
+ specification_record.each(&block)
end
##
# Returns every spec that matches +name+ and optional +requirements+.
- def self.find_all_by_name name, *requirements
- requirements = Gem::Requirement.default if requirements.empty?
+ def self.find_all_by_name(name, *requirements)
+ specification_record.find_all_by_name(name, *requirements)
+ end
- # TODO: maybe try: find_all { |s| spec === dep }
+ ##
+ # Returns every spec that has the given +full_name+
- Gem::Dependency.new(name, *requirements).matching_specs
+ 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
+ def self.find_by_name(name, *requirements)
requirements = Gem::Requirement.default if requirements.empty?
- # TODO: maybe try: find { |s| spec === dep }
-
Gem::Dependency.new(name, *requirements).to_spec
end
##
+ # Find the best specification matching a +full_name+.
+ def self.find_by_full_name(full_name)
+ stubs.find {|s| s.full_name == full_name }&.to_spec
+ end
+
+ ##
# Return the best specification that contains the file matching +path+.
- def self.find_by_path path
- self.find { |spec|
- spec.contains_requirable_file? path
- }
+ def self.find_by_path(path)
+ specification_record.find_by_path(path)
end
##
- # Return currently unresolved specs that contain the file matching +path+.
+ # 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
- def self.find_in_unresolved path
- # TODO: do we need these?? Kill it
- specs = Gem.unresolved_deps.values.map { |dep| dep.to_specs }.flatten
+ ##
+ # Return the best specification that contains the file matching +path+
+ # amongst the specs that are not activated.
- specs.find_all { |spec| spec.contains_requirable_file? path }
+ def self.find_inactive_by_path(path)
+ specification_record.find_inactive_by_path(path)
end
##
- # Search through all unresolved deps and sub-dependencies and return
- # specs that contain the file matching +path+.
+ # Return the best specification that contains the file matching +path+, among
+ # those already activated.
- def self.find_in_unresolved_tree path
- specs = Gem.unresolved_deps.values.map { |dep| dep.to_specs }.flatten
+ def self.find_active_stub_by_path(path)
+ specification_record.find_active_stub_by_path(path)
+ end
- specs.reverse_each do |spec|
- trails = []
- spec.traverse do |from_spec, dep, to_spec, trail|
- next unless to_spec.conflicts.empty?
- trails << trail if to_spec.contains_requirable_file? path
- end
+ ##
+ # Return currently unresolved specs that contain the file matching +path+.
- next if trails.empty?
+ def self.find_in_unresolved(path)
+ unresolved_specs.find_all {|spec| spec.contains_requirable_file? path }
+ end
- return trails.map(&:reverse).sort.first.reverse
+ ##
+ # 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
@@ -484,23 +1031,22 @@ class Gem::Specification
# +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 = YAML.load input
+ spec = Gem::SafeYAML.safe_load input
- if spec && spec.class == FalseClass then
+ if spec && spec.class == FalseClass
raise Gem::EndOfYAMLException
end
- unless Gem::Specification === spec then
+ unless Gem::Specification === spec
raise Gem::Exception, "YAML data doesn't evaluate to gem specification"
end
- unless (spec.instance_variables.include? '@specification_version' or
- spec.instance_variables.include? :@specification_version) and
- spec.instance_variable_get :@specification_version
- spec.instance_variable_set :@specification_version,
- NONEXISTENT_SPECIFICATION_VERSION
- end
+ spec.specification_version ||= NONEXISTENT_SPECIFICATION_VERSION
+ spec.reset_nil_attributes_to_default
+ spec.flatten_require_paths
spec
end
@@ -509,51 +1055,64 @@ class Gem::Specification
# Return the latest specs, optionally including prerelease specs if
# +prerelease+ is true.
- def self.latest_specs prerelease = false
- result = Hash.new { |h,k| h[k] = {} }
- native = {}
+ def self.latest_specs(prerelease = false)
+ specification_record.latest_specs(prerelease)
+ end
- Gem::Specification._all.reverse_each do |spec|
- next if spec.version.prerelease? unless prerelease
+ ##
+ # Return the latest installed spec for gem +name+.
- native[spec.name] = spec.version if spec.platform == Gem::Platform::RUBY
- result[spec.name][spec.platform] = spec
+ 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.map(&:last).map(&:values).flatten.reject { |spec|
- minimum = native[spec.name]
- minimum && spec.version < minimum
- }
+ result.flat_map(&:last).sort_by(&:name)
end
##
# Loads Ruby format gemspec from +file+.
- def self.load file
- return unless file && File.file?(file)
+ def self.load(file)
+ return unless file
- file = file.dup.untaint
+ spec = @load_cache_mutex.synchronize { @load_cache[file] }
+ return spec if spec
- code = if defined? Encoding
- File.read file, :encoding => "UTF-8"
- else
- File.read file
- end
+ return unless File.file?(file)
- code.untaint
+ code = Gem.open_file(file, "r:UTF-8:-", &:read)
begin
spec = eval code, binding, file
if Gem::Specification === spec
- spec.loaded_from = file.to_s
+ 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, Exception => e
+ rescue SyntaxError, StandardError => e
warn "Invalid gemspec in [#{file}]: #{e}"
end
@@ -572,7 +1131,8 @@ class Gem::Specification
def self.normalize_yaml_input(input)
result = input.respond_to?(:read) ? input.read : input
- result = "--- " + result unless result =~ /\A--- /
+ 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
@@ -581,35 +1141,43 @@ class Gem::Specification
end
##
- # Return a list of all outdated specifications. This method is HEAVY
+ # 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
- outdateds = []
+ 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.each do |local|
- dependency = Gem::Dependency.new local.name, ">= #{local.version}"
- remotes = fetcher.find_matching dependency
- remotes = remotes.map { |(_, version, _), _| version }
- latest = remotes.sort.last
+ latest_specs(true).each do |local_spec|
+ dependency =
+ Gem::Dependency.new local_spec.name, ">= #{local_spec.version}"
- outdateds << local.name if latest and local.version < latest
- end
+ remotes, = fetcher.search_for_dependency dependency
+ remotes = remotes.map {|n, _| n.version }
- outdateds
- end
+ latest_remote = remotes.sort.last
- ##
- # Removes +spec+ from the known specs.
+ yield [local_spec, latest_remote] if
+ latest_remote && local_spec.version < latest_remote
+ end
- def self.remove_spec spec
- # TODO: beat on the tests
- raise "wtf: #{spec.full_name} not in #{all_names.inspect}" unless
- _all.include? spec
- _all.delete spec
+ nil
end
##
@@ -632,34 +1200,113 @@ class Gem::Specification
def self.reset
@@dirs = nil
- # from = caller.first(10).reject { |s| s =~ /minitest/ }
- # warn ""
- # warn "NOTE: Specification.reset from #{from.inspect}"
- Gem.pre_reset_hooks.each { |hook| hook.call }
- @@all = nil
- Gem.post_reset_hooks.each { |hook| hook.call }
+ 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)
- array = Marshal.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 then
- spec.instance_variable_set :@specification_version,
- current_version
- MARSHAL_FIELDS[current_version]
- else
- MARSHAL_FIELDS[spec.specification_version]
- end
+ 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 then
+ if array.size < field_count
raise TypeError, "invalid Gem::Specification format #{array.inspect}"
end
@@ -667,21 +1314,20 @@ class Gem::Specification
# spec version
spec.instance_variable_set :@name, array[2]
spec.instance_variable_set :@version, array[3]
- spec.instance_variable_set :@date, array[4]
+ 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.instance_variable_set :@original_platform, array[8]
+ spec.platform = array[8]
spec.instance_variable_set :@dependencies, array[9]
- spec.instance_variable_set :@rubyforge_project, array[10]
+ # offset due to rubyforge_project removal
spec.instance_variable_set :@email, array[11]
spec.instance_variable_set :@authors, array[12]
spec.instance_variable_set :@description, array[13]
spec.instance_variable_set :@homepage, array[14]
- spec.instance_variable_set :@has_rdoc, array[15]
- spec.instance_variable_set :@new_platform, array[16]
- spec.instance_variable_set :@platform, array[16].to_s
- spec.instance_variable_set :@license, array[17]
+ # offset due to has_rdoc removal
+ spec.instance_variable_set :@licenses, array[17]
+ spec.instance_variable_set :@metadata, array[18]
spec.instance_variable_set :@loaded, false
spec.instance_variable_set :@activated, false
@@ -692,7 +1338,7 @@ class Gem::Specification
sort_obj <=> other.sort_obj
end
- def == other # :nodoc:
+ def ==(other) # :nodoc:
self.class === other &&
name == other.name &&
version == other.version &&
@@ -717,14 +1363,15 @@ class Gem::Specification
@required_rubygems_version,
@original_platform,
@dependencies,
- @rubyforge_project,
+ "", # rubyforge_project
@email,
@authors,
@description,
@homepage,
true, # has_rdoc
@new_platform,
- @licenses
+ @licenses,
+ @metadata,
]
end
@@ -735,40 +1382,94 @@ class Gem::Specification
# there are conflicts upon activation.
def activate
- raise_if_conflicts
+ other = Gem.loaded_specs[name]
+ if other
+ check_version_conflict other
+ return false
+ end
- return false if Gem.loaded_specs[self.name]
+ raise_if_conflicts
activate_dependencies
add_self_to_load_path
- Gem.loaded_specs[self.name] = self
+ Gem.loaded_specs[name] = self
@activated = true
@loaded = true
- return true
+ true
end
##
# Activate all unambiguously resolved runtime dependencies of this
- # spec. Add any ambigous dependencies to the unresolved list to be
+ # spec. Add any ambiguous dependencies to the unresolved list to be
# resolved later, as needed.
def activate_dependencies
- self.runtime_dependencies.each do |spec_dep|
- # TODO: check for conflicts! not just name!
- next if Gem.loaded_specs.include? spec_dep.name
- specs = spec_dep.to_specs
+ 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
- if specs.size == 1 then
+ 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
- Gem.unresolved_deps[name] = Gem.unresolved_deps[name].merge spec_dep
+ unresolved[name] = unresolved[name].merge spec_dep
end
end
- Gem.unresolved_deps.delete self.name
+ 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
##
@@ -778,13 +1479,13 @@ class Gem::Specification
def add_bindir(executables)
return nil if executables.nil?
- if @bindir then
- Array(executables).map { |e| File.join(@bindir, e) }
+ if @bindir
+ Array(executables).map {|e| File.join(@bindir, e) }
else
executables
end
- rescue
- return nil
+ rescue StandardError
+ nil
end
##
@@ -792,17 +1493,16 @@ class Gem::Specification
# +requirements+. Valid types are currently <tt>:runtime</tt> and
# <tt>:development</tt>.
- def add_dependency_with_type(dependency, type, *requirements)
- requirements = if requirements.empty? then
- Gem::Requirement.default
- else
- requirements.flatten
- end
+ 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?(:version_requirements)
-
- dependency = Gem::Dependency.new(dependency, requirements, type)
+ dependency.respond_to?(:requirement)
+ dependency = Gem::Dependency.new(dependency.to_s, requirements, type)
end
dependencies << dependency
@@ -810,111 +1510,113 @@ class Gem::Specification
private :add_dependency_with_type
- ##
- # Adds a development dependency named +gem+ with +requirements+ to this
- # Gem. For example:
- #
- # 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.
- # For example:
- #
- # spec.add_runtime_dependency 'example', '~> 1.1', '>= 1.1.4'
-
- def add_runtime_dependency(gem, *requirements)
- add_dependency_with_type(gem, :runtime, *requirements)
- end
-
- alias add_dependency add_runtime_dependency
+ alias_method :add_runtime_dependency, :add_dependency
##
# Adds this spec's require paths to LOAD_PATH, in the proper location.
def add_self_to_load_path
- paths = require_paths.map do |path|
- File.join full_gem_path, path
- end
+ return if default_gem?
- # gem directories must come after -I and ENV['RUBYLIB']
- insert_index = Gem.load_path_insert_index
+ paths = full_require_paths
- if insert_index then
- # gem directories must come after -I and ENV['RUBYLIB']
- $LOAD_PATH.insert(insert_index, *paths)
- else
- # we are probably testing in core, -I and RUBYLIB don't apply
- $LOAD_PATH.unshift(*paths)
- end
+ Gem.add_to_load_path(*paths)
end
##
- # Singular reader for #authors
+ # Singular reader for #authors. Returns the first author in the list
def author
- val = authors and val.first
+ (val = authors) && val.first
end
##
- # Singular writer for #authors
+ # The list of author names who wrote this gem.
+ #
+ # spec.authors = ['Chad Fowler', 'Jim Weirich', 'Rich Kilmer']
- def author= o
- self.authors = [o]
+ def authors
+ @authors ||= []
end
##
- # The list of author names who wrote this gem.
- #
- # If you are providing multiple authors and multiple emails they should be
- # in the same order such that:
- #
- # Hash[*spec.authors.zip(spec.emails).flatten]
+ # Returns the full path to installed gem's bin directory.
#
- # Gives a hash of author name to email address.
+ # NOTE: do not confuse this with +bindir+, which is just 'bin', not
+ # a full path.
- def authors
- @authors ||= []
+ def bin_dir
+ @bin_dir ||= File.join gem_dir, bindir
end
##
- # Sets the list of authors, ensuring it is an array.
+ # Returns the full path to an executable named +name+ in this gem.
- def authors= value
- @authors = Array(value).flatten.grep(String)
+ def bin_file(name)
+ File.join bin_dir, name
end
##
- # Returns the full path to the base gem directory.
- #
- # eg: /usr/local/lib/ruby/gems/1.8
+ # Returns the build_args used to install the gem
- def base_dir
- return Gem.dir unless loaded_from
- @base_dir ||= File.dirname File.dirname loaded_from
+ 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
##
- # 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.
+ # Builds extensions for this platform if the gem has extensions listed and
+ # the gem.build_complete file is missing.
- def bin_dir
- @bin_dir ||= File.join gem_dir, bindir # TODO: this is unfortunate
+ 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 an executable named +name+ in this gem.
+ # Returns the full path to the build info directory
- def bin_file name
- File.join bin_dir, name
+ 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
##
@@ -922,102 +1624,106 @@ class Gem::Specification
# spec's cached gem.
def cache_dir
- @cache_dir ||= File.join base_dir, "cache"
+ File.join base_dir, "cache"
end
##
# Returns the full path to the cached gem for this spec.
def cache_file
- @cache_file ||= File.join cache_dir, "#{full_name}.gem"
+ File.join cache_dir, "#{full_name}.gem"
end
- alias :cache_gem :cache_file
-
##
# Return any possible conflicts against the currently loaded specs.
def conflicts
conflicts = {}
- Gem.loaded_specs.values.each do |spec|
- bad = self.runtime_dependencies.find_all { |dep|
- spec.name == dep.name and not spec.satisfies_requirement? dep
- }
-
- conflicts[spec] = bad unless bad.empty?
+ 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 this spec can require +file+.
-
- def contains_requirable_file? file
- root = full_gem_path
+ # return true if there will be conflict when spec if loaded together with the list of specs.
- require_paths.each do |lib|
- base = "#{root}/#{lib}/#{file}"
- Gem.suffixes.each do |suf|
- path = "#{base}#{suf}"
- return true if File.file? path
- end
+ 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
-
- return false
+ result
end
##
- # The date this gem was created. Lazily defaults to TODAY.
+ # 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 ||= TODAY
+ @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.
+ # DO NOT set this, it is set automatically when the gem is packaged.
- def date= date
+ 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 /\A(\d{4})-(\d{2})-(\d{2})\Z/ =~ date 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, Date then
+ when Time, DateLike then
Time.utc(date.year, date.month, date.day)
else
TODAY
- end
- end
-
- ##
- # The default executable for this gem.
- #
- # Deprecated: The name of the gem is assumed to be the name of the
- # executable now. See Gem.bin_path.
-
- def default_executable
- if defined?(@default_executable) and @default_executable
- result = @default_executable
- elsif @executables and @executables.size == 1
- result = Array(@executables).first
- else
- result = nil
end
- result
end
##
# The default value for specification attribute +name+
- def default_value name
+ def default_value(name)
@@default_value[name]
end
@@ -1037,17 +1743,17 @@ class Gem::Specification
#
# [depending_gem, dependency, [list_of_gems_that_satisfy_dependency]]
- def dependent_gems
+ def dependent_gems(check_dev = true)
out = []
Gem::Specification.each do |spec|
- spec.dependencies.each do |dep|
- if self.satisfies_requirement?(dep) then
- sats = []
- find_all_satisfiers(dep) do |sat|
- sats << sat
- end
- out << [spec, dep, sats]
+ 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
@@ -1057,13 +1763,13 @@ class Gem::Specification
# Returns all specs that matches this spec's runtime dependencies.
def dependent_specs
- runtime_dependencies.map { |dep| dep.to_specs }.flatten
+ runtime_dependencies.flat_map(&:to_specs)
end
##
- # A long description of this gem
+ # A detailed description of this gem. See also #summary
- def description= str
+ def description=(str)
@description = str.to_s
end
@@ -1071,38 +1777,41 @@ class Gem::Specification
# List of dependencies that are used for development
def development_dependencies
- dependencies.select { |d| d.type == :development }
+ dependencies.select {|d| d.type == :development }
end
##
- # Returns the full path to this spec's documentation directory.
+ # 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
- @doc_dir ||= File.join base_dir, 'doc', full_name
- end
+ def doc_dir(type = nil)
+ @doc_dir ||= File.join base_dir, "doc", full_name
- def encode_with coder # :nodoc:
- mark_version
+ if type
+ File.join @doc_dir, type
+ else
+ @doc_dir
+ end
+ end
- coder.add 'name', @name
- coder.add 'version', @version
- platform = case @original_platform
- when nil, '' then
- 'ruby'
- when String then
- @original_platform
- else
- @original_platform.to_s
- end
- coder.add 'platform', platform
+ 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|
- coder.add name, instance_variable_get("@#{name}")
+ value = instance_variable_get("@#{name}")
+ coder.add name, value unless value.nil?
end
end
- def eql? other # :nodoc:
+ def eql?(other) # :nodoc:
self.class === other && same_attributes?(other)
end
@@ -1110,62 +1819,34 @@ class Gem::Specification
# Singular accessor for #executables
def executable
- val = executables and val.first
+ (val = executables) && val.first
end
##
# Singular accessor for #executables
- def executable=o
+ def executable=(o)
self.executables = [o]
end
##
- # Executables included in the gem.
-
- def executables
- @executables ||= []
- end
+ # Sets executables to +value+, ensuring it is an array.
- ##
- # Sets executables to +value+, ensuring it is an array. Don't
- # use this, push onto the array instead.
-
- def executables= value
- # TODO: warn about setting instead of pushing
+ def executables=(value)
@executables = Array(value)
end
##
- # Extensions to build when installing the gem. See
- # Gem::Installer#build_extensions for valid values.
-
- def extensions
- @extensions ||= []
- end
-
- ##
- # Sets extensions to +extensions+, ensuring it is an array. Don't
- # use this, push onto the array instead.
+ # Sets extensions to +extensions+, ensuring it is an array.
- def extensions= extensions
- # TODO: warn about setting instead of pushing
+ def extensions=(extensions)
@extensions = Array extensions
end
##
- # Extra files to add to RDoc such as README or doc/examples.txt
-
- def extra_rdoc_files
- @extra_rdoc_files ||= []
- end
-
- ##
- # Sets extra_rdoc_files to +files+, ensuring it is an array. Don't
- # use this, push onto the array instead.
+ # Sets extra_rdoc_files to +files+, ensuring it is an array.
- def extra_rdoc_files= files
- # TODO: warn about setting instead of pushing
+ def extra_rdoc_files=(files)
@extra_rdoc_files = Array files
end
@@ -1179,35 +1860,16 @@ class Gem::Specification
end
##
- # 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.
-
- def files
- # DO NOT CHANGE TO ||= ! This is not a normal accessor. (yes, it sucks)
- @files = [@files,
- @test_files,
- add_bindir(@executables),
- @extra_rdoc_files,
- @extensions,
- ].flatten.uniq.compact
- end
-
- ##
# Sets files to +files+, ensuring it is an array.
- def files= files
+ def files=(files)
@files = Array files
end
##
# Finds all gems that satisfy +dep+
- def find_all_satisfiers dep
+ def find_all_satisfiers(dep)
Gem::Specification.each do |spec|
yield spec if spec.satisfies_requirement? dep
end
@@ -1228,141 +1890,95 @@ class Gem::Specification
end
##
- # The full path to the gem (install path + full name).
+ # Work around old bundler versions removing my methods
+ # Can be removed once RubyGems can no longer install Bundler 2.5
- def full_gem_path
- # TODO: try to get rid of this... or the awkward
- # TODO: also, shouldn't it default to full_name if it hasn't been written?
- return @full_gem_path if defined?(@full_gem_path) && @full_gem_path
-
- @full_gem_path = File.expand_path File.join(gems_dir, full_name)
-
- return @full_gem_path if File.directory? @full_gem_path
-
- @full_gem_path = File.expand_path File.join(gems_dir, original_name)
+ def gem_dir # :nodoc:
+ super
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 or platform.nil? then
- "#{@name}-#{@version}"
- else
- "#{@name}-#{@version}-#{platform}"
- end
- end
-
- ##
- # Returns the full path to this spec's gem directory.
- # eg: /usr/local/lib/ruby/1.8/gems/mygem-1.0
-
- def gem_dir
- @gem_dir ||= File.expand_path File.join(gems_dir, full_name)
- 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
- # TODO: this logic seems terribly broken, but tests fail if just base_dir
- @gems_dir ||= File.join(loaded_from && base_dir || Gem.dir, "gems")
+ @gems_dir ||= File.join(base_dir, "gems")
end
##
- # Deprecated and ignored, defaults to true.
- #
- # Formerly used to indicate this gem was RDoc-capable.
-
- def has_rdoc
- true
- end
-
- ##
- # Deprecated and ignored.
- #
- # Formerly used to indicate this gem was RDoc-capable.
-
- def has_rdoc= ignored
- @has_rdoc = true
- end
-
- alias :has_rdoc? :has_rdoc
-
- ##
# True if this gem has files in test_files
- def has_unit_tests?
- not test_files.empty?
+ def has_unit_tests? # :nodoc:
+ !test_files.empty?
end
# :stopdoc:
- alias has_test_suite? has_unit_tests?
+ alias_method :has_test_suite?, :has_unit_tests?
# :startdoc:
def hash # :nodoc:
- @@attributes.inject(0) { |hash_code, (name, _)|
- hash_code ^ self.send(name).hash
- }
+ name.hash ^ version.hash
end
- def init_with coder # :nodoc:
+ 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
+ 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
- @@nil_attributes.each do |key|
- instance_variable_set "@#{key}", nil
- end
-
- @@non_nil_attributes.each do |key|
- default = default_value(key)
- value = case default
- when Time, Numeric, Symbol, true, false, nil then default
- else default.dup
- end
-
- instance_variable_set "@#{key}", value
- end
+ 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_attributes from +other_spec+ so state isn't shared.
-
- def initialize_copy other_spec
- other_ivars = other_spec.instance_variables
- other_ivars = other_ivars.map { |ivar| ivar.intern } if # for 1.9
- String === other_ivars.first
+ # 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_ivars.include? name
+ next unless other_spec.instance_variable_defined? name
begin
val = other_spec.instance_variable_get(name)
- if val then
+ if val
instance_variable_set name, val.dup
- else
+ elsif Gem.configuration.really_verbose
warn "WARNING: #{full_name} has an invalid nil value for #{name}"
end
rescue TypeError
@@ -1373,28 +1989,26 @@ class Gem::Specification
raise e
end
end
- end
-
- ##
- # The directory that this gem was installed into.
- # TODO: rename - horrible. this is the base_dir for a gem path
- def installation_path
- loaded_from && base_dir
+ @required_ruby_version = other_spec.required_ruby_version.dup
+ @required_rubygems_version = other_spec.required_rubygems_version.dup
end
- ##
- # Returns a string usable in Dir.glob to match all requirable paths
- # for this spec.
-
- def lib_dirs_glob
- dirs = if self.require_paths.size > 1 then
- "{#{self.require_paths.join(',')}}"
- else
- self.require_paths.first
- 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
- "#{self.full_gem_path}/#{dirs}"
+ def inspect # :nodoc:
+ if $DEBUG
+ super
+ else
+ "#{super[0..-2]} #{full_name}>"
+ end
end
##
@@ -1403,7 +2017,7 @@ class Gem::Specification
def lib_files
@files.select do |file|
require_paths.any? do |path|
- file.index(path) == 0
+ file.start_with? path
end
end
end
@@ -1412,62 +2026,39 @@ class Gem::Specification
# Singular accessor for #licenses
def license
- val = licenses and val.first
+ licenses.first
end
##
- # Singular accessor for #licenses
-
- 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.
+ # Plural accessor for setting licenses
+ #
+ # See #license= for details
def licenses
@licenses ||= []
end
- ##
- # Set licenses to +licenses+, ensuring it is an array.
-
- def licenses= licenses
- @licenses = Array licenses
- end
-
- ##
- # Set the location a Specification was loaded from. +obj+ is converted
- # to a String.
-
- def loaded_from= path
- @loaded_from = path.to_s
- end
-
- ##
- # Sets the rubygems_version to the current RubyGems version.
-
- def mark_version
- @rubygems_version = Gem::VERSION
- end
-
- ##
- # Return all files in this gem that match for +glob+.
-
- def matches_for_glob glob # TODO: rename?
- # TODO: do we need these?? Kill it
- glob = File.join(self.lib_dirs_glob, glob)
-
- Dir[glob].map { |f| f.untaint } # FIX our tests are broken, run w/ SAFE=1
+ 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 @specification_version > CURRENT_SPECIFICATION_VERSION and
- sym.to_s =~ /=$/ then
+ 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
@@ -1475,23 +2066,43 @@ class Gem::Specification
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) and @extra_rdoc_files then
+ if defined?(@extra_rdoc_files) && @extra_rdoc_files
@extra_rdoc_files.uniq!
@files ||= []
@files.concat(@extra_rdoc_files)
end
- @files = @files.uniq if @files
- @extensions = @extensions.uniq if @extensions
- @test_files = @test_files.uniq if @test_files
- @executables = @executables.uniq if @executables
- @extra_rdoc_files = @extra_rdoc_files.uniq if @extra_rdoc_files
+ @files = @files.uniq.sort if @files
+ @extensions = @extensions.uniq.sort if @extensions
+ @test_files = @test_files.uniq.sort if @test_files
+ @executables = @executables.uniq.sort if @executables
+ @extra_rdoc_files = @extra_rdoc_files.uniq.sort if @extra_rdoc_files
+ end
+
+ ##
+ # Return a NameTuple that represents this Specification
+
+ def name_tuple
+ Gem::NameTuple.new name, version, original_platform
end
##
@@ -1499,7 +2110,7 @@ class Gem::Specification
# platform. For use with legacy gems.
def original_name # :nodoc:
- if platform == Gem::Platform::RUBY or platform.nil? then
+ if platform == Gem::Platform::RUBY || platform.nil?
"#{@name}-#{@version}"
else
"#{@name}-#{@version}-#{@original_platform}"
@@ -1517,171 +2128,108 @@ class Gem::Specification
# The platform this gem runs on. See Gem::Platform for details.
def platform
- @new_platform ||= Gem::Platform::RUBY
- end
-
- ##
- # The platform this gem runs on. See Gem::Platform for details.
- #
- # Setting this to any value other than Gem::Platform::RUBY or
- # Gem::Platform::CURRENT is probably wrong.
-
- def platform= platform
- if @original_platform.nil? or
- @original_platform == Gem::Platform::RUBY then
- @original_platform = platform
- end
-
- 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
-
- @new_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.group 2, "Gem::Specification.new do |s|", "end" do
q.breakable
- # REFACTOR: each_attr - use in to_yaml as well
- @@attributes.each do |attr_name|
- current_value = self.send attr_name
- if current_value != default_value(attr_name) or
- self.class.required_attribute? attr_name then
+ attributes = @@attributes - [:name, :version]
+ attributes.unshift :installed_by_version
+ attributes.unshift :version
+ attributes.unshift :name
- q.text "s.#{attr_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)
- if attr_name == :date then
- current_value = current_value.utc
+ q.text "s.#{attr_name} = "
- q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})"
- else
- q.pp current_value
- end
+ if attr_name == :date
+ current_value = current_value.utc
- q.breakable
+ q.text "Time.utc(#{current_value.year}, #{current_value.month}, #{current_value.day})"
+ else
+ q.pp current_value
end
+
+ q.breakable
end
end
end
##
- # Check the spec for possible conflicts and freak out if there are any.
+ # Raise an exception if the version of this spec conflicts with the one
+ # that is already loaded (+other+)
- def raise_if_conflicts
- other = Gem.loaded_specs[self.name]
+ def check_version_conflict(other) # :nodoc:
+ return if version == other.version
- if other and self.version != other.version then
- # This gem is already loaded. If the currently loaded gem is not in the
- # list of candidate gems, then we have a version conflict.
+ # 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}"
+ msg = "can't activate #{full_name}, already activated #{other.full_name}"
- e = Gem::LoadError.new msg
- e.name = self.name
- # TODO: e.requirement = dep.requirement
+ e = Gem::LoadError.new msg
+ e.name = name
- raise e
- end
-
- conf = self.conflicts
-
- unless conf.empty? then
- y = conf.map { |act,con|
- "#{act.full_name} conflicts with #{con.join(", ")}"
- }.join ", "
-
- # TODO: improve message by saying who activated `con`
-
- raise Gem::LoadError, "Unable to activate #{self.full_name}, because #{y}"
- end
+ raise e
end
+ private :check_version_conflict
+
##
- # An ARGV style array of options to RDoc
+ # Check the spec for possible conflicts and freak out if there are any.
- def rdoc_options
- @rdoc_options ||= []
+ def raise_if_conflicts # :nodoc:
+ if has_conflicts?
+ raise Gem::ConflictError.new self, conflicts
+ end
end
##
- # Sets rdoc_options to +value+, ensuring it is an array. Don't
- # use this, push onto the array instead.
+ # 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
- # TODO: warn about setting instead of pushing
- @rdoc_options = Array options
+ 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 and val.first
+ (val = require_paths) && val.first
end
##
# Singular accessor for #require_paths
- def require_path= path
- self.require_paths = [path]
+ def require_path=(path)
+ self.require_paths = Array(path)
end
##
- # The version of ruby required by this gem
+ # Set requirements to +req+, ensuring it is an array.
- def required_ruby_version= req
- @required_ruby_version = Gem::Requirement.create req
- end
-
- ##
- # The RubyGems version required by this gem
-
- def required_rubygems_version= req
- @required_rubygems_version = Gem::Requirement.create req
- end
-
- ##
- # An array or things required by this gem. Not used by anything
- # presently.
-
- def requirements
- @requirements ||= []
+ def requirements=(req)
+ @requirements = Array req
end
- ##
- # Set requirements to +req+, ensuring it is an array. Don't
- # use this, push onto the array instead.
-
- def requirements= req
- # TODO: warn about setting instead of pushing
- @requirements = Array req
+ 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
+ @ri_dir ||= File.join base_dir, "ri", full_name
end
##
@@ -1690,15 +2238,20 @@ class Gem::Specification
def ruby_code(obj)
case obj
- when String then obj.dump
- when Array then '[' + obj.map { |x| ruby_code x }.join(", ") + ']'
- when Gem::Version then obj.to_s.dump
- when Date then obj.strftime('%Y-%m-%d').dump
- when Time then obj.strftime('%Y-%m-%d').dump
- when Numeric then obj.inspect
- when true, false, nil then obj.inspect
- when Gem::Platform then "Gem::Platform.new(#{obj.to_a.inspect})"
- when Gem::Requirement then "Gem::Requirement.new(#{obj.to_s.inspect})"
+ 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
@@ -1709,14 +2262,14 @@ class Gem::Specification
# List of dependencies that will automatically be activated at runtime.
def runtime_dependencies
- dependencies.select { |d| d.type == :runtime }
+ dependencies.select(&:runtime?)
end
##
# True if this gem has the same attributes as +other+.
- def same_attributes? spec
- @@attributes.all? { |name, default| self.send(name) == spec.send(name) }
+ def same_attributes?(spec)
+ @@attributes.all? {|name, _default| send(name) == spec.send(name) }
end
private :same_attributes?
@@ -1724,8 +2277,8 @@ class Gem::Specification
##
# Checks if this specification meets the requirement of +dependency+.
- def satisfies_requirement? dependency
- return @name == dependency.name &&
+ def satisfies_requirement?(dependency)
+ @name == dependency.name &&
dependency.requirement.satisfied_by?(@version)
end
@@ -1733,8 +2286,14 @@ class Gem::Specification
# Returns an object you can use to sort specifications in #sort_by.
def sort_obj
- # TODO: this is horrible. Deprecate it.
- [@name, @version, @new_platform == Gem::Platform::RUBY ? -1 : 1]
+ [@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
##
@@ -1765,7 +2324,7 @@ class Gem::Specification
##
# A short summary of this gem's description.
- def summary= str
+ def summary=(str)
@summary = str.to_s.strip.
gsub(/(\w-)\n[ \t]*(\w)/, '\1\2').gsub(/\n[ \t]*/, " ") # so. weird.
end
@@ -1773,14 +2332,14 @@ class Gem::Specification
##
# Singular accessor for #test_files
- def test_file
- val = test_files and val.first
+ def test_file # :nodoc:
+ (val = test_files) && val.first
end
##
- # Singular accessor for #test_files
+ # Singular mutator for #test_files
- def test_file= file
+ def test_file=(file) # :nodoc:
self.test_files = [file]
end
@@ -1788,15 +2347,15 @@ class Gem::Specification
# Test files included in this gem. You cannot append to this accessor, you
# must assign to it.
- def test_files
+ 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 then
+ if defined? @test_suite_file
@test_files = [@test_suite_file].flatten
@test_suite_file = nil
end
- if defined?(@test_files) and @test_files then
+ if defined?(@test_files) && @test_files
@test_files
else
@test_files = []
@@ -1804,91 +2363,71 @@ class Gem::Specification
end
##
- # Set test_files to +files+, ensuring it is an array.
-
- def test_files= files
- @test_files = Array files
- end
-
- def test_suite_file # :nodoc:
- # TODO: deprecate
- test_files.first
- end
-
- def test_suite_file= file # :nodoc:
- # TODO: deprecate
- @test_files = [] unless defined? @test_files
- @test_files << file
- 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
- mark_version
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? or platform == Gem::Platform::RUBY then
+ 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,
- :has_rdoc,
- :default_executable,
+ :metadata,
+ :signing_key,
]
@@attributes.each do |attr_name|
next if handled.include? attr_name
- current_value = self.send(attr_name)
- if current_value != default_value(attr_name) or
- self.class.required_attribute? attr_name then
+ 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
- result << nil
- result << " if s.respond_to? :specification_version then"
- result << " s.specification_version = #{specification_version}"
- result << nil
-
- result << " if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then"
-
- dependencies.each do |dep|
- req = dep.requirements_list.inspect
- dep.instance_variable_set :@type, :runtime if dep.type.nil? # HACK
- result << " s.add_#{dep.type}_dependency(%q<#{dep.name}>, #{req})"
+ if String === signing_key
+ result << " s.signing_key = #{ruby_code signing_key}"
end
- result << " else"
-
- dependencies.each do |dep|
- version_reqs_param = dep.requirements_list.inspect
- result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})"
+ if @installed_by_version
+ result << nil
+ result << " s.installed_by_version = #{ruby_code Gem::VERSION}"
end
- result << ' end'
+ unless dependencies.empty?
+ result << nil
+ result << " s.specification_version = #{specification_version}"
+ result << nil
- result << " else"
dependencies.each do |dep|
- version_reqs_param = dep.requirements_list.inspect
- result << " s.add_dependency(%q<#{dep.name}>, #{version_reqs_param})"
+ 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
- result << " end"
+ end
result << "end"
result << nil
@@ -1910,15 +2449,37 @@ class Gem::Specification
"#<Gem::Specification name=#{@name} version=#{@version}>"
end
+ ##
+ # Returns self
+
+ def to_spec
+ self
+ end
+
def to_yaml(opts = {}) # :nodoc:
- if YAML.const_defined?(:ENGINE) && !YAML::ENGINE.syck? then
- super.gsub(/ !!null \n/, " \n")
- else
- YAML.quick_emit object_id, opts do |out|
- out.map taguri, to_yaml_style do |map|
- encode_with map
- end
+ 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
@@ -1926,14 +2487,27 @@ class Gem::Specification
# Recursively walk dependencies of this spec, executing the +block+ for each
# hop.
- def traverse trail = [], &block
- trail = trail + [self]
- runtime_dependencies.each do |dep|
- dep.to_specs.each do |dep_spec|
- block[self, dep, dep_spec, trail + [dep_spec]]
- dep_spec.traverse(trail, &block) unless
- trail.map(&:name).include? dep_spec.name
+ 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
@@ -1944,185 +2518,87 @@ class Gem::Specification
# Raises InvalidSpecificationException if the spec does not pass the
# checks..
- def validate packaging = true
- require 'rubygems/user_interaction'
- extend Gem::UserInteraction
+ def validate(packaging = true, strict = false)
normalize
- nil_attributes = self.class.non_nil_attributes.find_all do |name|
- instance_variable_get("@#{name}").nil?
- end
-
- unless nil_attributes.empty? then
- raise Gem::InvalidSpecificationException,
- "#{nil_attributes.join ', '} must not be nil"
- end
-
- if packaging and rubygems_version != Gem::VERSION then
- raise Gem::InvalidSpecificationException,
- "expected RubyGems version #{Gem::VERSION}, was #{rubygems_version}"
- end
-
- @@required_attributes.each do |symbol|
- unless self.send symbol then
- raise Gem::InvalidSpecificationException,
- "missing value for attribute #{symbol}"
- end
- end
-
- unless String === name then
- raise Gem::InvalidSpecificationException,
- "invalid value for attribute name: \"#{name.inspect}\""
- end
-
- if require_paths.empty? then
- raise Gem::InvalidSpecificationException,
- 'specification must have at least one require_path'
- end
-
- @files.delete_if { |x| File.directory?(x) }
- @test_files.delete_if { |x| File.directory?(x) }
- @executables.delete_if { |x| File.directory?(File.join(@bindir, x)) }
- @extra_rdoc_files.delete_if { |x| File.directory?(x) }
- @extensions.delete_if { |x| File.directory?(x) }
-
- non_files = files.reject { |x| File.file?(x) }
-
- unless not packaging or non_files.empty? then
- raise Gem::InvalidSpecificationException,
- "[\"#{non_files.join "\", \""}\"] are not files"
- end
+ validation_policy = Gem::SpecificationPolicy.new(self)
+ validation_policy.packaging = packaging
+ validation_policy.validate(strict)
+ end
- unless specification_version.is_a?(Fixnum)
- raise Gem::InvalidSpecificationException,
- 'specification_version must be a Fixnum (did you mean version?)'
- 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
- case platform
- when Gem::Platform, Gem::Platform::RUBY then # ok
- else
- raise Gem::InvalidSpecificationException,
- "invalid platform #{platform.inspect}, see Gem::Platform"
- end
+ def validate_for_resolution
+ Gem::SpecificationPolicy.new(self).validate_for_resolution
+ end
- self.class.array_attributes.each do |field|
- val = self.send field
- klass = case field
- when :dependencies
- Gem::Dependency
- else
- String
- end
+ ##
+ # Set the version to +version+.
- unless Array === val and val.all? { |x| x.kind_of?(klass) } then
- raise(Gem::InvalidSpecificationException,
- "#{field} must be an Array of #{klass}")
- end
- end
+ def version=(version)
+ @version = version.nil? ? version : Gem::Version.create(version)
+ end
- [:authors].each do |field|
- val = self.send field
- raise Gem::InvalidSpecificationException, "#{field} may not be empty" if
- val.empty?
- end
+ def stubbed?
+ false
+ end
- licenses.each { |license|
- if license.length > 64
- raise Gem::InvalidSpecificationException,
- "each license must be 64 characters or less"
+ 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
- }
-
- # reject lazy developers:
-
- lazy = '"FIxxxXME" or "TOxxxDO"'.gsub(/xxx/, '')
-
- unless authors.grep(/FI XME|TO DO/x).empty? then
- raise Gem::InvalidSpecificationException, "#{lazy} is not an author"
- end
-
- unless Array(email).grep(/FI XME|TO DO/x).empty? then
- raise Gem::InvalidSpecificationException, "#{lazy} is not an email"
- end
-
- if description =~ /FI XME|TO DO/x then
- raise Gem::InvalidSpecificationException, "#{lazy} is not a description"
end
+ end
- if summary =~ /FI XME|TO DO/x then
- raise Gem::InvalidSpecificationException, "#{lazy} is not a summary"
- end
+ ##
+ # Reset nil attributes to their default values to make the spec valid
- if homepage and not homepage.empty? and
- homepage !~ /\A[a-z][a-z\d+.-]*:/i then
- raise Gem::InvalidSpecificationException,
- "\"#{homepage}\" is not a URI"
+ 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
- # Warnings
+ nil_attributes.each do |attribute|
+ default = default_value attribute
- %w[author description email homepage summary].each do |attribute|
- value = self.send attribute
- alert_warning "no #{attribute} specified" if value.nil? or value.empty?
- end
+ value = case default
+ when Time, Numeric, Symbol, true, false, nil then default
+ else default.dup
+ end
- if description == summary then
- alert_warning 'description and summary are identical'
+ instance_variable_set "@#{attribute}", value
end
- # TODO: raise at some given date
- alert_warning "deprecated autorequire specified" if autorequire
-
- executables.each do |executable|
- executable_path = File.join(bindir, executable)
- shebang = File.read(executable_path, 2) == '#!'
+ @installed_by_version ||= nil
- alert_warning "#{executable_path} is missing #! line" unless shebang
- end
-
- true
+ nil
end
- ##
- # Set the version to +version+, potentially also setting
- # required_rubygems_version if +version+ indicates it is a
- # prerelease.
+ def flatten_require_paths # :nodoc:
+ return unless raw_require_paths.first.is_a?(Array)
- def version= version
- @version = Gem::Version.create(version)
- self.required_rubygems_version = '> 1.3.1' if @version.prerelease?
- return @version
+ 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
- # FIX: have this handle the platform/new_platform/original_platform bullshit
- def yaml_initialize(tag, vals) # :nodoc:
- vals.each do |ivar, val|
- instance_variable_set "@#{ivar}", val
- end
-
- @original_platform = @platform # for backwards compatibility
- self.platform = Gem::Platform.new @platform
- end
-
- extend Gem::Deprecate
-
- deprecate :test_suite_file, :test_file, 2011, 10
- deprecate :test_suite_file=, :test_file=, 2011, 10
- deprecate :loaded, :activated, 2011, 10
- deprecate :loaded?, :activated?, 2011, 10
- deprecate :loaded=, :activated=, 2011, 10
- deprecate :installation_path, :base_dir, 2011, 10
- deprecate :cache_gem, :cache_file, 2011, 10
- # TODO:
- # deprecate :has_rdoc, :none, 2011, 10
- # deprecate :has_rdoc?, :none, 2011, 10
- # deprecate :has_rdoc=, :none, 2011, 10
- # deprecate :default_executable, :none, 2011, 10
- # deprecate :default_executable=, :none, 2011, 10
- # deprecate :spec_name, :spec_file, 2011, 10
- # deprecate :file_name, :cache_file, 2011, 10
- # deprecate :full_gem_path, :cache_file, 2011, 10
+ def raw_require_paths # :nodoc:
+ @require_paths
+ end
end
-
-Gem.clear_paths
-
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/test_case.rb b/lib/rubygems/test_case.rb
deleted file mode 100644
index 6aed3487c6..0000000000
--- a/lib/rubygems/test_case.rb
+++ /dev/null
@@ -1,877 +0,0 @@
-at_exit { $SAFE = 1 }
-
-if defined? Gem::QuickLoader
- Gem::QuickLoader.load_full_rubygems_library
-else
- require 'rubygems'
-end
-
-begin
- gem 'minitest'
-rescue Gem::LoadError
-end
-
-require "rubygems/deprecate"
-require 'minitest/autorun'
-require 'fileutils'
-require 'tmpdir'
-require 'uri'
-require 'rubygems/package'
-require 'rubygems/test_utilities'
-require 'pp'
-require 'zlib'
-require 'pathname'
-Gem.load_yaml
-
-require 'rubygems/mock_gem_ui'
-
-module Gem
-
- ##
- # Allows setting the gem path searcher. This method is available when
- # requiring 'rubygems/test_case'
-
- def self.searcher=(searcher)
- @searcher = searcher
- end
-
- ##
- # Allows setting the default SourceIndex. This method is available when
- # requiring 'rubygems/test_case'
-
- def self.source_index=(si)
- raise "This method is not supported"
- Gem::Specification.reset if si # HACK
- @@source_index = si
- end
-
- ##
- # Allows toggling Windows behavior. This method is available when requiring
- # 'rubygems/test_case'
-
- def self.win_platform=(val)
- @@win_platform = val
- end
-
- ##
- # Allows setting path to ruby. This method is available when requiring
- # 'rubygems/test_case'
-
- def self.ruby= ruby
- @ruby = ruby
- end
-
- ##
- # When rubygems/test_case is required the default user interaction is a
- # MockGemUi.
-
- module DefaultUserInteraction
- @ui = Gem::MockGemUi.new
- end
-end
-
-##
-# RubyGemTestCase provides a variety of methods for testing rubygems and
-# gem-related behavior in a sandbox. Through RubyGemTestCase you can install
-# and uninstall gems, fetch remote gems through a stub fetcher and be assured
-# your normal set of gems is not affected.
-#
-# Tests are always run at a safe level of 1.
-
-class Gem::TestCase < MiniTest::Unit::TestCase
-
- # TODO: move to minitest
- def assert_path_exists path, msg = nil
- msg = message(msg) { "Expected path '#{path}' to exist" }
- assert File.exist?(path), msg
- end
-
- # TODO: move to minitest
- def refute_path_exists path, msg = nil
- msg = message(msg) { "Expected path '#{path}' to not exist" }
- refute File.exist?(path), msg
- end
-
- include Gem::DefaultUserInteraction
-
- undef_method :default_test if instance_methods.include? 'default_test' or
- instance_methods.include? :default_test
-
- @@project_dir = Dir.pwd unless defined?(@@project_dir)
-
- ##
- # #setup prepares a sandboxed location to install gems. All installs are
- # directed to a temporary directory. All install plugins are removed.
- #
- # If the +RUBY+ environment variable is set the given path is used for
- # Gem::ruby. The local platform is set to <tt>i386-mswin32</tt> for Windows
- # or <tt>i686-darwin8.10.1</tt> otherwise.
- #
- # If the +KEEP_FILES+ environment variable is set the files will not be
- # removed from <tt>/tmp/test_rubygems_#{$$}.#{Time.now.to_i}</tt>.
-
- def setup
- super
-
- @orig_gem_home = ENV['GEM_HOME']
- @orig_gem_path = ENV['GEM_PATH']
-
- @current_dir = Dir.pwd
- @ui = Gem::MockGemUi.new
-
- tmpdir = nil
- Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp
-
- if ENV['KEEP_FILES'] then
- @tempdir = File.join(tmpdir, "test_rubygems_#{$$}.#{Time.now.to_i}")
- else
- @tempdir = File.join(tmpdir, "test_rubygems_#{$$}")
- end
- @tempdir.untaint
- @gemhome = File.join @tempdir, 'gemhome'
- @userhome = File.join @tempdir, 'userhome'
-
- @orig_ruby = if ruby = ENV['RUBY'] then
- Gem.class_eval { ruby, @ruby = @ruby, ruby }
- ruby
- end
-
- Gem.ensure_gem_subdirectories @gemhome
-
- @orig_LOAD_PATH = $LOAD_PATH.dup
- $LOAD_PATH.map! { |s| File.expand_path s }
-
- Dir.chdir @tempdir
-
- @orig_ENV_HOME = ENV['HOME']
- ENV['HOME'] = @userhome
- Gem.instance_variable_set :@user_home, nil
-
- FileUtils.mkdir_p @gemhome
- FileUtils.mkdir_p @userhome
-
- Gem.use_paths(@gemhome)
-
- Gem.loaded_specs.clear
- Gem.unresolved_deps.clear
-
- Gem.configuration.verbose = true
- Gem.configuration.update_sources = true
-
- @gem_repo = "http://gems.example.com/"
- @uri = URI.parse @gem_repo
- Gem.sources.replace [@gem_repo]
-
- Gem.searcher = nil
- Gem::SpecFetcher.fetcher = nil
-
- @orig_BASERUBY = Gem::ConfigMap[:BASERUBY]
- Gem::ConfigMap[:BASERUBY] = Gem::ConfigMap[:ruby_install_name]
-
- @orig_arch = Gem::ConfigMap[:arch]
-
- if win_platform?
- util_set_arch 'i386-mswin32'
- else
- util_set_arch 'i686-darwin8.10.1'
- end
-
- @marshal_version = "#{Marshal::MAJOR_VERSION}.#{Marshal::MINOR_VERSION}"
-
- # TODO: move to installer test cases
- Gem.post_build_hooks.clear
- Gem.post_install_hooks.clear
- Gem.post_uninstall_hooks.clear
- Gem.pre_install_hooks.clear
- Gem.pre_uninstall_hooks.clear
-
- # TODO: move to installer test cases
- Gem.post_build do |installer|
- @post_build_hook_arg = installer
- true
- end
-
- Gem.post_install do |installer|
- @post_install_hook_arg = installer
- end
-
- Gem.post_uninstall do |uninstaller|
- @post_uninstall_hook_arg = uninstaller
- end
-
- Gem.pre_install do |installer|
- @pre_install_hook_arg = installer
- true
- end
-
- Gem.pre_uninstall do |uninstaller|
- @pre_uninstall_hook_arg = uninstaller
- end
- end
-
- ##
- # #teardown restores the process to its original state and removes the
- # tempdir unless the +KEEP_FILES+ environment variable was set.
-
- def teardown
- $LOAD_PATH.replace @orig_LOAD_PATH
-
- Gem::ConfigMap[:BASERUBY] = @orig_BASERUBY
- Gem::ConfigMap[:arch] = @orig_arch
-
- if defined? Gem::RemoteFetcher then
- Gem::RemoteFetcher.fetcher = nil
- end
-
- Dir.chdir @current_dir
-
- FileUtils.rm_rf @tempdir unless ENV['KEEP_FILES']
-
- ENV['GEM_HOME'] = @orig_gem_home
- ENV['GEM_PATH'] = @orig_gem_path
-
- _ = @orig_ruby
- Gem.class_eval { @ruby = _ } if _
-
- if @orig_ENV_HOME then
- ENV['HOME'] = @orig_ENV_HOME
- else
- ENV.delete 'HOME'
- end
- end
-
- ##
- # Builds and installs the Gem::Specification +spec+
-
- def install_gem spec
- require 'rubygems/installer'
-
- use_ui Gem::MockGemUi.new do
- Dir.chdir @tempdir do
- Gem::Builder.new(spec).build
- end
- end
-
- gem = File.join(@tempdir, File.basename(spec.cache_file)).untaint
-
- Gem::Installer.new(gem, :wrappers => true).install
- end
-
- ##
- # Builds and installs the Gem::Specification +spec+ into the user dir
-
- def install_gem_user spec
- require 'rubygems/installer'
-
- use_ui Gem::MockGemUi.new do
- Dir.chdir @tempdir do
- Gem::Builder.new(spec).build
- end
- end
-
- gem = File.join(@tempdir, File.basename(spec.cache_file)).untaint
-
- i = Gem::Installer.new(gem, :wrappers => true, :user_install => true)
- i.install
- i.spec
- end
-
- ##
- # Uninstalls the Gem::Specification +spec+
- def uninstall_gem spec
- require 'rubygems/uninstaller'
-
- Gem::Uninstaller.new(spec.name,
- :executables => true, :user_install => true).uninstall
- end
-
- ##
- # creates a temporary directory with hax
-
- def create_tmpdir
- tmpdir = nil
- Dir.chdir Dir.tmpdir do tmpdir = Dir.pwd end # HACK OSX /private/tmp
- tmpdir = File.join tmpdir, "test_rubygems_#{$$}"
- FileUtils.mkdir_p tmpdir
- return tmpdir
- end
-
- ##
- # Enables pretty-print for all tests
-
- def mu_pp(obj)
- s = ''
- s = PP.pp obj, s
- s = s.force_encoding(Encoding.default_external) if defined? Encoding
- s.chomp
- end
-
- ##
- # Reads a Marshal file at +path+
-
- def read_cache(path)
- open path.dup.untaint, 'rb' do |io|
- Marshal.load io.read
- end
- end
-
- ##
- # Reads a binary file at +path+
-
- def read_binary(path)
- Gem.read_binary path
- end
-
- ##
- # Writes a binary file to +path+ which is relative to +@gemhome+
-
- def write_file(path)
- path = File.join @gemhome, path unless Pathname.new(path).absolute?
- dir = File.dirname path
- FileUtils.mkdir_p dir
-
- open path, 'wb' do |io|
- yield io if block_given?
- end
-
- path
- end
-
- def all_spec_names
- Gem::Specification.map(&:full_name)
- end
-
- ##
- # Creates a Gem::Specification with a minimum of extra work. +name+ and
- # +version+ are the gem's name and version, platform, author, email,
- # homepage, summary and description are defaulted. The specification is
- # yielded for customization.
- #
- # The gem is added to the installed gems in +@gemhome+ and to the current
- # source_index.
- #
- # Use this with #write_file to build an installed gem.
-
- def quick_gem(name, version='2')
- require 'rubygems/specification'
-
- spec = Gem::Specification.new do |s|
- s.platform = Gem::Platform::RUBY
- s.name = name
- s.version = version
- s.author = 'A User'
- s.email = 'example@example.com'
- s.homepage = 'http://example.com'
- s.summary = "this is a summary"
- s.description = "This is a test description"
-
- yield(s) if block_given?
- end
-
- Gem::Specification.map # HACK: force specs to (re-)load before we write
-
- written_path = write_file spec.spec_file do |io|
- io.write spec.to_ruby_for_cache
- end
-
- spec.loaded_from = spec.loaded_from = written_path
-
- Gem::Specification.add_spec spec.for_cache
-
- return spec
- end
-
- def quick_spec name, version = '2'
- # TODO: deprecate
- require 'rubygems/specification'
-
- spec = Gem::Specification.new do |s|
- s.platform = Gem::Platform::RUBY
- s.name = name
- s.version = version
- s.author = 'A User'
- s.email = 'example@example.com'
- s.homepage = 'http://example.com'
- s.summary = "this is a summary"
- s.description = "This is a test description"
-
- yield(s) if block_given?
- end
-
- spec.loaded_from = spec.spec_file
-
- Gem::Specification.add_spec spec
-
- return spec
- end
-
- ##
- # Builds a gem from +spec+ and places it in <tt>File.join @gemhome,
- # 'cache'</tt>. Automatically creates files based on +spec.files+
-
- def util_build_gem(spec)
- dir = spec.gem_dir
- FileUtils.mkdir_p dir
-
- Dir.chdir dir do
- spec.files.each do |file|
- next if File.exist? file
- FileUtils.mkdir_p File.dirname(file)
- File.open file, 'w' do |fp| fp.puts "# #{file}" end
- end
-
- use_ui Gem::MockGemUi.new do
- Gem::Builder.new(spec).build
- end
-
- cache = spec.cache_file
- FileUtils.mv File.basename(cache), cache
- end
- end
-
- ##
- # Removes all installed gems from +@gemhome+.
-
- def util_clear_gems
- FileUtils.rm_rf File.join(@gemhome, "gems") # TODO: use Gem::Dirs
- FileUtils.rm_rf File.join(@gemhome, "specifications")
- Gem::Specification.reset
- end
-
- ##
- # Install the provided specs
-
- def install_specs(*specs)
- Gem::Specification.add_specs(*specs)
- Gem.searcher = nil
- end
-
- ##
- # Create a new spec (or gem if passed an array of files) and set it
- # up properly. Use this instead of util_spec and util_gem.
-
- def new_spec name, version, deps = nil, *files
- require 'rubygems/specification'
-
- spec = Gem::Specification.new do |s|
- s.platform = Gem::Platform::RUBY
- s.name = name
- s.version = version
- s.author = 'A User'
- s.email = 'example@example.com'
- s.homepage = 'http://example.com'
- s.summary = "this is a summary"
- s.description = "This is a test description"
-
- Array(deps).each do |n, req|
- s.add_dependency n, (req || '>= 0')
- end
-
- s.files.push(*files) unless files.empty?
-
- yield s if block_given?
- end
-
- spec.loaded_from = spec.spec_file
-
- unless files.empty? then
- write_file spec.spec_file do |io|
- io.write spec.to_ruby_for_cache
- end
-
- util_build_gem spec
-
- cache_file = File.join @tempdir, 'gems', "#{spec.full_name}.gem"
- FileUtils.mkdir_p File.dirname cache_file
- FileUtils.mv spec.cache_file, cache_file
- FileUtils.rm spec.spec_file
- end
-
- spec
- end
-
- ##
- # Creates a spec with +name+, +version+ and +deps+.
-
- def util_spec(name, version, deps = nil, &block)
- # TODO: deprecate
- raise "deps or block, not both" if deps and block
-
- if deps then
- block = proc do |s|
- deps.each do |n, req|
- s.add_dependency n, (req || '>= 0')
- end
- end
- end
-
- quick_spec(name, version, &block)
- end
-
- ##
- # Creates a gem with +name+, +version+ and +deps+. The specification will
- # be yielded before gem creation for customization. The gem will be placed
- # in <tt>File.join @tempdir, 'gems'</tt>. The specification and .gem file
- # location are returned.
-
- def util_gem(name, version, deps = nil, &block)
- # TODO: deprecate
- raise "deps or block, not both" if deps and block
-
- if deps then
- block = proc do |s|
- deps.each do |n, req|
- s.add_dependency n, (req || '>= 0')
- end
- end
- end
-
- spec = quick_gem(name, version, &block)
-
- util_build_gem spec
-
- cache_file = File.join @tempdir, 'gems', "#{spec.original_name}.gem"
- FileUtils.mkdir_p File.dirname cache_file
- FileUtils.mv spec.cache_file, cache_file
- FileUtils.rm spec.spec_file
-
- spec.loaded_from = nil
-
- [spec, cache_file]
- end
-
- ##
- # Gzips +data+.
-
- def util_gzip(data)
- out = StringIO.new
-
- Zlib::GzipWriter.wrap out do |io|
- io.write data
- end
-
- out.string
- end
-
- ##
- # Creates several default gems which all have a lib/code.rb file. The gems
- # are not installed but are available in the cache dir.
- #
- # +@a1+:: gem a version 1, this is the best-described gem.
- # +@a2+:: gem a version 2
- # +@a3a:: gem a version 3.a
- # +@a_evil9+:: gem a_evil version 9, use this to ensure similarly-named gems
- # don't collide with a.
- # +@b2+:: gem b version 2
- # +@c1_2+:: gem c version 1.2
- # +@pl1+:: gem pl version 1, this gem has a legacy platform of i386-linux.
- #
- # Additional +prerelease+ gems may also be created:
- #
- # +@a2_pre+:: gem a version 2.a
- # TODO: nuke this and fix tests. this should speed up a lot
-
- def util_make_gems(prerelease = false)
- @a1 = quick_gem 'a', '1' do |s|
- s.files = %w[lib/code.rb]
- s.require_paths = %w[lib]
- s.date = Gem::Specification::TODAY - 86400
- s.homepage = 'http://a.example.com'
- s.email = %w[example@example.com example2@example.com]
- s.authors = %w[Example Example2]
- s.description = <<-DESC
-This line is really, really long. So long, in fact, that it is more than eighty characters long! The purpose of this line is for testing wrapping behavior because sometimes people don't wrap their text to eighty characters. Without the wrapping, the text might not look good in the RSS feed.
-
-Also, a list:
- * An entry that\'s actually kind of sort
- * an entry that\'s really long, which will probably get wrapped funny. That's ok, somebody wasn't thinking straight when they made it more than eighty characters.
- DESC
- end
-
- init = proc do |s|
- s.files = %w[lib/code.rb]
- s.require_paths = %w[lib]
- end
-
- @a2 = quick_gem('a', '2', &init)
- @a3a = quick_gem('a', '3.a', &init)
- @a_evil9 = quick_gem('a_evil', '9', &init)
- @b2 = quick_gem('b', '2', &init)
- @c1_2 = quick_gem('c', '1.2', &init)
-
- @pl1 = quick_gem 'pl', '1' do |s| # l for legacy
- s.files = %w[lib/code.rb]
- s.require_paths = %w[lib]
- s.platform = Gem::Platform.new 'i386-linux'
- s.instance_variable_set :@original_platform, 'i386-linux'
- end
-
- if prerelease
- @a2_pre = quick_gem('a', '2.a', &init)
- write_file File.join(*%W[gems #{@a2_pre.original_name} lib code.rb])
- util_build_gem @a2_pre
- end
-
- write_file File.join(*%W[gems #{@a1.original_name} lib code.rb])
- write_file File.join(*%W[gems #{@a2.original_name} lib code.rb])
- write_file File.join(*%W[gems #{@a3a.original_name} lib code.rb])
- write_file File.join(*%W[gems #{@b2.original_name} lib code.rb])
- write_file File.join(*%W[gems #{@c1_2.original_name} lib code.rb])
- write_file File.join(*%W[gems #{@pl1.original_name} lib code.rb])
-
- [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2, @pl1].each do |spec|
- util_build_gem spec
- end
-
- FileUtils.rm_r File.join(@gemhome, "gems", @pl1.original_name)
- end
-
- ##
- # Set the platform to +arch+
-
- def util_set_arch(arch)
- Gem::ConfigMap[:arch] = arch
- platform = Gem::Platform.new arch
-
- Gem.instance_variable_set :@platforms, nil
- Gem::Platform.instance_variable_set :@local, nil
-
- platform
- end
-
- ##
- # Sets up a fake fetcher using the gems from #util_make_gems. Optionally
- # additional +prerelease+ gems may be included.
- #
- # Gems created by this method may be fetched using Gem::RemoteFetcher.
-
- def util_setup_fake_fetcher(prerelease = false)
- require 'zlib'
- require 'socket'
- require 'rubygems/remote_fetcher'
-
- @fetcher = Gem::FakeFetcher.new
-
- util_make_gems(prerelease)
- Gem::Specification.reset
-
- @all_gems = [@a1, @a2, @a3a, @a_evil9, @b2, @c1_2].sort
- @all_gem_names = @all_gems.map { |gem| gem.full_name }
-
- gem_names = [@a1.full_name, @a2.full_name, @a3a.full_name, @b2.full_name]
- @gem_names = gem_names.sort.join("\n")
-
- Gem::RemoteFetcher.fetcher = @fetcher
- end
-
- ##
- # Sets up Gem::SpecFetcher to return information from the gems in +specs+.
- # Best used with +@all_gems+ from #util_setup_fake_fetcher.
-
- def util_setup_spec_fetcher(*specs)
- specs -= Gem::Specification._all
- Gem::Specification.add_specs(*specs)
-
- spec_fetcher = Gem::SpecFetcher.fetcher
-
- prerelease, _ = Gem::Specification.partition { |spec|
- spec.version.prerelease?
- }
-
- spec_fetcher.specs[@uri] = []
- Gem::Specification.each do |spec|
- spec_tuple = [spec.name, spec.version, spec.original_platform]
- spec_fetcher.specs[@uri] << spec_tuple
- end
-
- spec_fetcher.latest_specs[@uri] = []
- Gem::Specification.latest_specs.each do |spec|
- spec_tuple = [spec.name, spec.version, spec.original_platform]
- spec_fetcher.latest_specs[@uri] << spec_tuple
- end
-
- spec_fetcher.prerelease_specs[@uri] = []
- prerelease.each do |spec|
- spec_tuple = [spec.name, spec.version, spec.original_platform]
- spec_fetcher.prerelease_specs[@uri] << spec_tuple
- end
-
- v = Gem.marshal_version
-
- Gem::Specification.each do |spec|
- path = "#{@gem_repo}quick/Marshal.#{v}/#{spec.original_name}.gemspec.rz"
- data = Marshal.dump spec
- data_deflate = Zlib::Deflate.deflate data
- @fetcher.data[path] = data_deflate
- end unless Gem::RemoteFetcher === @fetcher # HACK for test_download_to_cache
-
- nil # force errors
- end
-
- ##
- # Deflates +data+
-
- def util_zip(data)
- Zlib::Deflate.deflate data
- end
-
- ##
- # Is this test being run on a Windows platform?
-
- def self.win_platform?
- Gem.win_platform?
- end
-
- ##
- # Is this test being run on a Windows platform?
-
- def win_platform?
- Gem.win_platform?
- end
-
- ##
- # Returns whether or not we're on a version of Ruby built with VC++ (or
- # Borland) versus Cygwin, Mingw, etc.
-
- def self.vc_windows?
- RUBY_PLATFORM.match('mswin')
- end
-
- ##
- # Returns whether or not we're on a version of Ruby built with VC++ (or
- # Borland) versus Cygwin, Mingw, etc.
-
- def vc_windows?
- RUBY_PLATFORM.match('mswin')
- end
-
- ##
- # Returns the make command for the current platform. For versions of Ruby
- # built on MS Windows with VC++ or Borland it will return 'nmake'. On all
- # other platforms, including Cygwin, it will return 'make'.
-
- def self.make_command
- ENV["make"] || (vc_windows? ? 'nmake' : 'make')
- end
-
- ##
- # Returns the make command for the current platform. For versions of Ruby
- # built on MS Windows with VC++ or Borland it will return 'nmake'. On all
- # other platforms, including Cygwin, it will return 'make'.
-
- def make_command
- ENV["make"] || (vc_windows? ? 'nmake' : 'make')
- end
-
- ##
- # Returns whether or not the nmake command could be found.
-
- def nmake_found?
- system('nmake /? 1>NUL 2>&1')
- end
-
- ##
- # Allows tests to use a random (but controlled) port number instead of
- # a hardcoded one. This helps CI tools when running parallels builds on
- # the same builder slave.
-
- def self.process_based_port
- @@process_based_port ||= 8000 + $$ % 1000
- end
-
- ##
- # See ::process_based_port
-
- def process_based_port
- self.class.process_based_port
- end
-
- ##
- # Allows the proper version of +rake+ to be used for the test.
-
- def build_rake_in
- gem_ruby = Gem.ruby
- Gem.ruby = @@ruby
- env_rake = ENV["rake"]
- ENV["rake"] = @@rake
- yield @@rake
- ensure
- Gem.ruby = gem_ruby
- if env_rake
- ENV["rake"] = env_rake
- else
- ENV.delete("rake")
- end
- end
-
- ##
- # Finds the path to the ruby executable
-
- def self.rubybin
- ruby = ENV["RUBY"]
- return ruby if ruby
- ruby = "ruby"
- rubyexe = "#{ruby}.exe"
-
- 3.times do
- if File.exist? ruby and File.executable? ruby and !File.directory? ruby
- return File.expand_path(ruby)
- end
- if File.exist? rubyexe and File.executable? rubyexe
- return File.expand_path(rubyexe)
- end
- ruby = File.join("..", ruby)
- end
-
- begin
- require "rbconfig"
- File.join(RbConfig::CONFIG["bindir"],
- RbConfig::CONFIG["ruby_install_name"] +
- RbConfig::CONFIG["EXEEXT"])
- rescue LoadError
- "ruby"
- end
- end
-
- @@ruby = rubybin
- env_rake = ENV['rake']
- ruby19_rake = File.expand_path("bin/rake", @@project_dir)
- @@rake = if env_rake then
- ENV["rake"]
- elsif File.exist? ruby19_rake then
- @@ruby + " " + ruby19_rake
- else
- 'rake'
- end
-
- ##
- # Construct a new Gem::Dependency.
-
- def dep name, *requirements
- Gem::Dependency.new name, *requirements
- end
-
- ##
- # Constructs a new Gem::Requirement.
-
- def req *requirements
- return requirements.first if Gem::Requirement === requirements.first
- Gem::Requirement.create requirements
- end
-
- ##
- # Constructs a new Gem::Specification.
-
- def spec name, version, &block
- Gem::Specification.new name, v(version), &block
- end
-
- ##
- # Construct a new Gem::Version.
-
- def v string
- Gem::Version.create string
- end
-
-end
-
diff --git a/lib/rubygems/test_utilities.rb b/lib/rubygems/test_utilities.rb
deleted file mode 100644
index 1a8fb5a0ad..0000000000
--- a/lib/rubygems/test_utilities.rb
+++ /dev/null
@@ -1,160 +0,0 @@
-require 'tempfile'
-require 'rubygems'
-require 'rubygems/remote_fetcher'
-
-##
-# A fake Gem::RemoteFetcher for use in tests or to avoid real live HTTP
-# requests when testing code that uses RubyGems.
-#
-# Example:
-#
-# @fetcher = Gem::FakeFetcher.new
-# @fetcher.data['http://gems.example.com/yaml'] = source_index.to_yaml
-# Gem::RemoteFetcher.fetcher = @fetcher
-#
-# # invoke RubyGems code
-#
-# paths = @fetcher.paths
-# assert_equal 'http://gems.example.com/yaml', paths.shift
-# assert paths.empty?, paths.join(', ')
-#
-# See RubyGems' tests for more examples of FakeFetcher.
-
-class Gem::FakeFetcher
-
- attr_reader :data
- attr_reader :last_request
- attr_accessor :paths
-
- def initialize
- @data = {}
- @paths = []
- end
-
- def find_data(path)
- path = path.to_s
- @paths << path
- raise ArgumentError, 'need full URI' unless path =~ %r'^https?://'
-
- unless @data.key? path then
- raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path)
- end
-
- @data[path]
- end
-
- def fetch_path path, mtime = nil
- data = find_data(path)
-
- if data.respond_to?(:call) then
- data.call
- else
- if path.to_s =~ /gz$/ and not data.nil? and not data.empty? then
- data = Gem.gunzip data
- end
-
- data
- end
- end
-
- # Thanks, FakeWeb!
- def open_uri_or_path(path)
- data = find_data(path)
- body, code, msg = data
-
- response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg)
- response.instance_variable_set(:@body, body)
- response.instance_variable_set(:@read, true)
- response
- end
-
- def request(uri, request_class, last_modified = nil)
- data = find_data(uri)
- body, code, msg = data
-
- @last_request = request_class.new uri.request_uri
- yield @last_request if block_given?
-
- response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg)
- response.instance_variable_set(:@body, body)
- response.instance_variable_set(:@read, true)
- response
- end
-
- def fetch_size(path)
- path = path.to_s
- @paths << path
-
- raise ArgumentError, 'need full URI' unless path =~ %r'^http://'
-
- unless @data.key? path then
- raise Gem::RemoteFetcher::FetchError.new("no data for #{path}", path)
- end
-
- data = @data[path]
-
- data.respond_to?(:call) ? data.call : data.length
- end
-
- def download spec, source_uri, install_dir = Gem.dir
- name = File.basename spec.cache_file
- path = File.join install_dir, "cache", name
-
- Gem.ensure_gem_subdirectories install_dir
-
- if source_uri =~ /^http/ then
- File.open(path, "wb") do |f|
- f.write fetch_path(File.join(source_uri, "gems", name))
- end
- else
- FileUtils.cp source_uri, path
- end
-
- path
- end
-
- def download_to_cache dependency
- found = Gem::SpecFetcher.fetcher.fetch dependency, true, true,
- dependency.prerelease?
-
- return if found.empty?
-
- spec, source_uri = found.first
-
- download spec, source_uri
- end
-
-end
-
-# :stopdoc:
-class Gem::RemoteFetcher
-
- def self.fetcher=(fetcher)
- @fetcher = fetcher
- end
-
-end
-# :startdoc:
-
-##
-# A StringIO duck-typed class that uses Tempfile instead of String as the
-# backing store.
-#
-# This is available when rubygems/test_utilities is required.
-#--
-# This class was added to flush out problems in Rubinius' IO implementation.
-
-class TempIO < Tempfile
- def initialize(string = '')
- super "TempIO"
- binmode
- write string
- rewind
- end
-
- def string
- flush
- Gem.read_binary path
- end
-end
-
diff --git a/lib/rubygems/text.rb b/lib/rubygems/text.rb
index 793cd953cb..0550dc473d 100644
--- a/lib/rubygems/text.rb
+++ b/lib/rubygems/text.rb
@@ -1,20 +1,41 @@
-require 'rubygems'
+# 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)
+ def format_text(text, wrap, indent = 0)
result = []
- work = text.dup
+ work = clean_text(text)
while work.length > wrap do
- if work =~ /^(.{0,#{wrap}})[ \n]/ then
+ if work =~ /^(.{0,#{wrap}})[ \n]/
result << $1.rstrip
work.slice!(0, $&.length)
else
@@ -26,40 +47,48 @@ module Gem::Text
result.join("\n").gsub(/^/, " " * indent)
end
- # This code is based directly on the Text gem implementation
- # Returns a value representing the "cost" of transforming str1 into str2
- def levenshtein_distance str1, str2
- s = str1
- t = str2
- n = s.length
- m = t.length
- max = n/2
+ def min3(a, b, c) # :nodoc:
+ if a < b && a < c
+ a
+ elsif b < c
+ b
+ else
+ c
+ end
+ end
- return m if (0 == n)
- return n if (0 == m)
- return n if (n - m).abs > max
+ # 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
- n.times do |i|
- e = i+1
-
- m.times do |j|
- cost = (s[i] == t[j]) ? 0 : 1
- x = [
- d[j+1] + 1, # insertion
- e + 1, # deletion
- d[j] + cost # substitution
- ].min
- d[j] = e
- e = x
- end
+ # 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
- return x
+ x
end
end
-
diff --git a/lib/rubygems/uninstaller.rb b/lib/rubygems/uninstaller.rb
index cc32ea48c4..fe4c3a80cf 100644
--- a/lib/rubygems/uninstaller.rb
+++ b/lib/rubygems/uninstaller.rb
@@ -1,14 +1,16 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'fileutils'
-require 'rubygems'
-require 'rubygems/dependency_list'
-require 'rubygems/doc_manager'
-require 'rubygems/user_interaction'
+require "fileutils"
+require_relative "../rubygems"
+require_relative "installer_uninstaller_utils"
+require_relative "dependency_list"
+require_relative "user_interaction"
##
# An Uninstaller.
@@ -19,16 +21,17 @@ require 'rubygems/user_interaction'
# 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 installed into
+ # The gem repository the gem will be uninstalled from
attr_reader :gem_home
@@ -39,27 +42,49 @@ class Gem::Uninstaller
attr_reader :spec
##
- # Constructs an uninstaller that will uninstall +gem+
+ # Constructs an uninstaller that will uninstall gem named +gem+.
+ # +options+ is a Hash with the following keys:
+ #
+ # :version:: Version requirement for the gem to uninstall. If not specified,
+ # uses Gem::Requirement.default.
+ # :install_dir:: The directory where the gem is installed. If not specified,
+ # uses Gem.dir.
+ # :executables:: Whether executables should be removed without confirmation or not. If nil, asks the user explicitly.
+ # :all:: If more than one version matches the requirement, whether to forcefully remove all matching versions or ask the user to select specific matching versions that should be removed.
+ # :ignore:: Ignore broken dependency checks when uninstalling.
+ # :bin_dir:: Directory containing executables to remove. If not specified,
+ # uses Gem.bindir.
+ # :format_executable:: In order to find executables to be removed, format executable names using Gem::Installer.exec_format.
+ # :abort_on_dependent:: Directly abort uninstallation if dependencies would be broken, rather than asking the user for confirmation.
+ # :check_dev:: When checking if uninstalling gem would leave broken dependencies around, also consider development dependencies.
+ # :force:: Set both :all and :ignore to true for forced uninstallation.
+ # :user_install:: Uninstall from user gem directory instead of system directory.
def initialize(gem, options = {})
- @gem = gem
- @version = options[:version] || Gem::Requirement.default
- @gem_home = File.expand_path(options[:install_dir] || Gem.dir)
- @force_executables = options[:executables]
- @force_all = options[:all]
- @force_ignore = options[:ignore]
- @bin_dir = options[:bin_dir]
- @format_executable = options[:format_executable]
+ @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 options[:install_dir]
+ @user_install = options[:user_install] unless @install_dir
- if @user_install then
- Gem.use_paths Gem.user_dir, @gem_home
- else
- Gem.use_paths @gem_home
- end
+ # Optimization: populated during #uninstall
+ @default_specs_matching_uninstall_params = []
end
##
@@ -67,26 +92,58 @@ class Gem::Uninstaller
# directory, and the cached .gem file.
def uninstall
- list = Gem::Specification.find_all_by_name(@gem, @version)
+ 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? then
+ 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?
- elsif list.size > 1 and @force_all then
+ 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 then
- gem_names = list.collect {|gem| gem.full_name} + ["All versions"]
+ 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 then
+ if index == list.size
remove_all list
- elsif index >= 0 && index < list.size then
+ elsif index && index >= 0 && index < list.size
uninstall_gem list[index]
else
- say "Error: must enter a number [1-#{list.size+1}]"
+ say "Error: must enter a number [1-#{list.size + 1}]"
end
else
uninstall_gem list.first
@@ -96,11 +153,13 @@ class Gem::Uninstaller
##
# Uninstalls gem +spec+
- def uninstall_gem(spec)
+ def uninstall_gem(stub)
+ spec = stub.to_spec
+
@spec = spec
unless dependencies_ok? spec
- unless ask_if_ok(spec)
+ if abort_on_dependent? || !ask_if_ok(spec)
raise Gem::DependencyRemovalException,
"Uninstallation aborted due to dependent gem(s)"
end
@@ -111,8 +170,13 @@ class Gem::Uninstaller
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
@@ -121,18 +185,20 @@ class Gem::Uninstaller
end
##
- # Removes installed executables and batch files (windows only) for
- # +gemspec+.
+ # Removes installed executables and batch files (windows only) for +spec+.
def remove_executables(spec)
- return if spec.nil? or spec.executables.empty?
-
- list = Gem::Specification.find_all { |s|
- s.name == spec.name && s.version != spec.version
- }
+ 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
@@ -141,20 +207,18 @@ class Gem::Uninstaller
return if executables.empty?
- executables = executables.map { |exec| formatted_program_filename exec }
+ executables = executables.map {|exec| formatted_program_filename exec }
- remove = if @force_executables.nil? then
- ask_yes_no("Remove executables:\n" \
- "\t#{executables.join ', '}\n\n" \
- "in addition to the gem?",
- true)
- else
- @force_executables
- end
-
- unless remove then
- say "Executables and scripts will remain installed."
+ 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
@@ -164,9 +228,11 @@ class Gem::Uninstaller
exe_file = File.join bin_dir, exe_name
- FileUtils.rm_f exe_file
- FileUtils.rm_f "#{exe_file}.bat"
+ safe_delete { FileUtils.rm exe_file }
+ safe_delete { FileUtils.rm "#{exe_file}.bat" }
end
+ else
+ say "Executables and scripts will remain installed."
end
end
@@ -176,21 +242,17 @@ class Gem::Uninstaller
# NOTE: removes uninstalled gems from +list+.
def remove_all(list)
- list.each { |spec| uninstall_gem spec }
+ list.each {|spec| uninstall_gem spec }
end
##
# spec:: the spec of the gem to be uninstalled
- # list:: the list of all such gems
- #
- # Warning: this method modifies the +list+ parameter. Once it has
- # uninstalled a gem, it is removed from that list.
def remove(spec)
- unless path_ok?(@gem_home, spec) or
- (@user_install and path_ok?(Gem.user_dir, spec)) then
+ unless path_ok?(@gem_home, spec) ||
+ (@user_install && path_ok?(@user_dir, spec))
e = Gem::GemNotInHomeException.new \
- "Gem is not installed in directory #{@gem_home}"
+ "Gem '#{spec.full_name}' is not installed in directory #{@gem_home}"
e.spec = spec
raise e
@@ -199,73 +261,180 @@ class Gem::Uninstaller
raise Gem::FilePermissionError, spec.base_dir unless
File.writable?(spec.base_dir)
- FileUtils.rm_rf spec.full_gem_path
+ full_gem_path = spec.full_gem_path
+ exclusions = []
+
+ if default_spec_matches?(spec) && spec.executables.any?
+ exclusions = spec.executables.map {|exe| File.join(spec.bin_dir, exe) }
+ exclusions << File.dirname(exclusions.last) until exclusions.last == full_gem_path
+ end
+
+ safe_delete { rm_r full_gem_path, exclusions: exclusions }
+ safe_delete { FileUtils.rm_r spec.extension_dir }
- # TODO: should this be moved to spec?... I vote eww (also exists in docmgr)
- old_platform_name = [spec.name,
- spec.version,
- spec.original_platform].join '-'
+ 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 then
+ unless File.exist? gemspec
gemspec = File.join(File.dirname(gemspec), "#{old_platform_name}.gemspec")
end
- FileUtils.rm_rf gemspec
+ safe_delete { FileUtils.rm_r gemspec }
+ announce_deletion_of(spec)
+ end
- gem = spec.cache_file
- gem = File.join(spec.cache_dir, "#{old_platform_name}.gem") unless
- File.exist? gem
+ ##
+ # Remove any plugin wrappers for +spec+.
- FileUtils.rm_rf gem
+ def remove_plugins(spec) # :nodoc:
+ return if spec.plugins.empty?
- Gem::DocManager.new(spec).uninstall_doc
+ remove_plugins_for(spec, plugin_dir_for(spec))
+ end
+
+ ##
+ # Regenerates plugin wrappers after removal.
- say "Successfully uninstalled #{spec.full_name}"
+ def regenerate_plugins
+ latest = specification_record.latest_spec_for(@spec.name)
+ return if latest.nil?
- Gem::Specification.remove_spec spec
+ 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 = 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
- def dependencies_ok?(spec)
+ ##
+ # 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)
+ deplist.ok_to_remove?(spec.full_name, @check_dev)
end
- def ask_if_ok(spec)
- msg = ['']
- msg << 'You have requested to uninstall the gem:'
+ ##
+ # 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 << ""
- spec.dependent_gems.each do |dep_spec, dep, satlist|
- msg <<
- ("#{dep_spec.name}-#{dep_spec.version} depends on " +
- "[#{dep.name} (#{dep.requirement})]")
+ siblings = Gem::Specification.select do |s|
+ s.name == spec.name && s.full_name != spec.full_name
end
- msg << 'If you remove this gems, one or more dependencies will not be met.'
- msg << 'Continue with Uninstall?'
- return ask_yes_no(msg.join("\n"), true)
+ 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
- def formatted_program_filename(filename)
- if @format_executable then
- require 'rubygems/installer'
+ ##
+ # 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
index 8024d37287..9fe3e755c4 100644
--- a/lib/rubygems/user_interaction.rb
+++ b/lib/rubygems/user_interaction.rb
@@ -1,14 +1,19 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
+require_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
@@ -62,13 +67,16 @@ module Gem::DefaultUserInteraction
def use_ui(new_ui, &block)
Gem::DefaultUserInteraction.use_ui(new_ui, &block)
end
-
end
##
-# Make the default UI accessible without the "ui." prefix. Classes
-# including this module may use the interaction methods on the default UI
-# directly. Classes may also reference the ui and ui= methods.
+# 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:
#
@@ -81,43 +89,79 @@ end
# end
module Gem::UserInteraction
-
include Gem::DefaultUserInteraction
- def alert(*args)
- ui.alert(*args)
+ ##
+ # Displays an alert +statement+. Asks a +question+ if given.
+
+ def alert(statement, question = nil)
+ ui.alert statement, question
end
- def alert_error(*args)
- ui.alert_error(*args)
+ ##
+ # 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
- def alert_warning(*args)
- ui.alert_warning(*args)
+ ##
+ # 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
- def ask(*args)
- ui.ask(*args)
+ ##
+ # Asks a +question+ and returns the answer.
+
+ def ask(question)
+ ui.ask question
end
- def ask_for_password(*args)
- ui.ask_for_password(*args)
+ ##
+ # Asks for a password with a +prompt+
+
+ def ask_for_password(prompt)
+ ui.ask_for_password prompt
end
- def ask_yes_no(*args)
- ui.ask_yes_no(*args)
+ ##
+ # 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
- def choose_from_list(*args)
- ui.choose_from_list(*args)
+ ##
+ # 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
- def say(*args)
- ui.say(*args)
+ ##
+ # Displays the given +statement+ on the standard output (or equivalent).
+
+ def say(statement = "")
+ ui.say statement
end
- def terminate_interaction(*args)
- ui.terminate_interaction(*args)
+ ##
+ # 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
@@ -125,22 +169,49 @@ end
# Gem::StreamUI implements a simple stream based user interface.
class Gem::StreamUI
+ ##
+ # The input stream
+
+ attr_reader :ins
+
+ ##
+ # The output stream
- attr_reader :ins, :outs, :errs
+ attr_reader :outs
- def initialize(in_stream, out_stream, err_stream=STDERR, usetty=true)
+ ##
+ # 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?
- if RUBY_VERSION < '1.9.3' and RUBY_PLATFORM =~ /mingw|mswin/ then
- @usetty
- else
- @usetty && @ins.tty?
- end
+ @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
##
@@ -152,7 +223,7 @@ class Gem::StreamUI
@outs.puts question
list.each_with_index do |item, index|
- @outs.puts " #{index+1}. #{item}"
+ @outs.puts " #{index + 1}. #{item}"
end
@outs.print "> "
@@ -163,7 +234,8 @@ class Gem::StreamUI
return nil, nil unless result
result = result.strip.to_i - 1
- return list[result], result
+ return nil, nil unless (0...list.size) === result
+ [list[result], result]
end
##
@@ -171,9 +243,9 @@ class Gem::StreamUI
# to a tty, raises an exception if default is nil, otherwise returns
# default.
- def ask_yes_no(question, default=nil)
- unless tty? then
- if default.nil? then
+ 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
@@ -183,12 +255,12 @@ class Gem::StreamUI
default_answer = case default
when nil
- 'yn'
+ "yn"
when true
- 'Yn'
+ "Yn"
else
- 'yN'
- end
+ "yN"
+ end
result = nil
@@ -197,113 +269,75 @@ class Gem::StreamUI
when /^y/i then true
when /^n/i then false
when /^$/ then default
- else nil
- end
+ end
end
- return result
+ result
end
##
# Ask a question. Returns an answer if connected to a tty, nil otherwise.
def ask(question)
- return nil if not tty?
+ return nil unless tty?
@outs.print(question + " ")
@outs.flush
result = @ins.gets
- result.chomp! if result
+ result&.chomp!
result
end
- if RUBY_VERSION > '1.9.2' then
- ##
- # Ask for a password. Does not echo response to terminal.
-
- def ask_for_password(question)
- return nil if not tty?
-
- require 'io/console'
-
- @outs.print(question + " ")
- @outs.flush
-
- password = @ins.noecho {@ins.gets}
- password.chomp! if password
- password
- end
- else
- ##
- # Ask for a password. Does not echo response to terminal.
+ ##
+ # Ask for a password. Does not echo response to terminal.
- def ask_for_password(question)
- return nil if not tty?
+ def ask_for_password(question)
+ return nil unless tty?
- @outs.print(question + " ")
- @outs.flush
+ @outs.print(question, " ")
+ @outs.flush
- Gem.win_platform? ? ask_for_password_on_windows : ask_for_password_on_unix
- end
+ password = _gets_noecho
+ @outs.puts
+ password&.chomp!
+ password
+ end
- ##
- # Asks for a password that works on windows. Ripped from the Heroku gem.
-
- def ask_for_password_on_windows
- return nil if not tty?
-
- require "Win32API"
- char = nil
- password = ''
-
- while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
- break if char == 10 || char == 13 # received carriage return or newline
- if char == 127 || char == 8 # backspace and delete
- password.slice!(-1, 1)
- else
- password << char.chr
- end
+ def require_io_console
+ @require_io_console ||= begin
+ begin
+ require "io/console"
+ rescue LoadError
end
-
- puts
- password
+ true
end
+ end
- ##
- # Asks for a password that works on unix
-
- def ask_for_password_on_unix
- return nil if not tty?
-
- system "stty -echo"
- password = @ins.gets
- password.chomp! if password
- system "stty echo"
- password
- end
+ def _gets_noecho
+ require_io_console
+ @ins.noecho { @ins.gets }
end
##
# Display a statement.
- def say(statement="")
+ def say(statement = "")
@outs.puts statement
end
##
# Display an informational alert. Will ask +question+ if it is not nil.
- def alert(statement, question=nil)
+ def alert(statement, question = nil)
@outs.puts "INFO: #{statement}"
ask(question) if question
end
##
- # Display a warning in a location expected to get error messages. Will
- # ask +question+ if it is not nil.
+ # Display a warning on stderr. Will ask +question+ if it is not nil.
- def alert_warning(statement, question=nil)
+ def alert_warning(statement, question = nil)
@errs.puts "WARNING: #{statement}"
ask(question) if question
end
@@ -312,34 +346,27 @@ class Gem::StreamUI
# Display an error message in a location expected to get error messages.
# Will ask +question+ if it is not nil.
- def alert_error(statement, question=nil)
+ def alert_error(statement, question = nil)
@errs.puts "ERROR: #{statement}"
ask(question) if question
end
##
- # Display a debug message on the same location as error messages.
-
- def debug(statement)
- @errs.puts statement
- 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)
- if self.kind_of?(Gem::SilentUI)
- return SilentProgressReporter.new(@outs, *args)
- end
-
case Gem.configuration.verbose
when nil, false
SilentProgressReporter.new(@outs, *args)
@@ -354,14 +381,28 @@ class Gem::StreamUI
# 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
@@ -370,13 +411,19 @@ class Gem::StreamUI
# A basic dotted progress reporter.
class SimpleProgressReporter
-
include Gem::DefaultUserInteraction
+ ##
+ # The number of progress items counted so far.
+
attr_reader :count
- def initialize(out_stream, size, initial_message,
- terminal_message = "complete")
+ ##
+ # 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
@@ -400,20 +447,25 @@ class Gem::StreamUI
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
- def initialize(out_stream, size, initial_message,
- terminal_message = 'complete')
+ ##
+ # 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
@@ -442,15 +494,10 @@ class Gem::StreamUI
# Return a download reporter object chosen from the current verbosity
def download_reporter(*args)
- if self.kind_of?(Gem::SilentUI)
- return SilentDownloadReporter.new(@outs, *args)
- end
-
- case Gem.configuration.verbose
- when nil, false
+ if [nil, false].include?(Gem.configuration.verbose) || !@outs.tty?
SilentDownloadReporter.new(@outs, *args)
else
- VerboseDownloadReporter.new(@outs, *args)
+ ThreadedDownloadReporter.new(@outs, *args)
end
end
@@ -458,78 +505,99 @@ class Gem::StreamUI
# 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 prints out messages about the current progress.
+ # A progress reporter that behaves nicely with threaded downloading.
- class VerboseDownloadReporter
- attr_reader :file_name, :total_bytes, :progress
+ 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
- @progress = 0
end
- def fetch(file_name, total_bytes)
- @file_name = file_name
- @total_bytes = total_bytes.to_i
- @units = @total_bytes.zero? ? 'B' : '%'
+ ##
+ # Tells the download reporter that the +file_name+ is being fetched.
+ # The other arguments are ignored.
- update_display(false)
+ def fetch(file_name, *args)
+ if @file_name.nil?
+ @file_name = file_name
+ locked_puts "Fetching #{@file_name}"
+ end
end
- def update(bytes)
- new_progress = if @units == 'B' then
- bytes
- else
- ((bytes.to_f * 100) / total_bytes.to_f).ceil
- end
-
- return if new_progress == @progress
+ ##
+ # Updates the threaded download reporter for the given number of +bytes+.
- @progress = new_progress
- update_display
+ def update(bytes)
+ # Do nothing.
end
+ ##
+ # Indicates the download is complete.
+
def done
- @progress = 100 if @units == '%'
- update_display(true, true)
+ # Do nothing.
end
private
- def update_display(show_progress = true, new_line = false)
- return unless @out.tty?
-
- if show_progress then
- @out.print "\rFetching: %s (%3d%s)" % [@file_name, @progress, @units]
- else
- @out.print "Fetching: %s" % @file_name
+ def locked_puts(message)
+ MUTEX.synchronize do
+ @out.puts message
end
- @out.puts if new_line
end
end
end
##
-# Subclass of StreamUI that instantiates the user interaction using STDIN,
-# STDOUT, and STDERR.
+# Subclass of StreamUI that instantiates the user interaction using $stdin,
+# $stdout, and $stderr.
class Gem::ConsoleUI < Gem::StreamUI
+ ##
+ # 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
+ super $stdin, $stdout, $stderr, true
end
end
@@ -537,26 +605,43 @@ 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
- reader, writer = nil, nil
-
- begin
- reader = File.open('/dev/null', 'r')
- writer = File.open('/dev/null', 'w')
- rescue Errno::ENOENT
- reader = File.open('nul', 'r')
- writer = File.open('nul', 'w')
- end
+ io = NullIO.new
+ super io, io, io, false
+ end
- super reader, writer, writer, false
+ def close
end
- def download_reporter(*args)
+ def download_reporter(*args) # :nodoc:
SilentDownloadReporter.new(@outs, *args)
end
- def progress_reporter(*args)
+ def progress_reporter(*args) # :nodoc:
SilentProgressReporter.new(@outs, *args)
end
-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
index ffeed88660..eb5b513570 100644
--- a/lib/rubygems/validator.rb
+++ b/lib/rubygems/validator.rb
@@ -1,74 +1,50 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems/format'
-require 'rubygems/installer'
+require_relative "package"
+require_relative "installer"
##
# Validator performs various gem file and gem database validation
class Gem::Validator
-
include Gem::UserInteraction
- def initialize
- require 'find'
- require 'digest'
- end
-
- ##
- # Given a gem file's contents, validates against its own MD5 checksum
- # gem_data:: [String] Contents of the gem file
-
- def verify_gem(gem_data)
- raise Gem::VerificationError, 'empty gem file' if gem_data.size == 0
-
- unless gem_data =~ /MD5SUM/ then
- return # Don't worry about it...this sucks. Need to fix MD5 stuff for
- # new format
- # FIXME
- end
-
- sum_data = gem_data.gsub(/MD5SUM = "([a-z0-9]+)"/,
- "MD5SUM = \"#{"F" * 32}\"")
-
- unless Digest::MD5.hexdigest(sum_data) == $1.to_s then
- raise Gem::VerificationError, 'invalid checksum for gem file'
- end
- end
-
- ##
- # Given the path to a gem file, validates against its own MD5 checksum
- #
- # gem_path:: [String] Path to gem file
-
- def verify_gem_file(gem_path)
- open gem_path, Gem.binary_mode do |file|
- gem_data = file.read
- verify_gem gem_data
- end
- rescue Errno::ENOENT, Errno::EINVAL
- raise Gem::VerificationError, "missing gem file #{gem_path}"
+ 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(/^\//, "")
+ fn = file_name[gem_directory.size..file_name.size - 1].sub(%r{^/}, "")
installed_files << fn unless
- fn =~ /CVS/ || fn.empty? || File.directory?(file_name)
+ fn.empty? || fn.include?("CVS") || File.directory?(file_name)
end
+
installed_files
end
public
- ErrorData = Struct.new :path, :problem
+ ##
+ # 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
@@ -80,70 +56,73 @@ class Gem::Validator
# * 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] = {} }
+ def alien(gems = [])
+ errors = Hash.new {|h,k| h[k] = {} }
- Gem::SourceIndex.from_installed_gems.each do |gem_name, gem_spec|
- next unless gems.include? gem_spec.name unless gems.empty?
+ Gem::Specification.each do |spec|
+ unless gems.empty?
+ next unless gems.include? spec.name
+ end
+ next if spec.default_gem?
- install_dir = gem_spec.installation_path
- gem_path = Gem.cache_gem(gem_spec.file_name, install_dir)
- spec_path = File.join install_dir, "specifications", gem_spec.spec_name
- gem_directory = gem_spec.full_gem_path
+ 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 then
- errors[gem_name][gem_spec.full_name] =
+ 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 then
+ unless File.exist? spec_path
errors[gem_name][spec_path] = "Spec file missing for installed gem"
end
begin
- verify_gem_file(gem_path)
+ unless File.readable?(gem_path)
+ raise Gem::VerificationError, "missing gem file #{gem_path}"
+ end
good, gone, unreadable = nil, nil, nil, nil
- open gem_path, Gem.binary_mode do |file|
- format = Gem::Format.from_file_by_path(gem_path)
+ File.open gem_path, Gem.binary_mode do |_file|
+ package = Gem::Package.new gem_path
- good, gone = format.file_entries.partition { |entry, _|
- File.exist? File.join(gem_directory, entry['path'])
- }
+ good, gone = package.contents.partition do |file_name|
+ File.exist? File.join(gem_directory, file_name)
+ end
- gone.map! { |entry, _| entry['path'] }
gone.sort.each do |path|
errors[gem_name][path] = "Missing file"
end
- good, unreadable = good.partition { |entry, _|
- File.readable? File.join(gem_directory, entry['path'])
- }
+ good, unreadable = good.partition do |file_name|
+ File.readable? File.join(gem_directory, file_name)
+ end
- unreadable.map! { |entry, _| entry['path'] }
unreadable.sort.each do |path|
errors[gem_name][path] = "Unreadable file"
end
good.each do |entry, data|
- begin
- next unless data # HACK `gem check -a mkrf`
-
- open File.join(gem_directory, entry['path']), Gem.binary_mode do |f|
- unless Digest::MD5.hexdigest(f.read).to_s ==
- Digest::MD5.hexdigest(data).to_s then
- errors[gem_name][entry['path']] = "Modified from original"
- end
+ 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)
- good.map! { |entry, _| entry['path'] }
extras = installed_files - good - unreadable
extras.each do |extra|
@@ -155,15 +134,11 @@ class Gem::Validator
end
errors.each do |name, subhash|
- errors[name] = subhash.map { |path, msg| ErrorData.new(path, msg) }
+ errors[name] = subhash.map do |path, msg|
+ ErrorData.new path, msg
+ end.sort
end
errors
end
-
- def remove_leading_dot_dir(path)
- path.sub(/^\.\//, "")
- 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
index 62af34462d..306733c1d7 100644
--- a/lib/rubygems/version.rb
+++ b/lib/rubygems/version.rb
@@ -1,3 +1,10 @@
+# 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
@@ -22,143 +29,169 @@
# 3. 1.0.a.2
# 4. 0.9
#
-# == How Software Changes
-#
-# Users expect to be able to specify a version constraint that gives them
-# some reasonable expectation that new versions of a library will work with
-# their software if the version constraint is true, and not work with their
-# software if the version constraint is false. In other words, the perfect
-# system will accept all compatible versions of the library and reject all
-# incompatible versions.
-#
-# Libraries change in 3 ways (well, more than 3, but stay focused here!).
+# If you want to specify a version restriction that includes both prereleases
+# and regular releases of 1.x or later versions:
#
-# 1. The change may be an implementation detail only and have no effect on
-# the client software.
-# 2. The change may add new features, but do so in a way that client software
-# written to an earlier version is still compatible.
-# 3. The change may change the public interface of the library in such a way
-# that old software is no longer compatible.
+# s.add_dependency 'example', '>= 1.0.0.a'
#
-# Some examples are appropriate at this point. Suppose I have a Stack class
-# that supports a <tt>push</tt> and a <tt>pop</tt> method.
-#
-# === Examples of Category 1 changes:
-#
-# * Switch from an array based implementation to a linked-list based
-# implementation.
-# * Provide an automatic (and transparent) backing store for large stacks.
+# == How Software Changes
#
-# === Examples of Category 2 changes might be:
+# Libraries generally change in 3 ways:
#
-# * Add a <tt>depth</tt> method to return the current depth of the stack.
-# * Add a <tt>top</tt> method that returns the current top of stack (without
-# changing the stack).
-# * Change <tt>push</tt> so that it returns the item pushed (previously it
-# had no usable return value).
+# 1. The change is an implementation detail, bug fix, security fix, or
+# optimization, and has no behavioral effect on the software using it.
#
-# === Examples of Category 3 changes might be:
+# 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.
#
-# * Changes <tt>pop</tt> so that it no longer returns a value (you must use
-# <tt>top</tt> to get the top of the stack).
-# * Rename the methods to <tt>push_item</tt> and <tt>pop_item</tt>.
+# 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
+# == RubyGems Rational Versioning (the recommended approach)
#
# * Versions shall be represented by three non-negative integers, separated
-# by periods (e.g. 3.1.4). The first integers is the "major" version
+# by periods (e.g. 3.1.4). The first integer is the "major" version
# number, the second integer is the "minor" version number, and the third
-# integer is the "build" number.
+# integer is the "patch" version number.
#
-# * A category 1 change (implementation detail) will increment the build
-# number.
+# * A category 1 change (implementation detail, bug fix, or security fix)
+# will increment the patch number.
#
# * A category 2 change (backwards compatible) will increment the minor
-# version number and reset the build number.
-#
-# * A category 3 change (incompatible) will increment the major build number
-# and reset the minor and build numbers.
-#
-# * Any "public" release of a gem should have a different version. Normally
-# that means incrementing the build number. This means a developer can
-# generate builds all day long for himself, but as soon as he/she makes a
-# public release, the version must be updated.
-#
-# === Examples
-#
-# Let's work through a project lifecycle using our Stack example from above.
-#
-# Version 0.0.1:: The initial Stack class is release.
-# Version 0.0.2:: Switched to a linked=list implementation because it is
-# cooler.
-# Version 0.1.0:: Added a <tt>depth</tt> method.
-# Version 1.0.0:: Added <tt>top</tt> and made <tt>pop</tt> return nil
-# (<tt>pop</tt> used to return the old top item).
-# Version 1.1.0:: <tt>push</tt> now returns the value pushed (it used it
-# return nil).
-# Version 1.1.1:: Fixed a bug in the linked list implementation.
-# Version 1.1.2:: Fixed a bug introduced in the last fix.
+# version number and reset the patch number.
#
-# Client A needs a stack with basic push/pop capability. He writes to the
-# original interface (no <tt>top</tt>), so his version constraint looks
-# like:
+# * A category 3 change (incompatible) will increment the major version number
+# and reset the minor and patch numbers.
#
-# gem 'stack', '~> 0.0'
+# * Any "public" release of a gem should have a different version.
#
-# Essentially, any version is OK with Client A. An incompatible change to
-# the library will cause him grief, but he is willing to take the chance (we
-# call Client A optimistic).
+# == Optimistic Vs. Pessimistic Dependency Versioning
#
-# Client B is just like Client A except for two things: (1) He uses the
-# <tt>depth</tt> method and (2) he is worried about future
-# incompatibilities, so he writes his version constraint like this:
-#
-# gem 'stack', '~> 0.1'
-#
-# The <tt>depth</tt> method was introduced in version 0.1.0, so that version
-# or anything later is fine, as long as the version stays below version 1.0
-# where incompatibilities are introduced. We call Client B pessimistic
-# because he is worried about incompatible future changes (it is OK to be
-# pessimistic!).
-#
-# == Preventing Version Catastrophe:
-#
-# From: http://blog.zenspider.com/2008/10/rubygems-howto-preventing-cata.html
-#
-# Let's say you're depending on the fnord gem version 2.y.z. If you
-# specify your dependency as ">= 2.0.0" then, you're good, right? What
-# happens if fnord 3.0 comes out and it isn't backwards compatible
-# with 2.y.z? Your stuff will break as a result of using ">=". The
-# better route is to specify your dependency with a "spermy" version
-# specifier. They're a tad confusing, so here is how the dependency
-# specifiers work:
-#
-# Specification From ... To (exclusive)
-# ">= 3.0" 3.0 ... &infin;
-# "~> 3.0" 3.0 ... 4.0
-# "~> 3.0.0" 3.0.0 ... 3.1
-# "~> 3.5" 3.5 ... 4.0
-# "~> 3.5.0" 3.5.0 ... 3.6
+# 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
- autoload :Requirement, 'rubygems/requirement'
-
include Comparable
- VERSION_PATTERN = '[0-9]+(\.[0-9a-zA-Z]+)*' # :nodoc:
- ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})*\s*\z/ # :nodoc:
+ VERSION_PATTERN = '[0-9]+(?>\.[0-9a-zA-Z]+)*(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?' # :nodoc:
+ ANCHORED_VERSION_PATTERN = /\A\s*(#{VERSION_PATTERN})?\s*\z/ # :nodoc:
+ RADIX_OPT = [9_500, 3_500, 260_000, 22_227, 24].freeze # :nodoc:
##
# A string representation of this Version.
- attr_reader :version
- alias to_s version
+ def version
+ @version
+ end
+
+ alias_method :to_s, :version
##
# True if the +version+ string matches RubyGems' requirements.
- def self.correct? version
- version.to_s =~ ANCHORED_VERSION_PATTERN
+ def self.correct?(version)
+ version.nil? || ANCHORED_VERSION_PATTERN.match?(version.to_s)
end
##
@@ -167,28 +200,48 @@ class Gem::Version
#
# ver1 = Version.create('1.3.17') # -> (Version object)
# ver2 = Version.create(ver1) # -> (ver1)
- # ver3 = Version.create(nil) # -> nil
- def self.create input
- if input.respond_to? :version then
+ def self.create(input)
+ if self === input # check yourself before you wreck yourself
input
- elsif input.nil? then
- nil
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
- raise ArgumentError, "Malformed version number string #{version}" unless
- self.class.correct?(version)
+ 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
- @version.strip!
+
+ # 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
##
@@ -198,24 +251,30 @@ class Gem::Version
# Pre-release (alpha) parts, e.g, 5.3.1.b.2 => 5.4, are ignored.
def bump
- segments = self.segments.dup
- segments.pop while segments.any? { |s| String === s }
- segments.pop if segments.size > 1
-
- segments[-1] = segments[-1].succ
- self.class.new segments.join(".")
+ @@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 and @version == other.version
+ def eql?(other)
+ self.class === other && @version == other.version
end
def hash # :nodoc:
- @hash ||= segments.hash
+ canonical_segments.hash
+ end
+
+ def init_with(coder) # :nodoc:
+ yaml_initialize coder.tag, coder.map
end
def inspect # :nodoc:
@@ -227,25 +286,41 @@ class Gem::Version
# string for backwards (RubyGems 1.3.5 and earlier) compatibility.
def marshal_dump
- [version]
+ [@version]
end
##
# Load custom marshal format. It's a string for backwards (RubyGems
# 1.3.5 and earlier) compatibility.
- def marshal_load array
- initialize array[0]
+ 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?
- @prerelease ||= @version =~ /[a-zA-Z]/
+ unless instance_variable_defined? :@prerelease
+ @prerelease = /[a-zA-Z]/.match?(version)
+ end
+ @prerelease
end
- def pretty_print q # :nodoc:
+ def pretty_print(q) # :nodoc:
q.text "Gem::Version.new(#{version.inspect})"
end
@@ -254,66 +329,144 @@ class Gem::Version
# Non-prerelease versions return themselves.
def release
- return self unless prerelease?
-
- segments = self.segments.dup
- segments.pop while segments.any? { |s| String === s }
- self.class.new segments.join('.')
+ @@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 is lazy so it can pick up version values that come from
- # old marshaled versions, which don't go through marshal_load.
-
- @segments ||= @version.scan(/[0-9]+|[a-z]+/i).map do |s|
- /^\d+$/ =~ s ? s.to_i : s
- end
+ _segments.dup
end
##
- # A recommended version for use with a ~> Requirement.
+ # A recommended version for use with a >= Requirement.
- def spermy_recommendation
- segments = self.segments.dup
+ def approximate_recommendation
+ segments = self.segments
- segments.pop while segments.any? { |s| String === s }
+ segments.pop while segments.any? {|s| String === s }
segments.pop while segments.size > 2
segments.push 0 while segments.size < 2
- "~> #{segments.join(".")}"
+ 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. Attempts to compare to something that's not a
- # <tt>Gem::Version</tt> return +nil+.
+ # 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
- def <=> other
- return unless Gem::Version === other
- return 0 if @version == other.version
+ # 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
- lhsegments = segments
- rhsegments = other.segments
+ def freeze
+ prerelease?
+ _segments
+ canonical_segments
+ super
+ end
- lhsize = lhsegments.size
- rhsize = rhsegments.size
- limit = (lhsize > rhsize ? lhsize : rhsize) - 1
+ protected
- i = 0
+ attr_reader :sort_key # :nodoc:
- while i <= limit
- lhs, rhs = lhsegments[i] || 0, rhsegments[i] || 0
- i += 1
+ def compute_sort_key
+ return if prerelease?
- next if lhs == rhs
- return -1 if String === lhs && Numeric === rhs
- return 1 if Numeric === lhs && String === rhs
+ segments = canonical_segments
+ return if segments.size > 5
- return lhs <=> rhs
+ 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
- return 0
+ 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
index a3de4dc9e7..7910fd3d1b 100644
--- a/lib/rubygems/version_option.rb
+++ b/lib/rubygems/version_option.rb
@@ -1,32 +1,32 @@
+# frozen_string_literal: true
+
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++
-require 'rubygems'
+require_relative "../rubygems"
##
# Mixin methods for --version and --platform Gem::Command options.
module Gem::VersionOption
-
##
# Add the --platform option to the option parser.
def add_platform_option(task = command, *wrap)
- OptionParser.accept Gem::Platform do |value|
- if value == Gem::Platform::RUBY then
+ 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] then
+ 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
@@ -42,6 +42,7 @@ module Gem::VersionOption
add_option("--[no-]prerelease",
"Allow prerelease versions of a gem", *wrap) do |value, options|
options[:prerelease] = value
+ options[:explicit_prerelease] = true
end
end
@@ -49,17 +50,31 @@ module Gem::VersionOption
# Add the --version option to the option parser.
def add_version_option(task = command, *wrap)
- OptionParser.accept Gem::Requirement do |value|
- Gem::Requirement.new value
+ Gem::OptionParser.accept Gem::Requirement do |value|
+ Gem::Requirement.new(*value.split(/\s*,\s*/))
end
- add_option('-v', '--version VERSION', Gem::Requirement,
- "Specify version of gem to #{task}", *wrap) do
- |value, options|
- options[:version] = value
- options[:prerelease] = true if value.prerelease?
+ 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
-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