1use crate::ffi;
24use crate::types::ZendStr;
25use crate::zend::try_catch;
26use std::fmt;
27use std::mem;
28use std::panic::AssertUnwindSafe;
29
30#[derive(Debug)]
32pub enum PhpEvalError {
33 MissingOpenTag,
35 CompilationFailed,
37 ExecutionFailed,
39 Bailout,
41}
42
43impl fmt::Display for PhpEvalError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 PhpEvalError::MissingOpenTag => {
47 write!(f, "PHP code must start with a <?php open tag")
48 }
49 PhpEvalError::CompilationFailed => write!(f, "PHP compilation failed (syntax error)"),
50 PhpEvalError::ExecutionFailed => {
51 write!(f, "PHP execution threw an unhandled exception")
52 }
53 PhpEvalError::Bailout => write!(f, "PHP fatal error (bailout) during execution"),
54 }
55 }
56}
57
58impl std::error::Error for PhpEvalError {}
59
60pub fn execute(code: impl AsRef<[u8]>) -> Result<(), PhpEvalError> {
82 let code = strip_bom(code.as_ref());
83 let code = strip_php_open_tag(code).ok_or(PhpEvalError::MissingOpenTag)?;
84
85 if code.is_empty() {
86 return Ok(());
87 }
88
89 let source = ZendStr::new(code, false);
90
91 let eg = unsafe { ffi::ext_php_rs_executor_globals() };
95 let prev_error_reporting = unsafe { mem::replace(&mut (*eg).error_reporting, 0) };
96
97 let result = try_catch(AssertUnwindSafe(|| unsafe {
98 let op_array = ffi::ext_php_rs_zend_compile_string(
99 source.as_ptr().cast_mut(),
100 c"embedded_php".as_ptr(),
101 );
102
103 if op_array.is_null() {
104 return Err(PhpEvalError::CompilationFailed);
105 }
106
107 ffi::ext_php_rs_zend_execute(op_array);
108
109 if !(*eg).exception.is_null() {
110 return Err(PhpEvalError::ExecutionFailed);
111 }
112
113 Ok(())
114 }));
115
116 unsafe { (*eg).error_reporting = prev_error_reporting };
117
118 match result {
119 Err(_) => Err(PhpEvalError::Bailout),
120 Ok(inner) => inner,
121 }
122}
123
124fn strip_bom(code: &[u8]) -> &[u8] {
125 if code.starts_with(&[0xEF, 0xBB, 0xBF]) {
126 &code[3..]
127 } else {
128 code
129 }
130}
131
132fn strip_php_open_tag(code: &[u8]) -> Option<&[u8]> {
133 let trimmed = match code.iter().position(|b| !b.is_ascii_whitespace()) {
134 Some(pos) => &code[pos..],
135 None => return None,
136 };
137
138 if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case(b"<?php") {
139 Some(trimmed[5..].trim_ascii_start())
140 } else {
141 None
142 }
143}
144
145#[cfg(feature = "embed")]
146#[cfg(test)]
147mod tests {
148 #![allow(clippy::unwrap_used)]
149 use super::*;
150 use crate::embed::Embed;
151
152 #[test]
153 fn test_execute_with_php_open_tag() {
154 Embed::run(|| {
155 let result = execute(b"<?php $x = 42;");
156 assert!(result.is_ok());
157 });
158 }
159
160 #[test]
161 fn test_execute_with_php_open_tag_and_newline() {
162 Embed::run(|| {
163 let result = execute(b"<?php\n$x = 42;");
164 assert!(result.is_ok());
165 });
166 }
167
168 #[test]
169 fn test_execute_tag_only() {
170 Embed::run(|| {
171 let result = execute(b"<?php");
172 assert!(result.is_ok());
173 });
174 }
175
176 #[test]
177 fn test_execute_exception() {
178 Embed::run(|| {
179 let result = execute(b"<?php throw new \\RuntimeException('test');");
180 assert!(matches!(result, Err(PhpEvalError::ExecutionFailed)));
181 });
182 }
183
184 #[test]
185 fn test_execute_missing_open_tag() {
186 Embed::run(|| {
187 let result = execute(b"$x = 1 + 2;");
188 assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
189 });
190 }
191
192 #[test]
193 fn test_execute_compilation_error() {
194 Embed::run(|| {
195 let result = execute(b"<?php this is not valid php {{{");
196 assert!(matches!(result, Err(PhpEvalError::CompilationFailed)));
197 });
198 }
199
200 #[test]
201 fn test_execute_with_bom() {
202 Embed::run(|| {
203 let mut code = vec![0xEF, 0xBB, 0xBF];
204 code.extend_from_slice(b"<?php $x = 'bom_test';");
205 let result = execute(&code);
206 assert!(result.is_ok());
207 });
208 }
209
210 #[test]
211 fn test_execute_defines_variable() {
212 Embed::run(|| {
213 let result = execute(b"<?php $embed_test = 'hello from embedded php';");
214 assert!(result.is_ok());
215
216 let val = Embed::eval("$embed_test;");
217 assert!(val.is_ok());
218 assert_eq!(val.unwrap().string().unwrap(), "hello from embedded php");
219 });
220 }
221
222 #[test]
223 fn test_execute_empty_code() {
224 Embed::run(|| {
225 let result = execute(b"");
226 assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
227 });
228 }
229
230 #[test]
231 fn test_execute_include_bytes_pattern() {
232 Embed::run(|| {
233 let code: &[u8] = b"<?php\n\
234 $embedded_value = 42;\n\
235 define('EMBEDDED_CONST', true);\n";
236 let result = execute(code);
237 assert!(result.is_ok());
238 });
239 }
240
241 #[test]
242 fn test_execute_with_str() {
243 Embed::run(|| {
244 let code: &str = "<?php $str_test = 'from_str';";
245 let result = execute(code);
246 assert!(result.is_ok());
247
248 let val = Embed::eval("$str_test;");
249 assert!(val.is_ok());
250 assert_eq!(val.unwrap().string().unwrap(), "from_str");
251 });
252 }
253
254 #[test]
255 fn test_execute_with_string() {
256 Embed::run(|| {
257 let code = String::from("<?php $string_test = 'from_string';");
258 let result = execute(code);
259 assert!(result.is_ok());
260
261 let val = Embed::eval("$string_test;");
262 assert!(val.is_ok());
263 assert_eq!(val.unwrap().string().unwrap(), "from_string");
264 });
265 }
266
267 #[test]
268 fn test_execute_with_vec() {
269 Embed::run(|| {
270 let code: Vec<u8> = b"<?php $vec_test = 'from_vec';".to_vec();
271 let result = execute(code);
272 assert!(result.is_ok());
273
274 let val = Embed::eval("$vec_test;");
275 assert!(val.is_ok());
276 assert_eq!(val.unwrap().string().unwrap(), "from_vec");
277 });
278 }
279
280 #[test]
281 fn test_strip_bom() {
282 let cases: &[(&[u8], &[u8])] = &[
283 (&[0xEF, 0xBB, 0xBF, b'h', b'i'], b"hi"),
284 (b"hello", b"hello"),
285 (b"", b""),
286 ];
287 for (input, expected) in cases {
288 assert_eq!(
289 super::strip_bom(input),
290 *expected,
291 "input: {:?}",
292 String::from_utf8_lossy(input)
293 );
294 }
295 }
296
297 #[test]
298 fn test_strip_php_open_tag() {
299 let cases: &[(&[u8], Option<&[u8]>)] = &[
300 (b"<?php $x;", Some(b"$x;")),
301 (b"<?php\n$x;", Some(b"$x;")),
302 (b"<?php\r\n$x;", Some(b"$x;")),
303 (b"<?php\t\n $x;", Some(b"$x;")),
304 (b"<?php", Some(b"")),
305 (b" <?php $x;", Some(b"$x;")),
306 (b"<?PHP $x;", Some(b"$x;")),
307 (b"<?Php\n$x;", Some(b"$x;")),
308 (b"", None),
309 (b" ", None),
310 (b"$x = 1;", None),
311 (b"hello", None),
312 ];
313 for (input, expected) in cases {
314 assert_eq!(
315 super::strip_php_open_tag(input),
316 *expected,
317 "input: {:?}",
318 String::from_utf8_lossy(input)
319 );
320 }
321 }
322}