bootsnap 1.6.0 → 1.18.3

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,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}")