bootsnap 1.9.1 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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}")