1use core::fmt;
4use std::{
5 borrow::Cow,
6 collections::{
7 BTreeMap,
8 BTreeSet,
9 HashSet,
10 },
11 ffi::CString,
12 fmt::{
13 Display,
14 Formatter,
15 },
16 fs,
17 io::{
18 self,
19 BufRead,
20 BufReader,
21 Read,
22 },
23 os::raw::c_int,
24 path::{
25 Path,
26 PathBuf,
27 },
28};
29
30use filedescriptor::AsRawFileDescriptor;
31use nix::{
32 fcntl::OFlag,
33 libc::{
34 AT_FDCWD,
35 gid_t,
36 },
37 unistd::{
38 Pid,
39 getpid,
40 },
41};
42use owo_colors::OwoColorize;
43use serde::{
44 Serialize,
45 Serializer,
46 ser::SerializeSeq,
47};
48use snafu::Snafu;
49use tracing::warn;
50
51use crate::{
52 cache::{
53 ArcStr,
54 StringCache,
55 },
56 event::OutputMsg,
57 pty::UnixSlavePty,
58};
59
60#[allow(unused)]
61pub fn read_argv(pid: Pid) -> color_eyre::Result<Vec<CString>> {
62 let filename = format!("/proc/{pid}/cmdline");
63 let buf = std::fs::read(filename)?;
64 Ok(
65 buf
66 .split(|&c| c == 0)
67 .map(CString::new)
68 .collect::<Result<Vec<_>, _>>()?,
69 )
70}
71
72pub fn read_comm(pid: Pid) -> color_eyre::Result<ArcStr> {
73 let filename = format!("/proc/{pid}/comm");
74 let mut buf = std::fs::read(filename)?;
75 buf.pop(); let utf8 = String::from_utf8_lossy(&buf);
77 Ok(CACHE.get_or_insert(&utf8))
78}
79
80pub fn read_cwd(pid: Pid) -> std::io::Result<ArcStr> {
81 let filename = format!("/proc/{pid}/cwd");
82 let buf = std::fs::read_link(filename)?;
83 Ok(cached_str(&buf.to_string_lossy()))
84}
85
86pub fn read_exe(pid: Pid) -> std::io::Result<ArcStr> {
87 let filename = format!("/proc/{pid}/exe");
88 let buf = std::fs::read_link(filename)?;
89 Ok(cached_str(&buf.to_string_lossy()))
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct ProcStatus {
94 pub cred: Cred,
95}
96
97#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
98pub struct Cred {
99 pub groups: Vec<gid_t>,
100 pub uid_real: u32,
101 pub uid_effective: u32,
102 pub uid_saved_set: u32,
103 pub uid_fs: u32,
104 pub gid_real: u32,
105 pub gid_effective: u32,
106 pub gid_saved_set: u32,
107 pub gid_fs: u32,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Snafu)]
111pub enum CredInspectError {
112 #[snafu(display("Failed to read credential info: {kind}"))]
113 Io { kind: std::io::ErrorKind },
114 #[snafu(display("Failed to inspect credential info from kernel"))]
115 Inspect,
116}
117
118pub fn read_status(pid: Pid) -> std::io::Result<ProcStatus> {
119 let filename = format!("/proc/{pid}/status");
120 let contents = fs::read_to_string(filename)?;
121 parse_status_contents(&contents)
122}
123
124fn parse_status_contents(contents: &str) -> std::io::Result<ProcStatus> {
125 let mut uid = None;
126 let mut gid = None;
127 let mut groups = None;
128
129 fn parse_ids(s: &str) -> std::io::Result<[u32; 4]> {
130 let mut iter = s.trim_ascii().split_ascii_whitespace().take(4).map(|v| {
131 v.parse()
132 .map_err(|_| std::io::Error::new(io::ErrorKind::InvalidData, "non numeric uid/gid"))
133 });
134 Ok([
135 iter
136 .next()
137 .transpose()?
138 .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
139 iter
140 .next()
141 .transpose()?
142 .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
143 iter
144 .next()
145 .transpose()?
146 .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
147 iter
148 .next()
149 .transpose()?
150 .ok_or_else(|| std::io::Error::new(io::ErrorKind::InvalidData, "not enough uid/gid(s)"))?,
151 ])
152 }
153
154 for line in contents.lines() {
155 if let Some(rest) = line.strip_prefix("Uid:") {
156 uid = Some(parse_ids(rest)?);
157 } else if let Some(rest) = line.strip_prefix("Gid:") {
158 gid = Some(parse_ids(rest)?);
159 } else if let Some(rest) = line.strip_prefix("Groups:") {
160 let r: Result<Vec<_>, _> = rest
161 .trim_ascii()
162 .split_ascii_whitespace()
163 .map(|v| {
164 v.parse()
165 .map_err(|_| std::io::Error::new(io::ErrorKind::InvalidData, "non numeric group id"))
166 })
167 .collect();
168 groups = Some(r?);
169 }
170
171 if uid.is_some() && gid.is_some() && groups.is_some() {
172 break;
173 }
174 }
175
176 let Some([uid_real, uid_effective, uid_saved_set, uid_fs]) = uid else {
177 return Err(std::io::Error::new(
178 io::ErrorKind::InvalidData,
179 "status output does not contain uids",
180 ));
181 };
182 let Some([gid_real, gid_effective, gid_saved_set, gid_fs]) = gid else {
183 return Err(std::io::Error::new(
184 io::ErrorKind::InvalidData,
185 "status output does not contain gids",
186 ));
187 };
188 let Some(groups) = groups else {
189 return Err(std::io::Error::new(
190 io::ErrorKind::InvalidData,
191 "status output does not contain groups",
192 ));
193 };
194
195 Ok(ProcStatus {
196 cred: Cred {
197 groups,
198 uid_real,
199 uid_effective,
200 uid_saved_set,
201 uid_fs,
202 gid_real,
203 gid_effective,
204 gid_saved_set,
205 gid_fs,
206 },
207 })
208}
209
210#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
211pub struct FileDescriptorInfoCollection {
212 #[serde(flatten)]
213 pub fdinfo: BTreeMap<c_int, FileDescriptorInfo>,
214}
215
216impl FileDescriptorInfoCollection {
217 pub fn stdin(&self) -> Option<&FileDescriptorInfo> {
218 self.fdinfo.get(&0)
219 }
220
221 pub fn stdout(&self) -> Option<&FileDescriptorInfo> {
222 self.fdinfo.get(&1)
223 }
224
225 pub fn stderr(&self) -> Option<&FileDescriptorInfo> {
226 self.fdinfo.get(&2)
227 }
228
229 pub fn get(&self, fd: c_int) -> Option<&FileDescriptorInfo> {
230 self.fdinfo.get(&fd)
231 }
232
233 pub fn new_baseline() -> color_eyre::Result<Self> {
234 let mut fdinfo = BTreeMap::new();
235 let pid = getpid();
236 fdinfo.insert(0, read_fdinfo(pid, 0)?);
237 fdinfo.insert(1, read_fdinfo(pid, 1)?);
238 fdinfo.insert(2, read_fdinfo(pid, 2)?);
239
240 Ok(Self { fdinfo })
241 }
242
243 pub fn with_pts(pts: &UnixSlavePty) -> color_eyre::Result<Self> {
244 let mut result = Self::default();
245 let ptyfd = &pts.fd;
246 let raw_fd = ptyfd.as_raw_file_descriptor();
247 let mut info = read_fdinfo(getpid(), raw_fd)?;
248 for fd in 0..3 {
249 info.fd = fd;
250 result.fdinfo.insert(fd, read_fdinfo(getpid(), raw_fd)?);
251 }
252 Ok(result)
253 }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
257pub struct FileDescriptorInfo {
258 pub fd: c_int,
259 pub path: OutputMsg,
260 pub pos: usize,
261 #[serde(serialize_with = "serialize_oflags")]
262 pub flags: OFlag,
263 pub mnt_id: c_int,
264 pub ino: u64,
265 pub mnt: ArcStr,
266 pub extra: Vec<ArcStr>,
267}
268
269impl FileDescriptorInfo {
270 pub fn not_same_file_as(&self, other: &Self) -> bool {
271 !self.same_file_as(other)
272 }
273
274 pub fn same_file_as(&self, other: &Self) -> bool {
275 self.ino == other.ino && self.mnt_id == other.mnt_id
276 }
277}
278
279fn serialize_oflags<S>(oflag: &OFlag, serializer: S) -> Result<S::Ok, S::Error>
280where
281 S: Serializer,
282{
283 let mut seq = serializer.serialize_seq(None)?;
284 let mut flag_display = String::with_capacity(16);
285 for f in oflag.iter() {
286 flag_display.clear();
287 bitflags::parser::to_writer(&f, &mut flag_display).unwrap();
288 seq.serialize_element(&flag_display)?;
289 }
290 seq.end()
291}
292
293impl Default for FileDescriptorInfo {
294 fn default() -> Self {
295 Self {
296 fd: Default::default(),
297 path: OutputMsg::Ok(ArcStr::default()),
298 pos: Default::default(),
299 flags: OFlag::empty(),
300 mnt_id: Default::default(),
301 ino: Default::default(),
302 mnt: Default::default(),
303 extra: Default::default(),
304 }
305 }
306}
307
308pub fn read_fd(pid: Pid, fd: i32) -> std::io::Result<ArcStr> {
309 if fd == AT_FDCWD {
310 return read_cwd(pid);
311 }
312 let filename = format!("/proc/{pid}/fd/{fd}");
313 Ok(cached_str(&std::fs::read_link(filename)?.to_string_lossy()))
314}
315
316pub fn read_fdinfo(pid: Pid, fd: i32) -> color_eyre::Result<FileDescriptorInfo> {
318 let filename = format!("/proc/{pid}/fdinfo/{fd}");
319 let file = std::fs::File::open(filename)?;
320 let reader = BufReader::new(file);
321 let mut info = FileDescriptorInfo::default();
322 for line in reader.lines() {
323 let line = line?;
324 let mut parts = line.split_ascii_whitespace();
325 let key = parts.next().unwrap_or("");
326 let value = parts.next().unwrap_or("");
327 match key {
328 "pos:" => info.pos = value.parse()?,
329 "flags:" => info.flags = OFlag::from_bits_truncate(c_int::from_str_radix(value, 8)?),
330 "mnt_id:" => info.mnt_id = value.parse()?,
331 "ino:" => info.ino = value.parse()?,
332 _ => {
333 let line = CACHE.get_or_insert_owned(line);
334 info.extra.push(line)
335 }
336 }
337 }
338 info.mnt = get_mountinfo_by_mnt_id(pid, info.mnt_id)?;
339 info.path = read_fd(pid, fd).map(OutputMsg::Ok)?;
340 Ok(info)
341}
342
343pub fn read_fds(pid: Pid) -> color_eyre::Result<FileDescriptorInfoCollection> {
344 let mut collection = FileDescriptorInfoCollection::default();
345 let filename = format!("/proc/{pid}/fdinfo");
346 for entry in std::fs::read_dir(filename)? {
347 let entry = entry?;
348 let fd = entry.file_name().to_string_lossy().parse()?;
349 collection.fdinfo.insert(fd, read_fdinfo(pid, fd)?);
350 }
351 Ok(collection)
352}
353
354fn get_mountinfo_by_mnt_id(pid: Pid, mnt_id: c_int) -> color_eyre::Result<ArcStr> {
355 let filename = format!("/proc/{pid}/mountinfo");
356 let file = std::fs::File::open(filename)?;
357 let reader = BufReader::new(file);
358 for line in reader.lines() {
359 let line = line?;
360 let parts = line.split_once(' ');
361 if parts.map(|(mount_id, _)| mount_id.parse()) == Some(Ok(mnt_id)) {
362 return Ok(CACHE.get_or_insert_owned(line));
363 }
364 }
365 Ok(CACHE.get_or_insert("Not found. This is probably a pipe or something else."))
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
369#[serde(tag = "what", content = "value", rename_all = "kebab-case")]
370pub enum Interpreter {
371 None,
372 Shebang(ArcStr),
373 ExecutableInaccessible,
374 Error(ArcStr),
375}
376
377impl Display for Interpreter {
378 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
379 match self {
380 Self::None => write!(f, "{}", "none".bold()),
381 Self::Shebang(s) => write!(f, "{s:?}"),
382 Self::ExecutableInaccessible => {
383 write!(f, "{}", "executable inaccessible".red().bold())
384 }
385 Self::Error(e) => write!(f, "({}: {})", "err".red().bold(), e.red().bold()),
386 }
387 }
388}
389
390pub fn read_interpreter_recursive(exe: impl AsRef<Path>) -> Vec<Interpreter> {
391 let mut exe = Cow::Borrowed(exe.as_ref());
392 let mut interpreters = Vec::new();
393 loop {
394 match read_interpreter(exe.as_ref()) {
395 Interpreter::Shebang(shebang) => {
396 exe = Cow::Owned(PathBuf::from(
397 shebang.split_ascii_whitespace().next().unwrap_or(""),
398 ));
399 interpreters.push(Interpreter::Shebang(shebang));
400 }
401 Interpreter::None => break,
402 err => {
403 interpreters.push(err);
404 break;
405 }
406 };
407 }
408 interpreters
409}
410
411pub fn read_interpreter(exe: &Path) -> Interpreter {
412 fn err_to_interpreter(e: io::Error) -> Interpreter {
413 if e.kind() == io::ErrorKind::PermissionDenied || e.kind() == io::ErrorKind::NotFound {
414 Interpreter::ExecutableInaccessible
415 } else {
416 let e = CACHE.get_or_insert_owned(e.to_string());
417 Interpreter::Error(e)
418 }
419 }
420 let file = match std::fs::File::open(exe) {
421 Ok(file) => file,
422 Err(e) => return err_to_interpreter(e),
423 };
424 let mut reader = BufReader::new(file);
425 let mut buf = [0u8; 2];
427
428 if let Err(e) = reader.read_exact(&mut buf) {
429 if e.kind() == std::io::ErrorKind::UnexpectedEof {
430 return Interpreter::None;
432 }
433 let e = CACHE.get_or_insert_owned(e.to_string());
434 return Interpreter::Error(e);
435 };
436 if &buf != b"#!" {
437 return Interpreter::None;
438 }
439 let mut buf = Vec::new();
441
442 if let Err(e) = reader.read_until(b'\n', &mut buf) {
443 let e = CACHE.get_or_insert_owned(e.to_string());
444 return Interpreter::Error(e);
445 };
446 let start = buf
449 .iter()
450 .position(|&c| !c.is_ascii_whitespace())
451 .unwrap_or(0);
452 let end = buf
453 .iter()
454 .rposition(|&c| !c.is_ascii_whitespace())
455 .map(|x| x + 1)
456 .unwrap_or(buf.len());
457 let shebang = String::from_utf8_lossy(&buf[start..end]);
458 let shebang = CACHE.get_or_insert(&shebang);
459 Interpreter::Shebang(shebang)
460}
461
462pub fn parse_env_entry(item: &str) -> (&str, &str) {
463 let Some(mut sep_loc) = item.as_bytes().iter().position(|&x| x == b'=') else {
465 warn!(
466 "Invalid envp entry: {:?}, assuming value to empty string!",
467 item
468 );
469 return (item, "");
470 };
471 if sep_loc == 0 {
472 sep_loc = item
474 .as_bytes()
475 .iter()
476 .skip(1)
477 .position(|&x| x == b'=')
478 .unwrap_or_else(|| {
479 warn!(
480 "Invalid envp entry starting with '=': {:?}, assuming value to empty string!",
481 item
482 );
483 item.len()
484 });
485 }
486 let (head, tail) = item.split_at(sep_loc);
487 (head, { if tail.is_empty() { "" } else { &tail[1..] } })
488}
489
490pub fn parse_failiable_envp(envp: Vec<OutputMsg>) -> (BTreeMap<OutputMsg, OutputMsg>, bool) {
491 let mut has_dash_var = false;
492 (
493 envp
494 .into_iter()
495 .map(|entry| {
496 if let OutputMsg::Ok(s) | OutputMsg::PartialOk(s) = entry {
497 let (key, value) = parse_env_entry(&s);
498 if key.starts_with('-') {
499 has_dash_var = true;
500 }
501 (
502 OutputMsg::Ok(CACHE.get_or_insert(key)),
503 OutputMsg::Ok(CACHE.get_or_insert(value)),
504 )
505 } else {
506 (entry.clone(), entry)
507 }
508 })
509 .collect(),
510 has_dash_var,
511 )
512}
513
514pub fn cached_str(s: &str) -> ArcStr {
515 CACHE.get_or_insert(s)
516}
517
518pub fn cached_string(s: String) -> ArcStr {
519 CACHE.get_or_insert_owned(s)
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
523pub struct EnvDiff {
524 has_added_or_modified_keys_starting_with_dash: bool,
525 pub added: BTreeMap<OutputMsg, OutputMsg>,
526 pub removed: BTreeSet<OutputMsg>,
527 pub modified: BTreeMap<OutputMsg, OutputMsg>,
528}
529
530impl EnvDiff {
531 #[cfg(test)]
532 pub(crate) fn empty() -> Self {
533 Self {
534 has_added_or_modified_keys_starting_with_dash: Default::default(),
535 added: Default::default(),
536 removed: Default::default(),
537 modified: Default::default(),
538 }
539 }
540
541 pub fn is_modified_or_removed(&self, key: &OutputMsg) -> bool {
542 self.modified.contains_key(key) || self.removed.contains(key)
543 }
544
545 pub fn need_env_argument_separator(&self) -> bool {
547 self.has_added_or_modified_keys_starting_with_dash
548 }
549}
550
551pub fn diff_env(
552 original: &BTreeMap<OutputMsg, OutputMsg>,
553 envp: &BTreeMap<OutputMsg, OutputMsg>,
554) -> EnvDiff {
555 let mut added = BTreeMap::new();
556 let mut modified = BTreeMap::<OutputMsg, OutputMsg>::new();
557 let mut removed: HashSet<OutputMsg> = original.keys().cloned().collect();
559 let mut has_added_or_modified_keys_starting_with_dash = false;
560 for (key, value) in envp.iter() {
561 if let Some(orig_v) = original.get(key) {
564 if orig_v != value {
565 modified.insert(key.clone(), value.clone());
566 if key.as_ref().starts_with('-') {
567 has_added_or_modified_keys_starting_with_dash = true;
568 }
569 }
570 removed.remove(key);
571 } else {
572 added.insert(key.clone(), value.clone());
573 if key.as_ref().starts_with('-') {
574 has_added_or_modified_keys_starting_with_dash = true;
575 }
576 }
577 }
578 EnvDiff {
579 has_added_or_modified_keys_starting_with_dash,
580 added,
581 removed: removed.into_iter().collect(),
582 modified,
583 }
584}
585
586#[derive(Debug, Clone, Serialize)]
587pub struct BaselineInfo {
588 pub cwd: OutputMsg,
589 pub env: BTreeMap<OutputMsg, OutputMsg>,
590 pub fdinfo: FileDescriptorInfoCollection,
591}
592
593impl BaselineInfo {
594 pub fn new() -> color_eyre::Result<Self> {
595 let cwd = cached_str(&std::env::current_dir()?.to_string_lossy()).into();
596 let env = std::env::vars()
597 .map(|(k, v)| {
598 (
599 CACHE.get_or_insert_owned(k).into(),
600 CACHE.get_or_insert_owned(v).into(),
601 )
602 })
603 .collect();
604 let fdinfo = FileDescriptorInfoCollection::new_baseline()?;
605 Ok(Self { cwd, env, fdinfo })
606 }
607
608 pub fn with_pts(pts: &UnixSlavePty) -> color_eyre::Result<Self> {
609 let cwd = cached_str(&std::env::current_dir()?.to_string_lossy()).into();
610 let env = std::env::vars()
611 .map(|(k, v)| {
612 (
613 CACHE.get_or_insert_owned(k).into(),
614 CACHE.get_or_insert_owned(v).into(),
615 )
616 })
617 .collect();
618 let fdinfo = FileDescriptorInfoCollection::with_pts(pts)?;
619 Ok(Self { cwd, env, fdinfo })
620 }
621}
622
623static CACHE: StringCache = StringCache;
624
625#[cfg(test)]
626mod proc_status_tests {
627 use super::*;
628
629 #[test]
630 fn test_parse_status_contents_valid() {
631 let sample = "\
632Name:\ttestproc
633State:\tR (running)
634Uid:\t1000\t1001\t1002\t1003
635Gid:\t2000\t2001\t2002\t2003
636Threads:\t1
637Groups:\t0\t1\t2
638";
639
640 let status = parse_status_contents(sample).unwrap();
641 assert_eq!(
642 status,
643 ProcStatus {
644 cred: Cred {
645 groups: vec![0, 1, 2],
646 uid_real: 1000,
647 uid_effective: 1001,
648 uid_saved_set: 1002,
649 uid_fs: 1003,
650 gid_real: 2000,
651 gid_effective: 2001,
652 gid_saved_set: 2002,
653 gid_fs: 2003,
654 }
655 }
656 );
657 }
658
659 #[test]
660 fn test_parse_status_contents_missing_gid() {
661 let sample = "Uid:\t1\t2\t3\t4\nGroups:\t0\n";
662 let e = parse_status_contents(sample).unwrap_err();
663 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
664 }
665
666 #[test]
667 fn test_parse_status_contents_missing_groups() {
668 let sample = "Uid:\t1\t2\t3\t4\nGid:\t0\t1\t2\t3\n";
669 let e = parse_status_contents(sample).unwrap_err();
670 assert_eq!(e.kind(), std::io::ErrorKind::InvalidData);
671 }
672
673 #[test]
674 fn test_parse_status_contents_non_numeric_uid() {
675 let sample = "\
676Uid:\ta\t2\t3\t4
677Gid:\t1\t2\t3\t4
678Groups:\t0
679";
680 let err = parse_status_contents(sample).unwrap_err();
681 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
682 }
683
684 #[test]
685 fn test_parse_status_contents_not_enough_uids() {
686 let sample = "\
687Uid:\t1\t2
688Gid:\t1\t2\t3\t4
689Groups:\t0
690";
691 let err = parse_status_contents(sample).unwrap_err();
692 assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
693 }
694}
695
696#[cfg(test)]
697mod env_tests {
698 use super::*;
699 use crate::event::FriendlyError;
700
701 #[test]
702 fn test_parse_env_entry_normal() {
703 let (k, v) = parse_env_entry("KEY=value");
704 assert_eq!(k, "KEY");
705 assert_eq!(v, "value");
706 }
707
708 #[test]
709 fn test_parse_env_entry_missing_equal() {
710 let (k, v) = parse_env_entry("KEY");
711 assert_eq!(k, "KEY");
712 assert_eq!(v, "");
713 }
714
715 #[test]
716 fn test_parse_env_entry_leading_equal() {
717 let (k, v) = parse_env_entry("=value");
718 assert_eq!(k, "=value");
719 assert_eq!(v, "");
720 }
721
722 #[test]
723 fn test_parse_env_entry_multiple_equals() {
724 let (k, v) = parse_env_entry("A=B=C");
725 assert_eq!(k, "A");
726 assert_eq!(v, "B=C");
727 }
728
729 #[test]
730 fn test_parse_failiable_envp_basic() {
731 let envp = vec![OutputMsg::Ok("A=1".into()), OutputMsg::Ok("B=2".into())];
732
733 let (map, has_dash) = parse_failiable_envp(envp);
734
735 assert!(!has_dash);
736 assert_eq!(map.len(), 2);
737 assert_eq!(
738 map.get(&OutputMsg::Ok("A".into())).unwrap(),
739 &OutputMsg::Ok("1".into())
740 );
741 }
742
743 #[test]
744 fn test_parse_failiable_envp_dash_key() {
745 let envp = vec![OutputMsg::Ok("-X=1".into())];
746
747 let (_map, has_dash) = parse_failiable_envp(envp);
748
749 assert!(has_dash);
750 }
751
752 #[test]
753 fn test_parse_failiable_envp_error_passthrough() {
754 let envp = vec![OutputMsg::Err(FriendlyError::InspectError(
755 nix::errno::Errno::EAGAIN,
756 ))];
757
758 let (map, _) = parse_failiable_envp(envp);
759
760 assert!(matches!(
761 map.values().next().unwrap(),
762 OutputMsg::Err(FriendlyError::InspectError(nix::errno::Errno::EAGAIN))
763 ));
764 }
765}
766
767#[cfg(test)]
768mod env_diff_tests {
769 use std::collections::BTreeMap;
770
771 use crate::{
772 event::OutputMsg,
773 proc::diff_env,
774 };
775
776 #[test]
777 fn test_env_diff_added_removed_modified() {
778 let orig = BTreeMap::from([
779 (OutputMsg::Ok("A".into()), OutputMsg::Ok("1".into())),
780 (OutputMsg::Ok("B".into()), OutputMsg::Ok("2".into())),
781 ]);
782
783 let new = BTreeMap::from([
784 (OutputMsg::Ok("A".into()), OutputMsg::Ok("10".into())),
785 (OutputMsg::Ok("C".into()), OutputMsg::Ok("3".into())),
786 ]);
787
788 let diff = diff_env(&orig, &new);
789
790 assert_eq!(diff.modified.len(), 1);
791 assert_eq!(diff.added.len(), 1);
792 assert_eq!(diff.removed.len(), 1);
793
794 assert!(diff.modified.contains_key(&OutputMsg::Ok("A".into())));
795 assert!(diff.added.contains_key(&OutputMsg::Ok("C".into())));
796 assert!(diff.removed.contains(&OutputMsg::Ok("B".into())));
797 }
798
799 #[test]
800 fn test_env_diff_dash_key_requires_separator() {
801 let orig = BTreeMap::new();
802 let new = BTreeMap::from([(
803 OutputMsg::Ok("-LD_PRELOAD".into()),
804 OutputMsg::Ok("evil.so".into()),
805 )]);
806
807 let diff = diff_env(&orig, &new);
808
809 assert!(diff.need_env_argument_separator());
810 }
811}
812
813#[cfg(test)]
814mod fdinfo_tests {
815 use crate::proc::FileDescriptorInfo;
816
817 #[test]
818 fn test_fdinfo_same_file() {
819 let a = FileDescriptorInfo {
820 ino: 1,
821 mnt_id: 2,
822 ..Default::default()
823 };
824
825 let b = FileDescriptorInfo {
826 ino: 1,
827 mnt_id: 2,
828 ..Default::default()
829 };
830
831 assert!(a.same_file_as(&b));
832 assert!(!a.not_same_file_as(&b));
833 }
834
835 #[test]
836 fn test_fdinfo_not_same_file() {
837 let a = FileDescriptorInfo {
838 ino: 1,
839 mnt_id: 2,
840 ..Default::default()
841 };
842
843 let b = FileDescriptorInfo {
844 ino: 3,
845 mnt_id: 2,
846 ..Default::default()
847 };
848
849 assert!(a.not_same_file_as(&b));
850 }
851}
852
853#[cfg(test)]
854mod interpreter_test {
855 use std::{
856 fs::{
857 self,
858 File,
859 },
860 io::Write,
861 os::unix::fs::PermissionsExt,
862 };
863
864 use tempfile::tempdir;
865
866 use crate::proc::{
867 Interpreter,
868 cached_str,
869 read_interpreter,
870 read_interpreter_recursive,
871 };
872
873 #[test]
874 fn test_interpreter_display() {
875 let none = Interpreter::None;
876 assert!(none.to_string().contains("none"));
877
878 let err = Interpreter::Error(cached_str("boom"));
879 assert!(err.to_string().contains("err"));
880 }
881
882 #[test]
883 fn test_read_interpreter_none() {
884 let dir = tempdir().unwrap();
885 let exe = dir.path().join("binary");
886 File::create(&exe).unwrap();
887
888 let result = read_interpreter(&exe);
889 assert_eq!(result, Interpreter::None);
890 dir.close().unwrap();
891 }
892
893 #[test]
894 fn test_read_interpreter_shebang() {
895 let dir = tempdir().unwrap();
896
897 let target = dir.path().join("target");
898 File::create(&target).unwrap();
899 fs::set_permissions(&target, fs::Permissions::from_mode(0o755)).unwrap();
900
901 let script = dir.path().join("script");
902 let mut f = File::create(&script).unwrap();
903 writeln!(f, "#!{}", target.display()).unwrap();
904
905 let result = read_interpreter(&script);
906 match result {
907 Interpreter::Shebang(s) => assert!(s.as_ref().ends_with("target")),
908 other => panic!("unexpected result: {other:?}"),
909 }
910 dir.close().unwrap();
911 }
912
913 #[test]
914 fn test_read_interpreter_inaccessible() {
915 let dir = tempdir().unwrap();
916 let exe = dir.path().join("noaccess");
917 File::create(&exe).unwrap();
918 fs::set_permissions(&exe, fs::Permissions::from_mode(0o000)).unwrap();
919
920 let result = read_interpreter(&exe);
921 assert_eq!(result, Interpreter::ExecutableInaccessible);
922 dir.close().unwrap();
923 }
924
925 #[test]
926 fn test_read_interpreter_empty_file() {
927 let dir = tempdir().unwrap();
928 let exe = dir.path().join("empty");
929 File::create(&exe).unwrap();
930
931 let result = read_interpreter(&exe);
932 assert_eq!(result, Interpreter::None);
933 dir.close().unwrap();
934 }
935
936 #[test]
937 fn test_read_interpreter_recursive_shebang_chain() {
938 use std::{
939 fs::{
940 self,
941 File,
942 },
943 io::Write,
944 };
945
946 use tempfile::tempdir;
947
948 use super::read_interpreter_recursive;
949
950 let dir = tempdir().unwrap();
951
952 let interp2 = dir.path().join("interp2");
955 File::create(&interp2).unwrap();
956 fs::set_permissions(&interp2, fs::Permissions::from_mode(0o755)).unwrap();
957
958 let interp1 = dir.path().join("interp1");
960 {
961 let mut f = File::create(&interp1).unwrap();
962 writeln!(f, "#!{}", interp2.display()).unwrap();
963 f.flush().unwrap();
964 }
965 fs::set_permissions(&interp1, fs::Permissions::from_mode(0o755)).unwrap();
966
967 let script = dir.path().join("script");
969 {
970 let mut f = File::create(&script).unwrap();
971 writeln!(f, "#!{}", interp1.display()).unwrap();
972 f.flush().unwrap();
973 }
974 fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).unwrap();
975
976 let result = read_interpreter_recursive(&script);
977
978 assert_eq!(result.len(), 2);
979
980 match &result[0] {
981 Interpreter::Shebang(s) => {
982 assert!(s.as_ref().ends_with("interp1"));
983 }
984 other => panic!("unexpected interpreter: {other:?}"),
985 }
986
987 match &result[1] {
988 Interpreter::Shebang(s) => {
989 assert!(s.as_ref().ends_with("interp2"));
990 }
991 other => panic!("unexpected interpreter: {other:?}"),
992 }
993
994 dir.close().unwrap();
995 }
996
997 #[test]
998 fn test_read_interpreter_recursive_no_shebang() {
999 use std::fs::File;
1000
1001 use tempfile::tempdir;
1002
1003 let dir = tempdir().unwrap();
1004 let exe = dir.path().join("binary");
1005 File::create(&exe).unwrap();
1006
1007 let result = read_interpreter_recursive(&exe);
1008 assert!(result.is_empty());
1009 dir.close().unwrap();
1010 }
1011}