bootsnap 1.4.1 → 1.10.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +189 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +67 -18
  5. data/exe/bootsnap +5 -0
  6. data/ext/bootsnap/bootsnap.c +319 -119
  7. data/ext/bootsnap/extconf.rb +22 -14
  8. data/lib/bootsnap/bundler.rb +2 -0
  9. data/lib/bootsnap/cli/worker_pool.rb +136 -0
  10. data/lib/bootsnap/cli.rb +281 -0
  11. data/lib/bootsnap/compile_cache/iseq.rb +65 -18
  12. data/lib/bootsnap/compile_cache/json.rb +88 -0
  13. data/lib/bootsnap/compile_cache/yaml.rb +332 -39
  14. data/lib/bootsnap/compile_cache.rb +35 -7
  15. data/lib/bootsnap/explicit_require.rb +5 -3
  16. data/lib/bootsnap/load_path_cache/cache.rb +83 -32
  17. data/lib/bootsnap/load_path_cache/change_observer.rb +6 -1
  18. data/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb +39 -47
  19. data/lib/bootsnap/load_path_cache/core_ext/loaded_features.rb +12 -0
  20. data/lib/bootsnap/load_path_cache/loaded_features_index.rb +69 -26
  21. data/lib/bootsnap/load_path_cache/path.rb +8 -5
  22. data/lib/bootsnap/load_path_cache/path_scanner.rb +56 -29
  23. data/lib/bootsnap/load_path_cache/realpath_cache.rb +6 -5
  24. data/lib/bootsnap/load_path_cache/store.rb +49 -18
  25. data/lib/bootsnap/load_path_cache.rb +20 -32
  26. data/lib/bootsnap/setup.rb +3 -33
  27. data/lib/bootsnap/version.rb +3 -1
  28. data/lib/bootsnap.rb +126 -36
  29. metadata +15 -97
  30. data/.gitignore +0 -17
  31. data/.rubocop.yml +0 -20
  32. data/.travis.yml +0 -24
  33. data/CODE_OF_CONDUCT.md +0 -74
  34. data/CONTRIBUTING.md +0 -21
  35. data/Gemfile +0 -8
  36. data/README.jp.md +0 -231
  37. data/Rakefile +0 -12
  38. data/bin/ci +0 -10
  39. data/bin/console +0 -14
  40. data/bin/setup +0 -8
  41. data/bin/test-minimal-support +0 -7
  42. data/bin/testunit +0 -8
  43. data/bootsnap.gemspec +0 -45
  44. data/dev.yml +0 -10
  45. data/lib/bootsnap/load_path_cache/core_ext/active_support.rb +0 -100
  46. data/shipit.rubygems.yml +0 -0
@@ -1,56 +1,349 @@
1
- require('bootsnap/bootsnap')
1
+ # frozen_string_literal: true
2
+
3
+ require("bootsnap/bootsnap")
2
4
 
3
5
  module Bootsnap
4
6
  module CompileCache
5
7
  module YAML
8
+ Uncompilable = Class.new(StandardError)
9
+ UnsupportedTags = Class.new(Uncompilable)
10
+
11
+ SUPPORTED_INTERNAL_ENCODINGS = [
12
+ nil, # UTF-8
13
+ Encoding::UTF_8,
14
+ Encoding::ASCII,
15
+ Encoding::BINARY,
16
+ ].freeze
17
+
6
18
  class << self
7
- attr_accessor(:msgpack_factory)
8
- end
19
+ attr_accessor(:msgpack_factory, :supported_options)
20
+ attr_reader(:implementation, :cache_dir)
9
21
 
10
- def self.input_to_storage(contents, _)
11
- raise(Uncompilable) if contents.index("!ruby/object")
12
- obj = ::YAML.load(contents)
13
- msgpack_factory.packer.write(obj).to_s
14
- rescue NoMethodError, RangeError
15
- # if the object included things that we can't serialize, fall back to
16
- # Marshal. It's a bit slower, but can encode anything yaml can.
17
- # NoMethodError is unexpected types; RangeError is Bignums
18
- Marshal.dump(obj)
19
- end
22
+ def cache_dir=(cache_dir)
23
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml"
24
+ end
25
+
26
+ def precompile(path)
27
+ return false unless CompileCache::YAML.supported_internal_encoding?
28
+
29
+ CompileCache::Native.precompile(
30
+ cache_dir,
31
+ path.to_s,
32
+ @implementation,
33
+ )
34
+ end
35
+
36
+ def install!(cache_dir)
37
+ self.cache_dir = cache_dir
38
+ init!
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
55
+ end
56
+
57
+ def init!
58
+ require("yaml")
59
+ require("msgpack")
60
+ require("date")
20
61
 
21
- def self.storage_to_output(data)
22
- # This could have a meaning in messagepack, and we're being a little lazy
23
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
24
- # is a positive integer, which is rare, to say the least.
25
- if data[0] == 0x04.chr && data[1] == 0x08.chr
26
- Marshal.load(data)
27
- else
28
- msgpack_factory.unpacker.feed(data).read
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
+ # MessagePack serializes symbols as strings by default.
68
+ # We want them to roundtrip cleanly, so we use a custom factory.
69
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
70
+ factory = MessagePack::Factory.new
71
+ factory.register_type(
72
+ 0x00,
73
+ Symbol,
74
+ packer: :to_msgpack_ext,
75
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
76
+ )
77
+
78
+ if defined? MessagePack::Timestamp
79
+ factory.register_type(
80
+ MessagePack::Timestamp::TYPE, # or just -1
81
+ Time,
82
+ packer: MessagePack::Time::Packer,
83
+ unpacker: MessagePack::Time::Unpacker,
84
+ )
85
+
86
+ marshal_fallback = {
87
+ packer: ->(value) { Marshal.dump(value) },
88
+ unpacker: ->(payload) { Marshal.load(payload) },
89
+ }
90
+ {
91
+ Date => 0x01,
92
+ Regexp => 0x02,
93
+ }.each do |type, code|
94
+ factory.register_type(code, type, marshal_fallback)
95
+ end
96
+ end
97
+
98
+ self.msgpack_factory = factory
99
+
100
+ self.supported_options = []
101
+ params = ::YAML.method(:load).parameters
102
+ if params.include?([:key, :symbolize_names])
103
+ supported_options << :symbolize_names
104
+ end
105
+ if params.include?([:key, :freeze])
106
+ if factory.load(factory.dump("yaml"), freeze: true).frozen?
107
+ supported_options << :freeze
108
+ end
109
+ end
110
+ supported_options.freeze
111
+ end
112
+
113
+ def patch
114
+ @implementation::Patch
115
+ end
116
+
117
+ def strict_load(payload)
118
+ ast = ::YAML.parse(payload)
119
+ return ast unless ast
120
+
121
+ strict_visitor.create.visit(ast)
122
+ end
123
+
124
+ def strict_visitor
125
+ self::NoTagsVisitor ||= Class.new(Psych::Visitors::ToRuby) do
126
+ def visit(target)
127
+ if target.tag
128
+ raise UnsupportedTags, "YAML tags are not supported: #{target.tag}"
129
+ end
130
+
131
+ super
132
+ end
133
+ end
29
134
  end
30
135
  end
31
136
 
32
- def self.input_to_output(data)
33
- ::YAML.load(data)
137
+ module Psych4
138
+ extend self
139
+
140
+ def input_to_storage(contents, _)
141
+ obj = SafeLoad.input_to_storage(contents, nil)
142
+ if UNCOMPILABLE.equal?(obj)
143
+ obj = UnsafeLoad.input_to_storage(contents, nil)
144
+ end
145
+ obj
146
+ end
147
+
148
+ module UnsafeLoad
149
+ extend self
150
+
151
+ def input_to_storage(contents, _)
152
+ obj = ::YAML.unsafe_load(contents)
153
+ packer = CompileCache::YAML.msgpack_factory.packer
154
+ packer.pack(false) # not safe loaded
155
+ begin
156
+ packer.pack(obj)
157
+ rescue NoMethodError, RangeError
158
+ return UNCOMPILABLE # The object included things that we can't serialize
159
+ end
160
+ packer.to_s
161
+ end
162
+
163
+ def storage_to_output(data, kwargs)
164
+ if kwargs&.key?(:symbolize_names)
165
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
166
+ end
167
+
168
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
169
+ unpacker.feed(data)
170
+ _safe_loaded = unpacker.unpack
171
+ unpacker.unpack
172
+ end
173
+
174
+ def input_to_output(data, kwargs)
175
+ ::YAML.unsafe_load(data, **(kwargs || {}))
176
+ end
177
+ end
178
+
179
+ module SafeLoad
180
+ extend self
181
+
182
+ def input_to_storage(contents, _)
183
+ obj = begin
184
+ CompileCache::YAML.strict_load(contents)
185
+ rescue Psych::DisallowedClass, Psych::BadAlias, Uncompilable
186
+ return UNCOMPILABLE
187
+ end
188
+
189
+ packer = CompileCache::YAML.msgpack_factory.packer
190
+ packer.pack(true) # safe loaded
191
+ begin
192
+ packer.pack(obj)
193
+ rescue NoMethodError, RangeError
194
+ return UNCOMPILABLE
195
+ end
196
+ packer.to_s
197
+ end
198
+
199
+ def storage_to_output(data, kwargs)
200
+ if kwargs&.key?(:symbolize_names)
201
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
202
+ end
203
+
204
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
205
+ unpacker.feed(data)
206
+ safe_loaded = unpacker.unpack
207
+ if safe_loaded
208
+ unpacker.unpack
209
+ else
210
+ UNCOMPILABLE
211
+ end
212
+ end
213
+
214
+ def input_to_output(data, kwargs)
215
+ ::YAML.load(data, **(kwargs || {}))
216
+ end
217
+ end
218
+
219
+ module Patch
220
+ def load_file(path, *args)
221
+ return super unless CompileCache::YAML.supported_internal_encoding?
222
+
223
+ return super if args.size > 1
224
+
225
+ if (kwargs = args.first)
226
+ return super unless kwargs.is_a?(Hash)
227
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
228
+ end
229
+
230
+ begin
231
+ CompileCache::Native.fetch(
232
+ CompileCache::YAML.cache_dir,
233
+ File.realpath(path),
234
+ CompileCache::YAML::Psych4::SafeLoad,
235
+ kwargs,
236
+ )
237
+ rescue Errno::EACCES
238
+ CompileCache.permission_error(path)
239
+ end
240
+ end
241
+
242
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
243
+
244
+ def unsafe_load_file(path, *args)
245
+ return super unless CompileCache::YAML.supported_internal_encoding?
246
+
247
+ return super if args.size > 1
248
+
249
+ if (kwargs = args.first)
250
+ return super unless kwargs.is_a?(Hash)
251
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
252
+ end
253
+
254
+ begin
255
+ CompileCache::Native.fetch(
256
+ CompileCache::YAML.cache_dir,
257
+ File.realpath(path),
258
+ CompileCache::YAML::Psych4::UnsafeLoad,
259
+ kwargs,
260
+ )
261
+ rescue Errno::EACCES
262
+ CompileCache.permission_error(path)
263
+ end
264
+ end
265
+
266
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
267
+ end
34
268
  end
35
269
 
36
- def self.install!(cache_dir)
37
- require('yaml')
38
- require('msgpack')
270
+ module Psych3
271
+ extend self
39
272
 
40
- # MessagePack serializes symbols as strings by default.
41
- # We want them to roundtrip cleanly, so we use a custom factory.
42
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
43
- factory = MessagePack::Factory.new
44
- factory.register_type(0x00, Symbol)
45
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
273
+ def input_to_storage(contents, _)
274
+ obj = ::YAML.load(contents)
275
+ packer = CompileCache::YAML.msgpack_factory.packer
276
+ packer.pack(false) # not safe loaded
277
+ begin
278
+ packer.pack(obj)
279
+ rescue NoMethodError, RangeError
280
+ return UNCOMPILABLE # The object included things that we can't serialize
281
+ end
282
+ packer.to_s
283
+ end
46
284
 
47
- klass = class << ::YAML; self; end
48
- klass.send(:define_method, :load_file) do |path|
49
- Bootsnap::CompileCache::Native.fetch(
50
- cache_dir,
51
- path,
52
- Bootsnap::CompileCache::YAML
53
- )
285
+ def storage_to_output(data, kwargs)
286
+ if kwargs&.key?(:symbolize_names)
287
+ kwargs[:symbolize_keys] = kwargs.delete(:symbolize_names)
288
+ end
289
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
290
+ unpacker.feed(data)
291
+ _safe_loaded = unpacker.unpack
292
+ unpacker.unpack
293
+ end
294
+
295
+ def input_to_output(data, kwargs)
296
+ ::YAML.load(data, **(kwargs || {}))
297
+ end
298
+
299
+ module Patch
300
+ def load_file(path, *args)
301
+ return super unless CompileCache::YAML.supported_internal_encoding?
302
+
303
+ return super if args.size > 1
304
+
305
+ if (kwargs = args.first)
306
+ return super unless kwargs.is_a?(Hash)
307
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
308
+ end
309
+
310
+ begin
311
+ CompileCache::Native.fetch(
312
+ CompileCache::YAML.cache_dir,
313
+ File.realpath(path),
314
+ CompileCache::YAML::Psych3,
315
+ kwargs,
316
+ )
317
+ rescue Errno::EACCES
318
+ CompileCache.permission_error(path)
319
+ end
320
+ end
321
+
322
+ ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
323
+
324
+ def unsafe_load_file(path, *args)
325
+ return super unless CompileCache::YAML.supported_internal_encoding?
326
+
327
+ return super if args.size > 1
328
+
329
+ if (kwargs = args.first)
330
+ return super unless kwargs.is_a?(Hash)
331
+ return super unless (kwargs.keys - CompileCache::YAML.supported_options).empty?
332
+ end
333
+
334
+ begin
335
+ CompileCache::Native.fetch(
336
+ CompileCache::YAML.cache_dir,
337
+ File.realpath(path),
338
+ CompileCache::YAML::Psych3,
339
+ kwargs,
340
+ )
341
+ rescue Errno::EACCES
342
+ CompileCache.permission_error(path)
343
+ end
344
+ end
345
+
346
+ ruby2_keywords :unsafe_load_file if respond_to?(:ruby2_keywords, true)
54
347
  end
55
348
  end
56
349
  end
@@ -1,9 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module CompileCache
3
- def self.setup(cache_dir:, iseq:, yaml:)
5
+ UNCOMPILABLE = BasicObject.new
6
+ def UNCOMPILABLE.inspect
7
+ "<Bootsnap::UNCOMPILABLE>"
8
+ end
9
+
10
+ Error = Class.new(StandardError)
11
+ PermissionError = Class.new(Error)
12
+
13
+ def self.setup(cache_dir:, iseq:, yaml:, json:)
4
14
  if iseq
5
15
  if supported?
6
- require_relative('compile_cache/iseq')
16
+ require_relative("compile_cache/iseq")
7
17
  Bootsnap::CompileCache::ISeq.install!(cache_dir)
8
18
  elsif $VERBOSE
9
19
  warn("[bootsnap/setup] bytecode caching is not supported on this implementation of Ruby")
@@ -12,19 +22,37 @@ module Bootsnap
12
22
 
13
23
  if yaml
14
24
  if supported?
15
- require_relative('compile_cache/yaml')
25
+ require_relative("compile_cache/yaml")
16
26
  Bootsnap::CompileCache::YAML.install!(cache_dir)
17
27
  elsif $VERBOSE
18
28
  warn("[bootsnap/setup] YAML parsing caching is not supported on this implementation of Ruby")
19
29
  end
20
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
+ end
41
+
42
+ def self.permission_error(path)
43
+ cpath = Bootsnap::CompileCache::ISeq.cache_dir
44
+ raise(
45
+ PermissionError,
46
+ "bootsnap doesn't have permission to write cache entries in '#{cpath}' " \
47
+ "(or, less likely, doesn't have permission to read '#{path}')",
48
+ )
21
49
  end
22
50
 
23
51
  def self.supported?
24
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
25
- RUBY_ENGINE == 'ruby' &&
26
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
27
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
52
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 2.3.0
53
+ RUBY_ENGINE == "ruby" &&
54
+ RUBY_PLATFORM =~ /darwin|linux|bsd|mswin|mingw|cygwin/ &&
55
+ Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
28
56
  end
29
57
  end
30
58
  end
@@ -1,8 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Bootsnap
2
4
  module ExplicitRequire
3
- ARCHDIR = RbConfig::CONFIG['archdir']
4
- RUBYLIBDIR = RbConfig::CONFIG['rubylibdir']
5
- DLEXT = RbConfig::CONFIG['DLEXT']
5
+ ARCHDIR = RbConfig::CONFIG["archdir"]
6
+ RUBYLIBDIR = RbConfig::CONFIG["rubylibdir"]
7
+ DLEXT = RbConfig::CONFIG["DLEXT"]
6
8
 
7
9
  def self.from_self(feature)
8
10
  require_relative("../#{feature}")
@@ -1,4 +1,6 @@
1
- require_relative('../explicit_require')
1
+ # frozen_string_literal: true
2
+
3
+ require_relative("../explicit_require")
2
4
 
3
5
  module Bootsnap
4
6
  module LoadPathCache
@@ -8,8 +10,8 @@ module Bootsnap
8
10
  def initialize(store, path_obj, development_mode: false)
9
11
  @development_mode = development_mode
10
12
  @store = store
11
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
12
- @path_obj = path_obj.map! { |f| File.exist?(f) ? File.realpath(f) : f }
13
+ @mutex = Mutex.new
14
+ @path_obj = path_obj.map! { |f| PathScanner.os_path(File.exist?(f) ? File.realpath(f) : f.dup) }
13
15
  @has_relative_paths = nil
14
16
  reinitialize
15
17
  end
@@ -26,15 +28,16 @@ module Bootsnap
26
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
27
29
  # Builtin features are of the form 'enumerator.so'.
28
30
  # All others include paths.
29
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
30
32
 
31
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
32
34
  ext = File.extname(feat) # .so
33
35
 
34
36
  features[feat] = nil # enumerator.so
35
37
  features[base] = nil # enumerator
36
38
 
37
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
38
41
  DL_EXTENSIONS.each do |dl_ext|
39
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
40
43
  end
@@ -42,21 +45,27 @@ module Bootsnap
42
45
 
43
46
  # Try to resolve this feature to an absolute path without traversing the
44
47
  # loadpath.
45
- def find(feature)
48
+ def find(feature, try_extensions: true)
46
49
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
47
- feature = feature.to_s
48
- return feature if absolute_path?(feature)
49
- return File.expand_path(feature) if feature.start_with?('./')
50
+ feature = feature.to_s.freeze
51
+
52
+ return feature if Bootsnap.absolute_path?(feature)
53
+
54
+ if feature.start_with?("./", "../")
55
+ return try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
56
+ end
57
+
50
58
  @mutex.synchronize do
51
- x = search_index(feature)
59
+ x = search_index(feature, try_extensions: try_extensions)
52
60
  return x if x
61
+ return unless try_extensions
53
62
 
54
63
  # Ruby has some built-in features that require lies about.
55
64
  # For example, 'enumerator' is built in. If you require it, ruby
56
65
  # returns false as if it were already loaded; however, there is no
57
66
  # file to find on disk. We've pre-built a list of these, and we
58
67
  # return false if any of them is loaded.
59
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
60
69
 
61
70
  # The feature wasn't found on our preliminary search through the index.
62
71
  # We resolve this differently depending on what the extension was.
@@ -65,13 +74,14 @@ module Bootsnap
65
74
  # native dynamic extension, e.g. .bundle or .so), we know it was a
66
75
  # failure and there's nothing more we can do to find the file.
67
76
  # no extension, .rb, (.bundle or .so)
68
- when '', *CACHED_EXTENSIONS # rubocop:disable Performance/CaseWhenSplat
77
+ when "", *CACHED_EXTENSIONS
69
78
  nil
70
79
  # Ruby allows specifying native extensions as '.so' even when DLEXT
71
80
  # is '.bundle'. This is where we handle that case.
72
81
  when DOT_SO
73
82
  x = search_index(feature[0..-4] + DLEXT)
74
83
  return x if x
84
+
75
85
  if DLEXT2
76
86
  x = search_index(feature[0..-4] + DLEXT2)
77
87
  return x if x
@@ -79,7 +89,7 @@ module Bootsnap
79
89
  else
80
90
  # other, unknown extension. For example, `.rake`. Since we haven't
81
91
  # cached these, we legitimately need to run the load path search.
82
- raise(LoadPathCache::FallbackScan, '', [])
92
+ return FALLBACK_SCAN
83
93
  end
84
94
  end
85
95
 
@@ -87,26 +97,18 @@ module Bootsnap
87
97
  # cases where the file doesn't appear to be on the load path. We should
88
98
  # be able to detect newly-created files without rebooting the
89
99
  # application.
90
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
91
- end
92
-
93
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
94
- def absolute_path?(path)
95
- path[1] == ':'
96
- end
97
- else
98
- def absolute_path?(path)
99
- path.start_with?(SLASH)
100
- end
100
+ return FALLBACK_SCAN if @development_mode
101
101
  end
102
102
 
103
103
  def unshift_paths(sender, *paths)
104
104
  return unless sender == @path_obj
105
+
105
106
  @mutex.synchronize { unshift_paths_locked(*paths) }
106
107
  end
107
108
 
108
109
  def push_paths(sender, *paths)
109
110
  return unless sender == @path_obj
111
+
110
112
  @mutex.synchronize { push_paths_locked(*paths) }
111
113
  end
112
114
 
@@ -139,10 +141,11 @@ module Bootsnap
139
141
  p = Path.new(path)
140
142
  @has_relative_paths = true if p.relative?
141
143
  next if p.non_directory?
144
+
142
145
  expanded_path = p.expanded_path
143
146
  entries, dirs = p.entries_and_dirs(@store)
144
147
  # push -> low precedence -> set only if unset
145
- dirs.each { |dir| @dirs[dir] ||= path }
148
+ dirs.each { |dir| @dirs[dir] ||= path }
146
149
  entries.each { |rel| @index[rel] ||= expanded_path }
147
150
  end
148
151
  end
@@ -153,6 +156,7 @@ module Bootsnap
153
156
  paths.map(&:to_s).reverse_each do |path|
154
157
  p = Path.new(path)
155
158
  next if p.non_directory?
159
+
156
160
  expanded_path = p.expanded_path
157
161
  entries, dirs = p.entries_and_dirs(@store)
158
162
  # unshift -> high precedence -> unconditional set
@@ -162,6 +166,10 @@ module Bootsnap
162
166
  end
163
167
  end
164
168
 
169
+ def expand_path(feature)
170
+ maybe_append_extension(File.expand_path(feature))
171
+ end
172
+
165
173
  def stale?
166
174
  @development_mode && @generated_at + AGE_THRESHOLD < now
167
175
  end
@@ -171,19 +179,62 @@ module Bootsnap
171
179
  end
172
180
 
173
181
  if DLEXT2
174
- def search_index(f)
175
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
182
+ def search_index(feature, try_extensions: true)
183
+ if try_extensions
184
+ try_index(feature + DOT_RB) ||
185
+ try_index(feature + DLEXT) ||
186
+ try_index(feature + DLEXT2) ||
187
+ try_index(feature)
188
+ else
189
+ try_index(feature)
190
+ end
191
+ end
192
+
193
+ def maybe_append_extension(feature)
194
+ try_ext(feature + DOT_RB) ||
195
+ try_ext(feature + DLEXT) ||
196
+ try_ext(feature + DLEXT2) ||
197
+ feature
176
198
  end
177
199
  else
178
- def search_index(f)
179
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
200
+ def search_index(feature, try_extensions: true)
201
+ if try_extensions
202
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
203
+ else
204
+ try_index(feature)
205
+ end
206
+ end
207
+
208
+ def maybe_append_extension(feature)
209
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
180
210
  end
181
211
  end
182
212
 
183
- def try_index(f)
184
- if (p = @index[f])
185
- p + '/' + f
213
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
214
+ if s.respond_to?(:-@)
215
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
216
+ def try_index(feature)
217
+ if (path = @index[feature])
218
+ -File.join(path, feature).freeze
219
+ end
220
+ end
221
+ else
222
+ def try_index(feature)
223
+ if (path = @index[feature])
224
+ -File.join(path, feature).untaint
225
+ end
226
+ end
186
227
  end
228
+ else
229
+ def try_index(feature)
230
+ if (path = @index[feature])
231
+ File.join(path, feature)
232
+ end
233
+ end
234
+ end
235
+
236
+ def try_ext(feature)
237
+ feature if File.exist?(feature)
187
238
  end
188
239
  end
189
240
  end