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

extphprs / ext-php-rs / 21751317827

06 Feb 2026 12:55PM UTC coverage: 34.803% (+0.4%) from 34.453%
21751317827

Pull #671

github

ptondereau
feat: eval PHP code from files
Pull Request #671: feat: eval PHP code from files

31 of 34 new or added lines in 1 file covered. (91.18%)

2116 of 6080 relevant lines covered (34.8%)

23.52 hits per line

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

91.18
/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
//! const SETUP: &[u8] = include_bytes!("../php/setup.php");
16
//!
17
//! php_eval::execute(SETUP).expect("failed to execute embedded PHP");
18
//! ```
19

20
use crate::ffi;
21
use crate::types::ZendStr;
22
use crate::zend::try_catch;
23
use std::mem;
24
use std::panic::AssertUnwindSafe;
25

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

39
/// Execute embedded PHP code within the running PHP engine.
40
///
41
/// The code **must** start with a `<?php` opening tag (case-insensitive),
42
/// optionally preceded by a UTF-8 BOM and/or whitespace. The tag is
43
/// stripped before compilation. The C wrapper uses
44
/// `ZEND_COMPILE_POSITION_AFTER_OPEN_TAG` (on PHP 8.2+) so the scanner
45
/// starts directly in PHP mode.
46
///
47
/// Error reporting is suppressed during execution and restored afterward,
48
/// matching the pattern used by production PHP extensions like Blackfire.
49
///
50
/// # Arguments
51
///
52
/// * `code` - Raw PHP source bytes, typically from `include_bytes!`.
53
///
54
/// # Errors
55
///
56
/// Returns [`PhpEvalError::MissingOpenTag`] if the code does not start
57
/// with `<?php`. Returns other [`PhpEvalError`] variants if compilation
58
/// fails, an exception is thrown, or a fatal error occurs.
59
pub fn execute(code: &[u8]) -> Result<(), PhpEvalError> {
8✔
60
    let code = strip_bom(code);
24✔
61
    let code = strip_php_open_tag(code).ok_or(PhpEvalError::MissingOpenTag)?;
40✔
62

63
    if code.is_empty() {
12✔
NEW
64
        return Ok(());
×
65
    }
66

67
    let source = ZendStr::new(code, false);
18✔
68

69
    let result = try_catch(AssertUnwindSafe(|| unsafe {
12✔
70
        let eg = ffi::ext_php_rs_executor_globals();
12✔
71
        // Suppress error reporting so compilation warnings from embedded
72
        // code don't bubble up to the application's error handler.
73
        let prev_error_reporting = mem::take(&mut (*eg).error_reporting);
18✔
74

75
        let op_array = ffi::ext_php_rs_zend_compile_string(
12✔
76
            source.as_ptr().cast_mut(),
12✔
77
            c"embedded_php".as_ptr(),
12✔
78
        );
79

80
        if op_array.is_null() {
12✔
81
            (*eg).error_reporting = prev_error_reporting;
1✔
82
            return Err(PhpEvalError::CompilationFailed);
1✔
83
        }
84

85
        ffi::ext_php_rs_zend_execute(op_array);
10✔
86

87
        (*eg).error_reporting = prev_error_reporting;
5✔
88

89
        if !(*eg).exception.is_null() {
5✔
NEW
90
            return Err(PhpEvalError::ExecutionFailed);
×
91
        }
92

93
        Ok(())
5✔
94
    }));
95

96
    match result {
6✔
NEW
97
        Err(_) => Err(PhpEvalError::Bailout),
×
98
        Ok(inner) => inner,
12✔
99
    }
100
}
101

102
fn strip_bom(code: &[u8]) -> &[u8] {
11✔
103
    if code.starts_with(&[0xEF, 0xBB, 0xBF]) {
33✔
104
        &code[3..]
2✔
105
    } else {
106
        code
9✔
107
    }
108
}
109

110
fn strip_php_open_tag(code: &[u8]) -> Option<&[u8]> {
20✔
111
    let trimmed = match code.iter().position(|b| !b.is_ascii_whitespace()) {
101✔
112
        Some(pos) => &code[pos..],
34✔
113
        None => return None,
3✔
114
    };
115

116
    if trimmed.len() >= 5 && trimmed[..5].eq_ignore_ascii_case(b"<?php") {
68✔
117
        Some(trimmed[5..].trim_ascii_start())
14✔
118
    } else {
119
        None
3✔
120
    }
121
}
122

123
#[cfg(feature = "embed")]
124
#[cfg(test)]
125
mod tests {
126
    #![allow(clippy::unwrap_used)]
127
    use super::*;
128
    use crate::embed::Embed;
129

130
    #[test]
131
    fn test_execute_with_php_open_tag() {
132
        Embed::run(|| {
133
            let result = execute(b"<?php $x = 42;");
134
            assert!(result.is_ok());
135
        });
136
    }
137

138
    #[test]
139
    fn test_execute_with_php_open_tag_and_newline() {
140
        Embed::run(|| {
141
            let result = execute(b"<?php\n$x = 42;");
142
            assert!(result.is_ok());
143
        });
144
    }
145

146
    #[test]
147
    fn test_execute_missing_open_tag() {
148
        Embed::run(|| {
149
            let result = execute(b"$x = 1 + 2;");
150
            assert!(matches!(result, Err(PhpEvalError::MissingOpenTag)));
151
        });
152
    }
153

154
    #[test]
155
    fn test_execute_compilation_error() {
156
        Embed::run(|| {
157
            let result = execute(b"<?php this is not valid php {{{");
158
            assert!(matches!(result, Err(PhpEvalError::CompilationFailed)));
159
        });
160
    }
161

162
    #[test]
163
    fn test_execute_with_bom() {
164
        Embed::run(|| {
165
            let mut code = vec![0xEF, 0xBB, 0xBF];
166
            code.extend_from_slice(b"<?php $x = 'bom_test';");
167
            let result = execute(&code);
168
            assert!(result.is_ok());
169
        });
170
    }
171

172
    #[test]
173
    fn test_execute_defines_variable() {
174
        Embed::run(|| {
175
            let result = execute(b"<?php $embed_test = 'hello from embedded php';");
176
            assert!(result.is_ok());
177

178
            let val = Embed::eval("$embed_test;");
179
            assert!(val.is_ok());
180
            assert_eq!(val.unwrap().string().unwrap(), "hello from embedded php");
181
        });
182
    }
183

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

192
    #[test]
193
    fn test_execute_include_bytes_pattern() {
194
        Embed::run(|| {
195
            let code: &[u8] = b"<?php\n\
196
                $embedded_value = 42;\n\
197
                define('EMBEDDED_CONST', true);\n";
198
            let result = execute(code);
199
            assert!(result.is_ok());
200
        });
201
    }
202

203
    #[test]
204
    fn test_strip_bom() {
205
        let cases: &[(&[u8], &[u8])] = &[
206
            (&[0xEF, 0xBB, 0xBF, b'h', b'i'], b"hi"),
207
            (b"hello", b"hello"),
208
            (b"", b""),
209
        ];
210
        for (input, expected) in cases {
211
            assert_eq!(
212
                super::strip_bom(input),
213
                *expected,
214
                "input: {:?}",
215
                String::from_utf8_lossy(input)
216
            );
217
        }
218
    }
219

220
    #[test]
221
    fn test_strip_php_open_tag() {
222
        let cases: &[(&[u8], Option<&[u8]>)] = &[
223
            (b"<?php $x;", Some(b"$x;")),
224
            (b"<?php\n$x;", Some(b"$x;")),
225
            (b"<?php\r\n$x;", Some(b"$x;")),
226
            (b"<?php\t\n  $x;", Some(b"$x;")),
227
            (b"<?php", Some(b"")),
228
            (b"  <?php $x;", Some(b"$x;")),
229
            (b"<?PHP $x;", Some(b"$x;")),
230
            (b"<?Php\n$x;", Some(b"$x;")),
231
            (b"", None),
232
            (b"   ", None),
233
            (b"$x = 1;", None),
234
            (b"hello", None),
235
        ];
236
        for (input, expected) in cases {
237
            assert_eq!(
238
                super::strip_php_open_tag(input),
239
                *expected,
240
                "input: {:?}",
241
                String::from_utf8_lossy(input)
242
            );
243
        }
244
    }
245
}
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