bootsnap 1.4.8 → 1.16.0

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,61 +1,351 @@
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
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
+
7
18
  class << self
8
- attr_accessor(:msgpack_factory)
9
- end
19
+ attr_accessor(:msgpack_factory, :supported_options)
20
+ attr_reader(:implementation, :cache_dir)
10
21
 
11
- def self.input_to_storage(contents, _)
12
- raise(Uncompilable) if contents.index("!ruby/object")
13
- obj = ::YAML.load(contents)
14
- msgpack_factory.packer.write(obj).to_s
15
- rescue NoMethodError, RangeError
16
- # if the object included things that we can't serialize, fall back to
17
- # Marshal. It's a bit slower, but can encode anything yaml can.
18
- # NoMethodError is unexpected types; RangeError is Bignums
19
- Marshal.dump(obj)
20
- end
22
+ def cache_dir=(cache_dir)
23
+ @cache_dir = cache_dir.end_with?("/") ? "#{cache_dir}yaml" : "#{cache_dir}-yaml"
24
+ end
21
25
 
22
- def self.storage_to_output(data)
23
- # This could have a meaning in messagepack, and we're being a little lazy
24
- # about it. -- but a leading 0x04 would indicate the contents of the YAML
25
- # is a positive integer, which is rare, to say the least.
26
- if data[0] == 0x04.chr && data[1] == 0x08.chr
27
- Marshal.load(data)
28
- else
29
- msgpack_factory.unpacker.feed(data).read
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
+ )
30
34
  end
31
- end
32
35
 
33
- def self.input_to_output(data)
34
- ::YAML.load(data)
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")
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
79
+
80
+ # MessagePack serializes symbols as strings by default.
81
+ # We want them to roundtrip cleanly, so we use a custom factory.
82
+ # see: https://github.com/msgpack/msgpack-ruby/pull/122
83
+ factory = MessagePack::Factory.new
84
+ factory.register_type(
85
+ 0x00,
86
+ Symbol,
87
+ packer: :to_msgpack_ext,
88
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
89
+ )
90
+
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
109
+ end
110
+
111
+ self.msgpack_factory = factory
112
+
113
+ self.supported_options = []
114
+ params = ::YAML.method(:load).parameters
115
+ if params.include?([:key, :symbolize_names])
116
+ supported_options << :symbolize_names
117
+ end
118
+ if params.include?([:key, :freeze]) && factory.load(factory.dump("yaml"), freeze: true).frozen?
119
+ supported_options << :freeze
120
+ end
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)
136
+ end
35
137
  end
36
138
 
37
- def self.install!(cache_dir)
38
- require('yaml')
39
- require('msgpack')
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
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
+ 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
242
+ end
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
40
271
 
41
- # MessagePack serializes symbols as strings by default.
42
- # We want them to roundtrip cleanly, so we use a custom factory.
43
- # see: https://github.com/msgpack/msgpack-ruby/pull/122
44
- factory = MessagePack::Factory.new
45
- factory.register_type(0x00, Symbol)
46
- Bootsnap::CompileCache::YAML.msgpack_factory = factory
272
+ module Psych3
273
+ extend self
47
274
 
48
- klass = class << ::YAML; self; end
49
- klass.send(:define_method, :load_file) do |path|
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
50
279
  begin
51
- Bootsnap::CompileCache::Native.fetch(
52
- cache_dir,
53
- path,
54
- Bootsnap::CompileCache::YAML
55
- )
56
- rescue Errno::EACCES
57
- 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)
58
290
  end
291
+ unpacker = CompileCache::YAML.msgpack_factory.unpacker(kwargs)
292
+ unpacker.feed(data)
293
+ _safe_loaded = unpacker.unpack
294
+ unpacker.unpack
295
+ end
296
+
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)
59
349
  end
60
350
  end
61
351
  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)
@@ -34,10 +53,8 @@ module Bootsnap
34
53
  end
35
54
 
36
55
  def self.supported?
37
- # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), and >= 2.3.0
38
- RUBY_ENGINE == 'ruby' &&
39
- RUBY_PLATFORM =~ /darwin|linux|bsd/ &&
40
- Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.3.0")
56
+ # only enable on 'ruby' (MRI), POSIX (darwin, linux, *bsd), Windows (RubyInstaller2) and >= 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}")
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative('../explicit_require')
3
+ require_relative("../explicit_require")
4
4
 
5
5
  module Bootsnap
6
6
  module LoadPathCache
@@ -10,8 +10,8 @@ module Bootsnap
10
10
  def initialize(store, path_obj, development_mode: false)
11
11
  @development_mode = development_mode
12
12
  @store = store
13
- @mutex = defined?(::Mutex) ? ::Mutex.new : ::Thread::Mutex.new # TODO: Remove once Ruby 2.2 support is dropped.
14
- @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) }
15
15
  @has_relative_paths = nil
16
16
  reinitialize
17
17
  end
@@ -28,15 +28,16 @@ module Bootsnap
28
28
  BUILTIN_FEATURES = $LOADED_FEATURES.each_with_object({}) do |feat, features|
29
29
  # Builtin features are of the form 'enumerator.so'.
30
30
  # All others include paths.
31
- next unless feat.size < 20 && !feat.include?('/')
31
+ next unless feat.size < 20 && !feat.include?("/")
32
32
 
33
- base = File.basename(feat, '.*') # enumerator.so -> enumerator
33
+ base = File.basename(feat, ".*") # enumerator.so -> enumerator
34
34
  ext = File.extname(feat) # .so
35
35
 
36
36
  features[feat] = nil # enumerator.so
37
37
  features[base] = nil # enumerator
38
38
 
39
39
  next unless [DOT_SO, *DL_EXTENSIONS].include?(ext)
40
+
40
41
  DL_EXTENSIONS.each do |dl_ext|
41
42
  features["#{base}#{dl_ext}"] = nil # enumerator.bundle
42
43
  end
@@ -47,8 +48,13 @@ module Bootsnap
47
48
  def find(feature)
48
49
  reinitialize if (@has_relative_paths && dir_changed?) || stale?
49
50
  feature = feature.to_s.freeze
50
- return feature if absolute_path?(feature)
51
- return expand_path(feature) if feature.start_with?('./')
51
+
52
+ return feature if Bootsnap.absolute_path?(feature)
53
+
54
+ if feature.start_with?("./", "../")
55
+ return expand_path(feature)
56
+ end
57
+
52
58
  @mutex.synchronize do
53
59
  x = search_index(feature)
54
60
  return x if x
@@ -58,7 +64,7 @@ module Bootsnap
58
64
  # returns false as if it were already loaded; however, there is no
59
65
  # file to find on disk. We've pre-built a list of these, and we
60
66
  # return false if any of them is loaded.
61
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
67
+ return false if BUILTIN_FEATURES.key?(feature)
62
68
 
63
69
  # The feature wasn't found on our preliminary search through the index.
64
70
  # We resolve this differently depending on what the extension was.
@@ -67,13 +73,14 @@ module Bootsnap
67
73
  # native dynamic extension, e.g. .bundle or .so), we know it was a
68
74
  # failure and there's nothing more we can do to find the file.
69
75
  # no extension, .rb, (.bundle or .so)
70
- when '', *CACHED_EXTENSIONS
76
+ when "", *CACHED_EXTENSIONS
71
77
  nil
72
78
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
79
  # is '.bundle'. This is where we handle that case.
74
80
  when DOT_SO
75
81
  x = search_index(feature[0..-4] + DLEXT)
76
82
  return x if x
83
+
77
84
  if DLEXT2
78
85
  x = search_index(feature[0..-4] + DLEXT2)
79
86
  return x if x
@@ -81,7 +88,7 @@ module Bootsnap
81
88
  else
82
89
  # other, unknown extension. For example, `.rake`. Since we haven't
83
90
  # cached these, we legitimately need to run the load path search.
84
- raise(LoadPathCache::FallbackScan, '', [])
91
+ return FALLBACK_SCAN
85
92
  end
86
93
  end
87
94
 
@@ -89,33 +96,25 @@ module Bootsnap
89
96
  # cases where the file doesn't appear to be on the load path. We should
90
97
  # be able to detect newly-created files without rebooting the
91
98
  # application.
92
- raise(LoadPathCache::FallbackScan, '', []) if @development_mode
93
- end
94
-
95
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
96
- def absolute_path?(path)
97
- path[1] == ':'
98
- end
99
- else
100
- def absolute_path?(path)
101
- path.start_with?(SLASH)
102
- end
99
+ return FALLBACK_SCAN if @development_mode
103
100
  end
104
101
 
105
102
  def unshift_paths(sender, *paths)
106
103
  return unless sender == @path_obj
104
+
107
105
  @mutex.synchronize { unshift_paths_locked(*paths) }
108
106
  end
109
107
 
110
108
  def push_paths(sender, *paths)
111
109
  return unless sender == @path_obj
110
+
112
111
  @mutex.synchronize { push_paths_locked(*paths) }
113
112
  end
114
113
 
115
114
  def reinitialize(path_obj = @path_obj)
116
115
  @mutex.synchronize do
117
116
  @path_obj = path_obj
118
- ChangeObserver.register(self, @path_obj)
117
+ ChangeObserver.register(@path_obj, self)
119
118
  @index = {}
120
119
  @dirs = {}
121
120
  @generated_at = now
@@ -141,6 +140,9 @@ module Bootsnap
141
140
  p = Path.new(path)
142
141
  @has_relative_paths = true if p.relative?
143
142
  next if p.non_directory?
143
+
144
+ p = p.to_realpath
145
+
144
146
  expanded_path = p.expanded_path
145
147
  entries, dirs = p.entries_and_dirs(@store)
146
148
  # push -> low precedence -> set only if unset
@@ -155,6 +157,9 @@ module Bootsnap
155
157
  paths.map(&:to_s).reverse_each do |path|
156
158
  p = Path.new(path)
157
159
  next if p.non_directory?
160
+
161
+ p = p.to_realpath
162
+
158
163
  expanded_path = p.expanded_path
159
164
  entries, dirs = p.entries_and_dirs(@store)
160
165
  # unshift -> high precedence -> unconditional set
@@ -177,31 +182,54 @@ module Bootsnap
177
182
  end
178
183
 
179
184
  if DLEXT2
180
- def search_index(f)
181
- try_index("#{f}#{DOT_RB}") || try_index("#{f}#{DLEXT}") || try_index("#{f}#{DLEXT2}") || try_index(f)
185
+ def search_index(feature)
186
+ try_index(feature + DOT_RB) ||
187
+ try_index(feature + DLEXT) ||
188
+ try_index(feature + DLEXT2) ||
189
+ try_index(feature)
182
190
  end
183
191
 
184
- def maybe_append_extension(f)
185
- try_ext("#{f}#{DOT_RB}") || try_ext("#{f}#{DLEXT}") || try_ext("#{f}#{DLEXT2}") || f
192
+ def maybe_append_extension(feature)
193
+ try_ext(feature + DOT_RB) ||
194
+ try_ext(feature + DLEXT) ||
195
+ try_ext(feature + DLEXT2) ||
196
+ feature
186
197
  end
187
198
  else
188
- def search_index(f)
189
- try_index("#{f}#{DOT_RB}") || try_index("#{f}#{DLEXT}") || try_index(f)
199
+ def search_index(feature)
200
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
190
201
  end
191
202
 
192
- def maybe_append_extension(f)
193
- try_ext("#{f}#{DOT_RB}") || try_ext("#{f}#{DLEXT}") || f
203
+ def maybe_append_extension(feature)
204
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
194
205
  end
195
206
  end
196
207
 
197
- def try_index(f)
198
- if (p = @index[f])
199
- "#{p}/#{f}"
208
+ s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
209
+ if s.respond_to?(:-@)
210
+ if ((-s).equal?(s) && (-s.dup).equal?(s)) || RUBY_VERSION >= "2.7"
211
+ def try_index(feature)
212
+ if (path = @index[feature])
213
+ -File.join(path, feature).freeze
214
+ end
215
+ end
216
+ else
217
+ def try_index(feature)
218
+ if (path = @index[feature])
219
+ -File.join(path, feature).untaint
220
+ end
221
+ end
222
+ end
223
+ else
224
+ def try_index(feature)
225
+ if (path = @index[feature])
226
+ File.join(path, feature)
227
+ end
200
228
  end
201
229
  end
202
230
 
203
- def try_ext(f)
204
- f if File.exist?(f)
231
+ def try_ext(feature)
232
+ feature if File.exist?(feature)
205
233
  end
206
234
  end
207
235
  end