bundler 2.4.21 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (183) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -0
  3. data/README.md +1 -2
  4. data/bundler.gemspec +4 -2
  5. data/exe/bundle +1 -10
  6. data/lib/bundler/build_metadata.rb +3 -3
  7. data/lib/bundler/capistrano.rb +1 -1
  8. data/lib/bundler/checksum.rb +245 -0
  9. data/lib/bundler/ci_detector.rb +75 -0
  10. data/lib/bundler/cli/add.rb +3 -3
  11. data/lib/bundler/cli/binstubs.rb +4 -4
  12. data/lib/bundler/cli/cache.rb +1 -1
  13. data/lib/bundler/cli/check.rb +1 -1
  14. data/lib/bundler/cli/common.rb +9 -1
  15. data/lib/bundler/cli/config.rb +8 -7
  16. data/lib/bundler/cli/console.rb +3 -2
  17. data/lib/bundler/cli/doctor.rb +2 -2
  18. data/lib/bundler/cli/exec.rb +1 -1
  19. data/lib/bundler/cli/gem.rb +31 -23
  20. data/lib/bundler/cli/info.rb +2 -13
  21. data/lib/bundler/cli/install.rb +5 -4
  22. data/lib/bundler/cli/issue.rb +1 -1
  23. data/lib/bundler/cli/lock.rb +4 -4
  24. data/lib/bundler/cli/open.rb +1 -1
  25. data/lib/bundler/cli/outdated.rb +6 -6
  26. data/lib/bundler/cli/plugin.rb +7 -14
  27. data/lib/bundler/cli/pristine.rb +38 -30
  28. data/lib/bundler/cli/show.rb +2 -2
  29. data/lib/bundler/cli/update.rb +5 -5
  30. data/lib/bundler/cli.rb +215 -263
  31. data/lib/bundler/compact_index_client/cache.rb +29 -9
  32. data/lib/bundler/compact_index_client/cache_file.rb +153 -0
  33. data/lib/bundler/compact_index_client/gem_parser.rb +7 -3
  34. data/lib/bundler/compact_index_client/updater.rb +79 -81
  35. data/lib/bundler/compact_index_client.rb +14 -7
  36. data/lib/bundler/constants.rb +1 -1
  37. data/lib/bundler/current_ruby.rb +5 -21
  38. data/lib/bundler/definition.rb +43 -16
  39. data/lib/bundler/dependency.rb +16 -12
  40. data/lib/bundler/digest.rb +2 -2
  41. data/lib/bundler/dsl.rb +43 -25
  42. data/lib/bundler/endpoint_specification.rb +6 -2
  43. data/lib/bundler/env.rb +1 -3
  44. data/lib/bundler/errors.rb +58 -0
  45. data/lib/bundler/fetcher/base.rb +3 -1
  46. data/lib/bundler/fetcher/compact_index.rb +4 -4
  47. data/lib/bundler/fetcher/downloader.rb +13 -11
  48. data/lib/bundler/fetcher/gem_remote_fetcher.rb +16 -0
  49. data/lib/bundler/fetcher/index.rb +1 -1
  50. data/lib/bundler/fetcher.rb +28 -25
  51. data/lib/bundler/friendly_errors.rb +5 -5
  52. data/lib/bundler/gem_helper.rb +1 -1
  53. data/lib/bundler/gem_helpers.rb +12 -2
  54. data/lib/bundler/graph.rb +9 -9
  55. data/lib/bundler/index.rb +1 -2
  56. data/lib/bundler/injector.rb +1 -1
  57. data/lib/bundler/inline.rb +3 -3
  58. data/lib/bundler/installer/gem_installer.rb +10 -10
  59. data/lib/bundler/installer/parallel_installer.rb +16 -8
  60. data/lib/bundler/installer/standalone.rb +2 -3
  61. data/lib/bundler/installer.rb +9 -9
  62. data/lib/bundler/lazy_specification.rb +28 -17
  63. data/lib/bundler/lockfile_generator.rb +9 -0
  64. data/lib/bundler/lockfile_parser.rb +81 -10
  65. data/lib/bundler/man/bundle-add.1 +3 -26
  66. data/lib/bundler/man/bundle-binstubs.1 +4 -16
  67. data/lib/bundler/man/bundle-cache.1 +3 -24
  68. data/lib/bundler/man/bundle-check.1 +3 -12
  69. data/lib/bundler/man/bundle-clean.1 +3 -10
  70. data/lib/bundler/man/bundle-config.1 +20 -211
  71. data/lib/bundler/man/bundle-config.1.ronn +6 -0
  72. data/lib/bundler/man/bundle-console.1 +4 -22
  73. data/lib/bundler/man/bundle-doctor.1 +4 -18
  74. data/lib/bundler/man/bundle-exec.1 +12 -73
  75. data/lib/bundler/man/bundle-gem.1 +13 -49
  76. data/lib/bundler/man/bundle-help.1 +3 -7
  77. data/lib/bundler/man/bundle-info.1 +3 -9
  78. data/lib/bundler/man/bundle-init.1 +3 -12
  79. data/lib/bundler/man/bundle-inject.1 +6 -19
  80. data/lib/bundler/man/bundle-install.1 +27 -125
  81. data/lib/bundler/man/bundle-install.1.ronn +1 -0
  82. data/lib/bundler/man/bundle-list.1 +4 -19
  83. data/lib/bundler/man/bundle-lock.1 +5 -29
  84. data/lib/bundler/man/bundle-open.1 +7 -27
  85. data/lib/bundler/man/bundle-outdated.1 +3 -55
  86. data/lib/bundler/man/bundle-outdated.1.ronn +1 -0
  87. data/lib/bundler/man/bundle-platform.1 +5 -27
  88. data/lib/bundler/man/bundle-plugin.1 +3 -29
  89. data/lib/bundler/man/bundle-pristine.1 +5 -16
  90. data/lib/bundler/man/bundle-remove.1 +4 -14
  91. data/lib/bundler/man/bundle-show.1 +3 -10
  92. data/lib/bundler/man/bundle-update.1 +18 -137
  93. data/lib/bundler/man/bundle-version.1 +3 -16
  94. data/lib/bundler/man/bundle-viz.1 +4 -16
  95. data/lib/bundler/man/bundle.1 +5 -44
  96. data/lib/bundler/man/gemfile.5 +24 -301
  97. data/lib/bundler/man/gemfile.5.ronn +4 -0
  98. data/lib/bundler/match_metadata.rb +4 -0
  99. data/lib/bundler/match_platform.rb +1 -1
  100. data/lib/bundler/plugin/api/source.rb +3 -2
  101. data/lib/bundler/plugin/index.rb +8 -0
  102. data/lib/bundler/plugin/installer.rb +1 -1
  103. data/lib/bundler/plugin.rb +12 -5
  104. data/lib/bundler/resolver/base.rb +1 -1
  105. data/lib/bundler/resolver/incompatibility.rb +1 -1
  106. data/lib/bundler/resolver/spec_group.rb +1 -4
  107. data/lib/bundler/resolver.rb +16 -16
  108. data/lib/bundler/ruby_dsl.rb +20 -12
  109. data/lib/bundler/ruby_version.rb +1 -1
  110. data/lib/bundler/rubygems_ext.rb +27 -54
  111. data/lib/bundler/rubygems_gem_installer.rb +23 -58
  112. data/lib/bundler/rubygems_integration.rb +25 -94
  113. data/lib/bundler/runtime.rb +2 -2
  114. data/lib/bundler/self_manager.rb +23 -7
  115. data/lib/bundler/settings.rb +27 -7
  116. data/lib/bundler/setup.rb +4 -1
  117. data/lib/bundler/shared_helpers.rb +35 -13
  118. data/lib/bundler/source/git/git_proxy.rb +22 -14
  119. data/lib/bundler/source/git.rb +4 -3
  120. data/lib/bundler/source/metadata.rb +16 -16
  121. data/lib/bundler/source/path.rb +7 -6
  122. data/lib/bundler/source/rubygems.rb +21 -14
  123. data/lib/bundler/source.rb +2 -0
  124. data/lib/bundler/spec_set.rb +43 -12
  125. data/lib/bundler/stub_specification.rb +1 -0
  126. data/lib/bundler/templates/Executable.bundler +1 -1
  127. data/lib/bundler/templates/newgem/README.md.tt +3 -3
  128. data/lib/bundler/templates/newgem/Rakefile.tt +2 -6
  129. data/lib/bundler/templates/newgem/ext/newgem/Cargo.toml.tt +1 -1
  130. data/lib/bundler/templates/newgem/newgem.gemspec.tt +1 -1
  131. data/lib/bundler/templates/newgem/standard.yml.tt +1 -1
  132. data/lib/bundler/ui/shell.rb +2 -2
  133. data/lib/bundler/vendor/connection_pool/lib/connection_pool/version.rb +1 -1
  134. data/lib/bundler/vendor/connection_pool/lib/connection_pool.rb +53 -6
  135. data/lib/bundler/vendor/fileutils/lib/fileutils.rb +8 -20
  136. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/connection.rb +4 -3
  137. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/pool.rb +23 -11
  138. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent/timed_stack_multi.rb +1 -1
  139. data/lib/bundler/vendor/net-http-persistent/lib/net/http/persistent.rb +36 -36
  140. data/lib/bundler/vendor/thor/lib/thor/actions/create_file.rb +3 -2
  141. data/lib/bundler/vendor/thor/lib/thor/actions/directory.rb +1 -1
  142. data/lib/bundler/vendor/thor/lib/thor/actions/empty_directory.rb +1 -1
  143. data/lib/bundler/vendor/thor/lib/thor/actions/file_manipulation.rb +8 -10
  144. data/lib/bundler/vendor/thor/lib/thor/actions/inject_into_file.rb +15 -4
  145. data/lib/bundler/vendor/thor/lib/thor/actions.rb +15 -15
  146. data/lib/bundler/vendor/thor/lib/thor/base.rb +140 -14
  147. data/lib/bundler/vendor/thor/lib/thor/command.rb +13 -4
  148. data/lib/bundler/vendor/thor/lib/thor/core_ext/hash_with_indifferent_access.rb +4 -0
  149. data/lib/bundler/vendor/thor/lib/thor/error.rb +16 -25
  150. data/lib/bundler/vendor/thor/lib/thor/group.rb +1 -1
  151. data/lib/bundler/vendor/thor/lib/thor/invocation.rb +1 -1
  152. data/lib/bundler/vendor/thor/lib/thor/nested_context.rb +2 -2
  153. data/lib/bundler/vendor/thor/lib/thor/parser/argument.rb +20 -1
  154. data/lib/bundler/vendor/thor/lib/thor/parser/arguments.rb +33 -17
  155. data/lib/bundler/vendor/thor/lib/thor/parser/option.rb +27 -8
  156. data/lib/bundler/vendor/thor/lib/thor/parser/options.rb +44 -6
  157. data/lib/bundler/vendor/thor/lib/thor/rake_compat.rb +2 -2
  158. data/lib/bundler/vendor/thor/lib/thor/runner.rb +40 -30
  159. data/lib/bundler/vendor/thor/lib/thor/shell/basic.rb +26 -150
  160. data/lib/bundler/vendor/thor/lib/thor/shell/color.rb +4 -46
  161. data/lib/bundler/vendor/thor/lib/thor/shell/column_printer.rb +29 -0
  162. data/lib/bundler/vendor/thor/lib/thor/shell/html.rb +3 -45
  163. data/lib/bundler/vendor/thor/lib/thor/shell/lcs_diff.rb +49 -0
  164. data/lib/bundler/vendor/thor/lib/thor/shell/table_printer.rb +134 -0
  165. data/lib/bundler/vendor/thor/lib/thor/shell/terminal.rb +42 -0
  166. data/lib/bundler/vendor/thor/lib/thor/shell/wrapped_printer.rb +38 -0
  167. data/lib/bundler/vendor/thor/lib/thor/shell.rb +1 -1
  168. data/lib/bundler/vendor/thor/lib/thor/util.rb +8 -7
  169. data/lib/bundler/vendor/thor/lib/thor/version.rb +1 -1
  170. data/lib/bundler/vendor/thor/lib/thor.rb +155 -8
  171. data/lib/bundler/vendor/tsort/lib/tsort.rb +3 -0
  172. data/lib/bundler/vendor/uri/lib/uri/common.rb +256 -132
  173. data/lib/bundler/vendor/uri/lib/uri/generic.rb +1 -0
  174. data/lib/bundler/vendor/uri/lib/uri/rfc3986_parser.rb +95 -31
  175. data/lib/bundler/vendor/uri/lib/uri/version.rb +1 -1
  176. data/lib/bundler/vendored_net_http.rb +8 -0
  177. data/lib/bundler/vendored_persistent.rb +0 -4
  178. data/lib/bundler/vendored_timeout.rb +8 -0
  179. data/lib/bundler/version.rb +1 -1
  180. data/lib/bundler/vlad.rb +1 -1
  181. data/lib/bundler/yaml_serializer.rb +9 -4
  182. data/lib/bundler.rb +38 -35
  183. metadata +16 -5
@@ -9,11 +9,8 @@ module Bundler
9
9
 
10
10
  def initialize(directory)
11
11
  @directory = Pathname.new(directory).expand_path
12
- info_roots.each do |dir|
13
- SharedHelpers.filesystem_access(dir) do
14
- FileUtils.mkdir_p(dir)
15
- end
16
- end
12
+ info_roots.each {|dir| mkdir(dir) }
13
+ mkdir(info_etag_root)
17
14
  end
18
15
 
19
16
  def names
@@ -24,6 +21,10 @@ module Bundler
24
21
  directory.join("names")
25
22
  end
26
23
 
24
+ def names_etag_path
25
+ directory.join("names.etag")
26
+ end
27
+
27
28
  def versions
28
29
  versions_by_name = Hash.new {|hash, key| hash[key] = [] }
29
30
  info_checksums_by_name = {}
@@ -31,12 +32,12 @@ module Bundler
31
32
  lines(versions_path).each do |line|
32
33
  name, versions_string, info_checksum = line.split(" ", 3)
33
34
  info_checksums_by_name[name] = info_checksum || ""
34
- versions_string.split(",").each do |version|
35
- if version.start_with?("-")
36
- version = version[1..-1].split("-", 2).unshift(name)
35
+ versions_string.split(",") do |version|
36
+ delete = version.delete_prefix!("-")
37
+ version = version.split("-", 2).unshift(name)
38
+ if delete
37
39
  versions_by_name[name].delete(version)
38
40
  else
39
- version = version.split("-", 2).unshift(name)
40
41
  versions_by_name[name] << version
41
42
  end
42
43
  end
@@ -49,6 +50,10 @@ module Bundler
49
50
  directory.join("versions")
50
51
  end
51
52
 
53
+ def versions_etag_path
54
+ directory.join("versions.etag")
55
+ end
56
+
52
57
  def checksums
53
58
  checksums = {}
54
59
 
@@ -76,8 +81,19 @@ module Bundler
76
81
  end
77
82
  end
78
83
 
84
+ def info_etag_path(name)
85
+ name = name.to_s
86
+ info_etag_root.join("#{name}-#{SharedHelpers.digest(:MD5).hexdigest(name).downcase}")
87
+ end
88
+
79
89
  private
80
90
 
91
+ def mkdir(dir)
92
+ SharedHelpers.filesystem_access(dir) do
93
+ FileUtils.mkdir_p(dir)
94
+ end
95
+ end
96
+
81
97
  def lines(path)
82
98
  return [] unless path.file?
83
99
  lines = SharedHelpers.filesystem_access(path, :read, &:read).split("\n")
@@ -96,6 +112,10 @@ module Bundler
96
112
  directory.join("info-special-characters"),
97
113
  ]
98
114
  end
115
+
116
+ def info_etag_root
117
+ directory.join("info-etags")
118
+ end
99
119
  end
100
120
  end
101
121
  end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../vendored_fileutils"
4
+ require "rubygems/package"
5
+
6
+ module Bundler
7
+ class 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
+ SharedHelpers.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| SharedHelpers.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
+ # remove this method when we stop generating md5 digests for legacy etags
90
+ def md5
91
+ @digests && @digests["md5"]
92
+ end
93
+
94
+ def digests?
95
+ @digests&.any?
96
+ end
97
+
98
+ # Open the temp file for writing, reusing original permissions, yielding the IO object.
99
+ def open(write_mode = "wb", perm = @perm, &block)
100
+ raise ClosedError, "Cannot reopen closed file" if @closed
101
+ SharedHelpers.filesystem_access(path, :write) do
102
+ path.open(write_mode, perm) do |f|
103
+ yield digests? ? Gem::Package::DigestIO.new(f, @digests) : f
104
+ end
105
+ end
106
+ end
107
+
108
+ # Returns false without appending when no digests since appending is too error prone to do without digests.
109
+ def append(data)
110
+ return false unless digests?
111
+ open("a") {|f| f.write data }
112
+ verify && commit
113
+ end
114
+
115
+ def write(data)
116
+ reset_digests
117
+ open {|f| f.write data }
118
+ commit!
119
+ end
120
+
121
+ def commit!
122
+ verify || raise(DigestMismatchError.new(@base64digests, @expected_digests))
123
+ commit
124
+ end
125
+
126
+ # Verify the digests, returning true on match, false on mismatch.
127
+ def verify
128
+ return true unless @expected_digests && digests?
129
+ @base64digests = @digests.transform_values!(&:base64digest)
130
+ @digests = nil
131
+ @base64digests.all? {|algo, digest| @expected_digests[algo] == digest }
132
+ end
133
+
134
+ # Replace the original file with the temp file without verifying digests.
135
+ # The file is permanently closed.
136
+ def commit
137
+ raise ClosedError, "Cannot commit closed file" if @closed
138
+ SharedHelpers.filesystem_access(original_path, :write) do
139
+ FileUtils.mv(path, original_path)
140
+ end
141
+ @closed = true
142
+ end
143
+
144
+ # Remove the temp file without replacing the original file.
145
+ # The file is permanently closed.
146
+ def close
147
+ return if @closed
148
+ FileUtils.remove_file(path) if @path&.file?
149
+ @closed = true
150
+ end
151
+ end
152
+ end
153
+ end
@@ -6,12 +6,15 @@ module Bundler
6
6
  GemParser = Gem::Resolver::APISet::GemParser
7
7
  else
8
8
  class GemParser
9
+ EMPTY_ARRAY = [].freeze
10
+ private_constant :EMPTY_ARRAY
11
+
9
12
  def parse(line)
10
13
  version_and_platform, rest = line.split(" ", 2)
11
14
  version, platform = version_and_platform.split("-", 2)
12
- dependencies, requirements = rest.split("|", 2).map {|s| s.split(",") } if rest
13
- dependencies = dependencies ? dependencies.map {|d| parse_dependency(d) } : []
14
- requirements = requirements ? requirements.map {|d| parse_dependency(d) } : []
15
+ dependencies, requirements = rest.split("|", 2).map! {|s| s.split(",") } if rest
16
+ dependencies = dependencies ? dependencies.map! {|d| parse_dependency(d) } : EMPTY_ARRAY
17
+ requirements = requirements ? requirements.map! {|d| parse_dependency(d) } : EMPTY_ARRAY
15
18
  [version, platform, dependencies, requirements]
16
19
  end
17
20
 
@@ -20,6 +23,7 @@ module Bundler
20
23
  def parse_dependency(string)
21
24
  dependency = string.split(":")
22
25
  dependency[-1] = dependency[-1].split("&") if dependency.size > 1
26
+ dependency[0] = -dependency[0]
23
27
  dependency
24
28
  end
25
29
  end
@@ -1,20 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../vendored_fileutils"
4
-
5
3
  module Bundler
6
4
  class CompactIndexClient
7
5
  class Updater
8
- class MisMatchedChecksumError < Error
9
- def initialize(path, server_checksum, local_checksum)
10
- @path = path
11
- @server_checksum = server_checksum
12
- @local_checksum = local_checksum
13
- end
14
-
15
- def message
16
- "The checksum of /#{@path} does not match the checksum provided by the server! Something is wrong " \
17
- "(local checksum is #{@local_checksum.inspect}, was expecting #{@server_checksum.inspect})."
6
+ class MismatchedChecksumError < Error
7
+ def initialize(path, message)
8
+ super "The checksum of /#{path} does not match the checksum provided by the server! Something is wrong. #{message}"
18
9
  end
19
10
  end
20
11
 
@@ -22,95 +13,102 @@ module Bundler
22
13
  @fetcher = fetcher
23
14
  end
24
15
 
25
- def update(local_path, remote_path, retrying = nil)
26
- headers = {}
27
-
28
- local_temp_path = local_path.sub(/$/, ".#{$$}")
29
- local_temp_path = local_temp_path.sub(/$/, ".retrying") if retrying
30
- local_temp_path = local_temp_path.sub(/$/, ".tmp")
31
-
32
- # first try to fetch any new bytes on the existing file
33
- if retrying.nil? && local_path.file?
34
- copy_file local_path, local_temp_path
16
+ def update(remote_path, local_path, etag_path)
17
+ append(remote_path, local_path, etag_path) || replace(remote_path, local_path, etag_path)
18
+ rescue CacheFile::DigestMismatchError => e
19
+ raise MismatchedChecksumError.new(remote_path, e.message)
20
+ rescue Zlib::GzipFile::Error
21
+ raise Bundler::HTTPError
22
+ end
35
23
 
36
- headers["If-None-Match"] = etag_for(local_temp_path)
37
- headers["Range"] =
38
- if local_temp_path.size.nonzero?
39
- # Subtract a byte to ensure the range won't be empty.
40
- # Avoids 416 (Range Not Satisfiable) responses.
41
- "bytes=#{local_temp_path.size - 1}-"
42
- else
43
- "bytes=#{local_temp_path.size}-"
44
- end
45
- end
24
+ private
46
25
 
47
- response = @fetcher.call(remote_path, headers)
48
- return nil if response.is_a?(Net::HTTPNotModified)
26
+ def append(remote_path, local_path, etag_path)
27
+ return false unless local_path.file? && local_path.size.nonzero?
49
28
 
50
- content = response.body
29
+ CacheFile.copy(local_path) do |file|
30
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
31
+ etag ||= generate_etag(etag_path, file) # Remove this after 2.5.0 has been out for a while.
51
32
 
52
- etag = (response["ETag"] || "").gsub(%r{\AW/}, "")
53
- correct_response = SharedHelpers.filesystem_access(local_temp_path) do
54
- if response.is_a?(Net::HTTPPartialContent) && local_temp_path.size.nonzero?
55
- local_temp_path.open("a") {|f| f << slice_body(content, 1..-1) }
33
+ # Subtract a byte to ensure the range won't be empty.
34
+ # Avoids 416 (Range Not Satisfiable) responses.
35
+ response = @fetcher.call(remote_path, request_headers(etag, file.size - 1))
36
+ break true if response.is_a?(Gem::Net::HTTPNotModified)
56
37
 
57
- etag_for(local_temp_path) == etag
38
+ file.digests = parse_digests(response)
39
+ # server may ignore Range and return the full response
40
+ if response.is_a?(Gem::Net::HTTPPartialContent)
41
+ break false unless file.append(response.body.byteslice(1..-1))
58
42
  else
59
- local_temp_path.open("wb") {|f| f << content }
60
-
61
- etag.length.zero? || etag_for(local_temp_path) == etag
43
+ file.write(response.body)
62
44
  end
45
+ CacheFile.write(etag_path, etag(response))
46
+ true
63
47
  end
48
+ end
64
49
 
65
- if correct_response
66
- SharedHelpers.filesystem_access(local_path) do
67
- FileUtils.mv(local_temp_path, local_path)
68
- end
69
- return nil
70
- end
50
+ # request without range header to get the full file or a 304 Not Modified
51
+ def replace(remote_path, local_path, etag_path)
52
+ etag = etag_path.read.tap(&:chomp!) if etag_path.file?
53
+ response = @fetcher.call(remote_path, request_headers(etag))
54
+ return true if response.is_a?(Gem::Net::HTTPNotModified)
55
+ CacheFile.write(local_path, response.body, parse_digests(response))
56
+ CacheFile.write(etag_path, etag(response))
57
+ end
71
58
 
72
- if retrying
73
- raise MisMatchedChecksumError.new(remote_path, etag, etag_for(local_temp_path))
74
- end
59
+ def request_headers(etag, range_start = nil)
60
+ headers = {}
61
+ headers["Range"] = "bytes=#{range_start}-" if range_start
62
+ headers["If-None-Match"] = etag if etag
63
+ headers
64
+ end
75
65
 
76
- update(local_path, remote_path, :retrying)
77
- rescue Zlib::GzipFile::Error
78
- raise Bundler::HTTPError
79
- ensure
80
- FileUtils.remove_file(local_temp_path) if File.exist?(local_temp_path)
66
+ def etag_for_request(etag_path)
67
+ etag_path.read.tap(&:chomp!) if etag_path.file?
81
68
  end
82
69
 
83
- def etag_for(path)
84
- sum = checksum_for_file(path)
85
- sum ? %("#{sum}") : nil
70
+ # When first releasing this opaque etag feature, we want to generate the old MD5 etag
71
+ # based on the content of the file. After that it will always use the saved opaque etag.
72
+ # This transparently saves existing users with good caches from updating a bunch of files.
73
+ # Remove this behavior after 2.5.0 has been out for a while.
74
+ def generate_etag(etag_path, file)
75
+ etag = file.md5.hexdigest
76
+ CacheFile.write(etag_path, etag)
77
+ etag
86
78
  end
87
79
 
88
- def slice_body(body, range)
89
- body.byteslice(range)
80
+ def etag(response)
81
+ return unless response["ETag"]
82
+ etag = response["ETag"].delete_prefix("W/")
83
+ return if etag.delete_prefix!('"') && !etag.delete_suffix!('"')
84
+ etag
90
85
  end
91
86
 
92
- def checksum_for_file(path)
93
- return nil unless path.file?
94
- # This must use File.read instead of Digest.file().hexdigest
95
- # because we need to preserve \n line endings on windows when calculating
96
- # the checksum
97
- SharedHelpers.filesystem_access(path, :read) do
98
- SharedHelpers.digest(:MD5).hexdigest(File.read(path))
87
+ # Unwraps and returns a Hash of digest algorithms and base64 values
88
+ # according to RFC 8941 Structured Field Values for HTTP.
89
+ # https://www.rfc-editor.org/rfc/rfc8941#name-parsing-a-byte-sequence
90
+ # Ignores unsupported algorithms.
91
+ def parse_digests(response)
92
+ return unless header = response["Repr-Digest"] || response["Digest"]
93
+ digests = {}
94
+ header.split(",") do |param|
95
+ algorithm, value = param.split("=", 2)
96
+ algorithm.strip!
97
+ algorithm.downcase!
98
+ next unless SUPPORTED_DIGESTS.key?(algorithm)
99
+ next unless value = byte_sequence(value)
100
+ digests[algorithm] = value
99
101
  end
102
+ digests.empty? ? nil : digests
100
103
  end
101
104
 
102
- private
103
-
104
- def copy_file(source, dest)
105
- SharedHelpers.filesystem_access(source, :read) do
106
- File.open(source, "r") do |s|
107
- SharedHelpers.filesystem_access(dest, :write) do
108
- File.open(dest, "wb", s.stat.mode) do |f|
109
- IO.copy_stream(s, f)
110
- end
111
- end
112
- end
113
- end
105
+ # Unwrap surrounding colons (byte sequence)
106
+ # The wrapping characters must be matched or we return nil.
107
+ # Also handles quotes because right now rubygems.org sends them.
108
+ def byte_sequence(value)
109
+ return if value.delete_prefix!(":") && !value.delete_suffix!(":")
110
+ return if value.delete_prefix!('"') && !value.delete_suffix!('"')
111
+ value
114
112
  end
115
113
  end
116
114
  end
@@ -5,7 +5,13 @@ require "set"
5
5
 
6
6
  module Bundler
7
7
  class CompactIndexClient
8
+ # NOTE: MD5 is here not because we expect a server to respond with it, but
9
+ # because we use it to generate the etag on first request during the upgrade
10
+ # to the compact index client that uses opaque etags saved to files.
11
+ # Remove once 2.5.0 has been out for a while.
12
+ SUPPORTED_DIGESTS = { "sha-256" => :SHA256, "md5" => :MD5 }.freeze
8
13
  DEBUG_MUTEX = Thread::Mutex.new
14
+
9
15
  def self.debug
10
16
  return unless ENV["DEBUG_COMPACT_INDEX"]
11
17
  DEBUG_MUTEX.synchronize { warn("[#{self}] #{yield}") }
@@ -14,6 +20,7 @@ module Bundler
14
20
  class Error < StandardError; end
15
21
 
16
22
  require_relative "compact_index_client/cache"
23
+ require_relative "compact_index_client/cache_file"
17
24
  require_relative "compact_index_client/updater"
18
25
 
19
26
  attr_reader :directory
@@ -54,13 +61,13 @@ module Bundler
54
61
 
55
62
  def names
56
63
  Bundler::CompactIndexClient.debug { "/names" }
57
- update(@cache.names_path, "names")
64
+ update("names", @cache.names_path, @cache.names_etag_path)
58
65
  @cache.names
59
66
  end
60
67
 
61
68
  def versions
62
69
  Bundler::CompactIndexClient.debug { "/versions" }
63
- update(@cache.versions_path, "versions")
70
+ update("versions", @cache.versions_path, @cache.versions_etag_path)
64
71
  versions, @info_checksums_by_name = @cache.versions
65
72
  versions
66
73
  end
@@ -76,36 +83,36 @@ module Bundler
76
83
  def update_and_parse_checksums!
77
84
  Bundler::CompactIndexClient.debug { "update_and_parse_checksums!" }
78
85
  return @info_checksums_by_name if @parsed_checksums
79
- update(@cache.versions_path, "versions")
86
+ update("versions", @cache.versions_path, @cache.versions_etag_path)
80
87
  @info_checksums_by_name = @cache.checksums
81
88
  @parsed_checksums = true
82
89
  end
83
90
 
84
91
  private
85
92
 
86
- def update(local_path, remote_path)
93
+ def update(remote_path, local_path, local_etag_path)
87
94
  Bundler::CompactIndexClient.debug { "update(#{local_path}, #{remote_path})" }
88
95
  unless synchronize { @endpoints.add?(remote_path) }
89
96
  Bundler::CompactIndexClient.debug { "already fetched #{remote_path}" }
90
97
  return
91
98
  end
92
- @updater.update(local_path, url(remote_path))
99
+ @updater.update(url(remote_path), local_path, local_etag_path)
93
100
  end
94
101
 
95
102
  def update_info(name)
96
103
  Bundler::CompactIndexClient.debug { "update_info(#{name})" }
97
104
  path = @cache.info_path(name)
98
- checksum = @updater.checksum_for_file(path)
99
105
  unless existing = @info_checksums_by_name[name]
100
106
  Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since it is missing from versions" }
101
107
  return
102
108
  end
109
+ checksum = SharedHelpers.checksum_for_file(path, :MD5)
103
110
  if checksum == existing
104
111
  Bundler::CompactIndexClient.debug { "skipping updating info for #{name} since the versions checksum matches the local checksum" }
105
112
  return
106
113
  end
107
114
  Bundler::CompactIndexClient.debug { "updating info for #{name} since the versions checksum #{existing} != the local checksum #{checksum}" }
108
- update(path, "info/#{name}")
115
+ update("info/#{name}", path, @cache.info_etag_path(name))
109
116
  end
110
117
 
111
118
  def url(path)
@@ -3,5 +3,5 @@
3
3
  module Bundler
4
4
  WINDOWS = RbConfig::CONFIG["host_os"] =~ /(msdos|mswin|djgpp|mingw)/
5
5
  FREEBSD = RbConfig::CONFIG["host_os"].to_s.include?("bsd")
6
- NULL = WINDOWS ? "NUL" : "/dev/null"
6
+ NULL = File::NULL
7
7
  end
@@ -43,7 +43,7 @@ module Bundler
43
43
  ].freeze
44
44
 
45
45
  def ruby?
46
- return true if Bundler::GemHelpers.generic_local_platform == Gem::Platform::RUBY
46
+ return true if Bundler::GemHelpers.generic_local_platform_is_ruby?
47
47
 
48
48
  !windows? && (RUBY_ENGINE == "ruby" || RUBY_ENGINE == "rbx" || RUBY_ENGINE == "maglev" || RUBY_ENGINE == "truffleruby")
49
49
  end
@@ -71,26 +71,10 @@ module Bundler
71
71
  def windows?
72
72
  Gem.win_platform?
73
73
  end
74
-
75
- def mswin?
76
- # For backwards compatibility
77
- windows?
78
-
79
- # TODO: This should correctly be:
80
- # windows? && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mswin32" && Bundler.local_platform.cpu == "x86"
81
- end
82
-
83
- def mswin64?
84
- windows? && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mswin64" && Bundler.local_platform.cpu == "x64"
85
- end
86
-
87
- def mingw?
88
- windows? && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os == "mingw32" && Bundler.local_platform.cpu != "x64"
89
- end
90
-
91
- def x64_mingw?
92
- Gem.win_platform? && Bundler.local_platform != Gem::Platform::RUBY && Bundler.local_platform.os.start_with?("mingw") && Bundler.local_platform.cpu == "x64"
93
- end
74
+ alias_method :mswin?, :windows?
75
+ alias_method :mswin64?, :windows?
76
+ alias_method :mingw?, :windows?
77
+ alias_method :x64_mingw?, :windows?
94
78
 
95
79
  (KNOWN_MINOR_VERSIONS + KNOWN_MAJOR_VERSIONS).each do |version|
96
80
  trimmed_version = version.tr(".", "")