File: //lib/ruby/vendor_ruby/bootsnap/load_path_cache/loaded_features_index.rb
# frozen_string_literal: true
module Bootsnap
module LoadPathCache
# LoadedFeaturesIndex partially mirrors an internal structure in ruby that
# we can't easily obtain an interface to.
#
# This works around an issue where, without bootsnap, *ruby* knows that it
# has already required a file by its short name (e.g. require 'bundler') if
# a new instance of bundler is added to the $LOAD_PATH which resolves to a
# different absolute path. This class makes bootsnap smart enough to
# realize that it has already loaded 'bundler', and not just
# '/path/to/bundler'.
#
# If you disable LoadedFeaturesIndex, you can see the problem this solves by:
#
# 1. `require 'a'`
# 2. Prepend a new $LOAD_PATH element containing an `a.rb`
# 3. `require 'a'`
#
# Ruby returns false from step 3.
# With bootsnap but with no LoadedFeaturesIndex, this loads two different
# `a.rb`s.
# With bootsnap and with LoadedFeaturesIndex, this skips the second load,
# returning false like ruby.
class LoadedFeaturesIndex
def initialize
@lfi = {}
@mutex = Mutex.new
# In theory the user could mutate $LOADED_FEATURES and invalidate our
# cache. If this ever comes up in practice — or if you, the
# enterprising reader, feels inclined to solve this problem — we could
# parallel the work done with ChangeObserver on $LOAD_PATH to mirror
# updates to our @lfi.
$LOADED_FEATURES.each do |feat|
hash = feat.hash
$LOAD_PATH.each do |lpe|
next unless feat.start_with?(lpe)
# /a/b/lib/my/foo.rb
# ^^^^^^^^^
short = feat[(lpe.length + 1)..-1]
stripped = strip_extension_if_elidable(short)
@lfi[short] = hash
@lfi[stripped] = hash
end
end
end
# We've optimized for initialize and register to be fast, and purge to be tolerable.
# If access patterns make this not-okay, we can lazy-invert the LFI on
# first purge and work from there.
def purge(feature)
@mutex.synchronize do
feat_hash = feature.hash
@lfi.reject! { |_, hash| hash == feat_hash }
end
end
def purge_multi(features)
rejected_hashes = features.each_with_object({}) { |f, h| h[f.hash] = true }
@mutex.synchronize do
@lfi.reject! { |_, hash| rejected_hashes.key?(hash) }
end
end
def key?(feature)
@mutex.synchronize { @lfi.key?(feature) }
end
# There is a relatively uncommon case where we could miss adding an
# entry:
#
# If the user asked for e.g. `require 'bundler'`, and we went through the
# `FallbackScan` pathway in `kernel_require.rb` and therefore did not
# pass `long` (the full expanded absolute path), then we did are not able
# to confidently add the `bundler.rb` form to @lfi.
#
# We could either:
#
# 1. Just add `bundler.rb`, `bundler.so`, and so on, which is close but
# not quite right; or
# 2. Inspect $LOADED_FEATURES upon return from yield to find the matching
# entry.
def register(short, long = nil)
if long.nil?
len = $LOADED_FEATURES.size
ret = yield
long = $LOADED_FEATURES[len..-1].detect do |feat|
offset = 0
while offset = feat.index(short, offset)
if feat.index(".", offset + 1) && !feat.index("/", offset + 2)
break true
else
offset += 1
end
end
end
else
ret = yield
end
hash = long.hash
# Do we have a filename with an elidable extension, e.g.,
# 'bundler.rb', or 'libgit2.so'?
altname = if extension_elidable?(short)
# Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
strip_extension_if_elidable(short)
elsif long && (ext = File.extname(long.freeze))
# We already know the extension of the actual file this
# resolves to, so put that back on.
short + ext
end
@mutex.synchronize do
@lfi[short] = hash
(@lfi[altname] = hash) if altname
end
ret
end
private
STRIP_EXTENSION = /\.[^.]*?$/
private_constant(:STRIP_EXTENSION)
# Might Ruby automatically search for this extension if
# someone tries to 'require' the file without it? E.g. Ruby
# will implicitly try 'x.rb' if you ask for 'x'.
#
# This is complex and platform-dependent, and the Ruby docs are a little
# handwavy about what will be tried when and in what order.
# So optimistically pretend that all known elidable extensions
# will be tried on all platforms, and that people are unlikely
# to name files in a way that assumes otherwise.
# (E.g. It's unlikely that someone will know that their code
# will _never_ run on MacOS, and therefore think they can get away
# with calling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
#
# See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
def extension_elidable?(f)
f.to_s.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
end
def strip_extension_if_elidable(f)
if extension_elidable?(f)
f.sub(STRIP_EXTENSION, '')
else
f
end
end
end
end
end