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