1use std::borrow::Cow;
2
3use nix::unistd::Pid;
4use regex_cursor::engines::pikevm::{
5 self,
6 PikeVM,
7};
8use strum::IntoStaticStr;
9
10use crate::{
11 event::OutputMsg,
12 primitives::regex::{
13 ArgvCursor,
14 SPACE,
15 },
16};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct BreakPointHit {
20 pub bid: u32,
21 pub pid: Pid,
22 pub stop: BreakPointStop,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoStaticStr)]
26pub enum BreakPointStop {
27 SyscallEnter,
28 SyscallExit,
29}
30
31impl BreakPointStop {
32 pub fn toggle(&mut self) {
33 *self = match self {
34 Self::SyscallEnter => Self::SyscallExit,
35 Self::SyscallExit => Self::SyscallEnter,
36 }
37 }
38}
39
40#[derive(Debug, Clone)]
41pub struct BreakPointRegex {
42 regex: PikeVM,
43 editable: String,
44}
45
46#[derive(Debug, Clone)]
47pub enum BreakPointPattern {
48 ArgvRegex(BreakPointRegex),
51 InFilename(String),
53 ExactFilename(String),
54}
55
56#[derive(Debug, Clone)]
57pub enum BreakPointType {
58 Once,
60 Permanent,
62}
63
64#[derive(Debug, Clone)]
65pub struct BreakPoint {
66 pub pattern: BreakPointPattern,
67 pub ty: BreakPointType,
68 pub activated: bool,
69 pub stop: BreakPointStop,
70}
71
72impl BreakPointPattern {
73 pub fn pattern(&self) -> &str {
74 match self {
75 Self::ArgvRegex(regex) => regex.editable.as_str(),
76 Self::InFilename(filename) => filename,
77 Self::ExactFilename(filename) => filename,
79 }
80 }
81
82 pub fn to_editable(&self) -> String {
83 match self {
84 Self::ArgvRegex(regex) => format!("argv-regex:{}", regex.editable),
85 Self::InFilename(filename) => format!("in-filename:{filename}"),
86 Self::ExactFilename(filename) => {
87 format!("exact-filename:{filename}")
88 }
89 }
90 }
91
92 pub fn from_editable(editable: &str) -> Result<Self, String> {
93 if let Some((prefix, rest)) = editable.split_once(':') {
94 match prefix {
95 "in-filename" => Ok(Self::InFilename(rest.to_string())),
96 "exact-filename" => Ok(Self::ExactFilename(rest.to_string())),
97 "argv-regex" => Ok(Self::ArgvRegex(BreakPointRegex {
98 regex: PikeVM::new(rest).map_err(|e| e.to_string())?,
99 editable: rest.to_string(),
100 })),
101 _ => Err(format!("Invalid breakpoint pattern type: {prefix}!")),
102 }
103 } else {
104 Err("No valid breakpoint pattern found!".to_string())
105 }
106 }
107
108 pub fn matches(&self, argv: Option<&[OutputMsg]>, filename: &OutputMsg) -> bool {
109 match self {
110 Self::ArgvRegex(regex) => {
111 let Some(argv) = argv else {
112 return false;
113 };
114 let space = &SPACE;
115 let argv = ArgvCursor::new(argv, space);
116 pikevm::is_match(
117 ®ex.regex,
118 &mut pikevm::Cache::new(®ex.regex),
119 &mut regex_cursor::Input::new(argv),
120 )
121 }
122 Self::InFilename(pattern) => {
123 let OutputMsg::Ok(filename) = filename else {
124 return false;
125 };
126 filename.contains(pattern)
127 }
128 Self::ExactFilename(path) => {
129 let OutputMsg::Ok(filename) = filename else {
130 return false;
131 };
132 filename.as_str() == path
133 }
134 }
135 }
136}
137
138impl TryFrom<&str> for BreakPoint {
139 type Error = Cow<'static, str>;
140
141 fn try_from(value: &str) -> Result<Self, Self::Error> {
142 let Some((stop, rest)) = value.split_once(':') else {
143 return Err("No valid syscall stop found! The breakpoint should start with \"sysenter:\" or \"sysexit:\".".into());
144 };
145 let stop = match stop {
146 "sysenter" => BreakPointStop::SyscallEnter,
147 "sysexit" => BreakPointStop::SyscallExit,
148 _ => {
149 return Err(
150 format!("Invalid syscall stop {stop:?}! The breakpoint should start with \"sysenter:\" or \"sysexit:\".")
151 .into(),
152 )
153 }
154 };
155 let Some((pattern_kind, pattern)) = rest.split_once(':') else {
156 return Err("No valid pattern kind found! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\".".into());
157 };
158 let pattern = match pattern_kind {
159 "argv-regex" => BreakPointPattern::ArgvRegex(BreakPointRegex {
160 regex: PikeVM::new(pattern).map_err(|e| format!("\n{e}"))?,
161 editable: pattern.to_string(),
162 }),
163 "exact-filename" => BreakPointPattern::ExactFilename(pattern.to_string()),
164 "in-filename" => BreakPointPattern::InFilename(pattern.to_string()),
165 _ => {
166 return Err(
167 format!(
168 "Invalid pattern kind {pattern_kind:?}! The breakpoint pattern should start with \"argv-regex:\", \"exact-filename:\" or \"in-filename:\"."
169 )
170 .into(),
171 )
172 }
173 };
174 Ok(Self {
175 ty: BreakPointType::Permanent,
176 stop,
177 pattern,
178 activated: true,
179 })
180 }
181}
182
183#[cfg(test)]
184mod tests {
185 use nix::errno::Errno;
186 use test_that::prelude::*;
187
188 use super::*;
189 use crate::cache::ArcStr;
190
191 #[test]
192 fn test_breakpoint_stop_toggle() {
193 let mut s = BreakPointStop::SyscallEnter;
194 s.toggle();
195 assert_eq!(s, BreakPointStop::SyscallExit);
196 s.toggle();
197 assert_eq!(s, BreakPointStop::SyscallEnter);
198 }
199
200 #[test]
201 fn test_from_editable_and_to_editable_and_pattern() {
202 let bp = BreakPointPattern::from_editable("argv-regex:foo").expect("argv-regex");
204 assert_eq!(bp.pattern(), "foo");
205 assert_eq!(bp.to_editable(), "argv-regex:foo");
206
207 let bp2 = BreakPointPattern::from_editable("in-filename:/tmp/test").expect("in-filename");
209 assert_eq!(bp2.pattern(), "/tmp/test");
210 assert_eq!(bp2.to_editable(), "in-filename:/tmp/test");
211
212 let bp3 = BreakPointPattern::from_editable("exact-filename:/bin/sh").expect("exact-filename");
214 assert_eq!(bp3.pattern(), "/bin/sh");
215 assert_eq!(bp3.to_editable(), "exact-filename:/bin/sh");
216
217 assert_that!(
219 BreakPointPattern::from_editable("unknown:abc"),
220 err(anything())
221 );
222 assert_that!(
224 BreakPointPattern::from_editable("no-colon"),
225 err(anything())
226 );
227 }
228
229 #[test]
230 fn test_matches_argv_regex() {
231 let pat = BreakPointPattern::from_editable("argv-regex:arg1").unwrap();
233
234 let argv = [
235 OutputMsg::Ok(ArcStr::from("arg0")),
236 OutputMsg::Ok(ArcStr::from("arg1")),
237 OutputMsg::Ok(ArcStr::from("arg2")),
238 ];
239 let filename = OutputMsg::Ok(ArcStr::from("/bin/prog"));
240
241 assert!(pat.matches(Some(&argv), &filename));
242
243 assert!(!pat.matches(None, &filename));
245 }
246
247 #[test]
248 fn test_matches_in_and_exact_filename() {
249 let in_pat = BreakPointPattern::from_editable("in-filename:log").unwrap();
250 let exact_pat = BreakPointPattern::from_editable("exact-filename:/var/log/app").unwrap();
251
252 let ok_filename = OutputMsg::Ok(ArcStr::from("/var/log/app"));
253 let other_filename = OutputMsg::Ok(ArcStr::from("/tmp/file"));
254 let partial_filename = OutputMsg::PartialOk(ArcStr::from("something"));
255 let err_filename = OutputMsg::Err(crate::event::FriendlyError::InspectError(Errno::EINVAL));
256
257 assert!(in_pat.matches(Some(&[]), &ok_filename));
259 assert!(!in_pat.matches(Some(&[]), &other_filename));
260 assert!(!in_pat.matches(Some(&[]), &partial_filename));
261 assert!(!in_pat.matches(Some(&[]), &err_filename));
262
263 assert!(exact_pat.matches(Some(&[]), &ok_filename));
265 assert!(!exact_pat.matches(Some(&[]), &other_filename));
266 assert!(!exact_pat.matches(Some(&[]), &partial_filename));
267 assert!(!exact_pat.matches(Some(&[]), &err_filename));
268 }
269
270 #[test]
271 fn test_try_from_breakpoint_valid_and_invalid() {
272 let bp = BreakPoint::try_from("sysenter:argv-regex:foo").expect("valid breakpoint");
274 assert_eq!(bp.stop, BreakPointStop::SyscallEnter);
275 match bp.pattern {
276 BreakPointPattern::ArgvRegex(r) => assert_eq!(r.editable, "foo"),
277 _ => panic!("expected ArgvRegex"),
278 }
279
280 let bp2 =
282 BreakPoint::try_from("sysexit:exact-filename:/bin/ls").expect("valid exact breakpoint");
283 assert_eq!(bp2.stop, BreakPointStop::SyscallExit);
284 match bp2.pattern {
285 BreakPointPattern::ExactFilename(s) => assert_eq!(s, "/bin/ls"),
286 _ => panic!("expected ExactFilename"),
287 }
288
289 assert_that!(BreakPoint::try_from("no-colon-here"), err(anything()));
291
292 assert_that!(
294 BreakPoint::try_from("badstop:argv-regex:foo"),
295 err(anything())
296 );
297
298 assert_that!(BreakPoint::try_from("sysenter:badformat"), err(anything()));
300
301 assert_that!(
303 BreakPoint::try_from("sysenter:unknown-kind:xyz"),
304 err(anything())
305 );
306 }
307}