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