use std::collections::{BTreeMap, HashMap};
use std::ffi::CString;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use ext_php_rs::convert::IntoZval;
use ext_php_rs::types::{ZendStr, Zval};
use ext_php_rs::zend::{ExecutorGlobals, try_catch};
use tokio_util::sync::CancellationToken;
use crate::engine::context::{
ClavironContext, PhpExecution, PhpProgramContext, ProgramModel,
};
use crate::engine::interrupt::{Active, Budget, ExecutionOwner};
use crate::engine::thread::{PhpThreadContext, bind_php_thread};
use crate::sapi::run_execution_scope;
mod value;
pub use crate::engine::startup::PhpIni;
pub use value::{PhpArrayKey, PhpValue};
#[derive(Clone, Debug)]
pub struct PhpBootstrap {
pub path: PathBuf,
pub argument: String,
}
#[derive(Clone, Debug)]
pub struct PhpEvaluator {
bootstrap: Option<PhpBootstrap>,
max_depth: usize,
cancellation: CancellationToken,
}
impl PhpEvaluator {
pub fn new(max_depth: usize) -> Self {
Self {
bootstrap: None,
max_depth,
cancellation: CancellationToken::new(),
}
}
pub unsafe fn with_ini(
self,
ini: PhpIni,
) -> Result<Self, String> {
crate::engine::startup::declare(ini)?;
Ok(self)
}
pub fn with_bootstrap(
mut self,
bootstrap: PhpBootstrap,
) -> Self {
self.bootstrap = Some(bootstrap);
self
}
pub fn with_cancellation(
mut self,
cancellation: CancellationToken,
) -> Self {
self.cancellation = cancellation;
self
}
pub fn evaluate_source(
&self,
path: &Path,
source: &[u8],
environment: &BTreeMap<String, String>,
) -> Result<PhpValue, String> {
if self.cancellation.is_cancelled() {
return Err("PHP evaluation cancelled".into());
}
std::thread::scope(|scope| {
let thread = std::thread::Builder::new()
.name("php-evaluate".into())
.spawn_scoped(scope, || {
self.evaluate_on_thread(path, source, environment)
})
.map_err(|error| error.to_string())?;
thread
.join()
.map_err(|_| "PHP evaluation thread panicked".to_owned())?
})
}
fn evaluate_on_thread(
&self,
path: &Path,
source: &[u8],
environment: &BTreeMap<String, String>,
) -> Result<PhpValue, String> {
let path =
std::path::absolute(path).map_err(|error| error.to_string())?;
crate::engine::ensure_sapi().map_err(|error| error.to_string())?;
let _thread = PhpThreadContext;
bind_php_thread("evaluate", ExecutionOwner::next())?;
let context = ClavironContext {
execution: PhpExecution::Program(PhpProgramContext {
model: ProgramModel::Evaluation,
name: path.display().to_string(),
instance: 0,
script: path.clone(),
environment: environment.clone(),
cancellation: self.cancellation.clone(),
current: None,
#[cfg(feature = "tasks")]
output: None,
}),
event_subscription: Mutex::new(None),
#[cfg(feature = "tasks")]
invocation_subscription: Mutex::new(None),
env_overlay: Mutex::new(HashMap::new()),
request_finished: false,
};
let _active = Active::enter();
let mut budget = None;
run_execution_scope(
context,
|| {
budget = Budget::from_php();
},
|| {
if let Some(bootstrap) = &self.bootstrap {
prepare(bootstrap, environment)?;
}
if self.cancellation.is_cancelled() {
return Err("PHP evaluation cancelled".into());
}
let value = execute_source(&path, source)?;
value::decode(&value, self.max_depth)
},
)
.map_err(|error| format!("PHP evaluation failed: {error:?}"))?
}
}
fn execute_source(
path: &Path,
bytes: &[u8],
) -> Result<Zval, String> {
let filename = CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|error| error.to_string())?;
let mut source = ZendStr::new(bytes, false);
let mut value = Zval::new();
let compile = unsafe { ext_php_rs::ffi::zend_compile_string }
.ok_or("Zend compiler is unavailable")?;
let op_array = try_catch(std::panic::AssertUnwindSafe(|| {
unsafe { compile(std::ptr::from_mut(&mut *source).cast(), filename.as_ptr(), ext_php_rs::ffi::_zend_compile_position_ZEND_COMPILE_POSITION_AT_SHEBANG) }
})).map_err(|error| error.to_string())?;
if op_array.is_null() {
return Err(exception_message("PHP document did not compile"));
}
let executed = try_catch(std::panic::AssertUnwindSafe(|| {
unsafe { ext_php_rs::ffi::zend_execute(op_array, &raw mut value) };
}));
unsafe {
ext_php_rs::ffi::destroy_op_array(op_array);
ext_php_rs::ffi::_efree(op_array.cast());
}
executed.map_err(|error| error.to_string())?;
if ExecutorGlobals::has_exception() {
return Err(exception_message("PHP document threw an exception"));
}
Ok(value)
}
fn exception_message(fallback: &str) -> String {
if let Some(mut exception) = ExecutorGlobals::take_exception() {
let mut temporary = Zval::new();
let object = std::ptr::from_mut(&mut *exception)
.cast::<ext_php_rs::ffi::zend_object>();
let message = try_catch(std::panic::AssertUnwindSafe(|| {
unsafe {
ext_php_rs::ffi::zend_read_property(
(*object).ce,
object,
c"message".as_ptr(),
7,
true,
&raw mut temporary,
)
.cast::<Zval>()
.as_ref()
.and_then(|value| value.str())
.map(str::to_owned)
}
}))
.ok()
.flatten()
.unwrap_or_else(|| fallback.to_owned());
format!("{fallback}: {message}")
} else {
fallback.to_owned()
}
}
fn prepare(
bootstrap: &PhpBootstrap,
environment: &BTreeMap<String, String>,
) -> Result<(), String> {
let source = std::fs::read(&bootstrap.path).map_err(|error| {
format!("PHP bootstrap {}: {error}", bootstrap.path.display())
})?;
let path = std::path::absolute(&bootstrap.path)
.map_err(|error| error.to_string())?;
let mut callable = execute_source(&path, &source)?;
if !callable.is_callable() {
return Err("PHP bootstrap must return a callable".into());
}
let mut parameters = [
bootstrap
.argument
.clone()
.into_zval(false)
.map_err(|error| error.to_string())?,
environment
.clone()
.into_zval(false)
.map_err(|error| error.to_string())?,
];
let mut result = Zval::new();
let status = try_catch(std::panic::AssertUnwindSafe(|| {
unsafe {
ext_php_rs::ffi::_call_user_function_impl(
std::ptr::null_mut(),
&raw mut callable,
&raw mut result,
parameters.len() as u32,
parameters.as_mut_ptr(),
std::ptr::null_mut(),
)
}
}))
.map_err(|error| error.to_string())?;
if ExecutorGlobals::has_exception() {
return Err(exception_message("PHP bootstrap threw an exception"));
}
if status < 0 {
return Err("PHP bootstrap callable failed".into());
}
Ok(())
}
#[cfg(test)]
#[path = "../tests/evaluate.rs"]
mod tests;