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