1use std::fmt;
15
16use regex::Regex;
17
18#[derive(Debug, Clone)]
20pub enum ProbeSpec {
21 Symbol {
23 library: Option<String>,
26 pattern: SymbolPattern,
28 offset: u64,
30 is_ret: bool,
32 },
33 SourceLocation {
35 file: String,
37 line: u32,
39 is_ret: bool,
41 },
42}
43
44#[derive(Debug, Clone)]
46pub enum SymbolPattern {
47 Exact(String),
49 Glob(String),
51 Regex(RegexWrapper),
53 Demangled(String),
56}
57
58#[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 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 SymbolPattern::Demangled(_) => false,
80 }
81 }
82
83 pub fn matches_demangled(&self, demangled_name: &str) -> bool {
87 match self {
88 SymbolPattern::Demangled(query) => {
89 demangled_name.contains(query.as_str())
92 }
93 other => other.matches(demangled_name),
95 }
96 }
97
98 pub fn is_exact(&self) -> bool {
102 matches!(self, SymbolPattern::Exact(_))
103 }
104
105 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
116pub 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 let (is_ret, rest) = if let Some(stripped) = input.strip_prefix("ret:") {
142 (true, stripped)
143 } else {
144 (false, input)
145 };
146
147 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 }
162
163 let (library, symbol_part) = split_library_and_symbol(rest)?;
167
168 if let Some(ref lib) = library {
171 if let Ok(line) = symbol_part.parse::<u32>() {
172 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 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 let (symbol_name, offset) = split_symbol_and_offset(&symbol_part)?;
198
199 let pattern = classify_pattern(&symbol_name);
201
202 Ok(ProbeSpec::Symbol {
203 library,
204 pattern,
205 offset,
206 is_ret,
207 })
208}
209
210fn split_library_and_symbol(s: &str) -> Result<(Option<String>, String), String> {
219 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 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 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 Ok((None, s.to_string()))
249 } else {
250 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
263fn 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 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
292fn 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
311fn 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 let last_slash = match s[1..].rfind('/') {
320 Some(pos) => pos + 1, 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
349fn classify_pattern(name: &str) -> SymbolPattern {
351 if name.contains("::") {
352 SymbolPattern::Demangled(name.to_string())
354 } else if name.contains('*') || name.contains('?') || name.contains('[') {
355 SymbolPattern::Glob(name.to_string())
357 } else {
358 SymbolPattern::Exact(name.to_string())
360 }
361}
362
363fn 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
372fn 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 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 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 while pi < pattern.len() && pattern[pi] == b'*' {
431 pi += 1;
432 }
433
434 pi == pattern.len()
435}
436
437fn 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 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 }
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 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}