1use std::path::{Path, PathBuf};
2
3use serde::{Deserialize, Serialize};
4use soma_ops::Timestamp;
5
6use crate::{RequestError, request::validate_absolute_path};
7
8const MAX_PROGRAM_CHARS: usize = 4096;
9const MAX_ARGUMENT_CHARS: usize = 4096;
10const MAX_ARGUMENTS: usize = 320;
12const MAX_OUTPUT_BYTES: usize = 16 * 1024 * 1024;
13const MAX_STDIN_BYTES: usize = 64 * 1024 * 1024;
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct CommandRequest {
18 program: String,
19 args: Vec<String>,
20 working_dir: Option<PathBuf>,
21 stdin: Option<Vec<u8>>,
22 deadline: Timestamp,
23 max_stdout_bytes: usize,
24 max_stderr_bytes: usize,
25}
26
27impl CommandRequest {
28 pub fn new<I, S>(
30 program: impl Into<String>,
31 args: I,
32 deadline: Timestamp,
33 ) -> Result<Self, RequestError>
34 where
35 I: IntoIterator<Item = S>,
36 S: Into<String>,
37 {
38 let program = program.into();
39 validate_program(&program)?;
40 let args = args.into_iter().map(Into::into).collect::<Vec<_>>();
41 if args.len() > MAX_ARGUMENTS {
42 return Err(RequestError::TooManyArguments {
43 count: args.len(),
44 max: MAX_ARGUMENTS,
45 });
46 }
47 for (index, argument) in args.iter().enumerate() {
48 if !valid_text(argument, MAX_ARGUMENT_CHARS) {
49 return Err(RequestError::InvalidArgument { index });
50 }
51 }
52 Ok(Self {
53 program,
54 args,
55 working_dir: None,
56 stdin: None,
57 deadline,
58 max_stdout_bytes: 256 * 1024,
59 max_stderr_bytes: 256 * 1024,
60 })
61 }
62
63 pub fn with_working_dir(
65 mut self,
66 working_dir: impl Into<PathBuf>,
67 ) -> Result<Self, RequestError> {
68 self.working_dir = Some(validate_absolute_path(working_dir.into())?);
69 Ok(self)
70 }
71
72 pub fn with_stdin(mut self, stdin: Vec<u8>) -> Result<Self, RequestError> {
74 if stdin.len() > MAX_STDIN_BYTES {
75 return Err(RequestError::InvalidStdinLimit {
76 bytes: stdin.len(),
77 max: MAX_STDIN_BYTES,
78 });
79 }
80 self.stdin = Some(stdin);
81 Ok(self)
82 }
83
84 pub fn with_output_limits(
86 mut self,
87 stdout_bytes: usize,
88 stderr_bytes: usize,
89 ) -> Result<Self, RequestError> {
90 validate_output_limit("stdout", stdout_bytes)?;
91 validate_output_limit("stderr", stderr_bytes)?;
92 self.max_stdout_bytes = stdout_bytes;
93 self.max_stderr_bytes = stderr_bytes;
94 Ok(self)
95 }
96
97 pub fn validate_at(&self, now: Timestamp) -> Result<(), RequestError> {
99 if self.deadline <= now {
100 Err(RequestError::DeadlineElapsed)
101 } else {
102 Ok(())
103 }
104 }
105
106 #[must_use]
108 pub fn program(&self) -> &str {
109 &self.program
110 }
111
112 #[must_use]
114 pub fn args(&self) -> &[String] {
115 &self.args
116 }
117
118 #[must_use]
120 pub fn working_dir(&self) -> Option<&Path> {
121 self.working_dir.as_deref()
122 }
123
124 #[must_use]
126 pub fn stdin(&self) -> Option<&[u8]> {
127 self.stdin.as_deref()
128 }
129
130 #[must_use]
132 pub const fn deadline(&self) -> Timestamp {
133 self.deadline
134 }
135
136 #[must_use]
138 pub const fn max_stdout_bytes(&self) -> usize {
139 self.max_stdout_bytes
140 }
141
142 #[must_use]
144 pub const fn max_stderr_bytes(&self) -> usize {
145 self.max_stderr_bytes
146 }
147}
148
149#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
151pub struct CommandOutput {
152 stdout: Vec<u8>,
153 stderr: Vec<u8>,
154 exit_code: Option<i32>,
155 truncated: bool,
156}
157
158impl CommandOutput {
159 #[must_use]
161 pub fn new(stdout: Vec<u8>, stderr: Vec<u8>, exit_code: Option<i32>, truncated: bool) -> Self {
162 Self {
163 stdout,
164 stderr,
165 exit_code,
166 truncated,
167 }
168 }
169
170 #[must_use]
172 pub fn stdout(&self) -> &[u8] {
173 &self.stdout
174 }
175
176 #[must_use]
178 pub fn stderr(&self) -> &[u8] {
179 &self.stderr
180 }
181
182 #[must_use]
184 pub const fn exit_code(&self) -> Option<i32> {
185 self.exit_code
186 }
187
188 #[must_use]
190 pub const fn truncated(&self) -> bool {
191 self.truncated
192 }
193}
194
195fn validate_program(program: &str) -> Result<(), RequestError> {
196 if valid_text(program, MAX_PROGRAM_CHARS) {
197 Ok(())
198 } else {
199 Err(RequestError::InvalidProgram)
200 }
201}
202
203fn valid_text(value: &str, max_chars: usize) -> bool {
204 let count = value.chars().count();
205 count > 0 && count <= max_chars && !value.chars().any(char::is_control)
206}
207
208fn validate_output_limit(stream: &'static str, bytes: usize) -> Result<(), RequestError> {
209 if bytes == 0 || bytes > MAX_OUTPUT_BYTES {
210 Err(RequestError::InvalidOutputLimit { stream, bytes })
211 } else {
212 Ok(())
213 }
214}
215#[cfg(test)]
216#[path = "command_tests.rs"]
217mod tests;