Skip to content

Commit 2a3644c

Browse files
committed
Only strip elidable extensions to make short forms
Otherwise we'll erroneously think that 'descriptor' ('descriptor.rb') is a loaded feature when somebody requires 'descriptor.pb' ('descriptor.pb.rb'), when in fact those are two completely unrelated files. Elidable extensions include Ruby '.rb' files and shared libraries. Ruby does some slightly magical things in 'require', e.g., letting you refer to shared libraries by extensions that _would_ have been valid on another platform ('libgit2.dll' works on Linux!) which would be difficult to replicate precisely without a clear spec, so this change makes the simplifying assumption that people won't use these extensions for anything other than shared libraries.
1 parent b2b30b3 commit 2a3644c

File tree

2 files changed

+66
-9
lines changed

2 files changed

+66
-9
lines changed

lib/bootsnap/load_path_cache/loaded_features_index.rb

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def initialize
4040
# /a/b/lib/my/foo.rb
4141
# ^^^^^^^^^
4242
short = feat[(lpe.length + 1)..-1]
43-
stripped = strip_extension(short)
43+
stripped = strip_extension_if_elidable(short)
4444
@lfi[short] = hash
4545
@lfi[stripped] = hash
4646
end
@@ -94,13 +94,14 @@ def register(short, long = nil)
9494

9595
hash = long.hash
9696

97-
# do we have 'bundler' or 'bundler.rb'?
98-
altname = if File.extname(short) != ''
99-
# strip the path from 'bundler.rb' -> 'bundler'
100-
strip_extension(short)
97+
# Do we have a filename with an elidable extension, e.g.,
98+
# 'bundler.rb', or 'libgit2.so'?
99+
altname = if is_extension_elidable(short)
100+
# Strip the extension off, e.g. 'bundler.rb' -> 'bundler'.
101+
strip_extension_if_elidable(short)
101102
elsif long && (ext = File.extname(long))
102-
# get the extension from the expanded path if given
103-
# 'bundler' + '.rb'
103+
# We already know the extension of the actual file this
104+
# resolves to, so put that back on.
104105
short + ext
105106
end
106107

@@ -117,8 +118,30 @@ def register(short, long = nil)
117118
STRIP_EXTENSION = /\.[^.]*?$/
118119
private_constant(:STRIP_EXTENSION)
119120

120-
def strip_extension(f)
121-
f.sub(STRIP_EXTENSION, '')
121+
# Might Ruby automatically search for this extension if
122+
# someone tries to 'require' the file without it? E.g. Ruby
123+
# will implicitly try 'x.rb' if you ask for 'x'.
124+
#
125+
# This is complex and platform-dependent, and the Ruby docs are a little
126+
# handwavy about what will be tried when and in what order.
127+
# So optimistically pretend that all known elidable extensions
128+
# will be tried on all platforms, and that people are unlikely
129+
# to name files in a way that assumes otherwise.
130+
# (E.g. It's unlikely that someone will know that their code
131+
# will _never_ run on MacOS, and therefore think they can get away
132+
# with callling a Ruby file 'x.dylib.rb' and then requiring it as 'x.dylib'.)
133+
#
134+
# See <https://ruby-doc.org/core-2.6.4/Kernel.html#method-i-require>.
135+
def is_extension_elidable(f)
136+
f.end_with?('.rb', '.so', '.o', '.dll', '.dylib')
137+
end
138+
139+
def strip_extension_if_elidable(f)
140+
if is_extension_elidable(f)
141+
f.sub(STRIP_EXTENSION, '')
142+
else
143+
f
144+
end
122145
end
123146
end
124147
end

test/load_path_cache/loaded_features_index_test.rb

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,40 @@ def test_infer_base_from_ext
4141
refute(@index.key?('foo'))
4242
end
4343

44+
def test_only_strip_elidable_ext
45+
# It is only valid to strip a '.rb' or shared library extension from the
46+
# end of a filename, not anything else.
47+
#
48+
# E.g. 'descriptor.pb.rb' if required via 'descriptor.pb'
49+
# should never be shortened to merely 'descriptor'!
50+
refute(@index.key?('descriptor.pb'))
51+
refute(@index.key?('descriptor.pb.rb'))
52+
refute(@index.key?('descriptor.rb'))
53+
refute(@index.key?('descriptor'))
54+
refute(@index.key?('foo'))
55+
@index.register('descriptor.pb.rb') {}
56+
assert(@index.key?('descriptor.pb'))
57+
assert(@index.key?('descriptor.pb.rb'))
58+
refute(@index.key?('descriptor.rb'))
59+
refute(@index.key?('descriptor'))
60+
refute(@index.key?('foo'))
61+
end
62+
63+
def test_shared_library_ext_considered_elidable
64+
# Check that '.dylib' (token shared library extension) is treated as elidable,
65+
# and doesn't get mixed up with Ruby '.rb' files.
66+
refute(@index.key?('libgit2.dylib'))
67+
refute(@index.key?('libgit2.dylib.rb'))
68+
refute(@index.key?('descriptor.rb'))
69+
refute(@index.key?('descriptor'))
70+
refute(@index.key?('foo'))
71+
@index.register('libgit2.dylib') {}
72+
assert(@index.key?('libgit2.dylib'))
73+
refute(@index.key?('libgit2.dylib.rb'))
74+
refute(@index.key?('libgit2.rb'))
75+
refute(@index.key?('foo'))
76+
end
77+
4478
def test_cannot_infer_ext_from_base # Current limitation
4579
refute(@index.key?('bundler'))
4680
refute(@index.key?('bundler.rb'))

0 commit comments

Comments
 (0)