Skip to main content

saf_core/spec/
pattern.rs

1//! Name pattern matching for function specifications.
2//!
3//! Supports three matching modes:
4//! - Exact match (default): `name: malloc`
5//! - Glob pattern: `name: "glob:str*"`
6//! - Regex pattern: `name: "regex:^mem(cpy|set|move)$"`
7
8use regex::Regex;
9use std::fmt;
10
11/// A name pattern for matching function names.
12#[derive(Debug, Clone)]
13pub enum NamePattern {
14    /// Exact string match.
15    Exact(String),
16    /// Glob pattern (using `glob` crate syntax).
17    Glob(glob::Pattern),
18    /// Regular expression pattern.
19    Regex(Regex),
20}
21
22impl NamePattern {
23    /// Parse a name string into a pattern.
24    ///
25    /// - `glob:pattern` → Glob pattern
26    /// - `regex:pattern` → Regex pattern
27    /// - anything else → Exact match
28    ///
29    /// # Errors
30    /// Returns an error if glob or regex pattern is invalid.
31    pub fn parse(name: &str) -> Result<Self, PatternError> {
32        if let Some(pattern) = name.strip_prefix("glob:") {
33            let glob_pattern = glob::Pattern::new(pattern).map_err(|e| PatternError::Glob {
34                pattern: pattern.to_string(),
35                message: e.to_string(),
36            })?;
37            Ok(Self::Glob(glob_pattern))
38        } else if let Some(pattern) = name.strip_prefix("regex:") {
39            let regex = Regex::new(pattern).map_err(|e| PatternError::Regex {
40                pattern: pattern.to_string(),
41                message: e.to_string(),
42            })?;
43            Ok(Self::Regex(regex))
44        } else {
45            Ok(Self::Exact(name.to_string()))
46        }
47    }
48
49    /// Check if this pattern matches the given function name.
50    #[must_use]
51    pub fn matches(&self, name: &str) -> bool {
52        match self {
53            Self::Exact(exact) => exact == name,
54            Self::Glob(pattern) => pattern.matches(name),
55            Self::Regex(regex) => regex.is_match(name),
56        }
57    }
58
59    /// Check if this is an exact match pattern.
60    #[must_use]
61    pub fn is_exact(&self) -> bool {
62        matches!(self, Self::Exact(_))
63    }
64
65    /// Get the original pattern string (for display).
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        match self {
69            Self::Exact(s) => s,
70            Self::Glob(p) => p.as_str(),
71            Self::Regex(r) => r.as_str(),
72        }
73    }
74}
75
76impl PartialEq for NamePattern {
77    fn eq(&self, other: &Self) -> bool {
78        match (self, other) {
79            (Self::Exact(a), Self::Exact(b)) => a == b,
80            (Self::Glob(a), Self::Glob(b)) => a.as_str() == b.as_str(),
81            (Self::Regex(a), Self::Regex(b)) => a.as_str() == b.as_str(),
82            _ => false,
83        }
84    }
85}
86
87impl fmt::Display for NamePattern {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Self::Exact(s) => write!(f, "{s}"),
91            Self::Glob(p) => write!(f, "glob:{}", p.as_str()),
92            Self::Regex(r) => write!(f, "regex:{}", r.as_str()),
93        }
94    }
95}
96
97/// Errors that can occur when parsing patterns.
98#[derive(Debug, Clone, thiserror::Error)]
99pub enum PatternError {
100    /// Invalid glob pattern.
101    #[error("invalid glob pattern '{pattern}': {message}")]
102    Glob {
103        /// The pattern that failed to parse.
104        pattern: String,
105        /// Error message from glob parser.
106        message: String,
107    },
108    /// Invalid regex pattern.
109    #[error("invalid regex pattern '{pattern}': {message}")]
110    Regex {
111        /// The pattern that failed to parse.
112        pattern: String,
113        /// Error message from regex parser.
114        message: String,
115    },
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_exact_match() {
124        let pattern = NamePattern::parse("malloc").unwrap();
125        assert!(pattern.is_exact());
126        assert!(pattern.matches("malloc"));
127        assert!(!pattern.matches("calloc"));
128        assert!(!pattern.matches("malloc_usable_size"));
129    }
130
131    #[test]
132    fn test_glob_prefix_match() {
133        let pattern = NamePattern::parse("glob:str*").unwrap();
134        assert!(!pattern.is_exact());
135        assert!(pattern.matches("strlen"));
136        assert!(pattern.matches("strcpy"));
137        assert!(pattern.matches("strcat"));
138        assert!(!pattern.matches("malloc"));
139    }
140
141    #[test]
142    fn test_glob_suffix_match() {
143        let pattern = NamePattern::parse("glob:*alloc").unwrap();
144        assert!(pattern.matches("malloc"));
145        assert!(pattern.matches("calloc"));
146        assert!(pattern.matches("realloc"));
147        assert!(!pattern.matches("free"));
148    }
149
150    #[test]
151    fn test_glob_complex() {
152        let pattern = NamePattern::parse("glob:mem[cs]*").unwrap();
153        assert!(pattern.matches("memcpy"));
154        assert!(pattern.matches("memset"));
155        assert!(pattern.matches("memcmp"));
156        assert!(!pattern.matches("memmove"));
157    }
158
159    #[test]
160    fn test_regex_alternation() {
161        let pattern = NamePattern::parse("regex:^mem(cpy|set|move)$").unwrap();
162        assert!(pattern.matches("memcpy"));
163        assert!(pattern.matches("memset"));
164        assert!(pattern.matches("memmove"));
165        assert!(!pattern.matches("memcmp"));
166        assert!(!pattern.matches("memory"));
167    }
168
169    #[test]
170    fn test_regex_prefix() {
171        let pattern = NamePattern::parse("regex:^pthread_").unwrap();
172        assert!(pattern.matches("pthread_create"));
173        assert!(pattern.matches("pthread_join"));
174        assert!(pattern.matches("pthread_mutex_lock"));
175        assert!(!pattern.matches("create_pthread"));
176    }
177
178    #[test]
179    fn test_invalid_glob() {
180        let result = NamePattern::parse("glob:[invalid");
181        assert!(result.is_err());
182        let err = result.unwrap_err();
183        assert!(matches!(err, PatternError::Glob { .. }));
184    }
185
186    #[test]
187    fn test_invalid_regex() {
188        let result = NamePattern::parse("regex:[invalid");
189        assert!(result.is_err());
190        let err = result.unwrap_err();
191        assert!(matches!(err, PatternError::Regex { .. }));
192    }
193
194    #[test]
195    fn test_pattern_display() {
196        assert_eq!(NamePattern::parse("malloc").unwrap().to_string(), "malloc");
197        assert_eq!(
198            NamePattern::parse("glob:str*").unwrap().to_string(),
199            "glob:str*"
200        );
201        assert_eq!(
202            NamePattern::parse("regex:^mem").unwrap().to_string(),
203            "regex:^mem"
204        );
205    }
206
207    #[test]
208    fn test_pattern_equality() {
209        let p1 = NamePattern::parse("malloc").unwrap();
210        let p2 = NamePattern::parse("malloc").unwrap();
211        let p3 = NamePattern::parse("calloc").unwrap();
212        assert_eq!(p1, p2);
213        assert_ne!(p1, p3);
214
215        let g1 = NamePattern::parse("glob:str*").unwrap();
216        let g2 = NamePattern::parse("glob:str*").unwrap();
217        let g3 = NamePattern::parse("glob:mem*").unwrap();
218        assert_eq!(g1, g2);
219        assert_ne!(g1, g3);
220    }
221}