Skip to main content

ext_php_rs/zend/
globals.rs

1//! Types related to the PHP executor, sapi and process globals.
2
3use parking_lot::{ArcRwLockReadGuard, ArcRwLockWriteGuard, RawRwLock, RwLock};
4use std::collections::HashMap;
5use std::ffi::CStr;
6use std::ops::{Deref, DerefMut};
7use std::slice;
8use std::str;
9use std::sync::{Arc, LazyLock};
10
11use crate::boxed::ZBox;
12use crate::error::{Error, Result};
13use crate::exception::{PhpException, PhpResult};
14#[cfg(php82)]
15use crate::ffi::zend_atomic_bool_store;
16use crate::ffi::{
17    _sapi_module_struct, _zend_compiler_globals, _zend_executor_globals, TRACK_VARS_COOKIE,
18    TRACK_VARS_ENV, TRACK_VARS_FILES, TRACK_VARS_GET, TRACK_VARS_POST, TRACK_VARS_SERVER,
19    ext_php_rs_compiler_globals, ext_php_rs_executor_globals, ext_php_rs_file_globals,
20    ext_php_rs_process_globals, ext_php_rs_sapi_globals, ext_php_rs_sapi_module, php_core_globals,
21    php_file_globals, sapi_globals_struct, sapi_header_struct, sapi_headers_struct,
22    sapi_request_info, zend_ini_entry, zend_is_auto_global,
23};
24#[cfg(not(php81))]
25use crate::ffi::{_zend_hash_find_known_hash, _zend_string};
26#[cfg(php81)]
27use crate::ffi::{
28    _zend_known_string_id_ZEND_STR_AUTOGLOBAL_REQUEST, zend_hash_find_known_hash,
29    zend_known_strings,
30};
31
32use crate::types::{ZendHashTable, ZendObject, ZendStr, Zval};
33
34use super::linked_list::ZendLinkedListIterator;
35
36/// Names the class of an exception object, falling back to `Exception` when the
37/// engine cannot report one.
38fn class_name_or_fallback(obj: &ZendObject) -> String {
39    obj.get_class_name().unwrap_or_else(|_| "Exception".into())
40}
41
42/// Stores global variables used in the PHP executor.
43pub type ExecutorGlobals = _zend_executor_globals;
44
45impl ExecutorGlobals {
46    /// Returns a reference to the PHP executor globals.
47    ///
48    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
49    /// immutable references at one time but only ever one mutable reference.
50    /// Attempting to retrieve the globals while already holding the global
51    /// guard will lead to a deadlock. Dropping the globals guard will release
52    /// the lock.
53    ///
54    /// # Panics
55    ///
56    /// * If static executor globals are not set
57    #[expect(
58        clippy::expect_used,
59        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
60    )]
61    pub fn get() -> GlobalReadGuard<Self> {
62        // SAFETY: PHP executor globals are statically declared therefore should never
63        // return an invalid pointer.
64        let globals = unsafe { ext_php_rs_executor_globals().as_ref() }
65            .expect("Static executor globals were invalid");
66
67        cfg_if::cfg_if! {
68            if #[cfg(php_zts)] {
69                let guard = lock::GLOBALS_LOCK.with(RwLock::read_arc);
70            } else {
71                let guard = lock::GLOBALS_LOCK.read_arc();
72            }
73        }
74
75        GlobalReadGuard { globals, guard }
76    }
77
78    /// Returns a mutable reference to the PHP executor globals.
79    ///
80    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
81    /// immutable references at one time but only ever one mutable reference.
82    /// Attempting to retrieve the globals while already holding the global
83    /// guard will lead to a deadlock. Dropping the globals guard will release
84    /// the lock.
85    ///
86    /// # Panics
87    ///
88    /// * If static executor globals are not set
89    #[expect(
90        clippy::expect_used,
91        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
92    )]
93    pub fn get_mut() -> GlobalWriteGuard<Self> {
94        // SAFETY: PHP executor globals are statically declared therefore should never
95        // return an invalid pointer.
96        let globals = unsafe { ext_php_rs_executor_globals().as_mut() }
97            .expect("Static executor globals were invalid");
98
99        cfg_if::cfg_if! {
100            if #[cfg(php_zts)] {
101                let guard = lock::GLOBALS_LOCK.with(RwLock::write_arc);
102            } else {
103                let guard = lock::GLOBALS_LOCK.write_arc();
104            }
105        }
106
107        GlobalWriteGuard { globals, guard }
108    }
109
110    /// Attempts to retrieve the global class hash table.
111    #[must_use]
112    pub fn class_table(&self) -> Option<&ZendHashTable> {
113        unsafe { self.class_table.as_ref() }
114    }
115
116    /// Attempts to retrieve the global functions hash table.
117    #[must_use]
118    pub fn function_table(&self) -> Option<&ZendHashTable> {
119        unsafe { self.function_table.as_ref() }
120    }
121
122    /// Attempts to retrieve the global functions hash table as mutable.
123    #[must_use]
124    pub fn function_table_mut(&mut self) -> Option<&mut ZendHashTable> {
125        unsafe { self.function_table.as_mut() }
126    }
127
128    /// Retrieves the ini values for all ini directives in the current executor
129    /// context..
130    ///
131    /// Returns an empty map if the ini directives have not been registered yet,
132    /// which is only the case before engine startup. Values that are not valid
133    /// UTF-8 are decoded lossily, since a `php.ini` directive may hold arbitrary
134    /// bytes; an absent value is reported as `None`.
135    #[must_use]
136    pub fn ini_values(&self) -> HashMap<String, Option<String>> {
137        // SAFETY: `EG(ini_directives)` is assigned during `zend_ini_startup` and
138        // remains valid for the lifetime of the request, but is null before that.
139        let Some(hash_table) = (unsafe { self.ini_directives.as_ref() }) else {
140            return HashMap::new();
141        };
142        let mut ini_hash_map: HashMap<String, Option<String>> = HashMap::new();
143        for (key, entry) in hash_table {
144            // SAFETY: entries are stored with `zend_hash_add_ptr` and owned by the
145            // hash table, which outlives this loop. A non-pointer zval is not
146            // reachable, but is skipped rather than trusted.
147            let value = unsafe {
148                let Some(entry) = entry.ptr::<zend_ini_entry>().and_then(|e| e.as_ref()) else {
149                    continue;
150                };
151                entry
152                    .value
153                    .as_ref()
154                    .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned())
155            };
156            ini_hash_map.insert(key.to_string(), value);
157        }
158        ini_hash_map
159    }
160
161    /// Attempts to retrieve the global constants table.
162    #[must_use]
163    pub fn constants(&self) -> Option<&ZendHashTable> {
164        unsafe { self.zend_constants.as_ref() }
165    }
166
167    /// Attempts to extract the last PHP exception captured by the interpreter.
168    /// Returned inside a [`ZBox`].
169    ///
170    /// This function requires the executor globals to be mutably held, which
171    /// could lead to a deadlock if the globals are already borrowed immutably
172    /// or mutably.
173    #[must_use]
174    pub fn take_exception() -> Option<ZBox<ZendObject>> {
175        {
176            // This avoid a write lock if there is no exception.
177            if Self::get().exception.is_null() {
178                return None;
179            }
180        }
181
182        let mut globals = Self::get_mut();
183
184        let mut exception_ptr = std::ptr::null_mut();
185        std::mem::swap(&mut exception_ptr, &mut globals.exception);
186
187        // SAFETY: `as_mut` checks for null.
188        Some(unsafe { ZBox::from_raw(exception_ptr.as_mut()?) })
189    }
190
191    /// Checks if the executor globals contain an exception.
192    #[must_use]
193    pub fn has_exception() -> bool {
194        !Self::get().exception.is_null()
195    }
196
197    /// Borrows the pending exception without taking ownership of it.
198    ///
199    /// The exception stays in the executor globals, so it keeps propagating on
200    /// its own once control returns to PHP. Use
201    /// [`take_exception`](Self::take_exception) to handle it from Rust instead.
202    #[must_use]
203    pub fn exception(&self) -> Option<&ZendObject> {
204        // SAFETY: the pointer is either null or a valid object owned by the engine,
205        // borrowed for no longer than the globals guard this method is called on.
206        unsafe { self.exception.as_ref() }
207    }
208
209    /// Returns the class name of the pending exception, leaving the exception
210    /// where it is.
211    ///
212    /// Returns [`None`] when no exception is pending. A class the engine cannot
213    /// name is reported as `Exception`.
214    #[must_use]
215    pub fn pending_exception_class() -> Option<String> {
216        Some(class_name_or_fallback(Self::get().exception()?))
217    }
218
219    /// Attempts to extract the last PHP exception captured by the interpreter.
220    /// Returned inside a [`PhpResult`].
221    ///
222    /// This function requires the executor globals to be mutably held, which
223    /// could lead to a deadlock if the globals are already borrowed immutably
224    /// or mutably.
225    ///
226    /// # Errors
227    ///
228    /// If an exception is present, it will be returned as `Err` value inside a
229    /// [`PhpResult`].
230    pub fn throw_if_exception() -> PhpResult<()> {
231        let Some(mut obj) = Self::take_exception() else {
232            return Ok(());
233        };
234
235        let class = class_name_or_fallback(&obj);
236        let mut zv = Zval::new();
237        zv.set_object(&mut obj);
238        Err(PhpException::from_message(class).with_object(zv))
239    }
240
241    /// Request an interrupt of the PHP VM. This will call the registered
242    /// interrupt handler function.
243    /// set with [`crate::ffi::zend_interrupt_function`].
244    pub fn request_interrupt(&mut self) {
245        cfg_if::cfg_if! {
246            if #[cfg(php82)] {
247                unsafe {
248                    zend_atomic_bool_store(&raw mut self.vm_interrupt, true);
249                }
250            } else {
251                self.vm_interrupt = true;
252            }
253        }
254    }
255
256    /// Cancel a requested an interrupt of the PHP VM.
257    pub fn cancel_interrupt(&mut self) {
258        cfg_if::cfg_if! {
259            if #[cfg(php82)] {
260                unsafe {
261                    zend_atomic_bool_store(&raw mut self.vm_interrupt, false);
262                }
263            } else {
264                self.vm_interrupt = true;
265            }
266        }
267    }
268}
269
270pub type CompilerGlobals = _zend_compiler_globals;
271
272impl CompilerGlobals {
273    /// Returns a reference to the PHP compiler globals.
274    ///
275    /// The compiler globals are guarded by a [`RwLock`]. There can be multiple
276    /// immutable references at one time but only ever one mutable reference.
277    /// Attempting to retrieve the globals while already holding the global
278    /// guard will lead to a deadlock. Dropping the globals guard will release
279    /// the lock.
280    ///
281    /// # Panics
282    ///
283    /// * If static executor globals are not set
284    #[expect(
285        clippy::expect_used,
286        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
287    )]
288    pub fn get() -> GlobalReadGuard<Self> {
289        // SAFETY: PHP compiler globals are statically declared therefore should never
290        // return an invalid pointer.
291        let globals = unsafe { ext_php_rs_compiler_globals().as_ref() }
292            .expect("Static compiler globals were invalid");
293
294        cfg_if::cfg_if! {
295            if #[cfg(php_zts)] {
296                let guard = lock::GLOBALS_LOCK.with(RwLock::read_arc);
297            } else {
298                let guard = lock::GLOBALS_LOCK.read_arc();
299            }
300        }
301
302        GlobalReadGuard { globals, guard }
303    }
304
305    /// Returns a mutable reference to the PHP compiler globals.
306    ///
307    /// The compiler globals are guarded by a [`RwLock`]. There can be multiple
308    /// immutable references at one time but only ever one mutable reference.
309    /// Attempting to retrieve the globals while already holding the global
310    /// guard will lead to a deadlock. Dropping the globals guard will release
311    /// the lock.
312    ///
313    /// # Panics
314    ///
315    /// * If static compiler globals are not set
316    #[expect(
317        clippy::expect_used,
318        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
319    )]
320    pub fn get_mut() -> GlobalWriteGuard<Self> {
321        // SAFETY: PHP compiler globals are statically declared therefore should never
322        // return an invalid pointer.
323        let globals = unsafe { ext_php_rs_compiler_globals().as_mut() }
324            .expect("Static compiler globals were invalid");
325
326        cfg_if::cfg_if! {
327            if #[cfg(php_zts)] {
328                let guard = lock::GLOBALS_LOCK.with(RwLock::write_arc);
329            } else {
330                let guard = lock::GLOBALS_LOCK.write_arc();
331            }
332        }
333
334        GlobalWriteGuard { globals, guard }
335    }
336}
337
338/// Stores the SAPI module used in the PHP executor.
339pub type SapiModule = _sapi_module_struct;
340
341impl SapiModule {
342    /// Returns a reference to the PHP SAPI module.
343    ///
344    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
345    /// immutable references at one time but only ever one mutable reference.
346    /// Attempting to retrieve the globals while already holding the global
347    /// guard will lead to a deadlock. Dropping the globals guard will release
348    /// the lock.
349    ///
350    /// # Panics
351    ///
352    /// * If static executor globals are not set
353    #[expect(
354        clippy::expect_used,
355        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
356    )]
357    pub fn get() -> GlobalReadGuard<Self> {
358        // SAFETY: PHP executor globals are statically declared therefore should never
359        // return an invalid pointer.
360        let globals = unsafe { ext_php_rs_sapi_module().as_ref() }
361            .expect("Static executor globals were invalid");
362        let guard = SAPI_MODULE_LOCK.read_arc();
363        GlobalReadGuard { globals, guard }
364    }
365
366    /// Returns a mutable reference to the PHP executor globals.
367    ///
368    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
369    /// immutable references at one time but only ever one mutable reference.
370    /// Attempting to retrieve the globals while already holding the global
371    /// guard will lead to a deadlock. Dropping the globals guard will release
372    /// the lock.
373    ///
374    /// # Panics
375    ///
376    /// * If static executor globals are not set
377    #[expect(
378        clippy::expect_used,
379        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
380    )]
381    pub fn get_mut() -> GlobalWriteGuard<Self> {
382        // SAFETY: PHP executor globals are statically declared therefore should never
383        // return an invalid pointer.
384        let globals = unsafe { ext_php_rs_sapi_module().as_mut() }
385            .expect("Static executor globals were invalid");
386        let guard = SAPI_MODULE_LOCK.write_arc();
387        GlobalWriteGuard { globals, guard }
388    }
389}
390
391/// Stores global variables used in the PHP executor.
392pub type ProcessGlobals = php_core_globals;
393
394impl ProcessGlobals {
395    /// Returns a reference to the PHP process globals.
396    ///
397    /// The process globals are guarded by a [`RwLock`]. There can be multiple
398    /// immutable references at one time but only ever one mutable reference.
399    /// Attempting to retrieve the globals while already holding the global
400    /// guard will lead to a deadlock. Dropping the globals guard will release
401    /// the lock.
402    pub fn get() -> GlobalReadGuard<Self> {
403        // SAFETY: PHP executor globals are statically declared therefore should never
404        // return an invalid pointer.
405        let globals = unsafe { &*ext_php_rs_process_globals() };
406
407        cfg_if::cfg_if! {
408            if #[cfg(php_zts)] {
409                let guard = lock::PROCESS_GLOBALS_LOCK.with(RwLock::read_arc);
410            } else {
411                let guard = lock::PROCESS_GLOBALS_LOCK.read_arc();
412            }
413        }
414
415        GlobalReadGuard { globals, guard }
416    }
417
418    /// Returns a mutable reference to the PHP executor globals.
419    ///
420    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
421    /// immutable references at one time but only ever one mutable reference.
422    /// Attempting to retrieve the globals while already holding the global
423    /// guard will lead to a deadlock. Dropping the globals guard will release
424    /// the lock.
425    pub fn get_mut() -> GlobalWriteGuard<Self> {
426        // SAFETY: PHP executor globals are statically declared therefore should never
427        // return an invalid pointer.
428        let globals = unsafe { &mut *ext_php_rs_process_globals() };
429
430        cfg_if::cfg_if! {
431            if #[cfg(php_zts)] {
432                let guard = lock::PROCESS_GLOBALS_LOCK.with(RwLock::write_arc);
433            } else {
434                let guard = lock::PROCESS_GLOBALS_LOCK.write_arc();
435            }
436        }
437
438        GlobalWriteGuard { globals, guard }
439    }
440
441    /// Get the HTTP Server variables. Equivalent of $_SERVER.
442    #[must_use]
443    pub fn http_server_vars(&self) -> Option<&ZendHashTable> {
444        // $_SERVER is lazy-initted, we need to call zend_is_auto_global
445        // if it's not already populated.
446        if !self.http_globals[TRACK_VARS_SERVER as usize].is_array() {
447            let mut name = ZendStr::new("_SERVER", false);
448            // SAFETY: `name` is bound to a local so the `ZBox` outlives the call.
449            // Taking the pointer from a temporary would free the `zend_string` at the
450            // end of the statement and leave `zend_is_auto_global` reading freed
451            // Zend memory.
452            unsafe { zend_is_auto_global(name.as_mut_ptr()) };
453        }
454        if self.http_globals[TRACK_VARS_SERVER as usize].is_array() {
455            self.http_globals[TRACK_VARS_SERVER as usize].array()
456        } else {
457            None
458        }
459    }
460
461    /// Get the HTTP POST variables. Equivalent of $_POST.
462    ///
463    /// Returns `None` if the global has not been populated as an array (yet).
464    #[must_use]
465    pub fn http_post_vars(&self) -> Option<&ZendHashTable> {
466        self.http_globals[TRACK_VARS_POST as usize].array()
467    }
468
469    /// Get the HTTP GET variables. Equivalent of $_GET.
470    ///
471    /// Returns `None` if the global has not been populated as an array (yet).
472    #[must_use]
473    pub fn http_get_vars(&self) -> Option<&ZendHashTable> {
474        self.http_globals[TRACK_VARS_GET as usize].array()
475    }
476
477    /// Get the HTTP Cookie variables. Equivalent of $_COOKIE.
478    ///
479    /// Returns `None` if the global has not been populated as an array (yet).
480    #[must_use]
481    pub fn http_cookie_vars(&self) -> Option<&ZendHashTable> {
482        self.http_globals[TRACK_VARS_COOKIE as usize].array()
483    }
484
485    /// Get the HTTP Request variables. Equivalent of $_REQUEST.
486    ///
487    /// Returns `Ok(None)` if the global has not been populated (yet).
488    ///
489    /// # Errors
490    ///
491    /// * [`Error::AutoGlobalLoadFailed`] - If the engine failed to load the
492    ///   `_REQUEST` auto-global.
493    /// * [`Error::ZvalConversion`] - If the global is not an array.
494    pub fn http_request_vars(&self) -> Result<Option<&ZendHashTable>> {
495        cfg_if::cfg_if! {
496            if #[cfg(php81)] {
497                let key = unsafe {
498                    *zend_known_strings.add(_zend_known_string_id_ZEND_STR_AUTOGLOBAL_REQUEST as usize)
499                };
500            } else {
501                // The `ZBox` must be bound so it outlives every use of `key` below;
502                // taking the pointer from a temporary would free the `zend_string` at
503                // the end of this statement.
504                let mut owned_key = _zend_string::new("_REQUEST", false);
505                let key = owned_key.as_mut_ptr();
506            }
507        };
508
509        // `$_REQUEST` is lazy-initted, we need to call `zend_is_auto_global` to make
510        // sure it's populated.
511        if !unsafe { zend_is_auto_global(key) } {
512            return Err(Error::AutoGlobalLoadFailed("_REQUEST"));
513        }
514
515        let symbol_table = &ExecutorGlobals::get().symbol_table;
516        cfg_if::cfg_if! {
517            if #[cfg(php81)] {
518                let request = unsafe { zend_hash_find_known_hash(symbol_table, key) };
519            } else {
520                let request = unsafe { _zend_hash_find_known_hash(symbol_table, key) };
521            }
522        };
523
524        let Some(request) = (unsafe { request.as_ref() }) else {
525            return Ok(None);
526        };
527
528        request
529            .array()
530            .map(Some)
531            .ok_or_else(|| Error::ZvalConversion(request.get_type()))
532    }
533
534    /// Get the HTTP Environment variables. Equivalent of $_ENV.
535    ///
536    /// Returns `None` if the global has not been populated as an array (yet).
537    #[must_use]
538    pub fn http_env_vars(&self) -> Option<&ZendHashTable> {
539        self.http_globals[TRACK_VARS_ENV as usize].array()
540    }
541
542    /// Get the HTTP Files variables. Equivalent of $_FILES.
543    ///
544    /// Returns `None` if the global has not been populated as an array (yet).
545    #[must_use]
546    pub fn http_files_vars(&self) -> Option<&ZendHashTable> {
547        self.http_globals[TRACK_VARS_FILES as usize].array()
548    }
549}
550
551/// Stores global variables used in the SAPI.
552pub type SapiGlobals = sapi_globals_struct;
553
554impl SapiGlobals {
555    /// Returns a reference to the PHP process globals.
556    ///
557    /// The process globals are guarded by a [`RwLock`]. There can be multiple
558    /// immutable references at one time but only ever one mutable reference.
559    /// Attempting to retrieve the globals while already holding the global
560    /// guard will lead to a deadlock. Dropping the globals guard will release
561    /// the lock.
562    #[must_use]
563    pub fn get() -> GlobalReadGuard<Self> {
564        // SAFETY: PHP executor globals are statically declared therefore should never
565        // return an invalid pointer.
566        let globals = unsafe { &*ext_php_rs_sapi_globals() };
567
568        cfg_if::cfg_if! {
569            if #[cfg(php_zts)] {
570                let guard = lock::SAPI_GLOBALS_LOCK.with(RwLock::read_arc);
571            } else {
572                let guard = lock::SAPI_GLOBALS_LOCK.read_arc();
573            }
574        }
575
576        GlobalReadGuard { globals, guard }
577    }
578
579    /// Returns a mutable reference to the PHP executor globals.
580    ///
581    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
582    /// immutable references at one time but only ever one mutable reference.
583    /// Attempting to retrieve the globals while already holding the global
584    /// guard will lead to a deadlock. Dropping the globals guard will release
585    /// the lock.
586    pub fn get_mut() -> GlobalWriteGuard<Self> {
587        // SAFETY: PHP executor globals are statically declared therefore should never
588        // return an invalid pointer.
589        let globals = unsafe { &mut *ext_php_rs_sapi_globals() };
590
591        cfg_if::cfg_if! {
592            if #[cfg(php_zts)] {
593                let guard = lock::SAPI_GLOBALS_LOCK.with(RwLock::write_arc);
594            } else {
595                let guard = lock::SAPI_GLOBALS_LOCK.write_arc();
596            }
597        }
598
599        GlobalWriteGuard { globals, guard }
600    }
601
602    /// Get the request info for the Sapi.
603    #[must_use]
604    pub fn request_info(&self) -> &SapiRequestInfo {
605        &self.request_info
606    }
607
608    /// Get the sapi headers for the Sapi.
609    #[must_use]
610    pub fn sapi_headers(&self) -> &SapiHeaders {
611        &self.sapi_headers
612    }
613}
614
615/// Stores SAPI headers. Exposed through `SapiGlobals`.
616pub type SapiHeaders = sapi_headers_struct;
617
618impl<'a> SapiHeaders {
619    /// Create an iterator over the headers.
620    pub fn headers(&'a mut self) -> ZendLinkedListIterator<'a, SapiHeader> {
621        self.headers.iter()
622    }
623}
624
625/// Manage a key/value pair of SAPI headers.
626pub type SapiHeader = sapi_header_struct;
627
628impl<'a> SapiHeader {
629    /// Get the header as a string.
630    ///
631    /// Returns `None` if the header is null, empty or not valid UTF-8.
632    #[must_use]
633    pub fn as_str(&'a self) -> Option<&'a str> {
634        if self.header.is_null() || self.header_len == 0 {
635            return None;
636        }
637        unsafe {
638            let slice = slice::from_raw_parts(self.header as *const u8, self.header_len);
639            str::from_utf8(slice).ok()
640        }
641    }
642
643    /// Returns the header name (key).
644    ///
645    /// Returns `None` if the header is null, empty or not valid UTF-8.
646    #[must_use]
647    pub fn name(&'a self) -> Option<&'a str> {
648        Some(self.as_str()?.split(':').next().unwrap_or("").trim())
649    }
650
651    /// Returns the header value.
652    ///
653    /// Returns `None` if the header is null, empty, not valid UTF-8 or has no
654    /// value.
655    #[must_use]
656    pub fn value(&'a self) -> Option<&'a str> {
657        self.as_str()?
658            .split_once(':')
659            .map(|(_, value)| value.trim())
660    }
661}
662
663/// Information about the current request, as the SAPI gives it.
664pub type SapiRequestInfo = sapi_request_info;
665
666impl SapiRequestInfo {
667    /// Get the request method.
668    #[must_use]
669    pub fn request_method(&self) -> Option<&str> {
670        if self.request_method.is_null() {
671            return None;
672        }
673        unsafe { CStr::from_ptr(self.request_method).to_str().ok() }
674    }
675
676    /// Get the query string.
677    #[must_use]
678    pub fn query_string(&self) -> Option<&str> {
679        if self.query_string.is_null() {
680            return None;
681        }
682        unsafe { CStr::from_ptr(self.query_string).to_str().ok() }
683    }
684
685    /// Get the cookie data.
686    #[must_use]
687    pub fn cookie_data(&self) -> Option<&str> {
688        if self.cookie_data.is_null() {
689            return None;
690        }
691        unsafe { CStr::from_ptr(self.cookie_data).to_str().ok() }
692    }
693
694    /// Get the content length.
695    #[must_use]
696    pub fn content_length(&self) -> i64 {
697        self.content_length
698    }
699
700    /// Get the path info.
701    #[must_use]
702    pub fn path_translated(&self) -> Option<&str> {
703        if self.path_translated.is_null() {
704            return None;
705        }
706        unsafe { CStr::from_ptr(self.path_translated).to_str().ok() }
707    }
708
709    /// Get the request uri.
710    #[must_use]
711    pub fn request_uri(&self) -> Option<&str> {
712        if self.request_uri.is_null() {
713            return None;
714        }
715        unsafe { CStr::from_ptr(self.request_uri).to_str().ok() }
716    }
717
718    // Todo: request_body _php_stream
719
720    /// Get the content type.
721    #[must_use]
722    pub fn content_type(&self) -> Option<&str> {
723        if self.content_type.is_null() {
724            return None;
725        }
726        unsafe { CStr::from_ptr(self.content_type).to_str().ok() }
727    }
728
729    /// Whether the request consists of headers only.
730    #[must_use]
731    pub fn headers_only(&self) -> bool {
732        self.headers_only
733    }
734
735    /// Whether the request has no headers.
736    #[must_use]
737    pub fn no_headers(&self) -> bool {
738        self.no_headers
739    }
740
741    /// Whether the request headers have been read.
742    #[must_use]
743    pub fn headers_read(&self) -> bool {
744        self.headers_read
745    }
746
747    // Todo: post_entry sapi_post_entry
748
749    /// Get the auth user.
750    #[must_use]
751    pub fn auth_user(&self) -> Option<&str> {
752        if self.auth_user.is_null() {
753            return None;
754        }
755        unsafe { CStr::from_ptr(self.auth_user).to_str().ok() }
756    }
757
758    /// Get the auth password.
759    #[must_use]
760    pub fn auth_password(&self) -> Option<&str> {
761        if self.auth_password.is_null() {
762            return None;
763        }
764        unsafe { CStr::from_ptr(self.auth_password).to_str().ok() }
765    }
766
767    /// Get the auth digest.
768    #[must_use]
769    pub fn auth_digest(&self) -> Option<&str> {
770        if self.auth_digest.is_null() {
771            return None;
772        }
773        unsafe { CStr::from_ptr(self.auth_digest).to_str().ok() }
774    }
775
776    /// Get argv0.
777    #[must_use]
778    pub fn argv0(&self) -> Option<&str> {
779        if self.argv0.is_null() {
780            return None;
781        }
782        unsafe { CStr::from_ptr(self.argv0).to_str().ok() }
783    }
784
785    /// Get the current user.
786    #[must_use]
787    pub fn current_user(&self) -> Option<&str> {
788        cfg_if::cfg_if! {
789            if #[cfg(php86)] {
790                unsafe { self.current_user.as_ref() }.and_then(|user| user.as_str().ok())
791            } else {
792                if self.current_user.is_null() {
793                    return None;
794                }
795                unsafe { CStr::from_ptr(self.current_user).to_str().ok() }
796            }
797        }
798    }
799
800    /// Get the current user length.
801    ///
802    /// PHP 8.6 removes this field. Use [`Self::current_user`] instead.
803    #[cfg(not(php86))]
804    #[must_use]
805    pub fn current_user_length(&self) -> i32 {
806        self.current_user_length
807    }
808
809    /// Get argvc.
810    #[must_use]
811    pub fn argvc(&self) -> i32 {
812        self.argc
813    }
814
815    /// Get argv.
816    #[must_use]
817    pub fn argv(&self) -> Option<&str> {
818        if self.argv.is_null() {
819            return None;
820        }
821        unsafe { CStr::from_ptr(*self.argv).to_str().ok() }
822    }
823
824    /// Get the protocol number.
825    #[must_use]
826    pub fn proto_num(&self) -> i32 {
827        self.proto_num
828    }
829}
830
831/// Stores global variables used in the SAPI.
832pub type FileGlobals = php_file_globals;
833
834impl FileGlobals {
835    /// Returns a reference to the PHP process globals.
836    ///
837    /// The process globals are guarded by a [`RwLock`]. There can be multiple
838    /// immutable references at one time but only ever one mutable reference.
839    /// Attempting to retrieve the globals while already holding the global
840    /// guard will lead to a deadlock. Dropping the globals guard will release
841    /// the lock.
842    ///
843    /// # Panics
844    ///
845    /// * If static file globals are not set
846    #[expect(
847        clippy::expect_used,
848        reason = "the engine declares these globals statically, so the pointer is valid for the life of the process"
849    )]
850    pub fn get() -> GlobalReadGuard<Self> {
851        // SAFETY: PHP executor globals are statically declared therefore should never
852        // return an invalid pointer.
853        let globals = unsafe { ext_php_rs_file_globals().as_ref() }
854            .expect("Static file globals were invalid");
855
856        cfg_if::cfg_if! {
857            if #[cfg(php_zts)] {
858                let guard = lock::FILE_GLOBALS_LOCK.with(RwLock::read_arc);
859            } else {
860                let guard = lock::FILE_GLOBALS_LOCK.read_arc();
861            }
862        }
863
864        GlobalReadGuard { globals, guard }
865    }
866
867    /// Returns a mutable reference to the PHP executor globals.
868    ///
869    /// The executor globals are guarded by a [`RwLock`]. There can be multiple
870    /// immutable references at one time but only ever one mutable reference.
871    /// Attempting to retrieve the globals while already holding the global
872    /// guard will lead to a deadlock. Dropping the globals guard will release
873    /// the lock.
874    #[must_use]
875    pub fn get_mut() -> GlobalWriteGuard<Self> {
876        // SAFETY: PHP executor globals are statically declared therefore should never
877        // return an invalid pointer.
878        let globals = unsafe { &mut *ext_php_rs_file_globals() };
879
880        cfg_if::cfg_if! {
881            if #[cfg(php_zts)] {
882                let guard = lock::FILE_GLOBALS_LOCK.with(RwLock::write_arc);
883            } else {
884                let guard = lock::FILE_GLOBALS_LOCK.write_arc();
885            }
886        }
887
888        GlobalWriteGuard { globals, guard }
889    }
890
891    /// Returns the stream wrappers
892    #[must_use]
893    pub fn stream_wrappers(&self) -> Option<&'static ZendHashTable> {
894        unsafe { self.stream_wrappers.as_ref() }
895    }
896}
897
898/// Executor globals rwlock.
899///
900/// PHP provides no indication if the executor globals are being accessed so
901/// this is only effective on the Rust side.
902#[cfg(not(php_zts))]
903pub(crate) mod lock {
904    use parking_lot::RwLock;
905    use std::sync::{Arc, LazyLock};
906
907    pub(crate) static GLOBALS_LOCK: LazyLock<Arc<RwLock<()>>> =
908        LazyLock::new(|| Arc::new(RwLock::new(())));
909    pub(crate) static PROCESS_GLOBALS_LOCK: LazyLock<Arc<RwLock<()>>> =
910        LazyLock::new(|| Arc::new(RwLock::new(())));
911    pub(crate) static SAPI_GLOBALS_LOCK: LazyLock<Arc<RwLock<()>>> =
912        LazyLock::new(|| Arc::new(RwLock::new(())));
913    pub(crate) static FILE_GLOBALS_LOCK: LazyLock<Arc<RwLock<()>>> =
914        LazyLock::new(|| Arc::new(RwLock::new(())));
915}
916
917/// Executor globals rwlock.
918///
919/// PHP provides no indication if the executor globals are being accessed so
920/// this is only effective on the Rust side.
921#[cfg(php_zts)]
922pub(crate) mod lock {
923    use parking_lot::{RwLock, const_rwlock};
924    use std::sync::Arc;
925
926    thread_local! {
927        pub(crate) static GLOBALS_LOCK: Arc<RwLock<()>> =  Arc::new(const_rwlock(()));
928        pub(crate) static PROCESS_GLOBALS_LOCK: Arc<RwLock<()>> = Arc::new( const_rwlock(()) );
929        pub(crate) static SAPI_GLOBALS_LOCK: Arc<RwLock<()>> = Arc::new( const_rwlock(()) );
930        pub(crate) static FILE_GLOBALS_LOCK: Arc<RwLock<()>> = Arc::new( const_rwlock(()) );
931    }
932}
933
934/// SAPI globals rwlock.
935///
936/// PHP provides no indication if the executor globals are being accessed so
937/// this is only effective on the Rust side.
938static SAPI_MODULE_LOCK: LazyLock<Arc<RwLock<()>>> = LazyLock::new(|| Arc::new(RwLock::new(())));
939
940/// Wrapper guard that contains a reference to a given type `T`. Dropping a
941/// guard releases the lock on the relevant rwlock.
942pub struct GlobalReadGuard<T: 'static> {
943    globals: &'static T,
944    #[allow(dead_code)]
945    guard: ArcRwLockReadGuard<RawRwLock, ()>,
946}
947
948impl<T> Deref for GlobalReadGuard<T> {
949    type Target = T;
950
951    fn deref(&self) -> &Self::Target {
952        self.globals
953    }
954}
955
956/// Wrapper guard that contains a mutable reference to a given type `T`.
957/// Dropping a guard releases the lock on the relevant rwlock.
958pub struct GlobalWriteGuard<T: 'static> {
959    globals: &'static mut T,
960    #[allow(dead_code)]
961    guard: ArcRwLockWriteGuard<RawRwLock, ()>,
962}
963
964impl<T> Deref for GlobalWriteGuard<T> {
965    type Target = T;
966
967    fn deref(&self) -> &Self::Target {
968        self.globals
969    }
970}
971
972impl<T> DerefMut for GlobalWriteGuard<T> {
973    fn deref_mut(&mut self) -> &mut Self::Target {
974        self.globals
975    }
976}
977
978#[cfg(test)]
979mod tests {
980    use super::*;
981    use std::os::raw::c_char;
982
983    #[test]
984    fn sapi_header_parses_name_and_value() {
985        let headers = [
986            ("Content-Type: text/html", "Content-Type", "text/html"),
987            ("X: Custom:Header", "X", "Custom:Header"),
988        ];
989
990        for (header_text, name, value) in headers {
991            let header = SapiHeader {
992                header: header_text.as_bytes().as_ptr() as *mut c_char,
993                header_len: header_text.len(),
994            };
995            assert_eq!(header.name(), Some(name), "Header name mismatch");
996            assert_eq!(header.value(), Some(value), "Header value mismatch");
997            assert_eq!(
998                header.as_str(),
999                Some(format!("{name}: {value}").as_str()),
1000                "Header string mismatch"
1001            );
1002        }
1003    }
1004
1005    #[test]
1006    fn sapi_header_with_invalid_utf8_returns_none() {
1007        let bytes: &[u8] = b"X: \xFF\xFE";
1008        let header = SapiHeader {
1009            header: bytes.as_ptr() as *mut c_char,
1010            header_len: bytes.len(),
1011        };
1012        assert_eq!(header.as_str(), None);
1013        assert_eq!(header.name(), None);
1014        assert_eq!(header.value(), None);
1015    }
1016
1017    #[test]
1018    fn sapi_header_with_null_pointer_returns_none() {
1019        let header = SapiHeader {
1020            header: std::ptr::null_mut(),
1021            header_len: 0,
1022        };
1023        assert_eq!(header.as_str(), None);
1024        assert_eq!(header.name(), None);
1025        assert_eq!(header.value(), None);
1026    }
1027
1028    #[test]
1029    fn sapi_header_with_zero_length_returns_none() {
1030        let header = SapiHeader {
1031            header: c"".as_ptr().cast_mut(),
1032            header_len: 0,
1033        };
1034        assert_eq!(header.as_str(), None);
1035        assert_eq!(header.name(), None);
1036        assert_eq!(header.value(), None);
1037    }
1038}
1039
1040#[cfg(feature = "embed")]
1041#[cfg(test)]
1042mod embed_tests {
1043    use super::*;
1044    use crate::embed::Embed;
1045
1046    #[test]
1047    fn test_executor_globals() {
1048        Embed::run(|| {
1049            let state = ExecutorGlobals::get().active;
1050            ExecutorGlobals::get_mut().active = !state;
1051            let changed = ExecutorGlobals::get().active;
1052            ExecutorGlobals::get_mut().active = state;
1053            assert_eq!(changed, !state);
1054        });
1055    }
1056
1057    #[test]
1058    fn test_compiler_globals() {
1059        Embed::run(|| {
1060            let state = CompilerGlobals::get().in_compilation;
1061            CompilerGlobals::get_mut().in_compilation = !state;
1062            let changed = CompilerGlobals::get().in_compilation;
1063            CompilerGlobals::get_mut().in_compilation = state;
1064            assert_eq!(changed, !state);
1065        });
1066    }
1067
1068    #[test]
1069    fn test_file_globals_reflect_ini_set() {
1070        let timeout = Embed::run(|| {
1071            Embed::eval("ini_set('default_socket_timeout', '123');")
1072                .expect("failed to set the ini value");
1073            FileGlobals::get().default_socket_timeout
1074        });
1075        assert_eq!(timeout, 123);
1076    }
1077
1078    #[test]
1079    fn test_ini_values_decodes_invalid_utf8_lossily() {
1080        // A `php.ini` directive may hold arbitrary bytes, which used to panic.
1081        let value: Option<String> = Embed::run(|| {
1082            Embed::eval("ini_set('error_append_string', \"\\xFF\\xFE\");")
1083                .expect("failed to set the ini value");
1084            ExecutorGlobals::get()
1085                .ini_values()
1086                .remove("error_append_string")
1087                .expect("the directive should be present")
1088        });
1089        assert_eq!(value.as_deref(), Some("\u{FFFD}\u{FFFD}"));
1090    }
1091}