bootsnap 1.7.2 → 1.11.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,63 +1,86 @@
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
48
66
 
49
67
  # MessagePack serializes symbols as strings by default.
50
68
  # We want them to roundtrip cleanly, so we use a custom factory.
51
69
  # see: https://github.com/msgpack/msgpack-ruby/pull/122
52
70
  factory = MessagePack::Factory.new
53
- factory.register_type(0x00, Symbol)
71
+ factory.register_type(
72
+ 0x00,
73
+ Symbol,
74
+ packer: :to_msgpack_ext,
75
+ unpacker: EncodingAwareSymbols.method(:unpack).to_proc,
76
+ )
54
77
 
55
78
  if defined? MessagePack::Timestamp
56
79
  factory.register_type(
57
80
  MessagePack::Timestamp::TYPE, # or just -1
58
81
  Time,
59
82
  packer: MessagePack::Time::Packer,
60
- unpacker: MessagePack::Time::Unpacker
83
+ unpacker: MessagePack::Time::Unpacker,
61
84
  )
62
85
 
63
86
  marshal_fallback = {
@@ -77,38 +100,251 @@ module Bootsnap
77
100
  self.supported_options = []
78
101
  params = ::YAML.method(:load).parameters
79
102
  if params.include?([:key, :symbolize_names])
80
- self.supported_options << :symbolize_names
103
+ supported_options << :symbolize_names
81
104
  end
82
105
  if params.include?([:key, :freeze])
83
- if factory.load(factory.dump('yaml'), freeze: true).frozen?
84
- self.supported_options << :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
85
132
  end
86
133
  end
87
- self.supported_options.freeze
88
134
  end
89
135
  end
90
136
 
91
- module Patch
92
- def load_file(path, *args)
93
- return super if args.size > 1
94
- if kwargs = args.first
95
- return super unless kwargs.is_a?(Hash)
96
- return super unless (kwargs.keys - ::Bootsnap::CompileCache::YAML.supported_options).empty?
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
97
161
  end
98
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
268
+ end
269
+
270
+ module Psych3
271
+ extend self
272
+
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
99
277
  begin
100
- ::Bootsnap::CompileCache::Native.fetch(
101
- Bootsnap::CompileCache::YAML.cache_dir,
102
- File.realpath(path),
103
- ::Bootsnap::CompileCache::YAML,
104
- kwargs,
105
- )
106
- rescue Errno::EACCES
107
- ::Bootsnap::CompileCache.permission_error(path)
278
+ packer.pack(obj)
279
+ rescue NoMethodError, RangeError
280
+ return UNCOMPILABLE # The object included things that we can't serialize
108
281
  end
282
+ packer.to_s
109
283
  end
110
284
 
111
- ruby2_keywords :load_file if respond_to?(:ruby2_keywords, true)
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)
347
+ end
112
348
  end
113
349
  end
114
350
  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:)
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,21 @@ 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
25
40
  end
26
41
 
27
42
  def self.permission_error(path)
@@ -35,9 +50,7 @@ module Bootsnap
35
50
 
36
51
  def self.supported?
37
52
  # 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")
53
+ RUBY_ENGINE == "ruby" && RUBY_PLATFORM.match?(/darwin|linux|bsd|mswin|mingw|cygwin/)
41
54
  end
42
55
  end
43
56
  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
@@ -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
@@ -44,21 +45,27 @@ module Bootsnap
44
45
 
45
46
  # Try to resolve this feature to an absolute path without traversing the
46
47
  # loadpath.
47
- def find(feature)
48
+ def find(feature, try_extensions: true)
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 try_extensions ? expand_path(feature) : File.expand_path(feature).freeze
56
+ end
57
+
52
58
  @mutex.synchronize do
53
- x = search_index(feature)
59
+ x = search_index(feature, try_extensions: try_extensions)
54
60
  return x if x
61
+ return unless try_extensions
55
62
 
56
63
  # Ruby has some built-in features that require lies about.
57
64
  # For example, 'enumerator' is built in. If you require it, ruby
58
65
  # returns false as if it were already loaded; however, there is no
59
66
  # file to find on disk. We've pre-built a list of these, and we
60
67
  # return false if any of them is loaded.
61
- raise(LoadPathCache::ReturnFalse, '', []) if BUILTIN_FEATURES.key?(feature)
68
+ return false if BUILTIN_FEATURES.key?(feature)
62
69
 
63
70
  # The feature wasn't found on our preliminary search through the index.
64
71
  # We resolve this differently depending on what the extension was.
@@ -67,13 +74,14 @@ module Bootsnap
67
74
  # native dynamic extension, e.g. .bundle or .so), we know it was a
68
75
  # failure and there's nothing more we can do to find the file.
69
76
  # no extension, .rb, (.bundle or .so)
70
- when '', *CACHED_EXTENSIONS
77
+ when "", *CACHED_EXTENSIONS
71
78
  nil
72
79
  # Ruby allows specifying native extensions as '.so' even when DLEXT
73
80
  # is '.bundle'. This is where we handle that case.
74
81
  when DOT_SO
75
82
  x = search_index(feature[0..-4] + DLEXT)
76
83
  return x if x
84
+
77
85
  if DLEXT2
78
86
  x = search_index(feature[0..-4] + DLEXT2)
79
87
  return x if x
@@ -81,7 +89,7 @@ module Bootsnap
81
89
  else
82
90
  # other, unknown extension. For example, `.rake`. Since we haven't
83
91
  # cached these, we legitimately need to run the load path search.
84
- raise(LoadPathCache::FallbackScan, '', [])
92
+ return FALLBACK_SCAN
85
93
  end
86
94
  end
87
95
 
@@ -89,26 +97,18 @@ module Bootsnap
89
97
  # cases where the file doesn't appear to be on the load path. We should
90
98
  # be able to detect newly-created files without rebooting the
91
99
  # 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
100
+ return FALLBACK_SCAN if @development_mode
103
101
  end
104
102
 
105
103
  def unshift_paths(sender, *paths)
106
104
  return unless sender == @path_obj
105
+
107
106
  @mutex.synchronize { unshift_paths_locked(*paths) }
108
107
  end
109
108
 
110
109
  def push_paths(sender, *paths)
111
110
  return unless sender == @path_obj
111
+
112
112
  @mutex.synchronize { push_paths_locked(*paths) }
113
113
  end
114
114
 
@@ -141,6 +141,9 @@ module Bootsnap
141
141
  p = Path.new(path)
142
142
  @has_relative_paths = true if p.relative?
143
143
  next if p.non_directory?
144
+
145
+ p = p.to_realpath
146
+
144
147
  expanded_path = p.expanded_path
145
148
  entries, dirs = p.entries_and_dirs(@store)
146
149
  # push -> low precedence -> set only if unset
@@ -155,6 +158,9 @@ module Bootsnap
155
158
  paths.map(&:to_s).reverse_each do |path|
156
159
  p = Path.new(path)
157
160
  next if p.non_directory?
161
+
162
+ p = p.to_realpath
163
+
158
164
  expanded_path = p.expanded_path
159
165
  entries, dirs = p.entries_and_dirs(@store)
160
166
  # unshift -> high precedence -> unconditional set
@@ -177,48 +183,62 @@ module Bootsnap
177
183
  end
178
184
 
179
185
  if DLEXT2
180
- def search_index(f)
181
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f + DLEXT2) || try_index(f)
186
+ def search_index(feature, try_extensions: true)
187
+ if try_extensions
188
+ try_index(feature + DOT_RB) ||
189
+ try_index(feature + DLEXT) ||
190
+ try_index(feature + DLEXT2) ||
191
+ try_index(feature)
192
+ else
193
+ try_index(feature)
194
+ end
182
195
  end
183
196
 
184
- def maybe_append_extension(f)
185
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || try_ext(f + DLEXT2) || f
197
+ def maybe_append_extension(feature)
198
+ try_ext(feature + DOT_RB) ||
199
+ try_ext(feature + DLEXT) ||
200
+ try_ext(feature + DLEXT2) ||
201
+ feature
186
202
  end
187
203
  else
188
- def search_index(f)
189
- try_index(f + DOT_RB) || try_index(f + DLEXT) || try_index(f)
204
+ def search_index(feature, try_extensions: true)
205
+ if try_extensions
206
+ try_index(feature + DOT_RB) || try_index(feature + DLEXT) || try_index(feature)
207
+ else
208
+ try_index(feature)
209
+ end
190
210
  end
191
211
 
192
- def maybe_append_extension(f)
193
- try_ext(f + DOT_RB) || try_ext(f + DLEXT) || f
212
+ def maybe_append_extension(feature)
213
+ try_ext(feature + DOT_RB) || try_ext(feature + DLEXT) || feature
194
214
  end
195
215
  end
196
216
 
197
217
  s = rand.to_s.force_encoding(Encoding::US_ASCII).freeze
198
218
  if s.respond_to?(:-@)
199
- if (-s).equal?(s) && (-s.dup).equal?(s)
200
- def try_index(f)
201
- if (p = @index[f])
202
- -(File.join(p, f).freeze)
219
+ if (-s).equal?(s) && (-s.dup).equal?(s) || RUBY_VERSION >= "2.7"
220
+ def try_index(feature)
221
+ if (path = @index[feature])
222
+ -File.join(path, feature).freeze
203
223
  end
204
224
  end
205
225
  else
206
- def try_index(f)
207
- if (p = @index[f])
208
- -File.join(p, f).untaint
226
+ def try_index(feature)
227
+ if (path = @index[feature])
228
+ -File.join(path, feature).untaint
209
229
  end
210
230
  end
211
231
  end
212
232
  else
213
- def try_index(f)
214
- if (p = @index[f])
215
- File.join(p, f)
233
+ def try_index(feature)
234
+ if (path = @index[feature])
235
+ File.join(path, feature)
216
236
  end
217
237
  end
218
238
  end
219
239
 
220
- def try_ext(f)
221
- f if File.exist?(f)
240
+ def try_ext(feature)
241
+ feature if File.exist?(feature)
222
242
  end
223
243
  end
224
244
  end