bundler 4.0.22 → 4.1.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/{lib/bundler/vendor/connection_pool/LICENSE → MIT.txt} +8 -6
  3. data/bundler.gemspec +23 -6
  4. data/lib/bundler/build_metadata.rb +2 -2
  5. data/lib/bundler/checksum.rb +40 -25
  6. data/lib/bundler/ci_detector.rb +1 -1
  7. data/lib/bundler/cli/add.rb +3 -4
  8. data/lib/bundler/cli/cache.rb +4 -0
  9. data/lib/bundler/cli/clean.rb +5 -0
  10. data/lib/bundler/cli/common.rb +41 -1
  11. data/lib/bundler/cli/config.rb +4 -2
  12. data/lib/bundler/cli/doctor/ssl.rb +4 -8
  13. data/lib/bundler/cli/gem.rb +0 -17
  14. data/lib/bundler/cli/info.rb +1 -1
  15. data/lib/bundler/cli/install.rb +4 -5
  16. data/lib/bundler/cli/lock.rb +15 -5
  17. data/lib/bundler/cli/open.rb +7 -1
  18. data/lib/bundler/cli/outdated.rb +16 -13
  19. data/lib/bundler/cli/pristine.rb +2 -1
  20. data/lib/bundler/cli/show.rb +5 -1
  21. data/lib/bundler/cli/update.rb +20 -11
  22. data/lib/bundler/cli.rb +15 -9
  23. data/lib/bundler/constants.rb +1 -1
  24. data/lib/bundler/definition.rb +102 -15
  25. data/lib/bundler/dependency.rb +4 -0
  26. data/lib/bundler/dsl.rb +137 -13
  27. data/lib/bundler/endpoint_specification.rb +51 -14
  28. data/lib/bundler/env.rb +7 -24
  29. data/lib/bundler/errors.rb +34 -5
  30. data/lib/bundler/fetcher/compact_index.rb +14 -5
  31. data/lib/bundler/fetcher/connection_pools.rb +150 -0
  32. data/lib/bundler/fetcher/downloader.rb +35 -16
  33. data/lib/bundler/fetcher.rb +8 -47
  34. data/lib/bundler/gem_helper.rb +1 -1
  35. data/lib/bundler/injector.rb +3 -3
  36. data/lib/bundler/inline.rb +44 -4
  37. data/lib/bundler/installer/parallel_installer.rb +27 -31
  38. data/lib/bundler/installer.rb +21 -7
  39. data/lib/bundler/lazy_specification.rb +45 -18
  40. data/lib/bundler/lockfile_generator.rb +24 -2
  41. data/lib/bundler/lockfile_parser.rb +44 -6
  42. data/lib/bundler/man/bundle-add.1 +7 -4
  43. data/lib/bundler/man/bundle-add.1.ronn +8 -4
  44. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  45. data/lib/bundler/man/bundle-cache.1 +2 -2
  46. data/lib/bundler/man/bundle-cache.1.ronn +2 -2
  47. data/lib/bundler/man/bundle-check.1 +1 -1
  48. data/lib/bundler/man/bundle-clean.1 +1 -1
  49. data/lib/bundler/man/bundle-config.1 +38 -6
  50. data/lib/bundler/man/bundle-config.1.ronn +143 -10
  51. data/lib/bundler/man/bundle-console.1 +1 -1
  52. data/lib/bundler/man/bundle-doctor.1 +1 -1
  53. data/lib/bundler/man/bundle-env.1 +1 -1
  54. data/lib/bundler/man/bundle-exec.1 +1 -1
  55. data/lib/bundler/man/bundle-fund.1 +1 -1
  56. data/lib/bundler/man/bundle-gem.1 +1 -1
  57. data/lib/bundler/man/bundle-help.1 +1 -1
  58. data/lib/bundler/man/bundle-info.1 +5 -2
  59. data/lib/bundler/man/bundle-info.1.ronn +4 -0
  60. data/lib/bundler/man/bundle-init.1 +1 -1
  61. data/lib/bundler/man/bundle-install.1 +2 -2
  62. data/lib/bundler/man/bundle-install.1.ronn +4 -4
  63. data/lib/bundler/man/bundle-issue.1 +1 -1
  64. data/lib/bundler/man/bundle-licenses.1 +1 -1
  65. data/lib/bundler/man/bundle-list.1 +1 -1
  66. data/lib/bundler/man/bundle-lock.1 +2 -2
  67. data/lib/bundler/man/bundle-lock.1.ronn +3 -2
  68. data/lib/bundler/man/bundle-open.1 +5 -2
  69. data/lib/bundler/man/bundle-open.1.ronn +4 -1
  70. data/lib/bundler/man/bundle-outdated.1 +1 -1
  71. data/lib/bundler/man/bundle-platform.1 +1 -1
  72. data/lib/bundler/man/bundle-plugin.1 +1 -1
  73. data/lib/bundler/man/bundle-pristine.1 +1 -1
  74. data/lib/bundler/man/bundle-remove.1 +1 -1
  75. data/lib/bundler/man/bundle-show.1 +5 -2
  76. data/lib/bundler/man/bundle-show.1.ronn +4 -0
  77. data/lib/bundler/man/bundle-update.1 +2 -2
  78. data/lib/bundler/man/bundle-update.1.ronn +2 -2
  79. data/lib/bundler/man/bundle-version.1 +1 -1
  80. data/lib/bundler/man/bundle.1 +1 -1
  81. data/lib/bundler/man/gemfile.5 +77 -1
  82. data/lib/bundler/man/gemfile.5.ronn +104 -0
  83. data/lib/bundler/match_metadata.rb +20 -0
  84. data/lib/bundler/match_platform.rb +25 -2
  85. data/lib/bundler/match_remote_metadata.rb +21 -6
  86. data/lib/bundler/materialization.rb +5 -2
  87. data/lib/bundler/override.rb +97 -0
  88. data/lib/bundler/plugin/dsl.rb +3 -5
  89. data/lib/bundler/plugin/events.rb +55 -13
  90. data/lib/bundler/plugin/index.rb +87 -11
  91. data/lib/bundler/plugin/installer/path.rb +1 -1
  92. data/lib/bundler/plugin/installer.rb +13 -5
  93. data/lib/bundler/plugin.rb +58 -30
  94. data/lib/bundler/remote_specification.rb +6 -3
  95. data/lib/bundler/resolver/base.rb +2 -1
  96. data/lib/bundler/resolver/incompatibility.rb +1 -1
  97. data/lib/bundler/resolver.rb +97 -23
  98. data/lib/bundler/ruby_version.rb +0 -2
  99. data/lib/bundler/rubygems_ext.rb +71 -1
  100. data/lib/bundler/rubygems_gem_installer.rb +41 -2
  101. data/lib/bundler/rubygems_integration.rb +7 -2
  102. data/lib/bundler/runtime.rb +69 -2
  103. data/lib/bundler/settings.rb +383 -56
  104. data/lib/bundler/shared_helpers.rb +2 -1
  105. data/lib/bundler/source/git/git_proxy.rb +31 -2
  106. data/lib/bundler/source/git.rb +49 -13
  107. data/lib/bundler/source/path/installer.rb +1 -1
  108. data/lib/bundler/source/rubygems/remote.rb +10 -5
  109. data/lib/bundler/source/rubygems.rb +48 -12
  110. data/lib/bundler/source/rubygems_aggregate.rb +1 -1
  111. data/lib/bundler/source_map.rb +7 -3
  112. data/lib/bundler/spec_set.rb +23 -7
  113. data/lib/bundler/stub_specification.rb +2 -1
  114. data/lib/bundler/templates/newgem/Gemfile.tt +4 -4
  115. data/lib/bundler/templates/newgem/newgem.gemspec.tt +3 -3
  116. data/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +1 -1
  117. data/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt +1 -1
  118. data/lib/bundler/ui/shell.rb +0 -4
  119. data/lib/bundler/ui/silent.rb +0 -4
  120. data/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +35 -2
  121. data/lib/bundler/vendor/thor/lib/thor/base.rb +2 -1
  122. data/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +14 -6
  123. data/lib/bundler/vendor/thor/lib/thor/shell/color.rb +2 -0
  124. data/lib/bundler/vendor/thor/lib/thor/version.rb +1 -1
  125. data/lib/bundler/vendor/thor/lib/thor.rb +34 -1
  126. data/lib/bundler/vendored_pub_grub.rb +5 -2
  127. data/lib/bundler/vendored_securerandom.rb +11 -8
  128. data/lib/bundler/vendored_uri.rb +14 -14
  129. data/lib/bundler/version.rb +1 -1
  130. data/lib/bundler/worker.rb +3 -2
  131. data/lib/bundler.rb +1 -1
  132. data/lib/rubygems/compact_index_client/cache.rb +122 -0
  133. data/lib/rubygems/compact_index_client/cache_file.rb +147 -0
  134. data/lib/rubygems/compact_index_client/http_fetcher.rb +90 -0
  135. data/lib/rubygems/compact_index_client/parser.rb +90 -0
  136. data/lib/rubygems/compact_index_client/updater.rb +113 -0
  137. data/lib/rubygems/compact_index_client.rb +111 -0
  138. data/lib/rubygems/cooldown_settings.rb +57 -0
  139. data/lib/rubygems/credential_store/native/linux.rb +97 -0
  140. data/lib/rubygems/credential_store/native/macos.rb +88 -0
  141. data/lib/rubygems/credential_store/native/windows.rb +130 -0
  142. data/lib/rubygems/credential_store.rb +332 -0
  143. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/assignment.rb +1 -1
  144. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +7 -7
  145. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/failure_writer.rb +1 -1
  146. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/incompatibility.rb +4 -4
  147. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/package.rb +1 -1
  148. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/partial_solution.rb +1 -1
  149. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/rubygems.rb +3 -3
  150. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/solve_failure.rb +1 -1
  151. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/static_package_source.rb +3 -3
  152. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/strategy.rb +2 -2
  153. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/term.rb +1 -1
  154. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version.rb +1 -1
  155. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_constraint.rb +3 -3
  156. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_range.rb +1 -1
  157. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_solver.rb +2 -2
  158. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_union.rb +1 -1
  159. data/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb +57 -0
  160. data/lib/{bundler → rubygems}/vendor/securerandom/lib/securerandom.rb +10 -6
  161. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/common.rb +119 -119
  162. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/file.rb +13 -13
  163. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ftp.rb +24 -24
  164. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/generic.rb +132 -132
  165. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/http.rb +18 -18
  166. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/https.rb +4 -4
  167. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldap.rb +14 -14
  168. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldaps.rb +4 -4
  169. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/mailto.rb +21 -21
  170. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc2396_parser.rb +39 -39
  171. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc3986_parser.rb +14 -14
  172. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/version.rb +1 -1
  173. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ws.rb +12 -12
  174. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/wss.rb +4 -4
  175. data/lib/{bundler → rubygems}/vendor/uri/lib/uri.rb +35 -31
  176. data/lib/rubygems/yaml_serializer.rb +880 -0
  177. metadata +391 -381
  178. data/CHANGELOG.md +0 -5801
  179. data/LICENSE.md +0 -22
  180. data/README.md +0 -58
  181. data/lib/bundler/compact_index_client/cache.rb +0 -107
  182. data/lib/bundler/compact_index_client/cache_file.rb +0 -148
  183. data/lib/bundler/compact_index_client/parser.rb +0 -87
  184. data/lib/bundler/compact_index_client/updater.rb +0 -105
  185. data/lib/bundler/compact_index_client.rb +0 -92
  186. data/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +0 -227
  187. data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +0 -3
  188. data/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb +0 -56
  189. data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +0 -230
  190. data/lib/bundler/vendor/net-http-persistent/README.rdoc +0 -82
  191. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb +0 -41
  192. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +0 -65
  193. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +0 -80
  194. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +0 -1153
  195. data/lib/bundler/vendor/pub_grub/lib/pub_grub.rb +0 -31
  196. data/lib/bundler/vendored_persistent.rb +0 -11
  197. data/lib/bundler/yaml_serializer.rb +0 -98
  198. /data/lib/{bundler → rubygems}/vendor/pub_grub/LICENSE.txt +0 -0
  199. /data/lib/{bundler → rubygems}/vendor/securerandom/COPYING +0 -0
  200. /data/lib/{bundler → rubygems}/vendor/uri/COPYING +0 -0
@@ -625,7 +625,7 @@ class Bundler::Thor
625
625
  # alias name.
626
626
  def find_command_possibilities(meth)
627
627
  len = meth.to_s.length
628
- possibilities = all_commands.reject { |_k, c| c.hidden? }.merge(map).keys.select { |n| meth == n[0, len] }.sort
628
+ possibilities = all_commands.reject {|k, v| v.is_a?(HiddenCommand) }.merge(map).keys.select { |n| meth == n[0, len] }.sort
629
629
  unique_possibilities = possibilities.map { |k| map[k] || k }.uniq
630
630
 
631
631
  if possibilities.include?(meth)
@@ -671,4 +671,37 @@ class Bundler::Thor
671
671
  self.class.help(shell, subcommand)
672
672
  end
673
673
  end
674
+
675
+ map TREE_MAPPINGS => :tree
676
+
677
+ desc "tree", "Print a tree of all available commands"
678
+ def tree
679
+ build_command_tree(self.class, "")
680
+ end
681
+
682
+ private
683
+
684
+ def build_command_tree(klass, indent)
685
+ # Print current class name if it's not the root Bundler::Thor class
686
+ unless klass == Bundler::Thor
687
+ say "#{indent}#{klass.namespace || 'default'}", :blue
688
+ indent = "#{indent} "
689
+ end
690
+
691
+ # Print all commands for this class
692
+ visible_commands = klass.commands.reject { |_, cmd| cmd.hidden? || cmd.name == "help" }
693
+ commands_count = visible_commands.count
694
+ visible_commands.sort.each_with_index do |(command_name, command), i|
695
+ description = command.description.split("\n").first || ""
696
+ icon = i == (commands_count - 1) ? "└─" : "├─"
697
+ say "#{indent}#{icon} ", nil, false
698
+ say command_name, :green, false
699
+ say " (#{description})" unless description.empty?
700
+ end
701
+
702
+ # Print all subcommands (from registered Bundler::Thor subclasses)
703
+ klass.subcommand_classes.each do |_, subclass|
704
+ build_command_tree(subclass, indent)
705
+ end
706
+ end
674
707
  end
@@ -1,4 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Bundler; end
4
- require_relative "vendor/pub_grub/lib/pub_grub"
3
+ # Reuse RubyGems' vendored PubGrub (Gem::PubGrub). The Bundler gem ships a copy
4
+ # under lib/rubygems/vendor, so this resolves even on RubyGems versions that
5
+ # predate it.
6
+
7
+ require "rubygems/vendor/pub_grub/lib/pub_grub"
@@ -1,12 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Use RubyGems vendored copy when available. Otherwise fallback to Bundler
4
- # vendored copy. The vendored copy in Bundler can be removed once support for
5
- # RubyGems 3.5.18 is dropped.
3
+ # Reuse RubyGems' vendored SecureRandom (Gem::SecureRandom). The Bundler gem
4
+ # ships a copy under lib/rubygems/vendor, so this resolves even on RubyGems
5
+ # versions that predate it. Fall back to the stdlib only when no vendored copy
6
+ # is available at all.
6
7
 
7
- begin
8
- require "rubygems/vendored_securerandom"
9
- rescue LoadError
10
- require_relative "vendor/securerandom/lib/securerandom"
11
- Gem::SecureRandom = Bundler::SecureRandom
8
+ unless defined?(Gem::SecureRandom)
9
+ begin
10
+ require "rubygems/vendor/securerandom/lib/securerandom"
11
+ rescue LoadError
12
+ require "securerandom"
13
+ Gem::SecureRandom = SecureRandom
14
+ end
12
15
  end
@@ -1,21 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Bundler; end
3
+ # Reuse RubyGems' vendored URI (Gem::URI). The Bundler gem ships a copy under
4
+ # lib/rubygems/vendor, so this resolves even on RubyGems versions that predate
5
+ # it. Fall back to the stdlib only when no vendored copy is available at all.
4
6
 
5
- # Use RubyGems vendored copy when available. Otherwise fallback to Bundler
6
- # vendored copy. The vendored copy in Bundler can be removed once support for
7
- # RubyGems 3.5 is dropped.
7
+ unless defined?(Gem::URI)
8
+ begin
9
+ require "rubygems/vendor/uri/lib/uri"
10
+ rescue LoadError
11
+ require "uri"
12
+ Gem::URI = URI
8
13
 
9
- begin
10
- require "rubygems/vendor/uri/lib/uri"
11
- rescue LoadError
12
- require_relative "vendor/uri/lib/uri"
13
- Gem::URI = Bundler::URI
14
-
15
- module Gem
16
- def URI(uri) # rubocop:disable Naming/MethodName
17
- Bundler::URI(uri)
14
+ module Gem
15
+ def URI(uri) # rubocop:disable Naming/MethodName
16
+ Kernel.URI(uri)
17
+ end
18
+ module_function :URI
18
19
  end
19
- module_function :URI
20
20
  end
21
21
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
3
  module Bundler
4
- VERSION = "4.0.22".freeze
4
+ VERSION = "4.1.0.beta2".freeze
5
5
 
6
6
  def self.bundler_major_version
7
7
  @bundler_major_version ||= gem_version.segments.first
@@ -19,11 +19,12 @@ module Bundler
19
19
  # @param size [Integer] Size of pool
20
20
  # @param name [String] name the name of the worker
21
21
  # @param func [Proc] job to run in inside the worker pool
22
- def initialize(size, name, func)
22
+ # @param response_queue [Thread::Queue] queue that receives completed jobs
23
+ def initialize(size, name, func, response_queue: Thread::Queue.new)
23
24
  @name = name
24
25
  @request_queue = Thread::Queue.new
25
26
  @request_queue_with_priority = Thread::Queue.new
26
- @response_queue = Thread::Queue.new
27
+ @response_queue = response_queue
27
28
  @func = func
28
29
  @size = size
29
30
  @threads = nil
data/lib/bundler.rb CHANGED
@@ -41,7 +41,6 @@ module Bundler
41
41
  autoload :Checksum, File.expand_path("bundler/checksum", __dir__)
42
42
  autoload :CLI, File.expand_path("bundler/cli", __dir__)
43
43
  autoload :CIDetector, File.expand_path("bundler/ci_detector", __dir__)
44
- autoload :CompactIndexClient, File.expand_path("bundler/compact_index_client", __dir__)
45
44
  autoload :Definition, File.expand_path("bundler/definition", __dir__)
46
45
  autoload :Dependency, File.expand_path("bundler/dependency", __dir__)
47
46
  autoload :Deprecate, File.expand_path("bundler/deprecate", __dir__)
@@ -62,6 +61,7 @@ module Bundler
62
61
  autoload :MatchRemoteMetadata, File.expand_path("bundler/match_remote_metadata", __dir__)
63
62
  autoload :Materialization, File.expand_path("bundler/materialization", __dir__)
64
63
  autoload :NULL, File.expand_path("bundler/constants", __dir__)
64
+ autoload :Override, File.expand_path("bundler/override", __dir__)
65
65
  autoload :ProcessLock, File.expand_path("bundler/process_lock", __dir__)
66
66
  autoload :RemoteSpecification, File.expand_path("bundler/remote_specification", __dir__)
67
67
  autoload :Resolver, File.expand_path("bundler/resolver", __dir__)
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "pathname" unless defined?(Pathname)
6
+ require "set"
7
+
8
+ class Gem::CompactIndexClient
9
+ # Calls the Updater to update the cached files on disk, reads the
10
+ # cached files and returns their contents.
11
+ class Cache
12
+ attr_reader :directory
13
+
14
+ def initialize(directory, fetcher = nil)
15
+ @directory = Pathname.new(directory).expand_path
16
+ @updater = Updater.new(fetcher) if fetcher
17
+ @mutex = Thread::Mutex.new
18
+ @endpoints = Set.new
19
+
20
+ @info_root = mkdir("info")
21
+ @special_characters_info_root = mkdir("info-special-characters")
22
+ @info_etag_root = mkdir("info-etags")
23
+ end
24
+
25
+ def names
26
+ fetch("names", names_path, names_etag_path)
27
+ end
28
+
29
+ def versions
30
+ fetch("versions", versions_path, versions_etag_path)
31
+ end
32
+
33
+ def info(name, remote_checksum = nil)
34
+ path = info_path(name)
35
+
36
+ if remote_checksum && remote_checksum != checksum_for_file(path)
37
+ fetch("info/#{name}", path, info_etag_path(name))
38
+ else
39
+ Gem::CompactIndexClient.debug { "update skipped info/#{name} (#{remote_checksum ? "versions index checksum matches local" : "versions index checksum is nil"})" }
40
+ read(path)
41
+ end
42
+ end
43
+
44
+ # Fetch a single gem's info file without consulting the versions
45
+ # index, refreshing the cached file with a conditional request.
46
+ def fetch_info(name)
47
+ fetch("info/#{name}", info_path(name), info_etag_path(name))
48
+ end
49
+
50
+ def reset!
51
+ @mutex.synchronize { @endpoints.clear }
52
+ end
53
+
54
+ private
55
+
56
+ def names_path = directory.join("names")
57
+ def names_etag_path = directory.join("names.etag")
58
+ def versions_path = directory.join("versions")
59
+ def versions_etag_path = directory.join("versions.etag")
60
+
61
+ def info_path(name)
62
+ name = name.to_s
63
+ validate_name!(name)
64
+ if /[^a-z0-9_-]/.match?(name)
65
+ name += "-#{Digest::MD5.hexdigest(name).downcase}"
66
+ @special_characters_info_root.join(name)
67
+ else
68
+ @info_root.join(name)
69
+ end
70
+ end
71
+
72
+ def info_etag_path(name)
73
+ name = name.to_s
74
+ validate_name!(name)
75
+ @info_etag_root.join("#{name}-#{Digest::MD5.hexdigest(name).downcase}")
76
+ end
77
+
78
+ # Gem names come from the remote index and are not otherwise
79
+ # validated, so refuse anything that would escape the cache
80
+ # directory when used as a path component.
81
+ def validate_name!(name)
82
+ return unless name.empty? || name.include?("\0") || name == "." || name == ".." || File.basename(name) != name
83
+
84
+ raise Gem::Exception, "malformed gem name: #{name.inspect}"
85
+ end
86
+
87
+ def checksum_for_file(path)
88
+ return unless path.file?
89
+ Gem::CompactIndexClient.filesystem_access(path, :read) do
90
+ Digest::MD5.file(path).hexdigest
91
+ end
92
+ end
93
+
94
+ def mkdir(name)
95
+ directory.join(name).tap do |dir|
96
+ Gem::CompactIndexClient.filesystem_access(dir) do
97
+ FileUtils.mkdir_p(dir)
98
+ end
99
+ end
100
+ end
101
+
102
+ def fetch(remote_path, path, etag_path)
103
+ if already_fetched?(remote_path)
104
+ Gem::CompactIndexClient.debug { "already fetched #{remote_path}" }
105
+ else
106
+ Gem::CompactIndexClient.debug { "fetching #{remote_path}" }
107
+ @updater&.update(remote_path, path, etag_path)
108
+ end
109
+
110
+ read(path)
111
+ end
112
+
113
+ def already_fetched?(remote_path)
114
+ @mutex.synchronize { !@endpoints.add?(remote_path) }
115
+ end
116
+
117
+ def read(path)
118
+ return unless path.file?
119
+ Gem::CompactIndexClient.filesystem_access(path, :read, &:read)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "rubygems/package"
6
+
7
+ class Gem::CompactIndexClient
8
+ # write cache files in a way that is robust to concurrent modifications
9
+ # if digests are given, the checksums will be verified
10
+ class CacheFile
11
+ DEFAULT_FILE_MODE = 0o644
12
+ private_constant :DEFAULT_FILE_MODE
13
+
14
+ class Error < RuntimeError; end
15
+ class ClosedError < Error; end
16
+
17
+ class DigestMismatchError < Error
18
+ def initialize(digests, expected_digests)
19
+ super "Calculated checksums #{digests.inspect} did not match expected #{expected_digests.inspect}."
20
+ end
21
+ end
22
+
23
+ # Initialize with a copy of the original file, then yield the instance.
24
+ def self.copy(path, &block)
25
+ new(path) do |file|
26
+ file.initialize_digests
27
+
28
+ Gem::CompactIndexClient.filesystem_access(path, :read) do
29
+ path.open("rb") do |s|
30
+ file.open {|f| IO.copy_stream(s, f) }
31
+ end
32
+ end
33
+
34
+ yield file
35
+ end
36
+ end
37
+
38
+ # Write data to a temp file, then replace the original file with it verifying the digests if given.
39
+ def self.write(path, data, digests = nil)
40
+ return unless data
41
+ new(path) do |file|
42
+ file.digests = digests
43
+ file.write(data)
44
+ end
45
+ end
46
+
47
+ attr_reader :original_path, :path
48
+
49
+ def initialize(original_path, &block)
50
+ @original_path = original_path
51
+ @perm = original_path.file? ? original_path.stat.mode : DEFAULT_FILE_MODE
52
+ @path = original_path.sub(/$/, ".#{$$}.tmp")
53
+ return unless block_given?
54
+ begin
55
+ yield self
56
+ ensure
57
+ close
58
+ end
59
+ end
60
+
61
+ def size
62
+ path.size
63
+ end
64
+
65
+ # initialize the digests using CompactIndexClient::SUPPORTED_DIGESTS, or a subset based on keys.
66
+ def initialize_digests(keys = nil)
67
+ @digests = keys ? SUPPORTED_DIGESTS.slice(*keys) : SUPPORTED_DIGESTS.dup
68
+ @digests.transform_values! {|algo_class| Digest(algo_class).new }
69
+ end
70
+
71
+ # reset the digests so they don't contain any previously read data
72
+ def reset_digests
73
+ @digests&.each_value(&:reset)
74
+ end
75
+
76
+ # set the digests that will be verified at the end
77
+ def digests=(expected_digests)
78
+ @expected_digests = expected_digests
79
+
80
+ if @expected_digests.nil?
81
+ @digests = nil
82
+ elsif @digests
83
+ @digests = @digests.slice(*@expected_digests.keys)
84
+ else
85
+ initialize_digests(@expected_digests.keys)
86
+ end
87
+ end
88
+
89
+ def digests?
90
+ @digests&.any?
91
+ end
92
+
93
+ # Open the temp file for writing, reusing original permissions, yielding the IO object.
94
+ def open(write_mode = "wb", perm = @perm, &block)
95
+ raise ClosedError, "Cannot reopen closed file" if @closed
96
+ Gem::CompactIndexClient.filesystem_access(path, :write) do
97
+ path.open(write_mode, perm) do |f|
98
+ yield digests? ? Gem::Package::DigestIO.new(f, @digests) : f
99
+ end
100
+ end
101
+ end
102
+
103
+ # Returns false without appending when no digests since appending is too error prone to do without digests.
104
+ def append(data)
105
+ return false unless digests?
106
+ open("ab") {|f| f.write data }
107
+ verify && commit
108
+ end
109
+
110
+ def write(data)
111
+ reset_digests
112
+ open {|f| f.write data }
113
+ commit!
114
+ end
115
+
116
+ def commit!
117
+ verify || raise(DigestMismatchError.new(@base64digests, @expected_digests))
118
+ commit
119
+ end
120
+
121
+ # Verify the digests, returning true on match, false on mismatch.
122
+ def verify
123
+ return true unless @expected_digests && digests?
124
+ @base64digests = @digests.transform_values!(&:base64digest)
125
+ @digests = nil
126
+ @base64digests.all? {|algo, digest| @expected_digests[algo] == digest }
127
+ end
128
+
129
+ # Replace the original file with the temp file without verifying digests.
130
+ # The file is permanently closed.
131
+ def commit
132
+ raise ClosedError, "Cannot commit closed file" if @closed
133
+ Gem::CompactIndexClient.filesystem_access(original_path, :write) do
134
+ FileUtils.mv(path, original_path)
135
+ end
136
+ @closed = true
137
+ end
138
+
139
+ # Remove the temp file without replacing the original file.
140
+ # The file is permanently closed.
141
+ def close
142
+ return if @closed
143
+ FileUtils.remove_file(path) if @path&.file?
144
+ @closed = true
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/remote_fetcher"
4
+
5
+ class Gem::CompactIndexClient
6
+ # Fetches compact index files relative to +base_uri+ using
7
+ # Gem::RemoteFetcher's connection infrastructure (proxy, TLS,
8
+ # connection pooling). Implements the fetcher interface expected by
9
+ # Gem::CompactIndexClient: #call(path, headers) returning a
10
+ # Gem::Net::HTTP response.
11
+ class HTTPFetcher
12
+ REDIRECT_LIMIT = 10
13
+ private_constant :REDIRECT_LIMIT
14
+
15
+ def initialize(base_uri, remote_fetcher = Gem::RemoteFetcher.fetcher)
16
+ base_uri = base_uri.to_s
17
+ base_uri += "/" unless base_uri.end_with?("/")
18
+ @base_uri = Gem::URI(base_uri)
19
+ @remote_fetcher = remote_fetcher
20
+ end
21
+
22
+ def call(path, headers = {})
23
+ fetch(@base_uri + path, headers, REDIRECT_LIMIT)
24
+ end
25
+
26
+ private
27
+
28
+ def fetch(uri, headers, redirects_remaining)
29
+ response = request(uri, headers)
30
+
31
+ case response
32
+ when Gem::Net::HTTPNotModified
33
+ response
34
+ when Gem::Net::HTTPSuccess
35
+ # The callers write the body into the cache, so a body-less success
36
+ # such as 204 would truncate the cached file.
37
+ raise bad_response(response, uri) unless response.class.body_permitted?
38
+
39
+ response
40
+ when Gem::Net::HTTPMovedPermanently, Gem::Net::HTTPFound, Gem::Net::HTTPSeeOther,
41
+ Gem::Net::HTTPTemporaryRedirect, Gem::Net::HTTPPermanentRedirect
42
+ raise Gem::RemoteFetcher::FetchError.new("too many redirects", uri) if redirects_remaining.zero?
43
+
44
+ location = response["Location"]
45
+ raise Gem::RemoteFetcher::FetchError.new("redirecting but no redirect location was given", uri) unless location
46
+
47
+ redirect = uri + location
48
+ if https?(uri) && !https?(redirect)
49
+ raise Gem::RemoteFetcher::FetchError.new("redirecting to non-https resource: #{Gem::Uri.redact(redirect)}", uri)
50
+ end
51
+ # An absolute Location on the same origin drops the credentials that a
52
+ # relative one would have kept. Another port or scheme on the same host
53
+ # is a different protection space (RFC 9110, Section 11.5).
54
+ same_origin = [redirect.scheme, redirect.host, redirect.port] == [uri.scheme, uri.host, uri.port]
55
+ redirect.userinfo = uri.userinfo if same_origin && !redirect.userinfo
56
+
57
+ fetch(redirect, headers, redirects_remaining - 1)
58
+ when Gem::Net::HTTPRangeNotSatisfiable
59
+ raise bad_response(response, uri) unless headers.key?("Range")
60
+
61
+ # The local cache is longer than the remote file, refetch it whole. A
62
+ # matching ETag would otherwise turn the retry into a 304 and keep the
63
+ # oversized cache.
64
+ fetch(uri, headers.except("Range", "If-None-Match"), redirects_remaining)
65
+ else
66
+ raise bad_response(response, uri)
67
+ end
68
+ end
69
+
70
+ # The callers fall back to the Marshal index when a fetch fails, and they
71
+ # only recognize a failure that arrives as a FetchError.
72
+ def request(uri, headers)
73
+ @remote_fetcher.request(uri, Gem::Net::HTTP::Get) do |req|
74
+ headers.each {|name, value| req[name] = value }
75
+ end
76
+ rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
77
+ *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
78
+ raise Gem::RemoteFetcher::FetchError.new("#{e.class}: #{e}", uri)
79
+ end
80
+
81
+ def bad_response(response, uri)
82
+ detail = response["X-Error-Message"] || response.message
83
+ Gem::RemoteFetcher::FetchError.new("bad response #{detail} #{response.code}", uri)
84
+ end
85
+
86
+ def https?(uri)
87
+ uri.scheme == "https"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/resolver/api_set/gem_parser"
4
+
5
+ class Gem::CompactIndexClient
6
+ class Parser
7
+ # `compact_index` - an object responding to #names, #versions, #info(name, checksum),
8
+ # returning the file contents as a string
9
+ def initialize(compact_index)
10
+ @compact_index = compact_index
11
+ @info_checksums = nil
12
+ @versions_by_name = nil
13
+ @available = nil
14
+ @gem_parser = nil
15
+ end
16
+
17
+ def names
18
+ lines(@compact_index.names)
19
+ end
20
+
21
+ def versions
22
+ @versions_by_name ||= Hash.new {|hash, key| hash[key] = [] }
23
+ @info_checksums = {}
24
+
25
+ lines(@compact_index.versions).each do |line|
26
+ name, versions_string, checksum = line.split(" ", 3)
27
+ @info_checksums[name] = checksum || ""
28
+ versions_string.split(",") do |version|
29
+ delete = version.delete_prefix!("-")
30
+ version = version.split("-", 2).unshift(name)
31
+ if delete
32
+ @versions_by_name[name].delete(version)
33
+ else
34
+ @versions_by_name[name] << version
35
+ end
36
+ end
37
+ end
38
+
39
+ @versions_by_name
40
+ end
41
+
42
+ def info(name)
43
+ parse_info(@compact_index.info(name, info_checksums[name]), name)
44
+ end
45
+
46
+ def parse_info(data, name)
47
+ lines(data).map {|line| gem_parser.parse(line).unshift(name) }
48
+ end
49
+
50
+ def available?
51
+ return @available unless @available.nil?
52
+ @available = !info_checksums.empty?
53
+ end
54
+
55
+ private
56
+
57
+ def info_checksums
58
+ @info_checksums ||= lines(@compact_index.versions).each_with_object({}) do |line, checksums|
59
+ parse_version_checksum(line, checksums)
60
+ end
61
+ end
62
+
63
+ def lines(data)
64
+ return [] if data.nil? || data.empty?
65
+ lines = data.split("\n")
66
+ header = lines.index("---")
67
+ header ? lines[header + 1..-1] : lines
68
+ end
69
+
70
+ def gem_parser
71
+ @gem_parser ||= Gem::Resolver::APISet::GemParser.new
72
+ end
73
+
74
+ # This is mostly the same as `split(" ", 3)` but it avoids allocating extra objects.
75
+ # This method gets called at least once for every gem when parsing versions.
76
+ def parse_version_checksum(line, checksums)
77
+ return unless (name_end = line.index(" ")) # Artifactory bug causes blank lines in artifactory index files
78
+ checksum_start = line.index(" ", name_end + 1)
79
+ return unless checksum_start
80
+ checksum_start += 1
81
+
82
+ checksum_end = line.size - checksum_start
83
+
84
+ line.freeze # allows slicing into the string to not allocate a copy of the line
85
+ name = line[0, name_end]
86
+ checksum = line[checksum_start, checksum_end]
87
+ checksums[name.freeze] = checksum # freeze name since it is used as a hash key
88
+ end
89
+ end
90
+ end