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