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**: diff --git a/relay-pattern/src/lib.rs b/relay-pattern/src/lib.rs index b89fc83a4ef..934817c5094 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), }; @@ -399,6 +402,9 @@ impl MatchStrategy { MatchStrategy::Contains(contains) => match_contains(contains, haystack, options), MatchStrategy::Static(matches) => *matches, MatchStrategy::Wildmatch(tokens) => wildmatch::is_match(haystack, tokens, options), + MatchStrategy::NegatedWildmatch(tokens) => { + !wildmatch::is_match(haystack, tokens, options) + } } } } @@ -500,6 +506,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 +681,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 +772,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 +973,7 @@ mod tests { MatchStrategy::Contains(_) => "Contains", MatchStrategy::Static(_) => "Static", MatchStrategy::Wildmatch(_) => "Wildmatch", + MatchStrategy::NegatedWildmatch(_) => "NegatedWildmatch", }; assert_eq!( kind, @@ -1585,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); @@ -1936,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 6f3482db780..c59a122cfb4 100644 --- a/relay-pattern/src/wildmatch.rs +++ b/relay-pattern/src/wildmatch.rs @@ -53,6 +53,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.