tracexec_core/
proc.rs

1//! This module provides utilities about processing process information(e.g. comm, argv, envp).
2
3use 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(); // remove trailing newline
76  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
316/// Read /proc/{pid}/fdinfo/{fd} to get more information about the file descriptor.
317pub 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  // First, check if it's a shebang script
426  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      // File is too short to contain a shebang
431      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  // Read the rest of the line
440  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  // Get trimmed shebang line [start, end) indices
447  // If the shebang line is empty, we don't care
448  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  // trace!("Parsing envp entry: {:?}", item);
464  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    // Find the next equal sign
473    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  /// Whether we need to use `--` to prevent argument injection
546  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  // Use str to avoid cloning all env vars
558  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    // Too bad that we still don't have if- and while-let-chains
562    // https://github.com/rust-lang/rust/issues/53667
563    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    // interpreter2: real binary (no shebang)
953    // Note: an edge case that the file length does not permit it to contain shebang thus EOF.
954    let interp2 = dir.path().join("interp2");
955    File::create(&interp2).unwrap();
956    fs::set_permissions(&interp2, fs::Permissions::from_mode(0o755)).unwrap();
957
958    // interpreter1: shebang -> interpreter2
959    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    // script: shebang -> interpreter1
968    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}