ripht_php_sapi/adapters/
cli.rs1use std::path::{Path, PathBuf};
7
8use crate::execution::ExecutionContext;
9use crate::sapi::ServerVars;
10
11#[cfg(feature = "tracing")]
12use tracing::debug;
13
14#[derive(Debug, Clone)]
16#[non_exhaustive]
17pub enum CliRequestError {
18 ScriptNotFound(PathBuf),
19}
20
21impl std::error::Error for CliRequestError {}
22
23impl std::fmt::Display for CliRequestError {
24 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25 match self {
26 Self::ScriptNotFound(path) => {
27 write!(f, "Script not found: {}", path.display())
28 }
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
37pub struct CliRequest {
38 stdin: Vec<u8>,
39 argv: Vec<String>,
40 working_dir: Option<PathBuf>,
41 env_vars: Vec<(String, String)>,
42 ini_overrides: Vec<(String, String)>,
43}
44
45impl Default for CliRequest {
46 fn default() -> Self {
47 Self {
48 stdin: Default::default(),
49 argv: Default::default(),
50 working_dir: Default::default(),
51 env_vars: Default::default(),
52 ini_overrides: vec![
53 ("html_errors".to_string(), "0".to_string()),
54 ("display_errors".to_string(), "1".to_string()),
55 ("implicit_flush".to_string(), "1".to_string()),
56 ("max_input_time".to_string(), "-1".to_string()),
57 ("output_buffering".to_string(), "0".to_string()),
58 ("max_execution_time".to_string(), "0".to_string()),
59 ],
60 }
61 }
62}
63
64impl CliRequest {
65 #[must_use]
66 pub fn new() -> Self {
67 Self::default()
68 }
69
70 #[must_use]
71 pub fn with_arg(mut self, s: impl Into<String>) -> Self {
72 self.argv.push(s.into());
73 self
74 }
75
76 #[must_use]
77 pub fn with_args<I, S>(mut self, iter: I) -> Self
78 where
79 I: IntoIterator<Item = S>,
80 S: Into<String>,
81 {
82 self.argv.extend(
83 iter.into_iter()
84 .map(|s| s.into()),
85 );
86
87 self
88 }
89
90 #[must_use]
91 pub fn with_stdin(mut self, bytes: impl Into<Vec<u8>>) -> Self {
92 self.stdin = bytes.into();
93 self
94 }
95
96 #[must_use]
97 pub fn with_env(
98 mut self,
99 key: impl Into<String>,
100 value: impl Into<String>,
101 ) -> Self {
102 self.env_vars
103 .push((key.into(), value.into()));
104 self
105 }
106
107 #[must_use]
108 pub fn with_envs<I, K, V>(mut self, iter: I) -> Self
109 where
110 I: IntoIterator<Item = (K, V)>,
111 K: Into<String>,
112 V: Into<String>,
113 {
114 self.env_vars.extend(
115 iter.into_iter()
116 .map(|(k, v)| (k.into(), v.into())),
117 );
118
119 self
120 }
121
122 #[must_use]
123 pub fn with_ini(
124 mut self,
125 key: impl Into<String>,
126 value: impl Into<String>,
127 ) -> Self {
128 self.ini_overrides
129 .push((key.into(), value.into()));
130 self
131 }
132
133 #[must_use]
134 pub fn with_ini_overrides<I, K, V>(mut self, iter: I) -> Self
135 where
136 I: IntoIterator<Item = (K, V)>,
137 K: Into<String>,
138 V: Into<String>,
139 {
140 self.ini_overrides.extend(
141 iter.into_iter()
142 .map(|(k, v)| (k.into(), v.into())),
143 );
144 self
145 }
146
147 #[must_use]
148 pub fn with_working_dir(mut self, path: impl Into<PathBuf>) -> Self {
149 self.working_dir = Some(path.into());
150
151 self
152 }
153
154 pub fn build(
155 self,
156 script_path: impl AsRef<Path>,
157 ) -> Result<ExecutionContext, CliRequestError> {
158 let script_path = script_path
159 .as_ref()
160 .to_path_buf();
161
162 #[cfg(feature = "tracing")]
163 debug!(
164 script_path = %script_path.display(),
165 args = ?self.argv,
166 "Building CLI request"
167 );
168
169 if !script_path.exists() {
170 return Err(CliRequestError::ScriptNotFound(script_path));
171 }
172
173 let script_filename = std::fs::canonicalize(&script_path)
174 .unwrap_or_else(|_| script_path.clone());
175
176 let script_name = script_filename
177 .file_name()
178 .map(|s| {
179 s.to_string_lossy()
180 .into_owned()
181 })
182 .unwrap_or_default();
183
184 let argc = self.argv.len() + 1;
185
186 let argv_str = std::iter::once(script_name.clone())
187 .chain(self.argv.iter().cloned())
188 .collect::<Vec<_>>()
189 .join(" ");
190
191 let mut vars = ServerVars::cli_defaults();
192
193 vars.script_filename(&script_filename)
194 .script_name(&script_name)
195 .path_translated(&script_filename)
196 .argc(argc)
197 .argv(&argv_str);
198
199 if let Some(ref wd) = self.working_dir {
200 vars.pwd(wd);
201 }
202
203 Ok(ExecutionContext {
204 script_path,
205 server_vars: vars,
206 input: self.stdin,
207 env_vars: self.env_vars,
208 ini_overrides: self.ini_overrides,
209 log_to_stderr: true,
210 })
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use std::path::PathBuf;
218
219 fn php_script_path(name: &str) -> PathBuf {
220 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
221 .join("tests/php_scripts")
222 .join(name)
223 }
224
225 #[test]
226 fn test_cli_request_sets_log_to_stderr() {
227 let script_path = php_script_path("hello.php");
228
229 let ctx = CliRequest::new()
230 .build(&script_path)
231 .expect("failed to build CLI request");
232
233 assert!(
234 ctx.log_to_stderr,
235 "CLI request should set log_to_stderr to true"
236 );
237 }
238
239 #[test]
240 fn test_cli_execution_captures_messages() {
241 use crate::RiphtSapi;
242
243 let sapi = RiphtSapi::instance();
244 let script_path = php_script_path("error_log_test.php");
245
246 let ctx = CliRequest::new()
247 .build(&script_path)
248 .expect("failed to build CLI request");
249
250 let result = sapi
251 .execute(ctx)
252 .expect("execution should succeed");
253
254 assert!(
255 !result.messages.is_empty(),
256 "CLI execution should capture error_log messages"
257 );
258
259 assert!(
260 result
261 .messages
262 .iter()
263 .any(|m| m
264 .message
265 .contains("Test error log message")),
266 "Should contain the error_log message"
267 );
268 }
269}