1use std::ffi::{CStr, CString};
11
12use super::ffi::zend_function_entry;
13use super::native::Function as NativeFunction;
14use super::native_functions;
15use super::SapiError;
16
17const DEFAULT_SAPI_NAME: &str = "ripht";
18const DEFAULT_PRETTY_NAME: &str = "Ripht PHP SAPI";
19const DEFAULT_SERVER_SOFTWARE: &str =
20 concat!("Ripht/", env!("CARGO_PKG_VERSION"));
21
22const DEFAULT_INI_ENTRIES: &[(&str, &str)] = &[
23 ("variables_order", "EGPCS"),
24 ("request_order", "GP"),
25 ("output_buffering", "4096"),
26 ("implicit_flush", "0"),
27 ("html_errors", "0"),
28 ("display_errors", "1"),
29 ("log_errors", "1"),
30];
31
32#[derive(Clone, Debug)]
38#[non_exhaustive]
39pub struct SapiConfig {
40 pub sapi_name: String,
41 pub pretty_name: String,
42 pub server_software: String,
43 pub ini_entries: Vec<(String, String)>,
44 pub ignore_php_ini: bool,
45 pub ignore_cwd_ini: bool,
46 pub ini_path: Option<String>,
47 native_functions: Vec<zend_function_entry>,
48}
49
50impl Default for SapiConfig {
51 fn default() -> Self {
52 Self {
53 sapi_name: DEFAULT_SAPI_NAME.to_owned(),
54 pretty_name: DEFAULT_PRETTY_NAME.to_owned(),
55 server_software: DEFAULT_SERVER_SOFTWARE.to_owned(),
56 ini_entries: DEFAULT_INI_ENTRIES
57 .iter()
58 .map(|(k, v)| ((*k).to_owned(), (*v).to_owned()))
59 .collect(),
60 ignore_php_ini: false,
61 ignore_cwd_ini: true,
62 ini_path: None,
63 native_functions: Vec::new(),
64 }
65 }
66}
67
68impl SapiConfig {
69 #[must_use]
70 pub fn new() -> Self {
71 Self::default()
72 }
73
74 #[must_use]
75 pub fn sapi_name(mut self, name: impl Into<String>) -> Self {
76 self.sapi_name = name.into();
77 self
78 }
79
80 #[must_use]
81 pub fn pretty_name(mut self, name: impl Into<String>) -> Self {
82 self.pretty_name = name.into();
83 self
84 }
85
86 #[must_use]
87 pub fn server_software(mut self, software: impl Into<String>) -> Self {
88 self.server_software = software.into();
89 self
90 }
91
92 #[must_use]
93 pub fn ini_entries(mut self, entries: Vec<(String, String)>) -> Self {
94 self.ini_entries = entries;
95 self
96 }
97
98 #[must_use]
99 pub fn ini_entry(
100 mut self,
101 key: impl Into<String>,
102 value: impl Into<String>,
103 ) -> Self {
104 self.ini_entries
105 .push((key.into(), value.into()));
106 self
107 }
108
109 #[must_use]
110 pub fn ignore_php_ini(mut self, ignore: bool) -> Self {
111 self.ignore_php_ini = ignore;
112 self
113 }
114
115 #[must_use]
116 pub fn ignore_cwd_ini(mut self, ignore: bool) -> Self {
117 self.ignore_cwd_ini = ignore;
118 self
119 }
120
121 #[must_use]
122 pub fn ini_path(mut self, path: impl Into<String>) -> Self {
123 self.ini_path = Some(path.into());
124 self
125 }
126
127 #[must_use]
128 pub fn native_functions(
129 mut self,
130 entries: &'static [NativeFunction],
131 ) -> Self {
132 self.native_functions = entries
133 .iter()
134 .copied()
135 .map(NativeFunction::entry)
136 .take_while(|entry| !entry.fname.is_null())
137 .collect();
138 self
139 }
140
141 pub(crate) fn resolve(self) -> Result<ResolvedConfig, SapiError> {
149 fn leak_null_terminated(s: String) -> &'static [u8] {
150 let mut bytes = s.into_bytes();
151 bytes.push(0);
152 Box::leak(bytes.into_boxed_slice())
153 }
154
155 if self.sapi_name.contains('\0') {
156 return Err(SapiError::InvalidSapiName(self.sapi_name));
157 }
158 let sapi_name = leak_null_terminated(self.sapi_name);
159
160 if self
161 .pretty_name
162 .contains('\0')
163 {
164 return Err(SapiError::InvalidPrettyName(self.pretty_name));
165 }
166 let pretty_name = leak_null_terminated(self.pretty_name);
167
168 if self
169 .server_software
170 .contains('\0')
171 {
172 return Err(SapiError::InvalidServerSoftware(self.server_software));
173 }
174 let server_software: &'static str = Box::leak(
175 self.server_software
176 .into_boxed_str(),
177 );
178
179 let mut ini_blob = String::new();
180 for (key, value) in &self.ini_entries {
181 if key.contains('\0') {
182 return Err(SapiError::InvalidIniKey);
183 }
184 if value.contains('\0') {
185 return Err(SapiError::InvalidIniValue);
186 }
187 ini_blob.push_str(key);
188 ini_blob.push('=');
189 ini_blob.push_str(value);
190 ini_blob.push('\n');
191 }
192 let ini_entries = leak_null_terminated(ini_blob);
193
194 let ini_path = match self.ini_path {
195 Some(path) => {
196 let cstring = CString::new(path)
197 .map_err(|_| SapiError::InvalidIniPath)?;
198 Some(Box::leak(cstring.into_boxed_c_str()) as &'static CStr)
199 }
200 None => None,
201 };
202
203 let mut native_entries = if self
204 .native_functions
205 .is_empty()
206 {
207 native_functions::entries()
208 .iter()
209 .copied()
210 .take_while(|entry| !entry.fname.is_null())
211 .collect()
212 } else {
213 self.native_functions
214 };
215 native_entries.push(zend_function_entry::end());
216 let native_functions = Box::leak(native_entries.into_boxed_slice());
217
218 Ok(ResolvedConfig {
219 sapi_name,
220 pretty_name,
221 server_software,
222 ini_entries,
223 ignore_php_ini: self.ignore_php_ini,
224 ignore_cwd_ini: self.ignore_cwd_ini,
225 ini_path,
226 native_functions,
227 })
228 }
229}
230
231pub(crate) struct ResolvedConfig {
234 pub sapi_name: &'static [u8],
235 pub pretty_name: &'static [u8],
236 pub server_software: &'static str,
237 pub ini_entries: &'static [u8],
238 pub ignore_php_ini: bool,
239 pub ignore_cwd_ini: bool,
240 pub ini_path: Option<&'static CStr>,
241 pub native_functions: &'static [zend_function_entry],
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use std::ffi::CStr;
248 use std::os::raw::{c_char, c_void};
249
250 use super::super::native::ReturnValue;
251
252 unsafe extern "C" fn fake_handler(
253 _execute_data: *mut c_void,
254 _return_value: *mut ReturnValue,
255 ) {
256 }
257
258 #[test]
259 fn default_sapi_name() {
260 assert_eq!(SapiConfig::default().sapi_name, "ripht");
261 }
262
263 #[test]
264 fn default_pretty_name() {
265 assert_eq!(SapiConfig::default().pretty_name, "Ripht PHP SAPI");
266 }
267
268 #[test]
269 fn default_server_software_prefix() {
270 assert!(SapiConfig::default()
271 .server_software
272 .starts_with("Ripht/"));
273 }
274
275 #[test]
276 fn default_ini_entries_count() {
277 assert_eq!(
278 SapiConfig::default()
279 .ini_entries
280 .len(),
281 7
282 );
283 }
284
285 #[test]
286 fn default_ini_contains_variables_order() {
287 let config = SapiConfig::default();
288 assert!(config
289 .ini_entries
290 .contains(&("variables_order".to_owned(), "EGPCS".to_owned())));
291 }
292
293 #[test]
294 fn default_ignore_flags() {
295 let config = SapiConfig::default();
296 assert!(!config.ignore_php_ini);
297 assert!(config.ignore_cwd_ini);
298 }
299
300 #[test]
301 fn builder_chaining() {
302 let config = SapiConfig::new()
303 .sapi_name("cli")
304 .pretty_name("Test SAPI")
305 .server_software("Test/1.0")
306 .ignore_php_ini(true)
307 .ignore_cwd_ini(false)
308 .ini_path("/etc/php.ini");
309
310 assert_eq!(config.sapi_name, "cli");
311 assert_eq!(config.pretty_name, "Test SAPI");
312 assert_eq!(config.server_software, "Test/1.0");
313 assert!(config.ignore_php_ini);
314 assert!(!config.ignore_cwd_ini);
315 assert_eq!(config.ini_path.as_deref(), Some("/etc/php.ini"));
316 }
317
318 #[test]
319 fn ini_entry_appends() {
320 let config = SapiConfig::new().ini_entry("custom_key", "custom_value");
321 assert_eq!(config.ini_entries.len(), 8);
322 assert_eq!(
323 config
324 .ini_entries
325 .last()
326 .unwrap(),
327 &("custom_key".to_owned(), "custom_value".to_owned())
328 );
329 }
330
331 #[test]
332 fn ini_entries_replaces_all() {
333 let config = SapiConfig::new()
334 .ini_entries(vec![("only".to_owned(), "one".to_owned())]);
335 assert_eq!(config.ini_entries.len(), 1);
336 }
337
338 #[test]
339 fn resolve_null_terminates_sapi_name() {
340 let resolved = SapiConfig::new()
341 .sapi_name("test")
342 .resolve()
343 .unwrap();
344 assert_eq!(resolved.sapi_name, b"test\0");
345 }
346
347 #[test]
348 fn resolve_null_terminates_pretty_name() {
349 let resolved = SapiConfig::new()
350 .pretty_name("My SAPI")
351 .resolve()
352 .unwrap();
353 assert_eq!(resolved.pretty_name, b"My SAPI\0");
354 }
355
356 #[test]
357 fn resolve_ini_blob_format() {
358 let resolved = SapiConfig::new()
359 .ini_entries(vec![
360 ("key1".to_owned(), "val1".to_owned()),
361 ("key2".to_owned(), "val2".to_owned()),
362 ])
363 .resolve()
364 .unwrap();
365 assert_eq!(resolved.ini_entries, b"key1=val1\nkey2=val2\n\0");
366 }
367
368 #[test]
369 fn resolve_empty_ini_blob() {
370 let resolved = SapiConfig::new()
371 .ini_entries(vec![])
372 .resolve()
373 .unwrap();
374 assert_eq!(resolved.ini_entries, b"\0");
375 }
376
377 #[test]
378 fn resolve_rejects_null_in_sapi_name() {
379 let result = SapiConfig::new()
380 .sapi_name("te\0st")
381 .resolve();
382 assert!(matches!(result, Err(SapiError::InvalidSapiName(_))));
383 }
384
385 #[test]
386 fn resolve_rejects_null_in_pretty_name() {
387 let result = SapiConfig::new()
388 .pretty_name("My\0SAPI")
389 .resolve();
390 assert!(matches!(result, Err(SapiError::InvalidPrettyName(_))));
391 }
392
393 #[test]
394 fn resolve_rejects_null_in_server_software() {
395 let result = SapiConfig::new()
396 .server_software("Bad\0/1.0")
397 .resolve();
398 assert!(matches!(result, Err(SapiError::InvalidServerSoftware(_))));
399 }
400
401 #[test]
402 fn resolve_rejects_null_in_ini_key() {
403 let result = SapiConfig::new()
404 .ini_entries(vec![("ke\0y".to_owned(), "val".to_owned())])
405 .resolve();
406 assert!(matches!(result, Err(SapiError::InvalidIniKey)));
407 }
408
409 #[test]
410 fn resolve_rejects_null_in_ini_value() {
411 let result = SapiConfig::new()
412 .ini_entries(vec![("key".to_owned(), "va\0l".to_owned())])
413 .resolve();
414 assert!(matches!(result, Err(SapiError::InvalidIniValue)));
415 }
416
417 #[test]
418 fn resolve_rejects_null_in_ini_path() {
419 let result = SapiConfig::new()
420 .ini_path("/etc/ph\0p.ini")
421 .resolve();
422 assert!(matches!(result, Err(SapiError::InvalidIniPath)));
423 }
424
425 #[test]
426 fn resolve_ini_path_produces_valid_cstr() {
427 let resolved = SapiConfig::new()
428 .ini_path("/etc/php.ini")
429 .resolve()
430 .unwrap();
431 let cstr = resolved.ini_path.unwrap();
432 assert_eq!(cstr.to_str().unwrap(), "/etc/php.ini");
433 }
434
435 #[test]
436 fn resolve_defaults_match_prior_constants() {
437 let resolved = SapiConfig::default()
438 .resolve()
439 .unwrap();
440 assert_eq!(resolved.sapi_name, b"ripht\0");
441 assert_eq!(resolved.pretty_name, b"Ripht PHP SAPI\0");
442 assert!(resolved
443 .server_software
444 .starts_with("Ripht/"));
445 assert!(!resolved.ignore_php_ini);
446 assert!(resolved.ignore_cwd_ini);
447 assert!(resolved.ini_path.is_none());
448
449 let ini = std::str::from_utf8(
450 &resolved.ini_entries[..resolved.ini_entries.len() - 1],
451 )
452 .unwrap();
453 assert!(ini.contains("variables_order=EGPCS"));
454 assert!(ini.contains("log_errors=1"));
455 }
456
457 #[test]
458 fn resolve_uses_custom_native_functions_before_terminator() {
459 static CUSTOM: [NativeFunction; 1] = [
460 unsafe {
463 NativeFunction::new_unchecked(
464 b"ripht_test_native\0".as_ptr() as *const c_char,
465 fake_handler,
466 std::ptr::null(),
467 0,
468 )
469 },
470 ];
471
472 let resolved = SapiConfig::new()
473 .native_functions(&CUSTOM)
474 .resolve()
475 .unwrap();
476
477 let names = resolved
478 .native_functions
479 .iter()
480 .take_while(|entry| !entry.fname.is_null())
481 .map(|entry| {
482 unsafe { CStr::from_ptr(entry.fname) }
483 .to_string_lossy()
484 .into_owned()
485 })
486 .collect::<Vec<_>>();
487
488 assert_eq!(names, vec!["ripht_test_native"]);
489 assert!(resolved
490 .native_functions
491 .last()
492 .unwrap()
493 .fname
494 .is_null());
495 }
496}