Skip to main content

ferroday_cage/
status.rs

1//! The exit status of a sandboxed command.
2
3use std::fmt;
4
5/// How the sandboxed command terminated.
6///
7/// Returned by [`Cage::run`] whenever the command was executed, regardless of
8/// its exit code: a non-zero exit is data, not an error. The status reflects
9/// either a normal exit with a code or termination by a signal, exactly one
10/// of which applies.
11///
12/// [`Cage::run`]: crate::Cage::run
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct ExitStatus(Kind);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17enum Kind {
18    Exited(i32),
19    Signaled(i32),
20}
21
22impl ExitStatus {
23    /// A status for a command that exited normally with `code`.
24    pub(crate) fn exited(code: i32) -> ExitStatus {
25        ExitStatus(Kind::Exited(code))
26    }
27
28    /// A status for a command terminated by signal number `signal`.
29    pub(crate) fn signaled(signal: i32) -> ExitStatus {
30        ExitStatus(Kind::Signaled(signal))
31    }
32
33    /// Returns `true` if the command exited normally with code `0`.
34    pub fn success(&self) -> bool {
35        self.code() == Some(0)
36    }
37
38    /// The exit code, if the command exited normally.
39    pub fn code(&self) -> Option<i32> {
40        match self.0 {
41            Kind::Exited(code) => Some(code),
42            Kind::Signaled(_) => None,
43        }
44    }
45
46    /// The signal number that terminated the command, if it was terminated
47    /// by a signal.
48    pub fn signal(&self) -> Option<i32> {
49        match self.0 {
50            Kind::Exited(_) => None,
51            Kind::Signaled(signal) => Some(signal),
52        }
53    }
54
55    /// The status as a process exit code, following the shell's convention.
56    ///
57    /// A command that exited reports its own code; a command a signal
58    /// terminated reports `128 + signal`, which is what `sh` reports in `$?`
59    /// and what a caller propagating the outcome to its own exit status wants.
60    /// This is what the `fcage` binary returns, and what a consumer that
61    /// re-exports a sandboxed command's outcome should return.
62    ///
63    /// The result is a `u8` because that is the width a process exit status
64    /// has: `wait` carries eight bits, so a code outside `0..=255` was never
65    /// reachable and no case is lost by narrowing here.
66    ///
67    /// # Example
68    ///
69    /// ```no_run
70    /// # fn main() -> Result<(), ferroday_cage::Error> {
71    /// # #[cfg(feature = "tarball")] {
72    /// use std::process::ExitCode;
73    ///
74    /// use ferroday_cage::Cage;
75    ///
76    /// # let rootfs = "/var/cache/myapp/rootfs";
77    /// let status = Cage::builder().command("/bin/false").rootfs(rootfs).build()?.run()?;
78    /// let _code = ExitCode::from(status.shell_code());
79    /// # }
80    /// # Ok(())
81    /// # }
82    /// ```
83    pub fn shell_code(&self) -> u8 {
84        match self.0 {
85            Kind::Exited(code) => code as u8,
86            // A signal number never approaches 128, so the sum is in range;
87            // the wrapping form states what happens to a nonsensical one
88            // rather than leaving it to a debug-only panic.
89            Kind::Signaled(signal) => 128_u8.wrapping_add(signal as u8),
90        }
91    }
92}
93
94impl fmt::Display for ExitStatus {
95    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96        match self.0 {
97            Kind::Exited(code) => write!(f, "exit code {code}"),
98            Kind::Signaled(signal) => write!(f, "terminated by signal {signal}"),
99        }
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn exited_zero_is_success() {
109        let status = ExitStatus::exited(0);
110        assert!(status.success());
111        assert_eq!(status.code(), Some(0));
112        assert_eq!(status.signal(), None);
113    }
114
115    #[test]
116    fn exited_nonzero_is_not_success() {
117        let status = ExitStatus::exited(42);
118        assert!(!status.success());
119        assert_eq!(status.code(), Some(42));
120        assert_eq!(status.signal(), None);
121    }
122
123    #[test]
124    fn signaled_reports_the_signal_only() {
125        let status = ExitStatus::signaled(9);
126        assert!(!status.success());
127        assert_eq!(status.code(), None);
128        assert_eq!(status.signal(), Some(9));
129    }
130
131    #[test]
132    fn the_shell_code_is_the_exit_code_or_128_plus_the_signal() {
133        assert_eq!(ExitStatus::exited(0).shell_code(), 0);
134        assert_eq!(ExitStatus::exited(42).shell_code(), 42);
135        assert_eq!(ExitStatus::exited(255).shell_code(), 255);
136        // SIGKILL and SIGTERM, the two a caller most often sees.
137        assert_eq!(ExitStatus::signaled(9).shell_code(), 137);
138        assert_eq!(ExitStatus::signaled(15).shell_code(), 143);
139    }
140}