1use 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 #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
31 pub struct OutputHandlerFlags: u32 {
32 const Cleanable = PHP_OUTPUT_HANDLER_CLEANABLE;
34 const Flushable = PHP_OUTPUT_HANDLER_FLUSHABLE;
36 const Removable = PHP_OUTPUT_HANDLER_REMOVABLE;
39 const Std = Self::Cleanable.bits() | Self::Flushable.bits() | Self::Removable.bits();
41 }
42}
43
44bitflags! {
45 #[derive(PartialEq, Eq, Hash, Debug, Clone, Copy)]
47 pub struct OutputOp: u32 {
48 const Start = PHP_OUTPUT_HANDLER_START;
50 const Clean = PHP_OUTPUT_HANDLER_CLEAN;
52 const Flush = PHP_OUTPUT_HANDLER_FLUSH;
54 const Final = PHP_OUTPUT_HANDLER_FINAL;
56 }
57}
58
59enum Slot<F> {
60 Idle(F),
61 Running,
62 Freed,
63 Poisoned,
64}
65
66pub 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 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 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 let slot = unsafe { *handler_context }.cast::<Slot<F>>();
158 let mut func = match unsafe { mem::replace(&mut *slot, Slot::Running) } {
161 Slot::Idle(func) => func,
162 other => {
163 unsafe { *slot = other };
165 return ZEND_RESULT_CODE_FAILURE;
166 }
167 };
168
169 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 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 if matches!(unsafe { &*slot }, Slot::Freed) {
202 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 unsafe { bailout() }
219 }
220
221 let written = unsafe { ptr::replace(&raw mut (*handler).buffer, buffer) };
224 let output = match result {
225 Ok(output) => output,
226 Err(error) => {
227 unsafe { free_buffer(written) };
230 if let CatchError::Bailout = error {
231 mem::forget(func);
232 unsafe {
235 *slot = Slot::Poisoned;
236 bailout()
237 }
238 }
239 unsafe { *slot = Slot::Idle(func) };
241 return ZEND_RESULT_CODE_FAILURE;
242 }
243 };
244 unsafe { *slot = Slot::Idle(func) };
246
247 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 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 let mut buffer: php_output_buffer = unsafe { mem::zeroed() };
283 buffer.data = emalloc(layout).cast();
284 buffer.size = layout.size();
285 buffer
286}
287
288unsafe fn free_buffer(buffer: php_output_buffer) {
292 if !buffer.data.is_null() {
293 unsafe { efree(buffer.data.cast()) };
295 }
296}
297
298unsafe 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 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}