rust_expect/
validation.rs1use crate::error::{ExpectError, SpawnError};
7
8pub const SHELL_METACHARACTERS: &[char] = &[
15 ';', '&', '|', '`', '$', '(', ')', '{', '}', '[', ']', '<', '>', '!', '*', '?', '#', '~', '\\',
16 '"', '\'', '\n', '\r',
17];
18
19#[derive(Debug, Clone, Default)]
21pub struct ValidationOptions {
22 pub reject_null_bytes: bool,
24 pub reject_shell_metacharacters: bool,
26 pub reject_empty: bool,
28}
29
30impl ValidationOptions {
31 #[must_use]
33 pub const fn strict() -> Self {
34 Self {
35 reject_null_bytes: true,
36 reject_shell_metacharacters: false,
37 reject_empty: true,
38 }
39 }
40
41 #[must_use]
43 pub const fn paranoid() -> Self {
44 Self {
45 reject_null_bytes: true,
46 reject_shell_metacharacters: true,
47 reject_empty: true,
48 }
49 }
50
51 #[must_use]
53 pub const fn permissive() -> Self {
54 Self {
55 reject_null_bytes: true,
56 reject_shell_metacharacters: false,
57 reject_empty: false,
58 }
59 }
60}
61
62#[must_use]
64pub fn contains_null_byte(s: &str) -> bool {
65 s.contains('\0')
66}
67
68#[must_use]
70pub fn contains_shell_metachar(s: &str) -> bool {
71 s.chars().any(|c| SHELL_METACHARACTERS.contains(&c))
72}
73
74#[must_use]
76pub fn find_shell_metachar(s: &str) -> Option<char> {
77 s.chars().find(|c| SHELL_METACHARACTERS.contains(c))
78}
79
80pub fn validate_command(command: &str, options: &ValidationOptions) -> crate::error::Result<()> {
91 if options.reject_empty && command.is_empty() {
92 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
93 kind: "command".to_string(),
94 value: String::new(),
95 reason: "command cannot be empty".to_string(),
96 }));
97 }
98
99 if options.reject_null_bytes && contains_null_byte(command) {
100 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
101 kind: "command".to_string(),
102 value: command.to_string(),
103 reason: "command contains null byte".to_string(),
104 }));
105 }
106
107 if options.reject_shell_metacharacters
108 && let Some(c) = find_shell_metachar(command)
109 {
110 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
111 kind: "command".to_string(),
112 value: command.to_string(),
113 reason: format!("command contains shell metacharacter '{c}'"),
114 }));
115 }
116
117 Ok(())
118}
119
120pub fn validate_argument(arg: &str, options: &ValidationOptions) -> crate::error::Result<()> {
131 if options.reject_null_bytes && contains_null_byte(arg) {
132 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
133 kind: "argument".to_string(),
134 value: arg.to_string(),
135 reason: "argument contains null byte".to_string(),
136 }));
137 }
138
139 if options.reject_shell_metacharacters
140 && let Some(c) = find_shell_metachar(arg)
141 {
142 return Err(ExpectError::Spawn(SpawnError::InvalidArgument {
143 kind: "argument".to_string(),
144 value: arg.to_string(),
145 reason: format!("argument contains shell metacharacter '{c}'"),
146 }));
147 }
148
149 Ok(())
150}
151
152pub fn validate_command_with_args<I, S>(
164 command: &str,
165 args: I,
166 options: &ValidationOptions,
167) -> crate::error::Result<()>
168where
169 I: IntoIterator<Item = S>,
170 S: AsRef<str>,
171{
172 validate_command(command, options)?;
173
174 for arg in args {
175 validate_argument(arg.as_ref(), options)?;
176 }
177
178 Ok(())
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn test_null_byte_detection() {
187 assert!(contains_null_byte("hello\0world"));
188 assert!(!contains_null_byte("hello world"));
189 }
190
191 #[test]
192 fn test_shell_metachar_detection() {
193 assert!(contains_shell_metachar("echo; rm -rf"));
194 assert!(contains_shell_metachar("$(whoami)"));
195 assert!(contains_shell_metachar("hello | world"));
196 assert!(!contains_shell_metachar("hello_world"));
197 assert!(!contains_shell_metachar("/usr/bin/test"));
198 }
199
200 #[test]
201 fn test_validate_command_null_byte() {
202 let opts = ValidationOptions::strict();
203 assert!(validate_command("test\0cmd", &opts).is_err());
204 assert!(validate_command("test_cmd", &opts).is_ok());
205 }
206
207 #[test]
208 fn test_validate_command_empty() {
209 let strict = ValidationOptions::strict();
210 let permissive = ValidationOptions::permissive();
211
212 assert!(validate_command("", &strict).is_err());
213 assert!(validate_command("", &permissive).is_ok());
214 }
215
216 #[test]
217 fn test_validate_command_metachar() {
218 let paranoid = ValidationOptions::paranoid();
219 let strict = ValidationOptions::strict();
220
221 assert!(validate_command("echo; rm", ¶noid).is_err());
222 assert!(validate_command("echo; rm", &strict).is_ok());
223 }
224
225 #[test]
226 fn test_validate_argument() {
227 let opts = ValidationOptions::strict();
228 assert!(validate_argument("normal_arg", &opts).is_ok());
229 assert!(validate_argument("--flag", &opts).is_ok());
230 assert!(validate_argument("arg\0value", &opts).is_err());
231 }
232
233 #[test]
234 fn test_validate_command_with_args() {
235 let opts = ValidationOptions::strict();
236
237 assert!(validate_command_with_args("/bin/echo", ["hello", "world"], &opts).is_ok());
238 assert!(validate_command_with_args("/bin/echo", ["hello\0world"], &opts).is_err());
239 }
240}