#![allow(unsafe_code)]
use std::ffi::CString;
use std::ptr;
use anyhow::{Context, Result, bail};
use tracing::{debug, warn};
use crate::ffi;
pub struct PhpInstance {
in_request: bool,
custom_sapi: bool,
owns_module: bool,
tsrm_ctx: *mut std::ffi::c_void,
}
unsafe impl Send for PhpInstance {}
unsafe impl Sync for PhpInstance {}
impl PhpInstance {
pub fn boot() -> Result<Self> {
debug!("booting PHP embed SAPI");
let result = unsafe { ffi::php_embed_init(0, ptr::null_mut()) };
if result != 0 {
bail!("php_embed_init() failed with code {result}");
}
unsafe { ffi::folk_install_output_handler() };
debug!("PHP embed SAPI initialized");
Ok(Self {
in_request: false,
custom_sapi: false,
owns_module: true,
tsrm_ctx: ptr::null_mut(),
})
}
pub fn boot_custom_sapi() -> Result<Self> {
debug!("booting PHP with Folk custom SAPI");
unsafe { ffi::folk_signals_save() };
let result = unsafe { ffi::folk_sapi_init() };
unsafe { ffi::folk_signals_restore() };
unsafe { ffi::folk_sigsegv_handler_install() };
if result != 0 {
bail!("folk_sapi_init() failed with code {result}");
}
debug!("Folk custom SAPI initialized");
Ok(Self {
in_request: false,
custom_sapi: true,
owns_module: true,
tsrm_ctx: ptr::null_mut(),
})
}
pub fn attach() -> Self {
debug!("attaching to existing PHP module");
let ctx = unsafe { ffi::folk_thread_init() };
if !ctx.is_null() {
unsafe { ffi::folk_thread_set_ctx(ctx) };
debug!("TSRM context allocated for worker thread");
}
Self {
in_request: false,
custom_sapi: true,
owns_module: false,
tsrm_ctx: ctx,
}
}
pub fn set_request_context(&self, ctx: &mut RequestContext) {
ctx.build_ffi();
unsafe { ffi::folk_request_context_set(&mut ctx.ffi) };
}
pub fn clear_request_context(&self) {
unsafe { ffi::folk_request_context_clear() };
}
pub fn request_startup(&mut self) -> Result<()> {
if self.in_request {
warn!("request_startup called while already in request — shutting down first");
self.request_shutdown();
}
unsafe { ffi::folk_clear_output() };
if self.custom_sapi {
unsafe { ffi::folk_response_clear() };
}
let result = unsafe { ffi::folk_request_startup_safe() };
match result {
0 => {
self.in_request = true;
Ok(())
},
-1 => bail!("php_request_startup: fatal error (bailout)"),
-2 => bail!("php_request_startup: startup failed"),
code => bail!("php_request_startup: unknown error {code}"),
}
}
pub fn request_shutdown(&mut self) {
if !self.in_request {
return;
}
let result = unsafe { ffi::folk_request_shutdown_safe() };
if result != 0 {
warn!("php_request_shutdown returned {result}");
}
if self.custom_sapi {
self.clear_request_context();
}
self.in_request = false;
}
pub fn execute_script(&mut self, filename: &str) -> Result<String> {
let c_filename = CString::new(filename).context("filename contains null byte")?;
let result = unsafe { ffi::folk_execute_script_safe(c_filename.as_ptr()) };
let output = self.take_output();
match result {
0 => Ok(output),
-1 => {
if output.is_empty() {
bail!("PHP script fatal error (bailout) in: {filename}")
} else {
bail!("PHP script fatal error: {output}")
}
},
other => bail!("PHP script error {other} in: {filename}"),
}
}
pub fn eval(&mut self, code: &str) -> Result<EvalResult> {
let c_code = CString::new(code).context("PHP code contains null byte")?;
let mut retval = ffi::zval::new_undef();
let result = unsafe { ffi::folk_eval_string_safe(c_code.as_ptr(), &mut retval) };
let output = self.take_output();
let return_value = if result == 0 {
ZvalValue::from_raw(&mut retval)
} else {
ZvalValue::Null
};
unsafe { ffi::folk_zval_dtor(&mut retval) };
match result {
0 => Ok(EvalResult {
output,
return_value,
}),
-1 => {
if output.is_empty() {
bail!("PHP eval fatal error (bailout) in: {code}")
} else {
bail!("PHP eval fatal error (bailout): {output}")
}
},
other => bail!("PHP eval error {other} in: {code}"),
}
}
pub fn call(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
let c_func = CString::new(func_name).context("function name contains null byte")?;
let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
let mut params: Vec<ffi::zval> = c_args
.iter()
.map(|s| {
let mut z = ffi::zval::new_undef();
unsafe {
ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
}
z
})
.collect();
let mut retval = ffi::zval::new_undef();
let result = unsafe {
ffi::folk_call_function_safe(
c_func.as_ptr(),
&mut retval,
u32::try_from(params.len()).expect("too many params"),
if params.is_empty() {
ptr::null_mut()
} else {
params.as_mut_ptr()
},
)
};
let return_value = ZvalValue::from_raw(&mut retval);
unsafe { ffi::folk_zval_dtor(&mut retval) };
for p in &mut params {
unsafe { ffi::folk_zval_dtor(p) };
}
match result {
0 => Ok(return_value),
-1 => bail!("PHP call_user_function fatal error (bailout) in: {func_name}"),
-2 => bail!("PHP call_user_function failed: {func_name}"),
code => bail!("PHP call_user_function error {code}: {func_name}"),
}
}
pub fn eval_protected(&mut self, code: &str) -> Result<EvalResult> {
let c_code = CString::new(code).context("PHP code contains null byte")?;
let mut retval = ffi::zval::new_undef();
let result = unsafe { ffi::folk_eval_string_protected(c_code.as_ptr(), &mut retval) };
let output = self.take_output();
let return_value = if result == 0 {
ZvalValue::from_raw(&mut retval)
} else {
ZvalValue::Null
};
unsafe { ffi::folk_zval_dtor(&mut retval) };
match result {
0 => Ok(EvalResult {
output,
return_value,
}),
-1 => bail!("PHP eval fatal error (bailout) in: {code}"),
-3 => bail!("PHP eval SIGSEGV caught in: {code}"),
code => bail!("PHP eval error {code} in: {code}"),
}
}
pub fn call_protected(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
let c_func = CString::new(func_name).context("function name contains null byte")?;
let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
let mut params: Vec<ffi::zval> = c_args
.iter()
.map(|s| {
let mut z = ffi::zval::new_undef();
unsafe {
ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
}
z
})
.collect();
let mut retval = ffi::zval::new_undef();
let result = unsafe {
ffi::folk_call_function_protected(
c_func.as_ptr(),
&mut retval,
u32::try_from(params.len()).expect("too many params"),
if params.is_empty() {
ptr::null_mut()
} else {
params.as_mut_ptr()
},
)
};
let return_value = ZvalValue::from_raw(&mut retval);
unsafe { ffi::folk_zval_dtor(&mut retval) };
for p in &mut params {
unsafe { ffi::folk_zval_dtor(p) };
}
match result {
0 => Ok(return_value),
-1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
-2 => bail!("PHP call failed: {func_name}"),
-3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
code => bail!("PHP call error {code}: {func_name}"),
}
}
pub fn call_binary(&mut self, func_name: &str, method: &str, params: &[u8]) -> Result<Vec<u8>> {
let c_func = CString::new(func_name).context("func_name contains null byte")?;
let mut response_buf: *mut std::ffi::c_char = ptr::null_mut();
let mut response_len: usize = 0;
let result = unsafe {
ffi::folk_call_with_binary(
c_func.as_ptr(),
method.as_ptr().cast(),
method.len(),
params.as_ptr().cast(),
params.len(),
&mut response_buf,
&mut response_len,
)
};
let response = if !response_buf.is_null() && response_len > 0 {
let bytes =
unsafe { std::slice::from_raw_parts(response_buf.cast::<u8>(), response_len) };
let owned = bytes.to_vec();
unsafe { ffi::folk_free_buffer(response_buf) };
owned
} else {
if !response_buf.is_null() {
unsafe { ffi::folk_free_buffer(response_buf) };
}
Vec::new()
};
match result {
0 => Ok(response),
-1 => {
let output = self.take_output();
if output.is_empty() {
bail!("PHP call fatal error (bailout) in: {func_name}")
} else {
bail!("PHP call fatal error (bailout): {output}")
}
},
-2 => bail!("PHP call failed: {func_name}"),
-3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
code => bail!("PHP call error {code}: {func_name}"),
}
}
pub fn take_output(&self) -> String {
let mut len: usize = 0;
let ptr = unsafe { ffi::folk_get_output(&mut len) };
let output = if ptr.is_null() || len == 0 {
String::new()
} else {
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
String::from_utf8_lossy(bytes).into_owned()
};
unsafe { ffi::folk_clear_output() };
output
}
pub fn take_response(&self) -> ResponseData {
let status = unsafe { ffi::folk_response_status_code() };
let status = u16::try_from(status).unwrap_or(500);
let header_count = unsafe { ffi::folk_response_header_count() };
let mut headers = Vec::with_capacity(header_count);
for i in 0..header_count {
let mut len: usize = 0;
let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
if !ptr.is_null() && len > 0 {
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
headers.push(String::from_utf8_lossy(bytes).into_owned());
}
}
let body = self.take_output();
ResponseData {
status_code: status,
headers,
body,
}
}
}
impl Drop for PhpInstance {
fn drop(&mut self) {
if self.in_request {
self.request_shutdown();
}
unsafe {
ffi::folk_free_output();
if self.custom_sapi {
ffi::folk_response_free();
}
}
if !self.tsrm_ctx.is_null() {
unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
self.tsrm_ctx = ptr::null_mut();
}
if self.owns_module {
debug!("shutting down PHP SAPI");
unsafe {
if self.custom_sapi {
ffi::folk_sapi_shutdown();
} else {
ffi::php_embed_shutdown();
}
}
}
}
}
pub struct RequestContext {
method: CString,
uri: CString,
query_string: Option<CString>,
content_type: Option<CString>,
content_length: usize,
path_translated: Option<CString>,
post_data: Vec<u8>,
cookie: Option<CString>,
server_name: Option<CString>,
server_port: i32,
protocol: Option<CString>,
header_names_c: Vec<CString>,
header_values_c: Vec<CString>,
header_name_ptrs: Vec<*const std::ffi::c_char>,
header_value_ptrs: Vec<*const std::ffi::c_char>,
ffi: ffi::folk_request_context,
}
impl RequestContext {
pub fn new(method: &str, uri: &str) -> Self {
Self {
method: CString::new(method).expect("method contains null"),
uri: CString::new(uri).expect("uri contains null"),
query_string: None,
content_type: None,
content_length: 0,
path_translated: None,
post_data: Vec::new(),
cookie: None,
server_name: None,
server_port: 0,
protocol: None,
header_names_c: Vec::new(),
header_values_c: Vec::new(),
header_name_ptrs: Vec::new(),
header_value_ptrs: Vec::new(),
ffi: unsafe { std::mem::zeroed() },
}
}
#[must_use]
pub fn query_string(mut self, qs: &str) -> Self {
self.query_string = Some(CString::new(qs).expect("query_string contains null"));
self
}
#[must_use]
pub fn content_type(mut self, ct: &str) -> Self {
self.content_type = Some(CString::new(ct).expect("content_type contains null"));
self
}
#[must_use]
pub fn body(mut self, data: &[u8]) -> Self {
self.post_data = data.to_vec();
self.content_length = data.len();
self
}
#[must_use]
pub fn cookie(mut self, cookie: &str) -> Self {
self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
self
}
#[must_use]
pub fn path_translated(mut self, path: &str) -> Self {
self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
self
}
#[must_use]
pub fn server(mut self, name: &str, port: i32) -> Self {
self.server_name = Some(CString::new(name).expect("server_name contains null"));
self.server_port = port;
self
}
#[must_use]
pub fn protocol(mut self, proto: &str) -> Self {
self.protocol = Some(CString::new(proto).expect("protocol contains null"));
self
}
#[must_use]
pub fn header(mut self, name: &str, value: &str) -> Self {
self.header_names_c
.push(CString::new(name).expect("header name contains null"));
self.header_values_c
.push(CString::new(value).expect("header value contains null"));
self
}
fn build_ffi(&mut self) {
self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
self.ffi = ffi::folk_request_context {
request_method: self.method.as_ptr(),
request_uri: self.uri.as_ptr(),
query_string: self
.query_string
.as_ref()
.map_or(ptr::null(), |s| s.as_ptr()),
content_type: self
.content_type
.as_ref()
.map_or(ptr::null(), |s| s.as_ptr()),
content_length: self.content_length,
path_translated: self
.path_translated
.as_ref()
.map_or(ptr::null(), |s| s.as_ptr()),
post_data: if self.post_data.is_empty() {
ptr::null()
} else {
self.post_data.as_ptr().cast()
},
post_data_len: self.post_data.len(),
post_data_read: 0,
cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
header_names: if self.header_name_ptrs.is_empty() {
ptr::null()
} else {
self.header_name_ptrs.as_ptr()
},
header_values: if self.header_value_ptrs.is_empty() {
ptr::null()
} else {
self.header_value_ptrs.as_ptr()
},
header_count: self.header_names_c.len(),
server_name: self
.server_name
.as_ref()
.map_or(ptr::null(), |s| s.as_ptr()),
server_port: self.server_port,
protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
};
}
}
#[derive(Debug, Clone)]
pub struct ResponseData {
pub status_code: u16,
pub headers: Vec<String>,
pub body: String,
}
#[derive(Debug)]
pub struct EvalResult {
pub output: String,
pub return_value: ZvalValue,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ZvalValue {
Null,
Bool(bool),
Long(i64),
Double(f64),
String(String),
Other(i32),
}
impl ZvalValue {
fn from_raw(z: &mut ffi::zval) -> Self {
let ztype = unsafe { ffi::folk_zval_type(z) };
match ztype {
ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
ffi::IS_FALSE => Self::Bool(false),
ffi::IS_TRUE => Self::Bool(true),
ffi::IS_LONG => {
let v = unsafe { ffi::folk_zval_get_long(z) };
Self::Long(v)
},
ffi::IS_STRING => {
let mut len: usize = 0;
let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
if ptr.is_null() {
Self::Null
} else {
let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
Self::String(String::from_utf8_lossy(bytes).into_owned())
}
},
other => Self::Other(other),
}
}
pub fn as_str(&self) -> Option<&str> {
match self {
Self::String(s) => Some(s),
_ => None,
}
}
pub fn as_long(&self) -> Option<i64> {
match self {
Self::Long(v) => Some(*v),
_ => None,
}
}
}