• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

extphprs / ext-php-rs / 23860974183

01 Apr 2026 05:08PM UTC coverage: 65.292% (-0.2%) from 65.463%
23860974183

Pull #714

github

web-flow
Merge fc8becaac into 1110cec61
Pull Request #714: fix(embed): add null pointer guards to SAPI trampolines

10 of 44 new or added lines in 1 file covered. (22.73%)

43 existing lines in 1 file now uncovered.

8200 of 12559 relevant lines covered (65.29%)

33.49 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

30.83
/src/embed/sapi_trait.rs
1
use crate::builders::SapiBuilder;
2
use crate::embed::SapiModule;
3
use crate::embed::context::ServerContext;
4
use crate::embed::server_vars::ServerVarRegistrar;
5
use crate::error::Result;
6
use crate::ffi::{ext_php_rs_sapi_globals, sapi_header_struct, sapi_headers_struct};
7
use crate::types::Zval;
8
use std::ffi::{c_char, c_int, c_void};
9

10
/// Safe wrapper around `sapi_headers_struct` providing access to the HTTP
11
/// response code set by PHP.
12
pub struct SapiHeaders {
13
    raw: *mut sapi_headers_struct,
14
}
15

16
impl SapiHeaders {
17
    /// Returns the HTTP response code set by PHP (e.g. 200, 404, 500).
18
    #[must_use]
NEW
19
    pub fn http_response_code(&self) -> i32 {
×
NEW
20
        unsafe { (*self.raw).http_response_code }
×
NEW
21
    }
×
22
}
23

24
/// Safe wrapper around `sapi_header_struct` providing access to a single
25
/// HTTP response header sent by PHP.
26
pub struct SapiHeader {
27
    raw: *mut sapi_header_struct,
28
}
29

30
impl SapiHeader {
31
    /// Returns the raw header string (e.g. `"Content-Type: text/html"`).
32
    ///
33
    /// Returns `None` if the header data is not valid UTF-8.
34
    #[must_use]
NEW
35
    pub fn as_str(&self) -> Option<&str> {
×
NEW
36
        let raw = unsafe { &*self.raw };
×
NEW
37
        if raw.header.is_null() || raw.header_len == 0 {
×
NEW
38
            return None;
×
NEW
39
        }
×
NEW
40
        let bytes = unsafe { std::slice::from_raw_parts(raw.header.cast::<u8>(), raw.header_len) };
×
NEW
41
        std::str::from_utf8(bytes).ok()
×
NEW
42
    }
×
43

44
    /// Returns the header parsed as a `(name, value)` pair, splitting on the
45
    /// first `:`.
46
    ///
47
    /// Both name and value are trimmed of whitespace. Returns `None` if the
48
    /// header is not valid UTF-8 or does not contain `:`.
49
    #[must_use]
NEW
50
    pub fn as_name_value(&self) -> Option<(&str, &str)> {
×
NEW
51
        let s = self.as_str()?;
×
NEW
52
        let (name, value) = s.split_once(':')?;
×
NEW
53
        Some((name.trim(), value.trim()))
×
NEW
54
    }
×
55

56
    /// Returns the length of the header string in bytes.
57
    #[must_use]
NEW
58
    pub fn len(&self) -> usize {
×
NEW
59
        unsafe { (*self.raw).header_len }
×
NEW
60
    }
×
61

62
    /// Returns `true` if the header is empty.
63
    #[must_use]
NEW
64
    pub fn is_empty(&self) -> bool {
×
NEW
65
        self.len() == 0
×
NEW
66
    }
×
67
}
68

69
/// Result type for the `send_headers` SAPI callback.
70
#[non_exhaustive]
71
pub enum SendHeadersResult {
72
    /// SAPI handled all headers. PHP will not call `send_header` per header.
73
    SentSuccessfully,
74
    /// PHP should iterate headers and call `send_header` for each one.
75
    DoSend,
76
    /// Header sending failed.
77
    Failed,
78
}
79

80
impl SendHeadersResult {
81
    fn into_c_int(self) -> c_int {
1✔
82
        match self {
1✔
NEW
83
            Self::SentSuccessfully => 1,
×
NEW
84
            Self::DoSend => 2,
×
85
            Self::Failed => 3,
1✔
86
        }
87
    }
1✔
88
}
89

90
/// High-level trait for implementing a custom PHP SAPI in safe Rust.
91
///
92
/// Generates `extern "C"` trampoline functions that retrieve `Self::Context`
93
/// from `SG(server_context)` and dispatch to safe trait methods.
94
///
95
/// # Examples
96
///
97
/// ```rust,no_run
98
/// use ext_php_rs::embed::{Sapi, ServerContext, RequestInfo, ServerVarRegistrar};
99
///
100
/// struct MySapi;
101
/// struct MyCtx;
102
///
103
/// impl ServerContext for MyCtx {
104
///     fn init_request_info(&self, _info: &mut RequestInfo) {}
105
///     fn read_post(&mut self, _buf: &mut [u8]) -> usize { 0 }
106
///     fn read_cookies(&self) -> Option<&str> { None }
107
///     fn finish_request(&mut self) -> bool { true }
108
///     fn is_request_finished(&self) -> bool { true }
109
/// }
110
///
111
/// impl Sapi for MySapi {
112
///     type Context = MyCtx;
113
///     fn name() -> &'static str { "my-sapi" }
114
///     fn pretty_name() -> &'static str { "My SAPI" }
115
///     fn ub_write(_ctx: &mut MyCtx, buf: &[u8]) -> usize { buf.len() }
116
///     fn log_message(msg: &str, _: i32) { eprintln!("{msg}"); }
117
/// }
118
/// ```
119
pub trait Sapi: Send + Sync + 'static {
120
    /// Per-request context type.
121
    type Context: ServerContext;
122

123
    /// SAPI identifier (e.g. "ferron-php").
124
    fn name() -> &'static str;
125

126
    /// Human-readable SAPI name (e.g. "Ferron PHP Module").
127
    fn pretty_name() -> &'static str;
128

129
    /// Write output. Called by PHP's `echo`, `print`, etc.
130
    fn ub_write(ctx: &mut Self::Context, buf: &[u8]) -> usize;
131

132
    /// Log a message from PHP.
133
    fn log_message(message: &str, syslog_type: i32);
134

135
    /// Flush output buffer.
UNCOV
136
    fn flush(_ctx: &mut Self::Context) {}
×
137

138
    /// Send all response headers at once.
UNCOV
139
    fn send_headers(_ctx: &mut Self::Context, _headers: &SapiHeaders) -> SendHeadersResult {
×
UNCOV
140
        SendHeadersResult::DoSend
×
UNCOV
141
    }
×
142

143
    /// Send a single response header.
UNCOV
144
    fn send_header(_ctx: &mut Self::Context, _header: &SapiHeader) {}
×
145

146
    /// Read POST body chunk. Delegates to `ServerContext::read_post` by default.
UNCOV
147
    fn read_post(ctx: &mut Self::Context, buf: &mut [u8]) -> usize {
×
UNCOV
148
        ctx.read_post(buf)
×
UNCOV
149
    }
×
150

151
    /// Read cookie header. Delegates to `ServerContext::read_cookies` by
152
    /// default.
UNCOV
153
    fn read_cookies(ctx: &mut Self::Context) -> Option<String> {
×
UNCOV
154
        ctx.read_cookies().map(String::from)
×
UNCOV
155
    }
×
156

157
    /// Register `$_SERVER` variables.
158
    fn register_server_variables(_ctx: &mut Self::Context, _registrar: &mut ServerVarRegistrar) {}
×
159

160
    /// Build a [`SapiModule`] from this trait implementation.
161
    ///
162
    /// # Errors
163
    ///
164
    /// Returns an error if the SAPI name or pretty name contain null bytes.
165
    fn build_module() -> Result<SapiModule>
1✔
166
    where
1✔
167
        Self: Sized,
1✔
168
    {
169
        SapiBuilder::new(Self::name(), Self::pretty_name())
1✔
170
            .ub_write_function(trampoline_ub_write::<Self>)
1✔
171
            .log_message_function(trampoline_log_message::<Self>)
1✔
172
            .flush_function(trampoline_flush::<Self>)
1✔
173
            .send_headers_function(trampoline_send_headers::<Self>)
1✔
174
            .send_header_function(trampoline_send_header::<Self>)
1✔
175
            .read_post_function(trampoline_read_post::<Self>)
1✔
176
            .read_cookies_function(trampoline_read_cookies::<Self>)
1✔
177
            .register_server_variables_function(trampoline_register_server_variables::<Self>)
1✔
178
            .build()
1✔
179
    }
1✔
180
}
181

182
fn get_server_context<S: Sapi>() -> Option<&'static mut S::Context> {
3✔
183
    let globals = unsafe { &*ext_php_rs_sapi_globals() };
3✔
184
    let ctx_ptr = globals.server_context;
3✔
185
    if ctx_ptr.is_null() {
3✔
186
        return None;
3✔
UNCOV
187
    }
×
UNCOV
188
    Some(unsafe { &mut *ctx_ptr.cast::<S::Context>() })
×
189
}
3✔
190

UNCOV
191
extern "C" fn trampoline_ub_write<S: Sapi>(str: *const c_char, str_length: usize) -> usize {
×
UNCOV
192
    if str.is_null() || str_length == 0 {
×
UNCOV
193
        return 0;
×
UNCOV
194
    }
×
UNCOV
195
    let Some(ctx) = get_server_context::<S>() else {
×
UNCOV
196
        return 0;
×
197
    };
UNCOV
198
    let buf = unsafe { std::slice::from_raw_parts(str.cast::<u8>(), str_length) };
×
UNCOV
199
    S::ub_write(ctx, buf)
×
UNCOV
200
}
×
201

UNCOV
202
extern "C" fn trampoline_log_message<S: Sapi>(message: *const c_char, syslog_type: c_int) {
×
UNCOV
203
    if message.is_null() {
×
UNCOV
204
        return;
×
UNCOV
205
    }
×
UNCOV
206
    let msg = unsafe { std::ffi::CStr::from_ptr(message) };
×
UNCOV
207
    let msg_str = msg.to_string_lossy();
×
UNCOV
208
    S::log_message(&msg_str, syslog_type);
×
209
}
×
210

211
extern "C" fn trampoline_flush<S: Sapi>(server_context: *mut c_void) {
1✔
212
    let _ = server_context;
1✔
213
    if let Some(ctx) = get_server_context::<S>() {
1✔
NEW
214
        S::flush(ctx);
×
215
    }
1✔
216
}
1✔
217

218
extern "C" fn trampoline_send_headers<S: Sapi>(sapi_headers: *mut sapi_headers_struct) -> c_int {
1✔
219
    if sapi_headers.is_null() {
1✔
220
        return SendHeadersResult::Failed.into_c_int();
×
221
    }
1✔
222
    let Some(ctx) = get_server_context::<S>() else {
1✔
223
        return SendHeadersResult::Failed.into_c_int();
1✔
224
    };
NEW
225
    let headers = SapiHeaders { raw: sapi_headers };
×
NEW
226
    S::send_headers(ctx, &headers).into_c_int()
×
227
}
1✔
228

229
extern "C" fn trampoline_send_header<S: Sapi>(
×
230
    header: *mut sapi_header_struct,
×
231
    _server_context: *mut c_void,
×
UNCOV
232
) {
×
UNCOV
233
    if header.is_null() {
×
UNCOV
234
        return;
×
UNCOV
235
    }
×
236
    if let Some(ctx) = get_server_context::<S>() {
×
UNCOV
237
        let header = SapiHeader { raw: header };
×
UNCOV
238
        S::send_header(ctx, &header);
×
UNCOV
239
    }
×
UNCOV
240
}
×
241

NEW
242
extern "C" fn trampoline_read_post<S: Sapi>(buffer: *mut c_char, length: usize) -> usize {
×
NEW
243
    if buffer.is_null() || length == 0 {
×
UNCOV
244
        return 0;
×
UNCOV
245
    }
×
UNCOV
246
    let Some(ctx) = get_server_context::<S>() else {
×
NEW
247
        return 0;
×
248
    };
UNCOV
249
    let buf = unsafe { std::slice::from_raw_parts_mut(buffer.cast::<u8>(), length) };
×
UNCOV
250
    S::read_post(ctx, buf)
×
251
}
×
252

253
extern "C" fn trampoline_read_cookies<S: Sapi>() -> *mut c_char {
×
254
    let Some(ctx) = get_server_context::<S>() else {
×
NEW
255
        return std::ptr::null_mut();
×
256
    };
NEW
257
    match S::read_cookies(ctx) {
×
258
        Some(cookies) => match std::ffi::CString::new(cookies) {
×
NEW
259
            Ok(c) => c.into_raw(),
×
NEW
260
            Err(_) => std::ptr::null_mut(),
×
261
        },
262
        None => std::ptr::null_mut(),
×
263
    }
264
}
×
265

266
extern "C" fn trampoline_register_server_variables<S: Sapi>(vars: *mut Zval) {
1✔
267
    if vars.is_null() {
1✔
268
        return;
×
269
    }
1✔
270
    let Some(ctx) = get_server_context::<S>() else {
1✔
271
        return;
1✔
272
    };
273
    let mut registrar = unsafe { ServerVarRegistrar::from_raw(vars) };
×
UNCOV
274
    S::register_server_variables(ctx, &mut registrar);
×
275
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc