bundler 4.0.21 → 4.1.0.beta1

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 (190) 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 +37 -1
  11. data/lib/bundler/cli/config.rb +4 -2
  12. data/lib/bundler/cli/gem.rb +0 -17
  13. data/lib/bundler/cli/install.rb +4 -5
  14. data/lib/bundler/cli/lock.rb +1 -2
  15. data/lib/bundler/cli/outdated.rb +8 -3
  16. data/lib/bundler/cli/pristine.rb +2 -1
  17. data/lib/bundler/cli/update.rb +21 -12
  18. data/lib/bundler/cli.rb +14 -11
  19. data/lib/bundler/definition.rb +61 -24
  20. data/lib/bundler/dependency.rb +4 -0
  21. data/lib/bundler/dsl.rb +77 -11
  22. data/lib/bundler/endpoint_specification.rb +51 -14
  23. data/lib/bundler/env.rb +7 -24
  24. data/lib/bundler/errors.rb +34 -5
  25. data/lib/bundler/fetcher/compact_index.rb +14 -5
  26. data/lib/bundler/fetcher/connection_pools.rb +150 -0
  27. data/lib/bundler/fetcher/downloader.rb +31 -16
  28. data/lib/bundler/fetcher.rb +8 -47
  29. data/lib/bundler/injector.rb +3 -3
  30. data/lib/bundler/inline.rb +44 -4
  31. data/lib/bundler/installer/parallel_installer.rb +25 -30
  32. data/lib/bundler/installer.rb +20 -6
  33. data/lib/bundler/lazy_specification.rb +44 -17
  34. data/lib/bundler/lockfile_generator.rb +24 -2
  35. data/lib/bundler/lockfile_parser.rb +43 -5
  36. data/lib/bundler/man/bundle-add.1 +7 -4
  37. data/lib/bundler/man/bundle-add.1.ronn +8 -4
  38. data/lib/bundler/man/bundle-binstubs.1 +1 -1
  39. data/lib/bundler/man/bundle-cache.1 +2 -2
  40. data/lib/bundler/man/bundle-cache.1.ronn +2 -2
  41. data/lib/bundler/man/bundle-check.1 +1 -1
  42. data/lib/bundler/man/bundle-clean.1 +1 -1
  43. data/lib/bundler/man/bundle-config.1 +38 -6
  44. data/lib/bundler/man/bundle-config.1.ronn +143 -10
  45. data/lib/bundler/man/bundle-console.1 +1 -1
  46. data/lib/bundler/man/bundle-doctor.1 +1 -1
  47. data/lib/bundler/man/bundle-env.1 +1 -1
  48. data/lib/bundler/man/bundle-exec.1 +1 -1
  49. data/lib/bundler/man/bundle-fund.1 +1 -1
  50. data/lib/bundler/man/bundle-gem.1 +1 -1
  51. data/lib/bundler/man/bundle-help.1 +1 -1
  52. data/lib/bundler/man/bundle-info.1 +1 -1
  53. data/lib/bundler/man/bundle-init.1 +1 -1
  54. data/lib/bundler/man/bundle-install.1 +2 -2
  55. data/lib/bundler/man/bundle-install.1.ronn +4 -4
  56. data/lib/bundler/man/bundle-issue.1 +1 -1
  57. data/lib/bundler/man/bundle-licenses.1 +1 -1
  58. data/lib/bundler/man/bundle-list.1 +1 -1
  59. data/lib/bundler/man/bundle-lock.1 +3 -3
  60. data/lib/bundler/man/bundle-lock.1.ronn +5 -5
  61. data/lib/bundler/man/bundle-open.1 +1 -1
  62. data/lib/bundler/man/bundle-outdated.1 +1 -1
  63. data/lib/bundler/man/bundle-platform.1 +1 -1
  64. data/lib/bundler/man/bundle-plugin.1 +1 -1
  65. data/lib/bundler/man/bundle-pristine.1 +1 -1
  66. data/lib/bundler/man/bundle-remove.1 +1 -1
  67. data/lib/bundler/man/bundle-show.1 +1 -1
  68. data/lib/bundler/man/bundle-update.1 +3 -3
  69. data/lib/bundler/man/bundle-update.1.ronn +3 -5
  70. data/lib/bundler/man/bundle-version.1 +1 -1
  71. data/lib/bundler/man/bundle.1 +1 -1
  72. data/lib/bundler/man/gemfile.5 +45 -1
  73. data/lib/bundler/man/gemfile.5.ronn +53 -0
  74. data/lib/bundler/match_metadata.rb +20 -0
  75. data/lib/bundler/match_platform.rb +25 -2
  76. data/lib/bundler/match_remote_metadata.rb +21 -6
  77. data/lib/bundler/materialization.rb +2 -1
  78. data/lib/bundler/override.rb +69 -0
  79. data/lib/bundler/plugin/dsl.rb +3 -5
  80. data/lib/bundler/plugin/events.rb +55 -13
  81. data/lib/bundler/plugin/index.rb +87 -11
  82. data/lib/bundler/plugin/installer/path.rb +1 -1
  83. data/lib/bundler/plugin/installer.rb +13 -5
  84. data/lib/bundler/plugin.rb +58 -30
  85. data/lib/bundler/remote_specification.rb +6 -3
  86. data/lib/bundler/resolver/base.rb +2 -1
  87. data/lib/bundler/resolver/incompatibility.rb +1 -1
  88. data/lib/bundler/resolver.rb +73 -23
  89. data/lib/bundler/ruby_version.rb +0 -2
  90. data/lib/bundler/rubygems_ext.rb +71 -1
  91. data/lib/bundler/rubygems_gem_installer.rb +41 -2
  92. data/lib/bundler/rubygems_integration.rb +7 -2
  93. data/lib/bundler/runtime.rb +69 -2
  94. data/lib/bundler/self_manager.rb +9 -15
  95. data/lib/bundler/settings.rb +373 -30
  96. data/lib/bundler/shared_helpers.rb +2 -1
  97. data/lib/bundler/source/git/git_proxy.rb +2 -10
  98. data/lib/bundler/source/git.rb +15 -6
  99. data/lib/bundler/source/path/installer.rb +1 -1
  100. data/lib/bundler/source/rubygems/remote.rb +10 -5
  101. data/lib/bundler/source/rubygems.rb +48 -12
  102. data/lib/bundler/spec_set.rb +16 -4
  103. data/lib/bundler/stub_specification.rb +2 -1
  104. data/lib/bundler/templates/newgem/Gemfile.tt +4 -4
  105. data/lib/bundler/templates/newgem/newgem.gemspec.tt +3 -3
  106. data/lib/bundler/templates/newgem/spec/newgem_spec.rb.tt +1 -1
  107. data/lib/bundler/templates/newgem/test/minitest/test_newgem.rb.tt +1 -1
  108. data/lib/bundler/ui/shell.rb +0 -4
  109. data/lib/bundler/ui/silent.rb +0 -4
  110. data/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +35 -2
  111. data/lib/bundler/vendor/thor/lib/thor/base.rb +2 -1
  112. data/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +14 -6
  113. data/lib/bundler/vendor/thor/lib/thor/shell/color.rb +2 -0
  114. data/lib/bundler/vendor/thor/lib/thor/version.rb +1 -1
  115. data/lib/bundler/vendor/thor/lib/thor.rb +34 -1
  116. data/lib/bundler/vendored_pub_grub.rb +5 -2
  117. data/lib/bundler/vendored_securerandom.rb +11 -8
  118. data/lib/bundler/vendored_uri.rb +14 -14
  119. data/lib/bundler/version.rb +1 -1
  120. data/lib/bundler/worker.rb +3 -2
  121. data/lib/bundler.rb +1 -1
  122. data/lib/rubygems/compact_index_client/cache.rb +122 -0
  123. data/lib/rubygems/compact_index_client/cache_file.rb +147 -0
  124. data/lib/rubygems/compact_index_client/http_fetcher.rb +88 -0
  125. data/lib/rubygems/compact_index_client/parser.rb +90 -0
  126. data/lib/rubygems/compact_index_client/updater.rb +113 -0
  127. data/lib/rubygems/compact_index_client.rb +111 -0
  128. data/lib/rubygems/cooldown_settings.rb +57 -0
  129. data/lib/rubygems/credential_store/native/linux.rb +97 -0
  130. data/lib/rubygems/credential_store/native/macos.rb +88 -0
  131. data/lib/rubygems/credential_store/native/windows.rb +130 -0
  132. data/lib/rubygems/credential_store.rb +332 -0
  133. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/assignment.rb +1 -1
  134. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/basic_package_source.rb +7 -7
  135. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/failure_writer.rb +1 -1
  136. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/incompatibility.rb +4 -4
  137. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/package.rb +1 -1
  138. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/partial_solution.rb +1 -1
  139. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/rubygems.rb +3 -3
  140. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/solve_failure.rb +1 -1
  141. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/static_package_source.rb +3 -3
  142. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/strategy.rb +2 -2
  143. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/term.rb +1 -1
  144. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version.rb +1 -1
  145. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_constraint.rb +3 -3
  146. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_range.rb +1 -1
  147. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_solver.rb +2 -2
  148. data/lib/{bundler → rubygems}/vendor/pub_grub/lib/pub_grub/version_union.rb +1 -1
  149. data/lib/rubygems/vendor/pub_grub/lib/pub_grub.rb +57 -0
  150. data/lib/{bundler → rubygems}/vendor/securerandom/lib/securerandom.rb +10 -6
  151. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/common.rb +119 -119
  152. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/file.rb +13 -13
  153. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ftp.rb +24 -24
  154. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/generic.rb +132 -132
  155. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/http.rb +18 -18
  156. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/https.rb +4 -4
  157. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldap.rb +14 -14
  158. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ldaps.rb +4 -4
  159. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/mailto.rb +21 -21
  160. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc2396_parser.rb +39 -39
  161. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/rfc3986_parser.rb +14 -14
  162. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/version.rb +1 -1
  163. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/ws.rb +12 -12
  164. data/lib/{bundler → rubygems}/vendor/uri/lib/uri/wss.rb +4 -4
  165. data/lib/{bundler → rubygems}/vendor/uri/lib/uri.rb +35 -31
  166. data/lib/rubygems/yaml_serializer.rb +880 -0
  167. metadata +77 -68
  168. data/CHANGELOG.md +0 -5784
  169. data/LICENSE.md +0 -22
  170. data/README.md +0 -58
  171. data/lib/bundler/compact_index_client/cache.rb +0 -107
  172. data/lib/bundler/compact_index_client/cache_file.rb +0 -148
  173. data/lib/bundler/compact_index_client/parser.rb +0 -87
  174. data/lib/bundler/compact_index_client/updater.rb +0 -105
  175. data/lib/bundler/compact_index_client.rb +0 -92
  176. data/lib/bundler/vendor/connection_pool/lib/connection_pool/timed_stack.rb +0 -227
  177. data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +0 -3
  178. data/lib/bundler/vendor/connection_pool/lib/connection_pool/wrapper.rb +0 -56
  179. data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +0 -230
  180. data/lib/bundler/vendor/net-http-persistent/README.rdoc +0 -82
  181. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb +0 -41
  182. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +0 -65
  183. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +0 -80
  184. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +0 -1153
  185. data/lib/bundler/vendor/pub_grub/lib/pub_grub.rb +0 -31
  186. data/lib/bundler/vendored_persistent.rb +0 -11
  187. data/lib/bundler/yaml_serializer.rb +0 -98
  188. /data/lib/{bundler → rubygems}/vendor/pub_grub/LICENSE.txt +0 -0
  189. /data/lib/{bundler → rubygems}/vendor/securerandom/COPYING +0 -0
  190. /data/lib/{bundler → rubygems}/vendor/uri/COPYING +0 -0
@@ -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,88 @@
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 host drops the credentials that a
52
+ # relative one would have kept.
53
+ redirect.userinfo = uri.userinfo if redirect.host == uri.host && !redirect.userinfo
54
+
55
+ fetch(redirect, headers, redirects_remaining - 1)
56
+ when Gem::Net::HTTPRangeNotSatisfiable
57
+ raise bad_response(response, uri) unless headers.key?("Range")
58
+
59
+ # The local cache is longer than the remote file, refetch it whole. A
60
+ # matching ETag would otherwise turn the retry into a 304 and keep the
61
+ # oversized cache.
62
+ fetch(uri, headers.except("Range", "If-None-Match"), redirects_remaining)
63
+ else
64
+ raise bad_response(response, uri)
65
+ end
66
+ end
67
+
68
+ # The callers fall back to the Marshal index when a fetch fails, and they
69
+ # only recognize a failure that arrives as a FetchError.
70
+ def request(uri, headers)
71
+ @remote_fetcher.request(uri, Gem::Net::HTTP::Get) do |req|
72
+ headers.each {|name, value| req[name] = value }
73
+ end
74
+ rescue Gem::Timeout::Error, IOError, SocketError, SystemCallError,
75
+ *(OpenSSL::SSL::SSLError if Gem::HAVE_OPENSSL) => e
76
+ raise Gem::RemoteFetcher::FetchError.new("#{e.class}: #{e}", uri)
77
+ end
78
+
79
+ def bad_response(response, uri)
80
+ detail = response["X-Error-Message"] || response.message
81
+ Gem::RemoteFetcher::FetchError.new("bad response #{detail} #{response.code}", uri)
82
+ end
83
+
84
+ def https?(uri)
85
+ uri.scheme == "https"
86
+ end
87
+ end
88
+ 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
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+
5
+ # On RubyGems versions too old to vendor Gem::Net (when this file is loaded
6
+ # from the copy shipped inside the Bundler gem), Bundler's own
7
+ # vendored_net_http shim has already defined it before this file is loaded.
8
+ begin
9
+ require "rubygems/vendored_net_http" unless defined?(Gem::Net::HTTP)
10
+ rescue LoadError
11
+ require "net/http"
12
+ end
13
+
14
+ class Gem::CompactIndexClient
15
+ # Updates the cached files on disk, keeping them in sync with the server
16
+ # using conditional requests (ETag) and ranged requests where possible.
17
+ class Updater
18
+ class MismatchedChecksumError < Error
19
+ def initialize(path, message)
20
+ super "The checksum of /#{path} does not match the checksum provided by the server! Something is wrong. #{message}"
21
+ end
22
+ end
23
+
24
+ def initialize(fetcher)
25
+ @fetcher = fetcher
26
+ end
27
+
28
+ def update(remote_path, local_path, etag_path)
29
+ append(remote_path, local_path, etag_path) || replace(remote_path, local_path, etag_path)
30
+ rescue CacheFile::DigestMismatchError => e
31
+ raise MismatchedChecksumError.new(remote_path, e.message)
32
+ rescue Zlib::GzipFile::Error
33
+ raise Error, "invalid gzip response while fetching /#{remote_path}"
34
+ end
35
+
36
+ private
37
+
38
+ def append(remote_path, local_path, etag_path)
39
+ return false unless local_path.file? && local_path.size.nonzero?
40
+
41
+ CacheFile.copy(local_path) do |file|
42
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
43
+
44
+ # Subtract a byte to ensure the range won't be empty.
45
+ # Avoids 416 (Range Not Satisfiable) responses.
46
+ response = @fetcher.call(remote_path, request_headers(etag, file.size - 1))
47
+ break true if response.is_a?(Gem::Net::HTTPNotModified)
48
+
49
+ file.digests = parse_digests(response)
50
+ # server may ignore Range and return the full response
51
+ if response.is_a?(Gem::Net::HTTPPartialContent)
52
+ tail = response.body.byteslice(1..-1)
53
+ break false unless tail && file.append(tail)
54
+ else
55
+ file.write(response.body)
56
+ end
57
+ CacheFile.write(etag_path, etag_from_response(response))
58
+ true
59
+ end
60
+ end
61
+
62
+ # request without range header to get the full file or a 304 Not Modified
63
+ def replace(remote_path, local_path, etag_path)
64
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
65
+ response = @fetcher.call(remote_path, request_headers(etag))
66
+ return true if response.is_a?(Gem::Net::HTTPNotModified)
67
+ CacheFile.write(local_path, response.body, parse_digests(response))
68
+ CacheFile.write(etag_path, etag_from_response(response))
69
+ end
70
+
71
+ def request_headers(etag, range_start = nil)
72
+ headers = {}
73
+ headers["Range"] = "bytes=#{range_start}-" if range_start
74
+ headers["If-None-Match"] = %("#{etag}") if etag
75
+ headers
76
+ end
77
+
78
+ def etag_from_response(response)
79
+ return unless response["ETag"]
80
+ etag = response["ETag"].delete_prefix("W/")
81
+ return if etag.delete_prefix!('"') && !etag.delete_suffix!('"')
82
+ etag
83
+ end
84
+
85
+ # Unwraps and returns a Hash of digest algorithms and base64 values
86
+ # according to RFC 8941 Structured Field Values for HTTP.
87
+ # https://www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence
88
+ # Ignores unsupported algorithms.
89
+ def parse_digests(response)
90
+ return unless header = response["Repr-Digest"] || response["Digest"]
91
+ digests = {}
92
+ header.split(",") do |param|
93
+ algorithm, value = param.split("=", 2)
94
+ algorithm.strip!
95
+ algorithm.downcase!
96
+ next unless SUPPORTED_DIGESTS.key?(algorithm)
97
+ next unless value
98
+ next unless value = byte_sequence(value)
99
+ digests[algorithm] = value
100
+ end
101
+ digests.empty? ? nil : digests
102
+ end
103
+
104
+ # Unwrap surrounding colons (byte sequence)
105
+ # The wrapping characters must be matched or we return nil.
106
+ # Also handles quotes because right now rubygems.org sends them.
107
+ def byte_sequence(value)
108
+ return if value.delete_prefix!(":") && !value.delete_suffix!(":")
109
+ return if value.delete_prefix!('"') && !value.delete_suffix!('"')
110
+ value
111
+ end
112
+ end
113
+ end