Skip to main content

cageforge_command/
command.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Command-line values for [`crate::CommandRequest`].
4//!
5//! [`crate::CommandSpec`] preserves the program and argv as native
6//! `OsString` values. Validation is local to construction, while process
7//! launching remains the responsibility of the adapter that consumes the
8//! request.
9
10use std::ffi::{OsStr, OsString};
11
12use crate::CommandError;
13
14/// An executable and its argv arguments.
15///
16/// The program and arguments use [`OsString`] so a local harness can preserve
17/// platform-native command-line values. This type does not interpret shell
18/// syntax; callers that need a shell must put the shell executable and its
19/// arguments in the vector explicitly.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct CommandSpec {
22    program: OsString,
23    args: Vec<OsString>,
24}
25
26impl CommandSpec {
27    /// Creates a command with no arguments.
28    pub fn new(program: impl Into<OsString>) -> Result<Self, CommandError> {
29        let program = program.into();
30        validate_program(&program)?;
31        Ok(Self {
32            program,
33            args: Vec::new(),
34        })
35    }
36
37    /// Adds one argv argument and returns the updated command.
38    pub fn with_arg(mut self, argument: impl Into<OsString>) -> Result<Self, CommandError> {
39        let argument = argument.into();
40        validate_argument(&argument)?;
41        self.args.push(argument);
42        Ok(self)
43    }
44
45    /// Adds several argv arguments and returns the updated command.
46    pub fn with_args<I, S>(mut self, arguments: I) -> Result<Self, CommandError>
47    where
48        I: IntoIterator<Item = S>,
49        S: Into<OsString>,
50    {
51        for argument in arguments {
52            let argument = argument.into();
53            validate_argument(&argument)?;
54            self.args.push(argument);
55        }
56        Ok(self)
57    }
58
59    /// Returns the executable program.
60    pub fn program(&self) -> &OsStr {
61        &self.program
62    }
63
64    /// Returns the arguments after the executable program.
65    pub fn args(&self) -> &[OsString] {
66        &self.args
67    }
68
69    /// Returns the executable and arguments as owned values.
70    pub fn into_parts(self) -> (OsString, Vec<OsString>) {
71        (self.program, self.args)
72    }
73}
74
75fn validate_program(program: &OsStr) -> Result<(), CommandError> {
76    if program.is_empty() {
77        return Err(CommandError::EmptyProgram);
78    }
79    if contains_nul(program) {
80        return Err(CommandError::ProgramContainsNul);
81    }
82    Ok(())
83}
84
85fn validate_argument(argument: &OsStr) -> Result<(), CommandError> {
86    if contains_nul(argument) {
87        return Err(CommandError::ArgumentContainsNul);
88    }
89    Ok(())
90}
91
92pub(crate) fn contains_nul(value: &OsStr) -> bool {
93    #[cfg(unix)]
94    {
95        use std::os::unix::ffi::OsStrExt;
96        value.as_bytes().contains(&0)
97    }
98    #[cfg(windows)]
99    {
100        use std::os::windows::ffi::OsStrExt;
101        value.encode_wide().any(|unit| unit == 0)
102    }
103    #[cfg(not(any(unix, windows)))]
104    {
105        value.to_string_lossy().contains('\0')
106    }
107}