[GlobPattern] add AsLiteral to help lld determine if a symbol is literal - #215854
Conversation
|
@llvm/pr-subscribers-lld-macho @llvm/pr-subscribers-lld Author: Peter Rong (DataCorrupted) Changeslld determines if a
Added tests in lld and unit test for GlobPattern Full diff: https://github.com/llvm/llvm-project/pull/215854.diff 5 Files Affected:
diff --git a/lld/MachO/Driver.cpp b/lld/MachO/Driver.cpp
index 2864c6d28fa49..2fa959eaf7e93 100644
--- a/lld/MachO/Driver.cpp
+++ b/lld/MachO/Driver.cpp
@@ -1360,12 +1360,21 @@ void SymbolPatterns::clear() {
}
void SymbolPatterns::insert(StringRef symbolName) {
- if (symbolName.find_first_of("*?[]") == StringRef::npos)
- literals.insert(CachedHashStringRef(symbolName));
- else if (Expected<GlobPattern> pattern = GlobPattern::create(symbolName))
- globs.emplace_back(*pattern);
- else
+ Expected<GlobPattern> pattern = GlobPattern::create(symbolName);
+ if (!pattern) {
+ llvm::consumeError(pattern.takeError());
error("invalid symbol-name pattern: " + symbolName);
+ return;
+ }
+ // A pattern that denotes a single string is kept as a literal: literals are
+ // matched by hash lookup, and only literals seed the force-load of lazy
+ // archive members below.
+ SmallVector<char, 128> storage;
+ if (std::optional<StringRef> literal = pattern->asLiteral(storage)) {
+ literals.insert(CachedHashStringRef(saver().save(*literal)));
+ return;
+ }
+ globs.emplace_back(std::move(*pattern));
}
bool SymbolPatterns::matchLiteral(StringRef symbolName) const {
diff --git a/lld/test/MachO/exported-symbols-list-escapes.s b/lld/test/MachO/exported-symbols-list-escapes.s
new file mode 100644
index 0000000000000..169ce49435174
--- /dev/null
+++ b/lld/test/MachO/exported-symbols-list-escapes.s
@@ -0,0 +1,65 @@
+# REQUIRES: x86
+
+## An -exported_symbols_list entry may escape glob metacharacters with a
+## backslash to name a symbol literally. This matters for Objective-C direct
+## methods, whose names contain square brackets that would otherwise be parsed
+## as a character class and match nothing.
+
+# RUN: rm -rf %t; split-file %s %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %t/bracket.s -o %t/bracket.o
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %t/anchor.s -o %t/anchor.o
+# RUN: llvm-ar --format=darwin rcs %t/bracket.a %t/bracket.o
+
+## An escaped entry is a literal: it exact-matches, and because literals seed
+## addUndefined it also force-loads the archive member that defines it.
+# RUN: %lld -dylib -exported_symbols_list %t/escaped.txt \
+# RUN: %t/anchor.o %t/bracket.a -o %t/escaped.dylib
+# RUN: llvm-nm --extern-only %t/escaped.dylib | FileCheck --check-prefix=ESCAPED %s
+
+# ESCAPED: -[C m]D
+
+## An unescaped entry keeps its glob meaning: "[C m]" is a character class, so
+## it does not match the real symbol and nothing is force-loaded.
+# RUN: %lld -dylib -exported_symbols_list %t/unescaped.txt \
+# RUN: %t/anchor.o %t/bracket.a -o %t/unescaped.dylib
+# RUN: llvm-nm --extern-only %t/unescaped.dylib | \
+# RUN: FileCheck --check-prefix=UNESCAPED --allow-empty %s
+
+# UNESCAPED-NOT: -[C m]D
+
+## Escaping a star names a symbol that literally contains one, rather than
+## matching any symbol by prefix.
+# RUN: llvm-mc -filetype=obj -triple=x86_64-apple-macos %t/star.s -o %t/star.o
+# RUN: %lld -dylib -exported_symbols_list %t/escaped-star.txt \
+# RUN: %t/star.o -o %t/star.dylib
+# RUN: llvm-nm --extern-only %t/star.dylib | FileCheck --check-prefix=STAR %s
+
+# STAR-DAG: _lit*eral
+# STAR-NOT: _litoteral
+
+#--- bracket.s
+.globl "_-[C m]D"
+"_-[C m]D":
+ retq
+
+#--- anchor.s
+.globl _anchor
+_anchor:
+ retq
+
+#--- star.s
+.globl "_lit*eral"
+"_lit*eral":
+ retq
+.globl _litoteral
+_litoteral:
+ retq
+
+#--- escaped.txt
+_-\[C m\]D
+
+#--- unescaped.txt
+_-[C m]D
+
+#--- escaped-star.txt
+_lit\*eral
diff --git a/llvm/include/llvm/Support/GlobPattern.h b/llvm/include/llvm/Support/GlobPattern.h
index 8c84c93834c6b..7b4875cd093db 100644
--- a/llvm/include/llvm/Support/GlobPattern.h
+++ b/llvm/include/llvm/Support/GlobPattern.h
@@ -61,6 +61,17 @@ class GlobPattern {
/// \returns \p true if \p S matches this glob pattern
LLVM_ABI bool match(StringRef S) const;
+ /// \returns the single string this pattern matches, if the pattern contains
+ /// no unescaped metacharacters; otherwise std::nullopt. Escapes are resolved,
+ /// so `a\*b` yields `a*b`. Characters that are only special in context (`]`,
+ /// `}`, `,`) do not make a pattern non-literal.
+ ///
+ /// \p Storage is used only when escapes have to be resolved; otherwise the
+ /// result aliases the text passed to create(). The result is valid for as
+ /// long as both remain alive.
+ LLVM_ABI std::optional<StringRef>
+ asLiteral(SmallVectorImpl<char> &Storage) const;
+
// Returns true for glob pattern "*". Can be used to avoid expensive
// preparation/acquisition of the input for match().
bool isTrivialMatchAll() const {
@@ -97,6 +108,8 @@ class GlobPattern {
/// \returns \p true if \p S matches this glob pattern
LLVM_ABI bool match(StringRef S, bool SlashAgnostic) const;
StringRef getPat() const { return StringRef(Pat.data(), Pat.size()); }
+ /// \returns \p true if this sub-pattern matches exactly one string.
+ bool isLiteral() const { return Brackets.empty() && !HasWildcard; }
// Brackets with their end position and matched bytes.
struct Bracket {
@@ -104,6 +117,8 @@ class GlobPattern {
BitVector Bytes;
};
SmallVector<Bracket, 0> Brackets;
+ // Set while parsing if an unescaped '*' or '?' is present.
+ bool HasWildcard = false;
SmallVector<char, 0> Pat;
};
SmallVector<SubGlobPattern, 1> SubGlobs;
diff --git a/llvm/lib/Support/GlobPattern.cpp b/llvm/lib/Support/GlobPattern.cpp
index ebaa08cba96aa..6f78a02af4606 100644
--- a/llvm/lib/Support/GlobPattern.cpp
+++ b/llvm/lib/Support/GlobPattern.cpp
@@ -182,6 +182,17 @@ static StringRef maxPlainSubstring(StringRef S, bool SlashAgnostic) {
return Best;
}
+// Writes S into Storage with its escaping backslashes removed.
+static void unescapePattern(StringRef S, SmallVectorImpl<char> &Storage) {
+ Storage.clear();
+ Storage.reserve(S.size());
+ for (size_t I = 0, E = S.size(); I != E; ++I) {
+ if (S[I] == '\\' && I + 1 != E)
+ ++I;
+ Storage.push_back(S[I]);
+ }
+}
+
Expected<GlobPattern> GlobPattern::create(StringRef S,
std::optional<size_t> MaxSubPatterns,
bool SlashAgnostic) {
@@ -225,6 +236,23 @@ Expected<GlobPattern> GlobPattern::create(StringRef S,
return Pat;
}
+std::optional<StringRef>
+GlobPattern::asLiteral(SmallVectorImpl<char> &Storage) const {
+ // The prefix and suffix are metacharacter-free by construction, so whether
+ // this pattern denotes a single string is decided by the sub-patterns, which
+ // recorded it while parsing. No sub-pattern at all means there was no
+ // metacharacter; more than one means brace expansion produced a choice.
+ if (!SubGlobs.empty() &&
+ !(SubGlobs.size() == 1 && SubGlobs[0].isLiteral()))
+ return std::nullopt;
+
+ if (!Pattern.contains('\\'))
+ return Pattern;
+
+ unescapePattern(Pattern, Storage);
+ return StringRef(Storage.data(), Storage.size());
+}
+
Expected<GlobPattern::SubGlobPattern>
GlobPattern::SubGlobPattern::create(StringRef S, bool SlashAgnostic) {
SubGlobPattern Pat;
@@ -260,6 +288,8 @@ GlobPattern::SubGlobPattern::create(StringRef S, bool SlashAgnostic) {
if (++I == E)
return make_error<StringError>("invalid glob pattern, stray '\\'",
errc::invalid_argument);
+ } else if (S[I] == '*' || S[I] == '?') {
+ Pat.HasWildcard = true;
}
}
return Pat;
diff --git a/llvm/unittests/Support/GlobPatternTest.cpp b/llvm/unittests/Support/GlobPatternTest.cpp
index 35423e37a3ae0..3be79476ca0c2 100644
--- a/llvm/unittests/Support/GlobPatternTest.cpp
+++ b/llvm/unittests/Support/GlobPatternTest.cpp
@@ -39,6 +39,63 @@ TEST_F(GlobPatternTest, Wildcard) {
EXPECT_FALSE(Pat1->match(""));
}
+TEST_F(GlobPatternTest, AsLiteral) {
+ SmallVector<char, 64> Storage;
+
+ // Plain literals, including characters that are only special in context.
+ // These need no unescaping, so the result aliases the pattern.
+ for (StringRef P : {"abc", "", "a]c", "a}c", "a,c"}) {
+ Expected<GlobPattern> Pat = GlobPattern::create(P);
+ ASSERT_TRUE((bool)Pat) << P;
+ std::optional<StringRef> Lit = Pat->asLiteral(Storage);
+ ASSERT_TRUE(Lit.has_value()) << P;
+ EXPECT_EQ(*Lit, P);
+ EXPECT_EQ(Lit->data(), P.data()) << P;
+ }
+
+ // Real metacharacters are not literals.
+ for (StringRef P : {"a*c", "a?c", "a[bc]d"}) {
+ Expected<GlobPattern> Pat = GlobPattern::create(P);
+ ASSERT_TRUE((bool)Pat) << P;
+ EXPECT_FALSE(Pat->asLiteral(Storage).has_value()) << P;
+ }
+
+ // Escaped metacharacters denote a single string, with escapes resolved.
+ Expected<GlobPattern> Star = GlobPattern::create("a\\*c");
+ ASSERT_TRUE((bool)Star);
+ ASSERT_TRUE(Star->asLiteral(Storage).has_value());
+ EXPECT_EQ(*Star->asLiteral(Storage), "a*c");
+ EXPECT_TRUE(Star->match("a*c"));
+ EXPECT_FALSE(Star->match("abc"));
+
+ // The motivating case: an Objective-C direct method symbol.
+ Expected<GlobPattern> Method = GlobPattern::create("-\\[C m\\]D");
+ ASSERT_TRUE((bool)Method);
+ ASSERT_TRUE(Method->asLiteral(Storage).has_value());
+ EXPECT_EQ(*Method->asLiteral(Storage), "-[C m]D");
+ EXPECT_TRUE(Method->match("-[C m]D"));
+
+ // An unescaped bracket expression is a character class, not this symbol.
+ Expected<GlobPattern> Class = GlobPattern::create("-[C m]D");
+ ASSERT_TRUE((bool)Class);
+ EXPECT_FALSE(Class->asLiteral(Storage).has_value());
+ EXPECT_FALSE(Class->match("-[C m]D"));
+
+ // '{' is a metacharacter only when brace expansion is enabled.
+ Expected<GlobPattern> NoBraces = GlobPattern::create("a{b,c}d");
+ ASSERT_TRUE((bool)NoBraces);
+ EXPECT_TRUE(NoBraces->asLiteral(Storage).has_value());
+ Expected<GlobPattern> Braces = GlobPattern::create("a{b,c}d", /*Max=*/1024);
+ ASSERT_TRUE((bool)Braces);
+ EXPECT_FALSE(Braces->asLiteral(Storage).has_value());
+
+ // An escaped backslash is a literal backslash.
+ Expected<GlobPattern> Backslash = GlobPattern::create("a\\\\c");
+ ASSERT_TRUE((bool)Backslash);
+ ASSERT_TRUE(Backslash->asLiteral(Storage).has_value());
+ EXPECT_EQ(*Backslash->asLiteral(Storage), "a\\c");
+}
+
TEST_F(GlobPatternTest, Escape) {
Expected<GlobPattern> Pat1 = GlobPattern::create("\\*");
EXPECT_TRUE((bool)Pat1);
|
|
✅ With the latest revision this PR passed the C/C++ code formatter. |
17b87fe to
52e4973
Compare
| /// result aliases the text passed to create(). The result is valid for as | ||
| /// long as both remain alive. | ||
| LLVM_ABI std::optional<StringRef> | ||
| asLiteral(SmallVectorImpl<char> &Storage) const; |
There was a problem hiding this comment.
| asLiteral(SmallVectorImpl<char> &Storage) const; | |
| asLiteral(SmallString<> &Storage) const; |
This allows us to use += on characters
There was a problem hiding this comment.
I don't think you can use SmallString<>, a size parameter is required e.g. SmallString<32>, but I don't want to encode that into the function parameter
|
|
||
| std::optional<StringRef> | ||
| GlobPattern::asLiteral(SmallVectorImpl<char> &Storage) const { | ||
| // The prefix and suffix are metacharacter-free by construction, so whether |
There was a problem hiding this comment.
Perhaps we should bail out this case too?
if (SlashAgnostic)
return std::nullopt;
There was a problem hiding this comment.
Nice find! Just added the corner case
| // A pattern that denotes a single string is kept as a literal: literals are | ||
| // matched by hash lookup, and only literals seed the force-load of lazy | ||
| // archive members below. | ||
| SmallVector<char, 128> storage; |
There was a problem hiding this comment.
| SmallVector<char, 128> storage; | |
| SmallString<128> storage; |
|
Please let me know if there are more concerns that I haven't address, or is it good to merge? |
Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
🐧 Linux x64 Test Results
✅ The build succeeded and all tests passed. |
🪟 Windows x64 Test Results
✅ The build succeeded and all tests passed. |
|
LGTM from the lld/Mach-O side. The Objective-C direct-method case is a strong use case for identifying escaped glob patterns as literals. @MaskRay @vitalybuka Would appreciate a Support maintainer confirming the new API. |
Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
|
Thanks for the review, merging |
|
LLVM Buildbot has detected a new failure on builder Full details are available at: https://lab.llvm.org/buildbot/#/builders/225/builds/17068 Here is the relevant piece of the build log for the reference |
|
Failures in openmp doesn't look related to us. |
…ral (llvm#215854) lld determines if a `symbolName` is literal by `find_first_of("*?[]")`, which is not accurate and ignored escape characters, we do the following: 1. Add a new API `std::optional<std::string> asLiteral(...)` that dumps the pattern as a literal string if it is one. 2. All `symbolNames` are considered as `GlobPattern` first to leverage `asLeteral` to help us determine if a string is a literal. The quick path in during `GlobPattern` construction is `if (!S.find_first_of(PrefixMetas)) return Pat;`, so for strings without `*?[]` (most of the `symbolName`s), the performance should be about the same. 3. `symbolName`s that are literal go back to hashing like lld did before Added tests in lld and unit test for GlobPattern --------- Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
…ral (llvm#215854) lld determines if a `symbolName` is literal by `find_first_of("*?[]")`, which is not accurate and ignored escape characters, we do the following: 1. Add a new API `std::optional<std::string> asLiteral(...)` that dumps the pattern as a literal string if it is one. 2. All `symbolNames` are considered as `GlobPattern` first to leverage `asLeteral` to help us determine if a string is a literal. The quick path in during `GlobPattern` construction is `if (!S.find_first_of(PrefixMetas)) return Pat;`, so for strings without `*?[]` (most of the `symbolName`s), the performance should be about the same. 3. `symbolName`s that are literal go back to hashing like lld did before Added tests in lld and unit test for GlobPattern --------- Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
…ral (llvm#215854) lld determines if a `symbolName` is literal by `find_first_of("*?[]")`, which is not accurate and ignored escape characters, we do the following: 1. Add a new API `std::optional<std::string> asLiteral(...)` that dumps the pattern as a literal string if it is one. 2. All `symbolNames` are considered as `GlobPattern` first to leverage `asLeteral` to help us determine if a string is a literal. The quick path in during `GlobPattern` construction is `if (!S.find_first_of(PrefixMetas)) return Pat;`, so for strings without `*?[]` (most of the `symbolName`s), the performance should be about the same. 3. `symbolName`s that are literal go back to hashing like lld did before Added tests in lld and unit test for GlobPattern --------- Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
…ral (llvm#215854) lld determines if a `symbolName` is literal by `find_first_of("*?[]")`, which is not accurate and ignored escape characters, we do the following: 1. Add a new API `std::optional<std::string> asLiteral(...)` that dumps the pattern as a literal string if it is one. 2. All `symbolNames` are considered as `GlobPattern` first to leverage `asLeteral` to help us determine if a string is a literal. The quick path in during `GlobPattern` construction is `if (!S.find_first_of(PrefixMetas)) return Pat;`, so for strings without `*?[]` (most of the `symbolName`s), the performance should be about the same. 3. `symbolName`s that are literal go back to hashing like lld did before Added tests in lld and unit test for GlobPattern --------- Co-authored-by: Ellis Hoag <ellis.sparky.hoag@gmail.com>
lld determines if a
symbolNameis literal byfind_first_of("*?[]"), which is not accurate and ignored escape characters, we do the following:std::optional<StringRef> asLiteral(...)that dumps the pattern as a literal string if it is one.symbolNamesare considered asGlobPatternfirst to leverageasLeteralto help us determine if a string is a literal. The quick path in duringGlobPatternconstruction isif (!S.find_first_of(PrefixMetas)) return Pat;, so for strings without*?[](most of thesymbolNames), the performance should be about the same.symbolNames that are literal go back to hashing like lld did beforeAdded tests in lld and unit test for GlobPattern