From 12115601fd12303f8da14a6b6f0ff5399265b530 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 7 Mar 2025 18:27:38 +0800 Subject: [PATCH 1/9] Switch to generic language handler interface --- Cargo.toml | 1 + __test__/index.spec.mjs | 27 +- crates/lang_handler/Cargo.toml | 19 + crates/lang_handler/build.rs | 26 ++ crates/lang_handler/src/handler.rs | 7 + crates/lang_handler/src/headers.rs | 158 +++++++++ crates/lang_handler/src/lib.rs | 20 ++ crates/lang_handler/src/request.rs | 293 ++++++++++++++++ crates/lang_handler/src/response.rs | 215 ++++++++++++ crates/lang_handler/src/url.rs | 106 ++++++ crates/php/Cargo.toml | 3 + crates/php/build.rs | 59 +++- crates/php/src/embed.rs | 136 ++++---- crates/php/src/lib.rs | 6 +- crates/php/src/main.rs | 38 +- crates/php/src/php_wrapper.c | 518 ++++++++++------------------ crates/php/src/request.rs | 124 ------- crates/php/src/response.rs | 55 --- crates/php_node/src/lib.rs | 266 ++++++++++++-- index.d.ts | 26 +- index.js | 6 +- 21 files changed, 1456 insertions(+), 653 deletions(-) create mode 100644 crates/lang_handler/Cargo.toml create mode 100644 crates/lang_handler/build.rs create mode 100644 crates/lang_handler/src/handler.rs create mode 100644 crates/lang_handler/src/headers.rs create mode 100644 crates/lang_handler/src/lib.rs create mode 100644 crates/lang_handler/src/request.rs create mode 100644 crates/lang_handler/src/response.rs create mode 100644 crates/lang_handler/src/url.rs delete mode 100644 crates/php/src/request.rs delete mode 100644 crates/php/src/response.rs diff --git a/Cargo.toml b/Cargo.toml index d98574bb..9dc13c56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "crates/lang_handler", "crates/php", "crates/php_node" ] diff --git a/__test__/index.spec.mjs b/__test__/index.spec.mjs index 0a783448..2b49ba1d 100644 --- a/__test__/index.spec.mjs +++ b/__test__/index.spec.mjs @@ -1,7 +1,28 @@ import test from 'ava' -import { handleRequest } from '../index.js' +import { Php, Request } from '../index.js' -test('sum from native', (t) => { - t.is(handleRequest(), "Hello, World!") +test('sum from native', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + http_response_code(400); + + echo phpinfo(); + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': 13 + }, + body: 'Hello, World!' + }) + + const res = await php.handleRequest(req) + t.is(res.status, 400) + t.is(res.body, "wat") }) diff --git a/crates/lang_handler/Cargo.toml b/crates/lang_handler/Cargo.toml new file mode 100644 index 00000000..699f3314 --- /dev/null +++ b/crates/lang_handler/Cargo.toml @@ -0,0 +1,19 @@ +[package] +edition = "2021" +name = "lang_handler" +version = "0.0.0" + +[lib] +name = "lang_handler" +crate-type = ["cdylib", "rlib"] +path = "src/lib.rs" + +[features] +default = [] +c = [] + +[build-dependencies] +cbindgen = "0.28.0" + +[dependencies] +url = "2.5.4" diff --git a/crates/lang_handler/build.rs b/crates/lang_handler/build.rs new file mode 100644 index 00000000..fb1c932f --- /dev/null +++ b/crates/lang_handler/build.rs @@ -0,0 +1,26 @@ +extern crate cbindgen; + +use std::env; +use std::path::PathBuf; + +fn main() { + let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + let out_dir = PathBuf::from(crate_dir.clone()); + + cbindgen::Builder::new() + .with_crate(crate_dir) + .with_include_guard("LANG_HANDLER_H") + .with_language(cbindgen::Language::C) + .with_parse_deps(true) + .with_parse_include(&["url"]) + .include_item("Url") + // .rename_item("Request", "lh_request_t") + // .rename_item("RequestBuilder", "lh_request_builder_t") + // .rename_item("Response", "lh_response_t") + // .rename_item("ResponseBuilder", "lh_response_builder_t") + // .rename_item("Headers", "lh_headers_t") + // .rename_item("Url", "lh_url_t") + .generate() + .expect("Unable to generate bindings") + .write_to_file(out_dir.join("../../target/release/lang_handler.h")); +} diff --git a/crates/lang_handler/src/handler.rs b/crates/lang_handler/src/handler.rs new file mode 100644 index 00000000..4f9283ef --- /dev/null +++ b/crates/lang_handler/src/handler.rs @@ -0,0 +1,7 @@ +use crate::{Request, Response}; + +pub trait Handler { + type Error; + + fn handle(&self, request: Request) -> Result; +} diff --git a/crates/lang_handler/src/headers.rs b/crates/lang_handler/src/headers.rs new file mode 100644 index 00000000..d5acde83 --- /dev/null +++ b/crates/lang_handler/src/headers.rs @@ -0,0 +1,158 @@ +use std::collections::HashMap; + +#[derive(Debug, Clone)] +pub struct Headers(HashMap>); + +impl Headers { + pub fn new() -> Self { + Headers(HashMap::new()) + } + + pub fn get(&self, key: K) -> Option<&Vec> + where + K: AsRef, + { + self.0.get(key.as_ref()) + } + + pub fn set(&mut self, key: K, value: V) + where + K: Into, + V: Into, + { + self.0 + .entry(key.into()) + .or_insert_with(Vec::new) + .push(value.into()); + } + + pub fn remove(&mut self, key: K) + where + K: AsRef, + { + self.0.remove(key.as_ref()); + } + + pub fn iter(&self) -> impl Iterator)> { + self.0.iter() + } + + pub fn iter_values(&self) -> impl Iterator { + self.0.values().flatten() + } +} + +#[allow(non_camel_case_types)] +pub struct lh_headers_t { + inner: Headers, +} + +impl From for lh_headers_t { + fn from(inner: Headers) -> Self { + Self { inner } + } +} + +impl From<&lh_headers_t> for Headers { + fn from(headers: &lh_headers_t) -> Headers { + headers.inner.clone() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_new() -> *mut lh_headers_t { + let headers = Headers::new(); + Box::into_raw(Box::new(headers.into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { + if !headers.is_null() { + unsafe { + drop(Box::from_raw(headers)); + } + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_count(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> usize { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(value) => value.len(), + None => 0 + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_get(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if values.len() > 0 { + values[values.len() - 1].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_get_nth(headers: *const lh_headers_t, key: *const std::os::raw::c_char, index: usize) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if index < values.len() { + values[index].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_headers_set(headers: *mut lh_headers_t, key: *const std::os::raw::c_char, value: *const std::os::raw::c_char) { + let headers = unsafe { + assert!(!headers.is_null()); + &mut *headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap().to_string() + }; + let value = unsafe { + assert!(!value.is_null()); + std::ffi::CStr::from_ptr(value).to_str().unwrap().to_string() + }; + headers.inner.set(key, value); +} diff --git a/crates/lang_handler/src/lib.rs b/crates/lang_handler/src/lib.rs new file mode 100644 index 00000000..d0cd60d8 --- /dev/null +++ b/crates/lang_handler/src/lib.rs @@ -0,0 +1,20 @@ +mod handler; +mod headers; +mod request; +mod response; +mod url; + +use std::ffi::{CString, c_char}; + +pub use handler::Handler; +pub use headers::{Headers, lh_headers_t}; +pub use request::{Request, RequestBuilder, lh_request_t, lh_request_builder_t}; +pub use response::{Response, ResponseBuilder, lh_response_t, lh_response_builder_t}; +pub use url::{Url, lh_url_t}; + +#[no_mangle] +pub extern "C" fn lh_reclaim_str(url: *const c_char) { + unsafe { + drop(CString::from_raw(url as *mut c_char)); + } +} diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs new file mode 100644 index 00000000..9286d185 --- /dev/null +++ b/crates/lang_handler/src/request.rs @@ -0,0 +1,293 @@ +#[cfg(feature = "c")] +use std::{ffi, ffi::{CStr, CString}}; + +use url::{ParseError, Url}; + +use crate::Headers; +use crate::headers::lh_headers_t; +use crate::url::lh_url_t; + +#[derive(Clone)] +pub struct Request { + method: String, + url: Url, + headers: Headers, + // TODO: Support Stream bodies when napi.rs supports it + body: Option, +} + +impl Request { + pub fn new(method: String, url: Url, headers: Headers, body: Option) -> Self { + Self { + method, + url, + headers, + body + } + } + + pub fn builder() -> RequestBuilder { + RequestBuilder::new() + } + + pub fn extend(&self) -> RequestBuilder { + RequestBuilder::extend(self) + } + + pub fn method(&self) -> &str { + &self.method + } + + pub fn url(&self) -> &Url { + &self.url + } + + pub fn headers(&self) -> &Headers { + &self.headers + } + + pub fn body(&self) -> Option<&String> { + self.body.as_ref() + } +} + +#[derive(Clone)] +pub struct RequestBuilder { + method: Option, + url: Option, + headers: Headers, + body: Option, +} + +impl RequestBuilder { + pub fn new() -> Self { + Self { + method: None, + url: None, + headers: Headers::new(), + body: None, + } + } + + pub fn extend(request: &Request) -> Self { + Self { + method: Some(request.method().into()), + url: Some(request.url().clone()), + headers: request.headers().clone(), + body: request.body().map(|b| b.into()), + } + } + + pub fn method>(mut self, method: T) -> Self { + self.method = Some(method.into()); + self + } + + pub fn url(mut self, url: T) -> Result + where + T: Into + { + match url.into().parse() { + Ok(url) => { + self.url = Some(url); + Ok(self) + }, + Err(e) => return Err(e), + } + } + + pub fn header(mut self, key: K, value: V) -> Self + where + K: Into, + V: Into + { + self.headers.set(key.into(), value.into()); + self + } + + pub fn body>(mut self, body: T) -> Self { + self.body = Some(body.into()); + self + } + + pub fn build(self) -> Request { + Request { + method: self.method.unwrap_or_else(|| "GET".to_string()), + url: self.url.unwrap_or_else(|| Url::parse("/").unwrap()), + headers: self.headers, + body: self.body, + } + } +} + +#[allow(non_camel_case_types)] +pub struct lh_request_t { + inner: Request, +} + +impl From for lh_request_t { + fn from(inner: Request) -> Self { + Self { inner } + } +} + +impl From<&lh_request_t> for Request { + fn from(request: &lh_request_t) -> Request { + request.inner.clone() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_new( + method: *const ffi::c_char, + url: *const ffi::c_char, + headers: *mut lh_headers_t, + body: *const ffi::c_char, +) -> *mut lh_request_t { + let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; + let url_str = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let url = Url::parse(&url_str).unwrap(); + let body = if body.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }) + }; + let headers = unsafe { &*headers }; + let request = Request::new(method, url, headers.into(), body); + Box::into_raw(Box::new(request.into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + CString::new(request.inner.method()).unwrap().into_raw() +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.url().clone().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_headers_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.headers().clone().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + match request.inner.body() { + Some(body) => CString::new(body.as_str().as_bytes()).unwrap().into_raw(), + None => std::ptr::null(), + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_free(request: *mut lh_request_t) { + if !request.is_null() { + unsafe { + drop(Box::from_raw(request)); + } + } +} + +#[allow(non_camel_case_types)] +pub struct lh_request_builder_t { + inner: RequestBuilder, +} + +impl From for lh_request_builder_t { + fn from(inner: RequestBuilder) -> Self { + Self { inner } + } +} + +impl From<&lh_request_builder_t> for RequestBuilder { + fn from(builder: &lh_request_builder_t) -> RequestBuilder { + builder.inner.clone() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_new() -> *mut lh_request_builder_t { + Box::into_raw(Box::new(RequestBuilder::new().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_extend(request: *const lh_request_t) -> *mut lh_request_builder_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(RequestBuilder::extend(&request.inner).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_method( + builder: *mut lh_request_builder_t, + method: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().method(method).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_url( + builder: *mut lh_request_builder_t, + url: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().url(&url).unwrap().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_header( + builder: *mut lh_request_builder_t, + key: *const ffi::c_char, + value: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let key = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().header(key, value).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_body( + builder: *mut lh_request_builder_t, + body: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let body = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().body(body).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_build(builder: *mut lh_request_builder_t) -> *mut lh_request_t { + let builder = unsafe { Box::from_raw(builder) }; + Box::into_raw(Box::new(builder.inner.build().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs new file mode 100644 index 00000000..c4ea3efb --- /dev/null +++ b/crates/lang_handler/src/response.rs @@ -0,0 +1,215 @@ +#[cfg(feature = "c")] +use std::ffi::{CStr, CString, c_char}; + +use crate::Headers; +use crate::headers::lh_headers_t; + +#[derive(Clone)] +pub struct Response { + status: u16, + headers: Headers, + // TODO: Support Stream bodies when napi.rs supports it + body: String, +} + +impl Response { + pub fn new(status: u16, headers: Headers, body: String) -> Self { + Self { + status, + headers, + body, + } + } + + pub fn builder() -> ResponseBuilder { + ResponseBuilder::new() + } + + pub fn extend(&self) -> ResponseBuilder { + ResponseBuilder::extend(self) + } + + pub fn status(&self) -> u16 { + self.status + } + + pub fn headers(&self) -> &Headers { + &self.headers + } + + pub fn body(&self) -> &String { + &self.body + } +} + +#[derive(Clone)] +pub struct ResponseBuilder { + status: Option, + headers: Headers, + body: Option, +} + +impl ResponseBuilder { + pub fn new() -> Self { + ResponseBuilder { + status: None, + headers: Headers::new(), + body: None, + } + } + + pub fn extend(response: &Response) -> Self { + ResponseBuilder { + status: Some(response.status), + headers: response.headers.clone(), + body: Some(response.body.clone()), + } + } + + pub fn status_code(&mut self, status: u16) -> &mut Self { + self.status = Some(status); + self + } + + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + K: Into, + V: Into, + { + self.headers.set(key, value); + self + } + + pub fn body(&mut self, body: B) -> &mut Self + where + B: Into, + { + self.body = Some(body.into()); + self + } + + pub fn build(&self) -> Response { + Response { + status: self.status.unwrap_or(200), + headers: self.headers.clone(), + body: self.body.clone().unwrap_or_default(), + } + } +} + +#[allow(non_camel_case_types)] +pub struct lh_response_t { + inner: Response, +} + +impl From for lh_response_t { + fn from(inner: Response) -> Self { + Self { inner } + } +} + +impl From<&lh_response_t> for Response { + fn from(response: &lh_response_t) -> Response { + response.inner.clone() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { + let body_str = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; + let headers = unsafe { &*headers }; + Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_free(response: *mut lh_response_t) { + if !response.is_null() { + unsafe { + drop(Box::from_raw(response)); + } + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_status(response: *const lh_response_t) -> u16 { + let response = unsafe { &*response }; + response.inner.status() +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh_headers_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(response.inner.headers().clone().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { + let response = unsafe { &*response }; + CString::new(response.inner.body().as_str().as_bytes()).unwrap().into_raw() +} + +#[allow(non_camel_case_types)] +pub struct lh_response_builder_t { + inner: ResponseBuilder, +} + +impl From for lh_response_builder_t { + fn from(inner: ResponseBuilder) -> Self { + Self { inner } + } +} + +impl From<&lh_response_builder_t> for ResponseBuilder { + fn from(builder: &lh_response_builder_t) -> ResponseBuilder { + builder.inner.clone() + } +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_new() -> *mut lh_response_builder_t { + Box::into_raw(Box::new(ResponseBuilder::new().into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_extend(response: *const lh_response_t) -> *mut lh_response_builder_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(ResponseBuilder::extend(&response.inner).into())) +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_status_code(builder: *mut lh_response_builder_t, status_code: u16) { + let builder = unsafe { &mut *builder }; + builder.inner.status_code(status_code); +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_header(builder: *mut lh_response_builder_t, key: *const c_char, value: *const c_char) { + let builder = unsafe { &mut *builder }; + let key_str = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value_str = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + builder.inner.header(key_str, value_str); +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_body(builder: *mut lh_response_builder_t, body: *const c_char) { + let builder = unsafe { &mut *builder }; + let body_str = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; + builder.inner.body(body_str); +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_build(builder: *const lh_response_builder_t) -> *mut lh_response_t { + let builder = unsafe { &*builder }; + Box::into_raw(Box::new(builder.inner.build().into())) +} diff --git a/crates/lang_handler/src/url.rs b/crates/lang_handler/src/url.rs new file mode 100644 index 00000000..757f568f --- /dev/null +++ b/crates/lang_handler/src/url.rs @@ -0,0 +1,106 @@ +use std::ffi::{CString, c_char}; + +pub use url::Url; + +#[allow(non_camel_case_types)] +pub struct lh_url_t { + inner: Url, +} + +impl From for lh_url_t { + fn from(inner: Url) -> Self { + Self { inner } + } +} + +impl From<&lh_url_t> for Url { + fn from(url: &lh_url_t) -> Url { + url.inner.clone() + } +} + +#[no_mangle] +pub extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.scheme()).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.host_str().unwrap_or("")).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.domain().unwrap_or("")).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { + let url = unsafe { &*url }; + url.inner.port().unwrap_or(0) +} + +#[no_mangle] +pub extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + let origin = match url.inner.origin() { + url::Origin::Opaque(_) => { + format!("{}://", url.inner.scheme()) + }, + url::Origin::Tuple(scheme, host, port) => { + format!("{}://{}:{}", scheme, host, port) + } + }; + CString::new(origin.as_str()).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { + let url = unsafe { &*url }; + url.inner.has_authority() +} + +#[no_mangle] +pub extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.authority()).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.username()).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.password().unwrap_or("")).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.path()).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.query().unwrap_or("")).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.fragment().unwrap_or("")).unwrap().into_raw() +} + +#[no_mangle] +pub extern "C" fn lh_url_uri(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.as_str()).unwrap().into_raw() +} diff --git a/crates/php/Cargo.toml b/crates/php/Cargo.toml index 40d6e144..79263fc1 100644 --- a/crates/php/Cargo.toml +++ b/crates/php/Cargo.toml @@ -11,6 +11,9 @@ path = "src/lib.rs" name = "php-main" path = "src/main.rs" +[dependencies] +lang_handler = { path = "../lang_handler", features = ["c"] } + [build-dependencies] autotools = "0.2" bindgen = "0.69.4" diff --git a/crates/php/build.rs b/crates/php/build.rs index 6cce4a9a..891840c9 100644 --- a/crates/php/build.rs +++ b/crates/php/build.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, env, ffi::OsStr, fmt::{Debug, Display}, @@ -56,6 +56,8 @@ fn get_spc() -> PathBuf { } fn main() { + println!("cargo:rerun-if-changed=build.rs"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); let current_dir = env::current_dir().unwrap(); @@ -170,9 +172,11 @@ fn main() { let spc = get_spc(); let spc_cmd = spc.to_str().unwrap(); - // Skip download and build of PHP if a build is already present - // TODO: Probably need to detect modification date somehow... - if !current_dir.join("buildroot/lib/libphp.a").exists() { + let has_downloads = current_dir.join("downloads").exists(); + let should_download = env::var("PHP_SHOULD_DOWNLOAD") + .map_or(!has_downloads, |s| s == "true"); + + if should_download { // Download PHP and requested extensions execute_command(&[ spc_cmd, @@ -181,16 +185,29 @@ fn main() { "--retry=10", "--prefer-pre-built", "--with-php=8.4" - ]); + ], None); + } + // TODO: Build if downloads modification time is more recent than libphp.a + let has_libphp = current_dir.join("buildroot/lib/libphp.a").exists(); + let should_build = env::var("PHP_SHOULD_BUILD") + .map_or(!has_libphp, |s| s == "true"); + + if should_build { + let mut env = HashMap::new(); + env.insert( + "SPC_CMD_PREFIX_PHP_CONFIGURE".to_string(), + "./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-cgi --disable-phpdbg --enable-debug".to_string() + ); // Build in embed mode execute_command(&[ spc_cmd, "build", &extensions, "--build-embed", - // "--enable-zts" - ]); + "--enable-zts", + "--no-strip", // Keep debug symbols? + ], Some(env)); } // Get the includes @@ -199,7 +216,7 @@ fn main() { "spc-config", &extensions, "--includes" - ]); + ], None); // Get the libs let libs = execute_command(&[ @@ -207,10 +224,10 @@ fn main() { "spc-config", &extensions, "--libs" - ]); + ], None); // Include main headers - let includes = includes.split(' ').collect::>(); + let mut includes = includes.split(' ').collect::>(); for dir in includes.iter() { println!("cargo:include={}", &dir[2..]); } @@ -237,6 +254,17 @@ fn main() { } } + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + + let lang_handler_include = crate_dir.join("../../target/release"); + + println!("cargo:rustc-link-search={}", lang_handler_include.display()); + println!("cargo:rustc-link-lib=lang_handler"); + println!("cargo:include={}", lang_handler_include.display()); + + let lang_handler_include_flag = format!("-I{}", lang_handler_include.display()); + includes.push(lang_handler_include_flag.as_str()); + let mut builder = cc::Build::new(); for include in &includes { builder.flag(include); @@ -257,6 +285,12 @@ fn main() { .blocklist_function("zend_startup") // Block the `zend_random_bytes_insecure` because it fails checks. .blocklist_item("zend_random_bytes_insecure") + .opaque_type("lh_request_t") + .opaque_type("lh_response_t") + .opaque_type("lh_request_builder_t") + .opaque_type("lh_response_builder_t") + .opaque_type("lh_headers_t") + .opaque_type("lh_url_t") .clang_args(&includes) .derive_default(true); @@ -275,8 +309,11 @@ fn main() { .expect("Unable to write output file"); } -fn execute_command + Debug>(argv: &[S]) -> String { +fn execute_command + Debug>(argv: &[S], env: Option>) -> String { let mut command = Command::new(&argv[0]); + if let Some(env) = env { + command.envs(env); + } command.args(&argv[1..]); let result = command diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index e279ab59..318a9faa 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -1,96 +1,100 @@ -use std::{env::Args, ffi::{CStr, CString}}; +use std::{env::Args, ffi::{c_void, c_char, CStr, CString}}; -use crate::{sys, Request, Response}; +use lang_handler::{Handler, Request, Response}; -pub struct Embed; +use crate::sys; -fn args_to_c(args: Args) -> (i32, *mut *mut std::os::raw::c_char) { - let mut c_args = Vec::new(); - let mut c_ptrs = Vec::new(); - - for arg in args { - let c_arg = std::ffi::CString::new(arg).unwrap(); - let c_ptr = c_arg.clone().into_raw(); - c_args.push(c_arg); - c_ptrs.push(c_ptr); - } - - let c_args = c_args - .into_iter() - .map(|c_arg| c_arg.into_raw()) - .collect::>(); - - let mut c_ptrs = c_ptrs - .into_iter() - .collect::>(); - - (c_args.len() as i32, c_ptrs.as_mut_ptr()) +#[derive(Debug, Clone)] +pub struct Embed { + code: String, + filename: Option, } +unsafe impl Send for Embed {} +unsafe impl Sync for Embed {} + impl Embed { - pub fn new() -> Self { - Embed::new_with_c_args(0, std::ptr::null_mut()) + pub fn new(code: C, filename: Option) -> Self + where + C: Into, + F: Into + { + Embed::new_with_argv::(code, filename, vec![]) } - pub fn new_with_args(args: Args) -> Self { + pub fn new_with_args(code: C, filename: Option, args: Args) -> Self + where + C: Into, + F: Into + { let argv: Vec = args.collect(); - Embed::new_with_argv(argv) + Embed::new_with_argv(code, filename, argv) } - pub fn new_with_argv(argv: Vec) -> Self + pub fn new_with_argv(code: C, filename: Option, argv: Vec) -> Self where + C: Into, + F: Into, S: AsRef, { - let mut c_args = Vec::new(); - let mut c_ptrs = Vec::new(); - - for arg in argv { - let c_arg = CString::new(arg.as_ref()).unwrap(); - let c_ptr = c_arg.clone().into_raw(); - c_args.push(c_arg); - c_ptrs.push(c_ptr); - } - - let c_args = c_args + let argc = argv.len() as i32; + let argv = argv .into_iter() - .map(|c_arg| c_arg.into_raw()) + .map(|v| CString::new(v.as_ref()).unwrap()) .collect::>(); - let mut c_ptrs = c_ptrs - .into_iter() - .collect::>(); + let mut argv_ptrs = argv + .iter() + .map(|v| v.as_ptr() as *mut c_char) + .collect::>(); - Embed::new_with_c_args(c_args.len() as i32, c_ptrs.as_mut_ptr()) + unsafe { + sys::php_embed_init(argc, argv_ptrs.as_mut_ptr()); + // Teardown initial request as we will start them ourselves later + sys::php_request_shutdown(std::ptr::null_mut()); + sys::php_http_setup(); + } + + Embed { + code: code.into(), + filename: filename.map(|v| v.into()), + } } +} - fn new_with_c_args(argc: i32, argv: *mut *mut std::os::raw::c_char) -> Self { - unsafe { sys::php_embed_init(argc, argv); } - Embed +impl Drop for Embed { + fn drop(&mut self) { + unsafe { + sys::php_request_startup(); + sys::php_embed_shutdown(); + } } +} - pub fn handle_request(&self, code: C, filename: Option, request: Request) -> Response - where - C: AsRef, - F: Into, - { - let code = CString::new(code.as_ref()) +impl Handler for Embed { + type Error = String; + + fn handle(&self, request: Request) -> Result { + let code = CString::new(self.code.clone()) .unwrap(); - let filename = filename - .map(|v| CString::new(v.into())) + let filename = self.filename + .as_ref() + .map(|v| CString::new(v.clone())) .unwrap_or(CString::new("")) .unwrap(); - unsafe { - sys::php_http_handle_request(code.as_ptr(), filename.as_ptr(), *request) - }.into() - } -} + let mut request: lang_handler::lh_request_t = request.into(); + let request = &mut request as *mut _ as *mut sys::lh_request_t; -impl Drop for Embed { - fn drop(&mut self) { - unsafe { - sys::php_embed_shutdown(); - } + let response = unsafe { + sys::php_http_handle_request(code.as_ptr(), filename.as_ptr(), request) + }; + + let response = unsafe { + &*(response as *mut lang_handler::lh_response_t) + }; + + Ok(response.into()) } } diff --git a/crates/php/src/lib.rs b/crates/php/src/lib.rs index 8b341815..6938770f 100644 --- a/crates/php/src/lib.rs +++ b/crates/php/src/lib.rs @@ -11,11 +11,9 @@ // #![deny(clippy::all)] mod embed; -mod request; -mod response; mod sys; +pub use lang_handler::{Handler, Headers, Request, Response, RequestBuilder, Url}; + pub use embed::Embed; -pub use request::Request; -pub use response::Response; pub use self::sys::*; diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs index 790e01db..5f078cbc 100644 --- a/crates/php/src/main.rs +++ b/crates/php/src/main.rs @@ -1,24 +1,36 @@ -use php::{Embed, Request}; +use php::{Embed, Request, Handler}; pub fn main() { - let embed = Embed::new_with_args(std::env::args()); + let code = " + http_response_code(404); + header('Content-Type: text/html'); + print('hello'); + flush(); + "; + let filename = Some("test.php"); + let embed = Embed::new_with_args(code, filename, std::env::args()); + // let embed = Embed::new(code, filename); let request = Request::builder() - .method("GET") - .path("/test.php") + .method("POST") + .url("http://example.com/test.php").expect("invalid url") + .header("Content-Type", "text/html") + .header("Content-Length", 13.to_string()) .body("Hello, World!") .build(); + println!("=== request ==="); println!("method: {}", request.method()); - println!("path: {}", request.path()); - println!("body: {}", request.body()); + println!("url: {:?}", request.url()); + println!("headers: {:?}", request.headers()); + println!("body: {:?}", request.body()); + println!(""); - let response = embed.handle_request( - ";echo 'Hello, World!';", - Some("test.php"), - request.clone() - ); + let response = embed.handle(request.clone()).unwrap(); - println!("Request: {:?}", request); - println!("Response: {:?}", response); + println!("\n=== response ==="); + println!("status: {:?}", response.status()); + println!("headers: {:?}", response.headers()); + println!("body: {:?}", response.body()); + println!(""); } diff --git a/crates/php/src/php_wrapper.c b/crates/php/src/php_wrapper.c index 6637e4cb..cc992f86 100644 --- a/crates/php/src/php_wrapper.c +++ b/crates/php/src/php_wrapper.c @@ -1,6 +1,7 @@ // TODO: Review these dependencies. They were all auto-added by IDE completion. // There's likely consolidated headers to get things from. #include "SAPI.h" +#include "php.h" #include "php_main.h" #include "zend.h" #include "zend_alloc.h" @@ -10,6 +11,9 @@ #include "zend_property_hooks.h" #include "zend_types.h" #include "zend_variables.h" +#include "zend_exceptions.h" +#include "lang_handler.h" +#include "php_ini_builder.h" #include #include @@ -21,330 +25,143 @@ #include #include -typedef struct string_array { - size_t buffer_size; - size_t used_size; - size_t count; - char* buffer; - size_t* offsets; -} string_array; - -string_array* string_array_new(size_t initial_size) { - string_array* arr = (string_array*)malloc(sizeof(string_array)); - if (!arr) return NULL; - - arr->buffer_size = initial_size; - arr->used_size = 0; - arr->count = 0; - arr->buffer = (char*)malloc(initial_size * sizeof(char)); - arr->offsets = (size_t*)malloc(initial_size * sizeof(size_t)); - - if (!arr->buffer || !arr->offsets) { - free(arr->buffer); - free(arr->offsets); - free(arr); - return NULL; - } +typedef struct php_server_context_s { + int foo; +} php_server_context_t; - return arr; +int php_sapi_module_startup(sapi_module_struct* sapi_module) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Startup from %d\n", context->foo); + return SUCCESS; } -bool string_array_grow(string_array* arr, size_t new_size) { - char* new_buffer = (char*)realloc(arr->buffer, new_size * sizeof(char)); - size_t* new_offsets = (size_t*)realloc(arr->offsets, new_size * sizeof(size_t)); - - if (!new_buffer || !new_offsets) { - free(new_buffer); - free(new_offsets); - return false; - } - arr->buffer = new_buffer; - arr->offsets = new_offsets; - arr->buffer_size = new_size; - - return true; +int php_sapi_activate() { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Activate from %d\n", context->foo); + return SUCCESS; } -bool string_array_add(string_array* arr, const char* str) { - size_t str_len = strlen(str) + 1; // include the null terminator - - if (arr->used_size + str_len > arr->buffer_size) { - if (!string_array_grow(arr, (arr->buffer_size + str_len) * 2)) { - return false; - } - } - arr->offsets[arr->count] = arr->used_size; - strcpy(&arr->buffer[arr->used_size], str); - arr->used_size += str_len; - arr->count++; +int php_sapi_deactivate() { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Deactivate from %d\n", context->foo); + return SUCCESS; +} - return true; +size_t sapi_ub_write(const char *str, size_t str_length) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("%.*s from %d", (int)str_length, str, context->foo); + return str_length; } -const char* string_array_get(string_array* arr, size_t index) { - if (index >= arr->count) { - return NULL; + +void sapi_node_flush() { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Flush occurred from %d\n", context->foo); + if (!SG(headers_sent)) { + sapi_send_headers(); + SG(headers_sent) = 1; } - return &arr->buffer[arr->offsets[index]]; } -bool string_array_remove(string_array* arr, size_t index) { - if (index >= arr->count) { - return false; - } - size_t start_offset = arr->offsets[index]; - size_t next_offset = (index + 1 < arr->count) ? arr->offsets[index + 1] : arr->used_size; - size_t length_to_move = arr->used_size - next_offset; +// void sapi_send_header(sapi_header_struct *sapi_header, void *server_context) { +// // Not sure _why_ this is necessary, but it is. +// if (sapi_header == NULL) return; +// php_server_context_t* context = (php_server_context_t*)server_context; +// printf("Header: %s from %d\n", sapi_header->header, context->foo); +// } - memmove(&arr->buffer[start_offset], &arr->buffer[next_offset], length_to_move); - arr->used_size -= (next_offset - start_offset); +int php_sapi_send_headers(sapi_headers_struct *sapi_headers) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Headers sent from %d\n", context->foo); - for (size_t i = index; i < arr->count - 1; ++i) { - arr->offsets[i] = arr->offsets[i + 1] - (next_offset - start_offset); - } - arr->count--; + sapi_header_struct *h; + zend_llist_position pos; - return true; -} -void string_array_free(string_array* arr) { - free(arr->buffer); - free(arr->offsets); - free(arr); + h = zend_llist_get_first_ex(&sapi_headers->headers, &pos); + while (h) { + if ( h->header_len > 0 ) { + printf("Header: %s\n", h->header); + } + h = zend_llist_get_next_ex(&sapi_headers->headers, &pos); + } + return 0; } - -/** - * A header key/value pair. - */ -typedef struct php_http_header { - const char* key; - string_array* values; -} php_http_header; - -php_http_header* php_http_header_init(php_http_header* self, const char* key, string_array* values) { - self->key = key; - self->values = values; - return self; +// TODO: Read n bytes from request body in ctx, memcpy to buffer, return remaining bytes. +size_t php_sapi_read_post(char *buffer, size_t count_bytes) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Read post from %d\n", context->foo); + return 0; } -php_http_header* php_http_header_new(const char* key) { - php_http_header* self = (php_http_header*)malloc(sizeof(php_http_header)); - if (!self) return NULL; - string_array* values = string_array_new(1); - if (!values) return NULL; - - return php_http_header_init(self, key, values); -} -const char* php_http_header_key(php_http_header* self) { - return self->key; -} -string_array* php_http_header_values(php_http_header* self) { - return self->values; -} -bool php_http_header_add_value(php_http_header* self, const char* value) { - return string_array_add(self->values, value); -} -const char* php_http_header_get_value(php_http_header* self, size_t index) { - return string_array_get(self->values, index); -} -size_t php_http_header_value_count(php_http_header* self) { - return self->values->count; -} -bool php_http_header_remove_value(php_http_header* self, size_t index) { - return string_array_remove(self->values, index); -} -void php_http_header_free(php_http_header* self) { - free(self); +char* php_sapi_read_cookies() { + return SG(request_info).cookie_data; } -/** - * A collection of headers. - */ -typedef struct php_http_headers { - size_t allocated; - size_t count; - php_http_header* headers; -} php_http_headers; - -bool php_http_headers_grow(php_http_headers* self, size_t count) { - size_t new_size = (self->allocated + count) * sizeof(php_http_header); - self->headers = (php_http_header*)realloc(self->headers, new_size); - if (self->headers == NULL) return false; - self->allocated += count; - return true; +void php_register_server_variables(zval *track_vars_array) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Register server variables from %d\n", context->foo); } -php_http_headers* php_http_headers_init(php_http_headers* self) { - self->allocated = 0; - self->count = 0; - self->headers = NULL; - return self; + +void php_sapi_log_message(const char *message, int syslog_type_int) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Log message: %s from %d\n", message, context->foo); } -php_http_headers* php_http_headers_new(size_t count) { - php_http_headers* self = (php_http_headers*)malloc(sizeof(php_http_headers)); - if (self == NULL) return NULL; - php_http_headers_init(self); - if (count > 0 && !php_http_headers_grow(self, count)) { - free(self); - return NULL; - } +// static const char HARDCODED_INI[] = + // "log_errors=1\n" + // "implicit_flush=1\n" + // "memory_limit=128MB\n" + // "output_buffering=0\n"; - return self; -} -void php_http_headers_free(php_http_headers* self) { - free(self->headers); - free(self); -} -bool php_http_headers_has_room(php_http_headers* self, size_t count) { - return self->allocated - self->count >= count; -} -size_t php_http_headers_count(php_http_headers* self) { - return self->count; -} -php_http_header* php_http_headers_get(php_http_headers* self, size_t index) { - return &self->headers[index]; -} -int php_http_headers_find_index(php_http_headers* self, const char* key) { - for (int i = 0; i < (int)self->count; i++) { - if (strcmp(self->headers[i].key, key) == 0) { - return i; - } - } - return -1; -} -php_http_header* php_http_headers_find(php_http_headers* self, const char* key) { - int index = php_http_headers_find_index(self, key); - if (index == -1) return NULL; - return &self->headers[index]; -} -php_http_header* php_http_headers_remove(php_http_headers* self, const char* key) { - int i = php_http_headers_find_index(self, key); - if (i == -1) return NULL; +void php_http_setup() { + sapi_module.startup = php_sapi_module_startup; - php_http_header* header = &self->headers[i]; - for (int j = i; j < (int)self->count - 1; j++) { - self->headers[j] = self->headers[j + 1]; - } - self->count--; + sapi_module.activate = php_sapi_activate; + sapi_module.deactivate = php_sapi_deactivate; - return header; -} -php_http_headers* php_http_headers_push(php_http_headers* self, const char* key, const char* value) { - php_http_header* header = php_http_headers_find(self, key); - if (header != NULL) { - if (!php_http_header_add_value(header, value)) { - return NULL; - } - return self; - } + sapi_module.ub_write = sapi_ub_write; + sapi_module.flush = sapi_node_flush; - if (!php_http_headers_has_room(self, 1) && !php_http_headers_grow(self, 1)) { - return NULL; - } + sapi_module.sapi_error = php_error; - string_array* values = string_array_new(1); - if (!values) return NULL; + // sapi_module.send_header = sapi_send_header; + sapi_module.send_headers = php_sapi_send_headers; - string_array_add(values, value); - header = &self->headers[self->count++]; - php_http_header_init(header, key, values); + sapi_module.read_post = php_sapi_read_post; + sapi_module.read_cookies = php_sapi_read_cookies; - return self; -} + sapi_module.register_server_variables = php_register_server_variables; + sapi_module.log_message = php_sapi_log_message; -/** - * An incoming request. - */ -typedef struct php_http_request { - const char* method; - const char* path; - php_http_headers headers; - const char* body; -} php_http_request; - -php_http_request* php_http_request_init(php_http_request* self) { - self->method = NULL; - self->path = NULL; - php_http_headers_init(&self->headers); - self->body = NULL; - return self; + // struct php_ini_builder ini_builder; + // php_ini_builder_init(&ini_builder); + // php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); } -php_http_request* php_http_request_new() { - php_http_request* self = (php_http_request*)malloc(sizeof(php_http_request)); - if (self == NULL) return NULL; - return php_http_request_init(self); -} -void php_http_request_free(php_http_request* self) { - free((void*)self->method); - free((void*)self->path); - php_http_headers_free(&self->headers); - free((void*)self->body); - free(self); -} -bool php_http_request_set_method(php_http_request* self, const char* method) { - self->method = strdup(method); - return true; -} -const char* php_http_request_get_method(php_http_request* self) { - return self->method; -} -bool php_http_request_set_path(php_http_request* self, const char* path) { - self->path = strdup(path); - return true; -} -const char* php_http_request_get_path(php_http_request* self) { - return self->path; -} -bool php_http_request_set_body(php_http_request* self, const char* body) { - self->body = strdup(body); - return true; -} -const char* php_http_request_get_body(php_http_request* self) { - return self->body; -} +void clean_superglobals() { + // request + if (SG(request_info).request_method != NULL) { + lh_reclaim_str(SG(request_info).request_method); + } -/** - * An outgoing response. - */ -typedef struct php_http_response { - int status; - php_http_headers headers; - const char* body; -} php_http_response; - -php_http_response* php_http_response_init(php_http_response* self) { - self->status = 0; - php_http_headers_init(&self->headers); - self->body = NULL; - return self; -} -php_http_response* php_http_response_new() { - php_http_response* self = (php_http_response*)malloc(sizeof(php_http_response)); - if (self == NULL) return NULL; + // url + if (SG(request_info).path_translated != NULL) { + lh_reclaim_str(SG(request_info).path_translated); + } + if (SG(request_info).query_string != NULL) { + lh_reclaim_str(SG(request_info).query_string); + } + if (SG(request_info).request_uri != NULL) { + lh_reclaim_str(SG(request_info).request_uri); + } - return php_http_response_init(self); -} -void php_http_response_free(php_http_response* self) { - php_http_headers_free(&self->headers); - free((void*)self->body); - free(self); -} -bool php_http_response_set_status(php_http_response* self, int status) { - self->status = status; - return true; -} -int php_http_response_get_status(php_http_response* self) { - return self->status; -} -bool php_http_response_set_body(php_http_response* self, const char* body) { - self->body = strdup(body); - return true; -} -const char* php_http_response_get_body(php_http_response* self) { - return self->body; -} -php_http_headers* php_http_response_get_headers(php_http_response* self) { - return &self->headers; + // headers + if (SG(request_info).content_type != NULL) { + lh_reclaim_str(SG(request_info).content_type); + } + if (SG(request_info).cookie_data != NULL) { + lh_reclaim_str(SG(request_info).cookie_data); + } } /** @@ -365,78 +182,89 @@ php_http_headers* php_http_response_get_headers(php_http_response* self) { * Each SAPI request is handled in an isolated PHP context, but code compilation * can be shared making spin up quick. Each of these contexts is single-threaded. */ -php_http_response* php_http_handle_request(const char* code, const char* filename, php_http_request* request) { - php_http_response* response = php_http_response_new(); - if (response == NULL) return NULL; - - // Teardown initial request. - php_request_shutdown((void*) 0); - - // Set up $_SERVER values. - // SG(request_info).script_filename = estrdup(filename); - // SG(request_info).php_self = estrdup(request->path); - SG(request_info).request_method = estrdup(request->method); - // SG(request_info).request_body = - SG(request_info).path_translated = estrdup(request->path); - // SG(request_info).query_string = estrdup(request->query_string); - // SG(request_info).request_uri = estrdup(request->request_uri); - // SG(request_info).cookie_data = estrdup(request->cookie_data); - // SG(request_info).content_type = estrdup(request->content_type); - // SG(request_info).content_length = estrdup(request->content_length); - // SG(request_info).server_software = estrdup(request->server_software); // wattpm? - // SG(request_info).gateway_interface = estrdup(request->gateway_interface); // CGI/1.1 - // SG(request_info).request_time_float = request->request_time_float; - // SG(request_info).document_root = estrdup(request->document_root); - // SG(request_info).remote_addr = estrdup(request->remote_addr); - // SG(request_info).remote_host = estrdup(request->remote_host); - // SG(request_info).remote_port = estrdup(request->remote_port); - // SG(request_info).remote_user = estrdup(request->remote_user); - // SG(request_info).server_port = estrdup(request->server_port); - // SG(request_info).script_name = estrdup(request->script_name); - // SG(request_info).php_auth_digest = estrdup(request->php_auth_digest); - // SG(request_info).php_auth_user = estrdup(request->php_auth_user); - // SG(request_info).php_auth_pw = estrdup(request->php_auth_pw); - // SG(request_info).auth_type = estrdup(request->auth_type); - // SG(request_info).path_info = estrdup(request->path_info); - - // SG(server_context) needs to be non-zero size because...reasons. ¯\_(ツ)_/¯ - SG(server_context) = (void*)(1); // Sigh. +lh_response_t* php_http_handle_request(const char* code, const char* filename, lh_request_t* request) { + // This is where we store the stuff for associating callbacks with this request. + // TODO: This should probably contain the request and response objects. + php_server_context_t* context = malloc(sizeof(php_server_context_t)); + context->foo = 555; + SG(server_context) = context; - // Needs to be set _after_ php_request_startup, also because reasons. - SG(request_info).proto_num = 110; + SG(options) |= SAPI_OPTION_NO_CHDIR; + SG(headers_sent) = 0; + + SG(request_info).argc = 0; + SG(request_info).argv = NULL; + + // Reset state + SG(sapi_headers).http_response_code = 200; + + // Set up superglobals + SG(request_info).request_method = lh_request_method(request); + + lh_url_t* url = lh_request_url(request); + SG(request_info).path_translated = (char*) lh_url_path(url); + SG(request_info).query_string = (char*) lh_url_query(url); + SG(request_info).request_uri = (char*) lh_url_uri(url); + // TODO: Add auth fields + + // Could implement a PHP stream to do this? + // SG(request_info).request_body = lh_request_body(request); + + lh_headers_t* headers = lh_request_headers(request); + + const char* content_type = lh_headers_get(headers, "Content-Type"); + if (content_type == NULL) { + SG(request_info).content_type = content_type; + } + + const char* content_length = lh_headers_get(headers, "Content-Length"); + if (content_length != NULL) { + SG(request_info).content_length = strtoll(content_length, NULL, 10); + } + + const char* cookie = lh_headers_get(headers, "Cookie"); + SG(request_info).cookie_data = (char*) cookie; // Start new request now that we've setup the environment fully. if (php_request_startup() == FAILURE) { return NULL; } - // php_embed_module.flush = + // Needs to be set _after_ php_request_startup, also because reasons. + SG(request_info).proto_num = 110; zval retval; - zend_try { - // size_t len = strlen(code); - // zend_eval_stringl_ex((char*)code, len, &retval, filename, true); - - zend_eval_string((char*)code, NULL, filename); + zend_first_try { + printf("code: %s\n", code); + zend_eval_string_ex((char*)code, &retval, filename, false); if (EG(exception)) { - // Can't call zend_clear_exception because there isn't a current - // execution stack (ie, `EG(current_execute_data)`) - zend_object* e = EG(exception); + zval rv; + // TODO: Figure out why this fails. + zval *msg = zend_read_property_ex(zend_ce_exception, EG(exception), ZSTR_KNOWN(ZEND_STR_MESSAGE), /* silent */ false, &rv); + zend_printf("Exception: %s\n", Z_STRVAL_P(msg)); + zend_object_release(EG(exception)); EG(exception) = NULL; - // TODO: do something with the error... - zval_ptr_dtor((zval*)e); } } zend_catch { return NULL; } zend_end_try(); - // Populate response fields - php_http_headers_push(&response->headers, "Content-Type", "text/plain"); - php_http_response_set_status(response, SG(sapi_headers).http_response_code); + zend_try { + php_request_shutdown(NULL); + } zend_end_try(); + clean_superglobals(); - // TODO: Need to figure out how php://output works. - response->body = "Hello, World!"; + // Reset headers to reuse for response object + lh_headers_free(headers); + headers = lh_headers_new(); + + const char* mime = SG(sapi_headers).mimetype; + if (mime == NULL) { + mime = "text/plain"; + } + lh_headers_set(headers, "Content-Type", mime); - return response; + int status = SG(sapi_headers).http_response_code; + return lh_response_new(status, headers, "Hello, World!"); } diff --git a/crates/php/src/request.rs b/crates/php/src/request.rs deleted file mode 100644 index 792b5ec4..00000000 --- a/crates/php/src/request.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::{env::Args, ffi::{CStr, CString}, ops::Deref}; - -use crate::sys; - -#[derive(Debug, Clone)] -pub struct Request { - request: *mut sys::php_http_request -} - -impl Deref for Request { - type Target = *mut sys::php_http_request; - - fn deref(&self) -> &Self::Target { - &self.request - } -} - -impl From<*mut sys::php_http_request> for Request { - fn from(request: *mut sys::php_http_request) -> Self { - Request { request } - } -} - -impl Request { - pub fn new() -> Self { - unsafe { sys::php_http_request_new() }.into() - } - - pub fn builder() -> RequestBuilder { - RequestBuilder::new() - } - - // Method - pub fn set_method(&self, method: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(method.as_ref()).unwrap(); - sys::php_http_request_set_method(self.request, str.as_ptr()); - } - } - pub fn method(&self) -> String { - unsafe { - let method = sys::php_http_request_get_method(self.request); - CStr::from_ptr(method).to_string_lossy().into_owned() - } - } - - // URI - pub fn set_path(&self, path: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(path.as_ref()).unwrap(); - sys::php_http_request_set_path(self.request, str.as_ptr()); - } - } - pub fn path(&self) -> String { - unsafe { - let path = sys::php_http_request_get_path(self.request); - CStr::from_ptr(path).to_string_lossy().into_owned() - } - } - - // Body - // TODO: Streaming bodies with futures::Stream - pub fn set_body(&self, body: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(body.as_ref()).unwrap(); - sys::php_http_request_set_body(self.request, str.as_ptr()); - } - } - pub fn body(&self) -> String { - unsafe { - let body = sys::php_http_request_get_body(self.request); - CStr::from_ptr(body).to_string_lossy().into_owned() - } - } -} - -pub struct RequestBuilder { - request: Request -} - -impl RequestBuilder { - pub fn new() -> Self { - RequestBuilder { - request: Request::new() - } - } - - pub fn method(self, method: T) -> Self - where - T: AsRef - { - self.request.set_method(method); - self - } - - pub fn path(self, path: T) -> Self - where - T: AsRef - { - self.request.set_path(path); - self - } - - pub fn body(self, body: T) -> Self - where - T: AsRef - { - self.request.set_body(body); - self - } - - pub fn build(self) -> Request { - self.request - } -} diff --git a/crates/php/src/response.rs b/crates/php/src/response.rs deleted file mode 100644 index 2a8cb235..00000000 --- a/crates/php/src/response.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{env::Args, ffi::{CStr, CString}, ops::Deref}; - -use crate::sys; - -#[derive(Debug, Clone)] -pub struct Response { - response: *mut sys::php_http_response -} - -impl Deref for Response { - type Target = *mut sys::php_http_response; - - fn deref(&self) -> &Self::Target { - &self.response - } -} - -impl From<*mut sys::php_http_response> for Response { - fn from(response: *mut sys::php_http_response) -> Self { - Response { - response - } - } -} - -impl Response { - pub fn status(&self) -> i32 { - unsafe { - sys::php_http_response_get_status(self.response) - } - } - - pub fn set_status(&self, status: i32) { - unsafe { - sys::php_http_response_set_status(self.response, status); - } - } - - pub fn body(&self) -> String { - unsafe { - let body = sys::php_http_response_get_body(self.response); - CStr::from_ptr(body).to_string_lossy().into_owned() - } - } - - pub fn set_body(&self, body: T) - where - T: AsRef - { - unsafe { - let str: CString = CString::new(body.as_ref()).unwrap(); - sys::php_http_response_set_body(self.response, str.as_ptr()); - } - } -} diff --git a/crates/php_node/src/lib.rs b/crates/php_node/src/lib.rs index 4ed698c6..6effed04 100644 --- a/crates/php_node/src/lib.rs +++ b/crates/php_node/src/lib.rs @@ -1,35 +1,243 @@ #[macro_use] extern crate napi_derive; -use php::{Embed, Request}; +use napi::{Error, Task, Env, Result}; +use napi::bindgen_prelude::*; + +use php::{Embed, Handler, Headers, Request, Response, RequestBuilder}; + +pub struct Entry(K, V); + +// This represents a map entries key/value pair. +impl ToNapiValue for Entry +where + T1: ToNapiValue, + T2: ToNapiValue, +{ + unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result { + let Entry(key, value) = val; + let key_napi_value = T1::to_napi_value(env, key)?; + let value_napi_value = T2::to_napi_value(env, value)?; + + let mut result: napi::sys::napi_value = std::ptr::null_mut(); + unsafe { + check_status!( + napi::sys::napi_create_array_with_length(env, 2, &mut result), + "Failed to create entry key/value pair" + )?; + + check_status!( + napi::sys::napi_set_element(env, result, 0, key_napi_value), + "Failed to set entry key" + )?; + + check_status!( + napi::sys::napi_set_element(env, result, 1, value_napi_value), + "Failed to set entry value" + )?; + }; + + Ok(result) + } +} + +#[napi(js_name = "Headers")] +pub struct PhpHeaders { + headers: Headers +} #[napi] -pub fn handle_request() { - let embed = Embed::new(); - - let request = Request::builder() - .method("GET") - .path("/test.php") - .body("Hello, World!") - .build(); - - println!("=== request ==="); - println!("method: {}", request.method()); - println!("path: {}", request.path()); - println!("body: {}", request.body()); - println!(""); - - let response = embed.handle_request( - " - echo 'Hello, World!\n'; - http_response_code(400); - echo $_SERVER['REQUEST_METHOD'] . '\n'; - ", - Some("test.php"), - request.clone() - ); - - println!("\n=== response ==="); - println!("status: {:?}", response.status()); - println!("body: {:?}", response.body()); +impl PhpHeaders { + #[napi] + pub fn get(&self, key: String) -> Option> { + self.headers.get(&key).map(|v| v.to_owned()) + } + + #[napi] + pub fn set(&mut self, key: String, value: String) { + self.headers.set(key, value) + } + + #[napi] + pub fn remove(&mut self, key: String) { + self.headers.remove(&key) + } + + #[napi] + pub fn entries(&self) -> Vec>> { + self.headers.iter().map(|(k, v)| Entry(k.to_owned(), v.to_owned())).collect() + } + + #[napi] + pub fn keys(&self) -> Vec { + self.headers.iter().map(|(k, _)| k.to_owned()).collect() + } + + #[napi] + pub fn values(&self) -> Vec { + self.headers.iter_values().map(|v| v.to_owned()).collect() + } +} + +#[napi(object)] +#[derive(Default)] +pub struct PhpRequestOptions { + pub method: Option, + pub url: Option, + pub headers: Option, + pub body: Option +} + +#[napi(js_name = "Request")] +pub struct PhpRequest { + request: Request +} + +#[napi] +impl PhpRequest { + #[napi(constructor)] + pub fn new(options: Option) -> Self { + let opts = options.unwrap_or_default(); + let mut builder: RequestBuilder = Request::builder(); + + if let Some(method) = opts.method { + builder = builder.method(method) + } + + if let Some(url) = opts.url { + builder = builder.url(url).expect("invalid url") + } + + if let Some(headers) = opts.headers { + for key in Object::keys(&headers).unwrap() { + let values: Vec = headers.get(&key).unwrap().unwrap(); + for value in values { + builder = builder.header(key.clone(), value.clone()) + } + } + } + + if let Some(body) = opts.body { + builder = builder.body(body) + } + + PhpRequest { + request: builder.build() + } + } + + #[napi(getter, enumerable = true)] + pub fn method(&self) -> String { + self.request.method().to_owned() + } + + #[napi(getter, enumerable = true)] + pub fn url(&self) -> String { + self.request + .url() + .as_str() + .to_owned() + } + + #[napi(getter, enumerable = true)] + pub fn headers(&self) -> PhpHeaders { + PhpHeaders { + headers: self.request.headers().clone() + } + } + + #[napi(getter, enumerable = true)] + pub fn body(&self) -> String { + self.request + .body() + .map(|v| v.to_owned()) + .unwrap_or_default() + } +} + +impl PhpRequest { + fn to_inner(&self) -> Request { + self.request.clone() + } +} + +#[napi(object)] +#[derive(Clone, Default)] +pub struct PhpOptions { + pub code: String, + pub file: Option +} + +#[napi] +pub struct Php { + pub options: PhpOptions +} + +#[napi] +impl Php { + #[napi(constructor)] + pub fn new(options: PhpOptions) -> Self { + Php { options } + } + + #[napi] + pub fn handle_request(&self, request: &PhpRequest) -> AsyncTask { + AsyncTask::new(PhpRequestTask { + options: self.options.clone(), + request: request.to_inner() + }) + } +} + +pub struct PhpRequestTask { + options: PhpOptions, + request: Request +} + +impl Task for PhpRequestTask { + type Output = Response; + type JsValue = PhpResponse; + + fn compute(&mut self) -> Result { + let code = self.options.code.clone(); + let filename = self.options.file.clone(); + // TODO: Need to figure out how to send an Embed across threads + // so we can reuse the same Embed instance for multiple requests. + let embed = Embed::new(code, filename); + + embed + .handle(self.request.clone()) + .map_err(|err| Error::from_reason(err)) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(PhpResponse { + response: output + }) + } +} + +#[napi] +pub struct PhpResponse { + response: Response +} + +#[napi] +impl PhpResponse { + #[napi(getter, enumerable = true)] + pub fn status(&self) -> u32 { + self.response.status() as u32 + } + + #[napi(getter, enumerable = true)] + pub fn headers(&self) -> PhpHeaders { + PhpHeaders { + headers: self.response.headers().clone() + } + } + + #[napi(getter, enumerable = true)] + pub fn body(&self) -> String { + self.response.body().to_owned() + } } diff --git a/index.d.ts b/index.d.ts index 8c2ea76f..adbb3b41 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,4 +3,28 @@ /* auto-generated by NAPI-RS */ -export declare function handleRequest(): void +export interface PhpRequestOptions { + method?: string + url?: string + body?: string +} +export interface PhpOptions { + code: string + file?: string +} +export type PhpRequest = Request +export declare class Request { + constructor(options?: PhpRequestOptions | undefined | null) + get method(): string + get url(): string + get body(): string +} +export declare class Php { + options: PhpOptions + constructor(options: PhpOptions) + handleRequest(request: Request): Promise +} +export declare class PhpResponse { + get status(): number + get body(): string +} diff --git a/index.js b/index.js index a75ed774..6b0a3d29 100644 --- a/index.js +++ b/index.js @@ -310,6 +310,8 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { handleRequest } = nativeBinding +const { Request, Php, PhpResponse } = nativeBinding -module.exports.handleRequest = handleRequest +module.exports.Request = Request +module.exports.Php = Php +module.exports.PhpResponse = PhpResponse From 7d4bc89023b7ade59b48fb46105f06d66ba62171 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 7 Mar 2025 23:30:49 +0800 Subject: [PATCH 2/9] Got input and output streams working. Need to wire them up to something now... --- crates/php/src/embed.rs | 12 +-- crates/php/src/main.rs | 17 +++- crates/php/src/php_wrapper.c | 189 +++++++++++++++++++++++++---------- 3 files changed, 154 insertions(+), 64 deletions(-) diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index 318a9faa..cc9b5a25 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -49,10 +49,7 @@ impl Embed { .collect::>(); unsafe { - sys::php_embed_init(argc, argv_ptrs.as_mut_ptr()); - // Teardown initial request as we will start them ourselves later - sys::php_request_shutdown(std::ptr::null_mut()); - sys::php_http_setup(); + sys::php_http_init(argc, argv_ptrs.as_mut_ptr()); } Embed { @@ -65,8 +62,7 @@ impl Embed { impl Drop for Embed { fn drop(&mut self) { unsafe { - sys::php_request_startup(); - sys::php_embed_shutdown(); + sys::php_http_shutdown(); } } } @@ -91,6 +87,10 @@ impl Handler for Embed { sys::php_http_handle_request(code.as_ptr(), filename.as_ptr(), request) }; + if response.is_null() { + return Err("Failed to handle request".into()); + } + let response = unsafe { &*(response as *mut lang_handler::lh_response_t) }; diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs index 5f078cbc..17542906 100644 --- a/crates/php/src/main.rs +++ b/crates/php/src/main.rs @@ -2,11 +2,21 @@ use php::{Embed, Request, Handler}; pub fn main() { let code = " - http_response_code(404); - header('Content-Type: text/html'); + http_response_code(123); + + // foreach ($_SERVER as $name => $value) { + // header(\"$name: $value\"); + // } + + echo file_get_contents(\"php://input\"); + print('hello'); flush(); "; + // let code = " + // http_response_code(123); + // echo $HTTP_RAW_POST_DATA; + // "; let filename = Some("test.php"); let embed = Embed::new_with_args(code, filename, std::env::args()); // let embed = Embed::new(code, filename); @@ -26,7 +36,8 @@ pub fn main() { println!("body: {:?}", request.body()); println!(""); - let response = embed.handle(request.clone()).unwrap(); + let response = embed.handle(request.clone()) + .expect("failed to handle request"); println!("\n=== response ==="); println!("status: {:?}", response.status()); diff --git a/crates/php/src/php_wrapper.c b/crates/php/src/php_wrapper.c index cc992f86..cd8419b1 100644 --- a/crates/php/src/php_wrapper.c +++ b/crates/php/src/php_wrapper.c @@ -29,27 +29,69 @@ typedef struct php_server_context_s { int foo; } php_server_context_t; -int php_sapi_module_startup(sapi_module_struct* sapi_module) { +void clean_superglobals() { + // request + if (SG(request_info).request_method != NULL) { + lh_reclaim_str(SG(request_info).request_method); + } + + // url + if (SG(request_info).path_translated != NULL) { + lh_reclaim_str(SG(request_info).path_translated); + } + if (SG(request_info).query_string != NULL) { + lh_reclaim_str(SG(request_info).query_string); + } + if (SG(request_info).request_uri != NULL) { + lh_reclaim_str(SG(request_info).request_uri); + } + + // headers + if (SG(request_info).content_type != NULL) { + lh_reclaim_str(SG(request_info).content_type); + } + if (SG(request_info).cookie_data != NULL) { + lh_reclaim_str(SG(request_info).cookie_data); + } +} + +void php_http_setup(); + +int php_sapi_startup(sapi_module_struct* sapi_module) { php_server_context_t* context = (php_server_context_t*)SG(server_context); printf("Startup from %d\n", context->foo); + php_http_setup(); + + // TODO: Make our own module rather than using the SAPI Embed one? + // php_module_startup(sapi_module, NULL, 0); + + return SUCCESS; +} + +int php_sapi_shutdown(sapi_module_struct* sapi_module) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Shutdown from %d\n", context->foo); + clean_superglobals(); return SUCCESS; } int php_sapi_activate() { php_server_context_t* context = (php_server_context_t*)SG(server_context); + if (!context) return SUCCESS; printf("Activate from %d\n", context->foo); return SUCCESS; } int php_sapi_deactivate() { php_server_context_t* context = (php_server_context_t*)SG(server_context); + if (!context) return SUCCESS; printf("Deactivate from %d\n", context->foo); return SUCCESS; } size_t sapi_ub_write(const char *str, size_t str_length) { php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("%.*s from %d", (int)str_length, str, context->foo); + printf("%.*s from %d\n", (int)str_length, str, context->foo); return str_length; } @@ -62,12 +104,20 @@ void sapi_node_flush() { } } -// void sapi_send_header(sapi_header_struct *sapi_header, void *server_context) { -// // Not sure _why_ this is necessary, but it is. -// if (sapi_header == NULL) return; -// php_server_context_t* context = (php_server_context_t*)server_context; -// printf("Header: %s from %d\n", sapi_header->header, context->foo); -// } +void php_sapi_error(int type, const char *error_msg, ...) { + va_list args; + va_start(args, error_msg); + php_server_context_t* context = (php_server_context_t*)SG(server_context); + printf("Error: %s from %d\n", error_msg, context->foo); + va_end(args); +} + +void sapi_send_header(sapi_header_struct *sapi_header, void *server_context) { + // Not sure _why_ this is necessary, but it is. + if (sapi_header == NULL) return; + php_server_context_t* context = (php_server_context_t*)server_context; + printf("Header: %s from %d\n", sapi_header->header, context->foo); +} int php_sapi_send_headers(sapi_headers_struct *sapi_headers) { php_server_context_t* context = (php_server_context_t*)SG(server_context); @@ -86,11 +136,23 @@ int php_sapi_send_headers(sapi_headers_struct *sapi_headers) { return 0; } +static bool sent_post_data = false; + // TODO: Read n bytes from request body in ctx, memcpy to buffer, return remaining bytes. +// This needs to block until it receives data, so will need some synchronization mechanism. size_t php_sapi_read_post(char *buffer, size_t count_bytes) { php_server_context_t* context = (php_server_context_t*)SG(server_context); printf("Read post from %d\n", context->foo); - return 0; + + if (sent_post_data) { + return 0; + } + + sent_post_data = true; + + memcpy(buffer, "Hello, from read_post!", 22); + + return 22; } char* php_sapi_read_cookies() { @@ -100,6 +162,7 @@ char* php_sapi_read_cookies() { void php_register_server_variables(zval *track_vars_array) { php_server_context_t* context = (php_server_context_t*)SG(server_context); printf("Register server variables from %d\n", context->foo); + php_import_environment_variables(track_vars_array); } void php_sapi_log_message(const char *message, int syslog_type_int) { @@ -107,61 +170,78 @@ void php_sapi_log_message(const char *message, int syslog_type_int) { printf("Log message: %s from %d\n", message, context->foo); } -// static const char HARDCODED_INI[] = - // "log_errors=1\n" - // "implicit_flush=1\n" - // "memory_limit=128MB\n" - // "output_buffering=0\n"; +zend_result php_get_request_time(double* ts) { + php_server_context_t* context = (php_server_context_t*)SG(server_context); + *ts = 0.0; + return SUCCESS; +} + +static const char HARDCODED_INI[] = + "display_errors=0\n" + "register_argc_argv=1\n" + "log_errors=1\n" + "implicit_flush=1\n" + "memory_limit=128MB\n" + "output_buffering=0\n"; void php_http_setup() { - sapi_module.startup = php_sapi_module_startup; + php_embed_module.name = "php-lang-handler"; + php_embed_module.pretty_name = "PHP Language Handler"; + + php_embed_module.startup = php_sapi_startup; + php_embed_module.shutdown = php_sapi_shutdown; + + php_embed_module.activate = php_sapi_activate; + php_embed_module.deactivate = php_sapi_deactivate; - sapi_module.activate = php_sapi_activate; - sapi_module.deactivate = php_sapi_deactivate; + php_embed_module.ub_write = sapi_ub_write; + php_embed_module.flush = sapi_node_flush; - sapi_module.ub_write = sapi_ub_write; - sapi_module.flush = sapi_node_flush; + php_embed_module.sapi_error = php_sapi_error; - sapi_module.sapi_error = php_error; + php_embed_module.send_header = sapi_send_header; + // php_embed_module.send_headers = php_sapi_send_headers; + php_embed_module.send_headers = NULL; - // sapi_module.send_header = sapi_send_header; - sapi_module.send_headers = php_sapi_send_headers; + php_embed_module.read_post = php_sapi_read_post; + php_embed_module.read_cookies = php_sapi_read_cookies; - sapi_module.read_post = php_sapi_read_post; - sapi_module.read_cookies = php_sapi_read_cookies; + php_embed_module.register_server_variables = php_register_server_variables; + php_embed_module.log_message = php_sapi_log_message; - sapi_module.register_server_variables = php_register_server_variables; - sapi_module.log_message = php_sapi_log_message; + php_embed_module.get_request_time = php_get_request_time; - // struct php_ini_builder ini_builder; - // php_ini_builder_init(&ini_builder); - // php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); + struct php_ini_builder ini_builder; + php_ini_builder_init(&ini_builder); + php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); } -void clean_superglobals() { - // request - if (SG(request_info).request_method != NULL) { - lh_reclaim_str(SG(request_info).request_method); - } +zend_result php_http_init(int argc, char **argv) { +#ifdef ZTS + php_tsrm_startup(); +#endif - // url - if (SG(request_info).path_translated != NULL) { - lh_reclaim_str(SG(request_info).path_translated); - } - if (SG(request_info).query_string != NULL) { - lh_reclaim_str(SG(request_info).query_string); - } - if (SG(request_info).request_uri != NULL) { - lh_reclaim_str(SG(request_info).request_uri); - } + php_http_setup(); + sapi_startup(&php_embed_module); - // headers - if (SG(request_info).content_type != NULL) { - lh_reclaim_str(SG(request_info).content_type); - } - if (SG(request_info).cookie_data != NULL) { - lh_reclaim_str(SG(request_info).cookie_data); - } + if (php_module_startup(&sapi_module, NULL) == FAILURE) { +#ifdef ZTS + tsrm_shutdown(); +#endif + return FAILURE; + } + + return SUCCESS; +} + +zend_result php_http_shutdown() { + php_module_shutdown(); + + sapi_shutdown(); + +#ifdef ZTS + tsrm_shutdown(); +#endif } /** @@ -233,10 +313,9 @@ lh_response_t* php_http_handle_request(const char* code, const char* filename, l // Needs to be set _after_ php_request_startup, also because reasons. SG(request_info).proto_num = 110; - zval retval; zend_first_try { - printf("code: %s\n", code); - zend_eval_string_ex((char*)code, &retval, filename, false); + size_t len = strlen(code); + zend_eval_stringl_ex((char*)code, len, NULL, filename, false); if (EG(exception)) { zval rv; @@ -245,6 +324,7 @@ lh_response_t* php_http_handle_request(const char* code, const char* filename, l zend_printf("Exception: %s\n", Z_STRVAL_P(msg)); zend_object_release(EG(exception)); EG(exception) = NULL; + EG(exit_status) = 1; } } zend_catch { return NULL; @@ -253,7 +333,6 @@ lh_response_t* php_http_handle_request(const char* code, const char* filename, l zend_try { php_request_shutdown(NULL); } zend_end_try(); - clean_superglobals(); // Reset headers to reuse for response object lh_headers_free(headers); From 8bd03050e3201cc76fbaa209c6a5a60e4ff22184 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Wed, 12 Mar 2025 22:57:06 +0800 Subject: [PATCH 3/9] Got I/O streams, logs, and exceptions working --- __test__/index.spec.mjs | 75 +++++- crates/lang_handler/Cargo.toml | 1 + crates/lang_handler/src/request.rs | 47 ++-- crates/lang_handler/src/response.rs | 106 ++++++-- crates/php/Cargo.toml | 2 + crates/php/build.rs | 2 +- crates/php/src/embed.rs | 67 +++-- crates/php/src/php_wrapper.c | 372 +++++++++++++--------------- crates/php_node/src/lib.rs | 96 ++++--- index.d.ts | 21 +- index.js | 3 +- yarn.lock | 18 +- 12 files changed, 479 insertions(+), 331 deletions(-) diff --git a/__test__/index.spec.mjs b/__test__/index.spec.mjs index 2b49ba1d..221cb260 100644 --- a/__test__/index.spec.mjs +++ b/__test__/index.spec.mjs @@ -2,13 +2,71 @@ import test from 'ava' import { Php, Request } from '../index.js' -test('sum from native', async (t) => { +test('input/output streams work', async (t) => { const php = new Php({ + argv: process.argv, file: 'index.php', code: ` - http_response_code(400); + if (file_get_contents('php://input') == 'Hello, from Node.js!') { + echo 'Hello, from PHP!'; + } + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php', + body: Buffer.from('Hello, from Node.js!') + }) + + const res = await php.handleRequest(req) + t.is(res.status, 200) + t.is(res.body.toString('utf8'), 'Hello, from PHP!') +}) + +test('logs work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + error_log('Hello, from error_log!'); + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php' + }) - echo phpinfo(); + const res = await php.handleRequest(req) + t.is(res.status, 200) + t.is(res.log.toString('utf8'), 'Hello, from error_log!\n') +}) + +test('exceptions work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + throw new Exception('Hello, from PHP!'); + ` + }) + + const req = new Request({ + method: 'GET', + url: 'http://example.com/test.php' + }) + + await t.throwsAsync(php.handleRequest(req), { + message: 'Hello, from PHP!' + }) +}) + +test('input and output headers work', async (t) => { + const php = new Php({ + file: 'index.php', + code: ` + if ($_SERVER['HTTP_X_TEST'] == 'Hello, from Node.js!') { + header('X-Test: Hello, from PHP!'); + } ` }) @@ -16,13 +74,12 @@ test('sum from native', async (t) => { method: 'GET', url: 'http://example.com/test.php', headers: { - 'Content-Type': 'application/json', - 'Content-Length': 13 - }, - body: 'Hello, World!' + 'X-Test': ['Hello, from Node.js!'] + } }) const res = await php.handleRequest(req) - t.is(res.status, 400) - t.is(res.body, "wat") + console.log(res) + t.is(res.status, 200) + // t.is(res.headers['X-Test'], 'Hello, from PHP!') }) diff --git a/crates/lang_handler/Cargo.toml b/crates/lang_handler/Cargo.toml index 699f3314..df1d4cfd 100644 --- a/crates/lang_handler/Cargo.toml +++ b/crates/lang_handler/Cargo.toml @@ -16,4 +16,5 @@ c = [] cbindgen = "0.28.0" [dependencies] +bytes = "1.10.1" url = "2.5.4" diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs index 9286d185..b0be10ff 100644 --- a/crates/lang_handler/src/request.rs +++ b/crates/lang_handler/src/request.rs @@ -1,6 +1,7 @@ #[cfg(feature = "c")] use std::{ffi, ffi::{CStr, CString}}; +use bytes::{Buf, Bytes, BytesMut}; use url::{ParseError, Url}; use crate::Headers; @@ -13,16 +14,16 @@ pub struct Request { url: Url, headers: Headers, // TODO: Support Stream bodies when napi.rs supports it - body: Option, + body: Bytes, } impl Request { - pub fn new(method: String, url: Url, headers: Headers, body: Option) -> Self { + pub fn new>(method: String, url: Url, headers: Headers, body: T) -> Self { Self { method, url, headers, - body + body: body.into() } } @@ -46,8 +47,8 @@ impl Request { &self.headers } - pub fn body(&self) -> Option<&String> { - self.body.as_ref() + pub fn body(&self) -> Bytes { + self.body.clone() } } @@ -56,7 +57,7 @@ pub struct RequestBuilder { method: Option, url: Option, headers: Headers, - body: Option, + body: BytesMut, } impl RequestBuilder { @@ -65,7 +66,7 @@ impl RequestBuilder { method: None, url: None, headers: Headers::new(), - body: None, + body: BytesMut::with_capacity(1024), } } @@ -74,7 +75,7 @@ impl RequestBuilder { method: Some(request.method().into()), url: Some(request.url().clone()), headers: request.headers().clone(), - body: request.body().map(|b| b.into()), + body: BytesMut::from(request.body()), } } @@ -105,8 +106,8 @@ impl RequestBuilder { self } - pub fn body>(mut self, body: T) -> Self { - self.body = Some(body.into()); + pub fn body>(mut self, body: T) -> Self { + self.body = body.into(); self } @@ -115,7 +116,7 @@ impl RequestBuilder { method: self.method.unwrap_or_else(|| "GET".to_string()), url: self.url.unwrap_or_else(|| Url::parse("/").unwrap()), headers: self.headers, - body: self.body, + body: self.body.freeze(), } } } @@ -151,10 +152,10 @@ pub extern "C" fn lh_request_new( let body = if body.is_null() { None } else { - Some(unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }) + Some(unsafe { CStr::from_ptr(body).to_bytes() }) }; let headers = unsafe { &*headers }; - let request = Request::new(method, url, headers.into(), body); + let request = Request::new(method, url, headers.into(), body.unwrap_or(&[])); Box::into_raw(Box::new(request.into())) } @@ -183,10 +184,22 @@ pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_he #[no_mangle] pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { let request = unsafe { &*request }; - match request.inner.body() { - Some(body) => CString::new(body.as_str().as_bytes()).unwrap().into_raw(), - None => std::ptr::null(), + CString::new(request.inner.body()).unwrap().into_raw() +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_request_body_read(request: *const lh_request_t, buffer: *mut ffi::c_char, length: usize) -> usize { + let request = unsafe { &*request }; + let body = request.inner.body(); + + let length = length.min(body.len()); + let chunk = body.take(length); + + unsafe { + std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut ffi::c_char, buffer, length); } + length } #[cfg(feature = "c")] @@ -270,7 +283,7 @@ pub extern "C" fn lh_request_builder_body( builder: *mut lh_request_builder_t, body: *const ffi::c_char, ) -> *mut lh_request_builder_t { - let body = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; + let body = unsafe { CStr::from_ptr(body).to_bytes() }; let builder = unsafe { &mut *builder }; Box::into_raw(Box::new(builder.inner.clone().body(body).into())) } diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs index c4ea3efb..e85dfe76 100644 --- a/crates/lang_handler/src/response.rs +++ b/crates/lang_handler/src/response.rs @@ -1,6 +1,8 @@ #[cfg(feature = "c")] use std::ffi::{CStr, CString, c_char}; +use bytes::{Bytes, BytesMut, BufMut}; + use crate::Headers; use crate::headers::lh_headers_t; @@ -9,15 +11,23 @@ pub struct Response { status: u16, headers: Headers, // TODO: Support Stream bodies when napi.rs supports it - body: String, + body: Bytes, + log: Bytes, + exception: Option, } impl Response { - pub fn new(status: u16, headers: Headers, body: String) -> Self { + pub fn new(status: u16, headers: Headers, body: B, log: L, exception: Option) -> Self + where + B: Into, + L: Into + { Self { status, headers, - body, + body: body.into(), + log: log.into(), + exception } } @@ -37,8 +47,16 @@ impl Response { &self.headers } - pub fn body(&self) -> &String { - &self.body + pub fn body(&self) -> Bytes { + self.body.clone() + } + + pub fn log(&self) -> Bytes { + self.log.clone() + } + + pub fn exception(&self) -> Option<&String> { + self.exception.as_ref() } } @@ -46,7 +64,9 @@ impl Response { pub struct ResponseBuilder { status: Option, headers: Headers, - body: Option, + body: BytesMut, + log: BytesMut, + exception: Option, } impl ResponseBuilder { @@ -54,7 +74,9 @@ impl ResponseBuilder { ResponseBuilder { status: None, headers: Headers::new(), - body: None, + body: BytesMut::with_capacity(1024), + log: BytesMut::with_capacity(1024), + exception: None, } } @@ -62,7 +84,9 @@ impl ResponseBuilder { ResponseBuilder { status: Some(response.status), headers: response.headers.clone(), - body: Some(response.body.clone()), + body: BytesMut::from(response.body()), + log: BytesMut::from(response.log()), + exception: response.exception.clone(), } } @@ -80,11 +104,18 @@ impl ResponseBuilder { self } - pub fn body(&mut self, body: B) -> &mut Self - where - B: Into, - { - self.body = Some(body.into()); + pub fn body>(&mut self, body: B) -> &mut Self { + self.body = body.into(); + self + } + + pub fn log>(&mut self, log: L) -> &mut Self { + self.log = log.into(); + self + } + + pub fn exception>(&mut self, exception: E) -> &mut Self { + self.exception = Some(exception.into()); self } @@ -92,7 +123,9 @@ impl ResponseBuilder { Response { status: self.status.unwrap_or(200), headers: self.headers.clone(), - body: self.body.clone().unwrap_or_default(), + body: self.body.clone().freeze(), + log: self.log.clone().freeze(), + exception: self.exception.clone(), } } } @@ -114,13 +147,13 @@ impl From<&lh_response_t> for Response { } } -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { - let body_str = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; - let headers = unsafe { &*headers }; - Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str).into())) -} +// #[cfg(feature = "c")] +// #[no_mangle] +// pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { +// let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; +// let headers = unsafe { &*headers }; +// Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str).into())) +// } #[cfg(feature = "c")] #[no_mangle] @@ -150,7 +183,7 @@ pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh #[no_mangle] pub extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { let response = unsafe { &*response }; - CString::new(response.inner.body().as_str().as_bytes()).unwrap().into_raw() + CString::new(response.inner.body()).unwrap().into_raw() } #[allow(non_camel_case_types)] @@ -203,10 +236,37 @@ pub extern "C" fn lh_response_builder_header(builder: *mut lh_response_builder_t #[no_mangle] pub extern "C" fn lh_response_builder_body(builder: *mut lh_response_builder_t, body: *const c_char) { let builder = unsafe { &mut *builder }; - let body_str = unsafe { CStr::from_ptr(body).to_string_lossy().into_owned() }; + let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; builder.inner.body(body_str); } +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_body_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.body.put(data); + return len; +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_log_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.log.put(data); + builder.inner.log.put("\n".as_bytes()); + return len; +} + +#[cfg(feature = "c")] +#[no_mangle] +pub extern "C" fn lh_response_builder_exception(builder: *mut lh_response_builder_t, exception: *const c_char) { + let builder = unsafe { &mut *builder }; + let exception_str = unsafe { CStr::from_ptr(exception).to_string_lossy().into_owned() }; + builder.inner.exception(exception_str); +} + #[cfg(feature = "c")] #[no_mangle] pub extern "C" fn lh_response_builder_build(builder: *const lh_response_builder_t) -> *mut lh_response_t { diff --git a/crates/php/Cargo.toml b/crates/php/Cargo.toml index 79263fc1..70b12fc3 100644 --- a/crates/php/Cargo.toml +++ b/crates/php/Cargo.toml @@ -13,6 +13,8 @@ path = "src/main.rs" [dependencies] lang_handler = { path = "../lang_handler", features = ["c"] } +libc = "0.2.171" +once_cell = "1.21.0" [build-dependencies] autotools = "0.2" diff --git a/crates/php/build.rs b/crates/php/build.rs index 891840c9..0882b7c5 100644 --- a/crates/php/build.rs +++ b/crates/php/build.rs @@ -191,7 +191,7 @@ fn main() { // TODO: Build if downloads modification time is more recent than libphp.a let has_libphp = current_dir.join("buildroot/lib/libphp.a").exists(); let should_build = env::var("PHP_SHOULD_BUILD") - .map_or(!has_libphp, |s| s == "true"); + .map_or(should_download || !has_libphp, |s| s == "true"); if should_build { let mut env = HashMap::new(); diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index cc9b5a25..232ba550 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -1,4 +1,8 @@ -use std::{env::Args, ffi::{c_void, c_char, CStr, CString}}; +use std::{ + env::Args, + ffi::{c_void, c_char, CStr, CString} +}; +use std::sync::OnceLock; use lang_handler::{Handler, Request, Response}; @@ -13,6 +17,37 @@ pub struct Embed { unsafe impl Send for Embed {} unsafe impl Sync for Embed {} +struct PhpInit; + +impl PhpInit { + pub fn new(argv: Vec) -> Self + where + S: AsRef + { + let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect(); + let argc = argv.len() as i32; + let mut argv_ptrs = argv + .iter() + .map(|v| v.as_ptr() as *mut c_char) + .collect::>(); + + unsafe { + sys::php_http_init(argc, argv_ptrs.as_mut_ptr()); + } + PhpInit + } +} + +impl Drop for PhpInit { + fn drop(&mut self) { + unsafe { + sys::php_http_destruct(); + } + } +} + +static PHP_INIT: OnceLock = OnceLock::new(); + impl Embed { pub fn new(code: C, filename: Option) -> Self where @@ -27,42 +62,20 @@ impl Embed { C: Into, F: Into { - let argv: Vec = args.collect(); - Embed::new_with_argv(code, filename, argv) + Embed::new_with_argv(code, filename, args.collect()) } pub fn new_with_argv(code: C, filename: Option, argv: Vec) -> Self where C: Into, F: Into, - S: AsRef, + S: AsRef + std::fmt::Debug, { - let argc = argv.len() as i32; - let argv = argv - .into_iter() - .map(|v| CString::new(v.as_ref()).unwrap()) - .collect::>(); - - let mut argv_ptrs = argv - .iter() - .map(|v| v.as_ptr() as *mut c_char) - .collect::>(); - - unsafe { - sys::php_http_init(argc, argv_ptrs.as_mut_ptr()); - } + PHP_INIT.get_or_init(|| PhpInit::new(argv)); Embed { code: code.into(), - filename: filename.map(|v| v.into()), - } - } -} - -impl Drop for Embed { - fn drop(&mut self) { - unsafe { - sys::php_http_shutdown(); + filename: filename.map(|v| v.into()) } } } diff --git a/crates/php/src/php_wrapper.c b/crates/php/src/php_wrapper.c index cd8419b1..dd643103 100644 --- a/crates/php/src/php_wrapper.c +++ b/crates/php/src/php_wrapper.c @@ -1,6 +1,7 @@ // TODO: Review these dependencies. They were all auto-added by IDE completion. // There's likely consolidated headers to get things from. #include "SAPI.h" +#include "TSRM.h" #include "php.h" #include "php_main.h" #include "zend.h" @@ -26,215 +27,176 @@ #include typedef struct php_server_context_s { - int foo; + lh_request_t* request; + lh_response_builder_t* response_builder; } php_server_context_t; -void clean_superglobals() { - // request - if (SG(request_info).request_method != NULL) { - lh_reclaim_str(SG(request_info).request_method); - } +static const char HARDCODED_INI[] = + "display_errors=0\n" + "register_argc_argv=1\n" + "log_errors=1\n" + "implicit_flush=1\n" + "memory_limit=128MB\n" + "output_buffering=0\n"; - // url - if (SG(request_info).path_translated != NULL) { - lh_reclaim_str(SG(request_info).path_translated); - } - if (SG(request_info).query_string != NULL) { - lh_reclaim_str(SG(request_info).query_string); - } - if (SG(request_info).request_uri != NULL) { - lh_reclaim_str(SG(request_info).request_uri); - } +int php_http_startup(sapi_module_struct* sapi_module) { + struct php_ini_builder ini_builder; + php_ini_builder_init(&ini_builder); + php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); + sapi_module->ini_entries = php_ini_builder_finish(&ini_builder); + php_ini_builder_deinit(&ini_builder); - // headers - if (SG(request_info).content_type != NULL) { - lh_reclaim_str(SG(request_info).content_type); - } - if (SG(request_info).cookie_data != NULL) { - lh_reclaim_str(SG(request_info).cookie_data); - } + return php_module_startup(sapi_module, NULL); } -void php_http_setup(); +int php_http_deactivate() { + php_server_context_t* context = SG(server_context); + if (!context) return SUCCESS; -int php_sapi_startup(sapi_module_struct* sapi_module) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Startup from %d\n", context->foo); - php_http_setup(); + SG(server_context) = NULL; - // TODO: Make our own module rather than using the SAPI Embed one? - // php_module_startup(sapi_module, NULL, 0); + SG(request_info).argc = 0; + SG(request_info).argv = NULL; - return SUCCESS; -} + // request + if (SG(request_info).request_method != NULL) { + lh_reclaim_str(SG(request_info).request_method); + } -int php_sapi_shutdown(sapi_module_struct* sapi_module) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Shutdown from %d\n", context->foo); - clean_superglobals(); - return SUCCESS; -} + // url + if (SG(request_info).path_translated != NULL) { + lh_reclaim_str(SG(request_info).path_translated); + } + if (SG(request_info).query_string != NULL) { + lh_reclaim_str(SG(request_info).query_string); + } + if (SG(request_info).request_uri != NULL) { + lh_reclaim_str(SG(request_info).request_uri); + } -int php_sapi_activate() { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - if (!context) return SUCCESS; - printf("Activate from %d\n", context->foo); - return SUCCESS; -} + // headers + if (SG(request_info).content_type != NULL) { + lh_reclaim_str(SG(request_info).content_type); + } + if (SG(request_info).cookie_data != NULL) { + lh_reclaim_str(SG(request_info).cookie_data); + } -int php_sapi_deactivate() { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - if (!context) return SUCCESS; - printf("Deactivate from %d\n", context->foo); return SUCCESS; } -size_t sapi_ub_write(const char *str, size_t str_length) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("%.*s from %d\n", (int)str_length, str, context->foo); - return str_length; +size_t php_http_ub_write(const char* str, size_t len) { + php_server_context_t* context = SG(server_context); + return lh_response_builder_body_write(context->response_builder, str, len); } -void sapi_node_flush() { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Flush occurred from %d\n", context->foo); +void php_http_flush() { if (!SG(headers_sent)) { sapi_send_headers(); SG(headers_sent) = 1; } } -void php_sapi_error(int type, const char *error_msg, ...) { - va_list args; - va_start(args, error_msg); - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Error: %s from %d\n", error_msg, context->foo); - va_end(args); -} - -void sapi_send_header(sapi_header_struct *sapi_header, void *server_context) { +void php_http_send_header( + sapi_header_struct* sapi_header, + __attribute__((unused)) void* server_context +) { // Not sure _why_ this is necessary, but it is. if (sapi_header == NULL) return; - php_server_context_t* context = (php_server_context_t*)server_context; - printf("Header: %s from %d\n", sapi_header->header, context->foo); + // printf("Header: %s\n", sapi_header->header); } -int php_sapi_send_headers(sapi_headers_struct *sapi_headers) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Headers sent from %d\n", context->foo); - - sapi_header_struct *h; +int php_http_send_headers(sapi_headers_struct* sapi_headers) { + // php_server_context_t* context = SG(server_context); + sapi_header_struct* h; zend_llist_position pos; h = zend_llist_get_first_ex(&sapi_headers->headers, &pos); while (h) { if ( h->header_len > 0 ) { - printf("Header: %s\n", h->header); + // printf("Header: %s\n", h->header); } h = zend_llist_get_next_ex(&sapi_headers->headers, &pos); } - return 0; + return SAPI_HEADER_SENT_SUCCESSFULLY; } -static bool sent_post_data = false; - -// TODO: Read n bytes from request body in ctx, memcpy to buffer, return remaining bytes. -// This needs to block until it receives data, so will need some synchronization mechanism. -size_t php_sapi_read_post(char *buffer, size_t count_bytes) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Read post from %d\n", context->foo); - - if (sent_post_data) { - return 0; - } - - sent_post_data = true; - - memcpy(buffer, "Hello, from read_post!", 22); - - return 22; +size_t php_http_read_post(char* buffer, size_t count_bytes) { + php_server_context_t* context = SG(server_context); + return lh_request_body_read(context->request, buffer, count_bytes); } -char* php_sapi_read_cookies() { +char* php_http_read_cookies() { return SG(request_info).cookie_data; } -void php_register_server_variables(zval *track_vars_array) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Register server variables from %d\n", context->foo); +void php_http_register_server_variables(zval* track_vars_array) { php_import_environment_variables(track_vars_array); } -void php_sapi_log_message(const char *message, int syslog_type_int) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - printf("Log message: %s from %d\n", message, context->foo); +void php_http_log_message( + const char* message, + __attribute__((unused)) int syslog_type_int +) { + php_server_context_t* context = SG(server_context); + size_t len = strlen(message); + lh_response_builder_log_write(context->response_builder, message, len); } -zend_result php_get_request_time(double* ts) { - php_server_context_t* context = (php_server_context_t*)SG(server_context); - *ts = 0.0; - return SUCCESS; -} +static sapi_module_struct php_http_sapi_module = { + "php-http", /* name */ + "PHP/HTTP", /* pretty name */ -static const char HARDCODED_INI[] = - "display_errors=0\n" - "register_argc_argv=1\n" - "log_errors=1\n" - "implicit_flush=1\n" - "memory_limit=128MB\n" - "output_buffering=0\n"; + php_http_startup, /* startup */ + php_module_shutdown_wrapper, /* shutdown */ -void php_http_setup() { - php_embed_module.name = "php-lang-handler"; - php_embed_module.pretty_name = "PHP Language Handler"; + NULL, /* activate */ + php_http_deactivate, /* deactivate */ - php_embed_module.startup = php_sapi_startup; - php_embed_module.shutdown = php_sapi_shutdown; + php_http_ub_write, /* unbuffered write */ + php_http_flush, /* flush */ - php_embed_module.activate = php_sapi_activate; - php_embed_module.deactivate = php_sapi_deactivate; + NULL, /* get uid */ + NULL, /* getenv */ - php_embed_module.ub_write = sapi_ub_write; - php_embed_module.flush = sapi_node_flush; + php_error, /* error handler */ - php_embed_module.sapi_error = php_sapi_error; + NULL, /* header handler */ + php_http_send_headers, /* send headers handler */ + php_http_send_header, /* send header handler */ - php_embed_module.send_header = sapi_send_header; - // php_embed_module.send_headers = php_sapi_send_headers; - php_embed_module.send_headers = NULL; + php_http_read_post, /* read POST data */ + php_http_read_cookies, /* read Cookies */ - php_embed_module.read_post = php_sapi_read_post; - php_embed_module.read_cookies = php_sapi_read_cookies; + php_http_register_server_variables, /* register server variables */ + php_http_log_message, /* Log message */ - php_embed_module.register_server_variables = php_register_server_variables; - php_embed_module.log_message = php_sapi_log_message; + NULL, /* Get request time */ + NULL, /* Child terminate */ - php_embed_module.get_request_time = php_get_request_time; - - struct php_ini_builder ini_builder; - php_ini_builder_init(&ini_builder); - php_ini_builder_prepend_literal(&ini_builder, HARDCODED_INI); -} + STANDARD_SAPI_MODULE_PROPERTIES +}; -zend_result php_http_init(int argc, char **argv) { +zend_result php_http_init(int argc, char** argv) { #ifdef ZTS php_tsrm_startup(); #endif - php_http_setup(); - sapi_startup(&php_embed_module); - - if (php_module_startup(&sapi_module, NULL) == FAILURE) { -#ifdef ZTS - tsrm_shutdown(); -#endif - return FAILURE; + if (argc > 0) { + php_http_sapi_module.executable_location = argv[0]; } + sapi_startup(&php_http_sapi_module); + + php_module_startup(&php_http_sapi_module, NULL); return SUCCESS; } -zend_result php_http_shutdown() { +zend_result php_http_destruct() { + // Why is this needed??? + // sapi_module.flush = NULL; + + // php_http_sapi_module.shutdown(&php_http_sapi_module); php_module_shutdown(); sapi_shutdown(); @@ -242,14 +204,13 @@ zend_result php_http_shutdown() { #ifdef ZTS tsrm_shutdown(); #endif + + return SUCCESS; } /** * TODO: - * - Learn how php_stream works and adapt to tokio streams? * - Provide high-performance PSR-7 implementation on libuv? - * - Use thread pool, or let wattpm handle it? - * - Don't need multiple instances of the native module and PHP runtime. * * NOTES: * @@ -263,87 +224,94 @@ zend_result php_http_shutdown() { * can be shared making spin up quick. Each of these contexts is single-threaded. */ lh_response_t* php_http_handle_request(const char* code, const char* filename, lh_request_t* request) { - // This is where we store the stuff for associating callbacks with this request. - // TODO: This should probably contain the request and response objects. - php_server_context_t* context = malloc(sizeof(php_server_context_t)); - context->foo = 555; - SG(server_context) = context; + lh_response_builder_t* response_builder = lh_response_builder_new(); + + if (php_http_sapi_module.startup(&php_http_sapi_module) == FAILURE) { +#ifdef ZTS + tsrm_shutdown(); +#endif + return lh_response_builder_build(response_builder); + } - SG(options) |= SAPI_OPTION_NO_CHDIR; - SG(headers_sent) = 0; + zend_first_try { + // This is where we store the stuff for associating callbacks with this request. + php_server_context_t context = { + .request = request, + .response_builder = response_builder + }; - SG(request_info).argc = 0; - SG(request_info).argv = NULL; + SG(server_context) = &context; - // Reset state - SG(sapi_headers).http_response_code = 200; + SG(options) |= SAPI_OPTION_NO_CHDIR; + SG(headers_sent) = 0; - // Set up superglobals - SG(request_info).request_method = lh_request_method(request); + SG(request_info).argc = 0; + SG(request_info).argv = NULL; - lh_url_t* url = lh_request_url(request); - SG(request_info).path_translated = (char*) lh_url_path(url); - SG(request_info).query_string = (char*) lh_url_query(url); - SG(request_info).request_uri = (char*) lh_url_uri(url); - // TODO: Add auth fields + // Reset state + SG(sapi_headers).http_response_code = 200; - // Could implement a PHP stream to do this? - // SG(request_info).request_body = lh_request_body(request); + // Set up superglobals + SG(request_info).request_method = lh_request_method(request); - lh_headers_t* headers = lh_request_headers(request); + lh_url_t* url = lh_request_url(request); + SG(request_info).path_translated = (char*) lh_url_path(url); + SG(request_info).query_string = (char*) lh_url_query(url); + SG(request_info).request_uri = (char*) lh_url_uri(url); + // TODO: Add auth fields - const char* content_type = lh_headers_get(headers, "Content-Type"); - if (content_type == NULL) { - SG(request_info).content_type = content_type; - } + lh_headers_t* headers = lh_request_headers(request); - const char* content_length = lh_headers_get(headers, "Content-Length"); - if (content_length != NULL) { - SG(request_info).content_length = strtoll(content_length, NULL, 10); - } + const char* content_type = lh_headers_get(headers, "Content-Type"); + if (content_type == NULL) { + SG(request_info).content_type = content_type; + } - const char* cookie = lh_headers_get(headers, "Cookie"); - SG(request_info).cookie_data = (char*) cookie; + const char* content_length = lh_headers_get(headers, "Content-Length"); + if (content_length != NULL) { + SG(request_info).content_length = strtoll(content_length, NULL, 10); + } - // Start new request now that we've setup the environment fully. - if (php_request_startup() == FAILURE) { - return NULL; - } + const char* cookie = lh_headers_get(headers, "Cookie"); + SG(request_info).cookie_data = (char*) cookie; + + // Start new request now that we've setup the environment fully. + if (php_request_startup() == FAILURE) { + return lh_response_builder_build(response_builder); + } - // Needs to be set _after_ php_request_startup, also because reasons. - SG(request_info).proto_num = 110; + // Needs to be set _after_ php_request_startup, also because reasons. + SG(request_info).proto_num = 110; - zend_first_try { size_t len = strlen(code); zend_eval_stringl_ex((char*)code, len, NULL, filename, false); if (EG(exception)) { zval rv; - // TODO: Figure out why this fails. - zval *msg = zend_read_property_ex(zend_ce_exception, EG(exception), ZSTR_KNOWN(ZEND_STR_MESSAGE), /* silent */ false, &rv); - zend_printf("Exception: %s\n", Z_STRVAL_P(msg)); + zend_class_entry* exception_ce = zend_get_exception_base(EG(exception)); + zval *msg = zend_read_property_ex(exception_ce, EG(exception), ZSTR_KNOWN(ZEND_STR_MESSAGE), /* silent */ false, &rv); + + SG(sapi_headers).http_response_code = 500; + lh_response_builder_exception(response_builder, Z_STRVAL_P(msg)); + zend_object_release(EG(exception)); EG(exception) = NULL; EG(exit_status) = 1; } - } zend_catch { - return NULL; - } zend_end_try(); - zend_try { - php_request_shutdown(NULL); - } zend_end_try(); + const char* mime = SG(sapi_headers).mimetype; + if (mime == NULL) { + mime = "text/plain"; + } + lh_response_builder_header(response_builder, "Content-Type", mime); + lh_response_builder_status_code(response_builder, SG(sapi_headers).http_response_code); - // Reset headers to reuse for response object - lh_headers_free(headers); - headers = lh_headers_new(); + php_request_shutdown(NULL); + lh_headers_free(headers); - const char* mime = SG(sapi_headers).mimetype; - if (mime == NULL) { - mime = "text/plain"; - } - lh_headers_set(headers, "Content-Type", mime); + php_header(); + php_output_flush_all(); + } zend_end_try(); - int status = SG(sapi_headers).http_response_code; - return lh_response_new(status, headers, "Hello, World!"); + return lh_response_builder_build(response_builder); } diff --git a/crates/php_node/src/lib.rs b/crates/php_node/src/lib.rs index 6effed04..4d020d26 100644 --- a/crates/php_node/src/lib.rs +++ b/crates/php_node/src/lib.rs @@ -1,6 +1,9 @@ #[macro_use] extern crate napi_derive; +use std::collections::HashMap; +use std::sync::Arc; + use napi::{Error, Task, Env, Result}; use napi::bindgen_prelude::*; @@ -82,10 +85,10 @@ impl PhpHeaders { #[napi(object)] #[derive(Default)] pub struct PhpRequestOptions { - pub method: Option, - pub url: Option, - pub headers: Option, - pub body: Option + pub method: String, + pub url: String, + pub headers: Option>>, + pub body: Option } #[napi(js_name = "Request")] @@ -96,29 +99,24 @@ pub struct PhpRequest { #[napi] impl PhpRequest { #[napi(constructor)] - pub fn new(options: Option) -> Self { - let opts = options.unwrap_or_default(); - let mut builder: RequestBuilder = Request::builder(); - - if let Some(method) = opts.method { - builder = builder.method(method) - } + pub fn new(options: PhpRequestOptions) -> Self { + let mut builder: RequestBuilder = Request::builder() + .method(options.method) + .url(options.url).expect("invalid url"); - if let Some(url) = opts.url { - builder = builder.url(url).expect("invalid url") - } + if let Some(headers) = options.headers { + for key in headers.keys() { + let values = headers.get(key) + .expect(format!("missing header values for key: {}", key).as_str()); - if let Some(headers) = opts.headers { - for key in Object::keys(&headers).unwrap() { - let values: Vec = headers.get(&key).unwrap().unwrap(); for value in values { builder = builder.header(key.clone(), value.clone()) } } } - if let Some(body) = opts.body { - builder = builder.body(body) + if let Some(body) = options.body { + builder = builder.body(body.as_ref()) } PhpRequest { @@ -147,11 +145,11 @@ impl PhpRequest { } #[napi(getter, enumerable = true)] - pub fn body(&self) -> String { + pub fn body(&self) -> Buffer { self.request .body() - .map(|v| v.to_owned()) - .unwrap_or_default() + .to_vec() + .into() } } @@ -164,33 +162,47 @@ impl PhpRequest { #[napi(object)] #[derive(Clone, Default)] pub struct PhpOptions { + pub argv: Option>, pub code: String, pub file: Option } #[napi] pub struct Php { - pub options: PhpOptions + embed: Arc } #[napi] impl Php { #[napi(constructor)] pub fn new(options: PhpOptions) -> Self { - Php { options } + let code = options.code.clone(); + let filename = options.file.clone(); + let argv = options.argv.clone(); + + // TODO: Need to figure out how to send an Embed across threads + // so we can reuse the same Embed instance for multiple requests. + let embed = match argv { + Some(argv) => Embed::new_with_argv(code, filename, argv), + None => Embed::new(code, filename) + }; + + Php { + embed: Arc::new(embed) + } } #[napi] pub fn handle_request(&self, request: &PhpRequest) -> AsyncTask { AsyncTask::new(PhpRequestTask { - options: self.options.clone(), + embed: self.embed.clone(), request: request.to_inner() }) } } pub struct PhpRequestTask { - options: PhpOptions, + embed: Arc, request: Request } @@ -199,18 +211,16 @@ impl Task for PhpRequestTask { type JsValue = PhpResponse; fn compute(&mut self) -> Result { - let code = self.options.code.clone(); - let filename = self.options.file.clone(); - // TODO: Need to figure out how to send an Embed across threads - // so we can reuse the same Embed instance for multiple requests. - let embed = Embed::new(code, filename); - - embed + self.embed .handle(self.request.clone()) .map_err(|err| Error::from_reason(err)) } fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + if let Some(exception) = output.exception() { + return Err(Error::from_reason(exception.to_owned())) + } + Ok(PhpResponse { response: output }) @@ -237,7 +247,23 @@ impl PhpResponse { } #[napi(getter, enumerable = true)] - pub fn body(&self) -> String { - self.response.body().to_owned() + pub fn body(&self) -> Buffer { + self.response + .body() + .to_vec() + .into() + } + + #[napi(getter, enumerable = true)] + pub fn log(&self) -> Buffer { + self.response + .log() + .to_vec() + .into() + } + + #[napi(getter, enumerable = true)] + pub fn exception(&self) -> Option { + self.response.exception().map(|v| v.to_owned()) } } diff --git a/index.d.ts b/index.d.ts index adbb3b41..58628ed9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -4,27 +4,34 @@ /* auto-generated by NAPI-RS */ export interface PhpRequestOptions { - method?: string - url?: string - body?: string + method: string + url: string + headers?: Record> + body?: Uint8Array } export interface PhpOptions { + argv?: Array code: string file?: string } +export type PhpHeaders = Headers +export declare class Headers { } export type PhpRequest = Request export declare class Request { - constructor(options?: PhpRequestOptions | undefined | null) + constructor(options: PhpRequestOptions) get method(): string get url(): string - get body(): string + get headers(): Headers + get body(): Buffer } export declare class Php { - options: PhpOptions constructor(options: PhpOptions) handleRequest(request: Request): Promise } export declare class PhpResponse { get status(): number - get body(): string + get headers(): Headers + get body(): Buffer + get log(): Buffer + get exception(): string | null } diff --git a/index.js b/index.js index 6b0a3d29..5c9e1aa2 100644 --- a/index.js +++ b/index.js @@ -310,8 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Request, Php, PhpResponse } = nativeBinding +const { Headers, Request, Php, PhpResponse } = nativeBinding +module.exports.Headers = Headers module.exports.Request = Request module.exports.Php = Php module.exports.PhpResponse = PhpResponse diff --git a/yarn.lock b/yarn.lock index fc9213f1..0f8ae0e1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -88,6 +88,15 @@ __metadata: languageName: node linkType: hard +"@platformatic/php@workspace:.": + version: 0.0.0-use.local + resolution: "@platformatic/php@workspace:." + dependencies: + "@napi-rs/cli": "npm:^2.18.4" + ava: "npm:^6.0.1" + languageName: unknown + linkType: soft + "@rollup/pluginutils@npm:^5.1.3": version: 5.1.4 resolution: "@rollup/pluginutils@npm:5.1.4" @@ -1171,15 +1180,6 @@ __metadata: languageName: node linkType: hard -"php-stackable@workspace:.": - version: 0.0.0-use.local - resolution: "php-stackable@workspace:." - dependencies: - "@napi-rs/cli": "npm:^2.18.4" - ava: "npm:^6.0.1" - languageName: unknown - linkType: soft - "picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" From 6c844f94864a071dd835ec763eaa2720ba03b18e Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 13 Mar 2025 14:23:28 +0800 Subject: [PATCH 4/9] Try to get CI to run... --- .github/workflows/CI.yml | 171 +++++++++++++++++++---------------- __test__/index.spec.mjs | 16 +++- crates/lang_handler/build.rs | 18 ++-- crates/php/build.rs | 30 +++++- 4 files changed, 139 insertions(+), 96 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index a0117d9b..ecb393c9 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -25,47 +25,62 @@ jobs: fail-fast: false matrix: settings: - - host: macos-latest + - host: macos-13 target: x86_64-apple-darwin build: yarn build --target x86_64-apple-darwin - - host: windows-latest - build: yarn build --target x86_64-pc-windows-msvc - target: x86_64-pc-windows-msvc - - host: windows-latest - build: | - yarn build --target i686-pc-windows-msvc - yarn test - target: i686-pc-windows-msvc - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian - build: yarn build --target x86_64-unknown-linux-gnu + # build: yarn build - host: macos-latest target: aarch64-apple-darwin build: yarn build --target aarch64-apple-darwin + # build: yarn build - host: ubuntu-latest - target: aarch64-unknown-linux-gnu - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 - build: yarn build --target aarch64-unknown-linux-gnu - - host: ubuntu-latest - target: armv7-unknown-linux-gnueabihf + target: x86_64-unknown-linux-gnu + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian + build: yarn build --target x86_64-unknown-linux-gnu + # build: yarn build setup: | sudo apt-get update - sudo apt-get install gcc-arm-linux-gnueabihf -y - build: yarn build --target armv7-unknown-linux-gnueabihf - - host: ubuntu-latest - target: armv7-unknown-linux-musleabihf - build: yarn build --target armv7-unknown-linux-musleabihf - - host: ubuntu-latest - target: aarch64-unknown-linux-musl - docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine - build: |- - set -e && - rustup target add aarch64-unknown-linux-musl && - yarn build --target aarch64-unknown-linux-musl - - host: windows-latest - target: aarch64-pc-windows-msvc - build: yarn build --target aarch64-pc-windows-msvc + sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian-aarch64 + # build: yarn build --target aarch64-unknown-linux-gnu + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: armv7-unknown-linux-gnueabihf + # setup: | + # sudo apt-get update + # sudo apt-get install gcc-arm-linux-gnueabihf libssl-dev -y + # build: yarn build --target armv7-unknown-linux-gnueabihf + # - host: ubuntu-latest + # target: armv7-unknown-linux-musleabihf + # build: yarn build --target armv7-unknown-linux-musleabihf + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: ubuntu-latest + # target: aarch64-unknown-linux-musl + # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + # build: |- + # set -e && + # rustup target add aarch64-unknown-linux-musl && + # yarn build --target aarch64-unknown-linux-musl + # setup: | + # sudo apt-get update + # sudo apt-get install libssl-dev -y + # - host: windows-latest + # target: aarch64-pc-windows-msvc + # build: yarn build --target aarch64-pc-windows-msvc + # - host: windows-latest + # target: x86_64-pc-windows-msvc + # build: yarn build --target x86_64-pc-windows-msvc + # - host: windows-latest + # target: i686-pc-windows-msvc + # build: | + # yarn build --target i686-pc-windows-msvc + # yarn test name: stable - ${{ matrix.settings.target }} - node@20 runs-on: ${{ matrix.settings.host }} steps: @@ -340,48 +355,48 @@ jobs: name: bindings-universal-apple-darwin path: ${{ env.APP_NAME }}.*.node if-no-files-found: error - publish: - name: Publish - runs-on: ubuntu-latest - needs: - - test-macOS-windows-binding - - test-linux-x64-gnu-binding - - test-linux-aarch64-gnu-binding - - test-linux-aarch64-musl-binding - - test-linux-arm-gnueabihf-binding - - universal-macOS - steps: - - uses: actions/checkout@v4 - - name: Setup node - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: yarn - - name: Install dependencies - run: yarn install - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: artifacts - - name: Move artifacts - run: yarn artifacts - - name: List packages - run: ls -R ./npm - shell: bash - - name: Publish - run: | - npm config set provenance true - if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --access public - elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; - then - echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc - npm publish --tag next --access public - else - echo "Not a release, skipping publish" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + # publish: + # name: Publish + # runs-on: ubuntu-latest + # needs: + # - test-macOS-windows-binding + # - test-linux-x64-gnu-binding + # - test-linux-aarch64-gnu-binding + # - test-linux-aarch64-musl-binding + # - test-linux-arm-gnueabihf-binding + # - universal-macOS + # steps: + # - uses: actions/checkout@v4 + # - name: Setup node + # uses: actions/setup-node@v4 + # with: + # node-version: 20 + # cache: yarn + # - name: Install dependencies + # run: yarn install + # - name: Download all artifacts + # uses: actions/download-artifact@v4 + # with: + # path: artifacts + # - name: Move artifacts + # run: yarn artifacts + # - name: List packages + # run: ls -R ./npm + # shell: bash + # - name: Publish + # run: | + # npm config set provenance true + # if git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+$"; + # then + # echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + # npm publish --access public + # elif git log -1 --pretty=%B | grep "^[0-9]\+\.[0-9]\+\.[0-9]\+"; + # then + # echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc + # npm publish --tag next --access public + # else + # echo "Not a release, skipping publish" + # fi + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/__test__/index.spec.mjs b/__test__/index.spec.mjs index 221cb260..a5e59cd1 100644 --- a/__test__/index.spec.mjs +++ b/__test__/index.spec.mjs @@ -60,12 +60,20 @@ test('exceptions work', async (t) => { }) }) -test('input and output headers work', async (t) => { +test('request headers work', async (t) => { const php = new Php({ file: 'index.php', code: ` - if ($_SERVER['HTTP_X_TEST'] == 'Hello, from Node.js!') { - header('X-Test: Hello, from PHP!'); + foreach($_SERVER as $key => $val) { + if ($key == 'argv') { + for ($i = 0; $i < count($val); $i++) { + $arg = $val[$i]; + echo "argv[$i]: $arg\n"; + } + continue; + } + + echo "$key: $val\n"; } ` }) @@ -81,5 +89,5 @@ test('input and output headers work', async (t) => { const res = await php.handleRequest(req) console.log(res) t.is(res.status, 200) - // t.is(res.headers['X-Test'], 'Hello, from PHP!') + t.is(res.body.toString(), 'Hello, from PHP!') }) diff --git a/crates/lang_handler/build.rs b/crates/lang_handler/build.rs index fb1c932f..7123e128 100644 --- a/crates/lang_handler/build.rs +++ b/crates/lang_handler/build.rs @@ -5,22 +5,18 @@ use std::path::PathBuf; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let out_dir = PathBuf::from(crate_dir.clone()); + let profile = env::var("PROFILE").unwrap(); + + let target_str = format!("../../target/{}", profile); + let header_path = PathBuf::from(crate_dir.clone()) + .join(target_str) + .join("lang_handler.h"); cbindgen::Builder::new() .with_crate(crate_dir) .with_include_guard("LANG_HANDLER_H") .with_language(cbindgen::Language::C) - .with_parse_deps(true) - .with_parse_include(&["url"]) - .include_item("Url") - // .rename_item("Request", "lh_request_t") - // .rename_item("RequestBuilder", "lh_request_builder_t") - // .rename_item("Response", "lh_response_t") - // .rename_item("ResponseBuilder", "lh_response_builder_t") - // .rename_item("Headers", "lh_headers_t") - // .rename_item("Url", "lh_url_t") .generate() .expect("Unable to generate bindings") - .write_to_file(out_dir.join("../../target/release/lang_handler.h")); + .write_to_file(header_path); } diff --git a/crates/php/build.rs b/crates/php/build.rs index 0882b7c5..44dc8f5f 100644 --- a/crates/php/build.rs +++ b/crates/php/build.rs @@ -10,6 +10,7 @@ use std::{ // use autotools::Config; use bindgen::Builder; use downloader::{Download, Downloader}; +#[cfg(not(target_os = "windows"))] use file_mode::ModePath; fn maybe_windowsify(path: T) -> String where T: Display { @@ -20,8 +21,26 @@ fn maybe_windowsify(path: T) -> String where T: Display { } fn spc_url() -> String { - let os = env::var("CARGO_CFG_TARGET_OS").unwrap(); - let arch = env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + let os = if cfg!(target_os = "macos") { + "macos" + } else if cfg!(target_os = "linux") { + "linux" + } else if cfg!(target_os = "windows") { + "windows" + } else { + panic!("Unsupported OS"); + }; + + let arch = if cfg!(target_arch = "x86") { + "x86" + } else if cfg!(target_arch = "x86_64") { + "x86_64" + } else if cfg!(target_arch = "aarch64") { + "aarch64" + } else { + panic!("Unsupported arch"); + }; + let url = format!("https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-{}-{}", os, arch); maybe_windowsify(url) @@ -51,6 +70,7 @@ fn get_spc() -> PathBuf { downloader.download(&vec![dl]).unwrap(); // Make the file executable + #[cfg(not(target_os = "windows"))] spc.set_mode("a+x").unwrap(); spc } @@ -172,6 +192,8 @@ fn main() { let spc = get_spc(); let spc_cmd = spc.to_str().unwrap(); + execute_command(&[spc_cmd, "doctor", "--auto-fix"], None); + let has_downloads = current_dir.join("downloads").exists(); let should_download = env::var("PHP_SHOULD_DOWNLOAD") .map_or(!has_downloads, |s| s == "true"); @@ -256,7 +278,9 @@ fn main() { let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - let lang_handler_include = crate_dir.join("../../target/release"); + let profile = env::var("PROFILE").unwrap(); + let lang_handler_include = crate_dir + .join(format!("../../target/{}", profile)); println!("cargo:rustc-link-search={}", lang_handler_include.display()); println!("cargo:rustc-link-lib=lang_handler"); From 081b888e76b17beae91b46d71e6cc731ea316ae8 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 13 Mar 2025 20:44:07 +0800 Subject: [PATCH 5/9] Minor improvements --- crates/lang_handler/src/request.rs | 4 +++- crates/lang_handler/src/response.rs | 2 +- crates/php/src/main.rs | 27 +++------------------------ 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs index b0be10ff..38d67482 100644 --- a/crates/lang_handler/src/request.rs +++ b/crates/lang_handler/src/request.rs @@ -1,6 +1,8 @@ #[cfg(feature = "c")] use std::{ffi, ffi::{CStr, CString}}; +use std::fmt::Debug; + use bytes::{Buf, Bytes, BytesMut}; use url::{ParseError, Url}; @@ -8,7 +10,7 @@ use crate::Headers; use crate::headers::lh_headers_t; use crate::url::lh_url_t; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Request { method: String, url: Url, diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs index e85dfe76..f3efa8ef 100644 --- a/crates/lang_handler/src/response.rs +++ b/crates/lang_handler/src/response.rs @@ -6,7 +6,7 @@ use bytes::{Bytes, BytesMut, BufMut}; use crate::Headers; use crate::headers::lh_headers_t; -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Response { status: u16, headers: Headers, diff --git a/crates/php/src/main.rs b/crates/php/src/main.rs index 17542906..e3c46b6e 100644 --- a/crates/php/src/main.rs +++ b/crates/php/src/main.rs @@ -3,23 +3,11 @@ use php::{Embed, Request, Handler}; pub fn main() { let code = " http_response_code(123); - - // foreach ($_SERVER as $name => $value) { - // header(\"$name: $value\"); - // } - + header('Content-Type: text/plain'); echo file_get_contents(\"php://input\"); - - print('hello'); - flush(); "; - // let code = " - // http_response_code(123); - // echo $HTTP_RAW_POST_DATA; - // "; let filename = Some("test.php"); let embed = Embed::new_with_args(code, filename, std::env::args()); - // let embed = Embed::new(code, filename); let request = Request::builder() .method("POST") @@ -29,19 +17,10 @@ pub fn main() { .body("Hello, World!") .build(); - println!("=== request ==="); - println!("method: {}", request.method()); - println!("url: {:?}", request.url()); - println!("headers: {:?}", request.headers()); - println!("body: {:?}", request.body()); - println!(""); + println!("request: {:#?}", request); let response = embed.handle(request.clone()) .expect("failed to handle request"); - println!("\n=== response ==="); - println!("status: {:?}", response.status()); - println!("headers: {:?}", response.headers()); - println!("body: {:?}", response.body()); - println!(""); + println!("response: {:#?}", response); } From ff04c0626a461eaefacc3a83d512483207234d55 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 13 Mar 2025 23:37:34 +0800 Subject: [PATCH 6/9] Cleanup, document, and add tests for lang_handler --- crates/lang_handler/src/ffi.rs | 1020 +++++++++++++++++++++++++++ crates/lang_handler/src/handler.rs | 50 ++ crates/lang_handler/src/headers.rs | 204 +++--- crates/lang_handler/src/lib.rs | 22 +- crates/lang_handler/src/request.rs | 469 +++++++----- crates/lang_handler/src/response.rs | 425 +++++++---- crates/lang_handler/src/url.rs | 106 --- 7 files changed, 1718 insertions(+), 578 deletions(-) create mode 100644 crates/lang_handler/src/ffi.rs delete mode 100644 crates/lang_handler/src/url.rs diff --git a/crates/lang_handler/src/ffi.rs b/crates/lang_handler/src/ffi.rs new file mode 100644 index 00000000..eae9f594 --- /dev/null +++ b/crates/lang_handler/src/ffi.rs @@ -0,0 +1,1020 @@ +use std::{ffi, ffi::{CStr, CString, c_char}}; + +use bytes::{Buf, BufMut}; + +use crate::{Headers, Request, RequestBuilder, Response, ResponseBuilder, Url}; + +/// Reclaim a string allocated by the library. +/// +/// # Examples +/// +/// ```c +/// const char* rust_string = get_string_from_rust(); +/// // Return ownership of the string to Rust and drop +/// lh_reclaim_str(rust_string); +/// ``` +#[no_mangle] +pub extern "C" fn lh_reclaim_str(url: *const c_char) { + unsafe { + drop(CString::from_raw(url as *mut c_char)); + } +} + +/// A multi-map of HTTP headers. Each key can have multiple values. +#[allow(non_camel_case_types)] +pub struct lh_headers_t { + inner: Headers, +} + +/// Convert a `Headers` into a `lh_headers_t`. +impl From for lh_headers_t { + fn from(inner: Headers) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_headers_t` into a `Headers`. +impl From<&lh_headers_t> for Headers { + fn from(headers: &lh_headers_t) -> Headers { + headers.inner.clone() + } +} + +/// Create a new `lh_headers_t`. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_new() -> *mut lh_headers_t { + let headers = Headers::new(); + Box::into_raw(Box::new(headers.into())) +} + +/// Free a `lh_headers_t`. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// +/// // Do something... +/// +/// lh_headers_free(headers); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { + if !headers.is_null() { + unsafe { + drop(Box::from_raw(headers)); + } + } +} + +/// Get the number of headers with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// size_t count = lh_headers_count(headers, "Accept"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_count(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> usize { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(value) => value.len(), + None => 0 + } +} + +/// Get the value of the last header with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// const char* value = lh_headers_get(headers, "Accept"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_get(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if values.len() > 0 { + values[values.len() - 1].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +/// Get the value of the nth header with the given key. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// const char* value = lh_headers_get_nth(headers, "Accept", 0); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_get_nth(headers: *const lh_headers_t, key: *const std::os::raw::c_char, index: usize) -> *const std::os::raw::c_char { + let headers = unsafe { + assert!(!headers.is_null()); + &*headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap() + }; + match headers.inner.get(key) { + Some(values) => { + if index < values.len() { + values[index].as_ptr() as *const std::os::raw::c_char + } else { + std::ptr::null() + } + }, + None => std::ptr::null() + } +} + +/// Set a header with the given key and value. +/// +/// # Examples +/// +/// ```c +/// lh_headers_t* headers = lh_headers_new(); +/// lh_headers_set(headers, "Accept", "application/json"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_headers_set(headers: *mut lh_headers_t, key: *const std::os::raw::c_char, value: *const std::os::raw::c_char) { + let headers = unsafe { + assert!(!headers.is_null()); + &mut *headers + }; + let key = unsafe { + assert!(!key.is_null()); + std::ffi::CStr::from_ptr(key).to_str().unwrap().to_string() + }; + let value = unsafe { + assert!(!value.is_null()); + std::ffi::CStr::from_ptr(value).to_str().unwrap().to_string() + }; + headers.inner.set(key, value); +} + +/// An HTTP request. Includes method, URL, headers, and body. +#[allow(non_camel_case_types)] +pub struct lh_request_t { + inner: Request, +} + +/// Convert a `Request` into a `lh_request_t`. +impl From for lh_request_t { + fn from(inner: Request) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_request_t` into a `Request`. +impl From<&lh_request_t> for Request { + fn from(request: &lh_request_t) -> Request { + request.inner.clone() + } +} + +/// Create a new `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_new( + method: *const ffi::c_char, + url: *const ffi::c_char, + headers: *mut lh_headers_t, + body: *const ffi::c_char, +) -> *mut lh_request_t { + let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; + let url_str = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let url = Url::parse(&url_str).unwrap(); + let body = if body.is_null() { + None + } else { + Some(unsafe { CStr::from_ptr(body).to_bytes() }) + }; + let headers = unsafe { &*headers }; + let request = Request::new(method, url, headers.into(), body.unwrap_or(&[])); + Box::into_raw(Box::new(request.into())) +} + +/// Free a `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// +/// // Do something... +/// +/// lh_request_free(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_free(request: *mut lh_request_t) { + if !request.is_null() { + unsafe { + drop(Box::from_raw(request)); + } + } +} + +/// Get the method of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// const char* method = lh_request_method(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + CString::new(request.inner.method()).unwrap().into_raw() +} + +/// Get the URL of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_url_t* url = lh_request_url(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.url().clone().into())) +} + +/// Get the headers of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_headers_t* headers = lh_request_headers(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_headers_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(request.inner.headers().clone().into())) +} + +/// Get the body of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// const char* body = lh_request_body(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { + let request = unsafe { &*request }; + CString::new(request.inner.body()).unwrap().into_raw() +} + +/// Read from the body of the request into a buffer. Consumes that many bytes from the body. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// char buffer[1024]; +/// size_t length = lh_request_body_read(request, buffer, 1024); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_body_read(request: *const lh_request_t, buffer: *mut ffi::c_char, length: usize) -> usize { + let request = unsafe { &*request }; + let body = request.inner.body(); + + let length = length.min(body.len()); + let chunk = body.take(length); + + unsafe { + std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut ffi::c_char, buffer, length); + } + length +} + +/// An HTTP request builder. Includes method, URL, headers, and body. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_request_builder_t { + inner: RequestBuilder, +} + +/// Convert a `RequestBuilder` into a `lh_request_builder_t`. +impl From for lh_request_builder_t { + fn from(inner: RequestBuilder) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_request_builder_t` into a `RequestBuilder`. +impl From<&lh_request_builder_t> for RequestBuilder { + fn from(builder: &lh_request_builder_t) -> RequestBuilder { + builder.inner.clone() + } +} + +/// Create a new `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_new() -> *mut lh_request_builder_t { + Box::into_raw(Box::new(RequestBuilder::new().into())) +} + +/// Free a `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// +/// // Do something... +/// +/// lh_request_builder_free(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} + +/// Create a new `lh_request_builder_t` from an existing `lh_request_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_t* request = lh_request_new("GET", "https://example.com", headers, "Hello, world!"); +/// lh_request_builder_t* builder = lh_request_builder_extend(request); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_extend(request: *const lh_request_t) -> *mut lh_request_builder_t { + let request = unsafe { &*request }; + Box::into_raw(Box::new(RequestBuilder::extend(&request.inner).into())) +} + +/// Set the method of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_method(builder, "GET"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_method( + builder: *mut lh_request_builder_t, + method: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().method(method).into())) +} + +/// Set the URL of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_url(builder, "https://example.com"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_url( + builder: *mut lh_request_builder_t, + url: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().url(&url).unwrap().into())) +} + +/// Add a header to the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_header(builder, "Content-Type", "text/plain"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_header( + builder: *mut lh_request_builder_t, + key: *const ffi::c_char, + value: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let key = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().header(key, value).into())) +} + +/// Set the body of the request. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// lh_request_builder_body(builder, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_body( + builder: *mut lh_request_builder_t, + body: *const ffi::c_char, +) -> *mut lh_request_builder_t { + let body = unsafe { CStr::from_ptr(body).to_bytes() }; + let builder = unsafe { &mut *builder }; + Box::into_raw(Box::new(builder.inner.clone().body(body).into())) +} + +/// Build a `lh_request_t` from a `lh_request_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_request_builder_t* builder = lh_request_builder_new(); +/// +/// // Populate the builder data... +/// +/// lh_request_t* request = lh_request_builder_build(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_request_builder_build(builder: *mut lh_request_builder_t) -> *mut lh_request_t { + let builder = unsafe { Box::from_raw(builder) }; + Box::into_raw(Box::new(builder.inner.build().into())) +} + +/// An HTTP response. Includes status code, headers, and body. +#[allow(non_camel_case_types)] +pub struct lh_response_t { + inner: Response, +} + +/// Convert a `Response` into a `lh_response_t`. +impl From for lh_response_t { + fn from(inner: Response) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_response_t` into a `Response`. +impl From<&lh_response_t> for Response { + fn from(response: &lh_response_t) -> Response { + response.inner.clone() + } +} + +/// Create a new `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { + let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; + let headers = unsafe { &*headers }; + Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str, "", None).into())) +} + +/// Free a `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// +/// // Do something... +/// +/// lh_response_free(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_free(response: *mut lh_response_t) { + if !response.is_null() { + unsafe { + drop(Box::from_raw(response)); + } + } +} + +/// Get the status code of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// uint16_t status = lh_response_status(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_status(response: *const lh_response_t) -> u16 { + let response = unsafe { &*response }; + response.inner.status() +} + +/// Get the headers of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// lh_headers_t* headers = lh_response_headers(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh_headers_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(response.inner.headers().clone().into())) +} + +/// Get the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// const char* body = lh_response_body(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { + let response = unsafe { &*response }; + CString::new(response.inner.body()).unwrap().into_raw() +} + +/// An HTTP response builder. Includes status, headers, body, log, and exception string. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_response_builder_t { + inner: ResponseBuilder, +} + +/// Convert a `ResponseBuilder` into a `lh_response_builder_t`. +impl From for lh_response_builder_t { + fn from(inner: ResponseBuilder) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_response_builder_t` into a `ResponseBuilder`. +impl From<&lh_response_builder_t> for ResponseBuilder { + fn from(builder: &lh_response_builder_t) -> ResponseBuilder { + builder.inner.clone() + } +} + +/// Create a new `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_new() -> *mut lh_response_builder_t { + Box::into_raw(Box::new(ResponseBuilder::new().into())) +} + +/// Free a `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// +/// // Do something... +/// +/// lh_response_builder_free(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_free(builder: *mut lh_response_builder_t) { + if !builder.is_null() { + unsafe { + drop(Box::from_raw(builder)); + } + } +} + +/// Create a new `lh_response_builder_t` from an existing `lh_response_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_t* response = lh_response_new(200, headers, "Hello, world!"); +/// lh_response_builder_t* builder = lh_response_builder_extend(response); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_extend(response: *const lh_response_t) -> *mut lh_response_builder_t { + let response = unsafe { &*response }; + Box::into_raw(Box::new(ResponseBuilder::extend(&response.inner).into())) +} + +/// Set the status code of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_status_code(builder, 200); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_status_code(builder: *mut lh_response_builder_t, status_code: u16) { + let builder = unsafe { &mut *builder }; + builder.inner.status(status_code); +} + +/// Add a header to the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_header(builder, "Content-Type", "text/plain"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_header(builder: *mut lh_response_builder_t, key: *const c_char, value: *const c_char) { + let builder = unsafe { &mut *builder }; + let key_str = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; + let value_str = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; + builder.inner.header(key_str, value_str); +} + +/// Set the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_body(builder, "Hello, world!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_body(builder: *mut lh_response_builder_t, body: *const c_char) { + let builder = unsafe { &mut *builder }; + let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; + builder.inner.body(body_str); +} + +/// Write to the body of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_body_write(builder, "Hello, world!", 13); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_body_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.body.put(data); + return len; +} + +/// Write to the log of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_log_write(builder, "Hello, world!", 13); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_log_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { + let builder = unsafe { &mut *builder }; + let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; + builder.inner.log.put(data); + builder.inner.log.put("\n".as_bytes()); + return len; +} + +/// Set the exception string of the response. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// lh_response_builder_exception(builder, "Something went wrong!"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_exception(builder: *mut lh_response_builder_t, exception: *const c_char) { + let builder = unsafe { &mut *builder }; + let exception_str = unsafe { CStr::from_ptr(exception).to_string_lossy().into_owned() }; + builder.inner.exception(exception_str); +} + +/// Build a `lh_response_t` from a `lh_response_builder_t`. +/// +/// # Examples +/// +/// ```c +/// lh_response_builder_t* builder = lh_response_builder_new(); +/// +/// // Populate the builder data... +/// +/// lh_response_t* response = lh_response_builder_build(builder); +/// ``` +#[no_mangle] +pub extern "C" fn lh_response_builder_build(builder: *const lh_response_builder_t) -> *mut lh_response_t { + let builder = unsafe { &*builder }; + Box::into_raw(Box::new(builder.inner.build().into())) +} + +/// An HTTP URL. Includes scheme, host, port, domain, origin, authority, username, password, path, query, fragment, and URI. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// ``` +#[allow(non_camel_case_types)] +pub struct lh_url_t { + inner: Url, +} + +/// Convert a `Url` into a `lh_url_t`. +impl From for lh_url_t { + fn from(inner: Url) -> Self { + Self { inner } + } +} + +/// Convert a `&lh_url_t` into a `Url`. +impl From<&lh_url_t> for Url { + fn from(url: &lh_url_t) -> Url { + url.inner.clone() + } +} + +/// Parse a URL into a `lh_url_t`. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_parse(url: *const c_char) -> *mut lh_url_t { + let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; + let url = Url::parse(&url).unwrap(); + Box::into_raw(Box::new(url.into())) +} + +/// Free a `lh_url_t`. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// +/// // Do something... +/// +/// lh_url_free(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_free(url: *mut lh_url_t) { + if !url.is_null() { + unsafe { + drop(Box::from_raw(url)); + } + } +} + +/// Get the scheme of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* scheme = lh_url_scheme(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.scheme()).unwrap().into_raw() +} + +/// Get the host of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* host = lh_url_host(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.host_str().unwrap_or("")).unwrap().into_raw() +} + +/// Get the port of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// uint16_t port = lh_url_port(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { + let url = unsafe { &*url }; + url.inner.port().unwrap_or(0) +} + +/// Get the domain of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* domain = lh_url_domain(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.domain().unwrap_or("")).unwrap().into_raw() +} + +/// Get the origin of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* origin = lh_url_origin(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + let origin = match url.inner.origin() { + url::Origin::Opaque(_) => { + format!("{}://", url.inner.scheme()) + }, + url::Origin::Tuple(scheme, host, port) => { + format!("{}://{}:{}", scheme, host, port) + } + }; + CString::new(origin.as_str()).unwrap().into_raw() +} + +/// Check if the URL has an authority. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// bool has_authority = lh_url_has_authority(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { + let url = unsafe { &*url }; + url.inner.has_authority() +} + +/// Get the authority of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* authority = lh_url_authority(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.authority()).unwrap().into_raw() +} + +/// Get the username of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* username = lh_url_username(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.username()).unwrap().into_raw() +} + +/// Get the password of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* password = lh_url_password(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.password().unwrap_or("")).unwrap().into_raw() +} + +/// Get the path of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* path = lh_url_path(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.path()).unwrap().into_raw() +} + +/// Get the query of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* query = lh_url_query(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.query().unwrap_or("")).unwrap().into_raw() +} + +/// Get the fragment of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* fragment = lh_url_fragment(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.fragment().unwrap_or("")).unwrap().into_raw() +} + +/// Get the URI of the URL. +/// +/// # Examples +/// +/// ```c +/// lh_url_t* url = lh_url_parse("https://example.com:8080/path/to/resource?query#fragment"); +/// const char* uri = lh_url_uri(url); +/// ``` +#[no_mangle] +pub extern "C" fn lh_url_uri(url: *const lh_url_t) -> *const c_char { + let url = unsafe { &*url }; + CString::new(url.inner.as_str()).unwrap().into_raw() +} diff --git a/crates/lang_handler/src/handler.rs b/crates/lang_handler/src/handler.rs index 4f9283ef..25756588 100644 --- a/crates/lang_handler/src/handler.rs +++ b/crates/lang_handler/src/handler.rs @@ -1,7 +1,57 @@ use crate::{Request, Response}; +/// Enables a type to support handling HTTP requests. +/// +/// # Example +/// +/// ``` +/// use lang_handler::{Handler, Request, Response, ResponseBuilder}; +/// +/// struct MyHandler; +/// +/// impl Handler for MyHandler { +/// type Error = String; +/// +/// fn handle(&self, request: Request) -> Result { +/// let response = Response::builder() +/// .status(200) +/// .header("Content-Type", "text/plain") +/// .body(request.body()) +/// .build(); +/// +/// Ok(response) +/// } +/// } pub trait Handler { type Error; + /// Handles an HTTP request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Handler, Request, Response}; + /// + /// # struct MyHandler; + /// # impl Handler for MyHandler { + /// # type Error = String; + /// # + /// # fn handle(&self, request: Request) -> Result { + /// # let response = Response::builder() + /// # .status(200) + /// # .header("Content-Type", "text/plain") + /// # .body(request.body()) + /// # .build(); + /// # + /// # Ok(response) + /// # } + /// # } + /// let handler = MyHandler; + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("invalid url") + /// .build(); + /// let response = handler.handle(request).unwrap(); + /// ``` fn handle(&self, request: Request) -> Result; } diff --git a/crates/lang_handler/src/headers.rs b/crates/lang_handler/src/headers.rs index d5acde83..9d08944d 100644 --- a/crates/lang_handler/src/headers.rs +++ b/crates/lang_handler/src/headers.rs @@ -1,13 +1,49 @@ use std::collections::HashMap; +/// A multi-map of HTTP headers. +/// +/// # Examples +/// +/// ``` +/// use lang_handler::Headers; +/// +/// let mut headers = Headers::new(); +/// headers.set("Content-Type", "text/plain"); +/// assert_eq!(headers.get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// ``` #[derive(Debug, Clone)] pub struct Headers(HashMap>); impl Headers { + /// Creates a new `Headers` instance. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let headers = Headers::new(); + /// ``` pub fn new() -> Self { Headers(HashMap::new()) } + /// Returns the values associated with a header field. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// assert_eq!(headers.get("Accept"), Some(&vec![ + /// "text/plain".to_string(), + /// "application/json".to_string() + /// ])); + /// ``` pub fn get(&self, key: K) -> Option<&Vec> where K: AsRef, @@ -15,6 +51,17 @@ impl Headers { self.0.get(key.as_ref()) } + /// Sets a header field with the given value. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Content-Type", "text/plain"); + /// assert_eq!(headers.get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// ``` pub fn set(&mut self, key: K, value: V) where K: Into, @@ -26,6 +73,18 @@ impl Headers { .push(value.into()); } + /// Removes a header field. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Content-Type", "text/plain"); + /// headers.remove("Content-Type"); + /// assert_eq!(headers.get("Content-Type"), None); + /// ``` pub fn remove(&mut self, key: K) where K: AsRef, @@ -33,126 +92,41 @@ impl Headers { self.0.remove(key.as_ref()); } + /// Returns an iterator over the headers. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// for (key, values) in headers.iter() { + /// println!("{}: {:?}", key, values); + /// } + /// ``` pub fn iter(&self) -> impl Iterator)> { self.0.iter() } + /// Returns an iterator over the header values. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::Headers; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/plain"); + /// headers.set("Accept", "application/json"); + /// + /// for value in headers.iter_values() { + /// println!("{:?}", value); + /// } + /// ``` pub fn iter_values(&self) -> impl Iterator { self.0.values().flatten() } } - -#[allow(non_camel_case_types)] -pub struct lh_headers_t { - inner: Headers, -} - -impl From for lh_headers_t { - fn from(inner: Headers) -> Self { - Self { inner } - } -} - -impl From<&lh_headers_t> for Headers { - fn from(headers: &lh_headers_t) -> Headers { - headers.inner.clone() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_new() -> *mut lh_headers_t { - let headers = Headers::new(); - Box::into_raw(Box::new(headers.into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_free(headers: *mut lh_headers_t) { - if !headers.is_null() { - unsafe { - drop(Box::from_raw(headers)); - } - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_count(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> usize { - let headers = unsafe { - assert!(!headers.is_null()); - &*headers - }; - let key = unsafe { - assert!(!key.is_null()); - std::ffi::CStr::from_ptr(key).to_str().unwrap() - }; - match headers.inner.get(key) { - Some(value) => value.len(), - None => 0 - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_get(headers: *const lh_headers_t, key: *const std::os::raw::c_char) -> *const std::os::raw::c_char { - let headers = unsafe { - assert!(!headers.is_null()); - &*headers - }; - let key = unsafe { - assert!(!key.is_null()); - std::ffi::CStr::from_ptr(key).to_str().unwrap() - }; - match headers.inner.get(key) { - Some(values) => { - if values.len() > 0 { - values[values.len() - 1].as_ptr() as *const std::os::raw::c_char - } else { - std::ptr::null() - } - }, - None => std::ptr::null() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_get_nth(headers: *const lh_headers_t, key: *const std::os::raw::c_char, index: usize) -> *const std::os::raw::c_char { - let headers = unsafe { - assert!(!headers.is_null()); - &*headers - }; - let key = unsafe { - assert!(!key.is_null()); - std::ffi::CStr::from_ptr(key).to_str().unwrap() - }; - match headers.inner.get(key) { - Some(values) => { - if index < values.len() { - values[index].as_ptr() as *const std::os::raw::c_char - } else { - std::ptr::null() - } - }, - None => std::ptr::null() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_headers_set(headers: *mut lh_headers_t, key: *const std::os::raw::c_char, value: *const std::os::raw::c_char) { - let headers = unsafe { - assert!(!headers.is_null()); - &mut *headers - }; - let key = unsafe { - assert!(!key.is_null()); - std::ffi::CStr::from_ptr(key).to_str().unwrap().to_string() - }; - let value = unsafe { - assert!(!value.is_null()); - std::ffi::CStr::from_ptr(value).to_str().unwrap().to_string() - }; - headers.inner.set(key, value); -} diff --git a/crates/lang_handler/src/lib.rs b/crates/lang_handler/src/lib.rs index d0cd60d8..d28698ed 100644 --- a/crates/lang_handler/src/lib.rs +++ b/crates/lang_handler/src/lib.rs @@ -1,20 +1,14 @@ +#[cfg(feature = "c")] +mod ffi; mod handler; mod headers; mod request; mod response; -mod url; - -use std::ffi::{CString, c_char}; +#[cfg(feature = "c")] +pub use ffi::*; pub use handler::Handler; -pub use headers::{Headers, lh_headers_t}; -pub use request::{Request, RequestBuilder, lh_request_t, lh_request_builder_t}; -pub use response::{Response, ResponseBuilder, lh_response_t, lh_response_builder_t}; -pub use url::{Url, lh_url_t}; - -#[no_mangle] -pub extern "C" fn lh_reclaim_str(url: *const c_char) { - unsafe { - drop(CString::from_raw(url as *mut c_char)); - } -} +pub use headers::Headers; +pub use request::{Request, RequestBuilder}; +pub use response::{Response, ResponseBuilder}; +pub use url::Url; diff --git a/crates/lang_handler/src/request.rs b/crates/lang_handler/src/request.rs index 38d67482..9938c13b 100644 --- a/crates/lang_handler/src/request.rs +++ b/crates/lang_handler/src/request.rs @@ -1,15 +1,35 @@ -#[cfg(feature = "c")] -use std::{ffi, ffi::{CStr, CString}}; - use std::fmt::Debug; -use bytes::{Buf, Bytes, BytesMut}; +use bytes::{Bytes, BytesMut}; use url::{ParseError, Url}; use crate::Headers; -use crate::headers::lh_headers_t; -use crate::url::lh_url_t; +/// Represents an HTTP request. Includes the method, URL, headers, and body. +/// +/// # Examples +/// +/// ``` +/// use lang_handler::{Request, Headers}; +/// +/// let request = Request::builder() +/// .method("POST") +/// .url("http://example.com/test.php").expect("invalid url") +/// .header("Accept", "text/html") +/// .header("Accept", "application/json") +/// .header("Host", "example.com") +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(request.method(), "POST"); +/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); +/// assert_eq!(request.headers().get("Accept"), Some(&vec![ +/// "text/html".to_string(), +/// "application/json".to_string() +/// ])); +/// assert_eq!(request.headers().get("Host"), Some(&vec!["example.com".to_string()])); +/// assert_eq!(request.body(), "Hello, World!"); +/// ``` #[derive(Clone, Debug)] pub struct Request { method: String, @@ -20,6 +40,22 @@ pub struct Request { } impl Request { + /// Creates a new `Request` with the given method, URL, headers, and body. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, Headers}; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/html"); + /// + /// let request = Request::new( + /// "POST".to_string(), + /// "http://example.com/test.php".parse().unwrap(), + /// headers, + /// "Hello, World!" + /// ); pub fn new>(method: String, url: Url, headers: Headers, body: T) -> Self { Self { method, @@ -29,31 +65,165 @@ impl Request { } } + /// Creates a new `RequestBuilder` to build a `Request`. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, RequestBuilder}; + /// + /// let request = Request::builder() + /// .method("POST") + /// .url("http://example.com/test.php").expect("invalid url") + /// .header("Content-Type", "text/html") + /// .header("Content-Length", 13.to_string()) + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(request.method(), "POST"); + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); + /// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["13".to_string()])); + /// assert_eq!(request.body(), "Hello, World!"); + /// ``` pub fn builder() -> RequestBuilder { RequestBuilder::new() } + /// Creates a new `RequestBuilder` to extend a `Request`. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, RequestBuilder}; + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com/test.php").expect("invalid url") + /// .header("Content-Type", "text/plain") + /// .build(); + /// + /// let extended = request.extend() + /// .method("POST") + /// .header("Content-Length", 12.to_string()) + /// .body("Hello, World") + /// .build(); + /// + /// assert_eq!(extended.method(), "POST"); + /// assert_eq!(extended.url().as_str(), "http://example.com/test.php"); + /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.headers().get("Content-Length"), Some(&vec!["12".to_string()])); + /// assert_eq!(extended.body(), "Hello, World"); + /// ``` pub fn extend(&self) -> RequestBuilder { RequestBuilder::extend(self) } + /// Returns the method of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, Headers}; + /// + /// let request = Request::new( + /// "POST".to_string(), + /// "http://example.com/test.php".parse().unwrap(), + /// Headers::new(), + /// "Hello, World!" + /// ); + /// + /// assert_eq!(request.method(), "POST"); + /// ``` pub fn method(&self) -> &str { &self.method } + /// Returns the URL of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, Headers}; + /// + /// let request = Request::new( + /// "POST".to_string(), + /// "http://example.com/test.php".parse().unwrap(), + /// Headers::new(), + /// "Hello, World!" + /// ); + /// + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); + /// ``` pub fn url(&self) -> &Url { &self.url } + /// Returns the headers of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, Headers}; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/html"); + /// + /// let request = Request::new( + /// "POST".to_string(), + /// "http://example.com/test.php".parse().unwrap(), + /// headers, + /// "Hello, World!" + /// ); + /// + /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// ``` pub fn headers(&self) -> &Headers { &self.headers } + /// Returns the body of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Request, Headers}; + /// + /// let request = Request::new( + /// "POST".to_string(), + /// "http://example.com/test.php".parse().unwrap(), + /// Headers::new(), + /// "Hello, World!" + /// ); + /// + /// assert_eq!(request.body(), "Hello, World!"); + /// ``` pub fn body(&self) -> Bytes { self.body.clone() } } +/// Builds an HTTP request. +/// +/// # Examples +/// +/// ``` +/// use lang_handler::{Request, RequestBuilder}; +/// +/// let request = Request::builder() +/// .method("POST") +/// .url("http://example.com/test.php").expect("invalid url") +/// .header("Content-Type", "text/html") +/// .header("Content-Length", 13.to_string()) +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(request.method(), "POST"); +/// assert_eq!(request.url().as_str(), "http://example.com/test.php"); +/// assert_eq!(request.headers().get("Content-Type"), Some(&vec!["text/html".to_string()])); +/// assert_eq!(request.headers().get("Content-Length"), Some(&vec!["13".to_string()])); +/// assert_eq!(request.body(), "Hello, World!"); +/// ``` #[derive(Clone)] pub struct RequestBuilder { method: Option, @@ -63,6 +233,15 @@ pub struct RequestBuilder { } impl RequestBuilder { + /// Creates a new `RequestBuilder`. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let builder = RequestBuilder::new(); + /// ``` pub fn new() -> Self { Self { method: None, @@ -72,6 +251,31 @@ impl RequestBuilder { } } + /// Creates a new `RequestBuilder` to extend an existing `Request`. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::{Headers, Request, RequestBuilder}; + /// + /// let mut headers = Headers::new(); + /// headers.set("Accept", "text/html"); + /// + /// let request = Request::new( + /// "GET".to_string(), + /// "http://example.com".parse().unwrap(), + /// headers, + /// "Hello, World!" + /// ); + /// + /// let extended = RequestBuilder::extend(&request) + /// .build(); + /// + /// assert_eq!(extended.method(), "GET"); + /// assert_eq!(extended.url().as_str(), "http://example.com/"); + /// assert_eq!(extended.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// assert_eq!(extended.body(), "Hello, World!"); + /// ``` pub fn extend(request: &Request) -> Self { Self { method: Some(request.method().into()), @@ -81,11 +285,37 @@ impl RequestBuilder { } } + /// Sets the method of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .method("POST") + /// .build(); + /// + /// assert_eq!(request.method(), "POST"); + /// ``` pub fn method>(mut self, method: T) -> Self { self.method = Some(method.into()); self } + /// Sets the URL of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .url("http://example.com/test.php").expect("invalid url") + /// .build(); + /// + /// assert_eq!(request.url().as_str(), "http://example.com/test.php"); + /// ``` pub fn url(mut self, url: T) -> Result where T: Into @@ -99,6 +329,19 @@ impl RequestBuilder { } } + /// Sets a header of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .header("Accept", "text/html") + /// .build(); + /// + /// assert_eq!(request.headers().get("Accept"), Some(&vec!["text/html".to_string()])); + /// ``` pub fn header(mut self, key: K, value: V) -> Self where K: Into, @@ -108,201 +351,45 @@ impl RequestBuilder { self } + /// Sets the body of the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(request.body(), "Hello, World!"); + /// ``` pub fn body>(mut self, body: T) -> Self { self.body = body.into(); self } + /// Builds the request. + /// + /// # Examples + /// + /// ``` + /// use lang_handler::RequestBuilder; + /// + /// let request = RequestBuilder::new() + /// .build(); + /// + /// assert_eq!(request.method(), "GET"); + /// assert_eq!(request.url().as_str(), "http://example.com/"); + /// assert_eq!(request.body(), ""); + /// ``` pub fn build(self) -> Request { Request { method: self.method.unwrap_or_else(|| "GET".to_string()), - url: self.url.unwrap_or_else(|| Url::parse("/").unwrap()), + // TODO: This is wrong. Return a Result instead. + url: self.url.unwrap_or_else(|| Url::parse("http://example.com").unwrap()), headers: self.headers, body: self.body.freeze(), } } } - -#[allow(non_camel_case_types)] -pub struct lh_request_t { - inner: Request, -} - -impl From for lh_request_t { - fn from(inner: Request) -> Self { - Self { inner } - } -} - -impl From<&lh_request_t> for Request { - fn from(request: &lh_request_t) -> Request { - request.inner.clone() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_new( - method: *const ffi::c_char, - url: *const ffi::c_char, - headers: *mut lh_headers_t, - body: *const ffi::c_char, -) -> *mut lh_request_t { - let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; - let url_str = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; - let url = Url::parse(&url_str).unwrap(); - let body = if body.is_null() { - None - } else { - Some(unsafe { CStr::from_ptr(body).to_bytes() }) - }; - let headers = unsafe { &*headers }; - let request = Request::new(method, url, headers.into(), body.unwrap_or(&[])); - Box::into_raw(Box::new(request.into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_method(request: *const lh_request_t) -> *const ffi::c_char { - let request = unsafe { &*request }; - CString::new(request.inner.method()).unwrap().into_raw() -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_url(request: *const lh_request_t) -> *mut lh_url_t { - let request = unsafe { &*request }; - Box::into_raw(Box::new(request.inner.url().clone().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_headers(request: *const lh_request_t) -> *mut lh_headers_t { - let request = unsafe { &*request }; - Box::into_raw(Box::new(request.inner.headers().clone().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_body(request: *const lh_request_t) -> *const ffi::c_char { - let request = unsafe { &*request }; - CString::new(request.inner.body()).unwrap().into_raw() -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_body_read(request: *const lh_request_t, buffer: *mut ffi::c_char, length: usize) -> usize { - let request = unsafe { &*request }; - let body = request.inner.body(); - - let length = length.min(body.len()); - let chunk = body.take(length); - - unsafe { - std::ptr::copy_nonoverlapping(chunk.chunk().as_ptr() as *mut ffi::c_char, buffer, length); - } - length -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_free(request: *mut lh_request_t) { - if !request.is_null() { - unsafe { - drop(Box::from_raw(request)); - } - } -} - -#[allow(non_camel_case_types)] -pub struct lh_request_builder_t { - inner: RequestBuilder, -} - -impl From for lh_request_builder_t { - fn from(inner: RequestBuilder) -> Self { - Self { inner } - } -} - -impl From<&lh_request_builder_t> for RequestBuilder { - fn from(builder: &lh_request_builder_t) -> RequestBuilder { - builder.inner.clone() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_new() -> *mut lh_request_builder_t { - Box::into_raw(Box::new(RequestBuilder::new().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_extend(request: *const lh_request_t) -> *mut lh_request_builder_t { - let request = unsafe { &*request }; - Box::into_raw(Box::new(RequestBuilder::extend(&request.inner).into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_method( - builder: *mut lh_request_builder_t, - method: *const ffi::c_char, -) -> *mut lh_request_builder_t { - let method = unsafe { CStr::from_ptr(method).to_string_lossy().into_owned() }; - let builder = unsafe { &mut *builder }; - Box::into_raw(Box::new(builder.inner.clone().method(method).into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_url( - builder: *mut lh_request_builder_t, - url: *const ffi::c_char, -) -> *mut lh_request_builder_t { - let url = unsafe { CStr::from_ptr(url).to_string_lossy().into_owned() }; - let builder = unsafe { &mut *builder }; - Box::into_raw(Box::new(builder.inner.clone().url(&url).unwrap().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_header( - builder: *mut lh_request_builder_t, - key: *const ffi::c_char, - value: *const ffi::c_char, -) -> *mut lh_request_builder_t { - let key = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; - let value = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; - let builder = unsafe { &mut *builder }; - Box::into_raw(Box::new(builder.inner.clone().header(key, value).into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_body( - builder: *mut lh_request_builder_t, - body: *const ffi::c_char, -) -> *mut lh_request_builder_t { - let body = unsafe { CStr::from_ptr(body).to_bytes() }; - let builder = unsafe { &mut *builder }; - Box::into_raw(Box::new(builder.inner.clone().body(body).into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_build(builder: *mut lh_request_builder_t) -> *mut lh_request_t { - let builder = unsafe { Box::from_raw(builder) }; - Box::into_raw(Box::new(builder.inner.build().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_request_builder_free(builder: *mut lh_request_builder_t) { - if !builder.is_null() { - unsafe { - drop(Box::from_raw(builder)); - } - } -} diff --git a/crates/lang_handler/src/response.rs b/crates/lang_handler/src/response.rs index f3efa8ef..982c6d91 100644 --- a/crates/lang_handler/src/response.rs +++ b/crates/lang_handler/src/response.rs @@ -1,11 +1,24 @@ -#[cfg(feature = "c")] -use std::ffi::{CStr, CString, c_char}; - -use bytes::{Bytes, BytesMut, BufMut}; +use bytes::{Bytes, BytesMut}; use crate::Headers; -use crate::headers::lh_headers_t; +/// Represents an HTTP response. This includes the status code, headers, body, log, and exception. +/// +/// # Example +/// +/// ``` +/// use lang_handler::{Response, ResponseBuilder}; +/// +/// let response = Response::builder() +/// .status(200) +/// .header("Content-Type", "text/plain") +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(response.status(), 200); +/// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// assert_eq!(response.body(), "Hello, World!"); +/// ``` #[derive(Clone, Debug)] pub struct Response { status: u16, @@ -17,6 +30,24 @@ pub struct Response { } impl Response { + /// Creates a new response with the given status code, headers, body, log, and exception. + /// + /// # Example + /// + /// ``` + /// use lang_handler::{Response, Headers}; + /// + /// let mut headers = Headers::new(); + /// headers.set("Content-Type", "text/plain"); + /// + /// let response = Response::new(200, headers, "Hello, World!", "log", Some("exception".to_string())); + /// + /// assert_eq!(response.status(), 200); + /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(response.body(), "Hello, World!"); + /// assert_eq!(response.log(), "log"); + /// assert_eq!(response.exception(), Some(&"exception".to_string())); + /// ``` pub fn new(status: u16, headers: Headers, body: B, log: L, exception: Option) -> Self where B: Into, @@ -31,45 +62,178 @@ impl Response { } } + /// Creates a new response builder. + /// + /// # Example + /// + /// ``` + /// use lang_handler::Response; + /// + /// let response = Response::builder() + /// .status(200) + /// .header("Content-Type", "text/plain") + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(response.status(), 200); + /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(response.body(), "Hello, World!"); + /// ``` pub fn builder() -> ResponseBuilder { ResponseBuilder::new() } + /// Create a new response builder that extends the given response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::{Response, ResponseBuilder}; + /// + /// let response = Response::builder() + /// .status(200) + /// .header("Content-Type", "text/plain") + /// .body("Hello, World!") + /// .build(); + /// + /// let extended = response.extend() + /// .status(201) + /// .build(); + /// + /// assert_eq!(extended.status(), 201); + /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.body(), "Hello, World!"); + /// ``` pub fn extend(&self) -> ResponseBuilder { ResponseBuilder::extend(self) } + /// Returns the status code of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::Response; + /// + /// let response = Response::builder() + /// .status(200) + /// .build(); + /// + /// assert_eq!(response.status(), 200); + /// ``` pub fn status(&self) -> u16 { self.status } + /// Returns the headers of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::{Response, Headers}; + /// + /// let response = Response::builder() + /// .status(200) + /// .header("Content-Type", "text/plain") + /// .build(); + /// + /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// ``` pub fn headers(&self) -> &Headers { &self.headers } + /// Returns the body of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::Response; + /// + /// let response = Response::builder() + /// .status(200) + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(response.body(), "Hello, World!"); + /// ``` pub fn body(&self) -> Bytes { self.body.clone() } + /// Returns the log of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::Response; + /// + /// let response = Response::builder() + /// .status(200) + /// .log("log") + /// .build(); + /// + /// assert_eq!(response.log(), "log"); + /// ``` pub fn log(&self) -> Bytes { self.log.clone() } + /// Returns the exception of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::Response; + /// + /// let response = Response::builder() + /// .status(200) + /// .exception("exception") + /// .build(); + /// + /// assert_eq!(response.exception(), Some(&"exception".to_string())); + /// ``` pub fn exception(&self) -> Option<&String> { self.exception.as_ref() } } +/// A builder for creating an HTTP response. +/// +/// # Example +/// +/// ``` +/// use lang_handler::{Response, ResponseBuilder}; +/// +/// let response = Response::builder() +/// .status(200) +/// .header("Content-Type", "text/plain") +/// .body("Hello, World!") +/// .build(); +/// +/// assert_eq!(response.status(), 200); +/// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); +/// assert_eq!(response.body(), "Hello, World!"); +/// ``` #[derive(Clone)] pub struct ResponseBuilder { status: Option, headers: Headers, - body: BytesMut, - log: BytesMut, + pub(crate) body: BytesMut, + pub(crate) log: BytesMut, exception: Option, } impl ResponseBuilder { + /// Creates a new response builder. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let builder = ResponseBuilder::new(); + /// ``` pub fn new() -> Self { ResponseBuilder { status: None, @@ -80,6 +244,27 @@ impl ResponseBuilder { } } + /// Creates a new response builder that extends the given response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::{Response, ResponseBuilder}; + /// + /// let response = Response::builder() + /// .status(200) + /// .header("Content-Type", "text/plain") + /// .body("Hello, World!") + /// .build(); + /// + /// let extended = response.extend() + /// .status(201) + /// .build(); + /// + /// assert_eq!(extended.status(), 201); + /// assert_eq!(extended.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// assert_eq!(extended.body(), "Hello, World!"); + /// ``` pub fn extend(response: &Response) -> Self { ResponseBuilder { status: Some(response.status), @@ -90,11 +275,37 @@ impl ResponseBuilder { } } - pub fn status_code(&mut self, status: u16) -> &mut Self { + /// Sets the status code of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let response = ResponseBuilder::new() + /// .status(300) + /// .build(); + /// + /// assert_eq!(response.status(), 300); + /// ``` + pub fn status(&mut self, status: u16) -> &mut Self { self.status = Some(status); self } + /// Sets the headers of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let response = ResponseBuilder::new() + /// .header("Content-Type", "text/plain") + /// .build(); + /// + /// assert_eq!(response.headers().get("Content-Type"), Some(&vec!["text/plain".to_string()])); + /// ``` pub fn header(&mut self, key: K, value: V) -> &mut Self where K: Into, @@ -104,21 +315,75 @@ impl ResponseBuilder { self } + /// Sets the body of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let builder = ResponseBuilder::new() + /// .body("Hello, World!") + /// .build(); + /// + /// assert_eq!(builder.body(), "Hello, World!"); + /// ``` pub fn body>(&mut self, body: B) -> &mut Self { self.body = body.into(); self } + /// Sets the log of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let builder = ResponseBuilder::new() + /// .log("log") + /// .build(); + /// + /// assert_eq!(builder.log(), "log"); + /// ``` pub fn log>(&mut self, log: L) -> &mut Self { self.log = log.into(); self } + /// Sets the exception of the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let builder = ResponseBuilder::new() + /// .exception("exception") + /// .build(); + /// + /// assert_eq!(builder.exception(), Some(&"exception".to_string())); + /// ``` pub fn exception>(&mut self, exception: E) -> &mut Self { self.exception = Some(exception.into()); self } + /// Builds the response. + /// + /// # Example + /// + /// ``` + /// use lang_handler::ResponseBuilder; + /// + /// let response = ResponseBuilder::new() + /// .build(); + /// + /// assert_eq!(response.status(), 200); + /// assert_eq!(response.body(), ""); + /// assert_eq!(response.log(), ""); + /// assert_eq!(response.exception(), None); + /// ``` pub fn build(&self) -> Response { Response { status: self.status.unwrap_or(200), @@ -129,147 +394,3 @@ impl ResponseBuilder { } } } - -#[allow(non_camel_case_types)] -pub struct lh_response_t { - inner: Response, -} - -impl From for lh_response_t { - fn from(inner: Response) -> Self { - Self { inner } - } -} - -impl From<&lh_response_t> for Response { - fn from(response: &lh_response_t) -> Response { - response.inner.clone() - } -} - -// #[cfg(feature = "c")] -// #[no_mangle] -// pub extern "C" fn lh_response_new(status_code: u16, headers: *mut lh_headers_t, body: *const c_char) -> *mut lh_response_t { -// let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; -// let headers = unsafe { &*headers }; -// Box::into_raw(Box::new(Response::new(status_code, headers.into(), body_str).into())) -// } - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_free(response: *mut lh_response_t) { - if !response.is_null() { - unsafe { - drop(Box::from_raw(response)); - } - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_status(response: *const lh_response_t) -> u16 { - let response = unsafe { &*response }; - response.inner.status() -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_headers(response: *const lh_response_t) -> *mut lh_headers_t { - let response = unsafe { &*response }; - Box::into_raw(Box::new(response.inner.headers().clone().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_body(response: *const lh_response_t) -> *const c_char { - let response = unsafe { &*response }; - CString::new(response.inner.body()).unwrap().into_raw() -} - -#[allow(non_camel_case_types)] -pub struct lh_response_builder_t { - inner: ResponseBuilder, -} - -impl From for lh_response_builder_t { - fn from(inner: ResponseBuilder) -> Self { - Self { inner } - } -} - -impl From<&lh_response_builder_t> for ResponseBuilder { - fn from(builder: &lh_response_builder_t) -> ResponseBuilder { - builder.inner.clone() - } -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_new() -> *mut lh_response_builder_t { - Box::into_raw(Box::new(ResponseBuilder::new().into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_extend(response: *const lh_response_t) -> *mut lh_response_builder_t { - let response = unsafe { &*response }; - Box::into_raw(Box::new(ResponseBuilder::extend(&response.inner).into())) -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_status_code(builder: *mut lh_response_builder_t, status_code: u16) { - let builder = unsafe { &mut *builder }; - builder.inner.status_code(status_code); -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_header(builder: *mut lh_response_builder_t, key: *const c_char, value: *const c_char) { - let builder = unsafe { &mut *builder }; - let key_str = unsafe { CStr::from_ptr(key).to_string_lossy().into_owned() }; - let value_str = unsafe { CStr::from_ptr(value).to_string_lossy().into_owned() }; - builder.inner.header(key_str, value_str); -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_body(builder: *mut lh_response_builder_t, body: *const c_char) { - let builder = unsafe { &mut *builder }; - let body_str = unsafe { CStr::from_ptr(body).to_bytes() }; - builder.inner.body(body_str); -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_body_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { - let builder = unsafe { &mut *builder }; - let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; - builder.inner.body.put(data); - return len; -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_log_write(builder: *mut lh_response_builder_t, data: *const c_char, len: usize) -> usize { - let builder = unsafe { &mut *builder }; - let data = unsafe { std::slice::from_raw_parts(data as *const u8, len) }; - builder.inner.log.put(data); - builder.inner.log.put("\n".as_bytes()); - return len; -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_exception(builder: *mut lh_response_builder_t, exception: *const c_char) { - let builder = unsafe { &mut *builder }; - let exception_str = unsafe { CStr::from_ptr(exception).to_string_lossy().into_owned() }; - builder.inner.exception(exception_str); -} - -#[cfg(feature = "c")] -#[no_mangle] -pub extern "C" fn lh_response_builder_build(builder: *const lh_response_builder_t) -> *mut lh_response_t { - let builder = unsafe { &*builder }; - Box::into_raw(Box::new(builder.inner.build().into())) -} diff --git a/crates/lang_handler/src/url.rs b/crates/lang_handler/src/url.rs deleted file mode 100644 index 757f568f..00000000 --- a/crates/lang_handler/src/url.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::ffi::{CString, c_char}; - -pub use url::Url; - -#[allow(non_camel_case_types)] -pub struct lh_url_t { - inner: Url, -} - -impl From for lh_url_t { - fn from(inner: Url) -> Self { - Self { inner } - } -} - -impl From<&lh_url_t> for Url { - fn from(url: &lh_url_t) -> Url { - url.inner.clone() - } -} - -#[no_mangle] -pub extern "C" fn lh_url_scheme(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.scheme()).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_host(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.host_str().unwrap_or("")).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_domain(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.domain().unwrap_or("")).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_port(url: *const lh_url_t) -> u16 { - let url = unsafe { &*url }; - url.inner.port().unwrap_or(0) -} - -#[no_mangle] -pub extern "C" fn lh_url_origin(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - let origin = match url.inner.origin() { - url::Origin::Opaque(_) => { - format!("{}://", url.inner.scheme()) - }, - url::Origin::Tuple(scheme, host, port) => { - format!("{}://{}:{}", scheme, host, port) - } - }; - CString::new(origin.as_str()).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_has_authority(url: *const lh_url_t) -> bool { - let url = unsafe { &*url }; - url.inner.has_authority() -} - -#[no_mangle] -pub extern "C" fn lh_url_authority(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.authority()).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_username(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.username()).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_password(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.password().unwrap_or("")).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_path(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.path()).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_query(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.query().unwrap_or("")).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_fragment(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.fragment().unwrap_or("")).unwrap().into_raw() -} - -#[no_mangle] -pub extern "C" fn lh_url_uri(url: *const lh_url_t) -> *const c_char { - let url = unsafe { &*url }; - CString::new(url.inner.as_str()).unwrap().into_raw() -} From a8626b0d41f0359782fb90bd9f33a95ec96cfe24 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Thu, 13 Mar 2025 23:52:50 +0800 Subject: [PATCH 7/9] Add docs to php crate --- crates/php/src/embed.rs | 99 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 90 insertions(+), 9 deletions(-) diff --git a/crates/php/src/embed.rs b/crates/php/src/embed.rs index 232ba550..5e7e7855 100644 --- a/crates/php/src/embed.rs +++ b/crates/php/src/embed.rs @@ -8,15 +8,8 @@ use lang_handler::{Handler, Request, Response}; use crate::sys; -#[derive(Debug, Clone)] -pub struct Embed { - code: String, - filename: Option, -} - -unsafe impl Send for Embed {} -unsafe impl Sync for Embed {} - +// This is a helper to ensure that PHP is initialized and deinitialized at the +// appropriate times. struct PhpInit; impl PhpInit { @@ -48,7 +41,44 @@ impl Drop for PhpInit { static PHP_INIT: OnceLock = OnceLock::new(); +/// Embed a PHP script into a Rust application to handle HTTP requests. +/// +/// # Examples +/// +/// ``` +/// use php::{Embed, Handler, Request, Response}; +/// +/// let handler = Embed::new("echo 'Hello, world!';", Some("example.php")); +/// +/// let request = Request::builder() +/// .method("GET") +/// .url("http://example.com").expect("invalid url") +/// .build(); +/// +/// let response = handler.handle(request).unwrap(); +/// +/// assert_eq!(response.status(), 200); +/// assert_eq!(response.body(), "Hello, world!"); +/// ``` +#[derive(Debug, Clone)] +pub struct Embed { + code: String, + filename: Option, +} + +unsafe impl Send for Embed {} +unsafe impl Sync for Embed {} + impl Embed { + /// Creates a new `Embed` instance. + /// + /// # Examples + /// + /// ``` + /// use php::Embed; + /// + /// let embed = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// ``` pub fn new(code: C, filename: Option) -> Self where C: Into, @@ -57,6 +87,16 @@ impl Embed { Embed::new_with_argv::(code, filename, vec![]) } + /// Creates a new `Embed` instance with command-line arguments. + /// + /// # Examples + /// + /// ``` + /// use php::Embed; + /// + /// let args = std::env::args(); + /// let embed = Embed::new_with_args("echo $argv[1];", Some("example.php"), args); + /// ``` pub fn new_with_args(code: C, filename: Option, args: Args) -> Self where C: Into, @@ -65,6 +105,28 @@ impl Embed { Embed::new_with_argv(code, filename, args.collect()) } + /// Creates a new `Embed` instance with command-line arguments. + /// + /// # Examples + /// + /// ``` + /// use php::{Embed, Handler, Request, Response}; + /// + /// let embed = Embed::new_with_argv("echo $_SERVER['argv'][0];", Some("example.php"), vec![ + /// "Hello, world!" + /// ]); + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("invalid url") + /// .build(); + /// + /// let response = embed.handle(request).unwrap(); + /// + /// assert_eq!(response.status(), 200); + /// # // TODO: Uncomment when argv gets passed through correctly. + /// # // assert_eq!(response.body(), "Hello, world!"); + /// ``` pub fn new_with_argv(code: C, filename: Option, argv: Vec) -> Self where C: Into, @@ -83,6 +145,25 @@ impl Embed { impl Handler for Embed { type Error = String; + /// Handles an HTTP request. + /// + /// # Examples + /// + /// ``` + /// use php::{Embed, Handler, Request, Response}; + /// + /// let handler = Embed::new("echo 'Hello, world!';", Some("example.php")); + /// + /// let request = Request::builder() + /// .method("GET") + /// .url("http://example.com").expect("invalid url") + /// .build(); + /// + /// let response = handler.handle(request).unwrap(); + /// + /// assert_eq!(response.status(), 200); + /// assert_eq!(response.body(), "Hello, world!"); + /// ``` fn handle(&self, request: Request) -> Result { let code = CString::new(self.code.clone()) .unwrap(); From af4ad195e32b59f9147811f72d9deb6c09d13fb6 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 14 Mar 2025 00:43:24 +0800 Subject: [PATCH 8/9] Clean up and document php-node --- crates/php_node/src/headers.rs | 181 ++++++++++++++++ crates/php_node/src/lib.rs | 275 +----------------------- crates/php_node/src/request.rs | 163 ++++++++++++++ crates/php_node/src/response.rs | 175 +++++++++++++++ crates/php_node/src/runtime.rs | 145 +++++++++++++ index.d.ts | 367 +++++++++++++++++++++++++++++++- index.js | 6 +- 7 files changed, 1033 insertions(+), 279 deletions(-) create mode 100644 crates/php_node/src/headers.rs create mode 100644 crates/php_node/src/request.rs create mode 100644 crates/php_node/src/response.rs create mode 100644 crates/php_node/src/runtime.rs diff --git a/crates/php_node/src/headers.rs b/crates/php_node/src/headers.rs new file mode 100644 index 00000000..49aece7a --- /dev/null +++ b/crates/php_node/src/headers.rs @@ -0,0 +1,181 @@ +use std::ptr; + +use napi::Result; +use napi::{sys, sys::{napi_env, napi_value}}; +use napi::bindgen_prelude::*; + +use php::Headers; + +pub struct Entry(K, V); + +// This represents a map entries key/value pair. +impl ToNapiValue for Entry +where + T1: ToNapiValue, + T2: ToNapiValue, +{ + unsafe fn to_napi_value(env: napi_env, val: Self) -> Result { + let Entry(key, value) = val; + let key_napi_value = T1::to_napi_value(env, key)?; + let value_napi_value = T2::to_napi_value(env, value)?; + + let mut result: napi_value = ptr::null_mut(); + unsafe { + check_status!( + sys::napi_create_array_with_length(env, 2, &mut result), + "Failed to create entry key/value pair" + )?; + + check_status!( + sys::napi_set_element(env, result, 0, key_napi_value), + "Failed to set entry key" + )?; + + check_status!( + sys::napi_set_element(env, result, 1, value_napi_value), + "Failed to set entry value" + )?; + }; + + Ok(result) + } +} + +/// A multi-map of HTTP headers. +/// +/// # Examples +/// +/// ```js +/// const headers = new Headers(); +/// headers.set('Content-Type', 'application/json'); +/// const contentType = headers.get('Content-Type'); +/// ``` +#[napi(js_name = "Headers")] +pub struct PhpHeaders { + headers: Headers +} + +impl PhpHeaders { + // Create a new PHP headers instance. + pub fn new(headers: Headers) -> Self { + PhpHeaders { + headers + } + } +} + +#[napi] +impl PhpHeaders { + /// Create a new PHP headers instance. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// ``` + #[napi(constructor)] + pub fn constructor() -> Self { + PhpHeaders { + headers: Headers::new() + } + } + + /// Get the values for a given header key. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Accept', 'application/json'); + /// headers.set('Accept', 'text/html'); + /// + /// for (const mime of headers.get('Accept')) { + /// console.log(mime); + /// } + /// ``` + #[napi] + pub fn get(&self, key: String) -> Option> { + self.headers.get(&key).map(|v| v.to_owned()) + } + + /// Set a header key/value pair. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// ``` + #[napi] + pub fn set(&mut self, key: String, value: String) { + self.headers.set(key, value) + } + + /// Remove a header key/value pair. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// headers.remove('Content-Type'); + /// ``` + #[napi] + pub fn remove(&mut self, key: String) { + self.headers.remove(&key) + } + + /// Get an iterator over the header entries. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// headers.set('Accept', 'application/json'); + /// + /// for (const [key, values] of headers.entries()) { + /// console.log(`${key}: ${values.join(', ')}`); + /// } + /// ``` + #[napi] + pub fn entries(&self) -> Vec>> { + self.headers.iter().map(|(k, v)| Entry(k.to_owned(), v.to_owned())).collect() + } + + /// Get an iterator over the header keys. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// headers.set('Accept', 'application/json'); + /// + /// for (const key of headers.keys()) { + /// console.log(key); + /// } + /// ``` + #[napi] + pub fn keys(&self) -> Vec { + self.headers.iter().map(|(k, _)| k.to_owned()).collect() + } + + /// Get an iterator over the header values. + /// + /// # Examples + /// + /// ```js + /// const headers = new Headers(); + /// headers.set('Content-Type', 'application/json'); + /// headers.set('Accept', 'application/json'); + /// + /// for (const value of headers.values()) { + /// console.log(value); + /// } + /// ``` + #[napi] + pub fn values(&self) -> Vec { + self.headers.iter_values().map(|v| v.to_owned()).collect() + } +} diff --git a/crates/php_node/src/lib.rs b/crates/php_node/src/lib.rs index 4d020d26..26ade1fc 100644 --- a/crates/php_node/src/lib.rs +++ b/crates/php_node/src/lib.rs @@ -1,269 +1,12 @@ #[macro_use] extern crate napi_derive; -use std::collections::HashMap; -use std::sync::Arc; - -use napi::{Error, Task, Env, Result}; -use napi::bindgen_prelude::*; - -use php::{Embed, Handler, Headers, Request, Response, RequestBuilder}; - -pub struct Entry(K, V); - -// This represents a map entries key/value pair. -impl ToNapiValue for Entry -where - T1: ToNapiValue, - T2: ToNapiValue, -{ - unsafe fn to_napi_value(env: napi::sys::napi_env, val: Self) -> napi::Result { - let Entry(key, value) = val; - let key_napi_value = T1::to_napi_value(env, key)?; - let value_napi_value = T2::to_napi_value(env, value)?; - - let mut result: napi::sys::napi_value = std::ptr::null_mut(); - unsafe { - check_status!( - napi::sys::napi_create_array_with_length(env, 2, &mut result), - "Failed to create entry key/value pair" - )?; - - check_status!( - napi::sys::napi_set_element(env, result, 0, key_napi_value), - "Failed to set entry key" - )?; - - check_status!( - napi::sys::napi_set_element(env, result, 1, value_napi_value), - "Failed to set entry value" - )?; - }; - - Ok(result) - } -} - -#[napi(js_name = "Headers")] -pub struct PhpHeaders { - headers: Headers -} - -#[napi] -impl PhpHeaders { - #[napi] - pub fn get(&self, key: String) -> Option> { - self.headers.get(&key).map(|v| v.to_owned()) - } - - #[napi] - pub fn set(&mut self, key: String, value: String) { - self.headers.set(key, value) - } - - #[napi] - pub fn remove(&mut self, key: String) { - self.headers.remove(&key) - } - - #[napi] - pub fn entries(&self) -> Vec>> { - self.headers.iter().map(|(k, v)| Entry(k.to_owned(), v.to_owned())).collect() - } - - #[napi] - pub fn keys(&self) -> Vec { - self.headers.iter().map(|(k, _)| k.to_owned()).collect() - } - - #[napi] - pub fn values(&self) -> Vec { - self.headers.iter_values().map(|v| v.to_owned()).collect() - } -} - -#[napi(object)] -#[derive(Default)] -pub struct PhpRequestOptions { - pub method: String, - pub url: String, - pub headers: Option>>, - pub body: Option -} - -#[napi(js_name = "Request")] -pub struct PhpRequest { - request: Request -} - -#[napi] -impl PhpRequest { - #[napi(constructor)] - pub fn new(options: PhpRequestOptions) -> Self { - let mut builder: RequestBuilder = Request::builder() - .method(options.method) - .url(options.url).expect("invalid url"); - - if let Some(headers) = options.headers { - for key in headers.keys() { - let values = headers.get(key) - .expect(format!("missing header values for key: {}", key).as_str()); - - for value in values { - builder = builder.header(key.clone(), value.clone()) - } - } - } - - if let Some(body) = options.body { - builder = builder.body(body.as_ref()) - } - - PhpRequest { - request: builder.build() - } - } - - #[napi(getter, enumerable = true)] - pub fn method(&self) -> String { - self.request.method().to_owned() - } - - #[napi(getter, enumerable = true)] - pub fn url(&self) -> String { - self.request - .url() - .as_str() - .to_owned() - } - - #[napi(getter, enumerable = true)] - pub fn headers(&self) -> PhpHeaders { - PhpHeaders { - headers: self.request.headers().clone() - } - } - - #[napi(getter, enumerable = true)] - pub fn body(&self) -> Buffer { - self.request - .body() - .to_vec() - .into() - } -} - -impl PhpRequest { - fn to_inner(&self) -> Request { - self.request.clone() - } -} - -#[napi(object)] -#[derive(Clone, Default)] -pub struct PhpOptions { - pub argv: Option>, - pub code: String, - pub file: Option -} - -#[napi] -pub struct Php { - embed: Arc -} - -#[napi] -impl Php { - #[napi(constructor)] - pub fn new(options: PhpOptions) -> Self { - let code = options.code.clone(); - let filename = options.file.clone(); - let argv = options.argv.clone(); - - // TODO: Need to figure out how to send an Embed across threads - // so we can reuse the same Embed instance for multiple requests. - let embed = match argv { - Some(argv) => Embed::new_with_argv(code, filename, argv), - None => Embed::new(code, filename) - }; - - Php { - embed: Arc::new(embed) - } - } - - #[napi] - pub fn handle_request(&self, request: &PhpRequest) -> AsyncTask { - AsyncTask::new(PhpRequestTask { - embed: self.embed.clone(), - request: request.to_inner() - }) - } -} - -pub struct PhpRequestTask { - embed: Arc, - request: Request -} - -impl Task for PhpRequestTask { - type Output = Response; - type JsValue = PhpResponse; - - fn compute(&mut self) -> Result { - self.embed - .handle(self.request.clone()) - .map_err(|err| Error::from_reason(err)) - } - - fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { - if let Some(exception) = output.exception() { - return Err(Error::from_reason(exception.to_owned())) - } - - Ok(PhpResponse { - response: output - }) - } -} - -#[napi] -pub struct PhpResponse { - response: Response -} - -#[napi] -impl PhpResponse { - #[napi(getter, enumerable = true)] - pub fn status(&self) -> u32 { - self.response.status() as u32 - } - - #[napi(getter, enumerable = true)] - pub fn headers(&self) -> PhpHeaders { - PhpHeaders { - headers: self.response.headers().clone() - } - } - - #[napi(getter, enumerable = true)] - pub fn body(&self) -> Buffer { - self.response - .body() - .to_vec() - .into() - } - - #[napi(getter, enumerable = true)] - pub fn log(&self) -> Buffer { - self.response - .log() - .to_vec() - .into() - } - - #[napi(getter, enumerable = true)] - pub fn exception(&self) -> Option { - self.response.exception().map(|v| v.to_owned()) - } -} +mod headers; +mod runtime; +mod request; +mod response; + +pub use headers::PhpHeaders; +pub use runtime::PhpRuntime; +pub use request::PhpRequest; +pub use response::PhpResponse; diff --git a/crates/php_node/src/request.rs b/crates/php_node/src/request.rs new file mode 100644 index 00000000..093d6603 --- /dev/null +++ b/crates/php_node/src/request.rs @@ -0,0 +1,163 @@ +use std::collections::HashMap; + +use napi::bindgen_prelude::*; + +use php::{Request, RequestBuilder}; + +use crate::PhpHeaders; + +/// Options for creating a new PHP request. +#[napi(object)] +#[derive(Default)] +pub struct PhpRequestOptions { + /// The HTTP method for the request. + pub method: String, + /// The URL for the request. + pub url: String, + /// The headers for the request. + pub headers: Option>>, + /// The body for the request. + pub body: Option +} + +/// A PHP request. +/// +/// # Examples +/// +/// ```js +/// const request = new Request({ +/// method: 'GET', +/// url: 'http://example.com', +/// headers: { +/// 'Content-Type': ['application/json'] +/// }, +/// body: new Uint8Array([1, 2, 3, 4]) +/// }); +/// ``` +#[napi(js_name = "Request")] +pub struct PhpRequest { + request: Request +} + +#[napi] +impl PhpRequest { + /// Create a new PHP request. + /// + /// # Examples + /// + /// ```js + /// const request = new Request({ + /// method: 'GET', + /// url: 'http://example.com', + /// headers: { + /// 'Content-Type': ['application/json'] + /// }, + /// body: new Uint8Array([1, 2, 3, 4]) + /// }); + /// ``` + #[napi(constructor)] + pub fn constructor(options: PhpRequestOptions) -> Self { + let mut builder: RequestBuilder = Request::builder() + .method(options.method) + .url(options.url).expect("invalid url"); + + if let Some(headers) = options.headers { + for key in headers.keys() { + let values = headers.get(key) + .expect(format!("missing header values for key: {}", key).as_str()); + + for value in values { + builder = builder.header(key.clone(), value.clone()) + } + } + } + + if let Some(body) = options.body { + builder = builder.body(body.as_ref()) + } + + PhpRequest { + request: builder.build() + } + } + + /// Get the HTTP method for the request. + /// + /// # Examples + /// + /// ```js + /// const request = new Request({ + /// method: 'GET' + /// }); + /// + /// console.log(request.method); + /// ``` + #[napi(getter, enumerable = true)] + pub fn method(&self) -> String { + self.request.method().to_owned() + } + + /// Get the URL for the request. + /// + /// # Examples + /// + /// ```js + /// const request = new Request({ + /// url: 'http://example.com' + /// }); + /// + /// console.log(request.url); + /// ``` + #[napi(getter, enumerable = true)] + pub fn url(&self) -> String { + self.request + .url() + .as_str() + .to_owned() + } + + /// Get the headers for the request. + /// + /// # Examples + /// + /// ```js + /// const request = new Request({ + /// headers: { + /// 'Accept': ['application/json', 'text/html'] + /// } + /// }); + /// + /// for (const mime of request.headers.get('Accept')) { + /// console.log(mime); + /// } + /// ``` + #[napi(getter, enumerable = true)] + pub fn headers(&self) -> PhpHeaders { + PhpHeaders::new(self.request.headers().clone()) + } + + /// Get the body for the request. + /// + /// # Examples + /// + /// ```js + /// const request = new Request({ + /// body: new Uint8Array([1, 2, 3, 4]) + /// }); + /// + /// console.log(request.body); + /// ``` + #[napi(getter, enumerable = true)] + pub fn body(&self) -> Buffer { + self.request + .body() + .to_vec() + .into() + } +} + +impl From<&PhpRequest> for Request { + fn from(request: &PhpRequest) -> Self { + request.request.clone() + } +} diff --git a/crates/php_node/src/response.rs b/crates/php_node/src/response.rs new file mode 100644 index 00000000..f53b785d --- /dev/null +++ b/crates/php_node/src/response.rs @@ -0,0 +1,175 @@ +use std::collections::HashMap; + +use napi::bindgen_prelude::*; + +use php::Response; + +use crate::PhpHeaders; + +/// Options for creating a new PHP response. +#[napi(object)] +pub struct PhpResponseOptions { + /// The HTTP status code for the response. + pub status: u32, + /// The headers for the response. + pub headers: Option>>, + /// The body for the response. + pub body: Option, + /// The log for the response. + pub log: Option, + /// The exception for the response. + pub exception: Option +} + +/// A PHP response. +#[napi(js_name = "Response")] +pub struct PhpResponse { + response: Response +} + +impl PhpResponse { + pub fn new(response: Response) -> Self { + PhpResponse { + response + } + } +} + +#[napi] +impl PhpResponse { + /// Create a new PHP response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// status: 200, + /// headers: { + /// 'Content-Type': ['application/json'] + /// }, + /// body: new Uint8Array([1, 2, 3, 4]) + /// }); + /// ``` + #[napi(constructor)] + pub fn constructor(options: PhpResponseOptions) -> Self { + let mut builder = Response::builder(); + builder.status(options.status as u16); + + if let Some(headers) = options.headers { + for key in headers.keys() { + let values = headers.get(key) + .expect(format!("missing header values for key: {}", key).as_str()); + + for value in values { + builder.header(key.clone(), value.clone()); + } + } + } + + if let Some(body) = options.body { + builder.body(body.as_ref()); + } + + if let Some(log) = options.log { + builder.log(log.as_ref()); + } + + if let Some(exception) = options.exception { + builder.exception(exception); + } + + PhpResponse { + response: builder.build() + } + } + + /// Get the HTTP status code for the response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// status: 200 + /// }); + /// + /// console.log(response.status); + /// ``` + #[napi(getter, enumerable = true)] + pub fn status(&self) -> u32 { + self.response.status() as u32 + } + + /// Get the headers for the response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// headers: { + /// 'Content-Type': ['application/json'] + /// } + /// }); + /// + /// for (const mime of response.headers.get('Content-Type')) { + /// console.log(mime); + /// } + /// ``` + #[napi(getter, enumerable = true)] + pub fn headers(&self) -> PhpHeaders { + PhpHeaders::new(self.response.headers().clone()) + } + + /// Get the body for the response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// body: new Uint8Array([1, 2, 3, 4]) + /// }); + /// + /// console.log(response.body); + /// ``` + #[napi(getter, enumerable = true)] + pub fn body(&self) -> Buffer { + self.response + .body() + .to_vec() + .into() + } + + /// Get the log for the response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// log: new Uint8Array([1, 2, 3, 4]) + /// }); + /// + /// console.log(response.log); + /// ``` + #[napi(getter, enumerable = true)] + pub fn log(&self) -> Buffer { + self.response + .log() + .to_vec() + .into() + } + + /// Get the exception for the response. + /// + /// # Examples + /// + /// ```js + /// const response = new Response({ + /// exception: 'An error occurred' + /// }); + /// + /// console.log(response.exception); + /// ``` + #[napi(getter, enumerable = true)] + pub fn exception(&self) -> Option { + self.response.exception().map(|v| v.to_owned()) + } +} diff --git a/crates/php_node/src/runtime.rs b/crates/php_node/src/runtime.rs new file mode 100644 index 00000000..5ba63a0c --- /dev/null +++ b/crates/php_node/src/runtime.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use napi::{Error, Task, Env, Result}; +use napi::bindgen_prelude::*; + +use php::{Embed, Handler, Request, Response}; + +use crate::{PhpRequest, PhpResponse}; + +/// Options for creating a new PHP instance. +#[napi(object)] +#[derive(Clone, Default)] +pub struct PhpOptions { + /// The command-line arguments for the PHP instance. + pub argv: Option>, + /// The PHP code to embed. + pub code: String, + /// The filename for the PHP code. + pub file: Option +} + +/// A PHP instance. +/// +/// # Examples +/// +/// ```js +/// const php = new Php({ +/// code: 'echo "Hello, world!";' +/// }); +/// +/// const response = php.handleRequest(new Request({ +/// method: 'GET', +/// url: 'http://example.com' +/// })); +/// +/// console.log(response.status); +/// console.log(response.body); +/// ``` +#[napi(js_name = "Php")] +pub struct PhpRuntime { + embed: Arc +} + +#[napi] +impl PhpRuntime { + /// Create a new PHP instance. + /// + /// # Examples + /// + /// ```js + /// const php = new Php({ + /// code: 'echo "Hello, world!";' + /// }); + /// ``` + #[napi(constructor)] + pub fn new(options: PhpOptions) -> Self { + let code = options.code.clone(); + let filename = options.file.clone(); + let argv = options.argv.clone(); + + // TODO: Need to figure out how to send an Embed across threads + // so we can reuse the same Embed instance for multiple requests. + let embed = match argv { + Some(argv) => Embed::new_with_argv(code, filename, argv), + None => Embed::new(code, filename) + }; + + Self { + embed: Arc::new(embed) + } + } + + /// Handle a PHP request. + /// + /// # Examples + /// + /// ```js + /// const php = new Php({ + /// code: 'echo "Hello, world!";' + /// }); + /// + /// const response = php.handleRequest(new Request({ + /// method: 'GET', + /// url: 'http://example.com' + /// })); + /// + /// console.log(response.status); + /// console.log(response.body); + /// ``` + #[napi] + pub fn handle_request(&self, request: &PhpRequest) -> AsyncTask { + AsyncTask::new(PhpRequestTask { + embed: self.embed.clone(), + request: request.into() + }) + } + + /// Handle a PHP request synchronously. + /// + /// # Examples + /// + /// ```js + /// const php = new Php({ + /// code: 'echo "Hello, world!";' + /// }); + /// + /// const response = php.handleRequestSync(new Request({ + /// method: 'GET', + /// url: 'http://example.com' + /// })); + /// + /// console.log(response.status); + /// console.log(response.body); + /// ``` + #[napi] + pub fn handle_request_sync(&self, request: &PhpRequest) -> Result { + self.embed + .handle(request.into()) + .map_err(|err| Error::from_reason(err)) + .map(PhpResponse::new) + } +} + +// Task container to run a PHP request in a worker thread. +pub struct PhpRequestTask { + embed: Arc, + request: Request +} + +impl Task for PhpRequestTask { + type Output = Response; + type JsValue = PhpResponse; + + // Handle the PHP request in the worker thread. + fn compute(&mut self) -> Result { + self.embed + .handle(self.request.clone()) + .map_err(|err| Error::from_reason(err)) + } + + // Handle converting the PHP response to a JavaScript response in the main thread. + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(PhpResponse::new(output)) + } +} diff --git a/index.d.ts b/index.d.ts index 58628ed9..ca9a26b9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,35 +3,382 @@ /* auto-generated by NAPI-RS */ +/** Options for creating a new PHP instance. */ +export interface PhpOptions { + /** The command-line arguments for the PHP instance. */ + argv?: Array + /** The PHP code to embed. */ + code: string + /** The filename for the PHP code. */ + file?: string +} +/** Options for creating a new PHP request. */ export interface PhpRequestOptions { + /** The HTTP method for the request. */ method: string + /** The URL for the request. */ url: string + /** The headers for the request. */ headers?: Record> + /** The body for the request. */ body?: Uint8Array } -export interface PhpOptions { - argv?: Array - code: string - file?: string +/** Options for creating a new PHP response. */ +export interface PhpResponseOptions { + /** The HTTP status code for the response. */ + status: number + /** The headers for the response. */ + headers?: Record> + /** The body for the response. */ + body?: Uint8Array + /** The log for the response. */ + log?: Uint8Array + /** The exception for the response. */ + exception?: string } export type PhpHeaders = Headers -export declare class Headers { } +/** + * A multi-map of HTTP headers. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * const contentType = headers.get('Content-Type'); + * ``` + */ +export declare class Headers { + constructor() + /** + * Get the values for a given header key. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Accept', 'application/json'); + * headers.set('Accept', 'text/html'); + * + * for (const mime of headers.get('Accept')) { + * console.log(mime); + * } + * ``` + */ + get(key: string): Array | null + /** + * Set a header key/value pair. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * ``` + */ + set(key: string, value: string): void + /** + * Remove a header key/value pair. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.remove('Content-Type'); + * ``` + */ + remove(key: string): void + /** + * Get an iterator over the header entries. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.set('Accept', 'application/json'); + * + * for (const [key, values] of headers.entries()) { + * console.log(`${key}: ${values.join(', ')}`); + * } + * ``` + */ + entries(): Array + /** + * Get an iterator over the header keys. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.set('Accept', 'application/json'); + * + * for (const key of headers.keys()) { + * console.log(key); + * } + * ``` + */ + keys(): Array + /** + * Get an iterator over the header values. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * headers.set('Content-Type', 'application/json'); + * headers.set('Accept', 'application/json'); + * + * for (const value of headers.values()) { + * console.log(value); + * } + * ``` + */ + values(): Array +} +export type PhpRuntime = Php +/** + * A PHP instance. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * + * const response = php.handleRequest(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ +export declare class Php { + /** + * Create a new PHP instance. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * ``` + */ + constructor(options: PhpOptions) + /** + * Handle a PHP request. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * + * const response = php.handleRequest(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ + handleRequest(request: PhpRequest): Promise +} export type PhpRequest = Request +/** + * A PHP request. + * + * # Examples + * + * ```js + * const request = new Request({ + * method: 'GET', + * url: 'http://example.com', + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * ``` + */ export declare class Request { + /** + * Create a new PHP request. + * + * # Examples + * + * ```js + * const request = new Request({ + * method: 'GET', + * url: 'http://example.com', + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * ``` + */ constructor(options: PhpRequestOptions) + /** + * Get the HTTP method for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * method: 'GET' + * }); + * + * console.log(request.method); + * ``` + */ get method(): string + /** + * Get the URL for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * url: 'http://example.com' + * }); + * + * console.log(request.url); + * ``` + */ get url(): string + /** + * Get the headers for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * headers: { + * 'Accept': ['application/json', 'text/html'] + * } + * }); + * + * for (const mime of request.headers.get('Accept')) { + * console.log(mime); + * } + * ``` + */ get headers(): Headers + /** + * Get the body for the request. + * + * # Examples + * + * ```js + * const request = new Request({ + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(request.body); + * ``` + */ get body(): Buffer } -export declare class Php { - constructor(options: PhpOptions) - handleRequest(request: Request): Promise -} -export declare class PhpResponse { +export type PhpResponse = Response +/** A PHP response. */ +export declare class Response { + /** + * Create a new PHP response. + * + * # Examples + * + * ```js + * const response = new Response({ + * status: 200, + * headers: { + * 'Content-Type': ['application/json'] + * }, + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * ``` + */ + constructor(options: PhpResponseOptions) + /** + * Get the HTTP status code for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * status: 200 + * }); + * + * console.log(response.status); + * ``` + */ get status(): number + /** + * Get the headers for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * headers: { + * 'Content-Type': ['application/json'] + * } + * }); + * + * for (const mime of response.headers.get('Content-Type')) { + * console.log(mime); + * } + * ``` + */ get headers(): Headers + /** + * Get the body for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * body: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(response.body); + * ``` + */ get body(): Buffer + /** + * Get the log for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * log: new Uint8Array([1, 2, 3, 4]) + * }); + * + * console.log(response.log); + * ``` + */ get log(): Buffer + /** + * Get the exception for the response. + * + * # Examples + * + * ```js + * const response = new Response({ + * exception: 'An error occurred' + * }); + * + * console.log(response.exception); + * ``` + */ get exception(): string | null } diff --git a/index.js b/index.js index 5c9e1aa2..1ddb6f55 100644 --- a/index.js +++ b/index.js @@ -310,9 +310,9 @@ if (!nativeBinding) { throw new Error(`Failed to load native binding`) } -const { Headers, Request, Php, PhpResponse } = nativeBinding +const { Headers, Php, Request, Response } = nativeBinding module.exports.Headers = Headers -module.exports.Request = Request module.exports.Php = Php -module.exports.PhpResponse = PhpResponse +module.exports.Request = Request +module.exports.Response = Response From e7cf3857964f80550ea4302caf37f64e0e110072 Mon Sep 17 00:00:00 2001 From: Stephen Belanger Date: Fri, 14 Mar 2025 12:54:33 +0800 Subject: [PATCH 9/9] Attempt to download SPC and build PHP from actions --- .github/workflows/CI.yml | 64 ++++++-- .gitignore | 5 + crates/lang_handler/build.rs | 10 +- crates/php/build.rs | 264 +++++--------------------------- crates/php_node/src/response.rs | 1 + index.d.ts | 29 ++++ scripts/build-php.sh | 35 +++++ scripts/download-php.sh | 8 + scripts/extensions.sh | 100 ++++++++++++ scripts/fetch-spc.sh | 20 +++ 10 files changed, 290 insertions(+), 246 deletions(-) create mode 100755 scripts/build-php.sh create mode 100755 scripts/download-php.sh create mode 100755 scripts/extensions.sh create mode 100755 scripts/fetch-spc.sh diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index ecb393c9..ea64d4b4 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -27,14 +27,22 @@ jobs: settings: - host: macos-13 target: x86_64-apple-darwin + os: macos + arch: x86_64 build: yarn build --target x86_64-apple-darwin - # build: yarn build - - host: macos-latest + setup: | + brew install autoconf automake libtool + - host: macos-15 target: aarch64-apple-darwin + os: macos + arch: aarch64 build: yarn build --target aarch64-apple-darwin - # build: yarn build + setup: | + brew install autoconf automake libtool - host: ubuntu-latest target: x86_64-unknown-linux-gnu + os: linux + arch: x86_64 # docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-debian build: yarn build --target x86_64-unknown-linux-gnu # build: yarn build @@ -54,6 +62,16 @@ jobs: # sudo apt-get update # sudo apt-get install gcc-arm-linux-gnueabihf libssl-dev -y # build: yarn build --target armv7-unknown-linux-gnueabihf + - host: ubuntu-latest + target: x86_64-unknown-linux-musl + docker: ghcr.io/napi-rs/napi-rs/nodejs-rust:lts-alpine + build: |- + set -e && + rustup target add x86_64-unknown-linux-musl && + yarn build --target x86_64-unknown-linux-musl + setup: | + sudo apt-get update + sudo apt-get install libssl-dev -y # - host: ubuntu-latest # target: armv7-unknown-linux-musleabihf # build: yarn build --target armv7-unknown-linux-musleabihf @@ -81,7 +99,7 @@ jobs: # build: | # yarn build --target i686-pc-windows-msvc # yarn test - name: stable - ${{ matrix.settings.target }} - node@20 + name: stable - ${{ matrix.settings.target }} - node@22 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v4 @@ -89,7 +107,7 @@ jobs: uses: actions/setup-node@v4 if: ${{ !matrix.settings.docker }} with: - node-version: 20 + node-version: 22 cache: yarn - name: Install uses: dtolnay/rust-toolchain@stable @@ -115,19 +133,35 @@ jobs: run: ${{ matrix.settings.setup }} if: ${{ matrix.settings.setup }} shell: bash - - name: Setup node x86 - if: matrix.settings.target == 'i686-pc-windows-msvc' - run: yarn config set supportedArchitectures.cpu "ia32" + - name: Fetch SPC shell: bash + run: ./scripts/fetch-spc.sh + - name: Cache PHP Download + id: cache-php-download + uses: actions/cache@v4 + with: + path: downloads + key: php-download-${{ matrix.settings.target }}-${{ matrix.settings.host }} + - name: Fetch PHP and extensions + if: steps.cache-php-download.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/download-php.sh + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Cache PHP + id: cache-php-build + uses: actions/cache@v4 + with: + path: | + buildroot + source + key: php-build-${{ matrix.settings.target }}-${{ matrix.settings.host }} + - name: Build PHP + if: steps.cache-php-build.outputs.cache-hit != 'true' + shell: bash + run: ./scripts/build-php.sh - name: Install dependencies run: yarn install - - name: Setup node x86 - uses: actions/setup-node@v4 - if: matrix.settings.target == 'i686-pc-windows-msvc' - with: - node-version: 20 - cache: yarn - architecture: x86 - name: Build in docker uses: addnab/docker-run-action@v3 if: ${{ matrix.settings.docker }} diff --git a/.gitignore b/.gitignore index 4f6f4b78..add4fce9 100644 --- a/.gitignore +++ b/.gitignore @@ -200,3 +200,8 @@ Cargo.lock crates/php/buildroot crates/php/downloads crates/php/source + +buildroot +downloads +source +spc diff --git a/crates/lang_handler/build.rs b/crates/lang_handler/build.rs index 7123e128..3f9ef3b6 100644 --- a/crates/lang_handler/build.rs +++ b/crates/lang_handler/build.rs @@ -5,11 +5,13 @@ use std::path::PathBuf; fn main() { let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); - let profile = env::var("PROFILE").unwrap(); - let target_str = format!("../../target/{}", profile); - let header_path = PathBuf::from(crate_dir.clone()) - .join(target_str) + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("../../..") + .canonicalize() + .unwrap(); + + let header_path = out_dir .join("lang_handler.h"); cbindgen::Builder::new() diff --git a/crates/php/build.rs b/crates/php/build.rs index 44dc8f5f..db07d485 100644 --- a/crates/php/build.rs +++ b/crates/php/build.rs @@ -1,236 +1,29 @@ use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, env, ffi::OsStr, - fmt::{Debug, Display}, + fmt::Debug, path::PathBuf, process::Command }; // use autotools::Config; use bindgen::Builder; -use downloader::{Download, Downloader}; -#[cfg(not(target_os = "windows"))] -use file_mode::ModePath; - -fn maybe_windowsify(path: T) -> String where T: Display { - match env::var("CARGO_CFG_TARGET_OS").as_deref() { - Ok("windows") => format!("{}.exe", path), - _ => path.to_string() - } -} - -fn spc_url() -> String { - let os = if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "windows") { - "windows" - } else { - panic!("Unsupported OS"); - }; - - let arch = if cfg!(target_arch = "x86") { - "x86" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - panic!("Unsupported arch"); - }; - - let url = format!("https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-{}-{}", os, arch); - - maybe_windowsify(url) -} - -fn get_spc() -> PathBuf { - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - - let filename = maybe_windowsify("spc"); - let spc = out_path.join(filename.clone()); - - println!("cargo:rerun-if-changed={}", spc.to_str().unwrap()); - - if spc.exists() { - return spc; - } - println!("spc is not present"); - - let mut downloader = Downloader::builder() - .download_folder(&out_path) - .build() - .unwrap(); - - let dl = Download::new(&spc_url()) - .file_name(filename.as_ref()); - - downloader.download(&vec![dl]).unwrap(); - - // Make the file executable - #[cfg(not(target_os = "windows"))] - spc.set_mode("a+x").unwrap(); - spc -} fn main() { println!("cargo:rerun-if-changed=build.rs"); - let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - let current_dir = env::current_dir().unwrap(); - - // Any commented out ones I was not able to get to build for some reason... - let available_extensions = HashSet::from([ - "amqp", - "apcu", - "ast", - "bcmath", - "bz2", - "calendar", - "ctype", - // "curl", - // "dba", - // "dio", - // "dom", - // "ds", - // "event", - // "exif", - // "ffi", - // "fileinfo", - // "filter", - // "ftp", - // "gd", - // "gettext", - // "glfw", - // "gmp", - // "gmssl", - // "grpc", - // "iconv", - // "igbinary", - // "imagick", - // "imap", - // "inotify", - // "intl", - // "ldap", - // "libxml", - // "mbregex", - // "mbstring", - // "mcrypt", - // "memcache", - // "memcached", - // "mongodb", - // "msgpack", - // "mysqli", - // "mysqlnd", - // "oci8", - // "opcache", - // "openssl", - // "parallel", // Requires zts - // "password-argon2", - // "pcntl", - // "pdo", - // "pdo_mysql", - // "pdo_pgsql", - // "pdo_sqlite", - // "pdo_sqlsrv", - // "pgsql", - // "phar", - // "posix", - // "protobuf", - // "rar", - // "rdkafka", - // "readline", - // "redis", - // "session", - // "shmop", - // "simdjson", - // "simplexml", - // "snappy", - // "soap", - // "sockets", - // "sodium", - // "spx", - // "sqlite3", - // "sqlsrv", - // "ssh2", - // "swoole", // Mutually exclusive with pdo_* - // "swoole-hook-mysql", // Mutually exclusive with pdo_* - // "swoole-hook-pgsql", // Mutually exclusive with pdo_* - // "swoole-hook-sqlite", // Mutually exclusive with pdo_* - // "swow", - // "sysvmsg", - // "sysvsem", - // "sysvshm", - // "tidy", - // "tokenizer", - // "uuid", - // "uv", - // "xdebug", - // "xhprof", - // "xlswriter", - // "xml", - // "xmlreader", - // "xmlwriter", - // "xsl", - // "yac", - // "yaml", - // "zip", - // "zlib", - // "zstd" - ]); - - // TODO: Make an actually reasonable selection of default extensions - let extensions = env::var("PHP_EXTENSIONS").unwrap_or_else( - |_| available_extensions.iter() - .map(|s| s.to_string()) - .collect::>() - .join(",") - ); + let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); + let project_root = crate_dir.join("../.."); - let spc = get_spc(); + let spc = project_root.join("spc"); let spc_cmd = spc.to_str().unwrap(); - execute_command(&[spc_cmd, "doctor", "--auto-fix"], None); - - let has_downloads = current_dir.join("downloads").exists(); - let should_download = env::var("PHP_SHOULD_DOWNLOAD") - .map_or(!has_downloads, |s| s == "true"); - - if should_download { - // Download PHP and requested extensions - execute_command(&[ - spc_cmd, - "download", - &format!("--for-extensions={}", extensions.clone()), - "--retry=10", - "--prefer-pre-built", - "--with-php=8.4" - ], None); - } - - // TODO: Build if downloads modification time is more recent than libphp.a - let has_libphp = current_dir.join("buildroot/lib/libphp.a").exists(); - let should_build = env::var("PHP_SHOULD_BUILD") - .map_or(should_download || !has_libphp, |s| s == "true"); + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); - if should_build { - let mut env = HashMap::new(); - env.insert( - "SPC_CMD_PREFIX_PHP_CONFIGURE".to_string(), - "./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-cgi --disable-phpdbg --enable-debug".to_string() - ); - // Build in embed mode - execute_command(&[ - spc_cmd, - "build", - &extensions, - "--build-embed", - "--enable-zts", - "--no-strip", // Keep debug symbols? - ], Some(env)); - } + let extensions_sh = project_root.join("scripts/extensions.sh"); + let extensions_cmd = extensions_sh.to_str().unwrap(); + let extensions = execute_command(&[extensions_cmd], None); // Get the includes let includes = execute_command(&[ @@ -255,7 +48,7 @@ fn main() { } // Include SAPI headers - let sapi_include = current_dir + let sapi_include = project_root .join("buildroot/include/php/sapi") .canonicalize() .unwrap(); @@ -264,7 +57,12 @@ fn main() { // Link libraries let libs = libs.split(' ').collect::>(); + let mut is_framework = false; for dir in libs.iter() { + if is_framework { + is_framework = false; + println!("cargo:rustc-link-lib=framework={}", dir); + } if dir.starts_with("-L") { println!("cargo:rustc-link-search={}", &dir[2..]); } @@ -272,21 +70,21 @@ fn main() { println!("cargo:rustc-link-lib={}", &dir[2..]); } if dir.starts_with("-framework") { - println!("cargo:rustc-link-lib=framework={}", &dir[11..]); + is_framework = true; } } - let crate_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()); - - let profile = env::var("PROFILE").unwrap(); - let lang_handler_include = crate_dir - .join(format!("../../target/{}", profile)); + // Locate lang_handler header and lib + let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap()) + .join("../../..") + .canonicalize() + .unwrap(); - println!("cargo:rustc-link-search={}", lang_handler_include.display()); + println!("cargo:rustc-link-search={}", out_dir.display()); println!("cargo:rustc-link-lib=lang_handler"); - println!("cargo:include={}", lang_handler_include.display()); + println!("cargo:include={}", out_dir.display()); - let lang_handler_include_flag = format!("-I{}", lang_handler_include.display()); + let lang_handler_include_flag = format!("-I{}", out_dir.display()); includes.push(lang_handler_include_flag.as_str()); let mut builder = cc::Build::new(); @@ -299,6 +97,13 @@ fn main() { println!("cargo:rerun-if-changed=src/php_wrapper.h"); + let sw_vers = execute_command(&[ + "sw_vers", + "-productVersion" + ], None); + + println!("cargo:env=MACOSX_DEPLOYMENT_TARGET={}", sw_vers); + let mut builder = Builder::default() // .parse_callbacks(Box::new(bindgen::CargoCallbacks::new())) .header("src/php_wrapper.c") @@ -340,13 +145,18 @@ fn execute_command + Debug>(argv: &[S], env: Option Self { PhpResponse { response diff --git a/index.d.ts b/index.d.ts index ca9a26b9..ade64d2d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -49,6 +49,15 @@ export type PhpHeaders = Headers * ``` */ export declare class Headers { + /** + * Create a new PHP headers instance. + * + * # Examples + * + * ```js + * const headers = new Headers(); + * ``` + */ constructor() /** * Get the values for a given header key. @@ -191,6 +200,26 @@ export declare class Php { * ``` */ handleRequest(request: PhpRequest): Promise + /** + * Handle a PHP request synchronously. + * + * # Examples + * + * ```js + * const php = new Php({ + * code: 'echo "Hello, world!";' + * }); + * + * const response = php.handleRequestSync(new Request({ + * method: 'GET', + * url: 'http://example.com' + * })); + * + * console.log(response.status); + * console.log(response.body); + * ``` + */ + handleRequestSync(request: PhpRequest): PhpResponse } export type PhpRequest = Request /** diff --git a/scripts/build-php.sh b/scripts/build-php.sh new file mode 100755 index 00000000..456afe8e --- /dev/null +++ b/scripts/build-php.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +set -euo pipefail + +EXTENSIONS=${1:-$(source ./scripts/extensions.sh)} + +OS=${2:-$(uname -s | tr '[:upper:]' '[:lower:]')} +ARCH=${3:-$(uname -m)} + +if [[ "$ARCH" == "arm64" ]]; then + ARCH="aarch64" +elif [[ "$ARCH" == "amd64" ]]; then + ARCH="x86_64" +fi + +if [[ "$OS" == "darwin" ]]; then + OS="macos" + # export MACOSX_DEPLOYMENT_TARGET=$(rustc --target ${ARCH}-apple-darwin --print deployment-target) +elif [[ "$OS" == "linux" ]]; then + export PATH="/usr/local/musl/bin:$PATH" + export CC="${ARCH}-linux-musl-gcc" + export AR="${ARCH}-linux-musl-ar" + if [[ "$(ldd --version 2>&1)" != *"musl"* ]]; then + export SPC_LIBC="glibc" + else + export SPC_LIBC="musl" + fi +fi + +# Ensure it is built with debug symbols when DEBUG is set +if [[ -n "${DEBUG:-}" ]]; then + SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable-shared=no --enable-static=yes --disable-all --disable-cgi --disable-phpdbg --enable-debug" +fi + +./spc build ${EXTENSIONS} --build-embed --enable-zts --no-strip --debug diff --git a/scripts/download-php.sh b/scripts/download-php.sh new file mode 100755 index 00000000..1de0ebee --- /dev/null +++ b/scripts/download-php.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +PHP_VERSION=${1:-8.4} +EXTENSIONS=${2:-$(source ./scripts/extensions.sh)} + +./spc download --prefer-pre-built --with-php=${PHP_VERSION} --retry=10 --for-extensions=${EXTENSIONS} diff --git a/scripts/extensions.sh b/scripts/extensions.sh new file mode 100755 index 00000000..b78ab5d8 --- /dev/null +++ b/scripts/extensions.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +printf "amqp" +printf ",apcu" +# printf ",ast" +printf ",bcmath" +printf ",bz2" +printf ",calendar" +printf ",ctype" +printf ",curl" +printf ",dba" +# printf ",dio" +printf ",dom" +printf ",ds" +# printf ",enchant" +printf ",event" +printf ",exif" +# printf ",ffi" +printf ",fileinfo" +printf ",filter" +printf ",ftp" +printf ",gd" +printf ",gettext" +# printf ",glfw" +printf ",gmp" +# printf ",gmssl" +# printf ",grpc" +printf ",iconv" +printf ",igbinary" +# printf ",imagick" +# printf ",imap" +# printf ",inotify" +printf ",intl" +# printf ",ldap" +# printf ",libxml" +printf ",mbregex" +printf ",mbstring" +# printf ",mcrypt" +printf ",memcache" +# printf ",memcached" +# printf ",mongodb" +# printf ",msgpack" +printf ",mysqli" +printf ",mysqlnd" +# printf ",oci8" +printf ",opcache" +printf ",openssl" +# printf ",opentelemetry" +# printf ",parallel" +printf ",password-argon2" +# printf ",pcntl" +printf ",pdo" +printf ",pdo_mysql" +# printf ",pdo_pgsql" +printf ",pdo_sqlite" +# printf ",pdo_sqlsrv" +# printf ",pgsql" +printf ",phar" +printf ",posix" +# printf ",protobuf" +printf ",rar" +# printf ",rdkafka" +# printf ",readline" +printf ",redis" +printf ",session" +printf ",shmop" +printf ",simdjson" +printf ",simplexml" +# printf ",snappy" +printf ",soap" +# printf ",sockets" +# printf ",sodium" +# printf ",spx" +printf ",sqlite3" +printf ",sqlsrv" +# printf ",swoole" +# printf ",swoole-hook-mysql" +# printf ",swoole-hook-pgsql" +# printf ",swoole-hook-sqlite" +printf ",swow" +printf ",sysvmsg" +printf ",sysvsem" +printf ",sysvshm" +printf ",tidy" +printf ",tokenizer" +printf ",uuid" +# printf ",uv" +# printf ",xdebug" +printf ",xhprof" +# printf ",xlswriter" +printf ",xml" +printf ",xmlreader" +printf ",xmlwriter" +# printf ",xsl" +printf ",yac" +printf ",yaml" +printf ",zip" +printf ",zlib" +# printf ",zstd" +printf "\n" diff --git a/scripts/fetch-spc.sh b/scripts/fetch-spc.sh new file mode 100755 index 00000000..b58c183d --- /dev/null +++ b/scripts/fetch-spc.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash + +set -euo pipefail + +OS=${1:-$(uname -s | tr '[:upper:]' '[:lower:]')} +ARCH=${2:-$(uname -m)} + +if [[ "$OS" == "darwin" ]]; then + OS="macos" +fi + +if [[ "$ARCH" == "arm64" ]]; then + ARCH="aarch64" +elif [[ "$ARCH" == "amd64" ]]; then + ARCH="x86_64" +fi + +curl -fsSL -o spc https://dl.static-php.dev/static-php-cli/spc-bin/nightly/spc-${OS}-${ARCH} +chmod +x ./spc +./spc doctor --auto-fix