ripht_php_sapi/sapi/
mod.rs

1//! Core SAPI implementation and PHP lifecycle management.
2//!
3//! Handles module startup/shutdown (MINIT/MSHUTDOWN), callback registration,
4//! and provides the primary `RiphtSapi` interface for script execution.
5
6use std::ffi::CString;
7use std::sync::OnceLock;
8
9use thiserror::Error;
10
11#[cfg(feature = "tracing")]
12use tracing::{error, info, trace};
13
14pub(crate) mod callbacks;
15mod executor;
16pub(crate) mod ffi;
17pub(crate) mod server_context;
18pub(crate) mod server_vars;
19
20pub use executor::{ExecutionError, Executor};
21pub(crate) use server_vars::{ServerVars, ServerVarsCString};
22
23use crate::execution::{ExecutionContext, ExecutionHooks, ExecutionResult};
24
25static PHP_INIT_RESULT: OnceLock<Result<(), SapiError>> = OnceLock::new();
26
27pub(crate) static SAPI_NAME: &[u8] = b"ripht\0";
28pub(crate) static SAPI_PRETTY_NAME: &[u8] = b"Ripht PHP SAPI\0";
29pub(crate) static SERVER_SOFTWARE: &str = "Ripht/0.1.0-rc.1";
30static INI_ENTRIES: &[u8] = b"\
31variables_order=EGPCS\n\
32request_order=GP\n\
33output_buffering=4096\n\
34implicit_flush=0\n\
35html_errors=0\n\
36display_errors=1\n\
37log_errors=1\n\
38\0";
39
40/// Errors from SAPI initialization and configuration.
41#[derive(Debug, Clone, Error)]
42#[non_exhaustive]
43pub enum SapiError {
44    #[error("PHP engine not initialized")]
45    NotInitialized,
46
47    #[error("PHP initialization failed: {0}")]
48    InitializationFailed(String),
49
50    #[error("INI key contains null byte")]
51    InvalidIniKey,
52
53    #[error("INI value contains null byte")]
54    InvalidIniValue,
55
56    #[error("Failed to set INI: {0}")]
57    IniSetFailed(String),
58
59    #[error(
60        "PHP library not found. Build PHP with --enable-embed=static and set RIPHT_PHP_SAPI_PREFIX"
61    )]
62    LibraryNotFound,
63}
64
65/// PHP SAPI instance. Initialize once, execute scripts repeatedly.
66pub struct RiphtSapi {
67    _marker: std::marker::PhantomData<*mut ()>,
68}
69
70impl RiphtSapi {
71    // Note: will panic if initialization fails
72    #[must_use]
73    pub fn instance() -> Self {
74        Self::init().expect("SAPI initialization failure")
75    }
76
77    fn init() -> Result<Self, SapiError> {
78        let init_result = PHP_INIT_RESULT.get_or_init(|| {
79            #[cfg(feature = "tracing")]
80            info!("Initializing RiphtSapi");
81
82            // SAFETY: One-time PHP engine initialization via OnceLock.
83            // All pointers/callbacks are static or 'static and remain valid.
84            unsafe {
85                ffi::sapi_module.name = SAPI_NAME.as_ptr() as *mut _;
86                ffi::sapi_module.pretty_name =
87                    SAPI_PRETTY_NAME.as_ptr() as *mut _;
88
89                // Register callbacks
90                ffi::sapi_module.startup = Some(callbacks::ripht_sapi_startup);
91                ffi::sapi_module.shutdown =
92                    Some(callbacks::ripht_sapi_shutdown);
93                ffi::sapi_module.activate =
94                    Some(callbacks::ripht_sapi_activate);
95                ffi::sapi_module.deactivate =
96                    Some(callbacks::ripht_sapi_deactivate);
97
98                ffi::sapi_module.ub_write =
99                    Some(callbacks::ripht_sapi_ub_write);
100                ffi::sapi_module.flush = Some(callbacks::ripht_sapi_flush);
101
102                ffi::sapi_module.send_headers =
103                    Some(callbacks::ripht_sapi_send_headers);
104                ffi::sapi_module.send_header =
105                    Some(callbacks::ripht_sapi_send_header);
106
107                ffi::sapi_module.read_post =
108                    Some(callbacks::ripht_sapi_read_post);
109                ffi::sapi_module.read_cookies =
110                    Some(callbacks::ripht_sapi_read_cookies);
111
112                ffi::sapi_module.register_server_variables =
113                    Some(callbacks::ripht_sapi_register_server_variables);
114
115                ffi::sapi_module.log_message =
116                    Some(callbacks::ripht_sapi_log_message);
117                ffi::sapi_module.get_request_time =
118                    Some(callbacks::ripht_sapi_get_request_time);
119                ffi::sapi_module.getenv = Some(callbacks::ripht_sapi_getenv);
120
121                ffi::sapi_module.php_ini_ignore = 0;
122                ffi::sapi_module.php_ini_ignore_cwd = 1;
123
124                ffi::sapi_module.input_filter =
125                    Some(callbacks::ripht_sapi_input_filter);
126                ffi::sapi_module.default_post_reader =
127                    Some(callbacks::ripht_sapi_default_post_reader);
128                ffi::sapi_module.treat_data =
129                    Some(callbacks::ripht_sapi_treat_data);
130
131                ffi::sapi_module.ini_entries = INI_ENTRIES.as_ptr() as *const _;
132
133                #[cfg(feature = "tracing")]
134                trace!("Starting SAPI");
135
136                ffi::sapi_startup(&mut ffi::sapi_module);
137
138                #[cfg(feature = "tracing")]
139                trace!("Initializing SAPI module");
140
141                let result = ffi::php_module_startup(
142                    &mut ffi::sapi_module,
143                    std::ptr::null_mut(),
144                );
145
146                if result == ffi::FAILURE {
147                    #[cfg(feature = "tracing")]
148                    error!("SAPI module startup failed");
149
150                    ffi::sapi_shutdown();
151
152                    Err(SapiError::InitializationFailed(
153                        "SAPI module initialization failed".to_string(),
154                    ))
155                } else {
156                    #[cfg(feature = "tracing")]
157                    info!("SAPI module initialized");
158                    Ok(())
159                }
160            }
161        });
162
163        match init_result {
164            Ok(()) => Ok(Self {
165                _marker: std::marker::PhantomData,
166            }),
167            // Clone the original error instead of wrapping it redundantly.
168            // The error already contains descriptive context.
169            Err(e) => Err(e.clone()),
170        }
171    }
172
173    /// Shuts down the PHP engine. Calling `execute()` after this is undefined behavior.
174    pub fn shutdown() {
175        unsafe {
176            ffi::php_module_shutdown();
177            ffi::sapi_shutdown();
178        }
179    }
180
181    pub fn set_ini(&self, key: impl Into<Vec<u8>>, value: impl Into<Vec<u8>>) -> Result<(), SapiError> {
182        let k_str = key.into();
183            let v_str = value.into();
184
185
186        let key_cstr =
187            CString::new(k_str.clone()).map_err(|_| SapiError::InvalidIniKey)?;
188        let value_cstr =
189            CString::new(v_str.clone()).map_err(|_| SapiError::InvalidIniValue)?;
190
191        unsafe {
192            let init = ffi::zend_string_init_interned
193                .expect("zend_string_init_interned is null");
194
195            let name = init(key_cstr.as_ptr(), k_str.len(), true);
196
197            if name.is_null() {
198                return Err(SapiError::IniSetFailed(String::from_utf8(k_str).unwrap_or_default()));
199            }
200
201            let result = ffi::zend_alter_ini_entry_chars(
202                name,
203                value_cstr.as_ptr(),
204                v_str.len(),
205                ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
206                ffi::ZEND_INI_STAGE_RUNTIME,
207            );
208
209            if result != ffi::SUCCESS {
210                return Err(SapiError::IniSetFailed(String::from_utf8(k_str).unwrap_or_default()));
211            }
212
213            Ok(())
214        }
215    }
216
217    pub fn get_ini(&self, key: &str) -> Option<String> {
218        #[cfg(feature = "tracing")]
219        trace!(ini_key = key, "Getting INI value");
220
221        let key_cstr = CString::new(key).ok()?;
222
223        unsafe {
224            let ptr = ffi::zend_ini_string(key_cstr.as_ptr(), key.len(), 0);
225            if ptr.is_null() {
226                None
227            } else {
228                Some(
229                    std::ffi::CStr::from_ptr(ptr)
230                        .to_string_lossy()
231                        .into_owned(),
232                )
233            }
234        }
235    }
236
237    pub fn executor(&self) -> Result<Executor<'_>, SapiError> {
238        Executor::new(self)
239    }
240
241    pub fn execute(
242        &self,
243        ctx: ExecutionContext,
244    ) -> Result<ExecutionResult, ExecutionError> {
245        self.executor()
246            .map_err(|_| ExecutionError::NotInitialized)?
247            .execute(ctx)
248    }
249
250    pub fn execute_streaming<F>(
251        &self,
252        ctx: ExecutionContext,
253        on_output: F,
254    ) -> Result<ExecutionResult, ExecutionError>
255    where
256        F: FnMut(&[u8]) + 'static,
257    {
258        self.executor()
259            .map_err(|_| ExecutionError::NotInitialized)?
260            .execute_streaming(ctx, on_output)
261    }
262
263    pub fn execute_with_hooks<H: ExecutionHooks + 'static>(
264        &self,
265        ctx: ExecutionContext,
266        hooks: H,
267    ) -> Result<ExecutionResult, ExecutionError> {
268        self.executor()
269            .map_err(|_| ExecutionError::NotInitialized)?
270            .execute_with_hooks(ctx, hooks)
271    }
272
273    pub fn is_initialized(&self) -> bool {
274        PHP_INIT_RESULT
275            .get()
276            .map(|result| result.is_ok())
277            .unwrap_or(false)
278    }
279}