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

davidcole1340 / ext-php-rs / 16006197694

01 Jul 2025 05:33PM UTC coverage: 21.654% (-0.3%) from 21.978%
16006197694

Pull #471

github

web-flow
Merge 7af465c1e into 68e218f9b
Pull Request #471: feat(sapi): expand `SapiBuilder`

66 of 69 new or added lines in 1 file covered. (95.65%)

748 existing lines in 21 files now uncovered.

830 of 3833 relevant lines covered (21.65%)

3.63 hits per line

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

66.67
/src/embed/mod.rs
1
//! Provides implementations for running php code from rust.
2
//! It only works on linux for now and you should have `php-embed` installed
3
//!
4
//! This crate was only test with PHP 8.2 please report any issue with other
5
//! version You should only use this crate for test purpose, it's not production
6
//! ready
7

8
mod ffi;
9
mod sapi;
10

11
use crate::boxed::ZBox;
12
use crate::embed::ffi::ext_php_rs_embed_callback;
13
use crate::ffi::{
14
    _zend_file_handle__bindgen_ty_1, php_execute_script, zend_eval_string, zend_file_handle,
15
    zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS,
16
};
17
use crate::types::{ZendObject, Zval};
18
use crate::zend::{panic_wrapper, try_catch, ExecutorGlobals};
19
use parking_lot::{const_rwlock, RwLock};
20
use std::ffi::{c_char, c_void, CString, NulError};
21
use std::panic::{resume_unwind, RefUnwindSafe};
22
use std::path::Path;
23
use std::ptr::null_mut;
24

25
pub use ffi::{ext_php_rs_php_error, ext_php_rs_sapi_startup};
26
pub use sapi::SapiModule;
27

28
/// The embed module provides a way to run php code from rust
29
pub struct Embed;
30

31
/// Error type for the embed module
32
#[derive(Debug)]
33
pub enum EmbedError {
34
    /// Failed to initialize
35
    InitError,
36
    /// The script exited with a non-zero code
37
    ExecuteError(Option<ZBox<ZendObject>>),
38
    /// The script exited with a non-zero code
39
    ExecuteScriptError,
40
    /// The script is not a valid [`CString`]
41
    InvalidEvalString(NulError),
42
    /// Failed to open the script file at the given path
43
    InvalidPath,
44
    /// The script was executed but an exception was thrown
45
    CatchError,
46
}
47

48
impl EmbedError {
49
    /// Check if the error is a bailout
50
    #[must_use]
51
    pub fn is_bailout(&self) -> bool {
1✔
52
        matches!(self, EmbedError::CatchError)
1✔
53
    }
54
}
55

56
static RUN_FN_LOCK: RwLock<()> = const_rwlock(());
57

58
impl Embed {
59
    /// Run a php script from a file
60
    ///
61
    /// This function will only work correctly when used inside the `Embed::run`
62
    /// function otherwise behavior is unexpected
63
    ///
64
    /// # Returns
65
    ///
66
    /// * `Ok(())` - The script was executed successfully
67
    ///
68
    /// # Errors
69
    ///
70
    /// * `Err(EmbedError)` - An error occurred during the execution of the
71
    ///   script
72
    ///
73
    /// # Example
74
    ///
75
    /// ```
76
    /// use ext_php_rs::embed::Embed;
77
    ///
78
    /// Embed::run(|| {
79
    ///     let result = Embed::run_script("src/embed/test-script.php");
80
    ///
81
    ///     assert!(result.is_ok());
82
    /// });
83
    /// ```
84
    pub fn run_script<P: AsRef<Path>>(path: P) -> Result<(), EmbedError> {
4✔
85
        let path = match path.as_ref().to_str() {
8✔
86
            Some(path) => match CString::new(path) {
8✔
87
                Ok(path) => path,
×
88
                Err(err) => return Err(EmbedError::InvalidEvalString(err)),
×
89
            },
90
            None => return Err(EmbedError::InvalidPath),
×
91
        };
92

93
        let mut file_handle = zend_file_handle {
94
            #[allow(clippy::used_underscore_items)]
95
            handle: _zend_file_handle__bindgen_ty_1 { fp: null_mut() },
×
96
            filename: null_mut(),
×
97
            opened_path: null_mut(),
×
98
            type_: 0,
99
            primary_script: false,
100
            in_list: false,
101
            buf: null_mut(),
×
102
            len: 0,
103
        };
104

105
        unsafe {
106
            zend_stream_init_filename(&raw mut file_handle, path.as_ptr());
×
107
        }
108

109
        let exec_result = try_catch(|| unsafe { php_execute_script(&raw mut file_handle) });
12✔
110

111
        match exec_result {
×
112
            Err(_) => Err(EmbedError::CatchError),
×
113
            Ok(true) => Ok(()),
3✔
114
            Ok(false) => Err(EmbedError::ExecuteScriptError),
1✔
115
        }
116
    }
117

118
    /// Start and run embed sapi engine
119
    ///
120
    /// This function will allow to run php code from rust, the same PHP context
121
    /// is keep between calls inside the function passed to this method.
122
    /// Which means subsequent calls to `Embed::eval` or `Embed::run_script`
123
    /// will be able to access variables defined in previous calls
124
    ///
125
    /// # Returns
126
    ///
127
    /// * R - The result of the function passed to this method
128
    ///
129
    /// R must implement [`Default`] so it can be returned in case of a bailout
130
    ///
131
    /// # Example
132
    ///
133
    /// ```
134
    /// use ext_php_rs::embed::Embed;
135
    ///
136
    /// Embed::run(|| {
137
    ///    let _ = Embed::eval("$foo = 'foo';");
138
    ///    let foo = Embed::eval("$foo;");
139
    ///    assert!(foo.is_ok());
140
    ///    assert_eq!(foo.unwrap().string().unwrap(), "foo");
141
    /// });
142
    /// ```
143
    pub fn run<R, F: FnMut() -> R + RefUnwindSafe>(func: F) -> R
55✔
144
    where
145
        R: Default,
146
    {
147
        // @TODO handle php thread safe
148
        //
149
        // This is to prevent multiple threads from running php at the same time
150
        // At some point we should detect if php is compiled with thread safety and
151
        // avoid doing that in this case
152
        let _guard = RUN_FN_LOCK.write();
165✔
153

154
        let panic = unsafe {
155
            ext_php_rs_embed_callback(
156
                0,
157
                null_mut(),
55✔
158
                panic_wrapper::<R, F>,
55✔
159
                (&raw const func).cast::<c_void>(),
110✔
160
            )
161
        };
162

163
        // This can happen if there is a bailout
164
        if panic.is_null() {
110✔
165
            return R::default();
6✔
166
        }
167

UNCOV
168
        match unsafe { *Box::from_raw(panic.cast::<std::thread::Result<R>>()) } {
×
169
            Ok(r) => r,
94✔
170
            Err(err) => {
2✔
171
                // we resume the panic here so it can be caught correctly by the test framework
UNCOV
172
                resume_unwind(err);
×
173
            }
174
        }
175
    }
176

177
    /// Evaluate a php code
178
    ///
179
    /// This function will only work correctly when used inside the `Embed::run`
180
    /// function
181
    ///
182
    /// # Returns
183
    ///
184
    /// * `Ok(Zval)` - The result of the evaluation
185
    ///
186
    /// # Errors
187
    ///
188
    /// * `Err(EmbedError)` - An error occurred during the evaluation
189
    ///
190
    /// # Example
191
    ///
192
    /// ```
193
    /// use ext_php_rs::embed::Embed;
194
    ///
195
    /// Embed::run(|| {
196
    ///    let foo = Embed::eval("$foo = 'foo';");
197
    ///    assert!(foo.is_ok());
198
    /// });
199
    /// ```
200
    pub fn eval(code: &str) -> Result<Zval, EmbedError> {
10✔
201
        let cstr = match CString::new(code) {
20✔
202
            Ok(cstr) => cstr,
203
            Err(err) => return Err(EmbedError::InvalidEvalString(err)),
×
204
        };
205

206
        let mut result = Zval::new();
207

208
        let exec_result = try_catch(|| unsafe {
209
            zend_eval_string(
10✔
210
                cstr.as_ptr().cast::<c_char>(),
20✔
211
                &raw mut result,
10✔
212
                c"run".as_ptr().cast(),
30✔
213
            )
214
        });
215

216
        match exec_result {
217
            Err(_) => Err(EmbedError::CatchError),
1✔
218
            Ok(ZEND_RESULT_CODE_SUCCESS) => Ok(result),
8✔
219
            Ok(_) => Err(EmbedError::ExecuteError(ExecutorGlobals::take_exception())),
1✔
220
        }
221
    }
222
}
223

224
#[cfg(test)]
225
mod tests {
226
    #![allow(clippy::unwrap_used)]
227
    use super::Embed;
228

229
    #[test]
230
    fn test_run() {
231
        Embed::run(|| {
232
            let result = Embed::eval("$foo = 'foo';");
233

234
            assert!(result.is_ok());
235
        });
236
    }
237

238
    #[test]
239
    fn test_run_error() {
240
        Embed::run(|| {
241
            let result = Embed::eval("stupid code;");
242

243
            assert!(result.is_err());
244
        });
245
    }
246

247
    #[test]
248
    fn test_run_script() {
249
        Embed::run(|| {
250
            let result = Embed::run_script("src/embed/test-script.php");
251

252
            assert!(result.is_ok());
253

254
            let zval = Embed::eval("$foo;").unwrap();
255

256
            assert!(zval.is_object());
257

258
            let obj = zval.object().unwrap();
259

260
            assert_eq!(obj.get_class_name().unwrap(), "Test");
261
        });
262
    }
263

264
    #[test]
265
    fn test_run_script_error() {
266
        Embed::run(|| {
267
            let result = Embed::run_script("src/embed/test-script-exception.php");
268

269
            assert!(result.is_err());
270
        });
271
    }
272

273
    #[test]
274
    #[should_panic(expected = "test panic")]
275
    fn test_panic() {
276
        Embed::run::<(), _>(|| {
277
            panic!("test panic");
278
        });
279
    }
280

281
    #[test]
282
    fn test_return() {
283
        let foo = Embed::run(|| "foo");
284

285
        assert_eq!(foo, "foo");
286
    }
287

288
    #[test]
289
    fn test_eval_bailout() {
290
        Embed::run(|| {
291
            let result = Embed::eval("trigger_error(\"Fatal error\", E_USER_ERROR);");
292

293
            assert!(result.is_err());
294
            assert!(result.unwrap_err().is_bailout());
295
        });
296
    }
297
}
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

© 2025 Coveralls, Inc