bootsnap 1.6.0 → 1.15.0

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