mod context;
mod ffi;
mod sapi;
mod sapi_trait;
mod server_vars;
mod thread;
mod worker;
use crate::boxed::ZBox;
use crate::ffi::{
_zend_file_handle__bindgen_ty_1, ZEND_RESULT_CODE_SUCCESS, php_execute_script,
zend_destroy_file_handle, zend_eval_string, zend_file_handle, zend_stream_init_filename,
};
use crate::types::{ZendObject, Zval};
use crate::zend::{ExecutorGlobals, panic_wrapper, try_catch};
use parking_lot::{RwLock, const_rwlock};
use std::ffi::{CString, NulError, c_char, c_void};
use std::panic::{AssertUnwindSafe, UnwindSafe, resume_unwind};
use std::path::Path;
use std::ptr::null_mut;
pub use context::{RequestInfo, ServerContext};
pub use ffi::*;
pub use sapi::SapiModule;
pub use sapi_trait::{Sapi, SapiHeader, SapiHeaders, SendHeadersResult};
pub use server_vars::ServerVarRegistrar;
pub use thread::PhpThreadGuard;
pub use worker::{
WorkerError, worker_request_shutdown, worker_request_startup, worker_reset_superglobals,
};
pub struct Embed;
#[derive(Debug)]
pub enum EmbedError {
InitError,
ExecuteError(Option<ZBox<ZendObject>>),
ExecuteScriptError,
InvalidEvalString(NulError),
InvalidPath,
CatchError,
}
impl EmbedError {
#[must_use]
pub fn is_bailout(&self) -> bool {
matches!(self, EmbedError::CatchError)
}
}
static RUN_FN_LOCK: RwLock<()> = const_rwlock(());
impl Embed {
pub fn run_script<P: AsRef<Path>>(path: P) -> Result<(), EmbedError> {
let path = match path.as_ref().to_str() {
Some(path) => match CString::new(path) {
Ok(path) => path,
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
},
None => return Err(EmbedError::InvalidPath),
};
let mut file_handle = zend_file_handle {
#[allow(clippy::used_underscore_items)]
handle: _zend_file_handle__bindgen_ty_1 { fp: null_mut() },
filename: null_mut(),
opened_path: null_mut(),
type_: 0,
primary_script: false,
in_list: false,
buf: null_mut(),
len: 0,
};
unsafe {
zend_stream_init_filename(&raw mut file_handle, path.as_ptr());
}
let exec_result = try_catch(AssertUnwindSafe(|| unsafe {
php_execute_script(&raw mut file_handle)
}));
unsafe { zend_destroy_file_handle(&raw mut file_handle) }
match exec_result {
Err(_) => Err(EmbedError::CatchError),
Ok(true) => Ok(()),
Ok(false) => Err(EmbedError::ExecuteScriptError),
}
}
pub fn run<R, F: FnOnce() -> R + UnwindSafe>(func: F) -> R
where
R: Default,
{
let _guard = RUN_FN_LOCK.write();
let panic = unsafe {
ext_php_rs_embed_callback(
0,
null_mut(),
panic_wrapper::<R, F>,
(&raw const func).cast::<c_void>(),
)
};
std::mem::forget(func);
if panic.is_null() {
return R::default();
}
match unsafe { *Box::from_raw(panic.cast::<std::thread::Result<R>>()) } {
Ok(r) => r,
Err(err) => {
resume_unwind(err);
}
}
}
pub fn eval(code: &str) -> Result<Zval, EmbedError> {
let cstr = match CString::new(code) {
Ok(cstr) => cstr,
Err(err) => return Err(EmbedError::InvalidEvalString(err)),
};
let mut result = Zval::new();
let exec_result = try_catch(AssertUnwindSafe(|| unsafe {
zend_eval_string(
cstr.as_ptr().cast::<c_char>(),
&raw mut result,
c"run".as_ptr().cast(),
)
}));
match exec_result {
Err(_) => Err(EmbedError::CatchError),
Ok(ZEND_RESULT_CODE_SUCCESS) => Ok(result),
Ok(_) => Err(EmbedError::ExecuteError(ExecutorGlobals::take_exception())),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::Embed;
#[test]
fn test_run() {
Embed::run(|| {
let result = Embed::eval("$foo = 'foo';");
assert!(result.is_ok());
});
}
#[test]
fn test_run_error() {
Embed::run(|| {
let result = Embed::eval("stupid code;");
assert!(result.is_err());
});
}
#[test]
fn test_run_script() {
Embed::run(|| {
let result = Embed::run_script("src/embed/test-script.php");
assert!(result.is_ok());
let zval = Embed::eval("$foo;").unwrap();
assert!(zval.is_object());
let obj = zval.object().unwrap();
assert_eq!(obj.get_class_name().unwrap(), "Test");
});
}
#[test]
fn test_run_script_error() {
Embed::run(|| {
let result = Embed::run_script("src/embed/test-script-exception.php");
assert!(result.is_err());
});
}
#[test]
#[should_panic(expected = "test panic")]
fn test_panic() {
Embed::run::<(), _>(|| {
panic!("test panic");
});
}
#[test]
fn test_return() {
let foo = Embed::run(|| "foo");
assert_eq!(foo, "foo");
}
#[test]
fn test_eval_bailout() {
Embed::run(|| {
let result = Embed::eval("trigger_error(\"Fatal error\", E_USER_ERROR);");
assert!(result.is_err());
assert!(result.unwrap_err().is_bailout());
});
}
#[test]
fn test_php_write() {
use crate::zend::write;
Embed::run(|| {
let bytes_written = write(b"Hello").expect("write failed");
assert_eq!(bytes_written, 5);
let bytes_written = write(b"Hello\x00World").expect("write failed");
assert_eq!(bytes_written, 11);
let bytes_written = php_write!(b"Test").expect("php_write failed");
assert_eq!(bytes_written, 4);
let bytes_written = php_write!(b"Binary\x00Data\x00Here").expect("php_write failed");
assert_eq!(bytes_written, 16);
let data: &[u8] = &[0x48, 0x65, 0x6c, 0x6c, 0x6f]; let bytes_written = php_write!(data).expect("php_write failed");
assert_eq!(bytes_written, 5);
let bytes_written = write(b"").expect("write failed");
assert_eq!(bytes_written, 0);
});
}
#[test]
fn test_php_write_bypasses_output_buffering() {
use crate::zend::write;
Embed::run(|| {
Embed::eval("ob_start();").expect("ob_start failed");
write(b"Direct output").expect("write failed");
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
let output = result.string().expect("expected string result");
assert_eq!(output, "", "ub_write should bypass output buffering");
});
}
#[test]
fn test_php_print_respects_output_buffering() {
use crate::zend::printf;
Embed::run(|| {
Embed::eval("ob_start();").expect("ob_start failed");
printf("Hello from Rust").expect("printf failed");
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
let output = result.string().expect("expected string result");
assert_eq!(output, "Hello from Rust");
});
}
#[test]
fn test_php_output_write_binary_safe_with_buffering() {
use crate::zend::output_write;
Embed::run(|| {
Embed::eval("ob_start();").expect("ob_start failed");
let bytes_written = output_write(b"Hello\x00World");
assert_eq!(bytes_written, 11);
let result = Embed::eval("ob_get_clean();").expect("ob_get_clean failed");
let output = result.string().expect("expected string result");
assert_eq!(output, "Hello\x00World");
});
}
}