1use 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
36fn class_name_or_fallback(obj: &ZendObject) -> String {
39 obj.get_class_name().unwrap_or_else(|_| "Exception".into())
40}
41
42pub type ExecutorGlobals = _zend_executor_globals;
44
45impl ExecutorGlobals {
46 #[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 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 #[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 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 #[must_use]
112 pub fn class_table(&self) -> Option<&ZendHashTable> {
113 unsafe { self.class_table.as_ref() }
114 }
115
116 #[must_use]
118 pub fn function_table(&self) -> Option<&ZendHashTable> {
119 unsafe { self.function_table.as_ref() }
120 }
121
122 #[must_use]
124 pub fn function_table_mut(&mut self) -> Option<&mut ZendHashTable> {
125 unsafe { self.function_table.as_mut() }
126 }
127
128 #[must_use]
136 pub fn ini_values(&self) -> HashMap<String, Option<String>> {
137 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 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 #[must_use]
163 pub fn constants(&self) -> Option<&ZendHashTable> {
164 unsafe { self.zend_constants.as_ref() }
165 }
166
167 #[must_use]
174 pub fn take_exception() -> Option<ZBox<ZendObject>> {
175 {
176 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 Some(unsafe { ZBox::from_raw(exception_ptr.as_mut()?) })
189 }
190
191 #[must_use]
193 pub fn has_exception() -> bool {
194 !Self::get().exception.is_null()
195 }
196
197 #[must_use]
203 pub fn exception(&self) -> Option<&ZendObject> {
204 unsafe { self.exception.as_ref() }
207 }
208
209 #[must_use]
215 pub fn pending_exception_class() -> Option<String> {
216 Some(class_name_or_fallback(Self::get().exception()?))
217 }
218
219 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 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 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 #[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 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 #[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 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
338pub type SapiModule = _sapi_module_struct;
340
341impl SapiModule {
342 #[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 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 #[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 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
391pub type ProcessGlobals = php_core_globals;
393
394impl ProcessGlobals {
395 pub fn get() -> GlobalReadGuard<Self> {
403 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 pub fn get_mut() -> GlobalWriteGuard<Self> {
426 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 #[must_use]
443 pub fn http_server_vars(&self) -> Option<&ZendHashTable> {
444 if !self.http_globals[TRACK_VARS_SERVER as usize].is_array() {
447 let mut name = ZendStr::new("_SERVER", false);
448 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 #[must_use]
465 pub fn http_post_vars(&self) -> Option<&ZendHashTable> {
466 self.http_globals[TRACK_VARS_POST as usize].array()
467 }
468
469 #[must_use]
473 pub fn http_get_vars(&self) -> Option<&ZendHashTable> {
474 self.http_globals[TRACK_VARS_GET as usize].array()
475 }
476
477 #[must_use]
481 pub fn http_cookie_vars(&self) -> Option<&ZendHashTable> {
482 self.http_globals[TRACK_VARS_COOKIE as usize].array()
483 }
484
485 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 let mut owned_key = _zend_string::new("_REQUEST", false);
505 let key = owned_key.as_mut_ptr();
506 }
507 };
508
509 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 #[must_use]
538 pub fn http_env_vars(&self) -> Option<&ZendHashTable> {
539 self.http_globals[TRACK_VARS_ENV as usize].array()
540 }
541
542 #[must_use]
546 pub fn http_files_vars(&self) -> Option<&ZendHashTable> {
547 self.http_globals[TRACK_VARS_FILES as usize].array()
548 }
549}
550
551pub type SapiGlobals = sapi_globals_struct;
553
554impl SapiGlobals {
555 #[must_use]
563 pub fn get() -> GlobalReadGuard<Self> {
564 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 pub fn get_mut() -> GlobalWriteGuard<Self> {
587 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 #[must_use]
604 pub fn request_info(&self) -> &SapiRequestInfo {
605 &self.request_info
606 }
607
608 #[must_use]
610 pub fn sapi_headers(&self) -> &SapiHeaders {
611 &self.sapi_headers
612 }
613}
614
615pub type SapiHeaders = sapi_headers_struct;
617
618impl<'a> SapiHeaders {
619 pub fn headers(&'a mut self) -> ZendLinkedListIterator<'a, SapiHeader> {
621 self.headers.iter()
622 }
623}
624
625pub type SapiHeader = sapi_header_struct;
627
628impl<'a> SapiHeader {
629 #[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 #[must_use]
647 pub fn name(&'a self) -> Option<&'a str> {
648 Some(self.as_str()?.split(':').next().unwrap_or("").trim())
649 }
650
651 #[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
663pub type SapiRequestInfo = sapi_request_info;
665
666impl SapiRequestInfo {
667 #[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 #[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 #[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 #[must_use]
696 pub fn content_length(&self) -> i64 {
697 self.content_length
698 }
699
700 #[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 #[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 #[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 #[must_use]
731 pub fn headers_only(&self) -> bool {
732 self.headers_only
733 }
734
735 #[must_use]
737 pub fn no_headers(&self) -> bool {
738 self.no_headers
739 }
740
741 #[must_use]
743 pub fn headers_read(&self) -> bool {
744 self.headers_read
745 }
746
747 #[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 #[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 #[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 #[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 #[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 #[cfg(not(php86))]
804 #[must_use]
805 pub fn current_user_length(&self) -> i32 {
806 self.current_user_length
807 }
808
809 #[must_use]
811 pub fn argvc(&self) -> i32 {
812 self.argc
813 }
814
815 #[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 #[must_use]
826 pub fn proto_num(&self) -> i32 {
827 self.proto_num
828 }
829}
830
831pub type FileGlobals = php_file_globals;
833
834impl FileGlobals {
835 #[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 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 #[must_use]
875 pub fn get_mut() -> GlobalWriteGuard<Self> {
876 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 #[must_use]
893 pub fn stream_wrappers(&self) -> Option<&'static ZendHashTable> {
894 unsafe { self.stream_wrappers.as_ref() }
895 }
896}
897
898#[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#[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
934static SAPI_MODULE_LOCK: LazyLock<Arc<RwLock<()>>> = LazyLock::new(|| Arc::new(RwLock::new(())));
939
940pub 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
956pub 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 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}