• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 23718488839

29 Mar 2026 08:35PM UTC coverage: 65.13% (+31.0%) from 34.103%
23718488839

push

github

web-flow
ci(coverage): switch from tarpaulin to cargo-llvm-cov (#702)

7811 of 11993 relevant lines covered (65.13%)

32.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.32
/src/php_eval.rs
1
//! Execute embedded PHP code within a running PHP extension.
2
//!
3
//! This module provides a way to compile and execute PHP code that has been
4
//! embedded into the extension binary at compile time using `include_bytes!`.
5
//!
6
//! Uses `zend_compile_string` + `zend_execute` (not `zend_eval_string`)
7
//! to avoid security scanner false positives and compatibility issues
8
//! with hardened PHP configurations.
9
//!
10
//! # Example
11
//!
12
//! ```rust,ignore
13
//! use ext_php_rs::php_eval;
14
//!
15
//! // Both include_bytes! and include_str! are supported:
16
//! const SETUP_BYTES: &[u8] = include_bytes!("../php/setup.php");
17
//! const SETUP_STR: &str = include_str!("../php/setup.php");
18
//!
19
//! php_eval::execute(SETUP_BYTES).expect("failed to execute embedded PHP");
20
//! php_eval::execute(SETUP_STR).expect("failed to execute embedded PHP");
21
//! ```
22

23
use crate::ffi;
24
use crate::types::ZendStr;
25
use crate::zend::try_catch;
26
use std::fmt;
27
use std::mem;
28
use std::panic::AssertUnwindSafe;
29

30
/// Errors that can occur when executing embedded PHP code.
31
#[derive(Debug)]
32
pub enum PhpEvalError {
33
    /// The code does not start with a `<?php` open tag.
34
    MissingOpenTag,
35
    /// PHP failed to compile the code (syntax error).
36
    CompilationFailed,
37
    /// The code executed but threw an unhandled exception.
38
    ExecutionFailed,
39
    /// A PHP fatal error (bailout) occurred during execution.
40
    Bailout,
41
}
42

43
impl 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

58
impl std::error::Error for PhpEvalError {}
59

60
/// Execute embedded PHP code within the running PHP engine.
61
///
62
/// The code **must** start with a `<?php` opening tag (case-insensitive),
63
/// optionally preceded by a UTF-8 BOM and/or whitespace. The tag is
64
/// stripped before compilation. The C wrapper uses
65
/// `ZEND_COMPILE_POSITION_AFTER_OPEN_TAG` (on PHP 8.2+) so the scanner
66
/// starts directly in PHP mode.
67
///
68
/// Error reporting is suppressed during execution and restored afterward,
69
/// matching the pattern used by production PHP extensions like Blackfire.
70
///
71
/// # Arguments
72
///
73
/// * `code` - Raw PHP source, typically from `include_bytes!` or
74
///   `include_str!`. Any type implementing `AsRef<[u8]>` is accepted.
75
///
76
/// # Errors
77
///
78
/// Returns [`PhpEvalError::MissingOpenTag`] if the code does not start
79
/// with `<?php`. Returns other [`PhpEvalError`] variants if compilation
80
/// fails, an exception is thrown, or a fatal error occurs.
81
pub fn execute(code: impl AsRef<[u8]>) -> Result<(), PhpEvalError> {
13✔
82
    let code = strip_bom(code.as_ref());
13✔
83
    let code = strip_php_open_tag(code).ok_or(PhpEvalError::MissingOpenTag)?;
13✔
84

85
    if code.is_empty() {
11✔
86
        return Ok(());
1✔
87
    }
10✔
88

89
    let source = ZendStr::new(code, false);
10✔
90

91
    // Suppress error reporting so compilation warnings from embedded
92
    // code don't bubble up to the application's error handler.
93
    // Saved outside `try_catch` so it is always restored, even on bailout.
94
    let eg = unsafe { ffi::ext_php_rs_executor_globals() };
10✔
95
    let prev_error_reporting = unsafe { mem::replace(&mut (*eg).error_reporting, 0) };
10✔
96

97
    let result = try_catch(AssertUnwindSafe(|| unsafe {
10✔
98
        let op_array = ffi::ext_php_rs_zend_compile_string(
10✔
99
            source.as_ptr().cast_mut(),
10✔
100
            c"embedded_php".as_ptr(),
10✔
101
        );
102

103
        if op_array.is_null() {
10✔
104
            return Err(PhpEvalError::CompilationFailed);
1✔
105
        }
9✔
106

107
        ffi::ext_php_rs_zend_execute(op_array);
9✔
108

109
        if !(*eg).exception.is_null() {
9✔
110
            return Err(PhpEvalError::ExecutionFailed);
1✔
111
        }
8✔
112

113
        Ok(())
8✔
114
    }));
10✔
115

116
    unsafe { (*eg).error_reporting = prev_error_reporting };
10✔
117

118
    match result {
10✔
119
        Err(_) => Err(PhpEvalError::Bailout),
×
120
        Ok(inner) => inner,
10✔
121
    }
122
}
13✔
123

124
fn strip_bom(code: &[u8]) -> &[u8] {
16✔
125
    if code.starts_with(&[0xEF, 0xBB, 0xBF]) {
16✔
126
        &code[3..]
2✔
127
    } else {
128
        code
14✔
129
    }
130
}
16✔
131

132
fn strip_php_open_tag(code: &[u8]) -> Option<&[u8]> {
25✔
133
    let trimmed = match code.iter().position(|b| !b.is_ascii_whitespace()) {
27✔
134
        Some(pos) => &code[pos..],
22✔
135
        None => return None,
3✔
136
    };
137

138
    if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case(b"<?php") {
22✔
139
        Some(trimmed[5..].trim_ascii_start())
19✔
140
    } else {
141
        None
3✔
142
    }
143
}
25✔
144

145
#[cfg(feature = "embed")]
146
#[cfg(test)]
147
mod 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() {
1✔
154
        Embed::run(|| {
1✔
155
            let result = execute(b"<?php $x = 42;");
1✔
156
            assert!(result.is_ok());
1✔
157
        });
1✔
158
    }
1✔
159

160
    #[test]
161
    fn test_execute_with_php_open_tag_and_newline() {
1✔
162
        Embed::run(|| {
1✔
163
            let result = execute(b"<?php\n$x = 42;");
1✔
164
            assert!(result.is_ok());
1✔
165
        });
1✔
166
    }
1✔
167

168
    #[test]
169
    fn test_execute_tag_only() {
1✔
170
        Embed::run(|| {
1✔
171
            let result = execute(b"<?php");
1✔
172
            assert!(result.is_ok());
1✔
173
        });
1✔
174
    }
1✔
175

176
    #[test]
177
    fn test_execute_exception() {
1✔
178
        Embed::run(|| {
1✔
179
            let result = execute(b"<?php throw new \\RuntimeException('test');");
1✔
180
            assert!(matches!(result, Err(PhpEvalError::ExecutionFailed)));
1✔
181
        });
1✔
182
    }
1✔
183

184
    #[test]
185
    fn test_execute_missing_open_tag() {
1✔
186
        Embed::run(|| {
1✔
187
            let result = execute(b"$x = 1 + 2;");
1✔
188
            assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
1✔
189
        });
1✔
190
    }
1✔
191

192
    #[test]
193
    fn test_execute_compilation_error() {
1✔
194
        Embed::run(|| {
1✔
195
            let result = execute(b"<?php this is not valid php {{{");
1✔
196
            assert!(matches!(result, Err(PhpEvalError::CompilationFailed)));
1✔
197
        });
1✔
198
    }
1✔
199

200
    #[test]
201
    fn test_execute_with_bom() {
1✔
202
        Embed::run(|| {
1✔
203
            let mut code = vec![0xEF, 0xBB, 0xBF];
1✔
204
            code.extend_from_slice(b"<?php $x = 'bom_test';");
1✔
205
            let result = execute(&code);
1✔
206
            assert!(result.is_ok());
1✔
207
        });
1✔
208
    }
1✔
209

210
    #[test]
211
    fn test_execute_defines_variable() {
1✔
212
        Embed::run(|| {
1✔
213
            let result = execute(b"<?php $embed_test = 'hello from embedded php';");
1✔
214
            assert!(result.is_ok());
1✔
215

216
            let val = Embed::eval("$embed_test;");
1✔
217
            assert!(val.is_ok());
1✔
218
            assert_eq!(val.unwrap().string().unwrap(), "hello from embedded php");
1✔
219
        });
1✔
220
    }
1✔
221

222
    #[test]
223
    fn test_execute_empty_code() {
1✔
224
        Embed::run(|| {
1✔
225
            let result = execute(b"");
1✔
226
            assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
1✔
227
        });
1✔
228
    }
1✔
229

230
    #[test]
231
    fn test_execute_include_bytes_pattern() {
1✔
232
        Embed::run(|| {
1✔
233
            let code: &[u8] = b"<?php\n\
1✔
234
                $embedded_value = 42;\n\
1✔
235
                define('EMBEDDED_CONST', true);\n";
1✔
236
            let result = execute(code);
1✔
237
            assert!(result.is_ok());
1✔
238
        });
1✔
239
    }
1✔
240

241
    #[test]
242
    fn test_execute_with_str() {
1✔
243
        Embed::run(|| {
1✔
244
            let code: &str = "<?php $str_test = 'from_str';";
1✔
245
            let result = execute(code);
1✔
246
            assert!(result.is_ok());
1✔
247

248
            let val = Embed::eval("$str_test;");
1✔
249
            assert!(val.is_ok());
1✔
250
            assert_eq!(val.unwrap().string().unwrap(), "from_str");
1✔
251
        });
1✔
252
    }
1✔
253

254
    #[test]
255
    fn test_execute_with_string() {
1✔
256
        Embed::run(|| {
1✔
257
            let code = String::from("<?php $string_test = 'from_string';");
1✔
258
            let result = execute(code);
1✔
259
            assert!(result.is_ok());
1✔
260

261
            let val = Embed::eval("$string_test;");
1✔
262
            assert!(val.is_ok());
1✔
263
            assert_eq!(val.unwrap().string().unwrap(), "from_string");
1✔
264
        });
1✔
265
    }
1✔
266

267
    #[test]
268
    fn test_execute_with_vec() {
1✔
269
        Embed::run(|| {
1✔
270
            let code: Vec<u8> = b"<?php $vec_test = 'from_vec';".to_vec();
1✔
271
            let result = execute(code);
1✔
272
            assert!(result.is_ok());
1✔
273

274
            let val = Embed::eval("$vec_test;");
1✔
275
            assert!(val.is_ok());
1✔
276
            assert_eq!(val.unwrap().string().unwrap(), "from_vec");
1✔
277
        });
1✔
278
    }
1✔
279

280
    #[test]
281
    fn test_strip_bom() {
1✔
282
        let cases: &[(&[u8], &[u8])] = &[
1✔
283
            (&[0xEF, 0xBB, 0xBF, b'h', b'i'], b"hi"),
1✔
284
            (b"hello", b"hello"),
1✔
285
            (b"", b""),
1✔
286
        ];
1✔
287
        for (input, expected) in cases {
3✔
288
            assert_eq!(
3✔
289
                super::strip_bom(input),
3✔
290
                *expected,
291
                "input: {:?}",
292
                String::from_utf8_lossy(input)
×
293
            );
294
        }
295
    }
1✔
296

297
    #[test]
298
    fn test_strip_php_open_tag() {
1✔
299
        let cases: &[(&[u8], Option<&[u8]>)] = &[
1✔
300
            (b"<?php $x;", Some(b"$x;")),
1✔
301
            (b"<?php\n$x;", Some(b"$x;")),
1✔
302
            (b"<?php\r\n$x;", Some(b"$x;")),
1✔
303
            (b"<?php\t\n  $x;", Some(b"$x;")),
1✔
304
            (b"<?php", Some(b"")),
1✔
305
            (b"  <?php $x;", Some(b"$x;")),
1✔
306
            (b"<?PHP $x;", Some(b"$x;")),
1✔
307
            (b"<?Php\n$x;", Some(b"$x;")),
1✔
308
            (b"", None),
1✔
309
            (b"   ", None),
1✔
310
            (b"$x = 1;", None),
1✔
311
            (b"hello", None),
1✔
312
        ];
1✔
313
        for (input, expected) in cases {
12✔
314
            assert_eq!(
12✔
315
                super::strip_php_open_tag(input),
12✔
316
                *expected,
317
                "input: {:?}",
318
                String::from_utf8_lossy(input)
×
319
            );
320
        }
321
    }
1✔
322
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc