From 9d39715f61f27462e0fd4c8553b95ddac1fcb190 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 4 Sep 2025 08:15:32 -0500 Subject: [PATCH 1/3] Add initial negation logic --- relay-pattern/src/lib.rs | 18 +++++++++++++++++- relay-pattern/src/wildmatch.rs | 12 +++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/relay-pattern/src/lib.rs b/relay-pattern/src/lib.rs index b89fc83a4ef..9d837357745 100644 --- a/relay-pattern/src/lib.rs +++ b/relay-pattern/src/lib.rs @@ -368,6 +368,8 @@ enum MatchStrategy { Static(bool), /// The pattern is complex and needs to be evaluated using [`wildmatch`]. Wildmatch(Tokens), + /// The pattern is complex and needs to be evaluated using [`wildmatch`]. + NegatedWildmatch(Tokens), // Possible future optimizations for `Any` variations: // Examples: `??`. `??suffix`, `prefix??` and `?contains?`. } @@ -384,6 +386,7 @@ impl MatchStrategy { [Token::Wildcard, Token::Literal(literal), Token::Wildcard] => { Self::Contains(std::mem::take(literal)) } + [Token::Negated, ..] => Self::NegatedWildmatch(tokens), _ => Self::Wildmatch(tokens), }; @@ -398,7 +401,12 @@ impl MatchStrategy { MatchStrategy::Suffix(suffix) => match_suffix(suffix, haystack, options), MatchStrategy::Contains(contains) => match_contains(contains, haystack, options), MatchStrategy::Static(matches) => *matches, - MatchStrategy::Wildmatch(tokens) => wildmatch::is_match(haystack, tokens, options), + MatchStrategy::Wildmatch(tokens) => { + wildmatch::is_match(haystack, tokens, options, false) + } + MatchStrategy::NegatedWildmatch(tokens) => { + wildmatch::is_match(haystack, tokens, options, true) + } } } } @@ -500,6 +508,10 @@ impl<'a> Parser<'a> { } fn parse(&mut self) -> Result<(), ErrorKind> { + if self.advance_if(|c| c == '!') { + self.push_token(Token::Negated); + }; + while let Some(c) = self.advance() { match c { '?' => self.push_token(Token::Any(NonZeroUsize::MIN)), @@ -671,6 +683,7 @@ impl<'a> Parser<'a> { /// - A [`Token::Any`] is never followed by [`Token::Any`]. /// - A [`Token::Literal`] is never followed by [`Token::Literal`]. /// - A [`Token::Class`] is never empty. +/// - A [`Token::Negated`] is always the first character in the string. #[derive(Clone, Debug, Default)] struct Tokens(Vec); @@ -761,6 +774,8 @@ enum Token { Any(NonZeroUsize), /// The wildcard token `*`. Wildcard, + /// The token `!`. + Negated, /// A class token `[abc]` or its negated variant `[!abc]`. Class { negated: bool, ranges: Ranges }, /// A list of nested alternate tokens `{a,b}`. @@ -960,6 +975,7 @@ mod tests { MatchStrategy::Contains(_) => "Contains", MatchStrategy::Static(_) => "Static", MatchStrategy::Wildmatch(_) => "Wildmatch", + MatchStrategy::NegatedWildmatch(_) => "NegatedWildmatch", }; assert_eq!( kind, diff --git a/relay-pattern/src/wildmatch.rs b/relay-pattern/src/wildmatch.rs index 6f3482db780..9443cbadf90 100644 --- a/relay-pattern/src/wildmatch.rs +++ b/relay-pattern/src/wildmatch.rs @@ -9,11 +9,12 @@ use crate::{Literal, Options, Ranges, Token, Tokens}; /// of the already pre-processed [`Tokens`] structure and its invariants. /// /// [Kirk J Krauss]: http://developforperformance.com/MatchingWildcards_AnImprovedAlgorithmForBigData.html -pub fn is_match(haystack: &str, tokens: &Tokens, options: Options) -> bool { - match options.case_insensitive { - false => is_match_impl::<_, CaseSensitive>(haystack, tokens.as_slice()), - true => is_match_impl::<_, CaseInsensitive>(haystack, tokens.as_slice()), - } +pub fn is_match(haystack: &str, tokens: &Tokens, options: Options, negated: bool) -> bool { + negated + ^ match options.case_insensitive { + false => is_match_impl::<_, CaseSensitive>(haystack, tokens.as_slice()), + true => is_match_impl::<_, CaseInsensitive>(haystack, tokens.as_slice()), + } } #[inline(always)] @@ -53,6 +54,7 @@ where t_next += 1; match token { + Token::Negated => true, Token::Literal(literal) => match M::is_prefix(h_current, literal) { Some(n) => advance!(n), // The literal does not match, but it may match after backtracking. From 3489ab669d3128b7ba041cfddef84d165fe8ec59 Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 4 Sep 2025 09:28:44 -0500 Subject: [PATCH 2/3] Add coverage for negation --- relay-pattern/src/lib.rs | 23 ++++++++++++++++++----- relay-pattern/src/wildmatch.rs | 11 +++++------ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/relay-pattern/src/lib.rs b/relay-pattern/src/lib.rs index 9d837357745..934817c5094 100644 --- a/relay-pattern/src/lib.rs +++ b/relay-pattern/src/lib.rs @@ -401,11 +401,9 @@ impl MatchStrategy { MatchStrategy::Suffix(suffix) => match_suffix(suffix, haystack, options), MatchStrategy::Contains(contains) => match_contains(contains, haystack, options), MatchStrategy::Static(matches) => *matches, - MatchStrategy::Wildmatch(tokens) => { - wildmatch::is_match(haystack, tokens, options, false) - } + MatchStrategy::Wildmatch(tokens) => wildmatch::is_match(haystack, tokens, options), MatchStrategy::NegatedWildmatch(tokens) => { - wildmatch::is_match(haystack, tokens, options, true) + !wildmatch::is_match(haystack, tokens, options) } } } @@ -1601,7 +1599,7 @@ mod tests { assert_pattern!("1.18.[!0-4].*", "1.18.5."); assert_pattern!("1.18.[!0-4].*", "1.18.5.aBc"); assert_pattern!("1.18.[!0-4].*", NOT "1.18.3.abc"); - assert_pattern!("!*!*.md", "!foo!.md"); // no `!` outside of character classes + assert_pattern!("*!*.md", "foo!.md"); // no `!` outside of character classes assert_pattern!("foo*foofoo*foobar", "foofoofooxfoofoobar"); assert_pattern!("foo*fooFOO*fOobar", "fooFoofooXfoofooBAR", i); assert_pattern!("[0-9]*a", "0aaaaaaaaa", i); @@ -1952,4 +1950,19 @@ mod tests { assert!(!patterns.is_match("foo")); assert!(patterns.is_match("bar")); } + + #[test] + fn test_pattern_negation() { + let patterns = Patterns::builder().add("!foo@*").unwrap().take(); + + assert!(patterns.is_match("bar@1.0")); + assert!(patterns.is_match("foobar@1.0")); + assert!(patterns.is_match("foo")); + assert!(patterns.is_match("barfoo@")); + + // foo@ is never matched. + assert!(!patterns.is_match("foo@1.0")); + assert!(!patterns.is_match("foo@2.2.3")); + assert!(!patterns.is_match("foo@anything")); + } } diff --git a/relay-pattern/src/wildmatch.rs b/relay-pattern/src/wildmatch.rs index 9443cbadf90..c59a122cfb4 100644 --- a/relay-pattern/src/wildmatch.rs +++ b/relay-pattern/src/wildmatch.rs @@ -9,12 +9,11 @@ use crate::{Literal, Options, Ranges, Token, Tokens}; /// of the already pre-processed [`Tokens`] structure and its invariants. /// /// [Kirk J Krauss]: http://developforperformance.com/MatchingWildcards_AnImprovedAlgorithmForBigData.html -pub fn is_match(haystack: &str, tokens: &Tokens, options: Options, negated: bool) -> bool { - negated - ^ match options.case_insensitive { - false => is_match_impl::<_, CaseSensitive>(haystack, tokens.as_slice()), - true => is_match_impl::<_, CaseInsensitive>(haystack, tokens.as_slice()), - } +pub fn is_match(haystack: &str, tokens: &Tokens, options: Options) -> bool { + match options.case_insensitive { + false => is_match_impl::<_, CaseSensitive>(haystack, tokens.as_slice()), + true => is_match_impl::<_, CaseInsensitive>(haystack, tokens.as_slice()), + } } #[inline(always)] From 7080fd11b4d40c5a2b3a3b862ea6ddc057db6acf Mon Sep 17 00:00:00 2001 From: Colton Allen Date: Thu, 4 Sep 2025 09:32:55 -0500 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af13a4f37da..53feb60e9ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Add InstallableBuild and SizeAnalysis data categories. ([#5084](https://github.com/getsentry/relay/pull/5084)) - Add dynamic PII derivation to `metastructure`. ([#5107](https://github.com/getsentry/relay/pull/5107)) +- Add negation pattern matching. ([#5116](https://github.com/getsentry/relay/pull/5116)) **Internal**: