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