use std::ffi::CString;
use std::sync::OnceLock;
use thiserror::Error;
#[cfg(feature = "tracing")]
use tracing::{error, info, trace};
pub(crate) mod callbacks;
pub mod config;
mod executor;
pub(crate) mod ffi;
pub mod native;
pub(crate) mod native_functions;
pub(crate) mod server_context;
pub(crate) mod server_vars;
pub use config::SapiConfig;
pub use executor::{ExecutionError, Executor};
pub(crate) use server_vars::{ServerVars, ServerVarsCString};
use crate::execution::{ExecutionContext, ExecutionHooks, ExecutionResult};
use config::ResolvedConfig;
static PHP_INIT_RESULT: OnceLock<Result<(), SapiError>> = OnceLock::new();
static SAPI_CONFIG: OnceLock<SapiConfig> = OnceLock::new();
pub(crate) static RESOLVED_CONFIG: OnceLock<ResolvedConfig> = OnceLock::new();
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum SapiError {
#[error("PHP engine not initialized")]
NotInitialized,
#[error("PHP initialization failed: {0}")]
InitializationFailed(String),
#[error("PHP engine already initialized — configure() must be called before instance()")]
AlreadyInitialized,
#[error("SAPI already configured — configure() can only be called once")]
AlreadyConfigured,
#[error("SAPI name contains null byte: {0:?}")]
InvalidSapiName(String),
#[error("pretty name contains null byte: {0:?}")]
InvalidPrettyName(String),
#[error("server software string contains null byte: {0:?}")]
InvalidServerSoftware(String),
#[error("INI key contains null byte")]
InvalidIniKey,
#[error("INI value contains null byte")]
InvalidIniValue,
#[error("INI path contains null byte")]
InvalidIniPath,
#[error("Failed to set INI: {0}")]
IniSetFailed(String),
#[error(
"PHP library not found. Build PHP with --enable-embed=static and set RIPHT_PHP_SAPI_PREFIX"
)]
LibraryNotFound,
}
pub struct RiphtSapi {
_marker: std::marker::PhantomData<*mut ()>,
}
impl RiphtSapi {
pub fn configure(config: SapiConfig) -> Result<(), SapiError> {
if PHP_INIT_RESULT
.get()
.is_some()
{
return Err(SapiError::AlreadyInitialized);
}
SAPI_CONFIG
.set(config)
.map_err(|_| SapiError::AlreadyConfigured)
}
#[must_use]
pub fn instance() -> Self {
Self::init().expect("SAPI initialization failure")
}
fn init() -> Result<Self, SapiError> {
let init_result = PHP_INIT_RESULT.get_or_init(|| {
#[cfg(feature = "tracing")]
info!("Initializing RiphtSapi");
let config = SAPI_CONFIG
.get()
.cloned()
.unwrap_or_default();
let resolved = config.resolve()?;
let resolved = RESOLVED_CONFIG.get_or_init(|| resolved);
unsafe {
ffi::sapi_module.name = resolved.sapi_name.as_ptr() as *mut _;
ffi::sapi_module.pretty_name =
resolved.pretty_name.as_ptr() as *mut _;
ffi::sapi_module.startup = Some(callbacks::ripht_sapi_startup);
ffi::sapi_module.shutdown =
Some(callbacks::ripht_sapi_shutdown);
ffi::sapi_module.activate =
Some(callbacks::ripht_sapi_activate);
ffi::sapi_module.deactivate =
Some(callbacks::ripht_sapi_deactivate);
ffi::sapi_module.ub_write =
Some(callbacks::ripht_sapi_ub_write);
ffi::sapi_module.flush = Some(callbacks::ripht_sapi_flush);
ffi::sapi_module.send_headers =
Some(callbacks::ripht_sapi_send_headers);
ffi::sapi_module.send_header =
Some(callbacks::ripht_sapi_send_header);
ffi::sapi_module.read_post =
Some(callbacks::ripht_sapi_read_post);
ffi::sapi_module.read_cookies =
Some(callbacks::ripht_sapi_read_cookies);
ffi::sapi_module.register_server_variables =
Some(callbacks::ripht_sapi_register_server_variables);
ffi::sapi_module.log_message =
Some(callbacks::ripht_sapi_log_message);
ffi::sapi_module.get_request_time =
Some(callbacks::ripht_sapi_get_request_time);
ffi::sapi_module.getenv = Some(callbacks::ripht_sapi_getenv);
ffi::sapi_module.php_ini_ignore =
i32::from(resolved.ignore_php_ini);
ffi::sapi_module.php_ini_ignore_cwd =
i32::from(resolved.ignore_cwd_ini);
if let Some(ini_path) = resolved.ini_path {
ffi::sapi_module.php_ini_path_override =
ini_path.as_ptr() as *mut _;
}
ffi::sapi_module.input_filter =
Some(callbacks::ripht_sapi_input_filter);
ffi::sapi_module.default_post_reader =
Some(callbacks::ripht_sapi_default_post_reader);
ffi::sapi_module.treat_data =
Some(callbacks::ripht_sapi_treat_data);
ffi::sapi_module.ini_entries =
resolved.ini_entries.as_ptr() as *const _;
ffi::sapi_module.additional_functions = resolved
.native_functions
.as_ptr();
#[cfg(feature = "tracing")]
trace!("Starting SAPI");
ffi::sapi_startup(&mut ffi::sapi_module);
#[cfg(feature = "tracing")]
trace!("Initializing SAPI module");
let result = ffi::php_module_startup(
&mut ffi::sapi_module,
std::ptr::null_mut(),
);
if result == ffi::FAILURE {
#[cfg(feature = "tracing")]
error!("SAPI module startup failed");
ffi::sapi_shutdown();
Err(SapiError::InitializationFailed(
"SAPI module initialization failed".to_string(),
))
} else {
#[cfg(feature = "tracing")]
info!("SAPI module initialized");
Ok(())
}
}
});
match init_result {
Ok(()) => Ok(Self {
_marker: std::marker::PhantomData,
}),
Err(e) => Err(e.clone()),
}
}
pub fn shutdown() {
unsafe {
ffi::php_module_shutdown();
ffi::sapi_shutdown();
}
}
pub fn set_ini(
&self,
key: impl Into<Vec<u8>>,
value: impl Into<Vec<u8>>,
) -> Result<(), SapiError> {
let k_str = key.into();
let v_str = value.into();
let key_cstr = CString::new(k_str.clone())
.map_err(|_| SapiError::InvalidIniKey)?;
let value_cstr = CString::new(v_str.clone())
.map_err(|_| SapiError::InvalidIniValue)?;
unsafe {
let init = ffi::zend_string_init_interned
.expect("zend_string_init_interned is null");
let name = init(key_cstr.as_ptr(), k_str.len(), true);
if name.is_null() {
return Err(SapiError::IniSetFailed(
String::from_utf8(k_str).unwrap_or_default(),
));
}
let result = ffi::zend_alter_ini_entry_chars(
name,
value_cstr.as_ptr(),
v_str.len(),
ffi::ZEND_INI_USER | ffi::ZEND_INI_SYSTEM,
ffi::ZEND_INI_STAGE_RUNTIME,
);
if result != ffi::SUCCESS {
return Err(SapiError::IniSetFailed(
String::from_utf8(k_str).unwrap_or_default(),
));
}
Ok(())
}
}
pub fn get_ini(&self, key: &str) -> Option<String> {
#[cfg(feature = "tracing")]
trace!(ini_key = key, "Getting INI value");
let key_cstr = CString::new(key).ok()?;
unsafe {
let ptr = ffi::zend_ini_string(key_cstr.as_ptr(), key.len(), 0);
if ptr.is_null() {
None
} else {
Some(
std::ffi::CStr::from_ptr(ptr)
.to_string_lossy()
.into_owned(),
)
}
}
}
pub fn executor(&self) -> Result<Executor<'_>, SapiError> {
Executor::new(self)
}
pub fn execute(
&self,
ctx: ExecutionContext,
) -> Result<ExecutionResult, ExecutionError> {
self.executor()
.map_err(|_| ExecutionError::NotInitialized)?
.execute(ctx)
}
pub fn execute_streaming<F>(
&self,
ctx: ExecutionContext,
on_output: F,
) -> Result<ExecutionResult, ExecutionError>
where
F: FnMut(&[u8]) + 'static,
{
self.executor()
.map_err(|_| ExecutionError::NotInitialized)?
.execute_streaming(ctx, on_output)
}
pub fn execute_with_hooks<H: ExecutionHooks + 'static>(
&self,
ctx: ExecutionContext,
hooks: H,
) -> Result<ExecutionResult, ExecutionError> {
self.executor()
.map_err(|_| ExecutionError::NotInitialized)?
.execute_with_hooks(ctx, hooks)
}
pub fn is_initialized(&self) -> bool {
PHP_INIT_RESULT
.get()
.map(|result| result.is_ok())
.unwrap_or(false)
}
}