ext_php_rs/types/zval.rs
1//! The base value in PHP. A Zval can contain any PHP type, and the type that it
2//! contains is determined by a property inside the struct. The content of the
3//! Zval is stored in a union.
4
5use std::{convert::TryInto, ffi::c_void, fmt::Debug, ptr};
6
7use cfg_if::cfg_if;
8
9use crate::types::ZendIterator;
10use crate::types::iterable::Iterable;
11use crate::{
12 binary::Pack,
13 binary_slice::PackSlice,
14 boxed::ZBox,
15 convert::{FromZval, FromZvalMut, IntoZval, IntoZvalDyn},
16 error::{Error, Result},
17 ffi::{
18 _zval_struct__bindgen_ty_1, _zval_struct__bindgen_ty_2, GC_IMMUTABLE,
19 ext_php_rs_zend_string_release, zend_array_dup, zend_is_callable, zend_is_identical,
20 zend_is_iterable, zend_is_true, zend_resource, zend_value, zval, zval_ptr_dtor,
21 },
22 flags::DataType,
23 flags::ZvalTypeFlags,
24 rc::PhpRc,
25 types::{ZendCallable, ZendHashTable, ZendLong, ZendObject, ZendStr},
26};
27
28/// A zend value. This is the primary storage container used throughout the Zend
29/// engine.
30///
31/// A zval can be thought of as a Rust enum, a type that can contain different
32/// values such as integers, strings, objects etc.
33pub type Zval = zval;
34
35// TODO(david): can we make zval send+sync? main problem is that refcounted
36// types do not have atomic refcounters, so technically two threads could
37// reference the same object and attempt to modify refcounter at the same time.
38// need to look into how ZTS works.
39
40// unsafe impl Send for Zval {}
41// unsafe impl Sync for Zval {}
42
43impl Zval {
44 /// Creates a new, empty zval.
45 #[must_use]
46 pub const fn new() -> Self {
47 Self {
48 value: zend_value {
49 ptr: ptr::null_mut(),
50 },
51 #[allow(clippy::used_underscore_items)]
52 u1: _zval_struct__bindgen_ty_1 {
53 type_info: DataType::Null.as_u32(),
54 },
55 #[allow(clippy::used_underscore_items)]
56 u2: _zval_struct__bindgen_ty_2 { next: 0 },
57 }
58 }
59
60 /// Creates a null zval
61 #[must_use]
62 pub fn null() -> Zval {
63 let mut zval = Zval::new();
64 zval.set_null();
65 zval
66 }
67
68 /// Creates a zval containing an empty array.
69 #[must_use]
70 pub fn new_array() -> Zval {
71 let mut zval = Zval::new();
72 zval.set_hashtable(ZendHashTable::new());
73 zval
74 }
75
76 /// Dereference the zval, if it is a reference or indirect.
77 #[must_use]
78 #[inline]
79 pub fn dereference(&self) -> &Self {
80 if self.is_reference() {
81 // SAFETY: `is_reference()` guarantees `value.ref_` is valid.
82 unsafe { &(*self.value.ref_).val }
83 } else if self.is_indirect() {
84 // SAFETY: `is_indirect()` guarantees `value.zv` is a valid Zval pointer.
85 unsafe { &*(self.value.zv.cast::<Zval>()) }
86 } else {
87 self
88 }
89 }
90
91 /// Dereference the zval mutable, if it is a reference.
92 #[must_use]
93 #[inline]
94 pub fn dereference_mut(&mut self) -> &mut Self {
95 if self.is_reference() {
96 // SAFETY: `is_reference()` guarantees `value.ref_` is valid.
97 let reference = unsafe { self.value.ref_.as_mut().unwrap_unchecked() };
98 return &mut reference.val;
99 }
100 if self.is_indirect() {
101 // SAFETY: `is_indirect()` guarantees `value.zv` is a valid Zval pointer.
102 return unsafe { &mut *self.value.zv.cast::<Zval>() };
103 }
104 self
105 }
106
107 /// Returns the value of the zval if it is a long.
108 ///
109 /// References are dereferenced transparently.
110 #[must_use]
111 pub fn long(&self) -> Option<ZendLong> {
112 if self.get_type() == DataType::Long {
113 return Some(unsafe { self.value.lval });
114 }
115 let zval = self.dereference();
116 if zval.get_type() == DataType::Long {
117 Some(unsafe { zval.value.lval })
118 } else {
119 None
120 }
121 }
122
123 /// Returns the value of the zval if it is a bool.
124 ///
125 /// References are dereferenced transparently.
126 #[must_use]
127 pub fn bool(&self) -> Option<bool> {
128 match self.get_type() {
129 DataType::True => return Some(true),
130 DataType::False => return Some(false),
131 _ => {}
132 }
133 let zval = self.dereference();
134 match zval.get_type() {
135 DataType::True => Some(true),
136 DataType::False => Some(false),
137 _ => None,
138 }
139 }
140
141 /// Returns the value of the zval if it is a double.
142 ///
143 /// References are dereferenced transparently.
144 #[must_use]
145 pub fn double(&self) -> Option<f64> {
146 if self.get_type() == DataType::Double {
147 return Some(unsafe { self.value.dval });
148 }
149 let zval = self.dereference();
150 if zval.get_type() == DataType::Double {
151 Some(unsafe { zval.value.dval })
152 } else {
153 None
154 }
155 }
156
157 /// Returns the value of the zval as a zend string, if it is a string.
158 ///
159 /// References are dereferenced transparently.
160 ///
161 /// Note that this functions output will not be the same as
162 /// [`string()`](#method.string), as this function does not attempt to
163 /// convert other types into a [`String`].
164 #[must_use]
165 pub fn zend_str(&self) -> Option<&ZendStr> {
166 if self.get_type() == DataType::String {
167 return unsafe { self.value.str_.as_ref() };
168 }
169 let zval = self.dereference();
170 if zval.get_type() == DataType::String {
171 unsafe { zval.value.str_.as_ref() }
172 } else {
173 None
174 }
175 }
176
177 /// Returns the value of the zval if it is a string.
178 ///
179 /// [`str()`]: #method.str
180 pub fn string(&self) -> Option<String> {
181 self.str().map(ToString::to_string)
182 }
183
184 /// Returns the value of the zval if it is a string.
185 ///
186 /// Note that this functions output will not be the same as
187 /// [`string()`](#method.string), as this function does not attempt to
188 /// convert other types into a [`String`], as it could not pass back a
189 /// [`&str`] in those cases.
190 #[inline]
191 #[must_use]
192 pub fn str(&self) -> Option<&str> {
193 self.zend_str().and_then(|zs| zs.as_str().ok())
194 }
195
196 /// Returns the value of the zval if it is a string and can be unpacked into
197 /// a vector of a given type. Similar to the [`unpack`] function in PHP,
198 /// except you can only unpack one type.
199 ///
200 /// # Safety
201 ///
202 /// There is no way to tell if the data stored in the string is actually of
203 /// the given type. The results of this function can also differ from
204 /// platform-to-platform due to the different representation of some
205 /// types on different platforms. Consult the [`pack`] function
206 /// documentation for more details.
207 ///
208 /// [`pack`]: https://www.php.net/manual/en/function.pack.php
209 /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
210 pub fn binary<T: Pack>(&self) -> Option<Vec<T>> {
211 self.zend_str().map(T::unpack_into)
212 }
213
214 /// Returns the value of the zval if it is a string and can be unpacked into
215 /// a slice of a given type. Similar to the [`unpack`] function in PHP,
216 /// except you can only unpack one type.
217 ///
218 /// This function is similar to [`Zval::binary`] except that a slice is
219 /// returned instead of a vector, meaning the contents of the string is
220 /// not copied.
221 ///
222 /// # Safety
223 ///
224 /// There is no way to tell if the data stored in the string is actually of
225 /// the given type. The results of this function can also differ from
226 /// platform-to-platform due to the different representation of some
227 /// types on different platforms. Consult the [`pack`] function
228 /// documentation for more details.
229 ///
230 /// [`pack`]: https://www.php.net/manual/en/function.pack.php
231 /// [`unpack`]: https://www.php.net/manual/en/function.unpack.php
232 pub fn binary_slice<T: PackSlice>(&self) -> Option<&[T]> {
233 self.zend_str().map(T::unpack_into)
234 }
235
236 /// Returns the value of the zval if it is a resource.
237 ///
238 /// References are dereferenced transparently.
239 #[must_use]
240 pub fn resource(&self) -> Option<*mut zend_resource> {
241 if self.get_type() == DataType::Resource {
242 return Some(unsafe { self.value.res });
243 }
244 let zval = self.dereference();
245 if zval.get_type() == DataType::Resource {
246 Some(unsafe { zval.value.res })
247 } else {
248 None
249 }
250 }
251
252 /// Returns an immutable reference to the underlying zval hashtable if the
253 /// zval contains an array.
254 ///
255 /// References are dereferenced transparently.
256 #[must_use]
257 pub fn array(&self) -> Option<&ZendHashTable> {
258 if self.get_type() == DataType::Array {
259 return unsafe { self.value.arr.as_ref() };
260 }
261 let zval = self.dereference();
262 if zval.get_type() == DataType::Array {
263 unsafe { zval.value.arr.as_ref() }
264 } else {
265 None
266 }
267 }
268
269 /// Returns a mutable reference to the underlying zval hashtable if the zval
270 /// contains an array.
271 ///
272 /// # Array Separation
273 ///
274 /// PHP arrays use copy-on-write (COW) semantics. Before returning a mutable
275 /// reference, this method checks if the array is shared (refcount > 1) and
276 /// if so, creates a private copy. This is equivalent to PHP's
277 /// `SEPARATE_ARRAY()` macro and prevents the "Assertion failed:
278 /// `zend_gc_refcount` == 1" error that occurs when modifying shared arrays.
279 pub fn array_mut(&mut self) -> Option<&mut ZendHashTable> {
280 let zval = if self.get_type() == DataType::Array {
281 self
282 } else {
283 self.dereference_mut()
284 };
285 if zval.get_type() == DataType::Array {
286 unsafe {
287 let arr = zval.value.arr;
288 let ht = &*arr;
289 if ht.is_immutable() {
290 zval.value.arr = zend_array_dup(arr);
291 } else if (*arr).gc.refcount > 1 {
292 (*arr).gc.refcount -= 1;
293 zval.value.arr = zend_array_dup(arr);
294 }
295 zval.value.arr.as_mut()
296 }
297 } else {
298 None
299 }
300 }
301
302 /// Returns the value of the zval if it is an object.
303 ///
304 /// References are dereferenced transparently.
305 #[must_use]
306 pub fn object(&self) -> Option<&ZendObject> {
307 if matches!(self.get_type(), DataType::Object(_)) {
308 return unsafe { self.value.obj.as_ref() };
309 }
310 let zval = self.dereference();
311 if matches!(zval.get_type(), DataType::Object(_)) {
312 unsafe { zval.value.obj.as_ref() }
313 } else {
314 None
315 }
316 }
317
318 /// Returns a mutable reference to the object contained in the [`Zval`], if
319 /// any.
320 ///
321 /// References are dereferenced transparently.
322 pub fn object_mut(&mut self) -> Option<&mut ZendObject> {
323 if matches!(self.get_type(), DataType::Object(_)) {
324 return unsafe { self.value.obj.as_mut() };
325 }
326 let zval = self.dereference_mut();
327 if matches!(zval.get_type(), DataType::Object(_)) {
328 unsafe { zval.value.obj.as_mut() }
329 } else {
330 None
331 }
332 }
333
334 /// Attempts to call a method on the object contained in the zval.
335 ///
336 /// # Errors
337 ///
338 /// * Returns an error if the [`Zval`] is not an object.
339 // TODO: Measure this
340 #[allow(clippy::inline_always)]
341 #[inline(always)]
342 pub fn try_call_method(&self, name: &str, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
343 self.object()
344 .ok_or(Error::Object)?
345 .try_call_method(name, params)
346 }
347
348 /// Returns the value of the zval if it is an internal indirect reference.
349 #[must_use]
350 pub fn indirect(&self) -> Option<&Zval> {
351 if self.is_indirect() {
352 Some(unsafe { &*(self.value.zv.cast::<Zval>()) })
353 } else {
354 None
355 }
356 }
357
358 /// Returns a mutable reference to the zval if it is an internal indirect
359 /// reference.
360 // TODO: Verify if this is safe to use, as it allows mutating the
361 // hashtable while only having a reference to it. #461
362 #[allow(clippy::mut_from_ref)]
363 #[must_use]
364 pub fn indirect_mut(&self) -> Option<&mut Zval> {
365 if self.is_indirect() {
366 Some(unsafe { &mut *(self.value.zv.cast::<Zval>()) })
367 } else {
368 None
369 }
370 }
371
372 /// Returns the value of the zval if it is a reference.
373 #[inline]
374 #[must_use]
375 pub fn reference(&self) -> Option<&Zval> {
376 if self.is_reference() {
377 Some(&unsafe { self.value.ref_.as_ref() }?.val)
378 } else {
379 None
380 }
381 }
382
383 /// Returns a mutable reference to the underlying zval if it is a reference.
384 pub fn reference_mut(&mut self) -> Option<&mut Zval> {
385 if self.is_reference() {
386 Some(&mut unsafe { self.value.ref_.as_mut() }?.val)
387 } else {
388 None
389 }
390 }
391
392 /// Returns the value of the zval if it is callable.
393 #[must_use]
394 pub fn callable(&self) -> Option<ZendCallable<'_>> {
395 // The Zval is checked if it is callable in the `new` function.
396 ZendCallable::new(self).ok()
397 }
398
399 /// Returns an iterator over the zval if it is traversable.
400 #[must_use]
401 pub fn traversable(&self) -> Option<&mut ZendIterator> {
402 if self.is_traversable() {
403 self.object()?.get_class_entry().get_iterator(self, false)
404 } else {
405 None
406 }
407 }
408
409 /// Returns an iterable over the zval if it is an array or traversable. (is
410 /// iterable)
411 #[must_use]
412 pub fn iterable(&self) -> Option<Iterable<'_>> {
413 if self.is_iterable() {
414 Iterable::from_zval(self)
415 } else {
416 None
417 }
418 }
419
420 /// Returns the value of the zval if it is a pointer.
421 ///
422 /// # Safety
423 ///
424 /// The caller must ensure that the pointer contained in the zval is in fact
425 /// a pointer to an instance of `T`, as the zval has no way of defining
426 /// the type of pointer.
427 #[must_use]
428 pub unsafe fn ptr<T>(&self) -> Option<*mut T> {
429 if self.is_ptr() {
430 Some(unsafe { self.value.ptr.cast::<T>() })
431 } else {
432 None
433 }
434 }
435
436 /// Attempts to call the zval as a callable with a list of arguments to pass
437 /// to the function. Note that a thrown exception inside the callable is
438 /// not detectable, therefore you should check if the return value is
439 /// valid rather than unwrapping. Returns a result containing the return
440 /// value of the function, or an error.
441 ///
442 /// You should not call this function directly, rather through the
443 /// [`call_user_func`] macro.
444 ///
445 /// # Parameters
446 ///
447 /// * `params` - A list of parameters to call the function with.
448 ///
449 /// # Errors
450 ///
451 /// * Returns an error if the [`Zval`] is not callable.
452 // TODO: Measure this
453 #[allow(clippy::inline_always)]
454 #[inline(always)]
455 pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
456 self.callable().ok_or(Error::Callable)?.try_call(params)
457 }
458
459 /// Returns the type of the Zval.
460 #[must_use]
461 pub fn get_type(&self) -> DataType {
462 DataType::from(u32::from(unsafe { self.u1.v.type_ }))
463 }
464
465 /// Returns true if the zval is a long, false otherwise.
466 #[must_use]
467 pub fn is_long(&self) -> bool {
468 self.get_type() == DataType::Long
469 }
470
471 /// Returns true if the zval is null, false otherwise.
472 #[must_use]
473 pub fn is_null(&self) -> bool {
474 self.get_type() == DataType::Null
475 }
476
477 /// Returns true if the zval is true, false otherwise.
478 #[must_use]
479 pub fn is_true(&self) -> bool {
480 self.get_type() == DataType::True
481 }
482
483 /// Returns true if the zval is false, false otherwise.
484 #[must_use]
485 pub fn is_false(&self) -> bool {
486 self.get_type() == DataType::False
487 }
488
489 /// Returns true if the zval is a bool, false otherwise.
490 #[must_use]
491 pub fn is_bool(&self) -> bool {
492 self.is_true() || self.is_false()
493 }
494
495 /// Returns true if the zval is a double, false otherwise.
496 #[must_use]
497 pub fn is_double(&self) -> bool {
498 self.get_type() == DataType::Double
499 }
500
501 /// Returns true if the zval is a string, false otherwise.
502 #[must_use]
503 pub fn is_string(&self) -> bool {
504 self.get_type() == DataType::String
505 }
506
507 /// Returns true if the zval is a resource, false otherwise.
508 #[must_use]
509 pub fn is_resource(&self) -> bool {
510 self.get_type() == DataType::Resource
511 }
512
513 /// Returns true if the zval is an array, false otherwise.
514 #[must_use]
515 pub fn is_array(&self) -> bool {
516 self.get_type() == DataType::Array
517 }
518
519 /// Returns true if the zval is an object, false otherwise.
520 #[must_use]
521 pub fn is_object(&self) -> bool {
522 matches!(self.get_type(), DataType::Object(_))
523 }
524
525 /// Returns true if the zval is a reference, false otherwise.
526 #[inline]
527 #[must_use]
528 pub fn is_reference(&self) -> bool {
529 self.get_type() == DataType::Reference
530 }
531
532 /// Returns true if the zval is a reference, false otherwise.
533 #[must_use]
534 pub fn is_indirect(&self) -> bool {
535 self.get_type() == DataType::Indirect
536 }
537
538 /// Returns true if the zval is callable, false otherwise.
539 #[must_use]
540 pub fn is_callable(&self) -> bool {
541 let ptr: *const Self = self;
542 unsafe { zend_is_callable(ptr.cast_mut(), 0, std::ptr::null_mut()) }
543 }
544
545 /// Checks if the zval is identical to another one.
546 /// This works like `===` in php.
547 ///
548 /// # Parameters
549 ///
550 /// * `other` - The the zval to check identity against.
551 #[must_use]
552 pub fn is_identical(&self, other: &Self) -> bool {
553 let self_p: *const Self = self;
554 let other_p: *const Self = other;
555 unsafe { zend_is_identical(self_p.cast_mut(), other_p.cast_mut()) }
556 }
557
558 /// Returns true if the zval is traversable, false otherwise.
559 #[must_use]
560 pub fn is_traversable(&self) -> bool {
561 match self.object() {
562 None => false,
563 Some(obj) => obj.is_traversable(),
564 }
565 }
566
567 /// Returns true if the zval is iterable (array or traversable), false
568 /// otherwise.
569 #[must_use]
570 pub fn is_iterable(&self) -> bool {
571 let ptr: *const Self = self;
572 unsafe { zend_is_iterable(ptr.cast_mut()) }
573 }
574
575 /// Returns true if the zval contains a pointer, false otherwise.
576 #[must_use]
577 pub fn is_ptr(&self) -> bool {
578 self.get_type() == DataType::Ptr
579 }
580
581 /// Returns true if the zval is a scalar value (integer, float, string, or
582 /// bool), false otherwise.
583 ///
584 /// This is equivalent to PHP's `is_scalar()` function.
585 #[must_use]
586 pub fn is_scalar(&self) -> bool {
587 matches!(
588 self.get_type(),
589 DataType::Long | DataType::Double | DataType::String | DataType::True | DataType::False
590 )
591 }
592
593 // =========================================================================
594 // Type Coercion Methods
595 // =========================================================================
596 //
597 // These methods convert the zval's value to a different type following
598 // PHP's type coercion rules. Unlike the mutating `coerce_into_*` methods
599 // in some implementations, these are pure functions that return a new value.
600
601 /// Coerces the value to a boolean following PHP's type coercion rules.
602 ///
603 /// This uses PHP's internal `zend_is_true` function to determine the
604 /// boolean value, which handles all PHP types correctly:
605 /// - `null` → `false`
606 /// - `false` → `false`, `true` → `true`
607 /// - `0`, `0.0`, `""`, `"0"` → `false`
608 /// - Empty arrays → `false`
609 /// - Everything else → `true`
610 ///
611 /// # Example
612 ///
613 /// ```no_run
614 /// use ext_php_rs::types::Zval;
615 ///
616 /// let mut zv = Zval::new();
617 /// zv.set_long(0);
618 /// assert_eq!(zv.coerce_to_bool(), false);
619 ///
620 /// zv.set_long(42);
621 /// assert_eq!(zv.coerce_to_bool(), true);
622 /// ```
623 #[must_use]
624 pub fn coerce_to_bool(&self) -> bool {
625 cfg_if! {
626 if #[cfg(php84)] {
627 let ptr: *const Self = self;
628 unsafe { zend_is_true(ptr) }
629 } else {
630 // Pre-PHP 8.4: zend_is_true takes *mut and returns c_int
631 let ptr = self as *const Self as *mut Self;
632 unsafe { zend_is_true(ptr) != 0 }
633 }
634 }
635 }
636
637 /// Coerces the value to a string following PHP's type coercion rules.
638 ///
639 /// This is a Rust implementation that matches PHP's behavior. Returns `None`
640 /// for types that cannot be meaningfully converted to strings (arrays,
641 /// resources, objects without `__toString`).
642 ///
643 /// Conversion rules:
644 /// - Strings → returned as-is
645 /// - Integers → decimal string representation
646 /// - Floats → string representation (uses uppercase `E` for scientific
647 /// notation when exponent >= 14 or <= -5, matching PHP)
648 /// - `true` → `"1"`, `false` → `""`
649 /// - `null` → `""`
650 /// - `INF` → `"INF"`, `-INF` → `"-INF"`, `NAN` → `"NAN"` (uppercase)
651 /// - Objects with `__toString()` → result of calling `__toString()`
652 /// - Arrays, resources, objects without `__toString()` → `None`
653 ///
654 /// # Example
655 ///
656 /// ```no_run
657 /// use ext_php_rs::types::Zval;
658 ///
659 /// let mut zv = Zval::new();
660 /// zv.set_long(42);
661 /// assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
662 ///
663 /// zv.set_bool(true);
664 /// assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
665 /// ```
666 #[must_use]
667 pub fn coerce_to_string(&self) -> Option<String> {
668 // Already a string
669 if let Some(s) = self.str() {
670 return Some(s.to_string());
671 }
672
673 // Boolean
674 if let Some(b) = self.bool() {
675 return Some(if b { "1".to_string() } else { String::new() });
676 }
677
678 // Null
679 if self.is_null() {
680 return Some(String::new());
681 }
682
683 // Integer
684 if let Some(l) = self.long() {
685 return Some(l.to_string());
686 }
687
688 // Float
689 if let Some(d) = self.double() {
690 // PHP uses uppercase for special float values
691 if d.is_nan() {
692 return Some("NAN".to_string());
693 }
694 if d.is_infinite() {
695 return Some(if d.is_sign_positive() {
696 "INF".to_string()
697 } else {
698 "-INF".to_string()
699 });
700 }
701 // PHP uses a format similar to printf's %G with ~14 digits precision
702 // and uppercase E for scientific notation
703 return Some(php_float_to_string(d));
704 }
705
706 // Object with __toString
707 if let Some(obj) = self.object()
708 && let Ok(result) = obj.try_call_method("__toString", vec![])
709 {
710 return result.str().map(ToString::to_string);
711 }
712
713 // Arrays, resources, and objects without __toString cannot be converted
714 None
715 }
716
717 /// Coerces the value to an integer following PHP's type coercion rules.
718 ///
719 /// This is a Rust implementation that matches PHP's behavior. Returns `None`
720 /// for types that cannot be meaningfully converted to integers (arrays,
721 /// resources, objects).
722 ///
723 /// Conversion rules:
724 /// - Integers → returned as-is
725 /// - Floats → truncated toward zero
726 /// - `true` → `1`, `false` → `0`
727 /// - `null` → `0`
728 /// - Strings → parsed as integer (leading numeric portion only; stops at
729 /// first non-digit; returns 0 if non-numeric; saturates on overflow)
730 /// - Arrays, resources, objects → `None`
731 ///
732 /// # Example
733 ///
734 /// ```no_run
735 /// use ext_php_rs::types::Zval;
736 ///
737 /// let mut zv = Zval::new();
738 /// zv.set_string("42abc", false);
739 /// assert_eq!(zv.coerce_to_long(), Some(42));
740 ///
741 /// zv.set_double(3.7);
742 /// assert_eq!(zv.coerce_to_long(), Some(3));
743 /// ```
744 #[must_use]
745 pub fn coerce_to_long(&self) -> Option<ZendLong> {
746 // Already an integer
747 if let Some(l) = self.long() {
748 return Some(l);
749 }
750
751 // Boolean
752 if let Some(b) = self.bool() {
753 return Some(ZendLong::from(b));
754 }
755
756 // Null
757 if self.is_null() {
758 return Some(0);
759 }
760
761 // Float - truncate toward zero
762 if let Some(d) = self.double() {
763 #[allow(clippy::cast_possible_truncation)]
764 return Some(d as ZendLong);
765 }
766
767 // String - parse leading numeric portion
768 if let Some(s) = self.str() {
769 return Some(parse_long_from_str(s));
770 }
771
772 // Arrays, resources, objects cannot be converted
773 None
774 }
775
776 /// Coerces the value to a float following PHP's type coercion rules.
777 ///
778 /// This is a Rust implementation that matches PHP's behavior. Returns `None`
779 /// for types that cannot be meaningfully converted to floats (arrays,
780 /// resources, objects).
781 ///
782 /// Conversion rules:
783 /// - Floats → returned as-is
784 /// - Integers → converted to float
785 /// - `true` → `1.0`, `false` → `0.0`
786 /// - `null` → `0.0`
787 /// - Strings → parsed as float (leading numeric portion including decimal
788 /// point and scientific notation; returns 0.0 if non-numeric)
789 /// - Arrays, resources, objects → `None`
790 ///
791 /// # Example
792 ///
793 /// ```no_run
794 /// use ext_php_rs::types::Zval;
795 ///
796 /// let mut zv = Zval::new();
797 /// zv.set_string("3.14abc", false);
798 /// assert_eq!(zv.coerce_to_double(), Some(3.14));
799 ///
800 /// zv.set_long(42);
801 /// assert_eq!(zv.coerce_to_double(), Some(42.0));
802 /// ```
803 #[must_use]
804 pub fn coerce_to_double(&self) -> Option<f64> {
805 // Already a float
806 if let Some(d) = self.double() {
807 return Some(d);
808 }
809
810 // Integer
811 if let Some(l) = self.long() {
812 #[allow(clippy::cast_precision_loss)]
813 return Some(l as f64);
814 }
815
816 // Boolean
817 if let Some(b) = self.bool() {
818 return Some(if b { 1.0 } else { 0.0 });
819 }
820
821 // Null
822 if self.is_null() {
823 return Some(0.0);
824 }
825
826 // String - parse leading numeric portion
827 if let Some(s) = self.str() {
828 return Some(parse_double_from_str(s));
829 }
830
831 // Arrays, resources, objects cannot be converted
832 None
833 }
834
835 /// Sets the value of the zval as a string. Returns nothing in a result when
836 /// successful.
837 ///
838 /// # Parameters
839 ///
840 /// * `val` - The value to set the zval as.
841 /// * `persistent` - Whether the string should persist between requests.
842 ///
843 /// # Persistent Strings
844 ///
845 /// When `persistent` is `true`, the string is allocated from PHP's
846 /// persistent heap (using `malloc`) rather than the request-bound heap.
847 /// This is typically used for strings that need to survive across multiple
848 /// PHP requests, such as class names, function names, or module-level data.
849 ///
850 /// **Important:** The string will still be freed when the Zval is dropped.
851 /// The `persistent` flag only affects which memory allocator is used. If
852 /// you need a string to outlive the Zval, consider using
853 /// [`std::mem::forget`] on the Zval or storing the string elsewhere.
854 ///
855 /// For most use cases (return values, function arguments, temporary
856 /// storage), you should use `persistent: false`.
857 ///
858 /// # Errors
859 ///
860 /// Never returns an error.
861 // TODO: Check if we can drop the result here.
862 pub fn set_string(&mut self, val: &str, persistent: bool) -> Result<()> {
863 self.set_zend_string(ZendStr::new(val, persistent));
864 Ok(())
865 }
866
867 /// Sets the value of the zval as a Zend string.
868 ///
869 /// The Zval takes ownership of the string. When the Zval is dropped,
870 /// the string will be released.
871 ///
872 /// # Parameters
873 ///
874 /// * `val` - String content.
875 pub fn set_zend_string(&mut self, val: ZBox<ZendStr>) {
876 let is_interned = unsafe { val.gc.u.type_info } & GC_IMMUTABLE != 0;
877 let flags = if is_interned {
878 ZvalTypeFlags::InternedStringEx
879 } else {
880 ZvalTypeFlags::StringEx
881 };
882 self.change_type(flags);
883 self.value.str_ = val.into_raw();
884 }
885
886 /// Sets the value of the zval as a binary string, which is represented in
887 /// Rust as a vector.
888 ///
889 /// # Parameters
890 ///
891 /// * `val` - The value to set the zval as.
892 pub fn set_binary<T: Pack>(&mut self, val: Vec<T>) {
893 self.change_type(ZvalTypeFlags::StringEx);
894 let ptr = T::pack_into(val);
895 self.value.str_ = ptr;
896 }
897
898 /// Sets the value of the zval as an interned string. Returns nothing in a
899 /// result when successful.
900 ///
901 /// Interned strings are stored once and are immutable. PHP stores them in
902 /// an internal hashtable. Unlike regular strings, interned strings are not
903 /// reference counted and should not be freed by `zval_ptr_dtor`.
904 ///
905 /// # Parameters
906 ///
907 /// * `val` - The value to set the zval as.
908 /// * `persistent` - Whether the string should persist between requests.
909 ///
910 /// # Errors
911 ///
912 /// Never returns an error.
913 // TODO: Check if we can drop the result here.
914 pub fn set_interned_string(&mut self, val: &str, persistent: bool) -> Result<()> {
915 // Use InternedStringEx (without RefCounted) because interned strings
916 // should not have their refcount modified by zval_ptr_dtor.
917 self.change_type(ZvalTypeFlags::InternedStringEx);
918 self.value.str_ = ZendStr::new_interned(val, persistent).into_raw();
919 Ok(())
920 }
921
922 /// Sets the value of the zval as a long.
923 ///
924 /// # Parameters
925 ///
926 /// * `val` - The value to set the zval as.
927 pub fn set_long<T: Into<ZendLong>>(&mut self, val: T) {
928 self.internal_set_long(val.into());
929 }
930
931 fn internal_set_long(&mut self, val: ZendLong) {
932 self.change_type(ZvalTypeFlags::Long);
933 self.value.lval = val;
934 }
935
936 /// Sets the value of the zval as a double.
937 ///
938 /// # Parameters
939 ///
940 /// * `val` - The value to set the zval as.
941 pub fn set_double<T: Into<f64>>(&mut self, val: T) {
942 self.internal_set_double(val.into());
943 }
944
945 fn internal_set_double(&mut self, val: f64) {
946 self.change_type(ZvalTypeFlags::Double);
947 self.value.dval = val;
948 }
949
950 /// Sets the value of the zval as a boolean.
951 ///
952 /// # Parameters
953 ///
954 /// * `val` - The value to set the zval as.
955 pub fn set_bool<T: Into<bool>>(&mut self, val: T) {
956 self.internal_set_bool(val.into());
957 }
958
959 fn internal_set_bool(&mut self, val: bool) {
960 self.change_type(if val {
961 ZvalTypeFlags::True
962 } else {
963 ZvalTypeFlags::False
964 });
965 }
966
967 /// Sets the value of the zval as null.
968 ///
969 /// This is the default of a zval.
970 pub fn set_null(&mut self) {
971 self.change_type(ZvalTypeFlags::Null);
972 }
973
974 /// Sets the value of the zval as a resource.
975 ///
976 /// # Parameters
977 ///
978 /// * `val` - The value to set the zval as.
979 pub fn set_resource(&mut self, val: *mut zend_resource) {
980 self.change_type(ZvalTypeFlags::ResourceEx);
981 self.value.res = val;
982 }
983
984 /// Sets the value of the zval as a reference to an object.
985 ///
986 /// # Parameters
987 ///
988 /// * `val` - The value to set the zval as.
989 pub fn set_object(&mut self, val: &mut ZendObject) {
990 self.change_type(ZvalTypeFlags::ObjectEx);
991 val.inc_count(); // TODO(david): not sure if this is needed :/
992 self.value.obj = ptr::from_ref(val).cast_mut();
993 }
994
995 /// Sets the value of the zval as an array. Returns nothing in a result on
996 /// success.
997 ///
998 /// # Parameters
999 ///
1000 /// * `val` - The value to set the zval as.
1001 ///
1002 /// # Errors
1003 ///
1004 /// * Returns an error if the conversion to a hashtable fails.
1005 pub fn set_array<T: TryInto<ZBox<ZendHashTable>, Error = Error>>(
1006 &mut self,
1007 val: T,
1008 ) -> Result<()> {
1009 self.set_hashtable(val.try_into()?);
1010 Ok(())
1011 }
1012
1013 /// Sets the value of the zval as an array. Returns nothing in a result on
1014 /// success.
1015 ///
1016 /// # Parameters
1017 ///
1018 /// * `val` - The value to set the zval as.
1019 pub fn set_hashtable(&mut self, val: ZBox<ZendHashTable>) {
1020 // Handle immutable shared arrays (e.g., the empty array) similar to
1021 // ZVAL_EMPTY_ARRAY. Immutable arrays should not be reference counted.
1022 let type_info = if val.is_immutable() {
1023 ZvalTypeFlags::Array
1024 } else {
1025 ZvalTypeFlags::ArrayEx
1026 };
1027 self.change_type(type_info);
1028 self.value.arr = val.into_raw();
1029 }
1030
1031 /// Sets the value of the zval as a pointer.
1032 ///
1033 /// # Parameters
1034 ///
1035 /// * `ptr` - The pointer to set the zval as.
1036 pub fn set_ptr<T>(&mut self, ptr: *mut T) {
1037 self.u1.type_info = ZvalTypeFlags::Ptr.bits();
1038 self.value.ptr = ptr.cast::<c_void>();
1039 }
1040
1041 /// Used to drop the Zval but keep the value of the zval intact.
1042 ///
1043 /// This is important when copying the value of the zval, as the actual
1044 /// value will not be copied, but the pointer to the value (string for
1045 /// example) will be copied.
1046 pub(crate) fn release(mut self) {
1047 // NOTE(david): don't use `change_type` here as we are wanting to keep the
1048 // contents intact.
1049 self.u1.type_info = ZvalTypeFlags::Null.bits();
1050 }
1051
1052 /// Changes the type of the zval, freeing the current contents when
1053 /// applicable.
1054 ///
1055 /// # Parameters
1056 ///
1057 /// * `ty` - The new type of the zval.
1058 fn change_type(&mut self, ty: ZvalTypeFlags) {
1059 // SAFETY: we have exclusive mutable access to this zval so can free the
1060 // contents.
1061 //
1062 // For strings, we use zend_string_release directly instead of zval_ptr_dtor
1063 // to correctly handle persistent strings. zend_string_release properly checks
1064 // the IS_STR_PERSISTENT flag and uses the correct deallocator (free vs efree).
1065 // This fixes heap corruption issues when dropping Zvals containing persistent
1066 // strings (see issue #424).
1067 //
1068 // For simple types (Long, Double, Bool, Null, Undef, True, False), we don't
1069 // need to call zval_ptr_dtor because they don't have heap-allocated data.
1070 // This is important for cargo-php stub generation where zval_ptr_dtor is
1071 // stubbed as a null pointer - calling it would crash.
1072 if self.is_string() {
1073 unsafe {
1074 if let Some(str_ptr) = self.value.str_.as_mut() {
1075 ext_php_rs_zend_string_release(str_ptr);
1076 }
1077 }
1078 } else if self.is_array()
1079 || self.is_object()
1080 || self.is_resource()
1081 || self.is_reference()
1082 || self.get_type() == DataType::ConstantExpression
1083 {
1084 // Only call zval_ptr_dtor for reference-counted types
1085 unsafe { zval_ptr_dtor(self) };
1086 }
1087 // Simple types (Long, Double, Bool, Null, etc.) need no cleanup
1088 self.u1.type_info = ty.bits();
1089 }
1090
1091 /// Extracts some type from a `Zval`.
1092 ///
1093 /// This is a wrapper function around `TryFrom`.
1094 #[must_use]
1095 pub fn extract<'a, T>(&'a self) -> Option<T>
1096 where
1097 T: FromZval<'a>,
1098 {
1099 FromZval::from_zval(self)
1100 }
1101
1102 /// Creates a shallow clone of the [`Zval`].
1103 ///
1104 /// This copies the contents of the [`Zval`], and increments the reference
1105 /// counter of the underlying value (if it is reference counted).
1106 ///
1107 /// For example, if the zval contains a long, it will simply copy the value.
1108 /// However, if the zval contains an object, the new zval will point to the
1109 /// same object, and the objects reference counter will be incremented.
1110 ///
1111 /// # Returns
1112 ///
1113 /// The cloned zval.
1114 #[must_use]
1115 pub fn shallow_clone(&self) -> Zval {
1116 let mut new = Zval::new();
1117 new.u1 = self.u1;
1118 new.value = self.value;
1119
1120 // SAFETY: `u1` union is only used for easier bitmasking. It is valid to read
1121 // from either of the variants.
1122 //
1123 // SAFETY: If the value if refcounted (`self.u1.type_info & Z_TYPE_FLAGS_MASK`)
1124 // then it is valid to dereference `self.value.counted`.
1125 unsafe {
1126 let flags = ZvalTypeFlags::from_bits_retain(self.u1.type_info);
1127 if flags.contains(ZvalTypeFlags::RefCounted) {
1128 (*self.value.counted).gc.refcount += 1;
1129 }
1130 }
1131
1132 new
1133 }
1134}
1135
1136impl Debug for Zval {
1137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1138 let mut dbg = f.debug_struct("Zval");
1139 let ty = self.get_type();
1140 dbg.field("type", &ty);
1141
1142 macro_rules! field {
1143 ($value: expr) => {
1144 dbg.field("val", &$value)
1145 };
1146 }
1147
1148 match ty {
1149 DataType::Undef | DataType::Null | DataType::ConstantExpression | DataType::Void => {
1150 field!(Option::<()>::None)
1151 }
1152 DataType::False => field!(false),
1153 DataType::True => field!(true),
1154 DataType::Long => field!(self.long()),
1155 DataType::Double => field!(self.double()),
1156 DataType::String | DataType::Mixed | DataType::Callable => field!(self.string()),
1157 DataType::Array => field!(self.array()),
1158 DataType::Object(_) => field!(self.object()),
1159 DataType::Resource => field!(self.resource()),
1160 DataType::Reference => field!(self.reference()),
1161 DataType::Bool => field!(self.bool()),
1162 DataType::Indirect => field!(self.indirect()),
1163 DataType::Iterable => field!(self.iterable()),
1164 // SAFETY: We are not accessing the pointer.
1165 DataType::Ptr => field!(unsafe { self.ptr::<c_void>() }),
1166 };
1167
1168 dbg.finish()
1169 }
1170}
1171
1172impl Drop for Zval {
1173 fn drop(&mut self) {
1174 self.change_type(ZvalTypeFlags::Null);
1175 }
1176}
1177
1178impl Default for Zval {
1179 fn default() -> Self {
1180 Self::new()
1181 }
1182}
1183
1184impl IntoZval for Zval {
1185 const TYPE: DataType = DataType::Mixed;
1186 const NULLABLE: bool = true;
1187
1188 fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1189 *zv = self;
1190 Ok(())
1191 }
1192}
1193
1194impl<'a> FromZval<'a> for &'a Zval {
1195 const TYPE: DataType = DataType::Mixed;
1196
1197 fn from_zval(zval: &'a Zval) -> Option<Self> {
1198 Some(zval)
1199 }
1200}
1201
1202impl<'a> FromZvalMut<'a> for &'a mut Zval {
1203 const TYPE: DataType = DataType::Mixed;
1204
1205 fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
1206 Some(zval)
1207 }
1208}
1209
1210/// Formats a float to a string following PHP's type coercion rules.
1211///
1212/// This is a Rust implementation that matches PHP's behavior. PHP uses a format
1213/// similar to printf's `%G` with 14 significant digits:
1214/// - Uses scientific notation when exponent >= 14 or <= -5
1215/// - Uses uppercase 'E' for the exponent (e.g., "1.0E+52")
1216/// - Removes trailing zeros after the decimal point
1217fn php_float_to_string(d: f64) -> String {
1218 // Use Rust's %G-like formatting: format with precision, uppercase E
1219 // PHP uses ~14 significant digits
1220 let formatted = format!("{d:.14E}");
1221
1222 // Parse the formatted string to clean it up
1223 if let Some(e_pos) = formatted.find('E') {
1224 let mantissa_part = &formatted[..e_pos];
1225 let exp_part = &formatted[e_pos..];
1226
1227 // Parse exponent to decide format
1228 let exp: i32 = exp_part[1..].parse().unwrap_or(0);
1229
1230 // PHP uses scientific notation when exponent >= 14 or <= -5
1231 if exp >= 14 || exp <= -5 {
1232 // Keep scientific notation, but clean up mantissa
1233 let mantissa = mantissa_part.trim_end_matches('0').trim_end_matches('.');
1234
1235 // Ensure mantissa has at least one decimal digit if there's a decimal point
1236 let mantissa = if mantissa.contains('.') && mantissa.ends_with('.') {
1237 format!("{mantissa}0")
1238 } else if !mantissa.contains('.') {
1239 format!("{mantissa}.0")
1240 } else {
1241 mantissa.to_string()
1242 };
1243
1244 // Format exponent with sign
1245 if exp >= 0 {
1246 format!("{mantissa}E+{exp}")
1247 } else {
1248 format!("{mantissa}E{exp}")
1249 }
1250 } else {
1251 // Use decimal notation
1252 let s = d.to_string();
1253 // Clean up trailing zeros
1254 if s.contains('.') {
1255 s.trim_end_matches('0').trim_end_matches('.').to_string()
1256 } else {
1257 s
1258 }
1259 }
1260 } else {
1261 formatted
1262 }
1263}
1264
1265/// Parses an integer from a string following PHP's type coercion rules.
1266///
1267/// This is a Rust implementation that matches PHP's behavior. It extracts the
1268/// leading numeric portion of a string:
1269/// - `"42"` → 42
1270/// - `"42abc"` → 42
1271/// - `" 42"` → 42 (leading whitespace is skipped)
1272/// - `"-42"` → -42
1273/// - `"abc"` → 0
1274/// - `""` → 0
1275/// - Hexadecimal (`"0xFF"`) → 0 (not interpreted, stops at 'x')
1276/// - Octal (`"010"`) → 10 (not interpreted as octal)
1277/// - Overflow saturates to `ZendLong::MAX` or `ZendLong::MIN`
1278fn parse_long_from_str(s: &str) -> ZendLong {
1279 let s = s.trim_start();
1280 let bytes = s.as_bytes();
1281 if bytes.is_empty() {
1282 return 0;
1283 }
1284
1285 let mut i = 0;
1286 let mut is_negative = false;
1287
1288 // Handle optional sign
1289 if bytes[i] == b'-' || bytes[i] == b'+' {
1290 is_negative = bytes[i] == b'-';
1291 i += 1;
1292 }
1293
1294 let digits_start = i;
1295
1296 // Collect digits
1297 while i < bytes.len() && bytes[i].is_ascii_digit() {
1298 i += 1;
1299 }
1300
1301 // No digits found
1302 if i == digits_start {
1303 return 0;
1304 }
1305
1306 // Parse the slice, saturating on overflow (matching PHP behavior)
1307 match s[..i].parse::<ZendLong>() {
1308 Ok(n) => n,
1309 Err(_) => {
1310 // Overflow: saturate to max or min depending on sign
1311 if is_negative {
1312 ZendLong::MIN
1313 } else {
1314 ZendLong::MAX
1315 }
1316 }
1317 }
1318}
1319
1320/// Parses a float from a string following PHP's type coercion rules.
1321///
1322/// This is a Rust implementation that matches PHP's behavior. It extracts the
1323/// leading numeric portion of a string:
1324/// - `"3.14"` → 3.14
1325/// - `"3.14abc"` → 3.14
1326/// - `" 3.14"` → 3.14 (leading whitespace is skipped)
1327/// - `"-3.14"` → -3.14
1328/// - `"1e10"` → 1e10 (scientific notation supported)
1329/// - `".5"` → 0.5 (leading decimal point)
1330/// - `"5."` → 5.0 (trailing decimal point)
1331/// - `"abc"` → 0.0
1332/// - `""` → 0.0
1333fn parse_double_from_str(s: &str) -> f64 {
1334 let s = s.trim_start();
1335 let bytes = s.as_bytes();
1336 if bytes.is_empty() {
1337 return 0.0;
1338 }
1339
1340 let mut i = 0;
1341 let mut has_decimal = false;
1342 let mut has_exponent = false;
1343 let mut has_digits = false;
1344
1345 // Handle optional sign
1346 if bytes[i] == b'-' || bytes[i] == b'+' {
1347 i += 1;
1348 }
1349
1350 // Collect digits, decimal point, and exponent
1351 while i < bytes.len() {
1352 let c = bytes[i];
1353 if c.is_ascii_digit() {
1354 has_digits = true;
1355 i += 1;
1356 } else if c == b'.' && !has_decimal && !has_exponent {
1357 has_decimal = true;
1358 i += 1;
1359 } else if (c == b'e' || c == b'E') && !has_exponent && has_digits {
1360 has_exponent = true;
1361 i += 1;
1362 // Handle optional sign after exponent
1363 if i < bytes.len() && (bytes[i] == b'-' || bytes[i] == b'+') {
1364 i += 1;
1365 }
1366 } else {
1367 break;
1368 }
1369 }
1370
1371 // Parse the slice or return 0.0
1372 s[..i].parse().unwrap_or(0.0)
1373}
1374
1375#[cfg(test)]
1376#[cfg(feature = "embed")]
1377#[allow(clippy::unwrap_used, clippy::approx_constant)]
1378mod tests {
1379 use super::*;
1380 use crate::embed::Embed;
1381
1382 #[test]
1383 fn test_zval_null() {
1384 Embed::run(|| {
1385 let zval = Zval::null();
1386 assert!(zval.is_null());
1387 });
1388 }
1389
1390 #[test]
1391 fn test_is_scalar() {
1392 Embed::run(|| {
1393 // Test scalar types - should return true
1394 let mut zval_long = Zval::new();
1395 zval_long.set_long(42);
1396 assert!(zval_long.is_scalar());
1397
1398 let mut zval_double = Zval::new();
1399 zval_double.set_double(1.5);
1400 assert!(zval_double.is_scalar());
1401
1402 let mut zval_true = Zval::new();
1403 zval_true.set_bool(true);
1404 assert!(zval_true.is_scalar());
1405
1406 let mut zval_false = Zval::new();
1407 zval_false.set_bool(false);
1408 assert!(zval_false.is_scalar());
1409
1410 let mut zval_string = Zval::new();
1411 zval_string
1412 .set_string("hello", false)
1413 .expect("set_string should succeed");
1414 assert!(zval_string.is_scalar());
1415
1416 // Test non-scalar types - should return false
1417 let zval_null = Zval::null();
1418 assert!(!zval_null.is_scalar());
1419
1420 let zval_array = Zval::new_array();
1421 assert!(!zval_array.is_scalar());
1422 });
1423 }
1424
1425 #[test]
1426 fn test_coerce_to_bool() {
1427 Embed::run(|| {
1428 let mut zv = Zval::new();
1429
1430 // === Truthy values ===
1431 // Integers
1432 zv.set_long(42);
1433 assert!(zv.coerce_to_bool(), "(bool)42 should be true");
1434
1435 zv.set_long(1);
1436 assert!(zv.coerce_to_bool(), "(bool)1 should be true");
1437
1438 zv.set_long(-1);
1439 assert!(zv.coerce_to_bool(), "(bool)-1 should be true");
1440
1441 // Floats
1442 zv.set_double(0.1);
1443 assert!(zv.coerce_to_bool(), "(bool)0.1 should be true");
1444
1445 zv.set_double(-0.1);
1446 assert!(zv.coerce_to_bool(), "(bool)-0.1 should be true");
1447
1448 // Strings - non-empty and not "0"
1449 zv.set_string("hello", false).unwrap();
1450 assert!(zv.coerce_to_bool(), "(bool)'hello' should be true");
1451
1452 zv.set_string("1", false).unwrap();
1453 assert!(zv.coerce_to_bool(), "(bool)'1' should be true");
1454
1455 // PHP: (bool)"false" is true (non-empty string)
1456 zv.set_string("false", false).unwrap();
1457 assert!(zv.coerce_to_bool(), "(bool)'false' should be true");
1458
1459 // PHP: (bool)"true" is true
1460 zv.set_string("true", false).unwrap();
1461 assert!(zv.coerce_to_bool(), "(bool)'true' should be true");
1462
1463 // PHP: (bool)" " (space) is true
1464 zv.set_string(" ", false).unwrap();
1465 assert!(zv.coerce_to_bool(), "(bool)' ' should be true");
1466
1467 // PHP: (bool)"00" is true (only exactly "0" is false)
1468 zv.set_string("00", false).unwrap();
1469 assert!(zv.coerce_to_bool(), "(bool)'00' should be true");
1470
1471 zv.set_bool(true);
1472 assert!(zv.coerce_to_bool(), "(bool)true should be true");
1473
1474 // === Falsy values ===
1475 // Integer zero
1476 zv.set_long(0);
1477 assert!(!zv.coerce_to_bool(), "(bool)0 should be false");
1478
1479 // Float zero (both positive and negative)
1480 zv.set_double(0.0);
1481 assert!(!zv.coerce_to_bool(), "(bool)0.0 should be false");
1482
1483 zv.set_double(-0.0);
1484 assert!(!zv.coerce_to_bool(), "(bool)-0.0 should be false");
1485
1486 // Empty string
1487 zv.set_string("", false).unwrap();
1488 assert!(!zv.coerce_to_bool(), "(bool)'' should be false");
1489
1490 // String "0" - the only non-empty falsy string
1491 zv.set_string("0", false).unwrap();
1492 assert!(!zv.coerce_to_bool(), "(bool)'0' should be false");
1493
1494 zv.set_bool(false);
1495 assert!(!zv.coerce_to_bool(), "(bool)false should be false");
1496
1497 // Null
1498 let null_zv = Zval::null();
1499 assert!(!null_zv.coerce_to_bool(), "(bool)null should be false");
1500
1501 // Empty array
1502 let empty_array = Zval::new_array();
1503 assert!(!empty_array.coerce_to_bool(), "(bool)[] should be false");
1504 });
1505 }
1506
1507 #[test]
1508 fn test_coerce_to_string() {
1509 Embed::run(|| {
1510 let mut zv = Zval::new();
1511
1512 // === Integer to string ===
1513 zv.set_long(42);
1514 assert_eq!(zv.coerce_to_string(), Some("42".to_string()));
1515
1516 zv.set_long(-123);
1517 assert_eq!(zv.coerce_to_string(), Some("-123".to_string()));
1518
1519 zv.set_long(0);
1520 assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1521
1522 // === Float to string ===
1523 zv.set_double(3.14);
1524 assert_eq!(zv.coerce_to_string(), Some("3.14".to_string()));
1525
1526 zv.set_double(-3.14);
1527 assert_eq!(zv.coerce_to_string(), Some("-3.14".to_string()));
1528
1529 zv.set_double(0.0);
1530 assert_eq!(zv.coerce_to_string(), Some("0".to_string()));
1531
1532 zv.set_double(1.0);
1533 assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1534
1535 zv.set_double(1.5);
1536 assert_eq!(zv.coerce_to_string(), Some("1.5".to_string()));
1537
1538 zv.set_double(100.0);
1539 assert_eq!(zv.coerce_to_string(), Some("100".to_string()));
1540
1541 // Special float values (uppercase like PHP)
1542 zv.set_double(f64::INFINITY);
1543 assert_eq!(zv.coerce_to_string(), Some("INF".to_string()));
1544
1545 zv.set_double(f64::NEG_INFINITY);
1546 assert_eq!(zv.coerce_to_string(), Some("-INF".to_string()));
1547
1548 zv.set_double(f64::NAN);
1549 assert_eq!(zv.coerce_to_string(), Some("NAN".to_string()));
1550
1551 // Scientific notation thresholds (PHP uses E notation at exp >= 14 or <= -5)
1552 // PHP: (string)1e13 -> "10000000000000"
1553 zv.set_double(1e13);
1554 assert_eq!(zv.coerce_to_string(), Some("10000000000000".to_string()));
1555
1556 // PHP: (string)1e14 -> "1.0E+14"
1557 zv.set_double(1e14);
1558 assert_eq!(zv.coerce_to_string(), Some("1.0E+14".to_string()));
1559
1560 // PHP: (string)1e52 -> "1.0E+52"
1561 zv.set_double(1e52);
1562 assert_eq!(zv.coerce_to_string(), Some("1.0E+52".to_string()));
1563
1564 // PHP: (string)0.0001 -> "0.0001"
1565 zv.set_double(0.0001);
1566 assert_eq!(zv.coerce_to_string(), Some("0.0001".to_string()));
1567
1568 // PHP: (string)1e-5 -> "1.0E-5"
1569 zv.set_double(1e-5);
1570 assert_eq!(zv.coerce_to_string(), Some("1.0E-5".to_string()));
1571
1572 // Negative scientific notation
1573 // PHP: (string)(-1e52) -> "-1.0E+52"
1574 zv.set_double(-1e52);
1575 assert_eq!(zv.coerce_to_string(), Some("-1.0E+52".to_string()));
1576
1577 // PHP: (string)(-1e-10) -> "-1.0E-10"
1578 zv.set_double(-1e-10);
1579 assert_eq!(zv.coerce_to_string(), Some("-1.0E-10".to_string()));
1580
1581 // === Boolean to string ===
1582 // PHP: (string)true -> "1"
1583 zv.set_bool(true);
1584 assert_eq!(zv.coerce_to_string(), Some("1".to_string()));
1585
1586 // PHP: (string)false -> ""
1587 zv.set_bool(false);
1588 assert_eq!(zv.coerce_to_string(), Some(String::new()));
1589
1590 // === Null to string ===
1591 // PHP: (string)null -> ""
1592 let null_zv = Zval::null();
1593 assert_eq!(null_zv.coerce_to_string(), Some(String::new()));
1594
1595 // === String unchanged ===
1596 zv.set_string("hello", false).unwrap();
1597 assert_eq!(zv.coerce_to_string(), Some("hello".to_string()));
1598
1599 // === Array cannot be converted ===
1600 let arr_zv = Zval::new_array();
1601 assert_eq!(arr_zv.coerce_to_string(), None);
1602 });
1603 }
1604
1605 #[test]
1606 fn test_coerce_to_long() {
1607 Embed::run(|| {
1608 let mut zv = Zval::new();
1609
1610 // === Integer unchanged ===
1611 zv.set_long(42);
1612 assert_eq!(zv.coerce_to_long(), Some(42));
1613
1614 zv.set_long(-42);
1615 assert_eq!(zv.coerce_to_long(), Some(-42));
1616
1617 zv.set_long(0);
1618 assert_eq!(zv.coerce_to_long(), Some(0));
1619
1620 // === Float truncated (towards zero) ===
1621 // PHP: (int)3.14 -> 3
1622 zv.set_double(3.14);
1623 assert_eq!(zv.coerce_to_long(), Some(3));
1624
1625 // PHP: (int)3.99 -> 3
1626 zv.set_double(3.99);
1627 assert_eq!(zv.coerce_to_long(), Some(3));
1628
1629 // PHP: (int)-3.14 -> -3
1630 zv.set_double(-3.14);
1631 assert_eq!(zv.coerce_to_long(), Some(-3));
1632
1633 // PHP: (int)-3.99 -> -3
1634 zv.set_double(-3.99);
1635 assert_eq!(zv.coerce_to_long(), Some(-3));
1636
1637 // PHP: (int)0.0 -> 0
1638 zv.set_double(0.0);
1639 assert_eq!(zv.coerce_to_long(), Some(0));
1640
1641 // PHP: (int)-0.0 -> 0
1642 zv.set_double(-0.0);
1643 assert_eq!(zv.coerce_to_long(), Some(0));
1644
1645 // === Boolean to integer ===
1646 // PHP: (int)true -> 1
1647 zv.set_bool(true);
1648 assert_eq!(zv.coerce_to_long(), Some(1));
1649
1650 // PHP: (int)false -> 0
1651 zv.set_bool(false);
1652 assert_eq!(zv.coerce_to_long(), Some(0));
1653
1654 // === Null to integer ===
1655 // PHP: (int)null -> 0
1656 let null_zv = Zval::null();
1657 assert_eq!(null_zv.coerce_to_long(), Some(0));
1658
1659 // === String to integer (leading numeric portion) ===
1660 // PHP: (int)"123" -> 123
1661 zv.set_string("123", false).unwrap();
1662 assert_eq!(zv.coerce_to_long(), Some(123));
1663
1664 // PHP: (int)" 123" -> 123
1665 zv.set_string(" 123", false).unwrap();
1666 assert_eq!(zv.coerce_to_long(), Some(123));
1667
1668 // PHP: (int)"123abc" -> 123
1669 zv.set_string("123abc", false).unwrap();
1670 assert_eq!(zv.coerce_to_long(), Some(123));
1671
1672 // PHP: (int)"abc123" -> 0
1673 zv.set_string("abc123", false).unwrap();
1674 assert_eq!(zv.coerce_to_long(), Some(0));
1675
1676 // PHP: (int)"" -> 0
1677 zv.set_string("", false).unwrap();
1678 assert_eq!(zv.coerce_to_long(), Some(0));
1679
1680 // PHP: (int)"0" -> 0
1681 zv.set_string("0", false).unwrap();
1682 assert_eq!(zv.coerce_to_long(), Some(0));
1683
1684 // PHP: (int)"-0" -> 0
1685 zv.set_string("-0", false).unwrap();
1686 assert_eq!(zv.coerce_to_long(), Some(0));
1687
1688 // PHP: (int)"+123" -> 123
1689 zv.set_string("+123", false).unwrap();
1690 assert_eq!(zv.coerce_to_long(), Some(123));
1691
1692 // PHP: (int)" -456" -> -456
1693 zv.set_string(" -456", false).unwrap();
1694 assert_eq!(zv.coerce_to_long(), Some(-456));
1695
1696 // PHP: (int)"3.14" -> 3 (stops at decimal point)
1697 zv.set_string("3.14", false).unwrap();
1698 assert_eq!(zv.coerce_to_long(), Some(3));
1699
1700 // PHP: (int)"3.99" -> 3
1701 zv.set_string("3.99", false).unwrap();
1702 assert_eq!(zv.coerce_to_long(), Some(3));
1703
1704 // PHP: (int)"-3.99" -> -3
1705 zv.set_string("-3.99", false).unwrap();
1706 assert_eq!(zv.coerce_to_long(), Some(-3));
1707
1708 // PHP: (int)"abc" -> 0
1709 zv.set_string("abc", false).unwrap();
1710 assert_eq!(zv.coerce_to_long(), Some(0));
1711
1712 // === Array cannot be converted ===
1713 let arr_zv = Zval::new_array();
1714 assert_eq!(arr_zv.coerce_to_long(), None);
1715 });
1716 }
1717
1718 #[test]
1719 fn test_coerce_to_double() {
1720 Embed::run(|| {
1721 let mut zv = Zval::new();
1722
1723 // === Float unchanged ===
1724 zv.set_double(3.14);
1725 assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1726
1727 zv.set_double(-3.14);
1728 assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1729
1730 zv.set_double(0.0);
1731 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1732
1733 // === Integer to float ===
1734 // PHP: (float)42 -> 42.0
1735 zv.set_long(42);
1736 assert!((zv.coerce_to_double().unwrap() - 42.0).abs() < f64::EPSILON);
1737
1738 // PHP: (float)-42 -> -42.0
1739 zv.set_long(-42);
1740 assert!((zv.coerce_to_double().unwrap() - (-42.0)).abs() < f64::EPSILON);
1741
1742 // PHP: (float)0 -> 0.0
1743 zv.set_long(0);
1744 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1745
1746 // === Boolean to float ===
1747 // PHP: (float)true -> 1.0
1748 zv.set_bool(true);
1749 assert!((zv.coerce_to_double().unwrap() - 1.0).abs() < f64::EPSILON);
1750
1751 // PHP: (float)false -> 0.0
1752 zv.set_bool(false);
1753 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1754
1755 // === Null to float ===
1756 // PHP: (float)null -> 0.0
1757 let null_zv = Zval::null();
1758 assert!((null_zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1759
1760 // === String to float ===
1761 // PHP: (float)"3.14" -> 3.14
1762 zv.set_string("3.14", false).unwrap();
1763 assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1764
1765 // PHP: (float)" 3.14" -> 3.14
1766 zv.set_string(" 3.14", false).unwrap();
1767 assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1768
1769 // PHP: (float)"3.14abc" -> 3.14
1770 zv.set_string("3.14abc", false).unwrap();
1771 assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1772
1773 // PHP: (float)"abc3.14" -> 0.0
1774 zv.set_string("abc3.14", false).unwrap();
1775 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1776
1777 // PHP: (float)"" -> 0.0
1778 zv.set_string("", false).unwrap();
1779 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1780
1781 // PHP: (float)"0" -> 0.0
1782 zv.set_string("0", false).unwrap();
1783 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1784
1785 // PHP: (float)"0.0" -> 0.0
1786 zv.set_string("0.0", false).unwrap();
1787 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1788
1789 // PHP: (float)"+3.14" -> 3.14
1790 zv.set_string("+3.14", false).unwrap();
1791 assert!((zv.coerce_to_double().unwrap() - 3.14).abs() < f64::EPSILON);
1792
1793 // PHP: (float)" -3.14" -> -3.14
1794 zv.set_string(" -3.14", false).unwrap();
1795 assert!((zv.coerce_to_double().unwrap() - (-3.14)).abs() < f64::EPSILON);
1796
1797 // Scientific notation
1798 // PHP: (float)"1e10" -> 1e10
1799 zv.set_string("1e10", false).unwrap();
1800 assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1801
1802 // PHP: (float)"1E10" -> 1e10
1803 zv.set_string("1E10", false).unwrap();
1804 assert!((zv.coerce_to_double().unwrap() - 1e10).abs() < 1.0);
1805
1806 // PHP: (float)"1.5e-3" -> 0.0015
1807 zv.set_string("1.5e-3", false).unwrap();
1808 assert!((zv.coerce_to_double().unwrap() - 0.0015).abs() < f64::EPSILON);
1809
1810 // PHP: (float)".5" -> 0.5
1811 zv.set_string(".5", false).unwrap();
1812 assert!((zv.coerce_to_double().unwrap() - 0.5).abs() < f64::EPSILON);
1813
1814 // PHP: (float)"5." -> 5.0
1815 zv.set_string("5.", false).unwrap();
1816 assert!((zv.coerce_to_double().unwrap() - 5.0).abs() < f64::EPSILON);
1817
1818 // PHP: (float)"abc" -> 0.0
1819 zv.set_string("abc", false).unwrap();
1820 assert!((zv.coerce_to_double().unwrap() - 0.0).abs() < f64::EPSILON);
1821
1822 // === Array cannot be converted ===
1823 let arr_zv = Zval::new_array();
1824 assert_eq!(arr_zv.coerce_to_double(), None);
1825 });
1826 }
1827
1828 #[test]
1829 fn test_parse_long_from_str() {
1830 use crate::ffi::zend_long as ZendLong;
1831
1832 // Basic cases
1833 assert_eq!(parse_long_from_str("42"), 42);
1834 assert_eq!(parse_long_from_str("0"), 0);
1835 assert_eq!(parse_long_from_str("-42"), -42);
1836 assert_eq!(parse_long_from_str("+42"), 42);
1837
1838 // Leading/trailing content
1839 assert_eq!(parse_long_from_str("42abc"), 42);
1840 assert_eq!(parse_long_from_str(" 42"), 42);
1841 assert_eq!(parse_long_from_str("\t\n42"), 42);
1842 assert_eq!(parse_long_from_str(" -42"), -42);
1843 assert_eq!(parse_long_from_str("42.5"), 42); // stops at decimal point
1844
1845 // Non-numeric strings
1846 assert_eq!(parse_long_from_str("abc"), 0);
1847 assert_eq!(parse_long_from_str(""), 0);
1848 assert_eq!(parse_long_from_str(" "), 0);
1849 assert_eq!(parse_long_from_str("-"), 0);
1850 assert_eq!(parse_long_from_str("+"), 0);
1851 assert_eq!(parse_long_from_str("abc123"), 0);
1852
1853 // Edge cases matching PHP behavior:
1854 // - Hexadecimal: PHP's (int)"0xFF" returns 0 (stops at 'x')
1855 assert_eq!(parse_long_from_str("0xFF"), 0);
1856 assert_eq!(parse_long_from_str("0x10"), 0);
1857
1858 // - Octal notation is NOT interpreted, just reads leading digits
1859 assert_eq!(parse_long_from_str("010"), 10); // not 8
1860
1861 // - Binary notation is NOT interpreted
1862 assert_eq!(parse_long_from_str("0b101"), 0);
1863
1864 // Overflow tests - saturates to max/min
1865 assert_eq!(
1866 parse_long_from_str("99999999999999999999999999"),
1867 ZendLong::MAX
1868 );
1869 assert_eq!(
1870 parse_long_from_str("-99999999999999999999999999"),
1871 ZendLong::MIN
1872 );
1873
1874 // Large but valid numbers
1875 assert_eq!(parse_long_from_str("9223372036854775807"), ZendLong::MAX); // i64::MAX
1876 assert_eq!(parse_long_from_str("-9223372036854775808"), ZendLong::MIN); // i64::MIN
1877 }
1878
1879 #[test]
1880 fn test_parse_double_from_str() {
1881 // Basic cases
1882 assert!((parse_double_from_str("3.14") - 3.14).abs() < f64::EPSILON);
1883 assert!((parse_double_from_str("0.0") - 0.0).abs() < f64::EPSILON);
1884 assert!((parse_double_from_str("-3.14") - (-3.14)).abs() < f64::EPSILON);
1885 assert!((parse_double_from_str("+3.14") - 3.14).abs() < f64::EPSILON);
1886 assert!((parse_double_from_str(".5") - 0.5).abs() < f64::EPSILON);
1887 assert!((parse_double_from_str("5.") - 5.0).abs() < f64::EPSILON);
1888
1889 // Scientific notation
1890 assert!((parse_double_from_str("1e10") - 1e10).abs() < 1.0);
1891 assert!((parse_double_from_str("1E10") - 1e10).abs() < 1.0);
1892 assert!((parse_double_from_str("1.5e-3") - 1.5e-3).abs() < f64::EPSILON);
1893 assert!((parse_double_from_str("1.5E+3") - 1500.0).abs() < f64::EPSILON);
1894 assert!((parse_double_from_str("-1.5e2") - (-150.0)).abs() < f64::EPSILON);
1895
1896 // Leading/trailing content
1897 assert!((parse_double_from_str("3.14abc") - 3.14).abs() < f64::EPSILON);
1898 assert!((parse_double_from_str(" 3.14") - 3.14).abs() < f64::EPSILON);
1899 assert!((parse_double_from_str("\t\n3.14") - 3.14).abs() < f64::EPSILON);
1900 assert!((parse_double_from_str("1e10xyz") - 1e10).abs() < 1.0);
1901
1902 // Non-numeric strings
1903 assert!((parse_double_from_str("abc") - 0.0).abs() < f64::EPSILON);
1904 assert!((parse_double_from_str("") - 0.0).abs() < f64::EPSILON);
1905 assert!((parse_double_from_str(" ") - 0.0).abs() < f64::EPSILON);
1906 assert!((parse_double_from_str("-") - 0.0).abs() < f64::EPSILON);
1907 assert!((parse_double_from_str("e10") - 0.0).abs() < f64::EPSILON); // no digits before e
1908
1909 // Integer values
1910 assert!((parse_double_from_str("42") - 42.0).abs() < f64::EPSILON);
1911 assert!((parse_double_from_str("-42") - (-42.0)).abs() < f64::EPSILON);
1912
1913 // Edge cases:
1914 // - Hexadecimal: not supported, returns 0
1915 assert!((parse_double_from_str("0xFF") - 0.0).abs() < f64::EPSILON);
1916
1917 // - Multiple decimal points: stops at second
1918 assert!((parse_double_from_str("1.2.3") - 1.2).abs() < f64::EPSILON);
1919
1920 // - Multiple exponents: stops at second e
1921 assert!((parse_double_from_str("1e2e3") - 100.0).abs() < f64::EPSILON);
1922 }
1923
1924 #[test]
1925 fn test_php_float_to_string() {
1926 // Basic decimal values
1927 assert_eq!(php_float_to_string(3.14), "3.14");
1928 assert_eq!(php_float_to_string(42.0), "42");
1929 assert_eq!(php_float_to_string(-3.14), "-3.14");
1930 assert_eq!(php_float_to_string(0.0), "0");
1931
1932 // Large numbers use scientific notation with uppercase E
1933 // PHP: (string)(float)'999...9' -> "1.0E+52"
1934 assert_eq!(php_float_to_string(1e52), "1.0E+52");
1935 assert_eq!(php_float_to_string(1e20), "1.0E+20");
1936 assert_eq!(php_float_to_string(1e14), "1.0E+14");
1937
1938 // Very small numbers also use scientific notation (at exp <= -5)
1939 assert_eq!(php_float_to_string(1e-10), "1.0E-10");
1940 assert_eq!(php_float_to_string(1e-5), "1.0E-5");
1941 assert_eq!(php_float_to_string(0.00001), "1.0E-5");
1942
1943 // Numbers that don't need scientific notation
1944 assert_eq!(php_float_to_string(1e13), "10000000000000");
1945 assert_eq!(php_float_to_string(0.001), "0.001");
1946 assert_eq!(php_float_to_string(0.0001), "0.0001"); // 1e-4 is decimal
1947 }
1948}