keg_relocate: strip high bytes before matched paths

- Apple's `strings` under brew's UTF-8 locale can report a high byte
  just before an embedded path as part of the same run.
- `text_matches_in_file` then fails `File.exist?` on the joined string
  and drops the match, so `brew bottle` can mark a keg that embeds its
  Cellar path as `cellar: :any`.
- Strip those bytes rather than running `strings` with `LC_ALL=C`,
  which would split prefixes containing non-ASCII characters.
- Pass `-e S` to GNU `strings` on Linux, which otherwise splits
  those prefixes and misses them in text files.
This commit is contained in:
Andrew Nesbitt committed 2026-10-24 16:33:29 +01:00
1 parent 13bc9549b4
commit 0235bb5551
3 files changed
+34 -2

No files matched your search

@@ -10,6 +10,15 @@ module OS
requires_ancestor { ::Keg }
module ClassMethods
# GNU `strings` splits runs at non-ASCII bytes, such as those in a
# prefix like `/Users/José`, unless asked to keep 8-bit bytes.
sig { returns(T::Array[String]) }
def strings_encoding_args
["-e", "S"]
end
end
sig {
params(relocation: ::Keg::Relocation, with_placeholders: T::Boolean,
files: T.nilable(T::Array[::Pathname])).returns(T::Array[::Pathname])
@@ -126,4 +135,5 @@ module OS
end
end
Keg.singleton_class.prepend(OS::Linux::Keg::ClassMethods)
Keg.prepend(OS::Linux::Keg)
+8 -2
View File
@@ -614,7 +614,8 @@ class Keg
# Some binaries contain strings with lists of files
# e.g. `/usr/local/lib/foo:/usr/local/share/foo:/usr/lib/foo`
# Each item in the list should be checked separately
match.split(":").each do |sub_match|
# A high byte just before a path can be reported as part of the same run.
match.split(":").map { it.b.sub(%r{\A[\x80-\xFF]+(?=/)}n, "") }.each do |sub_match|
# Not all items in the list may be matches
next unless sub_match.match? path_regex
next if linked_libraries.include? sub_match # Don't bother reporting a string if it was found by otool
@@ -648,7 +649,7 @@ class Keg
return
end
Utils.popen_read("strings", "-t", "x", "-", file.to_s) do |io|
Utils.popen_read("strings", *strings_encoding_args, "-t", "x", "-", file.to_s) do |io|
until io.eof?
line = io.readline.chomp
offset, match = line.split(" ", 2)
@@ -823,6 +824,11 @@ class Keg
[]
end
sig { returns(T::Array[String]) }
def self.strings_encoding_args
[]
end
private
sig { returns(T::Boolean) }
@@ -48,6 +48,22 @@ RSpec.describe Keg do
expect(result.count).to eq 2
end
specify "::text_matches_in_file finds a path preceded by a non-ASCII byte" do
ENV["LC_ALL"] = "en_US.UTF-8"
file.binwrite("\0\xD6#{dir}/file.txt\0".b)
expect(described_class.text_matches_in_file(file, dir.to_s, [], [], nil).count).to eq 1
end
specify "::text_matches_in_file finds a path containing non-ASCII characters",
unless: ENV.fetch("HOMEBREW_TEST_GENERIC_OS", nil) do
ENV["LC_ALL"] = "en_US.UTF-8"
(dir/"José").mkpath
file.binwrite("\0#{dir}/José\0".b)
expect(described_class.text_matches_in_file(file, dir.to_s, [], [], nil).count).to eq 1
end
specify "::each_candidate_string still yields runs that are not valid UTF-8" do
setup_file