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