use crate::ffi;
use crate::types::ZendStr;
use crate::zend::try_catch;
use std::fmt;
use std::mem;
use std::panic::AssertUnwindSafe;
#[derive(Debug)]
pub enum PhpEvalError {
MissingOpenTag,
CompilationFailed,
ExecutionFailed,
Bailout,
}
impl fmt::Display for PhpEvalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PhpEvalError::MissingOpenTag => {
write!(f, "PHP code must start with a <?php open tag")
}
PhpEvalError::CompilationFailed => write!(f, "PHP compilation failed (syntax error)"),
PhpEvalError::ExecutionFailed => {
write!(f, "PHP execution threw an unhandled exception")
}
PhpEvalError::Bailout => write!(f, "PHP fatal error (bailout) during execution"),
}
}
}
impl std::error::Error for PhpEvalError {}
pub fn execute(code: impl AsRef<[u8]>) -> Result<(), PhpEvalError> {
let code = strip_bom(code.as_ref());
let code = strip_php_open_tag(code).ok_or(PhpEvalError::MissingOpenTag)?;
if code.is_empty() {
return Ok(());
}
let source = ZendStr::new(code, false);
let eg = unsafe { ffi::ext_php_rs_executor_globals() };
let prev_error_reporting = unsafe { mem::replace(&mut (*eg).error_reporting, 0) };
let result = try_catch(AssertUnwindSafe(|| unsafe {
let op_array = ffi::ext_php_rs_zend_compile_string(
source.as_ptr().cast_mut(),
c"embedded_php".as_ptr(),
);
if op_array.is_null() {
return Err(PhpEvalError::CompilationFailed);
}
ffi::ext_php_rs_zend_execute(op_array);
if !(*eg).exception.is_null() {
return Err(PhpEvalError::ExecutionFailed);
}
Ok(())
}));
unsafe { (*eg).error_reporting = prev_error_reporting };
match result {
Err(_) => Err(PhpEvalError::Bailout),
Ok(inner) => inner,
}
}
fn strip_bom(code: &[u8]) -> &[u8] {
if code.starts_with(&[0xEF, 0xBB, 0xBF]) {
&code[3..]
} else {
code
}
}
fn strip_php_open_tag(code: &[u8]) -> Option<&[u8]> {
let trimmed = match code.iter().position(|b| !b.is_ascii_whitespace()) {
Some(pos) => &code[pos..],
None => return None,
};
if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case(b"<?php") {
Some(trimmed[5..].trim_ascii_start())
} else {
None
}
}
#[cfg(feature = "embed")]
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
use crate::embed::Embed;
#[test]
fn test_execute_with_php_open_tag() {
Embed::run(|| {
let result = execute(b"<?php $x = 42;");
assert!(result.is_ok());
});
}
#[test]
fn test_execute_with_php_open_tag_and_newline() {
Embed::run(|| {
let result = execute(b"<?php\n$x = 42;");
assert!(result.is_ok());
});
}
#[test]
fn test_execute_tag_only() {
Embed::run(|| {
let result = execute(b"<?php");
assert!(result.is_ok());
});
}
#[test]
fn test_execute_exception() {
Embed::run(|| {
let result = execute(b"<?php throw new \\RuntimeException('test');");
assert!(matches!(result, Err(PhpEvalError::ExecutionFailed)));
});
}
#[test]
fn test_execute_missing_open_tag() {
Embed::run(|| {
let result = execute(b"$x = 1 + 2;");
assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
});
}
#[test]
fn test_execute_compilation_error() {
Embed::run(|| {
let result = execute(b"<?php this is not valid php {{{");
assert!(matches!(result, Err(PhpEvalError::CompilationFailed)));
});
}
#[test]
fn test_execute_with_bom() {
Embed::run(|| {
let mut code = vec![0xEF, 0xBB, 0xBF];
code.extend_from_slice(b"<?php $x = 'bom_test';");
let result = execute(&code);
assert!(result.is_ok());
});
}
#[test]
fn test_execute_defines_variable() {
Embed::run(|| {
let result = execute(b"<?php $embed_test = 'hello from embedded php';");
assert!(result.is_ok());
let val = Embed::eval("$embed_test;");
assert!(val.is_ok());
assert_eq!(val.unwrap().string().unwrap(), "hello from embedded php");
});
}
#[test]
fn test_execute_empty_code() {
Embed::run(|| {
let result = execute(b"");
assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
});
}
#[test]
fn test_execute_include_bytes_pattern() {
Embed::run(|| {
let code: &[u8] = b"<?php\n\
$embedded_value = 42;\n\
define('EMBEDDED_CONST', true);\n";
let result = execute(code);
assert!(result.is_ok());
});
}
#[test]
fn test_execute_with_str() {
Embed::run(|| {
let code: &str = "<?php $str_test = 'from_str';";
let result = execute(code);
assert!(result.is_ok());
let val = Embed::eval("$str_test;");
assert!(val.is_ok());
assert_eq!(val.unwrap().string().unwrap(), "from_str");
});
}
#[test]
fn test_execute_with_string() {
Embed::run(|| {
let code = String::from("<?php $string_test = 'from_string';");
let result = execute(code);
assert!(result.is_ok());
let val = Embed::eval("$string_test;");
assert!(val.is_ok());
assert_eq!(val.unwrap().string().unwrap(), "from_string");
});
}
#[test]
fn test_execute_with_vec() {
Embed::run(|| {
let code: Vec<u8> = b"<?php $vec_test = 'from_vec';".to_vec();
let result = execute(code);
assert!(result.is_ok());
let val = Embed::eval("$vec_test;");
assert!(val.is_ok());
assert_eq!(val.unwrap().string().unwrap(), "from_vec");
});
}
#[test]
fn test_strip_bom() {
let cases: &[(&[u8], &[u8])] = &[
(&[0xEF, 0xBB, 0xBF, b'h', b'i'], b"hi"),
(b"hello", b"hello"),
(b"", b""),
];
for (input, expected) in cases {
assert_eq!(
super::strip_bom(input),
*expected,
"input: {:?}",
String::from_utf8_lossy(input)
);
}
}
#[test]
fn test_strip_php_open_tag() {
let cases: &[(&[u8], Option<&[u8]>)] = &[
(b"<?php $x;", Some(b"$x;")),
(b"<?php\n$x;", Some(b"$x;")),
(b"<?php\r\n$x;", Some(b"$x;")),
(b"<?php\t\n $x;", Some(b"$x;")),
(b"<?php", Some(b"")),
(b" <?php $x;", Some(b"$x;")),
(b"<?PHP $x;", Some(b"$x;")),
(b"<?Php\n$x;", Some(b"$x;")),
(b"", None),
(b" ", None),
(b"$x = 1;", None),
(b"hello", None),
];
for (input, expected) in cases {
assert_eq!(
super::strip_php_open_tag(input),
*expected,
"input: {:?}",
String::from_utf8_lossy(input)
);
}
}
}