1use std::collections::BTreeMap;
4use std::io::{BufRead, BufReader};
5use std::path::{Component, Path, PathBuf};
6
7use crate::{Access, Category, Operation};
8use anyhow::Result;
9
10pub fn normalize(path: &Path) -> PathBuf {
11 let mut result = PathBuf::new();
12 for part in path.components() {
13 match part {
14 Component::CurDir => {}
15 Component::ParentDir => {
16 result.pop();
17 }
18 _ => result.push(part.as_os_str()),
19 }
20 }
21 result
22}
23
24pub struct Limits<'a> {
26 pub records: usize,
28 pub bytes: usize,
29 pub accesses: usize,
31 pub keep: Option<&'a dyn Fn(&Path) -> bool>,
33 pub resolved: bool,
37}
38
39pub const ANALYSIS: Limits<'static> = Limits {
42 records: 200_000,
43 bytes: 32 * 1024 * 1024,
44 accesses: 100_000,
45 keep: None,
46 resolved: false,
47};
48
49static UNKNOWN_CWD: std::sync::LazyLock<PathBuf> =
54 std::sync::LazyLock::new(|| std::iter::once("/").chain(["\0"; 64]).collect());
55
56fn kept(event: &Access, keep: &dyn Fn(&Path) -> bool, resolved: bool) -> bool {
59 keep(Path::new(&event.path))
60 || (resolved
61 && event
62 .resolved_path
63 .as_deref()
64 .is_some_and(|resolved| keep(Path::new(resolved))))
65}
66
67fn relevant(process: u32, call: &str, keep: &dyn Fn(&Path) -> bool, resolved: bool) -> bool {
71 let unknown = UNKNOWN_CWD.as_path();
72 let Some((events, change, child)) = decode(process, call, unknown) else {
73 return true;
74 };
75 change.is_some()
76 || child.is_some()
77 || events.iter().any(|event| {
78 let path = Path::new(&event.path);
79 path.components()
81 .any(|component| component.as_os_str() == "\0")
82 || kept(event, keep, resolved)
83 })
84}
85
86pub fn collect(directory: &Path, cwd: &Path) -> Result<(Vec<Access>, Vec<String>)> {
87 collect_within(directory, cwd, &ANALYSIS)
88}
89
90pub fn collect_within(
93 directory: &Path,
94 cwd: &Path,
95 limits: &Limits,
96) -> Result<(Vec<Access>, Vec<String>)> {
97 let mut records = Vec::new();
98 let mut diagnostics = Vec::new();
99 let mut pending = BTreeMap::<u32, (f64, String)>::new();
100 let mut bytes_read = 0usize;
101 let mut files = std::fs::read_dir(directory)?.collect::<std::io::Result<Vec<_>>>()?;
102 files.sort_by_key(|entry| entry.file_name());
103 'files: for entry in &files {
104 let name = entry.file_name().to_string_lossy().into_owned();
105 let Some(process) = name
106 .rsplit('.')
107 .next()
108 .and_then(|id| id.parse::<u32>().ok())
109 else {
110 continue;
111 };
112 let reader = BufReader::new(std::fs::File::open(entry.path())?);
113 for (sequence, line) in reader.lines().enumerate() {
114 let line = line?;
115 if bytes_read > limits.bytes || records.len() >= limits.records {
116 diagnostics.push(format!(
117 "trace collection exceeded its {} MiB/{}-record budget; report truncated",
118 limits.bytes / 1024 / 1024,
119 limits.records
120 ));
121 break 'files;
122 }
123 let Some((time, call)) = line.split_once(' ') else {
124 continue;
125 };
126 let Ok(mut time) = time.parse::<f64>() else {
127 diagnostics.push("trace line has an invalid timestamp".into());
128 continue;
129 };
130 let call = call.trim();
131 if call.starts_with("---") || call.starts_with("+++") {
132 continue;
133 }
134 if let Some(prefix) = call.strip_suffix("<unfinished ...>") {
135 pending.insert(process, (time, prefix.to_owned()));
136 continue;
137 }
138 let call = if call.starts_with("<...") {
139 match (pending.remove(&process), call.split_once("resumed>")) {
140 (Some((started, prefix)), Some((_, suffix))) => {
141 time = started;
142 format!("{prefix}{suffix}")
143 }
144 _ => {
145 diagnostics
146 .push(format!("unmatched resumed syscall for process {process}"));
147 continue;
148 }
149 }
150 } else {
151 call.to_owned()
152 };
153 if limits
154 .keep
155 .is_some_and(|keep| !relevant(process, &call, keep, limits.resolved))
156 {
157 continue;
158 }
159 bytes_read = bytes_read.saturating_add(call.len());
160 records.push((time, process, sequence, call));
161 }
162 }
163 if files.is_empty() {
164 diagnostics.push("no syscall trace files were produced".into());
165 }
166 if !pending.is_empty() {
167 diagnostics.push("some syscalls did not finish before recording stopped".into());
168 }
169 records.sort_by(|a, b| a.0.total_cmp(&b.0).then(a.1.cmp(&b.1)).then(a.2.cmp(&b.2)));
170 let mut directories = BTreeMap::new();
171 let mut accesses = Vec::new();
172 let mut undecoded = 0;
173 let mut undecoded_names = std::collections::BTreeSet::new();
174 let mut undecoded_example = None;
176 for (_, process, _, call) in records {
177 let current = directories
178 .entry(process)
179 .or_insert_with(|| cwd.to_owned())
180 .clone();
181 match decode(process, &call, ¤t) {
182 Some((events, change, child)) => {
183 accesses.extend(events.into_iter().filter(|event| {
184 limits
185 .keep
186 .is_none_or(|keep| kept(event, keep, limits.resolved))
187 }));
188 if let Some(directory) = change {
189 directories.insert(process, directory);
190 }
191 if let Some(child) = child {
192 directories.insert(child, current);
193 }
194 }
195 None => {
196 undecoded += 1;
197 undecoded_names.insert(call.split('(').next().unwrap_or("unknown").to_owned());
198 undecoded_example
199 .get_or_insert_with(|| call.chars().take(EXAMPLE_CHARS).collect::<String>());
200 }
201 }
202 if accesses.len() > limits.accesses {
203 accesses.truncate(limits.accesses);
204 diagnostics.push(format!(
205 "recording exceeded {} accesses; report truncated",
206 limits.accesses
207 ));
208 break;
209 }
210 }
211 if undecoded > 0 {
212 diagnostics.push(format!(
213 "{undecoded} syscall records could not be decoded: {}, such as {:?}",
214 undecoded_names.into_iter().collect::<Vec<_>>().join(", "),
215 undecoded_example.unwrap_or_default()
216 ));
217 }
218 diagnostics.sort();
219 diagnostics.dedup();
220 Ok((accesses, diagnostics))
221}
222
223type Decoded = (Vec<Access>, Option<PathBuf>, Option<u32>);
224
225const EXAMPLE_CHARS: usize = 200;
227
228fn decode(process: u32, call: &str, cwd: &Path) -> Option<Decoded> {
229 let (name, tail) = call.split_once('(')?;
230 if matches!(
235 name,
236 "exit"
237 | "exit_group"
238 | "???"
239 | "kill"
240 | "tkill"
241 | "tgkill"
242 | "rt_sigqueueinfo"
243 | "rt_tgsigqueueinfo"
244 | "pidfd_send_signal"
245 ) {
246 return Some((Vec::new(), None, None));
247 }
248 let (args, result) = tail.rsplit_once(" = ")?;
249 let args = args.trim_end().strip_suffix(')')?;
250 let args = arguments(args);
251 if result.starts_with('?') && lookup(name) {
254 return Some((Vec::new(), None, None));
255 }
256 let success = !result.starts_with("-1") && !result.starts_with('?');
257 let status = if result.starts_with("-1 ENOENT") {
258 "missing"
259 } else if result.starts_with("-1") {
260 result.split_whitespace().nth(1).unwrap_or("failed")
261 } else if success {
262 "success"
263 } else {
264 "unknown"
265 };
266 let mut events = Vec::new();
267 let mut change = None;
268 let mut child = None;
269 let mut emit = |path: PathBuf, operation| {
270 events.push(Access {
271 process,
272 path: path.to_string_lossy().into_owned(),
273 resolved_path: None,
274 keyed_path: None,
275 package: None,
276 operation,
277 result: status.to_owned(),
278 category: Category::Uncovered,
279 });
280 };
281 let path = |index: usize, dir: Option<usize>| -> Option<PathBuf> {
282 let value = quoted(args.get(index)?)?;
283 let value = Path::new(&value);
284 if value.is_absolute() {
285 return Some(normalize(value));
286 }
287 let base = match dir {
288 Some(index) => descriptor(args[index])
289 .or_else(|| args[index].starts_with("AT_FDCWD").then(|| cwd.to_owned()))?,
290 _ => cwd.to_owned(),
291 };
292 Some(normalize(&base.join(value)))
293 };
294 match name {
295 "clone" | "clone3" | "fork" | "vfork" => {
296 if success {
297 child = result.split_whitespace().next()?.parse().ok();
298 }
299 }
300 "chdir" => {
301 if success {
302 change = Some(path(0, None)?);
303 }
304 }
305 "fchdir" => {
306 if success {
307 change = Some(descriptor(args.first()?)?);
308 }
309 }
310 "getcwd" => {
311 if success {
312 change = Some(PathBuf::from(quoted(args.first()?)?));
313 }
314 }
315 "open" | "openat" | "openat2" | "creat" => {
316 let at = name.starts_with("openat");
317 let file = path(usize::from(at), at.then_some(0))?;
318 let flags = args.get(usize::from(at) + 1).copied().unwrap_or("");
319 if name != "creat" && !flags.contains("O_WRONLY") && !flags.contains("O_PATH") {
320 emit(file.clone(), Operation::OpenRead);
321 } else if flags.contains("O_PATH") {
322 emit(file.clone(), Operation::Metadata);
323 }
324 if name == "creat"
325 || flags.contains("O_WRONLY")
326 || flags.contains("O_RDWR")
327 || flags.contains("O_CREAT")
328 || flags.contains("O_TRUNC")
329 {
330 emit(file, Operation::Write);
331 }
332 let resolved = success.then(|| descriptor(result)).flatten();
334 for event in &mut events {
335 event.resolved_path = resolved
336 .as_ref()
337 .map(|path| path.to_string_lossy().into_owned());
338 }
339 }
340 "getdents64" => emit(descriptor(args.first()?)?, Operation::ListDirectory),
341 "readlink" => emit(path(0, None)?, Operation::ReadLink),
342 "readlinkat" => emit(path(1, Some(0))?, Operation::ReadLink),
343 "stat" | "lstat" | "stat64" | "lstat64" | "access" | "statfs" | "statfs64" | "getxattr"
344 | "lgetxattr" | "listxattr" | "llistxattr" => emit(path(0, None)?, Operation::Metadata),
345 "newfstatat" | "fstatat64" | "statx" | "faccessat" | "faccessat2" | "name_to_handle_at" => {
346 let null = args.get(1) == Some(&"NULL");
352 let flags = match name {
355 "statx" => args.get(2),
356 "name_to_handle_at" => args.get(4),
357 "faccessat" => None,
358 _ => args.get(3),
359 };
360 let probe = null && !flags.is_some_and(|flags| flags.contains("AT_EMPTY_PATH"));
361 let empty = null || quoted(args.get(1)?).is_some_and(|path| path.is_empty());
362 let unnamed = empty && args[0].contains('<') && descriptor(args[0]).is_none();
363 if !probe && !unnamed {
364 let target = if null {
365 normalize(
366 &descriptor(args[0])
367 .or_else(|| args[0].starts_with("AT_FDCWD").then(|| cwd.to_owned()))?,
368 )
369 } else {
370 path(1, Some(0))?
371 };
372 emit(target, Operation::Metadata);
373 }
374 }
375 "inotify_add_watch" => emit(path(1, None)?, Operation::Metadata),
376 "unlink" | "rmdir" => emit(path(0, None)?, Operation::Delete),
377 "unlinkat" => emit(path(1, Some(0))?, Operation::Delete),
378 "rename" => {
379 emit(path(0, None)?, Operation::Delete);
380 emit(path(1, None)?, Operation::Rename);
381 }
382 "renameat" | "renameat2" => {
383 emit(path(1, Some(0))?, Operation::Delete);
384 emit(path(3, Some(2))?, Operation::Rename);
385 }
386 "mkdir" | "truncate" | "chmod" | "chown" | "lchown" | "utime" | "utimes" | "mknod"
387 | "setxattr" | "lsetxattr" | "removexattr" | "lremovexattr" => {
388 emit(path(0, None)?, Operation::Write)
389 }
390 "utimensat" if args.get(1).is_some_and(|path| *path == "NULL") => {
393 emit(normalize(&descriptor(args[0])?), Operation::Write)
394 }
395 "mkdirat" | "fchmodat" | "fchmodat2" | "fchownat" | "utimensat" | "mknodat"
396 | "futimesat" => emit(path(1, Some(0))?, Operation::Write),
397 "symlink" => emit(path(1, None)?, Operation::Write),
398 "symlinkat" => emit(path(2, Some(1))?, Operation::Write),
399 "link" => {
400 emit(path(0, None)?, Operation::Metadata);
401 emit(path(1, None)?, Operation::Write);
402 }
403 "linkat" => {
404 emit(path(1, Some(0))?, Operation::Metadata);
405 emit(path(3, Some(2))?, Operation::Write);
406 }
407 "execve" | "execveat" | "exit" | "exit_group" | "wait4" | "waitid" | "close" | "dup"
408 | "dup2" | "dup3" | "kill" | "tkill" | "tgkill" | "rt_sigqueueinfo"
409 | "rt_tgsigqueueinfo" | "pidfd_send_signal" | "pidfd_open" => {}
410 _ => return None,
411 }
412 Some((events, change, child))
413}
414
415fn lookup(name: &str) -> bool {
417 matches!(
418 name,
419 "stat"
420 | "lstat"
421 | "stat64"
422 | "lstat64"
423 | "access"
424 | "statfs"
425 | "statfs64"
426 | "getxattr"
427 | "lgetxattr"
428 | "listxattr"
429 | "llistxattr"
430 | "newfstatat"
431 | "fstatat64"
432 | "statx"
433 | "faccessat"
434 | "faccessat2"
435 | "name_to_handle_at"
436 | "readlink"
437 | "readlinkat"
438 | "getdents64"
439 )
440}
441
442fn descriptor(arg: &str) -> Option<PathBuf> {
443 let (_, path) = arg.split_once('<')?;
444 let path = path.strip_suffix('>')?;
445 let path = path.strip_suffix(" (deleted)").unwrap_or(path);
446 path.starts_with('/').then(|| PathBuf::from(path))
447}
448
449fn arguments(text: &str) -> Vec<&str> {
451 let mut result = Vec::new();
452 let (mut start, mut nesting, mut string, mut escaped) = (0, 0usize, false, false);
453 for (index, ch) in text.char_indices() {
454 if string {
455 if escaped {
456 escaped = false;
457 } else if ch == '\\' {
458 escaped = true;
459 } else if ch == '"' {
460 string = false;
461 }
462 continue;
463 }
464 match ch {
465 '"' => string = true,
466 '(' | '[' | '{' | '<' => nesting += 1,
467 ')' | ']' | '}' | '>' => nesting = nesting.saturating_sub(1),
468 ',' if nesting == 0 => {
469 result.push(text[start..index].trim());
470 start = index + 1;
471 }
472 _ => {}
473 }
474 }
475 result.push(text[start..].trim());
476 result
477}
478
479fn quoted(text: &str) -> Option<String> {
480 let bytes = text.as_bytes();
481 if bytes.first() != Some(&b'"') {
482 return None;
483 }
484 let mut result = Vec::new();
485 let mut index = 1;
486 while index < bytes.len() {
487 let byte = bytes[index];
488 index += 1;
489 if byte == b'"' {
490 if text[index..].trim().starts_with("...") {
491 return None;
492 }
493 return String::from_utf8(result).ok();
494 }
495 if byte != b'\\' {
496 result.push(byte);
497 continue;
498 }
499 let escaped = *bytes.get(index)?;
500 index += 1;
501 result.push(match escaped {
502 b'n' => b'\n',
503 b'r' => b'\r',
504 b't' => b'\t',
505 b'\\' => b'\\',
506 b'"' => b'"',
507 b'0'..=b'7' => {
508 let mut value = u16::from(escaped - b'0');
509 for _ in 0..2 {
510 if let Some(next @ b'0'..=b'7') = bytes.get(index) {
511 value = value * 8 + u16::from(*next - b'0');
512 index += 1;
513 } else {
514 break;
515 }
516 }
517 u8::try_from(value).ok()?
518 }
519 _ => return None,
520 });
521 }
522 None
523}
524
525#[cfg(all(test, unix))]
526mod tests {
527 use super::*;
528
529 #[test]
530 fn resolves_cwd_dirfds_and_failed_probes_without_confusing_writes() {
531 let cwd = Path::new("/repo/app");
532 let (events, _, _) = decode(
533 1,
534 "openat(3</repo/shared>, \"a b.json\", O_RDONLY|O_CLOEXEC) = 4</repo/shared/a b.json>",
535 cwd,
536 )
537 .unwrap();
538 assert_eq!(events[0].path, "/repo/shared/a b.json");
539 let (events, _, _) = decode(1, "newfstatat(AT_FDCWD, \"../missing\", 0x123, 0) = -1 ENOENT (No such file or directory)", cwd).unwrap();
540 assert_eq!(events[0].result, "missing");
541 assert_eq!(events[0].path, "/repo/missing");
542 let (events, _, _) = decode(
543 1,
544 "openat(AT_FDCWD, \"out\", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 5</repo/app/out>",
545 cwd,
546 )
547 .unwrap();
548 assert_eq!(events.len(), 1);
549 assert_eq!(events[0].operation, Operation::Write);
550 let (events, _, _) = decode(
551 1,
552 "getdents64(5</repo/app/templates>, [{d_name=\"a\"}], 4096) = 24",
553 cwd,
554 )
555 .unwrap();
556 assert_eq!(events[0].operation, Operation::ListDirectory);
557 }
558
559 #[test]
560 fn decodes_futimens_as_a_write_to_the_descriptors_file() {
561 let (events, _, _) = decode(
562 1,
563 concat!(
564 "utimensat(23</repo/build/browserconfig.xml>, NULL, ",
565 "[{tv_sec=1792930995, tv_nsec=88344326} /* 2026-10-25T12:23:15.088344326+0000 */, ",
566 "{tv_sec=1792930966, tv_nsec=7823650} /* 2026-10-25T12:22:46.007823650+0000 */], 0) = 0",
567 ),
568 Path::new("/repo"),
569 )
570 .unwrap();
571 assert_eq!(events.len(), 1);
572 assert_eq!(events[0].path, "/repo/build/browserconfig.xml");
573 assert_eq!(events[0].operation, Operation::Write);
574 }
575
576 #[test]
577 fn detects_truncated_strings_and_decodes_escaped_paths() {
578 assert_eq!(quoted(r#""a\040b\"c""#), Some("a b\"c".into()));
579 assert_eq!(quoted(r#""long"..."#), None);
580 assert_eq!(
581 arguments(r#"3</a,b>, "c,d", {flags=O_RDONLY, mode=0}"#).len(),
582 3
583 );
584 }
585
586 #[test]
587 fn accepts_padded_exit_records_and_uses_annotated_cwd() {
588 assert!(decode(1, "exit_group(0) = ?", Path::new("/repo")).is_some());
589 for call in ["exit_group(0)", "???() = ?", "???( <detached ...>"] {
591 let (events, _, _) = decode(1, call, Path::new("/repo")).unwrap();
592 assert!(events.is_empty(), "{call}");
593 }
594 let (events, _, _) = decode(
595 2,
596 "openat(AT_FDCWD</repo/sub>, \"file\", O_RDONLY) = 3</repo/sub/file>",
597 Path::new("/repo"),
598 )
599 .unwrap();
600 assert_eq!(events[0].path, "/repo/sub/file");
601 }
602
603 #[test]
604 fn signals_and_path_metadata_do_not_disable_successful_collection() {
605 let directory = tempfile::tempdir().unwrap();
606 std::fs::write(directory.path().join("command-0.123"), concat!(
607 "1.000 tgkill(123, 124, SIGURG) = 0\n",
608 "1.001 kill(125, SIGTERM) = 0\n",
609 "1.002 lgetxattr(\"f\", \"security.selinux\", 0x0, 0) = -1 ENODATA (No data available)\n",
610 "1.003 inotify_add_watch(3, \"src\", IN_MODIFY) = 1\n",
611 "1.004 setxattr(\"f\", \"user.test\", 0x123, 1, 0) = 0\n",
612 "1.005 futimesat(AT_FDCWD, \"f\", NULL) = 0\n",
613 "1.006 name_to_handle_at(AT_FDCWD, \"f\", 0x123, 0x124, 0) = 0\n",
614 )).unwrap();
615 let (events, diagnostics) = collect(directory.path(), Path::new("/repo")).unwrap();
616 assert!(diagnostics.is_empty(), "{diagnostics:?}");
617 assert_eq!(events.len(), 5);
618 assert_eq!(events[0].path, "/repo/f");
619 assert_eq!(events[0].operation, Operation::Metadata);
620 assert_eq!(events[0].result, "ENODATA");
621 assert_eq!(events[1].path, "/repo/src");
622 assert_eq!(events[1].operation, Operation::Metadata);
623 assert_eq!(events[2].operation, Operation::Write);
624 assert_eq!(events[3].operation, Operation::Write);
625 assert_eq!(events[4].operation, Operation::Metadata);
626 assert!(decode(123, "chroot(\"/other\") = 0", Path::new("/repo")).is_none());
627 }
628
629 #[test]
630 fn a_filter_keeps_only_what_can_matter() {
631 let directory = tempfile::tempdir().unwrap();
632 std::fs::write(
633 directory.path().join("command-0.10"),
634 concat!(
635 "1.000 openat(AT_FDCWD</repo>, \"node_modules/x/index.js\", O_RDONLY) = 3</repo/node_modules/x/index.js>\n",
636 "1.001 openat(AT_FDCWD</repo>, \"src/a.ts\", O_RDONLY) = 3</repo/src/a.ts>\n",
637 "1.0015 openat(AT_FDCWD</repo>, \"node_modules/lib/index.ts\", O_RDONLY) = 3</repo/libs/lib/index.ts>\n",
639 "1.002 openat(AT_FDCWD</repo>, \"/usr/lib/libc.so\", O_RDONLY) = 3</usr/lib/libc.so>\n",
640 "1.003 chdir(\"lib\") = 0\n",
641 "1.004 stat(\"../src/b.ts\", {...}) = 0\n",
644 ),
645 )
646 .unwrap();
647 let keep =
648 |path: &Path| path.starts_with("/repo") && !path.starts_with("/repo/node_modules");
649 let limits = Limits {
650 keep: Some(&keep),
651 resolved: true,
652 ..ANALYSIS
653 };
654 let (events, diagnostics) =
655 collect_within(directory.path(), Path::new("/repo"), &limits).unwrap();
656 assert!(diagnostics.is_empty(), "{diagnostics:?}");
657 let paths: Vec<&str> = events.iter().map(|event| event.path.as_str()).collect();
658 assert_eq!(
659 paths,
660 [
661 "/repo/src/a.ts",
662 "/repo/node_modules/lib/index.ts",
663 "/repo/src/b.ts"
664 ]
665 );
666 assert_eq!(
667 events[1].resolved_path.as_deref(),
668 Some("/repo/libs/lib/index.ts")
669 );
670 let named = Limits {
672 keep: Some(&keep),
673 ..ANALYSIS
674 };
675 let (events, _) = collect_within(directory.path(), Path::new("/repo"), &named).unwrap();
676 let paths: Vec<&str> = events.iter().map(|event| event.path.as_str()).collect();
677 assert_eq!(paths, ["/repo/src/a.ts", "/repo/src/b.ts"]);
678 let tight = Limits {
680 accesses: 3,
681 keep: Some(&keep),
682 resolved: true,
683 ..ANALYSIS
684 };
685 let (_, diagnostics) =
686 collect_within(directory.path(), Path::new("/repo"), &tight).unwrap();
687 assert!(diagnostics.is_empty(), "{diagnostics:?}");
688 }
689
690 #[test]
691 fn stats_of_pipes_and_sockets_name_no_path() {
692 let cwd = Path::new("/repo");
693 for call in [
694 "newfstatat(1<pipe:[123]>, \"\", {st_mode=S_IFIFO|0600, ...}, AT_EMPTY_PATH) = 0",
695 "statx(17<socket:[456]>, \"\", AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {...}) = 0",
696 "statx(0</dev/null<char 1:3>>, NULL, AT_STATX_SYNC_AS_STAT, STATX_ALL, NULL) = -1 EFAULT (Bad address)",
699 "statx(3</repo/AT_EMPTY_PATH.txt>, NULL, AT_STATX_SYNC_AS_STAT, STATX_ALL, NULL) = -1 EFAULT (Bad address)",
700 ] {
701 let (events, _, _) = decode(1, call, cwd).unwrap();
702 assert!(events.is_empty(), "{call}");
703 }
704 for call in [
707 "newfstatat(3</repo/a.txt>, \"\", {st_mode=S_IFREG|0644, ...}, AT_EMPTY_PATH) = 0",
708 "statx(3</repo/a.txt>, NULL, AT_STATX_SYNC_AS_STAT|AT_EMPTY_PATH, STATX_ALL, {...}) = 0",
709 ] {
710 let (events, _, _) = decode(1, call, cwd).unwrap();
711 assert_eq!(events[0].path, "/repo/a.txt", "{call}");
712 }
713 }
714
715 #[test]
716 fn a_lookup_its_process_ending_cut_short_is_no_access() {
717 let cwd = Path::new("/repo");
718 for call in [
719 "access(0x7fff7546b1d0, F_OK) = ?",
720 "newfstatat(AT_FDCWD, \"a.txt\", 0x7ffd, 0) = ?",
721 "getdents64(3</repo>, 0x5555, 32768) = ?",
722 ] {
723 let (events, _, _) = decode(1, call, cwd).expect(call);
724 assert!(events.is_empty(), "{call}");
725 }
726 let (events, _, _) = decode(1, "unlink(\"/repo/a.txt\") = ?", cwd).unwrap();
728 assert_eq!(events.len(), 1);
729 }
730
731 #[test]
732 fn a_signal_is_no_access_however_strace_writes_it() {
733 let cwd = Path::new("/repo");
734 for call in [
735 "tgkill(42, 43, SIGRTMIN) = 0",
736 "tgkill(42, 42, SIGKILL <unfinished ...>) = ?",
737 "tgkill(42, 42, SIGABRT",
738 "kill(42, SIGTERM) = 0",
739 ] {
740 let (events, _, _) = decode(1, call, cwd).unwrap_or_else(|| panic!("{call}"));
741 assert!(events.is_empty(), "{call}");
742 }
743 }
744}