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 => bail!("PHP call fatal error (bailout) in: {func_name}"),
397 -2 => bail!("PHP call failed: {func_name}"),
398 -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
399 code => bail!("PHP call error {code}: {func_name}"),
400 }
401 }
402
403 pub fn take_output(&self) -> String {
405 let mut len: usize = 0;
406 let ptr = unsafe { ffi::folk_get_output(&mut len) };
407 let output = if ptr.is_null() || len == 0 {
408 String::new()
409 } else {
410 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
411 String::from_utf8_lossy(bytes).into_owned()
412 };
413 unsafe { ffi::folk_clear_output() };
414 output
415 }
416
417 pub fn take_response(&self) -> ResponseData {
419 let status = unsafe { ffi::folk_response_status_code() };
420 let status = u16::try_from(status).unwrap_or(500);
421 let header_count = unsafe { ffi::folk_response_header_count() };
422
423 let mut headers = Vec::with_capacity(header_count);
424 for i in 0..header_count {
425 let mut len: usize = 0;
426 let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
427 if !ptr.is_null() && len > 0 {
428 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
429 headers.push(String::from_utf8_lossy(bytes).into_owned());
430 }
431 }
432
433 let body = self.take_output();
434
435 ResponseData {
436 status_code: status,
437 headers,
438 body,
439 }
440 }
441}
442
443impl Drop for PhpInstance {
444 fn drop(&mut self) {
445 if self.in_request {
446 self.request_shutdown();
447 }
448
449 unsafe {
450 ffi::folk_free_output();
451
452 if self.custom_sapi {
453 ffi::folk_response_free();
454 }
455 }
456
457 if !self.tsrm_ctx.is_null() {
459 unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
460 self.tsrm_ctx = ptr::null_mut();
461 }
462
463 if self.owns_module {
465 debug!("shutting down PHP SAPI");
466 unsafe {
467 if self.custom_sapi {
468 ffi::folk_sapi_shutdown();
469 } else {
470 ffi::php_embed_shutdown();
471 }
472 }
473 }
474 }
475}
476
477pub struct RequestContext {
483 method: CString,
485 uri: CString,
486 query_string: Option<CString>,
487 content_type: Option<CString>,
488 content_length: usize,
489 path_translated: Option<CString>,
490 post_data: Vec<u8>,
491 cookie: Option<CString>,
492 server_name: Option<CString>,
493 server_port: i32,
494 protocol: Option<CString>,
495
496 header_names_c: Vec<CString>,
498 header_values_c: Vec<CString>,
499 header_name_ptrs: Vec<*const std::ffi::c_char>,
500 header_value_ptrs: Vec<*const std::ffi::c_char>,
501
502 ffi: ffi::folk_request_context,
504}
505
506impl RequestContext {
507 pub fn new(method: &str, uri: &str) -> Self {
508 Self {
509 method: CString::new(method).expect("method contains null"),
510 uri: CString::new(uri).expect("uri contains null"),
511 query_string: None,
512 content_type: None,
513 content_length: 0,
514 path_translated: None,
515 post_data: Vec::new(),
516 cookie: None,
517 server_name: None,
518 server_port: 0,
519 protocol: None,
520 header_names_c: Vec::new(),
521 header_values_c: Vec::new(),
522 header_name_ptrs: Vec::new(),
523 header_value_ptrs: Vec::new(),
524 ffi: unsafe { std::mem::zeroed() },
525 }
526 }
527
528 #[must_use]
529 pub fn query_string(mut self, qs: &str) -> Self {
530 self.query_string = Some(CString::new(qs).expect("query_string contains null"));
531 self
532 }
533
534 #[must_use]
535 pub fn content_type(mut self, ct: &str) -> Self {
536 self.content_type = Some(CString::new(ct).expect("content_type contains null"));
537 self
538 }
539
540 #[must_use]
541 pub fn body(mut self, data: &[u8]) -> Self {
542 self.post_data = data.to_vec();
543 self.content_length = data.len();
544 self
545 }
546
547 #[must_use]
548 pub fn cookie(mut self, cookie: &str) -> Self {
549 self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
550 self
551 }
552
553 #[must_use]
554 pub fn path_translated(mut self, path: &str) -> Self {
555 self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
556 self
557 }
558
559 #[must_use]
560 pub fn server(mut self, name: &str, port: i32) -> Self {
561 self.server_name = Some(CString::new(name).expect("server_name contains null"));
562 self.server_port = port;
563 self
564 }
565
566 #[must_use]
567 pub fn protocol(mut self, proto: &str) -> Self {
568 self.protocol = Some(CString::new(proto).expect("protocol contains null"));
569 self
570 }
571
572 #[must_use]
573 pub fn header(mut self, name: &str, value: &str) -> Self {
574 self.header_names_c
575 .push(CString::new(name).expect("header name contains null"));
576 self.header_values_c
577 .push(CString::new(value).expect("header value contains null"));
578 self
579 }
580
581 fn build_ffi(&mut self) {
584 self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
586 self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
587
588 self.ffi = ffi::folk_request_context {
589 request_method: self.method.as_ptr(),
590 request_uri: self.uri.as_ptr(),
591 query_string: self
592 .query_string
593 .as_ref()
594 .map_or(ptr::null(), |s| s.as_ptr()),
595 content_type: self
596 .content_type
597 .as_ref()
598 .map_or(ptr::null(), |s| s.as_ptr()),
599 content_length: self.content_length,
600 path_translated: self
601 .path_translated
602 .as_ref()
603 .map_or(ptr::null(), |s| s.as_ptr()),
604 post_data: if self.post_data.is_empty() {
605 ptr::null()
606 } else {
607 self.post_data.as_ptr().cast()
608 },
609 post_data_len: self.post_data.len(),
610 post_data_read: 0,
611 cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
612 header_names: if self.header_name_ptrs.is_empty() {
613 ptr::null()
614 } else {
615 self.header_name_ptrs.as_ptr()
616 },
617 header_values: if self.header_value_ptrs.is_empty() {
618 ptr::null()
619 } else {
620 self.header_value_ptrs.as_ptr()
621 },
622 header_count: self.header_names_c.len(),
623 server_name: self
624 .server_name
625 .as_ref()
626 .map_or(ptr::null(), |s| s.as_ptr()),
627 server_port: self.server_port,
628 protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
629 };
630 }
631}
632
633#[derive(Debug, Clone)]
635pub struct ResponseData {
636 pub status_code: u16,
638 pub headers: Vec<String>,
640 pub body: String,
642}
643
644#[derive(Debug)]
646pub struct EvalResult {
647 pub output: String,
649 pub return_value: ZvalValue,
651}
652
653#[derive(Debug, Clone, PartialEq)]
655pub enum ZvalValue {
656 Null,
657 Bool(bool),
658 Long(i64),
659 Double(f64),
660 String(String),
661 Other(i32),
663}
664
665impl ZvalValue {
666 fn from_raw(z: &mut ffi::zval) -> Self {
667 let ztype = unsafe { ffi::folk_zval_type(z) };
668 match ztype {
669 ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
670 ffi::IS_FALSE => Self::Bool(false),
671 ffi::IS_TRUE => Self::Bool(true),
672 ffi::IS_LONG => {
673 let v = unsafe { ffi::folk_zval_get_long(z) };
674 Self::Long(v)
675 },
676 ffi::IS_STRING => {
677 let mut len: usize = 0;
678 let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
679 if ptr.is_null() {
680 Self::Null
681 } else {
682 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
683 Self::String(String::from_utf8_lossy(bytes).into_owned())
684 }
685 },
686 other => Self::Other(other),
687 }
688 }
689
690 pub fn as_str(&self) -> Option<&str> {
691 match self {
692 Self::String(s) => Some(s),
693 _ => None,
694 }
695 }
696
697 pub fn as_long(&self) -> Option<i64> {
698 match self {
699 Self::Long(v) => Some(*v),
700 _ => None,
701 }
702 }
703}