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}
28
29impl PhpInstance {
30 pub fn boot() -> Result<Self> {
34 debug!("booting PHP embed SAPI");
35
36 let result = unsafe { ffi::php_embed_init(0, ptr::null_mut()) };
37
38 if result != 0 {
39 bail!("php_embed_init() failed with code {result}");
40 }
41
42 unsafe { ffi::folk_install_output_handler() };
44
45 debug!("PHP embed SAPI initialized");
46
47 Ok(Self {
48 in_request: false,
49 custom_sapi: false,
50 })
51 }
52
53 pub fn boot_custom_sapi() -> Result<Self> {
58 debug!("booting PHP with Folk custom SAPI");
59
60 unsafe { ffi::folk_signals_save() };
62
63 let result = unsafe { ffi::folk_sapi_init() };
64
65 unsafe { ffi::folk_signals_restore() };
67
68 unsafe { ffi::folk_sigsegv_handler_install() };
70
71 if result != 0 {
72 bail!("folk_sapi_init() failed with code {result}");
73 }
74
75 debug!("Folk custom SAPI initialized");
76
77 Ok(Self {
78 in_request: false,
79 custom_sapi: true,
80 })
81 }
82
83 pub fn set_request_context(&self, ctx: &mut RequestContext) {
87 ctx.build_ffi();
88 unsafe { ffi::folk_request_context_set(&mut ctx.ffi) };
89 }
90
91 pub fn clear_request_context(&self) {
93 unsafe { ffi::folk_request_context_clear() };
94 }
95
96 pub fn request_startup(&mut self) -> Result<()> {
98 if self.in_request {
99 warn!("request_startup called while already in request — shutting down first");
100 self.request_shutdown();
101 }
102
103 unsafe { ffi::folk_clear_output() };
104
105 if self.custom_sapi {
106 unsafe { ffi::folk_response_clear() };
107 }
108
109 let result = unsafe { ffi::folk_request_startup_safe() };
110 match result {
111 0 => {
112 self.in_request = true;
113 Ok(())
114 },
115 -1 => bail!("php_request_startup: fatal error (bailout)"),
116 -2 => bail!("php_request_startup: startup failed"),
117 code => bail!("php_request_startup: unknown error {code}"),
118 }
119 }
120
121 pub fn request_shutdown(&mut self) {
123 if !self.in_request {
124 return;
125 }
126
127 let result = unsafe { ffi::folk_request_shutdown_safe() };
128 if result != 0 {
129 warn!("php_request_shutdown returned {result}");
130 }
131
132 if self.custom_sapi {
133 self.clear_request_context();
134 }
135
136 self.in_request = false;
137 }
138
139 pub fn eval(&mut self, code: &str) -> Result<EvalResult> {
141 let c_code = CString::new(code).context("PHP code contains null byte")?;
142 let mut retval = ffi::zval::new_undef();
143
144 let result = unsafe { ffi::folk_eval_string_safe(c_code.as_ptr(), &mut retval) };
145
146 let output = self.take_output();
147
148 let return_value = if result == 0 {
149 ZvalValue::from_raw(&mut retval)
150 } else {
151 ZvalValue::Null
152 };
153
154 unsafe { ffi::folk_zval_dtor(&mut retval) };
155
156 match result {
157 0 => Ok(EvalResult {
158 output,
159 return_value,
160 }),
161 -1 => bail!("PHP eval fatal error (bailout) in: {code}"),
162 code => bail!("PHP eval error {code} in: {code}"),
163 }
164 }
165
166 pub fn call(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
168 let c_func = CString::new(func_name).context("function name contains null byte")?;
169
170 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
171
172 let mut params: Vec<ffi::zval> = c_args
173 .iter()
174 .map(|s| {
175 let mut z = ffi::zval::new_undef();
176 unsafe {
177 ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
178 }
179 z
180 })
181 .collect();
182
183 let mut retval = ffi::zval::new_undef();
184
185 let result = unsafe {
186 ffi::folk_call_function_safe(
187 c_func.as_ptr(),
188 &mut retval,
189 u32::try_from(params.len()).expect("too many params"),
190 if params.is_empty() {
191 ptr::null_mut()
192 } else {
193 params.as_mut_ptr()
194 },
195 )
196 };
197
198 let return_value = ZvalValue::from_raw(&mut retval);
199
200 unsafe { ffi::folk_zval_dtor(&mut retval) };
201 for p in &mut params {
202 unsafe { ffi::folk_zval_dtor(p) };
203 }
204
205 match result {
206 0 => Ok(return_value),
207 -1 => bail!("PHP call_user_function fatal error (bailout) in: {func_name}"),
208 -2 => bail!("PHP call_user_function failed: {func_name}"),
209 code => bail!("PHP call_user_function error {code}: {func_name}"),
210 }
211 }
212
213 pub fn eval_protected(&mut self, code: &str) -> Result<EvalResult> {
218 let c_code = CString::new(code).context("PHP code contains null byte")?;
219 let mut retval = ffi::zval::new_undef();
220
221 let result = unsafe { ffi::folk_eval_string_protected(c_code.as_ptr(), &mut retval) };
222
223 let output = self.take_output();
224
225 let return_value = if result == 0 {
226 ZvalValue::from_raw(&mut retval)
227 } else {
228 ZvalValue::Null
229 };
230
231 unsafe { ffi::folk_zval_dtor(&mut retval) };
232
233 match result {
234 0 => Ok(EvalResult {
235 output,
236 return_value,
237 }),
238 -1 => bail!("PHP eval fatal error (bailout) in: {code}"),
239 -3 => bail!("PHP eval SIGSEGV caught in: {code}"),
240 code => bail!("PHP eval error {code} in: {code}"),
241 }
242 }
243
244 pub fn call_protected(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
248 let c_func = CString::new(func_name).context("function name contains null byte")?;
249
250 let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
251
252 let mut params: Vec<ffi::zval> = c_args
253 .iter()
254 .map(|s| {
255 let mut z = ffi::zval::new_undef();
256 unsafe {
257 ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
258 }
259 z
260 })
261 .collect();
262
263 let mut retval = ffi::zval::new_undef();
264
265 let result = unsafe {
266 ffi::folk_call_function_protected(
267 c_func.as_ptr(),
268 &mut retval,
269 u32::try_from(params.len()).expect("too many params"),
270 if params.is_empty() {
271 ptr::null_mut()
272 } else {
273 params.as_mut_ptr()
274 },
275 )
276 };
277
278 let return_value = ZvalValue::from_raw(&mut retval);
279
280 unsafe { ffi::folk_zval_dtor(&mut retval) };
281 for p in &mut params {
282 unsafe { ffi::folk_zval_dtor(p) };
283 }
284
285 match result {
286 0 => Ok(return_value),
287 -1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
288 -2 => bail!("PHP call failed: {func_name}"),
289 -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
290 code => bail!("PHP call error {code}: {func_name}"),
291 }
292 }
293
294 pub fn call_binary(&mut self, func_name: &str, method: &str, params: &[u8]) -> Result<Vec<u8>> {
300 let c_func = CString::new(func_name).context("func_name contains null byte")?;
301
302 let mut response_buf: *mut std::ffi::c_char = ptr::null_mut();
303 let mut response_len: usize = 0;
304
305 let result = unsafe {
306 ffi::folk_call_with_binary(
307 c_func.as_ptr(),
308 method.as_ptr().cast(),
309 method.len(),
310 params.as_ptr().cast(),
311 params.len(),
312 &mut response_buf,
313 &mut response_len,
314 )
315 };
316
317 let response = if !response_buf.is_null() && response_len > 0 {
318 let bytes =
319 unsafe { std::slice::from_raw_parts(response_buf.cast::<u8>(), response_len) };
320 let owned = bytes.to_vec();
321 unsafe { ffi::folk_free_buffer(response_buf) };
322 owned
323 } else {
324 if !response_buf.is_null() {
325 unsafe { ffi::folk_free_buffer(response_buf) };
326 }
327 Vec::new()
328 };
329
330 match result {
331 0 => Ok(response),
332 -1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
333 -2 => bail!("PHP call failed: {func_name}"),
334 -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
335 code => bail!("PHP call error {code}: {func_name}"),
336 }
337 }
338
339 pub fn take_output(&self) -> String {
341 let mut len: usize = 0;
342 let ptr = unsafe { ffi::folk_get_output(&mut len) };
343 let output = if ptr.is_null() || len == 0 {
344 String::new()
345 } else {
346 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
347 String::from_utf8_lossy(bytes).into_owned()
348 };
349 unsafe { ffi::folk_clear_output() };
350 output
351 }
352
353 pub fn take_response(&self) -> ResponseData {
355 let status = unsafe { ffi::folk_response_status_code() };
356 let status = u16::try_from(status).unwrap_or(500);
357 let header_count = unsafe { ffi::folk_response_header_count() };
358
359 let mut headers = Vec::with_capacity(header_count);
360 for i in 0..header_count {
361 let mut len: usize = 0;
362 let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
363 if !ptr.is_null() && len > 0 {
364 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
365 headers.push(String::from_utf8_lossy(bytes).into_owned());
366 }
367 }
368
369 let body = self.take_output();
370
371 ResponseData {
372 status_code: status,
373 headers,
374 body,
375 }
376 }
377}
378
379impl Drop for PhpInstance {
380 fn drop(&mut self) {
381 if self.in_request {
382 self.request_shutdown();
383 }
384
385 debug!("shutting down PHP SAPI");
386
387 unsafe {
388 ffi::folk_free_output();
389
390 if self.custom_sapi {
391 ffi::folk_response_free();
392 ffi::folk_sapi_shutdown();
393 } else {
394 ffi::php_embed_shutdown();
395 }
396 }
397 }
398}
399
400pub struct RequestContext {
406 method: CString,
408 uri: CString,
409 query_string: Option<CString>,
410 content_type: Option<CString>,
411 content_length: usize,
412 path_translated: Option<CString>,
413 post_data: Vec<u8>,
414 cookie: Option<CString>,
415 server_name: Option<CString>,
416 server_port: i32,
417 protocol: Option<CString>,
418
419 header_names_c: Vec<CString>,
421 header_values_c: Vec<CString>,
422 header_name_ptrs: Vec<*const std::ffi::c_char>,
423 header_value_ptrs: Vec<*const std::ffi::c_char>,
424
425 ffi: ffi::folk_request_context,
427}
428
429impl RequestContext {
430 pub fn new(method: &str, uri: &str) -> Self {
431 Self {
432 method: CString::new(method).expect("method contains null"),
433 uri: CString::new(uri).expect("uri contains null"),
434 query_string: None,
435 content_type: None,
436 content_length: 0,
437 path_translated: None,
438 post_data: Vec::new(),
439 cookie: None,
440 server_name: None,
441 server_port: 0,
442 protocol: None,
443 header_names_c: Vec::new(),
444 header_values_c: Vec::new(),
445 header_name_ptrs: Vec::new(),
446 header_value_ptrs: Vec::new(),
447 ffi: unsafe { std::mem::zeroed() },
448 }
449 }
450
451 #[must_use]
452 pub fn query_string(mut self, qs: &str) -> Self {
453 self.query_string = Some(CString::new(qs).expect("query_string contains null"));
454 self
455 }
456
457 #[must_use]
458 pub fn content_type(mut self, ct: &str) -> Self {
459 self.content_type = Some(CString::new(ct).expect("content_type contains null"));
460 self
461 }
462
463 #[must_use]
464 pub fn body(mut self, data: &[u8]) -> Self {
465 self.post_data = data.to_vec();
466 self.content_length = data.len();
467 self
468 }
469
470 #[must_use]
471 pub fn cookie(mut self, cookie: &str) -> Self {
472 self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
473 self
474 }
475
476 #[must_use]
477 pub fn path_translated(mut self, path: &str) -> Self {
478 self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
479 self
480 }
481
482 #[must_use]
483 pub fn server(mut self, name: &str, port: i32) -> Self {
484 self.server_name = Some(CString::new(name).expect("server_name contains null"));
485 self.server_port = port;
486 self
487 }
488
489 #[must_use]
490 pub fn protocol(mut self, proto: &str) -> Self {
491 self.protocol = Some(CString::new(proto).expect("protocol contains null"));
492 self
493 }
494
495 #[must_use]
496 pub fn header(mut self, name: &str, value: &str) -> Self {
497 self.header_names_c
498 .push(CString::new(name).expect("header name contains null"));
499 self.header_values_c
500 .push(CString::new(value).expect("header value contains null"));
501 self
502 }
503
504 fn build_ffi(&mut self) {
507 self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
509 self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
510
511 self.ffi = ffi::folk_request_context {
512 request_method: self.method.as_ptr(),
513 request_uri: self.uri.as_ptr(),
514 query_string: self
515 .query_string
516 .as_ref()
517 .map_or(ptr::null(), |s| s.as_ptr()),
518 content_type: self
519 .content_type
520 .as_ref()
521 .map_or(ptr::null(), |s| s.as_ptr()),
522 content_length: self.content_length,
523 path_translated: self
524 .path_translated
525 .as_ref()
526 .map_or(ptr::null(), |s| s.as_ptr()),
527 post_data: if self.post_data.is_empty() {
528 ptr::null()
529 } else {
530 self.post_data.as_ptr().cast()
531 },
532 post_data_len: self.post_data.len(),
533 post_data_read: 0,
534 cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
535 header_names: if self.header_name_ptrs.is_empty() {
536 ptr::null()
537 } else {
538 self.header_name_ptrs.as_ptr()
539 },
540 header_values: if self.header_value_ptrs.is_empty() {
541 ptr::null()
542 } else {
543 self.header_value_ptrs.as_ptr()
544 },
545 header_count: self.header_names_c.len(),
546 server_name: self
547 .server_name
548 .as_ref()
549 .map_or(ptr::null(), |s| s.as_ptr()),
550 server_port: self.server_port,
551 protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
552 };
553 }
554}
555
556#[derive(Debug, Clone)]
558pub struct ResponseData {
559 pub status_code: u16,
561 pub headers: Vec<String>,
563 pub body: String,
565}
566
567#[derive(Debug)]
569pub struct EvalResult {
570 pub output: String,
572 pub return_value: ZvalValue,
574}
575
576#[derive(Debug, Clone, PartialEq)]
578pub enum ZvalValue {
579 Null,
580 Bool(bool),
581 Long(i64),
582 Double(f64),
583 String(String),
584 Other(i32),
586}
587
588impl ZvalValue {
589 fn from_raw(z: &mut ffi::zval) -> Self {
590 let ztype = unsafe { ffi::folk_zval_type(z) };
591 match ztype {
592 ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
593 ffi::IS_FALSE => Self::Bool(false),
594 ffi::IS_TRUE => Self::Bool(true),
595 ffi::IS_LONG => {
596 let v = unsafe { ffi::folk_zval_get_long(z) };
597 Self::Long(v)
598 },
599 ffi::IS_STRING => {
600 let mut len: usize = 0;
601 let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
602 if ptr.is_null() {
603 Self::Null
604 } else {
605 let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
606 Self::String(String::from_utf8_lossy(bytes).into_owned())
607 }
608 },
609 other => Self::Other(other),
610 }
611 }
612
613 pub fn as_str(&self) -> Option<&str> {
614 match self {
615 Self::String(s) => Some(s),
616 _ => None,
617 }
618 }
619
620 pub fn as_long(&self) -> Option<i64> {
621 match self {
622 Self::Long(v) => Some(*v),
623 _ => None,
624 }
625 }
626}