Skip to main content

ext_php_rs/zend/
output.rs

1//! Output handlers that filter what PHP writes, the way `ob_start()` does.
2
3use std::{
4    alloc::Layout,
5    borrow::Cow,
6    ffi::{c_int, c_void},
7    mem,
8    panic::AssertUnwindSafe,
9    ptr, slice,
10};
11
12use bitflags::bitflags;
13
14use crate::{
15    alloc::{efree, emalloc},
16    error::{Error, Result},
17    ffi::{
18        PHP_OUTPUT_HANDLER_CLEAN, PHP_OUTPUT_HANDLER_CLEANABLE, PHP_OUTPUT_HANDLER_DEFAULT_SIZE,
19        PHP_OUTPUT_HANDLER_FINAL, PHP_OUTPUT_HANDLER_FLUSH, PHP_OUTPUT_HANDLER_FLUSHABLE,
20        PHP_OUTPUT_HANDLER_REMOVABLE, PHP_OUTPUT_HANDLER_START, ZEND_RESULT_CODE_FAILURE,
21        ZEND_RESULT_CODE_SUCCESS, ext_php_rs_output_activated, php_output_buffer,
22        php_output_context, php_output_handler, php_output_handler_create_internal,
23        php_output_handler_free, php_output_handler_set_context, php_output_handler_start,
24    },
25    zend::{CatchError, bailout, try_catch},
26};
27
28bitflags! {
29    /// What userland may do with a handler started by [`start_output_handler`].
30    #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
31    pub struct OutputHandlerFlags: u32 {
32        /// `ob_clean()` may discard the buffered output.
33        const Cleanable = PHP_OUTPUT_HANDLER_CLEANABLE;
34        /// `ob_flush()` may send the buffered output.
35        const Flushable = PHP_OUTPUT_HANDLER_FLUSHABLE;
36        /// `ob_end_flush()`, `ob_end_clean()` and `ob_get_clean()` may remove
37        /// the handler.
38        const Removable = PHP_OUTPUT_HANDLER_REMOVABLE;
39        /// The flags `ob_start()` uses by default.
40        const Std = Self::Cleanable.bits() | Self::Flushable.bits() | Self::Removable.bits();
41    }
42}
43
44bitflags! {
45    /// Why PHP runs the output handler. An empty set is a plain write.
46    #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
47    pub struct OutputOp: u32 {
48        /// First run of the handler.
49        const Start = PHP_OUTPUT_HANDLER_START;
50        /// The buffer is cleaned. PHP discards what the handler returns.
51        const Clean = PHP_OUTPUT_HANDLER_CLEAN;
52        /// The buffer is flushed.
53        const Flush = PHP_OUTPUT_HANDLER_FLUSH;
54        /// Last run of the handler. PHP removes it after this run.
55        const Final = PHP_OUTPUT_HANDLER_FINAL;
56    }
57}
58
59enum Slot<F> {
60    Idle(F),
61    Running,
62    Freed,
63    Poisoned,
64}
65
66/// Pushes `handler` on top of PHP's output buffer stack, like `ob_start()`
67/// with a callback.
68///
69/// PHP runs `handler` with the buffered bytes and an [`OutputOp`] when the
70/// buffer reaches `chunk_size` bytes (`0` means no limit), and when it is
71/// flushed, cleaned or removed. PHP sends on the bytes that `handler` returns.
72/// Return `Cow::Borrowed(input)` to pass the input through unchanged, or an
73/// empty slice to drop it. At the end of the request, PHP runs `handler` one
74/// last time with [`OutputOp::Final`] and then drops it.
75///
76/// The input is not copied. A borrowed start of the input (`input` or
77/// `&input[..n]`) is sent without a copy. Any other output is copied once to
78/// the request heap.
79///
80/// PHP discards the output that `handler` writes while it runs. If `handler`
81/// panics, PHP sends the buffered bytes unchanged and disables the handler. If
82/// `handler` causes a fatal error, the closure leaks.
83///
84/// # Errors
85///
86/// [`Error::OutputHandlerStartFailed`] if no request is active (for example
87/// during `MINIT`), or if a conflict check registered for `name` rejects the
88/// handler.
89///
90/// # Example
91///
92/// ```ignore
93/// use std::borrow::Cow;
94/// use ext_php_rs::zend::{OutputHandlerFlags, start_output_handler};
95///
96/// start_output_handler("upper", 0, OutputHandlerFlags::Std, |input, _| {
97///     Cow::Owned(input.to_ascii_uppercase())
98/// })?;
99/// ```
100pub fn start_output_handler<F>(
101    name: &str,
102    chunk_size: usize,
103    flags: OutputHandlerFlags,
104    handler: F,
105) -> Result<()>
106where
107    F: for<'a> FnMut(&'a [u8], OutputOp) -> Cow<'a, [u8]> + 'static,
108{
109    // SAFETY: the output globals exist from module startup until the process
110    // ends.
111    if !unsafe { ext_php_rs_output_activated() } {
112        return Err(Error::OutputHandlerStartFailed);
113    }
114
115    let state = Box::into_raw(Box::new(Slot::Idle(handler)));
116    let name_ptr = if name.is_empty() {
117        c"".as_ptr()
118    } else {
119        name.as_ptr().cast()
120    };
121
122    // SAFETY: output is active, so the handler goes on the request heap and on
123    // the live handler stack. The engine copies `name`. From `set_context`
124    // on, the handler owns `state` and frees it through `drop_state::<F>`.
125    unsafe {
126        let mut php_handler = php_output_handler_create_internal(
127            name_ptr,
128            name.len(),
129            Some(call_handler::<F>),
130            chunk_size,
131            flags.bits().cast_signed(),
132        );
133        php_output_handler_set_context(php_handler, state.cast(), Some(drop_state::<F>));
134        if php_output_handler_start(php_handler) == ZEND_RESULT_CODE_SUCCESS {
135            return Ok(());
136        }
137        php_output_handler_free(&raw mut php_handler);
138    }
139    Err(Error::OutputHandlerStartFailed)
140}
141
142enum Output {
143    Empty,
144    Input(usize),
145    Copied(*mut u8, usize),
146}
147
148unsafe extern "C" fn call_handler<F>(
149    handler_context: *mut *mut c_void,
150    output_context: *mut php_output_context,
151) -> c_int
152where
153    F: for<'a> FnMut(&'a [u8], OutputOp) -> Cow<'a, [u8]> + 'static,
154{
155    // SAFETY: the engine passes `&handler->opaq`, which `start_output_handler`
156    // set to a live `Slot<F>`.
157    let slot = unsafe { *handler_context }.cast::<Slot<F>>();
158    // SAFETY: while the slot is `Running`, `drop_state` marks it `Freed`
159    // instead of freeing it, so it outlives this call.
160    let mut func = match unsafe { mem::replace(&mut *slot, Slot::Running) } {
161        Slot::Idle(func) => func,
162        other => {
163            // SAFETY: see above.
164            unsafe { *slot = other };
165            return ZEND_RESULT_CODE_FAILURE;
166        }
167    };
168
169    // SAFETY: `handler_context` points at the `opaq` field of a live handler,
170    // and the engine fed `in_` with that handler's buffer. Detaching the
171    // buffer sends output written by `func` to a fresh one, so `input` stays
172    // valid and unchanged.
173    let (handler, buffer, op) = unsafe {
174        let handler = handler_context
175            .byte_sub(mem::offset_of!(php_output_handler, opaq))
176            .cast::<php_output_handler>();
177        let buffer = ptr::replace(&raw mut (*handler).buffer, mem::zeroed());
178        let op = OutputOp::from_bits_truncate((*output_context).op.cast_unsigned());
179        (handler, buffer, op)
180    };
181    let input = if buffer.used == 0 {
182        &[][..]
183    } else {
184        // SAFETY: the detached buffer holds `used` bytes and only this call
185        // can reach it.
186        unsafe { slice::from_raw_parts(buffer.data.cast::<u8>(), buffer.used) }
187    };
188
189    let result = try_catch(AssertUnwindSafe(|| {
190        let output = func(input, op);
191        if output.is_empty() {
192            Output::Empty
193        } else if matches!(output, Cow::Borrowed(bytes) if bytes.as_ptr() == input.as_ptr()) {
194            Output::Input(output.len())
195        } else {
196            Output::Copied(emalloc_copy(&output), output.len())
197        }
198    }));
199
200    // SAFETY: the slot is live, see above.
201    if matches!(unsafe { &*slot }, Slot::Freed) {
202        // SAFETY: the engine freed the handler on a fatal error that `func`
203        // swallowed with `try_catch`, so the slot and the buffers are only
204        // ours. The fatal error is raised again, as the engine expects.
205        unsafe {
206            drop(Box::from_raw(slot));
207            free_buffer(buffer);
208            if let Ok(Output::Copied(data, _)) = result {
209                efree(data);
210            }
211        }
212        if matches!(result, Err(CatchError::Bailout)) {
213            mem::forget(func);
214        } else {
215            drop(func);
216        }
217        // SAFETY: re-raises the fatal error to the enclosing `zend_try`.
218        unsafe { bailout() }
219    }
220
221    // SAFETY: the handler is live as the slot is not `Freed`. It gets its
222    // buffer back, and the engine drops what `func` wrote meanwhile.
223    let written = unsafe { ptr::replace(&raw mut (*handler).buffer, buffer) };
224    let output = match result {
225        Ok(output) => output,
226        Err(error) => {
227            // SAFETY: `written` came from the engine and nothing else points
228            // at it.
229            unsafe { free_buffer(written) };
230            if let CatchError::Bailout = error {
231                mem::forget(func);
232                // SAFETY: the slot is live. Re-raises the bailout to the
233                // enclosing `zend_try`.
234                unsafe {
235                    *slot = Slot::Poisoned;
236                    bailout()
237                }
238            }
239            // SAFETY: the slot is live.
240            unsafe { *slot = Slot::Idle(func) };
241            return ZEND_RESULT_CODE_FAILURE;
242        }
243    };
244    // SAFETY: the slot is live.
245    unsafe { *slot = Slot::Idle(func) };
246
247    // SAFETY: the engine owns `out` with `free` set, so no later write to the
248    // handler, even from a header callback that runs the handler again, can
249    // change or free what it points at.
250    unsafe {
251        let context = &mut *output_context;
252        match output {
253            Output::Empty => free_buffer(written),
254            Output::Copied(data, len) => {
255                free_buffer(written);
256                give_to_engine(&mut context.out, data, len, len);
257            }
258            Output::Input(len) => {
259                (*handler).buffer = if written.data.is_null() {
260                    new_buffer()
261                } else {
262                    php_output_buffer { used: 0, ..written }
263                };
264                context.in_ = mem::zeroed();
265                give_to_engine(&mut context.out, buffer.data.cast(), len, buffer.size);
266            }
267        }
268    }
269    ZEND_RESULT_CODE_SUCCESS
270}
271
272fn emalloc_copy(bytes: &[u8]) -> *mut u8 {
273    let data = emalloc(Layout::for_value(bytes));
274    // SAFETY: `data` is a fresh request allocation of `bytes.len()` bytes.
275    unsafe { ptr::copy_nonoverlapping(bytes.as_ptr(), data, bytes.len()) };
276    data
277}
278
279fn new_buffer() -> php_output_buffer {
280    let layout = Layout::new::<[u8; PHP_OUTPUT_HANDLER_DEFAULT_SIZE as usize]>();
281    // SAFETY: an all-zero `php_output_buffer` is an empty buffer.
282    let mut buffer: php_output_buffer = unsafe { mem::zeroed() };
283    buffer.data = emalloc(layout).cast();
284    buffer.size = layout.size();
285    buffer
286}
287
288/// # Safety
289///
290/// `buffer.data` must be null or a request allocation that nothing else uses.
291unsafe fn free_buffer(buffer: php_output_buffer) {
292    if !buffer.data.is_null() {
293        // SAFETY: see the function contract.
294        unsafe { efree(buffer.data.cast()) };
295    }
296}
297
298/// # Safety
299///
300/// `out` must be the empty output buffer of a live context, and `data` a
301/// request allocation of `size` bytes that nothing else uses.
302unsafe fn give_to_engine(out: &mut php_output_buffer, data: *mut u8, used: usize, size: usize) {
303    out.data = data.cast();
304    out.used = used;
305    out.size = size;
306    out.set_free(1);
307}
308
309unsafe extern "C" fn drop_state<F>(state: *mut c_void) {
310    let slot = state.cast::<Slot<F>>();
311    // SAFETY: `state` is the box leaked by `start_output_handler`. The engine
312    // calls this once, when it frees the handler. A running `call_handler`
313    // owns the box until it returns.
314    unsafe {
315        if matches!(*slot, Slot::Running) {
316            *slot = Slot::Freed;
317        } else {
318            drop(Box::from_raw(slot));
319        }
320    }
321}
322
323#[cfg(all(test, feature = "embed"))]
324mod tests {
325    use std::{cell::RefCell, panic::AssertUnwindSafe, rc::Rc};
326
327    use super::*;
328    use crate::{
329        embed::Embed,
330        zend::{SapiGlobals, output_write},
331    };
332
333    fn filter<F>(input: &'static [u8], handler: F) -> String
334    where
335        F: for<'a> FnMut(&'a [u8], OutputOp) -> Cow<'a, [u8]> + 'static,
336    {
337        Embed::run(AssertUnwindSafe(move || {
338            Embed::eval("ob_start();").expect("ob_start failed");
339            start_output_handler("test", 0, OutputHandlerFlags::Std, handler)
340                .expect("start_output_handler failed");
341            let _ = output_write(input);
342            Embed::eval("ob_end_flush();").expect("ob_end_flush failed");
343            Embed::eval("ob_get_clean();")
344                .ok()
345                .and_then(|output| output.string())
346                .unwrap_or_default()
347        }))
348    }
349
350    #[test]
351    fn borrowed_input_passes_through() {
352        assert_eq!(filter(b"hello", |input, _| Cow::Borrowed(input)), "hello");
353    }
354
355    #[test]
356    fn borrowed_prefix_is_sent() {
357        assert_eq!(
358            filter(b"hello", |input, _| Cow::Borrowed(&input[..2])),
359            "he"
360        );
361    }
362
363    #[test]
364    fn borrowed_middle_is_sent() {
365        assert_eq!(
366            filter(b"hello", |input, _| Cow::Borrowed(&input[1..4])),
367            "ell"
368        );
369    }
370
371    #[test]
372    fn owned_output_replaces_input() {
373        let output = filter(b"hello", |input, _| Cow::Owned(input.to_ascii_uppercase()));
374
375        assert_eq!(output, "HELLO");
376    }
377
378    #[test]
379    fn static_output_replaces_input() {
380        assert_eq!(filter(b"hello", |_, _| Cow::Borrowed(b"bye")), "bye");
381    }
382
383    #[test]
384    fn empty_output_drops_input() {
385        assert_eq!(filter(b"hello", |_, _| Cow::Borrowed(&[])), "");
386    }
387
388    #[test]
389    fn output_written_by_the_handler_leaves_input_intact() {
390        let output = filter(b"hello", |input, _| {
391            for _ in 0..64 {
392                let _ = output_write(&[b'x'; 1024]);
393            }
394            Cow::Borrowed(input)
395        });
396
397        assert_eq!(output, "hello");
398    }
399
400    #[test]
401    fn panicking_handler_passes_input_through() {
402        assert_eq!(filter(b"hello", |_, _| panic!("handler panic")), "hello");
403    }
404
405    #[test]
406    fn handler_sees_each_op() {
407        let ops = Embed::run(|| {
408            let ops = Rc::new(RefCell::new(Vec::new()));
409            let seen = Rc::clone(&ops);
410            Embed::eval("ob_start();").expect("ob_start failed");
411            start_output_handler("test", 0, OutputHandlerFlags::Std, move |input, op| {
412                seen.borrow_mut().push(op);
413                Cow::Borrowed(input)
414            })
415            .expect("start_output_handler failed");
416            let _ = output_write(b"a");
417            Embed::eval("ob_flush();").expect("ob_flush failed");
418            let _ = output_write(b"b");
419            Embed::eval("ob_end_flush();").expect("ob_end_flush failed");
420            Embed::eval("ob_end_clean();").expect("ob_end_clean failed");
421            ops.take()
422        });
423
424        assert_eq!(ops, [OutputOp::Start | OutputOp::Flush, OutputOp::Final]);
425    }
426
427    fn send_headers_through<F>(handler: F) -> usize
428    where
429        F: for<'a> FnMut(&'a [u8], OutputOp) -> Cow<'a, [u8]> + 'static,
430    {
431        Embed::run(AssertUnwindSafe(move || {
432            {
433                let mut sapi = SapiGlobals::get_mut();
434                sapi.headers_sent = 0;
435                sapi.request_info.no_headers = false;
436            }
437            Embed::eval("header_register_callback(fn () => print(str_repeat('b', 65536)));")
438                .expect("header_register_callback failed");
439            let calls = Rc::new(RefCell::new(0));
440            let seen = Rc::clone(&calls);
441            let mut handler = handler;
442            start_output_handler("test", 1, OutputHandlerFlags::Std, move |input, op| {
443                *seen.borrow_mut() += 1;
444                handler(input, op)
445            })
446            .expect("start_output_handler failed");
447            let _ = output_write(&[b'a'; 4096]);
448            calls.take()
449        }))
450    }
451
452    #[test]
453    fn header_callback_output_leaves_owned_output_intact() {
454        let calls = send_headers_through(|input, _| Cow::Owned(input.to_ascii_uppercase()));
455
456        assert!(calls > 1);
457    }
458
459    #[test]
460    fn header_callback_output_leaves_borrowed_input_intact() {
461        assert!(send_headers_through(|input, _| Cow::Borrowed(input)) > 1);
462    }
463
464    #[test]
465    fn fatal_error_swallowed_by_the_handler_is_raised_again() {
466        let bailed_out = Embed::run(|| {
467            start_output_handler("test", 0, OutputHandlerFlags::Std, |input, _| {
468                let _ = Embed::eval("ob_end_clean();");
469                Cow::Borrowed(input)
470            })
471            .expect("start_output_handler failed");
472            let _ = output_write(b"hello");
473            Embed::eval("ob_end_flush();").is_err_and(|err| err.is_bailout())
474        });
475
476        assert!(bailed_out);
477    }
478}