bootsnap 1.9.1 → 1.16.0

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.
@@ -1,80 +1,99 @@
1
1
  # frozen_string_literal: true
2
- require('bootsnap/bootsnap')
2
+
3
+ require("bootsnap/bootsnap")
3
4
 
4
5
  module Bootsnap
5
6
  module CompileCache
6
7
  module YAML
7
- class << self
8
- attr_accessor(:msgpack_factory, :cache_dir, :supported_options)
8
+ Uncompilable = Class.new(StandardError)
9
+ UnsupportedTags = Class.new(Uncompilable)
9
10
 
10
- def input_to_storage(contents, _)
11
- obj = strict_load(contents)
12
- msgpack_factory.dump(obj)
13
- rescue NoMethodError, RangeError
14
- # The object included things that we can't serialize
15
- raise(Uncompilable)
16
- end
11
+ SUPPORTED_INTERNAL_ENCODINGS = [
12
+ nil, # UTF-8
13
+ Encoding::UTF_8,
14
+ Encoding::ASCII,
15
+ Encoding::BINARY,
16
+ ].freeze
17
17
 
18
- def storage_to_output(data, kwargs)
19
- if kwargs && kwargs.key?(:symbolize_names)
20
- kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
21
- end
22
- msgpack_factory.load(data, kwargs)
23
- end
18
+ class << self
19
+ attr_accessor(:msgpack_factory, :supported_options)
20
+ attr_reader(:implementation, :cache_dir)
24
21
 
25
- def input_to_output(data, kwargs)
26
- if ::YAML.respond_to?(:unsafe_load)
27
- ::YAML.unsafe_load(data, **(kwargs || {}))
28
- else
29
- ::YAML.load(data, **(kwargs || {}))
30
- end
22
+ def cache_dir=(cache_dir)
23
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml"
31
24
  end
32
25
 
33
- def strict_load(payload, *args)
34
- ast = ::YAML.parse(payload)
35
- return ast unless ast
36
- strict_visitor.create(*args).visit(ast)
37
- end
38
- ruby2_keywords :strict_load if respond_to?(:ruby2_keywords, true)
26
+ def precompile(path)
27
+ return false unless CompileCache::YAML.supported_internal_encoding?
39
28
 
40
- def precompile(path, cache_dir: YAML.cache_dir)
41
- Bootsnap::CompileCache::Native.precompile(
29
+ CompileCache::Native.precompile(
42
30
  cache_dir,
43
31
  path.to_s,
44
- Bootsnap::CompileCache::YAML,
32
+ @implementation,
45
33
  )
46
34
  end
47
35
 
48
36
  def install!(cache_dir)
49
37
  self.cache_dir = cache_dir
50
38
  init!
51
- ::YAML.singleton_class.prepend(Patch)
39
+ ::YAML.singleton_class.prepend(@implementation::Patch)
40
+ end
41
+
42
+ # Psych coerce strings to `Encoding.default_internal` but Message Pack only support
43
+ # UTF-8, US-ASCII and BINARY. So if Encoding.default_internal is set to anything else
44
+ # we can't safely use the cache
45
+ def supported_internal_encoding?
46
+ SUPPORTED_INTERNAL_ENCODINGS.include?(Encoding.default_internal)
47
+ end
48
+
49
+ module EncodingAwareSymbols
50
+ extend self
51
+
52
+ def unpack(payload)
53
+ (+payload).force_encoding(Encoding::UTF_8).to_sym
54
+ end
52
55
  end
53
56
 
54
57
  def init!
55
- require('yaml')
56
- require('msgpack')
57
- require('date')
58
+ require("yaml")
59
+ require("msgpack")
60
+ require("date")
58
61
 
59
- if Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
60
- Patch.send(:remove_method, :unsafe_load_file)
62
+ @implementation = ::YAML::VERSION >= "4" ? Psych4 : Psych3
63
+ if @implementation::Patch.method_defined?(:unsafe_load_file) && !::YAML.respond_to?(:unsafe_load_file)
64
+ @implementation::Patch.send(:remove_method, :unsafe_load_file)
61
65
  end
62
- if Patch.method_defined?(:load_file) && ::YAML::VERSION >= '4'
63
- Patch.send(:remove_method, :load_file)
66
+
67
+ unless const_defined?(:NoTagsVisitor)
68
+ visitor = Class.new(Psych::Visitors::NoAliasRuby) do
69
+ def visit(target)
70
+ if target.tag
71
+ raise UnsupportedTags, "YAML tags are not supported: #{target.tag}"
72
+ end
73
+
74
+ super
75
+ end
76
+ end
77
+ const_set(:NoTagsVisitor, visitor)
64
78
  end
65
79
 
66
80
  # MessagePack serializes symbols as strings by default.
67
81
  # We want them to roundtrip cleanly, so we use a custom factory.
68
82
  # see: https://github.com/msgpack/msgpack-ruby/pull/122
69
83
  factory = MessagePack::Factory.new
70
- factory.register_type(0x00, Symbol)
84
+ factory.register_type(
85
+ 0x00,
86
+ Symbol,
87
+ packer: :to_msgpack_ext,
88
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
89
+ )
71
90
 
72
91
  if defined? MessagePack::Timestamp
73
92
  factory.register_type(
74
93
  MessagePack::Timestamp::TYPE, # or just -1
75
94
  Time,
76
95
  packer: MessagePack::Time::Packer,
77
- unpacker: MessagePack::Time::Unpacker
96
+ unpacker: MessagePack::Time::Unpacker,
78
97
  )
79
98
 
80
99
  marshal_fallback = {
@@ -94,70 +113,240 @@ module Bootsnap
94
113
  self.supported_options = []
95
114
  params = ::YAML.method(:load).parameters
96
115
  if params.include?([:key, :symbolize_names])
97
- self.supported_options << :symbolize_names
116
+ supported_options << :symbolize_names
98
117
  end
99
- if params.include?([:key, :freeze])
100
- if factory.load(factory.dump('yaml'), freeze: true).frozen?
101
- self.supported_options << :freeze
102
- end
118
+ if params.include?([:key, :freeze]) && factory.load(factory.dump("yaml"), freeze: true).frozen?
119
+ supported_options << :freeze
103
120
  end
104
- self.supported_options.freeze
121
+ supported_options.freeze
105
122
  end
106
123
 
107
- def strict_visitor
108
- self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
109
- def visit(target)
110
- if target.tag
111
- raise Uncompilable, "YAML tags are not supported: #{target.tag}"
112
- end
113
- super
124
+ def patch
125
+ @implementation::Patch
126
+ end
127
+
128
+ def strict_load(payload)
129
+ ast = ::YAML.parse(payload)
130
+ return ast unless ast
131
+
132
+ loader = ::Psych::ClassLoader::Restricted.new(["Symbol"], [])
133
+ scanner = ::Psych::ScalarScanner.new(loader)
134
+
135
+ NoTagsVisitor.new(scanner, loader).visit(ast)
136
+ end
137
+ end
138
+
139
+ module Psych4
140
+ extend self
141
+
142
+ def input_to_storage(contents, _)
143
+ obj = SafeLoad.input_to_storage(contents, nil)
144
+ if UNCOMPILABLE.equal?(obj)
145
+ obj = UnsafeLoad.input_to_storage(contents, nil)
146
+ end
147
+ obj
148
+ end
149
+
150
+ module UnsafeLoad
151
+ extend self
152
+
153
+ def input_to_storage(contents, _)
154
+ obj = ::YAML.unsafe_load(contents)
155
+ packer = CompileCache::YAML.msgpack_factory.packer
156
+ packer.pack(false) # not safe loaded
157
+ begin
158
+ packer.pack(obj)
159
+ rescue NoMethodError, RangeError
160
+ return UNCOMPILABLE # The object included things that we can't serialize
114
161
  end
162
+ packer.to_s
163
+ end
164
+
165
+ def storage_to_output(data, kwargs)
166
+ if kwargs&.key?(:symbolize_names)
167
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
168
+ end
169
+
170
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
171
+ unpacker.feed(data)
172
+ _safe_loaded = unpacker.unpack
173
+ unpacker.unpack
174
+ end
175
+
176
+ def input_to_output(data, kwargs)
177
+ ::YAML.unsafe_load(data, **(kwargs || {}))
115
178
  end
116
179
  end
117
- end
118
180
 
119
- module Patch
120
- def load_file(path, *args)
121
- return super if args.size > 1
122
- if kwargs = args.first
123
- return super unless kwargs.is_a?(Hash)
124
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
181
+ module SafeLoad
182
+ extend self
183
+
184
+ def input_to_storage(contents, _)
185
+ obj = begin
186
+ CompileCache::YAML.strict_load(contents)
187
+ rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable
188
+ return UNCOMPILABLE
189
+ end
190
+
191
+ packer = CompileCache::YAML.msgpack_factory.packer
192
+ packer.pack(true) # safe loaded
193
+ begin
194
+ packer.pack(obj)
195
+ rescue NoMethodError, RangeError
196
+ return UNCOMPILABLE
197
+ end
198
+ packer.to_s
125
199
  end
126
200
 
127
- begin
128
- ::Bootsnap::CompileCache::Native.fetch(
129
- Bootsnap::CompileCache::YAML.cache_dir,
130
- File.realpath(path),
131
- ::Bootsnap::CompileCache::YAML,
132
- kwargs,
133
- )
134
- rescue Errno::EACCES
135
- ::Bootsnap::CompileCache.permission_error(path)
201
+ def storage_to_output(data, kwargs)
202
+ if kwargs&.key?(:symbolize_names)
203
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
204
+ end
205
+
206
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
207
+ unpacker.feed(data)
208
+ safe_loaded = unpacker.unpack
209
+ if safe_loaded
210
+ unpacker.unpack
211
+ else
212
+ UNCOMPILABLE
213
+ end
214
+ end
215
+
216
+ def input_to_output(data, kwargs)
217
+ ::YAML.load(data, **(kwargs || {}))
136
218
  end
137
219
  end
138
220
 
139
- ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
221
+ module Patch
222
+ def load_file(path, *args)
223
+ return super unless CompileCache::YAML.supported_internal_encoding?
224
+
225
+ return super if args.size > 1
226
+
227
+ if (kwargs = args.first)
228
+ return super unless kwargs.is_a?(Hash)
229
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
230
+ end
140
231
 
141
- def unsafe_load_file(path, *args)
142
- return super if args.size > 1
143
- if kwargs = args.first
144
- return super unless kwargs.is_a?(Hash)
145
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
232
+ begin
233
+ CompileCache::Native.fetch(
234
+ CompileCache::YAML.cache_dir,
235
+ File.realpath(path),
236
+ CompileCache::YAML::Psych4::SafeLoad,
237
+ kwargs,
238
+ )
239
+ rescue Errno::EACCES
240
+ CompileCache.permission_error(path)
241
+ end
146
242
  end
147
243
 
244
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
245
+
246
+ def unsafe_load_file(path, *args)
247
+ return super unless CompileCache::YAML.supported_internal_encoding?
248
+
249
+ return super if args.size > 1
250
+
251
+ if (kwargs = args.first)
252
+ return super unless kwargs.is_a?(Hash)
253
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
254
+ end
255
+
256
+ begin
257
+ CompileCache::Native.fetch(
258
+ CompileCache::YAML.cache_dir,
259
+ File.realpath(path),
260
+ CompileCache::YAML::Psych4::UnsafeLoad,
261
+ kwargs,
262
+ )
263
+ rescue Errno::EACCES
264
+ CompileCache.permission_error(path)
265
+ end
266
+ end
267
+
268
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
269
+ end
270
+ end
271
+
272
+ module Psych3
273
+ extend self
274
+
275
+ def input_to_storage(contents, _)
276
+ obj = ::YAML.load(contents)
277
+ packer = CompileCache::YAML.msgpack_factory.packer
278
+ packer.pack(false) # not safe loaded
148
279
  begin
149
- ::Bootsnap::CompileCache::Native.fetch(
150
- Bootsnap::CompileCache::YAML.cache_dir,
151
- File.realpath(path),
152
- ::Bootsnap::CompileCache::YAML,
153
- kwargs,
154
- )
155
- rescue Errno::EACCES
156
- ::Bootsnap::CompileCache.permission_error(path)
280
+ packer.pack(obj)
281
+ rescue NoMethodError, RangeError
282
+ return UNCOMPILABLE # The object included things that we can't serialize
283
+ end
284
+ packer.to_s
285
+ end
286
+
287
+ def storage_to_output(data, kwargs)
288
+ if kwargs&.key?(:symbolize_names)
289
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
157
290
  end
291
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
292
+ unpacker.feed(data)
293
+ _safe_loaded = unpacker.unpack
294
+ unpacker.unpack
158
295
  end
159
296
 
160
- ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
297
+ def input_to_output(data, kwargs)
298
+ ::YAML.load(data, **(kwargs || {}))
299
+ end
300
+
301
+ module Patch
302
+ def load_file(path, *args)
303
+ return super unless CompileCache::YAML.supported_internal_encoding?
304
+
305
+ return super if args.size > 1
306
+
307
+ if (kwargs = args.first)
308
+ return super unless kwargs.is_a?(Hash)
309
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
310
+ end
311
+
312
+ begin
313
+ CompileCache::Native.fetch(
314
+ CompileCache::YAML.cache_dir,
315
+ File.realpath(path),
316
+ CompileCache::YAML::Psych3,
317
+ kwargs,
318
+ )
319
+ rescue Errno::EACCES
320
+ CompileCache.permission_error(path)
321
+ end
322
+ end
323
+
324
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
325
+
326
+ def unsafe_load_file(path, *args)
327
+ return super unless CompileCache::YAML.supported_internal_encoding?
328
+
329
+ return super if args.size > 1
330
+
331
+ if (kwargs = args.first)
332
+ return super unless kwargs.is_a?(Hash)
333
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
334
+ end
335
+
336
+ begin
337
+ CompileCache::Native.fetch(
338
+ CompileCache::YAML.cache_dir,
339
+ File.realpath(path),
340
+ CompileCache::YAML::Psych3,
341
+ kwargs,
342
+ )
343
+ rescue Errno::EACCES
344
+ CompileCache.permission_error(path)
345
+ end
346
+ end
347
+
348
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
349
+ end
161
350
  end
162
351
  end
163
352
  end
@@ -1,13 +1,19 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module CompileCache
4
- Error = Class.new(StandardError)
5
+ UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
9
+
10
+ Error = Class.new(StandardError)
5
11
  PermissionError = Class.new(Error)
6
12
 
7
- def self.setup(cache_dir:, iseq:, yaml:, json:)
13
+ def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false)
8
14
  if iseq
9
15
  if supported?
10
- require_relative('compile_cache/iseq')
16
+ require_relative("compile_cache/iseq")
11
17
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
12
18
  elsif $VERBOSE
13
19
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -16,7 +22,7 @@ module Bootsnap
16
22
 
17
23
  if yaml
18
24
  if supported?
19
- require_relative('compile_cache/yaml')
25
+ require_relative("compile_cache/yaml")
20
26
  Bootsnap::CompileCache::YAML.install!(cache_dir)
21
27
  elsif $VERBOSE
22
28
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
@@ -25,12 +31,16 @@ module Bootsnap
25
31
 
26
32
  if json
27
33
  if supported?
28
- require_relative('compile_cache/json')
34
+ require_relative("compile_cache/json")
29
35
  Bootsnap::CompileCache::JSON.install!(cache_dir)
30
36
  elsif $VERBOSE
31
37
  warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
32
38
  end
33
39
  end
40
+
41
+ if supported? && defined?(Bootsnap::CompileCache::Native)
42
+ Bootsnap::CompileCache::Native.readonly = readonly
43
+ end
34
44
  end
35
45
 
36
46
  def self.permission_error(path)
@@ -44,9 +54,7 @@ module Bootsnap
44
54
 
45
55
  def self.supported?
46
56
  # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
47
- RUBY_ENGINE == 'ruby' &&
48
- RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
49
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
57
+ RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
50
58
  end
51
59
  end
52
60
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module ExplicitRequire
4
- ARCHDIR = RbConfig::CONFIG['archdir']
5
- RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
6
- DLEXT = RbConfig::CONFIG['DLEXT']
5
+ ARCHDIR = RbConfig::CONFIG["archdir"]
6
+ RUBYLIBDIR = RbConfig::CONFIG["rubylibdir"]
7
+ DLEXT = RbConfig::CONFIG["DLEXT"]
7
8
 
8
9
  def self.from_self(feature)
9
10
  require_relative("../#{feature}")