1#![allow(unsafe_code)]
6
7use std::ffi::CString;
8use std::ptr;
9
10use anyhow::{Context, Result, bail};
11use tracing::{debug, warn};
12
13use crate::ffi;
14
15pub struct PhpInstance {
23 in_request: bool,
25 custom_sapi: bool,
27 owns_module: bool,
29 tsrm_ctx: *mut std::ffi::c_void,
31}
32
33unsafe impl Send for PhpInstance {}
38unsafe impl Sync for PhpInstance {}
39
40impl PhpInstance {
41 pub fn boot() -> Result<Self> {
45 debug!("booting PHP embed SAPI");
46
47 let result = unsafe { ffi::php_embed_init(0, ptr::null_mut()) };
48
49 if result != 0 {
50 bail!("php_embed_init() failed with code {result}");
51 }
52
53 unsafe { ffi::folk_install_output_handler() };
55
56 debug!("PHP embed SAPI initialized");
57
58 Ok(Self {
59 in_request: false,
60 custom_sapi: false,
61 owns_module: true,
62 tsrm_ctx: ptr::null_mut(),
63 })
64 }
65
66 pub fn boot_custom_sapi() -> Result<Self> {
72 debug!("booting PHP with Folk custom SAPI");
73
74 unsafe { ffi::folk_signals_save() };
76
77 let result = unsafe { ffi::folk_sapi_init() };
78
79 unsafe { ffi::folk_signals_restore() };
81
82 unsafe { ffi::folk_sigsegv_handler_install() };
84
85 if result != 0 {
86 bail!("folk_sapi_init() failed with code {result}");
87 }
88
89 debug!("Folk custom SAPI initialized");
90
91 Ok(Self {
92 in_request: false,
93 custom_sapi: true,
94 owns_module: true,
95 tsrm_ctx: ptr::null_mut(),
96 })
97 }
98
99 pub fn attach() -> Self {
105 debug!("attaching to existing PHP module");
106
107 let ctx = unsafe { ffi::folk_thread_init() };
108 if !ctx.is_null() {
109 unsafe { ffi::folk_thread_set_ctx(ctx) };
110 debug!("TSRM context allocated for worker thread");
111 }
112
113 Self {
114 in_request: false,
115 custom_sapi: true,
116 owns_module: false,
117 tsrm_ctx: ctx,
118 }
119 }
120
121 pub fn set_request_context(&self, ctx: &mut RequestContext) {
125 ctx.build_ffi();
126 unsafe { ffi::folk_request_context_set(&mut ctx.ffi) };
127 }
128
129 pub fn clear_request_context(&self) {
131 unsafe { ffi::folk_request_context_clear() };
132 }
133
134 pub fn request_startup(&mut self) -> Result<()> {
136 if self.in_request {
137 warn!("request_startup called while already in request — shutting down first");
138 self.request_shutdown();
139 }
140
141 unsafe { ffi::folk_clear_output() };
142
143 if self.custom_sapi {
144 unsafe { ffi::folk_response_clear() };
145 }
146
147 let result = unsafe { ffi::folk_request_startup_safe() };
148 match result {
149 0 => {
150 self.in_request = true;
151 Ok(())
152 },
153 -1 => bail!("php_request_startup: fatal error (bailout)"),
154 -2 => bail!("php_request_startup: startup failed"),
155 code => bail!("php_request_startup: unknown error {code}"),
156 }
157 }
158
159 pub fn request_shutdown(&mut self) {
161 if !self.in_request {
162 return;
163 }
164
165 let result = unsafe { ffi::folk_request_shutdown_safe() };
166 if result != 0 {
167 warn!("php_request_shutdown returned {result}");
168 }
169
170 if self.custom_sapi {
171 self.clear_request_context();
172 }
173
174 self.in_request = false;
175 }
176
177 pub fn execute_script(&mut self, filename: &str) -> Result<String> {
179 let c_filename = CString::new(filename).context("filename contains null byte")?;
180
181 let result = unsafe { ffi::folk_execute_script_safe(c_filename.as_ptr()) };
182 let output = self.take_output();
183
184 match result {
185 0 => Ok(output),
186 -1 => {
187 if output.is_empty() {
188 bail!("PHP script fatal error (bailout) in: {filename}")
189 } else {
190 bail!("PHP script fatal error: {output}")
191 }
192 },
193 other => bail!("PHP script error {other} in: {filename}"),
194 }
195 }
196
197 pub fn eval(&mut self, code: &str) -> Result<EvalResult> {
199 let c_code = CString::new(code).context("PHP code contains null byte")?;
200 let mut retval = ffi::zval::new_undef();
201
202 let result = unsafe { ffi::folk_eval_string_safe(c_code.as_ptr(), &mut retval) };
203
204 let output = self.take_output();
205
206 let return_value = if result == 0 {
207 ZvalValue::from_raw(&mut retval)
208 } else {
209 ZvalValue::Null
210 };
211
212 unsafe { ffi::folk_zval_dtor(&mut retval) };
213
214 match result {
215 0 => Ok(EvalResult {
216 output,
217 return_value,
218 }),
219 -1 => {
220 if output.is_empty() {
221 bail!("PHP eval fatal error (bailout) in: {code}")
222 } else {
223 bail!("PHP eval fatal error (bailout): {output}")
224 }
225 },
226 other => bail!("PHP eval error {other} in: {code}"),
227 }
228 }
229
230 pub fn call(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
232 let c_func = CString::new(func_name).context("function name contains null byte")?;
233
234 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
235
236 let mut params: Vec<ffi::zval> = c_args
237 .iter()
238 .map(|s| {
239 let mut z = ffi::zval::new_undef();
240 unsafe {
241 ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
242 }
243 z
244 })
245 .collect();
246
247 let mut retval = ffi::zval::new_undef();
248
249 let result = unsafe {
250 ffi::folk_call_function_safe(
251 c_func.as_ptr(),
252 &mut retval,
253 u32::try_from(params.len()).expect("too many params"),
254 if params.is_empty() {
255 ptr::null_mut()
256 } else {
257 params.as_mut_ptr()
258 },
259 )
260 };
261
262 let return_value = ZvalValue::from_raw(&mut retval);
263
264 unsafe { ffi::folk_zval_dtor(&mut retval) };
265 for p in &mut params {
266 unsafe { ffi::folk_zval_dtor(p) };
267 }
268
269 match result {
270 0 => Ok(return_value),
271 -1 => bail!("PHP call_user_function fatal error (bailout) in: {func_name}"),
272 -2 => bail!("PHP call_user_function failed: {func_name}"),
273 code => bail!("PHP call_user_function error {code}: {func_name}"),
274 }
275 }
276
277 pub fn eval_protected(&mut self, code: &str) -> Result<EvalResult> {
282 let c_code = CString::new(code).context("PHP code contains null byte")?;
283 let mut retval = ffi::zval::new_undef();
284
285 let result = unsafe { ffi::folk_eval_string_protected(c_code.as_ptr(), &mut retval) };
286
287 let output = self.take_output();
288
289 let return_value = if result == 0 {
290 ZvalValue::from_raw(&mut retval)
291 } else {
292 ZvalValue::Null
293 };
294
295 unsafe { ffi::folk_zval_dtor(&mut retval) };
296
297 match result {
298 0 => Ok(EvalResult {
299 output,
300 return_value,
301 }),
302 -1 => bail!("PHP eval fatal error (bailout) in: {code}"),
303 -3 => bail!("PHP eval SIGSEGV caught in: {code}"),
304 code => bail!("PHP eval error {code} in: {code}"),
305 }
306 }
307
308 pub fn call_protected(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
312 let c_func = CString::new(func_name).context("function name contains null byte")?;
313
314 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
315
316 let mut params: Vec<ffi::zval> = c_args
317 .iter()
318 .map(|s| {
319 let mut z = ffi::zval::new_undef();
320 unsafe {
321 ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
322 }
323 z
324 })
325 .collect();
326
327 let mut retval = ffi::zval::new_undef();
328
329 let result = unsafe {
330 ffi::folk_call_function_protected(
331 c_func.as_ptr(),
332 &mut retval,
333 u32::try_from(params.len()).expect("too many params"),
334 if params.is_empty() {
335 ptr::null_mut()
336 } else {
337 params.as_mut_ptr()
338 },
339 )
340 };
341
342 let return_value = ZvalValue::from_raw(&mut retval);
343
344 unsafe { ffi::folk_zval_dtor(&mut retval) };
345 for p in &mut params {
346 unsafe { ffi::folk_zval_dtor(p) };
347 }
348
349 match result {
350 0 => Ok(return_value),
351 -1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
352 -2 => bail!("PHP call failed: {func_name}"),
353 -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
354 code => bail!("PHP call error {code}: {func_name}"),
355 }
356 }
357
358 pub fn call_binary(&mut self, func_name: &str, method: &str, params: &[u8]) -> Result<Vec<u8>> {
364 let c_func = CString::new(func_name).context("func_name contains null byte")?;
365
366 let mut response_buf: *mut std::ffi::c_char = ptr::null_mut();
367 let mut response_len: usize = 0;
368
369 let result = unsafe {
370 ffi::folk_call_with_binary(
371 c_func.as_ptr(),
372 method.as_ptr().cast(),
373 method.len(),
374 params.as_ptr().cast(),
375 params.len(),
376 &mut response_buf,
377 &mut response_len,
378 )
379 };
380
381 let response = if !response_buf.is_null() && response_len > 0 {
382 let bytes =
383 unsafe { std::slice::from_raw_parts(response_buf.cast::<u8>(), response_len) };
384 let owned = bytes.to_vec();
385 unsafe { ffi::folk_free_buffer(response_buf) };
386 owned
387 } else {
388 if !response_buf.is_null() {
389 unsafe { ffi::folk_free_buffer(response_buf) };
390 }
391 Vec::new()
392 };
393
394 match result {
395 0 => Ok(response),
396 -1 => {
397 let output = self.take_output();
398 if output.is_empty() {
399 bail!("PHP call fatal error (bailout) in: {func_name}")
400 } else {
401 bail!("PHP call fatal error (bailout): {output}")
402 }
403 },
404 -2 => bail!("PHP call failed: {func_name}"),
405 -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
406 code => bail!("PHP call error {code}: {func_name}"),
407 }
408 }
409
410 pub fn take_output(&self) -> String {
412 let mut len: usize = 0;
413 let ptr = unsafe { ffi::folk_get_output(&mut len) };
414 let output = if ptr.is_null() || len == 0 {
415 String::new()
416 } else {
417 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
418 String::from_utf8_lossy(bytes).into_owned()
419 };
420 unsafe { ffi::folk_clear_output() };
421 output
422 }
423
424 pub fn take_response(&self) -> ResponseData {
426 let status = unsafe { ffi::folk_response_status_code() };
427 let status = u16::try_from(status).unwrap_or(500);
428 let header_count = unsafe { ffi::folk_response_header_count() };
429
430 let mut headers = Vec::with_capacity(header_count);
431 for i in 0..header_count {
432 let mut len: usize = 0;
433 let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
434 if !ptr.is_null() && len > 0 {
435 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
436 headers.push(String::from_utf8_lossy(bytes).into_owned());
437 }
438 }
439
440 let body = self.take_output();
441
442 ResponseData {
443 status_code: status,
444 headers,
445 body,
446 }
447 }
448}
449
450impl Drop for PhpInstance {
451 fn drop(&mut self) {
452 if self.in_request {
453 self.request_shutdown();
454 }
455
456 unsafe {
457 ffi::folk_free_output();
458
459 if self.custom_sapi {
460 ffi::folk_response_free();
461 }
462 }
463
464 if !self.tsrm_ctx.is_null() {
466 unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
467 self.tsrm_ctx = ptr::null_mut();
468 }
469
470 if self.owns_module {
472 debug!("shutting down PHP SAPI");
473 unsafe {
474 if self.custom_sapi {
475 ffi::folk_sapi_shutdown();
476 } else {
477 ffi::php_embed_shutdown();
478 }
479 }
480 }
481 }
482}
483
484pub struct RequestContext {
490 method: CString,
492 uri: CString,
493 query_string: Option<CString>,
494 content_type: Option<CString>,
495 content_length: usize,
496 path_translated: Option<CString>,
497 post_data: Vec<u8>,
498 cookie: Option<CString>,
499 server_name: Option<CString>,
500 server_port: i32,
501 protocol: Option<CString>,
502
503 header_names_c: Vec<CString>,
505 header_values_c: Vec<CString>,
506 header_name_ptrs: Vec<*const std::ffi::c_char>,
507 header_value_ptrs: Vec<*const std::ffi::c_char>,
508
509 ffi: ffi::folk_request_context,
511}
512
513impl RequestContext {
514 pub fn new(method: &str, uri: &str) -> Self {
515 Self {
516 method: CString::new(method).expect("method contains null"),
517 uri: CString::new(uri).expect("uri contains null"),
518 query_string: None,
519 content_type: None,
520 content_length: 0,
521 path_translated: None,
522 post_data: Vec::new(),
523 cookie: None,
524 server_name: None,
525 server_port: 0,
526 protocol: None,
527 header_names_c: Vec::new(),
528 header_values_c: Vec::new(),
529 header_name_ptrs: Vec::new(),
530 header_value_ptrs: Vec::new(),
531 ffi: unsafe { std::mem::zeroed() },
532 }
533 }
534
535 #[must_use]
536 pub fn query_string(mut self, qs: &str) -> Self {
537 self.query_string = Some(CString::new(qs).expect("query_string contains null"));
538 self
539 }
540
541 #[must_use]
542 pub fn content_type(mut self, ct: &str) -> Self {
543 self.content_type = Some(CString::new(ct).expect("content_type contains null"));
544 self
545 }
546
547 #[must_use]
548 pub fn body(mut self, data: &[u8]) -> Self {
549 self.post_data = data.to_vec();
550 self.content_length = data.len();
551 self
552 }
553
554 #[must_use]
555 pub fn cookie(mut self, cookie: &str) -> Self {
556 self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
557 self
558 }
559
560 #[must_use]
561 pub fn path_translated(mut self, path: &str) -> Self {
562 self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
563 self
564 }
565
566 #[must_use]
567 pub fn server(mut self, name: &str, port: i32) -> Self {
568 self.server_name = Some(CString::new(name).expect("server_name contains null"));
569 self.server_port = port;
570 self
571 }
572
573 #[must_use]
574 pub fn protocol(mut self, proto: &str) -> Self {
575 self.protocol = Some(CString::new(proto).expect("protocol contains null"));
576 self
577 }
578
579 #[must_use]
580 pub fn header(mut self, name: &str, value: &str) -> Self {
581 self.header_names_c
582 .push(CString::new(name).expect("header name contains null"));
583 self.header_values_c
584 .push(CString::new(value).expect("header value contains null"));
585 self
586 }
587
588 fn build_ffi(&mut self) {
591 self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
593 self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
594
595 self.ffi = ffi::folk_request_context {
596 request_method: self.method.as_ptr(),
597 request_uri: self.uri.as_ptr(),
598 query_string: self
599 .query_string
600 .as_ref()
601 .map_or(ptr::null(), |s| s.as_ptr()),
602 content_type: self
603 .content_type
604 .as_ref()
605 .map_or(ptr::null(), |s| s.as_ptr()),
606 content_length: self.content_length,
607 path_translated: self
608 .path_translated
609 .as_ref()
610 .map_or(ptr::null(), |s| s.as_ptr()),
611 post_data: if self.post_data.is_empty() {
612 ptr::null()
613 } else {
614 self.post_data.as_ptr().cast()
615 },
616 post_data_len: self.post_data.len(),
617 post_data_read: 0,
618 cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
619 header_names: if self.header_name_ptrs.is_empty() {
620 ptr::null()
621 } else {
622 self.header_name_ptrs.as_ptr()
623 },
624 header_values: if self.header_value_ptrs.is_empty() {
625 ptr::null()
626 } else {
627 self.header_value_ptrs.as_ptr()
628 },
629 header_count: self.header_names_c.len(),
630 server_name: self
631 .server_name
632 .as_ref()
633 .map_or(ptr::null(), |s| s.as_ptr()),
634 server_port: self.server_port,
635 protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
636 };
637 }
638}
639
640#[derive(Debug, Clone)]
642pub struct ResponseData {
643 pub status_code: u16,
645 pub headers: Vec<String>,
647 pub body: String,
649}
650
651#[derive(Debug)]
653pub struct EvalResult {
654 pub output: String,
656 pub return_value: ZvalValue,
658}
659
660#[derive(Debug, Clone, PartialEq)]
662pub enum ZvalValue {
663 Null,
664 Bool(bool),
665 Long(i64),
666 Double(f64),
667 String(String),
668 Other(i32),
670}
671
672impl ZvalValue {
673 fn from_raw(z: &mut ffi::zval) -> Self {
674 let ztype = unsafe { ffi::folk_zval_type(z) };
675 match ztype {
676 ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
677 ffi::IS_FALSE => Self::Bool(false),
678 ffi::IS_TRUE => Self::Bool(true),
679 ffi::IS_LONG => {
680 let v = unsafe { ffi::folk_zval_get_long(z) };
681 Self::Long(v)
682 },
683 ffi::IS_STRING => {
684 let mut len: usize = 0;
685 let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
686 if ptr.is_null() {
687 Self::Null
688 } else {
689 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
690 Self::String(String::from_utf8_lossy(bytes).into_owned())
691 }
692 },
693 other => Self::Other(other),
694 }
695 }
696
697 pub fn as_str(&self) -> Option<&str> {
698 match self {
699 Self::String(s) => Some(s),
700 _ => None,
701 }
702 }
703
704 pub fn as_long(&self) -> Option<i64> {
705 match self {
706 Self::Long(v) => Some(*v),
707 _ => None,
708 }
709 }
710}