ripht_php_sapi/sapi/
mod.rs1use 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#[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
65pub struct RiphtSapi {
67 _marker: std::marker::PhantomData<*mut ()>,
68}
69
70impl RiphtSapi {
71 #[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 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 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 Err(e) => Err(e.clone()),
170 }
171 }
172
173 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}