bootsnap 1.4.8 → 1.16.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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