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
@@ -0,0 +1,880 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Skip reloading when an identical copy (e.g. the one shipped inside the Bundler
4
+ # gem) was already required from a different path, to avoid redefinition warnings.
5
+ return if defined?(Gem::YAMLSerializer)
6
+
7
+ unless defined?(Psych::VERSION)
8
+ module Psych
9
+ class Exception < ::RuntimeError; end
10
+ class SyntaxError < Exception; end
11
+ class DisallowedClass < Exception; end
12
+ class BadAlias < Exception; end
13
+ class AliasesNotEnabled < BadAlias; end
14
+ end
15
+ end
16
+
17
+ module Gem
18
+ module YAMLSerializer
19
+ Scalar = Struct.new(:value, :tag, :anchor, keyword_init: true)
20
+
21
+ Mapping = Struct.new(:pairs, :tag, :anchor, keyword_init: true) do
22
+ def initialize(pairs: [], tag: nil, anchor: nil)
23
+ super
24
+ end
25
+ end
26
+
27
+ Sequence = Struct.new(:items, :tag, :anchor, keyword_init: true) do
28
+ def initialize(items: [], tag: nil, anchor: nil)
29
+ super
30
+ end
31
+ end
32
+
33
+ AliasRef = Struct.new(:name, keyword_init: true)
34
+
35
+ class Parser
36
+ # A plain (unquoted) mapping key followed by ":". "#" inside a key is
37
+ # literal unless preceded by a space (which would start a comment).
38
+ MAPPING_KEY_RE = /^((?:[^#:\s]|:[^ ])(?:[^#:]|:[^ ]|(?<=[^ ])#)*):(?:[ ]+(.*))?$/
39
+ # A quoted mapping key followed by ":". A whole-line quoted scalar
40
+ # cannot match because its closing quote is at the end of the line.
41
+ QUOTED_KEY_RE = /^("(?:[^"\\]|\\.)*"|'(?:[^']|'')*'):(?:[ ]+(.*))?$/
42
+ MAX_NESTING_DEPTH = 1_000
43
+
44
+ STRING_UNESCAPES = {
45
+ "\\\\" => "\\",
46
+ "\\\"" => "\"",
47
+ "\\0" => "\0",
48
+ "\\a" => "\a",
49
+ "\\b" => "\b",
50
+ "\\t" => "\t",
51
+ "\\n" => "\n",
52
+ "\\v" => "\v",
53
+ "\\f" => "\f",
54
+ "\\r" => "\r",
55
+ "\\e" => "\e",
56
+ }.freeze
57
+
58
+ def initialize(source)
59
+ @lines = source.split(/\r?\n/)
60
+ @anchors = {}
61
+ @depth = 0
62
+ strip_document_prefix
63
+ end
64
+
65
+ def parse
66
+ return nil if @lines.empty?
67
+
68
+ root = nil
69
+ while @lines.any?
70
+ before = @lines.size
71
+ node = parse_node(-1)
72
+ @lines.shift if @lines.size == before && @lines.any?
73
+
74
+ if root.is_a?(Mapping) && node.is_a?(Mapping)
75
+ root.pairs.concat(node.pairs)
76
+ elsif root.nil?
77
+ root = node
78
+ end
79
+ end
80
+ root
81
+ end
82
+
83
+ private
84
+
85
+ def strip_document_prefix
86
+ return if @lines.empty?
87
+ return unless @lines[0]&.start_with?("---")
88
+
89
+ if @lines[0].strip == "---"
90
+ @lines.shift
91
+ else
92
+ @lines[0] = @lines[0].sub(/^---\s*/, "")
93
+ end
94
+ end
95
+
96
+ def parse_node(base_indent)
97
+ @depth += 1
98
+ raise_max_nesting! if @depth > MAX_NESTING_DEPTH
99
+
100
+ skip_blank_and_comments
101
+ return nil if @lines.empty?
102
+
103
+ line = @lines[0]
104
+ stripped = line.lstrip
105
+ indent = line.size - stripped.size
106
+ return nil if indent < base_indent
107
+
108
+ return parse_alias_ref if stripped.start_with?("*")
109
+
110
+ anchor = consume_anchor
111
+
112
+ if anchor
113
+ line = @lines[0]
114
+ stripped = line.lstrip
115
+ end
116
+
117
+ if stripped.start_with?("- ") || stripped == "-"
118
+ parse_sequence(indent, anchor)
119
+ elsif QUOTED_KEY_RE.match?(stripped)
120
+ # A mapping whose key is quoted, e.g. '"have: colon": value'.
121
+ parse_mapping(indent, anchor)
122
+ elsif (stripped.start_with?("\"") && stripped.end_with?("\"")) ||
123
+ (stripped.start_with?("'") && stripped.end_with?("'"))
124
+ # A whole-line quoted scalar, e.g. '"system: foo: bar"'. It may
125
+ # contain ": ", so it must be checked before MAPPING_KEY_RE.
126
+ parse_plain_scalar(indent, anchor)
127
+ elsif stripped =~ MAPPING_KEY_RE && !stripped.start_with?("!ruby/object:")
128
+ parse_mapping(indent, anchor)
129
+ elsif stripped.start_with?("!ruby/object:")
130
+ parse_tagged_node(indent, anchor)
131
+ elsif stripped.start_with?("|")
132
+ modifier = stripped[1..].to_s.strip
133
+ @lines.shift
134
+ register_anchor(anchor, Scalar.new(value: parse_block_scalar(indent, modifier)))
135
+ else
136
+ parse_plain_scalar(indent, anchor)
137
+ end
138
+ ensure
139
+ @depth -= 1
140
+ end
141
+
142
+ def parse_sequence(indent, anchor)
143
+ items = []
144
+ while @lines.any?
145
+ line = @lines[0]
146
+ stripped = line.lstrip
147
+ break unless line.size - stripped.size == indent &&
148
+ (stripped.start_with?("- ") || stripped == "-")
149
+ content = @lines.shift.lstrip[1..].strip
150
+ item_anchor, content = extract_item_anchor(content)
151
+ item = parse_sequence_item(content, indent)
152
+ items << register_anchor(item_anchor, item)
153
+ end
154
+ register_anchor(anchor, Sequence.new(items: items))
155
+ end
156
+
157
+ def parse_sequence_item(content, indent)
158
+ if content.start_with?("*")
159
+ parse_inline_alias(content)
160
+ elsif content.empty?
161
+ @lines.any? && current_indent > indent ? parse_node(indent) : nil
162
+ elsif content.start_with?("!ruby/object:")
163
+ parse_tagged_content(content.strip, indent)
164
+ elsif content.start_with?("!binary ")
165
+ parse_binary_value(content, indent)
166
+ elsif content.start_with?("-")
167
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
168
+ parse_node(indent)
169
+ elsif QUOTED_KEY_RE.match?(content) ||
170
+ (content =~ MAPPING_KEY_RE && !content.start_with?("!ruby/object:"))
171
+ @lines.unshift("#{" " * (indent + 2)}#{content}")
172
+ parse_node(indent)
173
+ elsif content.start_with?("|")
174
+ Scalar.new(value: parse_block_scalar(indent, content[1..].to_s.strip))
175
+ else
176
+ parse_inline_scalar(content, indent)
177
+ end
178
+ end
179
+
180
+ def parse_mapping(indent, anchor)
181
+ pairs = []
182
+ while @lines.any?
183
+ line = @lines[0]
184
+ stripped = line.lstrip
185
+ break unless line.size - stripped.size == indent
186
+
187
+ if (match = QUOTED_KEY_RE.match(stripped))
188
+ key = coerce(match[1])
189
+ elsif (match = MAPPING_KEY_RE.match(stripped)) && !stripped.start_with?("!ruby/object:")
190
+ key = match[1].strip
191
+ key = decode_binary_tag(key) if key.start_with?("!binary ")
192
+ else
193
+ break
194
+ end
195
+ @lines.shift
196
+ val = strip_comment(match[2].to_s.strip)
197
+
198
+ val_anchor, val = consume_value_anchor(val)
199
+ value = parse_mapping_value(val, indent)
200
+ value = register_anchor(val_anchor, value) if val_anchor
201
+
202
+ pairs << [Scalar.new(value: key), value]
203
+ end
204
+ register_anchor(anchor, Mapping.new(pairs: pairs))
205
+ end
206
+
207
+ def parse_mapping_value(val, indent)
208
+ if val.start_with?("*")
209
+ parse_inline_alias(val)
210
+ elsif val.start_with?("!ruby/object:")
211
+ parse_tagged_content(val.strip, indent)
212
+ elsif val.start_with?("!binary ")
213
+ parse_binary_value(val, indent)
214
+ elsif val.empty?
215
+ next_stripped = nil
216
+ next_indent = nil
217
+ if @lines.any?
218
+ next_stripped = @lines[0].lstrip
219
+ next_indent = @lines[0].size - next_stripped.size
220
+ end
221
+ if next_stripped &&
222
+ (next_stripped.start_with?("- ") || next_stripped == "-") &&
223
+ next_indent == indent
224
+ parse_node(indent)
225
+ else
226
+ parse_node(indent + 1)
227
+ end
228
+ elsif val == "[]"
229
+ Sequence.new
230
+ elsif val == "{}"
231
+ Mapping.new
232
+ elsif val.start_with?("|")
233
+ Scalar.new(value: parse_block_scalar(indent, val[1..].to_s.strip))
234
+ else
235
+ parse_inline_scalar(val, indent)
236
+ end
237
+ end
238
+
239
+ def parse_tagged_node(indent, anchor)
240
+ tag = @lines.shift.strip
241
+ nested = parse_node(indent)
242
+ apply_tag(nested, tag, anchor)
243
+ end
244
+
245
+ def parse_tagged_content(tag, indent)
246
+ nested = parse_node(indent)
247
+ apply_tag(nested, tag, nil)
248
+ end
249
+
250
+ def apply_tag(node, tag, anchor)
251
+ if node.is_a?(Mapping)
252
+ node.tag = tag
253
+ node.anchor = anchor
254
+ node
255
+ else
256
+ Mapping.new(pairs: [[Scalar.new(value: "value"), node]], tag: tag, anchor: anchor)
257
+ end
258
+ end
259
+
260
+ def parse_block_scalar(base_indent, modifier)
261
+ parts = []
262
+ block_indent = nil
263
+
264
+ while @lines.any?
265
+ line = @lines[0]
266
+ if line.strip.empty?
267
+ parts << "\n"
268
+ @lines.shift
269
+ else
270
+ line_indent = line.size - line.lstrip.size
271
+ break if line_indent <= base_indent
272
+ block_indent ||= line_indent
273
+ parts << @lines.shift[block_indent..].to_s << "\n"
274
+ end
275
+ end
276
+
277
+ res = parts.join
278
+ res.chomp! if modifier == "-" && res.end_with?("\n")
279
+ res
280
+ end
281
+
282
+ def parse_plain_scalar(indent, anchor)
283
+ result = coerce(@lines.shift.strip)
284
+ return register_anchor(anchor, result) if result.is_a?(Mapping) || result.is_a?(Sequence)
285
+
286
+ while result.is_a?(String) && @lines.any? &&
287
+ !@lines[0].strip.empty? && current_indent > indent
288
+ result << " " << @lines.shift.strip
289
+ end
290
+ register_anchor(anchor, Scalar.new(value: result))
291
+ end
292
+
293
+ def parse_inline_scalar(val, indent)
294
+ result = coerce(val)
295
+ return result if result.is_a?(Mapping) || result.is_a?(Sequence)
296
+
297
+ while result.is_a?(String) && @lines.any? &&
298
+ !@lines[0].strip.empty? && current_indent > indent
299
+ result << " " << @lines.shift.strip
300
+ end
301
+ Scalar.new(value: result)
302
+ end
303
+
304
+ def coerce(val, depth = 0)
305
+ raise_max_nesting! if depth > MAX_NESTING_DEPTH
306
+
307
+ val = val.sub(/^! /, "") if val.start_with?("! ")
308
+
309
+ if val =~ /^"(.*)"$/
310
+ $1.gsub(/\\(?:["\\0abtnvfre]|x\h{2})/) do |m|
311
+ STRING_UNESCAPES[m] || m[2..].to_i(16).chr(Encoding::UTF_8)
312
+ end
313
+ elsif val =~ /^'(.*)'$/
314
+ $1.gsub(/''/, "'")
315
+ elsif val == "true"
316
+ true
317
+ elsif val == "false"
318
+ false
319
+ elsif ["~", "null"].include?(val)
320
+ nil
321
+ elsif val == "{}"
322
+ Mapping.new
323
+ elsif val =~ /^\[(.*)\]$/
324
+ inner = $1.strip
325
+ return Sequence.new if inner.empty?
326
+ items = inner.split(/\s*,\s*/).reject(&:empty?).map {|e| Scalar.new(value: coerce(e, depth + 1)) }
327
+ Sequence.new(items: items)
328
+ elsif /\A\d{4}-\d{2}-\d{2}([ T]\d{2}:\d{2}:\d{2})?/.match?(val)
329
+ begin
330
+ Time.new(val)
331
+ rescue ArgumentError
332
+ # date-only format like "2024-06-15" is not supported by Time.new
333
+ if /\A(\d{4})-(\d{2})-(\d{2})\z/.match(val)
334
+ Time.utc($1.to_i, $2.to_i, $3.to_i)
335
+ else
336
+ val
337
+ end
338
+ end
339
+ elsif /^-?\d+$/.match?(val)
340
+ val.to_i
341
+ else
342
+ val
343
+ end
344
+ end
345
+
346
+ def decode_binary_tag(str)
347
+ content = str.sub(/\A!binary\s+/, "")
348
+ content = $1 if content =~ /\A"(.*)"\z/ || content =~ /\A'(.*)'\z/
349
+ content.unpack1("m")
350
+ end
351
+
352
+ def parse_binary_value(val, indent)
353
+ rest = val.sub(/\A!binary\s+/, "")
354
+ if rest.start_with?("|")
355
+ content = parse_block_scalar(indent, rest[1..].to_s.strip)
356
+ Scalar.new(value: content.unpack1("m"))
357
+ else
358
+ Scalar.new(value: decode_binary_tag(val))
359
+ end
360
+ end
361
+
362
+ def parse_alias_ref
363
+ AliasRef.new(name: @lines.shift.lstrip[1..].strip)
364
+ end
365
+
366
+ def parse_inline_alias(content)
367
+ AliasRef.new(name: content[1..].strip)
368
+ end
369
+
370
+ def current_indent
371
+ line = @lines[0]
372
+ line.size - line.lstrip.size
373
+ end
374
+
375
+ def consume_anchor
376
+ line = @lines[0]
377
+ stripped = line.lstrip
378
+ return nil unless stripped.start_with?("&") && stripped =~ /^&(\S+)\s+/
379
+
380
+ anchor = $1
381
+ @lines[0] = line.sub(/&#{Regexp.escape(anchor)}\s+/, "")
382
+ anchor
383
+ end
384
+
385
+ def extract_item_anchor(content)
386
+ return [nil, content] unless content =~ /^&(\S+)/
387
+
388
+ anchor = $1
389
+ [anchor, content.sub(/^&#{Regexp.escape(anchor)}\s*/, "")]
390
+ end
391
+
392
+ def consume_value_anchor(val)
393
+ return [nil, val] unless val =~ /^&(\S+)\s+/
394
+
395
+ anchor = $1
396
+ [anchor, val.sub(/^&#{Regexp.escape(anchor)}\s+/, "")]
397
+ end
398
+
399
+ def register_anchor(name, node)
400
+ if name
401
+ @anchors[name] = node
402
+ node.anchor = name if node.respond_to?(:anchor=)
403
+ end
404
+ node
405
+ end
406
+
407
+ def raise_max_nesting!
408
+ message = "exceeded maximum nesting depth (#{MAX_NESTING_DEPTH})"
409
+ if defined?(Psych::VERSION)
410
+ raise Psych::SyntaxError.new(nil, 0, 0, 0, message, nil)
411
+ else
412
+ raise Psych::SyntaxError, message
413
+ end
414
+ end
415
+
416
+ def skip_blank_and_comments
417
+ while @lines.any?
418
+ line = @lines[0]
419
+ stripped = line.lstrip
420
+ break unless stripped.empty? || stripped.start_with?("#")
421
+ @lines.shift
422
+ end
423
+ end
424
+
425
+ def strip_comment(val)
426
+ return val unless val.include?("#")
427
+ return "" if val.lstrip.start_with?("#")
428
+
429
+ in_single = false
430
+ in_double = false
431
+ escape = false
432
+
433
+ val.each_char.with_index do |ch, i|
434
+ if escape
435
+ escape = false
436
+ next
437
+ end
438
+
439
+ if in_single
440
+ in_single = false if ch == "'"
441
+ elsif in_double
442
+ if ch == "\\"
443
+ escape = true
444
+ elsif ch == '"'
445
+ in_double = false
446
+ end
447
+ else
448
+ case ch
449
+ when "'" then in_single = true
450
+ when '"' then in_double = true
451
+ when "#"
452
+ # A "#" starts a comment only when preceded by whitespace.
453
+ return val[0...i].rstrip if [" ", "\t"].include?(val[i - 1])
454
+ end
455
+ end
456
+ end
457
+
458
+ val
459
+ end
460
+ end
461
+
462
+ class Builder
463
+ VALID_OPS = %w[= != > < >= <= ~>].freeze
464
+ ARRAY_FIELDS = %w[files test_files executables extra_rdoc_files].freeze
465
+ MAX_ALIAS_RESOLUTIONS = 1_000
466
+
467
+ def initialize(permitted_classes: [], permitted_symbols: [], aliases: true)
468
+ @permitted_classes = permitted_classes.map {|c| "!ruby/object:#{c}" }
469
+ @permitted_symbols = permitted_symbols
470
+ @aliases = aliases
471
+ @anchor_values = {}
472
+ @alias_count = 0
473
+ end
474
+
475
+ def build(node)
476
+ return nil if node.nil?
477
+
478
+ result = build_node(node)
479
+
480
+ if result.is_a?(Hash) && result[:tag] == "!ruby/object:Gem::Specification"
481
+ build_specification(result)
482
+ else
483
+ result
484
+ end
485
+ end
486
+
487
+ private
488
+
489
+ def build_node(node)
490
+ case node
491
+ when nil then nil
492
+ when AliasRef then resolve_alias(node)
493
+ when Scalar then store_anchor(node.anchor, node.value)
494
+ when Mapping then build_mapping(node)
495
+ when Sequence then store_anchor(node.anchor, node.items.map {|item| build_node(item) })
496
+ else node # already a Ruby object
497
+ end
498
+ end
499
+
500
+ def resolve_alias(node)
501
+ raise Psych::AliasesNotEnabled unless @aliases
502
+ @alias_count += 1
503
+ if @alias_count > MAX_ALIAS_RESOLUTIONS
504
+ raise Psych::BadAlias, "exceeded maximum alias resolutions (#{MAX_ALIAS_RESOLUTIONS})"
505
+ end
506
+ unless @anchor_values.key?(node.name)
507
+ klass = defined?(Psych::AnchorNotDefined) ? Psych::AnchorNotDefined : Psych::BadAlias
508
+ raise klass, "An alias referenced an unknown anchor: #{node.name}"
509
+ end
510
+ @anchor_values.fetch(node.name)
511
+ end
512
+
513
+ def store_anchor(name, value)
514
+ @anchor_values[name] = value if name
515
+ value
516
+ end
517
+
518
+ def build_mapping(node)
519
+ validate_tag!(node.tag) if node.tag
520
+
521
+ result = case node.tag
522
+ when "!ruby/object:Gem::Version"
523
+ build_version(node)
524
+ when "!ruby/object:Gem::Platform"
525
+ build_platform(node)
526
+ when "!ruby/object:Gem::Requirement", "!ruby/object:Gem::Version::Requirement"
527
+ build_requirement(node)
528
+ when "!ruby/object:Gem::Dependency"
529
+ build_dependency(node)
530
+ when nil
531
+ build_hash(node)
532
+ when "!ruby/object:Gem::Specification"
533
+ hash = build_hash(node)
534
+ hash[:tag] = node.tag
535
+ hash
536
+ else
537
+ raise ArgumentError, "undefined class/module #{node.tag.sub("!ruby/object:", "")}"
538
+ end
539
+
540
+ store_anchor(node.anchor, result)
541
+ end
542
+
543
+ def build_hash(node)
544
+ result = {}
545
+ node.pairs.each do |key_node, value_node|
546
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
547
+ value = build_node(value_node)
548
+
549
+ if ARRAY_FIELDS.include?(key)
550
+ value = normalize_array_field(value)
551
+ end
552
+
553
+ result[key] = value
554
+ end
555
+ result
556
+ end
557
+
558
+ def build_version(node)
559
+ hash = pairs_to_hash(node)
560
+ Gem::Version.new((hash["version"] || hash["value"]).to_s)
561
+ end
562
+
563
+ PLATFORM_FIELDS = %w[cpu os version].freeze
564
+ PLATFORM_ALLOWED_IVARS = %w[cpu os version value].freeze
565
+
566
+ def build_platform(node)
567
+ hash = pairs_to_hash(node)
568
+ if (hash.keys & PLATFORM_FIELDS).any?
569
+ Gem::Platform.new([hash["cpu"], hash["os"], hash["version"]])
570
+ elsif hash["value"].is_a?(Array)
571
+ # Malformed platform (e.g. sequence instead of mapping).
572
+ # Return the raw value so yaml_initialize handles it like Psych does.
573
+ hash["value"]
574
+ else
575
+ plat = Gem::Platform.allocate
576
+ hash.each do |k, v|
577
+ plat.instance_variable_set(:"@#{k}", v) if PLATFORM_ALLOWED_IVARS.include?(k)
578
+ end
579
+ plat
580
+ end
581
+ end
582
+
583
+ def build_requirement(node)
584
+ r = Gem::Requirement.allocate
585
+ hash = pairs_to_hash(node)
586
+ reqs = hash["requirements"] || hash["value"]
587
+
588
+ if reqs.is_a?(Array) && !reqs.empty?
589
+ safe_reqs = []
590
+ reqs.each do |item|
591
+ if item.is_a?(Array) && item.size == 2
592
+ op = item[0].to_s
593
+ ver = item[1]
594
+ if VALID_OPS.include?(op)
595
+ version_obj = ver.is_a?(Gem::Version) ? ver : Gem::Version.new(ver.to_s)
596
+ safe_reqs << [op, version_obj]
597
+ end
598
+ elsif item.is_a?(String)
599
+ parsed = Gem::Requirement.parse(item)
600
+ safe_reqs << parsed
601
+ end
602
+ rescue Gem::Requirement::BadRequirementError, Gem::Version::BadVersionError
603
+ # Skip malformed items silently
604
+ end
605
+ reqs = safe_reqs unless safe_reqs.empty?
606
+ end
607
+
608
+ r.instance_variable_set(:@requirements, reqs)
609
+ r
610
+ end
611
+
612
+ def build_dependency(node)
613
+ hash = pairs_to_hash(node)
614
+ d = Gem::Dependency.allocate
615
+ d.instance_variable_set(:@name, hash["name"])
616
+
617
+ d.instance_variable_set(:@requirement, hash["requirement"] || hash["version_requirements"])
618
+
619
+ raw_type = hash["type"]
620
+ if raw_type
621
+ name = raw_type.to_s.sub(/^:/, "")
622
+ validate_symbol!(name)
623
+ type = name.to_sym
624
+ else
625
+ type = :runtime
626
+ end
627
+ d.instance_variable_set(:@type, type)
628
+
629
+ d.instance_variable_set(:@prerelease, ["true", true].include?(hash["prerelease"]))
630
+ d.instance_variable_set(:@version_requirements, d.instance_variable_get(:@requirement))
631
+ d
632
+ end
633
+
634
+ def build_specification(hash)
635
+ spec = Gem::Specification.allocate
636
+
637
+ normalize_specification_version!(hash)
638
+ normalize_array_fields!(hash)
639
+
640
+ spec.yaml_initialize("!ruby/object:Gem::Specification", hash)
641
+ spec
642
+ end
643
+
644
+ def pairs_to_hash(node)
645
+ result = {}
646
+ node.pairs.each do |key_node, value_node|
647
+ key = key_node.is_a?(Scalar) ? key_node.value.to_s : build_node(key_node).to_s
648
+ result[key] = build_node(value_node)
649
+ end
650
+ result
651
+ end
652
+
653
+ def validate_tag!(tag)
654
+ return if @permitted_classes.include?(tag)
655
+ raise_disallowed_class!(tag)
656
+ end
657
+
658
+ def raise_disallowed_class!(tag)
659
+ if defined?(Psych::VERSION)
660
+ raise Psych::DisallowedClass.new("load", tag)
661
+ else
662
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{tag}"
663
+ end
664
+ end
665
+
666
+ def validate_symbol!(name)
667
+ return if @permitted_symbols.empty? || @permitted_symbols.include?(name)
668
+
669
+ label = ":#{name}"
670
+ if defined?(Psych::VERSION)
671
+ raise Psych::DisallowedClass.new("load", label)
672
+ else
673
+ raise Psych::DisallowedClass, "Tried to load unspecified class: #{label}"
674
+ end
675
+ end
676
+
677
+ def normalize_specification_version!(hash)
678
+ val = hash["specification_version"]
679
+ return unless val && !val.is_a?(Integer)
680
+ hash["specification_version"] = val.to_i if val.is_a?(String) && /\A\d+\z/.match?(val)
681
+ end
682
+
683
+ def normalize_array_fields!(hash)
684
+ ARRAY_FIELDS.each do |field|
685
+ hash[field] = normalize_array_field(hash[field]) if hash[field]
686
+ end
687
+ end
688
+
689
+ def normalize_array_field(value)
690
+ if value.is_a?(Hash)
691
+ value.values.flatten.compact
692
+ elsif !value.is_a?(Array) && value
693
+ [value].flatten.compact
694
+ else
695
+ value
696
+ end
697
+ end
698
+ end
699
+
700
+ class Emitter
701
+ STRING_ESCAPES = {
702
+ "\\" => "\\\\",
703
+ "\"" => "\\\"",
704
+ "\0" => "\\0",
705
+ "\a" => "\\a",
706
+ "\b" => "\\b",
707
+ "\t" => "\\t",
708
+ "\n" => "\\n",
709
+ "\v" => "\\v",
710
+ "\f" => "\\f",
711
+ "\r" => "\\r",
712
+ "\e" => "\\e",
713
+ }.freeze
714
+
715
+ def emit(obj)
716
+ "---#{emit_node(obj, 0)}"
717
+ end
718
+
719
+ private
720
+
721
+ def emit_node(obj, indent, quote: false)
722
+ case obj
723
+ when Gem::Specification then emit_specification(obj, indent)
724
+ when Gem::Version then emit_version(obj, indent)
725
+ when Gem::Platform then emit_platform(obj, indent)
726
+ when Gem::Requirement then emit_requirement(obj, indent)
727
+ when Gem::Dependency then emit_dependency(obj, indent)
728
+ when Hash then emit_hash(obj, indent)
729
+ when Array then emit_array(obj, indent)
730
+ when Time then emit_time(obj)
731
+ when String then emit_string(obj, indent, quote: quote)
732
+ when NilClass
733
+ "\n"
734
+ when Numeric, Symbol, TrueClass, FalseClass
735
+ " #{obj.inspect}\n"
736
+ else
737
+ " #{quote_string(obj.to_s)}\n"
738
+ end
739
+ end
740
+
741
+ def emit_specification(spec, indent)
742
+ parts = [" !ruby/object:Gem::Specification\n"]
743
+ parts << "#{pad(indent)}name:#{emit_node(spec.name, indent + 2)}"
744
+ parts << "#{pad(indent)}version:#{emit_node(spec.version, indent + 2)}"
745
+ parts << "#{pad(indent)}platform: #{spec.platform}\n"
746
+ if spec.platform.to_s != spec.original_platform.to_s
747
+ parts << "#{pad(indent)}original_platform: #{spec.original_platform}\n"
748
+ end
749
+
750
+ attributes = Gem::Specification.attribute_names.map(&:to_s).sort - %w[name version platform]
751
+ attributes.each do |name|
752
+ val = spec.instance_variable_get("@#{name}")
753
+ next if val.nil?
754
+ parts << "#{pad(indent)}#{name}:#{emit_node(val, indent + 2)}"
755
+ end
756
+
757
+ res = parts.join
758
+ res << "\n" unless res.end_with?("\n")
759
+ res
760
+ end
761
+
762
+ def emit_version(ver, indent)
763
+ " !ruby/object:Gem::Version\n" \
764
+ "#{pad(indent)}version: #{emit_node(ver.version.to_s, indent + 2).lstrip}"
765
+ end
766
+
767
+ def emit_platform(plat, indent)
768
+ " !ruby/object:Gem::Platform\n" \
769
+ "#{pad(indent)}cpu:#{emit_node(plat.cpu, indent + 2)}" \
770
+ "#{pad(indent)}os:#{emit_node(plat.os, indent + 2)}" \
771
+ "#{pad(indent)}version:#{emit_node(plat.version, indent + 2)}"
772
+ end
773
+
774
+ def emit_requirement(req, indent)
775
+ " !ruby/object:Gem::Requirement\n" \
776
+ "#{pad(indent)}requirements:#{emit_node(req.requirements, indent + 2)}"
777
+ end
778
+
779
+ def emit_dependency(dep, indent)
780
+ [
781
+ " !ruby/object:Gem::Dependency\n",
782
+ "#{pad(indent)}name: #{emit_node(dep.name, indent + 2).lstrip}",
783
+ "#{pad(indent)}requirement:#{emit_node(dep.requirement, indent + 2)}",
784
+ "#{pad(indent)}type: #{emit_node(dep.type, indent + 2).lstrip}",
785
+ "#{pad(indent)}prerelease: #{emit_node(dep.prerelease?, indent + 2).lstrip}",
786
+ "#{pad(indent)}version_requirements:#{emit_node(dep.requirement, indent + 2)}",
787
+ ].join
788
+ end
789
+
790
+ def emit_hash(hash, indent)
791
+ if hash.empty?
792
+ " {}\n"
793
+ else
794
+ parts = ["\n"]
795
+ hash.each do |k, v|
796
+ is_symbol = k.is_a?(Symbol) || (k.is_a?(String) && k.start_with?(":"))
797
+ key_str = k.is_a?(Symbol) ? k.inspect : k.to_s
798
+ key_str = quote_string(key_str) if !is_symbol && needs_quoting?(key_str)
799
+ parts << "#{pad(indent)}#{key_str}:#{emit_node(v, indent + 2, quote: is_symbol)}"
800
+ end
801
+ parts.join
802
+ end
803
+ end
804
+
805
+ def emit_array(arr, indent)
806
+ if arr.empty?
807
+ " []\n"
808
+ else
809
+ parts = ["\n"]
810
+ arr.each do |v|
811
+ parts << "#{pad(indent)}-#{emit_node(v, indent + 2)}"
812
+ end
813
+ parts.join
814
+ end
815
+ end
816
+
817
+ def emit_time(time)
818
+ " #{time.utc.strftime("%Y-%m-%d %H:%M:%S.%N Z")}\n"
819
+ end
820
+
821
+ def emit_string(str, indent, quote: false)
822
+ # A CR cannot be represented in a block scalar because parsing
823
+ # normalizes CRLF line breaks to LF, so quote and escape instead.
824
+ if str.include?("\n") && !str.include?("\r")
825
+ emit_block_scalar(str, indent)
826
+ elsif needs_quoting?(str, quote)
827
+ " #{quote_string(str)}\n"
828
+ else
829
+ " #{str}\n"
830
+ end
831
+ end
832
+
833
+ def emit_block_scalar(str, indent)
834
+ parts = [str.end_with?("\n") ? " |\n" : " |-\n"]
835
+ str.each_line do |line|
836
+ parts << "#{pad(indent + 2)}#{line}"
837
+ end
838
+ res = parts.join
839
+ res << "\n" unless res.end_with?("\n")
840
+ res
841
+ end
842
+
843
+ def needs_quoting?(str, quote = false)
844
+ quote || str.empty? || str != str.strip || str =~ /[[:cntrl:]]/ ||
845
+ str =~ /^[!*&:@%$"'|`]/ || str =~ /^-?\d+(\.\d+)?$/ || str =~ /^[<>=-]/ ||
846
+ str == "true" || str == "false" || str == "nil" || str == "null" || str == "~" ||
847
+ str.include?(":") || str.include?("#") || str.include?("[") || str.include?("]") ||
848
+ str.include?("{") || str.include?("}") || str.include?(",")
849
+ end
850
+
851
+ def quote_string(str)
852
+ %("#{str.gsub(/[\\"]|[[:cntrl:]]/) {|c| STRING_ESCAPES[c] || format("\\x%02X", c.ord) }}")
853
+ end
854
+
855
+ def pad(indent)
856
+ " " * indent
857
+ end
858
+ end
859
+
860
+ module_function
861
+
862
+ def dump(obj)
863
+ Emitter.new.emit(obj)
864
+ end
865
+
866
+ def load(str, permitted_classes: [], permitted_symbols: [], aliases: true)
867
+ raise TypeError, "no implicit conversion of nil into String" if str.nil?
868
+ return nil if str.empty?
869
+
870
+ ast = Parser.new(str).parse
871
+ return nil if ast.nil?
872
+
873
+ Builder.new(
874
+ permitted_classes: permitted_classes,
875
+ permitted_symbols: permitted_symbols,
876
+ aliases: aliases
877
+ ).build(ast)
878
+ end
879
+ end
880
+ end