bootsnap 1.6.0 → 1.18.3

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::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)
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,224 @@ 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
+ loader = ::Psych::ClassLoader::Restricted.new(["Symbol"], [])
133
+ scanner = ::Psych::ScalarScanner.new(loader)
134
+
135
+ NoTagsVisitor.new(scanner, loader).visit(ast)
85
136
  end
86
137
  end
87
138
 
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?
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)
94
146
  end
147
+ obj
148
+ end
95
149
 
96
- begin
97
- ::Bootsnap::CompileCache::Native.fetch(
98
- Bootsnap::CompileCache::YAML.cache_dir,
99
- path,
100
- ::Bootsnap::CompileCache::YAML,
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
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 || {}))
178
+ end
179
+ end
180
+
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
199
+ end
200
+
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 || {}))
218
+ end
219
+ end
220
+
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
231
+
232
+ CompileCache::Native.fetch(
233
+ CompileCache::YAML.cache_dir,
234
+ File.realpath(path),
235
+ CompileCache::YAML::Psych4::SafeLoad,
236
+ kwargs,
237
+ )
238
+ end
239
+
240
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
241
+
242
+ def unsafe_load_file(path, *args)
243
+ return super unless CompileCache::YAML.supported_internal_encoding?
244
+
245
+ return super if args.size > 1
246
+
247
+ if (kwargs = args.first)
248
+ return super unless kwargs.is_a?(Hash)
249
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
250
+ end
251
+
252
+ CompileCache::Native.fetch(
253
+ CompileCache::YAML.cache_dir,
254
+ File.realpath(path),
255
+ CompileCache::YAML::Psych4::UnsafeLoad,
101
256
  kwargs,
102
257
  )
103
- rescue Errno::EACCES
104
- ::Bootsnap::CompileCache.permission_error(path)
105
258
  end
259
+
260
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
261
+ end
262
+ end
263
+
264
+ module Psych3
265
+ extend self
266
+
267
+ def input_to_storage(contents, _)
268
+ obj = ::YAML.load(contents)
269
+ packer = CompileCache::YAML.msgpack_factory.packer
270
+ packer.pack(false) # not safe loaded
271
+ begin
272
+ packer.pack(obj)
273
+ rescue NoMethodError, RangeError
274
+ return UNCOMPILABLE # The object included things that we can't serialize
275
+ end
276
+ packer.to_s
277
+ end
278
+
279
+ def storage_to_output(data, kwargs)
280
+ if kwargs&.key?(:symbolize_names)
281
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
282
+ end
283
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
284
+ unpacker.feed(data)
285
+ _safe_loaded = unpacker.unpack
286
+ unpacker.unpack
287
+ end
288
+
289
+ def input_to_output(data, kwargs)
290
+ ::YAML.load(data, **(kwargs || {}))
106
291
  end
107
292
 
108
- ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
293
+ module Patch
294
+ def load_file(path, *args)
295
+ return super unless CompileCache::YAML.supported_internal_encoding?
296
+
297
+ return super if args.size > 1
298
+
299
+ if (kwargs = args.first)
300
+ return super unless kwargs.is_a?(Hash)
301
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
302
+ end
303
+
304
+ CompileCache::Native.fetch(
305
+ CompileCache::YAML.cache_dir,
306
+ File.realpath(path),
307
+ CompileCache::YAML::Psych3,
308
+ kwargs,
309
+ )
310
+ end
311
+
312
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
313
+
314
+ def unsafe_load_file(path, *args)
315
+ return super unless CompileCache::YAML.supported_internal_encoding?
316
+
317
+ return super if args.size > 1
318
+
319
+ if (kwargs = args.first)
320
+ return super unless kwargs.is_a?(Hash)
321
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
322
+ end
323
+
324
+ CompileCache::Native.fetch(
325
+ CompileCache::YAML.cache_dir,
326
+ File.realpath(path),
327
+ CompileCache::YAML::Psych3,
328
+ kwargs,
329
+ )
330
+ end
331
+
332
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
333
+ end
109
334
  end
110
335
  end
111
336
  end
@@ -1,13 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Bootsnap
3
4
  module CompileCache
4
- Error = Class.new(StandardError)
5
- PermissionError = Class.new(Error)
5
+ UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
9
+
10
+ Error = Class.new(StandardError)
6
11
 
7
- def self.setup(cache_dir:, iseq:, yaml:)
12
+ def self.setup(cache_dir:, iseq:, yaml:, json:, readonly: false, revalidation: false)
8
13
  if iseq
9
14
  if supported?
10
- require_relative('compile_cache/iseq')
15
+ require_relative "compile_cache/iseq"
11
16
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
12
17
  elsif $VERBOSE
13
18
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -16,28 +21,32 @@ module Bootsnap
16
21
 
17
22
  if yaml
18
23
  if supported?
19
- require_relative('compile_cache/yaml')
24
+ require_relative "compile_cache/yaml"
20
25
  Bootsnap::CompileCache::YAML.install!(cache_dir)
21
26
  elsif $VERBOSE
22
27
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
23
28
  end
24
29
  end
25
- end
26
30
 
27
- def self.permission_error(path)
28
- cpath = Bootsnap::CompileCache::ISeq.cache_dir
29
- raise(
30
- PermissionError,
31
- "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
32
- "(or, less likely, doesn't have permission to read '#{path}')",
33
- )
31
+ if json
32
+ if supported?
33
+ require_relative "compile_cache/json"
34
+ Bootsnap::CompileCache::JSON.install!(cache_dir)
35
+ elsif $VERBOSE
36
+ warn("[bootsnap/setup] JSON parsing caching is not supported on this implementation of Ruby")
37
+ end
38
+ end
39
+
40
+ if supported? && defined?(Bootsnap::CompileCache::Native)
41
+ Bootsnap::CompileCache::Native.readonly = readonly
42
+ Bootsnap::CompileCache::Native.revalidation = revalidation
43
+ end
34
44
  end
35
45
 
36
46
  def self.supported?
37
- # 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")
47
+ # only enable on 'ruby' (MRI) and TruffleRuby for POSIX (darwin, linux, *bsd), Windows (RubyInstaller2)
48
+ %w[ruby truffleruby].include?(RUBY_ENGINE) &&
49
+ RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
41
50
  end
42
51
  end
43
52
  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}")