tracexec_core/
breakpoint.rs

1use std::{borrow::Cow, error::Error};
2
3use nix::unistd::Pid;
4use regex_cursor::engines::pikevm::{self, PikeVM};
5use strum::IntoStaticStr;
6
7use crate::{
8  event::OutputMsg,
9  primitives::regex::{ArgvCursor, SPACE},
10};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub struct BreakPointHit {
14  pub bid: u32,
15  pub pid: Pid,
16  pub stop: BreakPointStop,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)]
20pub enum BreakPointStop {
21  SyscallEnter,
22  SyscallExit,
23}
24
25impl BreakPointStop {
26  pub fn toggle(&mut self) {
27    *self = match self {
28      Self::SyscallEnter => Self::SyscallExit,
29      Self::SyscallExit => Self::SyscallEnter,
30    }
31  }
32}
33
34#[derive(Debug, Clone)]
35pub struct BreakPointRegex {
36  regex: PikeVM,
37  editable: String,
38}
39
40#[derive(Debug, Clone)]
41pub enum BreakPointPattern {
42  /// A regular expression that matches the cmdline of the process. The cmdline is the argv
43  /// concatenated with spaces without any escaping.
44  ArgvRegex(BreakPointRegex),
45  // CmdlineRegex(BreakPointRegex),
46  InFilename(String),
47  ExactFilename(String),
48}
49
50#[derive(Debug, Clone)]
51pub enum BreakPointType {
52  /// The breakpoint will be hit once and then deactivated.
53  Once,
54  /// The breakpoint will be hit every time it is encountered.
55  Permanent,
56}
57
58#[derive(Debug, Clone)]
59pub struct BreakPoint {
60  pub pattern: BreakPointPattern,
61  pub ty: BreakPointType,
62  pub activated: bool,
63  pub stop: BreakPointStop,
64}
65
66impl BreakPointPattern {
67  pub fn pattern(&self) -> &str {
68    match self {
69      Self::ArgvRegex(regex) => regex.editable.as_str(),
70      Self::InFilename(filename) => filename,
71      // Unwrap is fine since user inputs the filename as str
72      Self::ExactFilename(filename) => filename,
73    }
74  }
75
76  pub fn to_editable(&self) -> String {
77    match self {
78      Self::ArgvRegex(regex) => format!("argv-regex:{}", regex.editable),
79      Self::InFilename(filename) => format!("in-filename:{filename}"),
80      Self::ExactFilename(filename) => {
81        format!("exact-filename:{filename}")
82      }
83    }
84  }
85
86  pub fn from_editable(editable: &str) -> Result<Self, String> {
87    if let Some((prefix, rest)) = editable.split_once(':') {
88      match prefix {
89        "in-filename" => Ok(Self::InFilename(rest.to_string())),
90        "exact-filename" => Ok(Self::ExactFilename(rest.to_string())),
91        "argv-regex" => Ok(Self::ArgvRegex(BreakPointRegex {
92          regex: PikeVM::new(rest).map_err(|e| e.to_string())?,
93          editable: rest.to_string(),
94        })),
95        _ => Err(format!("Invalid breakpoint pattern type: {prefix}!")),
96      }
97    } else {
98      Err("No valid breakpoint pattern found!".to_string())
99    }
100  }
101
102  pub fn matches(&self, argv: Option<&[OutputMsg]>, filename: &OutputMsg) -> bool {
103    match self {
104      Self::ArgvRegex(regex) => {
105        let Some(argv) = argv else {
106          return false;
107        };
108        let space = &SPACE;
109        let argv = ArgvCursor::new(argv, space);
110        pikevm::is_match(
111          &regex.regex,
112          &mut pikevm::Cache::new(&regex.regex),
113          &mut regex_cursor::Input::new(argv),
114        )
115      }
116      Self::InFilename(pattern) => {
117        let OutputMsg::Ok(filename) = filename else {
118          return false;
119        };
120        filename.contains(pattern)
121      }
122      Self::ExactFilename(path) => {
123        let OutputMsg::Ok(filename) = filename else {
124          return false;
125        };
126        filename == path
127      }
128    }
129  }
130}
131
132impl TryFrom<&str> for BreakPoint {
133  type Error = Cow<'static, str>;
134
135  fn try_from(value: &str) -> Result<Self, Self::Error> {
136    let Some((stop, rest)) = value.split_once(':') else {
137      return Err("No valid syscall stop found! The breakpoint should start with \"sysenter:\" or \"sysexit:\".".into());
138    };
139    let stop = match stop {
140      "sysenter" => BreakPointStop::SyscallEnter,
141      "sysexit" => BreakPointStop::SyscallExit,
142      _ => {
143        return Err(
144          format!("Invalid syscall stop {stop:?}! The breakpoint should start with \"sysenter:\" or \"sysexit:\".")
145            .into(),
146        )
147      }
148    };
149    let Some((pattern_kind, pattern)) = rest.split_once(':') else {
150      return Err("No valid pattern kind found! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\".".into());
151    };
152    let pattern = match pattern_kind {
153      "argv-regex" => BreakPointPattern::ArgvRegex(BreakPointRegex {
154        regex: PikeVM::new(pattern).map_err(|e| format!("\n{}", e.source().unwrap()))?,
155        editable: pattern.to_string(),
156      }),
157      "exact-filename" => BreakPointPattern::ExactFilename(pattern.to_string()),
158      "in-filename" => BreakPointPattern::InFilename(pattern.to_string()),
159      _ => {
160        return Err(
161          format!(
162            "Invalid pattern kind {pattern_kind:?}! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\"."
163          )
164          .into(),
165        )
166      }
167    };
168    Ok(Self {
169      ty: BreakPointType::Permanent,
170      stop,
171      pattern,
172      activated: true,
173    })
174  }
175}