Skip to main content

profile_bee/
probe_spec.rs

1//! Probe specification parser for GDB-style uprobe targeting.
2//!
3//! Supports a unified spec syntax:
4//!   malloc                        - auto-discover which library has it
5//!   libc:malloc                   - explicit library prefix
6//!   /usr/lib/libc.so.6:malloc     - explicit absolute path
7//!   malloc+0x10                   - function + offset
8//!   ret:malloc                    - uretprobe (return probe)
9//!   file.c:42                     - source file + line number (needs DWARF)
10//!   MyClass::method               - C++ mangled name matching
11//!   pthread_*                     - glob pattern matching
12//!   /regex_pattern/               - regex matching
13
14use std::fmt;
15
16use regex::Regex;
17
18/// A parsed probe specification.
19#[derive(Debug, Clone)]
20pub enum ProbeSpec {
21    /// Match by symbol name pattern (exact, glob, regex, or demangled).
22    Symbol {
23        /// Optional library constraint ("libc", "/usr/lib/foo.so", etc.).
24        /// None means auto-discover across all loaded libraries.
25        library: Option<String>,
26        /// The pattern to match symbol names against.
27        pattern: SymbolPattern,
28        /// Byte offset within the matched function.
29        offset: u64,
30        /// Whether this is a return probe (uretprobe).
31        is_ret: bool,
32    },
33    /// Match by source file and line number (requires DWARF debug info).
34    SourceLocation {
35        /// Source file name or path (e.g. "main.c", "src/lib.rs").
36        file: String,
37        /// Line number in the source file.
38        line: u32,
39        /// Whether this is a return probe (uretprobe).
40        is_ret: bool,
41    },
42}
43
44/// Pattern types for matching symbol names.
45#[derive(Debug, Clone)]
46pub enum SymbolPattern {
47    /// Exact symbol name match (e.g. "malloc").
48    Exact(String),
49    /// Glob pattern match (e.g. "pthread_*", "sql_?uery").
50    Glob(String),
51    /// Regex match (e.g. "/^sql_.*query/").
52    Regex(RegexWrapper),
53    /// Demangled name match -- demangles each symbol and matches against this string.
54    /// Triggered by C++ `::` separator (e.g. "MyClass::method", "std::vector::push_back").
55    Demangled(String),
56}
57
58/// Wrapper around regex::Regex to implement Debug + Clone.
59#[derive(Clone)]
60pub struct RegexWrapper {
61    pub regex: Regex,
62    pub source: String,
63}
64
65impl fmt::Debug for RegexWrapper {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        write!(f, "Regex({})", self.source)
68    }
69}
70
71impl SymbolPattern {
72    /// Test whether a raw (mangled) symbol name matches this pattern.
73    pub fn matches(&self, symbol_name: &str) -> bool {
74        match self {
75            SymbolPattern::Exact(name) => symbol_name == name,
76            SymbolPattern::Glob(pattern) => glob_match(pattern, symbol_name),
77            SymbolPattern::Regex(rw) => rw.regex.is_match(symbol_name),
78            // For demangled patterns, the caller should demangle first then call matches_demangled.
79            SymbolPattern::Demangled(_) => false,
80        }
81    }
82
83    /// Test whether a demangled symbol name matches this pattern.
84    /// For Demangled patterns, checks if the demangled name contains the query.
85    /// For other patterns, delegates to normal matching against the demangled name.
86    pub fn matches_demangled(&self, demangled_name: &str) -> bool {
87        match self {
88            SymbolPattern::Demangled(query) => {
89                // Support both exact match and substring match on demangled names.
90                // e.g. "MyClass::method" matches "MyNamespace::MyClass::method(int)"
91                demangled_name.contains(query.as_str())
92            }
93            // For non-demangled patterns, also try matching against the demangled form
94            other => other.matches(demangled_name),
95        }
96    }
97
98    /// Returns true if this is a single-match pattern (Exact), meaning it can
99    /// potentially be resolved by Aya's built-in symbol resolution without
100    /// scanning all ELFs ourselves.
101    pub fn is_exact(&self) -> bool {
102        matches!(self, SymbolPattern::Exact(_))
103    }
104
105    /// Return the pattern string for display purposes.
106    pub fn display_str(&self) -> &str {
107        match self {
108            SymbolPattern::Exact(s) => s,
109            SymbolPattern::Glob(s) => s,
110            SymbolPattern::Regex(rw) => &rw.source,
111            SymbolPattern::Demangled(s) => s,
112        }
113    }
114}
115
116/// Parse a probe specification string into a `ProbeSpec`.
117///
118/// Grammar:
119///   spec          = ["ret:"] [library ":"] pattern ["+" offset]
120///   spec          = ["ret:"] file ":" line_number
121///   library       = absolute_path | library_name
122///   pattern       = "/" regex "/" | glob_pattern | demangled_pattern | exact_name
123///   offset        = hex_or_decimal_number
124///
125/// Examples:
126///   "malloc"                      -> Symbol { library: None, pattern: Exact("malloc"), ... }
127///   "libc:malloc"                 -> Symbol { library: Some("libc"), pattern: Exact("malloc"), ... }
128///   "ret:malloc"                  -> Symbol { is_ret: true, ... }
129///   "pthread_*"                   -> Symbol { pattern: Glob("pthread_*"), ... }
130///   "/sql_.*query/"               -> Symbol { pattern: Regex(...), ... }
131///   "std::vector::push_back"      -> Symbol { pattern: Demangled("std::vector::push_back"), ... }
132///   "main.c:42"                   -> SourceLocation { file: "main.c", line: 42 }
133///   "/usr/lib/libc.so.6:malloc"   -> Symbol { library: Some("/usr/lib/libc.so.6"), ... }
134pub fn parse_probe_spec(input: &str) -> Result<ProbeSpec, String> {
135    let input = input.trim();
136    if input.is_empty() {
137        return Err("empty probe specification".to_string());
138    }
139
140    // Step 1: Strip "ret:" prefix
141    let (is_ret, rest) = if let Some(stripped) = input.strip_prefix("ret:") {
142        (true, stripped)
143    } else {
144        (false, input)
145    };
146
147    // Step 2: Check for bare regex pattern /pattern/ (no library prefix).
148    // We distinguish from absolute paths: absolute paths like /usr/lib/...
149    // contain slashes within the regex_str portion, so try_parse_regex_symbol
150    // will return None for those.
151    if rest.starts_with('/') && !rest.contains(':') {
152        if let Some((regex_wrapper, offset)) = try_parse_regex_symbol(rest)? {
153            return Ok(ProbeSpec::Symbol {
154                library: None,
155                pattern: SymbolPattern::Regex(regex_wrapper),
156                offset,
157                is_ret,
158            });
159        }
160        // If not a valid regex, fall through — it's an absolute path
161    }
162
163    // Step 3: Split on colon to detect library:symbol or file:line
164    // But be careful: C++ demangled names contain "::" which is NOT a library separator.
165    // Also, absolute paths start with "/" and contain colons only as library:symbol separator.
166    let (library, symbol_part) = split_library_and_symbol(rest)?;
167
168    // Step 4: Check if this is a source location (file:line where line is numeric)
169    // This only applies when we got a colon split and the "symbol" part is purely numeric.
170    if let Some(ref lib) = library {
171        if let Ok(line) = symbol_part.parse::<u32>() {
172            // Looks like file.c:42
173            // Heuristic: if the "library" part looks like a source file
174            // (has a file extension or is a relative path), treat as source location.
175            if looks_like_source_file(lib) {
176                return Ok(ProbeSpec::SourceLocation {
177                    file: lib.clone(),
178                    line,
179                    is_ret,
180                });
181            }
182        }
183    }
184
185    // Step 5: Check if symbol part is a regex /pattern/ (handles library:/regex/ case)
186    if let Some(regex_result) = try_parse_regex_symbol(&symbol_part)? {
187        let (regex_wrapper, offset) = regex_result;
188        return Ok(ProbeSpec::Symbol {
189            library,
190            pattern: SymbolPattern::Regex(regex_wrapper),
191            offset,
192            is_ret,
193        });
194    }
195
196    // Step 6: Parse offset from symbol part
197    let (symbol_name, offset) = split_symbol_and_offset(&symbol_part)?;
198
199    // Step 7: Determine pattern type
200    let pattern = classify_pattern(&symbol_name);
201
202    Ok(ProbeSpec::Symbol {
203        library,
204        pattern,
205        offset,
206        is_ret,
207    })
208}
209
210/// Split a spec string into optional library prefix and symbol/pattern part.
211///
212/// Handles:
213///   "malloc"                    -> (None, "malloc")
214///   "libc:malloc"               -> (Some("libc"), "malloc")
215///   "/usr/lib/libc.so.6:malloc" -> (Some("/usr/lib/libc.so.6"), "malloc")
216///   "std::vector::push_back"    -> (None, "std::vector::push_back")  [C++ demangled]
217///   "main.c:42"                 -> (Some("main.c"), "42")
218fn split_library_and_symbol(s: &str) -> Result<(Option<String>, String), String> {
219    // If it starts with "/" and doesn't end with "/", it could be an absolute path prefix.
220    // Find the LAST ":" that isn't part of "::"
221    // e.g. "/usr/lib/libc.so.6:malloc" -> split at the last ":"
222    //      "std::vector::push_back"     -> no split (all colons are part of "::")
223
224    // Simple approach: find all single-colon positions (not part of "::")
225    let bytes = s.as_bytes();
226    let mut single_colon_positions = Vec::new();
227
228    let mut i = 0;
229    while i < bytes.len() {
230        if bytes[i] == b':' {
231            // Check if this is part of "::"
232            let is_double =
233                (i + 1 < bytes.len() && bytes[i + 1] == b':') || (i > 0 && bytes[i - 1] == b':');
234            if !is_double {
235                single_colon_positions.push(i);
236            } else {
237                // Skip the next ':' if this is the first of a "::" pair
238                if i + 1 < bytes.len() && bytes[i + 1] == b':' {
239                    i += 1;
240                }
241            }
242        }
243        i += 1;
244    }
245
246    if single_colon_positions.is_empty() {
247        // No library prefix
248        Ok((None, s.to_string()))
249    } else {
250        // Use the LAST single colon as the separator.
251        // This handles "/usr/lib/x86_64-linux-gnu/libc.so.6:malloc" correctly
252        // even if there were somehow other colons in the path.
253        let pos = *single_colon_positions.last().unwrap();
254        let library = &s[..pos];
255        let symbol = &s[pos + 1..];
256        if symbol.is_empty() {
257            return Err(format!("empty symbol after library prefix '{}'", library));
258        }
259        Ok((Some(library.to_string()), symbol.to_string()))
260    }
261}
262
263/// Split "function_name+0x10" or "function_name+16" into (name, offset).
264fn split_symbol_and_offset(s: &str) -> Result<(String, u64), String> {
265    if let Some(plus_pos) = s.rfind('+') {
266        let name = &s[..plus_pos];
267        let offset_str = &s[plus_pos + 1..];
268
269        // Guard: don't split on "+" inside C++ operator names like "operator+"
270        if name.ends_with("operator") {
271            return Ok((s.to_string(), 0));
272        }
273
274        let offset = if let Some(hex) = offset_str
275            .strip_prefix("0x")
276            .or_else(|| offset_str.strip_prefix("0X"))
277        {
278            u64::from_str_radix(hex, 16)
279                .map_err(|e| format!("invalid hex offset '{}': {}", offset_str, e))?
280        } else {
281            offset_str
282                .parse::<u64>()
283                .map_err(|e| format!("invalid offset '{}': {}", offset_str, e))?
284        };
285
286        Ok((name.to_string(), offset))
287    } else {
288        Ok((s.to_string(), 0))
289    }
290}
291
292/// Parse optional "+offset" from the end of a string (used after regex patterns).
293fn parse_trailing_offset(s: &str) -> Result<u64, String> {
294    let s = s.trim();
295    if s.is_empty() {
296        return Ok(0);
297    }
298    if let Some(rest) = s.strip_prefix('+') {
299        if let Some(hex) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
300            u64::from_str_radix(hex, 16)
301                .map_err(|e| format!("invalid hex offset '{}': {}", rest, e))
302        } else {
303            rest.parse::<u64>()
304                .map_err(|e| format!("invalid offset '{}': {}", rest, e))
305        }
306    } else {
307        Err(format!("unexpected trailing text: '{}'", s))
308    }
309}
310
311/// Try to parse a symbol string as a `/regex/` pattern (with optional trailing +offset).
312/// Returns `Some((RegexWrapper, offset))` if it matches, `None` if not regex syntax.
313fn try_parse_regex_symbol(s: &str) -> Result<Option<(RegexWrapper, u64)>, String> {
314    if !s.starts_with('/') || s.len() < 3 {
315        return Ok(None);
316    }
317
318    // Find the closing '/' — must not be the first character
319    let last_slash = match s[1..].rfind('/') {
320        Some(pos) => pos + 1, // offset back into full string
321        None => return Ok(None),
322    };
323
324    if last_slash == 0 {
325        return Ok(None);
326    }
327
328    let regex_str = &s[1..last_slash];
329    let after_regex = &s[last_slash + 1..];
330
331    if regex_str.is_empty() {
332        return Ok(None);
333    }
334
335    let offset = parse_trailing_offset(after_regex)?;
336
337    let regex =
338        Regex::new(regex_str).map_err(|e| format!("invalid regex '{}': {}", regex_str, e))?;
339
340    Ok(Some((
341        RegexWrapper {
342            regex,
343            source: regex_str.to_string(),
344        },
345        offset,
346    )))
347}
348
349/// Classify a symbol name string into the appropriate SymbolPattern variant.
350fn classify_pattern(name: &str) -> SymbolPattern {
351    if name.contains("::") {
352        // C++ or Rust demangled name
353        SymbolPattern::Demangled(name.to_string())
354    } else if name.contains('*') || name.contains('?') || name.contains('[') {
355        // Glob pattern
356        SymbolPattern::Glob(name.to_string())
357    } else {
358        // Exact match
359        SymbolPattern::Exact(name.to_string())
360    }
361}
362
363/// Heuristic: does this string look like a source file name?
364fn looks_like_source_file(s: &str) -> bool {
365    let extensions = [
366        ".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".hxx", ".rs", ".go", ".java", ".py", ".rb",
367        ".js", ".ts", ".S", ".s", ".asm",
368    ];
369    extensions.iter().any(|ext| s.ends_with(ext))
370}
371
372/// Simple glob matching supporting `*`, `?`, and `[...]` character classes.
373fn glob_match(pattern: &str, text: &str) -> bool {
374    glob_match_recursive(pattern.as_bytes(), text.as_bytes())
375}
376
377fn glob_match_recursive(pattern: &[u8], text: &[u8]) -> bool {
378    let mut pi = 0;
379    let mut ti = 0;
380    let mut star_pi = usize::MAX;
381    let mut star_ti = 0;
382
383    while ti < text.len() {
384        if pi < pattern.len() && pattern[pi] == b'?' {
385            pi += 1;
386            ti += 1;
387        } else if pi < pattern.len() && pattern[pi] == b'*' {
388            star_pi = pi;
389            star_ti = ti;
390            pi += 1;
391        } else if pi < pattern.len() && pattern[pi] == b'[' {
392            // Character class
393            if let Some((matched, end)) = match_char_class(&pattern[pi..], text[ti]) {
394                if matched {
395                    pi += end;
396                    ti += 1;
397                } else if star_pi != usize::MAX {
398                    pi = star_pi + 1;
399                    star_ti += 1;
400                    ti = star_ti;
401                } else {
402                    return false;
403                }
404            } else {
405                // Malformed character class, treat as literal
406                if pattern[pi] == text[ti] {
407                    pi += 1;
408                    ti += 1;
409                } else if star_pi != usize::MAX {
410                    pi = star_pi + 1;
411                    star_ti += 1;
412                    ti = star_ti;
413                } else {
414                    return false;
415                }
416            }
417        } else if pi < pattern.len() && pattern[pi] == text[ti] {
418            pi += 1;
419            ti += 1;
420        } else if star_pi != usize::MAX {
421            pi = star_pi + 1;
422            star_ti += 1;
423            ti = star_ti;
424        } else {
425            return false;
426        }
427    }
428
429    // Consume trailing *
430    while pi < pattern.len() && pattern[pi] == b'*' {
431        pi += 1;
432    }
433
434    pi == pattern.len()
435}
436
437/// Match a character class pattern like [abc], [a-z], [^abc].
438/// Returns (matched, bytes_consumed_in_pattern) or None if malformed.
439fn match_char_class(pattern: &[u8], ch: u8) -> Option<(bool, usize)> {
440    if pattern.is_empty() || pattern[0] != b'[' {
441        return None;
442    }
443
444    let mut i = 1;
445    let negate = if i < pattern.len() && (pattern[i] == b'^' || pattern[i] == b'!') {
446        i += 1;
447        true
448    } else {
449        false
450    };
451
452    let mut matched = false;
453    while i < pattern.len() && pattern[i] != b']' {
454        if i + 2 < pattern.len() && pattern[i + 1] == b'-' {
455            // Range like a-z
456            if ch >= pattern[i] && ch <= pattern[i + 2] {
457                matched = true;
458            }
459            i += 3;
460        } else {
461            if ch == pattern[i] {
462                matched = true;
463            }
464            i += 1;
465        }
466    }
467
468    if i < pattern.len() && pattern[i] == b']' {
469        Some((matched ^ negate, i + 1))
470    } else {
471        None // No closing bracket
472    }
473}
474
475impl fmt::Display for ProbeSpec {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        match self {
478            ProbeSpec::Symbol {
479                library,
480                pattern,
481                offset,
482                is_ret,
483            } => {
484                if *is_ret {
485                    write!(f, "ret:")?;
486                }
487                if let Some(lib) = library {
488                    write!(f, "{}:", lib)?;
489                }
490                write!(f, "{}", pattern.display_str())?;
491                if *offset > 0 {
492                    write!(f, "+0x{:x}", offset)?;
493                }
494            }
495            ProbeSpec::SourceLocation { file, line, is_ret } => {
496                if *is_ret {
497                    write!(f, "ret:")?;
498                }
499                write!(f, "{}:{}", file, line)?;
500            }
501        }
502        Ok(())
503    }
504}
505
506#[cfg(test)]
507mod tests {
508    use super::*;
509
510    #[test]
511    fn test_exact_symbol() {
512        let spec = parse_probe_spec("malloc").unwrap();
513        match spec {
514            ProbeSpec::Symbol {
515                library,
516                pattern,
517                offset,
518                is_ret,
519            } => {
520                assert!(library.is_none());
521                assert!(matches!(pattern, SymbolPattern::Exact(ref s) if s == "malloc"));
522                assert_eq!(offset, 0);
523                assert!(!is_ret);
524            }
525            _ => panic!("expected Symbol"),
526        }
527    }
528
529    #[test]
530    fn test_library_prefix() {
531        let spec = parse_probe_spec("libc:malloc").unwrap();
532        match spec {
533            ProbeSpec::Symbol {
534                library, pattern, ..
535            } => {
536                assert_eq!(library, Some("libc".to_string()));
537                assert!(matches!(pattern, SymbolPattern::Exact(ref s) if s == "malloc"));
538            }
539            _ => panic!("expected Symbol"),
540        }
541    }
542
543    #[test]
544    fn test_absolute_path_prefix() {
545        let spec = parse_probe_spec("/usr/lib/libc.so.6:malloc").unwrap();
546        match spec {
547            ProbeSpec::Symbol {
548                library, pattern, ..
549            } => {
550                assert_eq!(library, Some("/usr/lib/libc.so.6".to_string()));
551                assert!(matches!(pattern, SymbolPattern::Exact(ref s) if s == "malloc"));
552            }
553            _ => panic!("expected Symbol"),
554        }
555    }
556
557    #[test]
558    fn test_ret_prefix() {
559        let spec = parse_probe_spec("ret:malloc").unwrap();
560        match spec {
561            ProbeSpec::Symbol { is_ret, .. } => {
562                assert!(is_ret);
563            }
564            _ => panic!("expected Symbol"),
565        }
566    }
567
568    #[test]
569    fn test_offset_decimal() {
570        let spec = parse_probe_spec("malloc+16").unwrap();
571        match spec {
572            ProbeSpec::Symbol { offset, .. } => {
573                assert_eq!(offset, 16);
574            }
575            _ => panic!("expected Symbol"),
576        }
577    }
578
579    #[test]
580    fn test_offset_hex() {
581        let spec = parse_probe_spec("malloc+0x10").unwrap();
582        match spec {
583            ProbeSpec::Symbol { offset, .. } => {
584                assert_eq!(offset, 0x10);
585            }
586            _ => panic!("expected Symbol"),
587        }
588    }
589
590    #[test]
591    fn test_glob_pattern() {
592        let spec = parse_probe_spec("pthread_*").unwrap();
593        match spec {
594            ProbeSpec::Symbol { pattern, .. } => {
595                assert!(matches!(pattern, SymbolPattern::Glob(ref s) if s == "pthread_*"));
596            }
597            _ => panic!("expected Symbol"),
598        }
599    }
600
601    #[test]
602    fn test_regex_pattern() {
603        let spec = parse_probe_spec("/^sql_.*query/").unwrap();
604        match spec {
605            ProbeSpec::Symbol { pattern, .. } => {
606                assert!(matches!(pattern, SymbolPattern::Regex(_)));
607            }
608            _ => panic!("expected Symbol"),
609        }
610    }
611
612    #[test]
613    fn test_demangled_pattern() {
614        let spec = parse_probe_spec("std::vector::push_back").unwrap();
615        match spec {
616            ProbeSpec::Symbol { pattern, .. } => {
617                assert!(
618                    matches!(pattern, SymbolPattern::Demangled(ref s) if s == "std::vector::push_back")
619                );
620            }
621            _ => panic!("expected Symbol"),
622        }
623    }
624
625    #[test]
626    fn test_source_location() {
627        let spec = parse_probe_spec("main.c:42").unwrap();
628        match spec {
629            ProbeSpec::SourceLocation { file, line, is_ret } => {
630                assert_eq!(file, "main.c");
631                assert_eq!(line, 42);
632                assert!(!is_ret);
633            }
634            _ => panic!("expected SourceLocation"),
635        }
636    }
637
638    #[test]
639    fn test_source_location_with_ret() {
640        let spec = parse_probe_spec("ret:main.c:42").unwrap();
641        match spec {
642            ProbeSpec::SourceLocation { file, line, is_ret } => {
643                assert_eq!(file, "main.c");
644                assert_eq!(line, 42);
645                assert!(is_ret);
646            }
647            _ => panic!("expected SourceLocation"),
648        }
649    }
650
651    #[test]
652    fn test_combined_library_ret_offset() {
653        let spec = parse_probe_spec("ret:libc:malloc+0x10").unwrap();
654        match spec {
655            ProbeSpec::Symbol {
656                library,
657                pattern,
658                offset,
659                is_ret,
660            } => {
661                assert_eq!(library, Some("libc".to_string()));
662                assert!(matches!(pattern, SymbolPattern::Exact(ref s) if s == "malloc"));
663                assert_eq!(offset, 0x10);
664                assert!(is_ret);
665            }
666            _ => panic!("expected Symbol"),
667        }
668    }
669
670    #[test]
671    fn test_glob_matching() {
672        assert!(glob_match("pthread_*", "pthread_create"));
673        assert!(glob_match("pthread_*", "pthread_mutex_lock"));
674        assert!(!glob_match("pthread_*", "malloc"));
675        assert!(glob_match("*alloc*", "malloc"));
676        assert!(glob_match("*alloc*", "calloc"));
677        assert!(glob_match("*alloc*", "realloc"));
678        assert!(glob_match("sql_?uery", "sql_query"));
679        assert!(!glob_match("sql_?uery", "sql_xquery"));
680    }
681
682    #[test]
683    fn test_demangled_matching() {
684        let pattern = SymbolPattern::Demangled("MyClass::method".to_string());
685        assert!(pattern.matches_demangled("namespace::MyClass::method(int, float)"));
686        assert!(pattern.matches_demangled("MyClass::method()"));
687        assert!(!pattern.matches_demangled("OtherClass::method()"));
688    }
689
690    #[test]
691    fn test_empty_spec() {
692        assert!(parse_probe_spec("").is_err());
693        assert!(parse_probe_spec("  ").is_err());
694    }
695
696    #[test]
697    fn test_display() {
698        assert_eq!(parse_probe_spec("malloc").unwrap().to_string(), "malloc");
699        assert_eq!(
700            parse_probe_spec("ret:libc:malloc+0x10")
701                .unwrap()
702                .to_string(),
703            "ret:libc:malloc+0x10"
704        );
705        assert_eq!(
706            parse_probe_spec("main.c:42").unwrap().to_string(),
707            "main.c:42"
708        );
709    }
710
711    #[test]
712    fn test_regex_with_library_prefix() {
713        let spec = parse_probe_spec("libc:/malloc.*/").unwrap();
714        match spec {
715            ProbeSpec::Symbol {
716                library,
717                pattern,
718                offset,
719                is_ret,
720            } => {
721                assert_eq!(library, Some("libc".to_string()));
722                assert!(matches!(pattern, SymbolPattern::Regex(ref rw) if rw.source == "malloc.*"));
723                assert_eq!(offset, 0);
724                assert!(!is_ret);
725            }
726            _ => panic!("expected Symbol with Regex pattern"),
727        }
728    }
729
730    #[test]
731    fn test_regex_with_library_prefix_and_offset() {
732        let spec = parse_probe_spec("libc:/^mem.*/+0x10").unwrap();
733        match spec {
734            ProbeSpec::Symbol {
735                library,
736                pattern,
737                offset,
738                ..
739            } => {
740                assert_eq!(library, Some("libc".to_string()));
741                assert!(matches!(pattern, SymbolPattern::Regex(ref rw) if rw.source == "^mem.*"));
742                assert_eq!(offset, 0x10);
743            }
744            _ => panic!("expected Symbol with Regex pattern"),
745        }
746    }
747
748    #[test]
749    fn test_regex_with_ret_and_library() {
750        let spec = parse_probe_spec("ret:libpthread:/pthread_.*/").unwrap();
751        match spec {
752            ProbeSpec::Symbol {
753                library,
754                pattern,
755                is_ret,
756                ..
757            } => {
758                assert_eq!(library, Some("libpthread".to_string()));
759                assert!(matches!(pattern, SymbolPattern::Regex(_)));
760                assert!(is_ret);
761            }
762            _ => panic!("expected Symbol with Regex pattern"),
763        }
764    }
765
766    #[test]
767    fn test_absolute_path_not_confused_with_regex() {
768        // /usr/lib/libc.so.6:malloc should NOT be parsed as regex
769        let spec = parse_probe_spec("/usr/lib/libc.so.6:malloc").unwrap();
770        match spec {
771            ProbeSpec::Symbol {
772                library, pattern, ..
773            } => {
774                assert_eq!(library, Some("/usr/lib/libc.so.6".to_string()));
775                assert!(matches!(pattern, SymbolPattern::Exact(ref s) if s == "malloc"));
776            }
777            _ => panic!("expected Symbol with Exact pattern"),
778        }
779    }
780}