Skip to main content

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