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}