ripht_php_sapi/sapi/
mod.rs1use std::ffi::CString;
18use std::sync::OnceLock;
19
20use thiserror::Error;
21
22#[cfg(feature = "tracing")]
23use tracing::{error, info, trace};
24
25pub(crate) mod callbacks;
26pub mod config;
27mod executor;
28pub(crate) mod ffi;
29pub mod native;
30pub(crate) mod native_functions;
31pub(crate) mod server_context;
32pub(crate) mod server_vars;
33
34pub use config::SapiConfig;
35pub use executor::{ExecutionError, Executor};
36pub(crate) use server_vars::{ServerVars, ServerVarsCString};
37
38use crate::execution::{ExecutionContext, ExecutionHooks, ExecutionResult};
39use config::ResolvedConfig;
40
41static PHP_INIT_RESULT: OnceLock<Result<(), SapiError>> = OnceLock::new();
42static SAPI_CONFIG: OnceLock<SapiConfig> = OnceLock::new();
43pub(crate) static RESOLVED_CONFIG: OnceLock<ResolvedConfig> = OnceLock::new();
44
45#[derive(Debug, Clone, Error)]
47#[non_exhaustive]
48pub enum SapiError {
49 #[error("PHP engine not initialized")]
50 NotInitialized,
51
52 #[error("PHP initialization failed: {0}")]
53 InitializationFailed(String),
54
55 #[error("PHP engine already initialized — configure() must be called before instance()")]
56 AlreadyInitialized,
57
58 #[error("SAPI already configured — configure() can only be called once")]
59 AlreadyConfigured,
60
61 #[error("SAPI name contains null byte: {0:?}")]
62 InvalidSapiName(String),
63
64 #[error("pretty name contains null byte: {0:?}")]
65 InvalidPrettyName(String),
66
67 #[error("server software string contains null byte: {0:?}")]
68 InvalidServerSoftware(String),
69
70 #[error("INI key contains null byte")]
71 InvalidIniKey,
72
73 #[error("INI value contains null byte")]
74 InvalidIniValue,
75
76 #[error("INI path contains null byte")]
77 InvalidIniPath,
78
79 #[error("Failed to set INI: {0}")]
80 IniSetFailed(String),
81
82 #[error(
83 "PHP library not found. Build PHP with --enable-embed=static and set RIPHT_PHP_SAPI_PREFIX"
84 )]
85 LibraryNotFound,
86}
87
88pub struct RiphtSapi {
90 _marker: std::marker::PhantomData<*mut ()>,
91}
92
93impl RiphtSapi {
94 pub fn configure(config: SapiConfig) -> Result<(), SapiError> {
106 if PHP_INIT_RESULT
107 .get()
108 .is_some()
109 {
110 return Err(SapiError::AlreadyInitialized);
111 }
112 SAPI_CONFIG
113 .set(config)
114 .map_err(|_| SapiError::AlreadyConfigured)
115 }
116
117 #[must_use]
119 pub fn instance() -> Self {
120 Self::init().expect("SAPI initialization failure")
121 }
122
123 fn init() -> Result<Self, SapiError> {
124 let init_result = PHP_INIT_RESULT.get_or_init(|| {
125 #[cfg(feature = "tracing")]
126 info!("Initializing RiphtSapi");
127
128 let config = SAPI_CONFIG
129 .get()
130 .cloned()
131 .unwrap_or_default();
132 let resolved = config.resolve()?;
133 let resolved = RESOLVED_CONFIG.get_or_init(|| resolved);
134
135 unsafe {
139 ffi::sapi_module.name = resolved.sapi_name.as_ptr() as *mut _;
140 ffi::sapi_module.pretty_name =
141 resolved.pretty_name.as_ptr() as *mut _;
142
143 ffi::sapi_module.startup = Some(callbacks::ripht_sapi_startup);
145 ffi::sapi_module.shutdown =
146 Some(callbacks::ripht_sapi_shutdown);
147 ffi::sapi_module.activate =
148 Some(callbacks::ripht_sapi_activate);
149 ffi::sapi_module.deactivate =
150 Some(callbacks::ripht_sapi_deactivate);
151
152 ffi::sapi_module.ub_write =
153 Some(callbacks::ripht_sapi_ub_write);
154 ffi::sapi_module.flush = Some(callbacks::ripht_sapi_flush);
155
156 ffi::sapi_module.send_headers =
157 Some(callbacks::ripht_sapi_send_headers);
158 ffi::sapi_module.send_header =
159 Some(callbacks::ripht_sapi_send_header);
160
161 ffi::sapi_module.read_post =
162 Some(callbacks::ripht_sapi_read_post);
163 ffi::sapi_module.read_cookies =
164 Some(callbacks::ripht_sapi_read_cookies);
165
166 ffi::sapi_module.register_server_variables =
167 Some(callbacks::ripht_sapi_register_server_variables);
168
169 ffi::sapi_module.log_message =
170 Some(callbacks::ripht_sapi_log_message);
171 ffi::sapi_module.get_request_time =
172 Some(callbacks::ripht_sapi_get_request_time);
173 ffi::sapi_module.getenv = Some(callbacks::ripht_sapi_getenv);
174
175 ffi::sapi_module.php_ini_ignore =
176 i32::from(resolved.ignore_php_ini);
177 ffi::sapi_module.php_ini_ignore_cwd =
178 i32::from(resolved.ignore_cwd_ini);
179
180 if let Some(ini_path) = resolved.ini_path {
181 ffi::sapi_module.php_ini_path_override =
182 ini_path.as_ptr() as *mut _;
183 }
184
185 ffi::sapi_module.input_filter =
186 Some(callbacks::ripht_sapi_input_filter);
187 ffi::sapi_module.default_post_reader =
188 Some(callbacks::ripht_sapi_default_post_reader);
189 ffi::sapi_module.treat_data =
190 Some(callbacks::ripht_sapi_treat_data);
191
192 ffi::sapi_module.ini_entries =
193 resolved.ini_entries.as_ptr() as *const _;
194
195 ffi::sapi_module.additional_functions = resolved
196 .native_functions
197 .as_ptr();
198
199 #[cfg(feature = "tracing")]
200 trace!("Starting SAPI");
201
202 ffi::sapi_startup(&mut ffi::sapi_module);
203
204 #[cfg(feature = "tracing")]
205 trace!("Initializing SAPI module");
206
207 let result = ffi::php_module_startup(
208 &mut ffi::sapi_module,
209 std::ptr::null_mut(),
210 );
211
212 if result == ffi::FAILURE {
213 #[cfg(feature = "tracing")]
214 error!("SAPI module startup failed");
215
216 ffi::sapi_shutdown();
217
218 Err(SapiError::InitializationFailed(
219 "SAPI module initialization failed".to_string(),
220 ))
221 } else {
222 #[cfg(feature = "tracing")]
223 info!("SAPI module initialized");
224 Ok(())
225 }
226 }
227 });
228
229 match init_result {
230 Ok(()) => Ok(Self {
231 _marker: std::marker::PhantomData,
232 }),
233 Err(e) => Err(e.clone()),
236 }
237 }
238
239 pub fn shutdown() {
241 unsafe {
242 ffi::php_module_shutdown();
243 ffi::sapi_shutdown();
244 }
245 }
246
247 pub fn set_ini(
248 &self,
249 key: impl Into<Vec<u8>>,
250 value: impl Into<Vec<u8>>,
251 ) -> Result<(), SapiError> {
252 let k_str = key.into();
253 let v_str = value.into();
254
255 let key_cstr = CString::new(k_str.clone())
256 .map_err(|_| SapiError::InvalidIniKey)?;
257 let value_cstr = CString::new(v_str.clone())
258 .map_err(|_| SapiError::InvalidIniValue)?;
259
260 unsafe {
261 let init = ffi::zend_string_init_interned
262 .expect("zend_string_init_interned is null");
263
264 let name = init(key_cstr.as_ptr(), k_str.len(), true);
265
266 if name.is_null() {
267 return Err(SapiError::IniSetFailed(
268 String::from_utf8(k_str).unwrap_or_default(),
269 ));
270 }
271
272 let result = ffi::zend_alter_ini_entry_chars(
273 name,
274 value_cstr.as_ptr(),
275 v_str.len(),
276 ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
277 ffi::ZEND_INI_STAGE_RUNTIME,
278 );
279
280 if result != ffi::SUCCESS {
281 return Err(SapiError::IniSetFailed(
282 String::from_utf8(k_str).unwrap_or_default(),
283 ));
284 }
285
286 Ok(())
287 }
288 }
289
290 pub fn get_ini(&self, key: &str) -> Option<String> {
291 #[cfg(feature = "tracing")]
292 trace!(ini_key = key, "Getting INI value");
293
294 let key_cstr = CString::new(key).ok()?;
295
296 unsafe {
297 let ptr = ffi::zend_ini_string(key_cstr.as_ptr(), key.len(), 0);
298 if ptr.is_null() {
299 None
300 } else {
301 Some(
302 std::ffi::CStr::from_ptr(ptr)
303 .to_string_lossy()
304 .into_owned(),
305 )
306 }
307 }
308 }
309
310 pub fn executor(&self) -> Result<Executor<'_>, SapiError> {
311 Executor::new(self)
312 }
313
314 pub fn execute(
315 &self,
316 ctx: ExecutionContext,
317 ) -> Result<ExecutionResult, ExecutionError> {
318 self.executor()
319 .map_err(|_| ExecutionError::NotInitialized)?
320 .execute(ctx)
321 }
322
323 pub fn execute_streaming<F>(
324 &self,
325 ctx: ExecutionContext,
326 on_output: F,
327 ) -> Result<ExecutionResult, ExecutionError>
328 where
329 F: FnMut(&[u8]) + 'static,
330 {
331 self.executor()
332 .map_err(|_| ExecutionError::NotInitialized)?
333 .execute_streaming(ctx, on_output)
334 }
335
336 pub fn execute_with_hooks<H: ExecutionHooks + 'static>(
337 &self,
338 ctx: ExecutionContext,
339 hooks: H,
340 ) -> Result<ExecutionResult, ExecutionError> {
341 self.executor()
342 .map_err(|_| ExecutionError::NotInitialized)?
343 .execute_with_hooks(ctx, hooks)
344 }
345
346 pub fn is_initialized(&self) -> bool {
347 PHP_INIT_RESULT
348 .get()
349 .map(|result| result.is_ok())
350 .unwrap_or(false)
351 }
352}