ext_php_rs/types/
array.rs

1//! Represents an array in PHP. As all arrays in PHP are associative arrays,
2//! they are represented by hash tables.
3
4use std::{
5    collections::HashMap,
6    convert::{TryFrom, TryInto},
7    ffi::CString,
8    fmt::{Debug, Display},
9    iter::FromIterator,
10    ptr,
11    str::FromStr,
12};
13
14use crate::{
15    boxed::{ZBox, ZBoxable},
16    convert::{FromZval, IntoZval},
17    error::{Error, Result},
18    ffi::zend_ulong,
19    ffi::{
20        _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean,
21        zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex,
22        zend_hash_get_current_key_zval_ex, zend_hash_index_del, zend_hash_index_find,
23        zend_hash_index_update, zend_hash_move_backwards_ex, zend_hash_move_forward_ex,
24        zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
25        HashPosition, HT_MIN_SIZE,
26    },
27    flags::DataType,
28    types::Zval,
29};
30
31/// A PHP hashtable.
32///
33/// In PHP, arrays are represented as hashtables. This allows you to push values
34/// onto the end of the array like a vector, while also allowing you to insert
35/// at arbitrary string key indexes.
36///
37/// A PHP hashtable stores values as [`Zval`]s. This allows you to insert
38/// different types into the same hashtable. Types must implement [`IntoZval`]
39/// to be able to be inserted into the hashtable.
40///
41/// # Examples
42///
43/// ```no_run
44/// use ext_php_rs::types::ZendHashTable;
45///
46/// let mut ht = ZendHashTable::new();
47/// ht.push(1);
48/// ht.push("Hello, world!");
49/// ht.insert("Like", "Hashtable");
50///
51/// assert_eq!(ht.len(), 3);
52/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(1));
53/// ```
54pub type ZendHashTable = crate::ffi::HashTable;
55
56// Clippy complains about there being no `is_empty` function when implementing
57// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
58#[allow(clippy::len_without_is_empty)]
59impl ZendHashTable {
60    /// Creates a new, empty, PHP hashtable, returned inside a [`ZBox`].
61    ///
62    /// # Example
63    ///
64    /// ```no_run
65    /// use ext_php_rs::types::ZendHashTable;
66    ///
67    /// let ht = ZendHashTable::new();
68    /// ```
69    ///
70    /// # Panics
71    ///
72    /// Panics if memory for the hashtable could not be allocated.
73    #[must_use]
74    pub fn new() -> ZBox<Self> {
75        Self::with_capacity(HT_MIN_SIZE)
76    }
77
78    /// Creates a new, empty, PHP hashtable with an initial size, returned
79    /// inside a [`ZBox`].
80    ///
81    /// # Parameters
82    ///
83    /// * `size` - The size to initialize the array with.
84    ///
85    /// # Example
86    ///
87    /// ```no_run
88    /// use ext_php_rs::types::ZendHashTable;
89    ///
90    /// let ht = ZendHashTable::with_capacity(10);
91    /// ```
92    ///
93    /// # Panics
94    ///
95    /// Panics if memory for the hashtable could not be allocated.
96    #[must_use]
97    pub fn with_capacity(size: u32) -> ZBox<Self> {
98        unsafe {
99            // SAFETY: PHP allocator handles the creation of the array.
100            #[allow(clippy::used_underscore_items)]
101            let ptr = _zend_new_array(size);
102
103            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
104            ZBox::from_raw(
105                ptr.as_mut()
106                    .expect("Failed to allocate memory for hashtable"),
107            )
108        }
109    }
110
111    /// Returns the current number of elements in the array.
112    ///
113    /// # Example
114    ///
115    /// ```no_run
116    /// use ext_php_rs::types::ZendHashTable;
117    ///
118    /// let mut ht = ZendHashTable::new();
119    ///
120    /// ht.push(1);
121    /// ht.push("Hello, world");
122    ///
123    /// assert_eq!(ht.len(), 2);
124    /// ```
125    #[must_use]
126    pub fn len(&self) -> usize {
127        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
128    }
129
130    /// Returns whether the hash table is empty.
131    ///
132    /// # Example
133    ///
134    /// ```no_run
135    /// use ext_php_rs::types::ZendHashTable;
136    ///
137    /// let mut ht = ZendHashTable::new();
138    ///
139    /// assert_eq!(ht.is_empty(), true);
140    ///
141    /// ht.push(1);
142    /// ht.push("Hello, world");
143    ///
144    /// assert_eq!(ht.is_empty(), false);
145    /// ```
146    #[must_use]
147    pub fn is_empty(&self) -> bool {
148        self.len() == 0
149    }
150
151    /// Clears the hash table, removing all values.
152    ///
153    /// # Example
154    ///
155    /// ```no_run
156    /// use ext_php_rs::types::ZendHashTable;
157    ///
158    /// let mut ht = ZendHashTable::new();
159    ///
160    /// ht.insert("test", "hello world");
161    /// assert_eq!(ht.is_empty(), false);
162    ///
163    /// ht.clear();
164    /// assert_eq!(ht.is_empty(), true);
165    /// ```
166    pub fn clear(&mut self) {
167        unsafe { zend_hash_clean(self) }
168    }
169
170    /// Attempts to retrieve a value from the hash table with a string key.
171    ///
172    /// # Parameters
173    ///
174    /// * `key` - The key to search for in the hash table.
175    ///
176    /// # Returns
177    ///
178    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
179    ///   table.
180    /// * `None` - No value at the given position was found.
181    ///
182    /// # Example
183    ///
184    /// ```no_run
185    /// use ext_php_rs::types::ZendHashTable;
186    ///
187    /// let mut ht = ZendHashTable::new();
188    ///
189    /// ht.insert("test", "hello world");
190    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
191    /// ```
192    #[must_use]
193    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
194    where
195        K: Into<ArrayKey<'a>>,
196    {
197        match key.into() {
198            ArrayKey::Long(index) => unsafe {
199                #[allow(clippy::cast_sign_loss)]
200                zend_hash_index_find(self, index as zend_ulong).as_ref()
201            },
202            ArrayKey::String(key) => {
203                if let Ok(index) = i64::from_str(key.as_str()) {
204                    #[allow(clippy::cast_sign_loss)]
205                    unsafe {
206                        zend_hash_index_find(self, index as zend_ulong).as_ref()
207                    }
208                } else {
209                    unsafe {
210                        zend_hash_str_find(
211                            self,
212                            CString::new(key.as_str()).ok()?.as_ptr(),
213                            key.len() as _,
214                        )
215                        .as_ref()
216                    }
217                }
218            }
219            ArrayKey::Str(key) => {
220                if let Ok(index) = i64::from_str(key) {
221                    #[allow(clippy::cast_sign_loss)]
222                    unsafe {
223                        zend_hash_index_find(self, index as zend_ulong).as_ref()
224                    }
225                } else {
226                    unsafe {
227                        zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
228                            .as_ref()
229                    }
230                }
231            }
232        }
233    }
234
235    /// Attempts to retrieve a value from the hash table with a string key.
236    ///
237    /// # Parameters
238    ///
239    /// * `key` - The key to search for in the hash table.
240    ///
241    /// # Returns
242    ///
243    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
244    ///   table.
245    /// * `None` - No value at the given position was found.
246    ///
247    /// # Example
248    ///
249    /// ```no_run
250    /// use ext_php_rs::types::ZendHashTable;
251    ///
252    /// let mut ht = ZendHashTable::new();
253    ///
254    /// ht.insert("test", "hello world");
255    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
256    /// ```
257    // TODO: Verify if this is safe to use, as it allows mutating the
258    // hashtable while only having a reference to it. #461
259    #[allow(clippy::mut_from_ref)]
260    #[must_use]
261    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
262    where
263        K: Into<ArrayKey<'a>>,
264    {
265        match key.into() {
266            ArrayKey::Long(index) => unsafe {
267                #[allow(clippy::cast_sign_loss)]
268                zend_hash_index_find(self, index as zend_ulong).as_mut()
269            },
270            ArrayKey::String(key) => {
271                if let Ok(index) = i64::from_str(key.as_str()) {
272                    #[allow(clippy::cast_sign_loss)]
273                    unsafe {
274                        zend_hash_index_find(self, index as zend_ulong).as_mut()
275                    }
276                } else {
277                    unsafe {
278                        zend_hash_str_find(
279                            self,
280                            CString::new(key.as_str()).ok()?.as_ptr(),
281                            key.len() as _,
282                        )
283                        .as_mut()
284                    }
285                }
286            }
287            ArrayKey::Str(key) => {
288                if let Ok(index) = i64::from_str(key) {
289                    #[allow(clippy::cast_sign_loss)]
290                    unsafe {
291                        zend_hash_index_find(self, index as zend_ulong).as_mut()
292                    }
293                } else {
294                    unsafe {
295                        zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
296                            .as_mut()
297                    }
298                }
299            }
300        }
301    }
302
303    /// Attempts to retrieve a value from the hash table with an index.
304    ///
305    /// # Parameters
306    ///
307    /// * `key` - The key to search for in the hash table.
308    ///
309    /// # Returns
310    ///
311    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
312    ///   table.
313    /// * `None` - No value at the given position was found.
314    ///
315    /// # Example
316    ///
317    /// ```no_run
318    /// use ext_php_rs::types::ZendHashTable;
319    ///
320    /// let mut ht = ZendHashTable::new();
321    ///
322    /// ht.push(100);
323    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
324    /// ```
325    #[must_use]
326    pub fn get_index(&self, key: i64) -> Option<&Zval> {
327        #[allow(clippy::cast_sign_loss)]
328        unsafe {
329            zend_hash_index_find(self, key as zend_ulong).as_ref()
330        }
331    }
332
333    /// Attempts to retrieve a value from the hash table with an index.
334    ///
335    /// # Parameters
336    ///
337    /// * `key` - The key to search for in the hash table.
338    ///
339    /// # Returns
340    ///
341    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
342    ///   table.
343    /// * `None` - No value at the given position was found.
344    ///
345    /// # Example
346    ///
347    /// ```no_run
348    /// use ext_php_rs::types::ZendHashTable;
349    ///
350    /// let mut ht = ZendHashTable::new();
351    ///
352    /// ht.push(100);
353    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
354    /// ```
355    // TODO: Verify if this is safe to use, as it allows mutating the
356    // hashtable while only having a reference to it. #461
357    #[allow(clippy::mut_from_ref)]
358    #[must_use]
359    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
360        unsafe {
361            #[allow(clippy::cast_sign_loss)]
362            zend_hash_index_find(self, key as zend_ulong).as_mut()
363        }
364    }
365
366    /// Attempts to remove a value from the hash table with a string key.
367    ///
368    /// # Parameters
369    ///
370    /// * `key` - The key to remove from the hash table.
371    ///
372    /// # Returns
373    ///
374    /// * `Some(())` - Key was successfully removed.
375    /// * `None` - No key was removed, did not exist.
376    ///
377    /// # Example
378    ///
379    /// ```no_run
380    /// use ext_php_rs::types::ZendHashTable;
381    ///
382    /// let mut ht = ZendHashTable::new();
383    ///
384    /// ht.insert("test", "hello world");
385    /// assert_eq!(ht.len(), 1);
386    ///
387    /// ht.remove("test");
388    /// assert_eq!(ht.len(), 0);
389    /// ```
390    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
391    where
392        K: Into<ArrayKey<'a>>,
393    {
394        let result = match key.into() {
395            ArrayKey::Long(index) => unsafe {
396                #[allow(clippy::cast_sign_loss)]
397                zend_hash_index_del(self, index as zend_ulong)
398            },
399            ArrayKey::String(key) => {
400                if let Ok(index) = i64::from_str(key.as_str()) {
401                    #[allow(clippy::cast_sign_loss)]
402                    unsafe {
403                        zend_hash_index_del(self, index as zend_ulong)
404                    }
405                } else {
406                    unsafe {
407                        zend_hash_str_del(
408                            self,
409                            CString::new(key.as_str()).ok()?.as_ptr(),
410                            key.len() as _,
411                        )
412                    }
413                }
414            }
415            ArrayKey::Str(key) => {
416                if let Ok(index) = i64::from_str(key) {
417                    #[allow(clippy::cast_sign_loss)]
418                    unsafe {
419                        zend_hash_index_del(self, index as zend_ulong)
420                    }
421                } else {
422                    unsafe {
423                        zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
424                    }
425                }
426            }
427        };
428
429        if result < 0 {
430            None
431        } else {
432            Some(())
433        }
434    }
435
436    /// Attempts to remove a value from the hash table with a string key.
437    ///
438    /// # Parameters
439    ///
440    /// * `key` - The key to remove from the hash table.
441    ///
442    /// # Returns
443    ///
444    /// * `Ok(())` - Key was successfully removed.
445    /// * `None` - No key was removed, did not exist.
446    ///
447    /// # Example
448    ///
449    /// ```no_run
450    /// use ext_php_rs::types::ZendHashTable;
451    ///
452    /// let mut ht = ZendHashTable::new();
453    ///
454    /// ht.push("hello");
455    /// assert_eq!(ht.len(), 1);
456    ///
457    /// ht.remove_index(0);
458    /// assert_eq!(ht.len(), 0);
459    /// ```
460    pub fn remove_index(&mut self, key: i64) -> Option<()> {
461        let result = unsafe {
462            #[allow(clippy::cast_sign_loss)]
463            zend_hash_index_del(self, key as zend_ulong)
464        };
465
466        if result < 0 {
467            None
468        } else {
469            Some(())
470        }
471    }
472
473    /// Attempts to insert an item into the hash table, or update if the key
474    /// already exists. Returns nothing in a result if successful.
475    ///
476    /// # Parameters
477    ///
478    /// * `key` - The key to insert the value at in the hash table.
479    /// * `value` - The value to insert into the hash table.
480    ///
481    /// # Returns
482    ///
483    /// Returns nothing in a result on success.
484    ///
485    /// # Errors
486    ///
487    /// Returns an error if the key could not be converted into a [`CString`],
488    /// or converting the value into a [`Zval`] failed.
489    ///
490    /// # Example
491    ///
492    /// ```no_run
493    /// use ext_php_rs::types::ZendHashTable;
494    ///
495    /// let mut ht = ZendHashTable::new();
496    ///
497    /// ht.insert("a", "A");
498    /// ht.insert("b", "B");
499    /// ht.insert("c", "C");
500    /// assert_eq!(ht.len(), 3);
501    /// ```
502    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
503    where
504        K: Into<ArrayKey<'a>>,
505        V: IntoZval,
506    {
507        let mut val = val.into_zval(false)?;
508        match key.into() {
509            ArrayKey::Long(index) => {
510                unsafe {
511                    #[allow(clippy::cast_sign_loss)]
512                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
513                };
514            }
515            ArrayKey::String(key) => {
516                if let Ok(index) = i64::from_str(&key) {
517                    unsafe {
518                        #[allow(clippy::cast_sign_loss)]
519                        zend_hash_index_update(self, index as zend_ulong, &raw mut val)
520                    };
521                } else {
522                    unsafe {
523                        zend_hash_str_update(
524                            self,
525                            CString::new(key.as_str())?.as_ptr(),
526                            key.len(),
527                            &raw mut val,
528                        )
529                    };
530                }
531            }
532            ArrayKey::Str(key) => {
533                if let Ok(index) = i64::from_str(key) {
534                    unsafe {
535                        #[allow(clippy::cast_sign_loss)]
536                        zend_hash_index_update(self, index as zend_ulong, &raw mut val)
537                    };
538                } else {
539                    unsafe {
540                        zend_hash_str_update(
541                            self,
542                            CString::new(key)?.as_ptr(),
543                            key.len(),
544                            &raw mut val,
545                        )
546                    };
547                }
548            }
549        }
550        val.release();
551        Ok(())
552    }
553
554    /// Inserts an item into the hash table at a specified index, or updates if
555    /// the key already exists. Returns nothing in a result if successful.
556    ///
557    /// # Parameters
558    ///
559    /// * `key` - The index at which the value should be inserted.
560    /// * `val` - The value to insert into the hash table.
561    ///
562    /// # Returns
563    ///
564    /// Returns nothing in a result on success.
565    ///
566    /// # Errors
567    ///
568    /// Returns an error if converting the value into a [`Zval`] failed.
569    ///
570    /// # Example
571    ///
572    /// ```no_run
573    /// use ext_php_rs::types::ZendHashTable;
574    ///
575    /// let mut ht = ZendHashTable::new();
576    ///
577    /// ht.insert_at_index(0, "A");
578    /// ht.insert_at_index(5, "B");
579    /// ht.insert_at_index(0, "C"); // notice overriding index 0
580    /// assert_eq!(ht.len(), 2);
581    /// ```
582    pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
583    where
584        V: IntoZval,
585    {
586        let mut val = val.into_zval(false)?;
587        unsafe {
588            #[allow(clippy::cast_sign_loss)]
589            zend_hash_index_update(self, key as zend_ulong, &raw mut val)
590        };
591        val.release();
592        Ok(())
593    }
594
595    /// Pushes an item onto the end of the hash table. Returns a result
596    /// containing nothing if the element was successfully inserted.
597    ///
598    /// # Parameters
599    ///
600    /// * `val` - The value to insert into the hash table.
601    ///
602    /// # Returns
603    ///
604    /// Returns nothing in a result on success.
605    ///
606    /// # Errors
607    ///
608    /// Returns an error if converting the value into a [`Zval`] failed.
609    ///
610    /// # Example
611    ///
612    /// ```no_run
613    /// use ext_php_rs::types::ZendHashTable;
614    ///
615    /// let mut ht = ZendHashTable::new();
616    ///
617    /// ht.push("a");
618    /// ht.push("b");
619    /// ht.push("c");
620    /// assert_eq!(ht.len(), 3);
621    /// ```
622    pub fn push<V>(&mut self, val: V) -> Result<()>
623    where
624        V: IntoZval,
625    {
626        let mut val = val.into_zval(false)?;
627        unsafe { zend_hash_next_index_insert(self, &raw mut val) };
628        val.release();
629
630        Ok(())
631    }
632
633    /// Checks if the hashtable only contains numerical keys.
634    ///
635    /// # Returns
636    ///
637    /// True if all keys on the hashtable are numerical.
638    ///
639    /// # Example
640    ///
641    /// ```no_run
642    /// use ext_php_rs::types::ZendHashTable;
643    ///
644    /// let mut ht = ZendHashTable::new();
645    ///
646    /// ht.push(0);
647    /// ht.push(3);
648    /// ht.push(9);
649    /// assert!(ht.has_numerical_keys());
650    ///
651    /// ht.insert("obviously not numerical", 10);
652    /// assert!(!ht.has_numerical_keys());
653    /// ```
654    #[must_use]
655    pub fn has_numerical_keys(&self) -> bool {
656        !self.into_iter().any(|(k, _)| !k.is_long())
657    }
658
659    /// Checks if the hashtable has numerical, sequential keys.
660    ///
661    /// # Returns
662    ///
663    /// True if all keys on the hashtable are numerical and are in sequential
664    /// order (i.e. starting at 0 and not skipping any keys).
665    ///
666    /// # Panics
667    ///
668    /// Panics if the number of elements in the hashtable exceeds `i64::MAX`.
669    ///
670    /// # Example
671    ///
672    /// ```no_run
673    /// use ext_php_rs::types::ZendHashTable;
674    ///
675    /// let mut ht = ZendHashTable::new();
676    ///
677    /// ht.push(0);
678    /// ht.push(3);
679    /// ht.push(9);
680    /// assert!(ht.has_sequential_keys());
681    ///
682    /// ht.insert_at_index(90, 10);
683    /// assert!(!ht.has_sequential_keys());
684    /// ```
685    #[must_use]
686    pub fn has_sequential_keys(&self) -> bool {
687        !self
688            .into_iter()
689            .enumerate()
690            .any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
691    }
692
693    /// Returns an iterator over the values contained inside the hashtable, as
694    /// if it was a set or list.
695    ///
696    /// # Example
697    ///
698    /// ```no_run
699    /// use ext_php_rs::types::ZendHashTable;
700    ///
701    /// let mut ht = ZendHashTable::new();
702    ///
703    /// for val in ht.values() {
704    ///     dbg!(val);
705    /// }
706    #[inline]
707    #[must_use]
708    pub fn values(&self) -> Values<'_> {
709        Values::new(self)
710    }
711
712    /// Returns an iterator over the key(s) and value contained inside the
713    /// hashtable.
714    ///
715    /// # Example
716    ///
717    /// ```no_run
718    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
719    ///
720    /// let mut ht = ZendHashTable::new();
721    ///
722    /// for (key, val) in ht.iter() {
723    ///     match &key {
724    ///         ArrayKey::Long(index) => {
725    ///         }
726    ///         ArrayKey::String(key) => {
727    ///         }
728    ///         ArrayKey::Str(key) => {
729    ///         }
730    ///     }
731    ///     dbg!(key, val);
732    /// }
733    #[inline]
734    #[must_use]
735    pub fn iter(&self) -> Iter<'_> {
736        self.into_iter()
737    }
738}
739
740unsafe impl ZBoxable for ZendHashTable {
741    fn free(&mut self) {
742        // SAFETY: ZBox has immutable access to `self`.
743        unsafe { zend_array_destroy(self) }
744    }
745}
746
747impl Debug for ZendHashTable {
748    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
749        f.debug_map()
750            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
751            .finish()
752    }
753}
754
755impl ToOwned for ZendHashTable {
756    type Owned = ZBox<ZendHashTable>;
757
758    fn to_owned(&self) -> Self::Owned {
759        unsafe {
760            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
761            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
762
763            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
764            ZBox::from_raw(
765                ptr.as_mut()
766                    .expect("Failed to allocate memory for hashtable"),
767            )
768        }
769    }
770}
771
772/// Immutable iterator upon a reference to a hashtable.
773pub struct Iter<'a> {
774    ht: &'a ZendHashTable,
775    current_num: i64,
776    end_num: i64,
777    pos: HashPosition,
778    end_pos: HashPosition,
779}
780
781/// Represents the key of a PHP array, which can be either a long or a string.
782#[derive(Debug, Clone, PartialEq)]
783pub enum ArrayKey<'a> {
784    /// A numerical key.
785    /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs
786    /// to be cast to `zend_ulong` before passing into Zend functions.
787    Long(i64),
788    /// A string key.
789    String(String),
790    /// A string key by reference.
791    Str(&'a str),
792}
793
794impl From<String> for ArrayKey<'_> {
795    fn from(value: String) -> Self {
796        Self::String(value)
797    }
798}
799
800impl ArrayKey<'_> {
801    /// Check if the key is an integer.
802    ///
803    /// # Returns
804    ///
805    /// Returns true if the key is an integer, false otherwise.
806    #[must_use]
807    pub fn is_long(&self) -> bool {
808        match self {
809            ArrayKey::Long(_) => true,
810            ArrayKey::String(_) | ArrayKey::Str(_) => false,
811        }
812    }
813}
814
815impl Display for ArrayKey<'_> {
816    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
817        match self {
818            ArrayKey::Long(key) => write!(f, "{key}"),
819            ArrayKey::String(key) => write!(f, "{key}"),
820            ArrayKey::Str(key) => write!(f, "{key}"),
821        }
822    }
823}
824
825impl<'a> From<&'a str> for ArrayKey<'a> {
826    fn from(key: &'a str) -> ArrayKey<'a> {
827        ArrayKey::Str(key)
828    }
829}
830
831impl<'a> From<i64> for ArrayKey<'a> {
832    fn from(index: i64) -> ArrayKey<'a> {
833        ArrayKey::Long(index)
834    }
835}
836
837impl<'a> FromZval<'a> for ArrayKey<'_> {
838    const TYPE: DataType = DataType::String;
839
840    fn from_zval(zval: &'a Zval) -> Option<Self> {
841        if let Some(key) = zval.long() {
842            return Some(ArrayKey::Long(key));
843        }
844        if let Some(key) = zval.string() {
845            return Some(ArrayKey::String(key));
846        }
847        None
848    }
849}
850
851impl<'a> Iter<'a> {
852    /// Creates a new iterator over a hashtable.
853    ///
854    /// # Parameters
855    ///
856    /// * `ht` - The hashtable to iterate.
857    pub fn new(ht: &'a ZendHashTable) -> Self {
858        let end_num: i64 = ht
859            .len()
860            .try_into()
861            .expect("Integer overflow in hashtable length");
862        let end_pos = if ht.nNumOfElements > 0 {
863            ht.nNumOfElements - 1
864        } else {
865            0
866        };
867
868        Self {
869            ht,
870            current_num: 0,
871            end_num,
872            pos: 0,
873            end_pos,
874        }
875    }
876}
877
878impl<'a> IntoIterator for &'a ZendHashTable {
879    type Item = (ArrayKey<'a>, &'a Zval);
880    type IntoIter = Iter<'a>;
881
882    /// Returns an iterator over the key(s) and value contained inside the
883    /// hashtable.
884    ///
885    /// # Example
886    ///
887    /// ```no_run
888    /// use ext_php_rs::types::ZendHashTable;
889    ///
890    /// let mut ht = ZendHashTable::new();
891    ///
892    /// for (key, val) in ht.iter() {
893    /// //   ^ Index if inserted at an index.
894    /// //        ^ Optional string key, if inserted like a hashtable.
895    /// //             ^ Inserted value.
896    ///
897    ///     dbg!(key, val);
898    /// }
899    #[inline]
900    fn into_iter(self) -> Self::IntoIter {
901        Iter::new(self)
902    }
903}
904
905impl<'a> Iterator for Iter<'a> {
906    type Item = (ArrayKey<'a>, &'a Zval);
907
908    fn next(&mut self) -> Option<Self::Item> {
909        self.next_zval()
910            .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
911    }
912
913    fn count(self) -> usize
914    where
915        Self: Sized,
916    {
917        self.ht.len()
918    }
919}
920
921impl ExactSizeIterator for Iter<'_> {
922    fn len(&self) -> usize {
923        self.ht.len()
924    }
925}
926
927impl DoubleEndedIterator for Iter<'_> {
928    fn next_back(&mut self) -> Option<Self::Item> {
929        if self.end_num <= self.current_num {
930            return None;
931        }
932
933        let key_type = unsafe {
934            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
935        };
936
937        if key_type == -1 {
938            return None;
939        }
940
941        let key = Zval::new();
942
943        unsafe {
944            zend_hash_get_current_key_zval_ex(
945                ptr::from_ref(self.ht).cast_mut(),
946                (&raw const key).cast_mut(),
947                &raw mut self.end_pos,
948            );
949        }
950        let value = unsafe {
951            &*zend_hash_get_current_data_ex(
952                ptr::from_ref(self.ht).cast_mut(),
953                &raw mut self.end_pos,
954            )
955        };
956
957        let key = match ArrayKey::from_zval(&key) {
958            Some(key) => key,
959            None => ArrayKey::Long(self.end_num),
960        };
961
962        unsafe {
963            zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos)
964        };
965        self.end_num -= 1;
966
967        Some((key, value))
968    }
969}
970
971impl<'a> Iter<'a> {
972    pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> {
973        if self.current_num >= self.end_num {
974            return None;
975        }
976
977        let key_type = unsafe {
978            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
979        };
980
981        // Key type `-1` is ???
982        // Key type `1` is string
983        // Key type `2` is long
984        // Key type `3` is null meaning the end of the array
985        if key_type == -1 || key_type == 3 {
986            return None;
987        }
988
989        let mut key = Zval::new();
990
991        unsafe {
992            zend_hash_get_current_key_zval_ex(
993                ptr::from_ref(self.ht).cast_mut(),
994                (&raw const key).cast_mut(),
995                &raw mut self.pos,
996            );
997        }
998        let value = unsafe {
999            let val_ptr =
1000                zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos);
1001
1002            if val_ptr.is_null() {
1003                return None;
1004            }
1005
1006            &*val_ptr
1007        };
1008
1009        if !key.is_long() && !key.is_string() {
1010            key.set_long(self.current_num);
1011        }
1012
1013        unsafe { zend_hash_move_forward_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) };
1014        self.current_num += 1;
1015
1016        Some((key, value))
1017    }
1018}
1019
1020/// Immutable iterator which iterates over the values of the hashtable, as it
1021/// was a set or list.
1022pub struct Values<'a>(Iter<'a>);
1023
1024impl<'a> Values<'a> {
1025    /// Creates a new iterator over a hashtables values.
1026    ///
1027    /// # Parameters
1028    ///
1029    /// * `ht` - The hashtable to iterate.
1030    pub fn new(ht: &'a ZendHashTable) -> Self {
1031        Self(Iter::new(ht))
1032    }
1033}
1034
1035impl<'a> Iterator for Values<'a> {
1036    type Item = &'a Zval;
1037
1038    fn next(&mut self) -> Option<Self::Item> {
1039        self.0.next().map(|(_, zval)| zval)
1040    }
1041
1042    fn count(self) -> usize
1043    where
1044        Self: Sized,
1045    {
1046        self.0.count()
1047    }
1048}
1049
1050impl ExactSizeIterator for Values<'_> {
1051    fn len(&self) -> usize {
1052        self.0.len()
1053    }
1054}
1055
1056impl DoubleEndedIterator for Values<'_> {
1057    fn next_back(&mut self) -> Option<Self::Item> {
1058        self.0.next_back().map(|(_, zval)| zval)
1059    }
1060}
1061
1062impl Default for ZBox<ZendHashTable> {
1063    fn default() -> Self {
1064        ZendHashTable::new()
1065    }
1066}
1067
1068impl Clone for ZBox<ZendHashTable> {
1069    fn clone(&self) -> Self {
1070        (**self).to_owned()
1071    }
1072}
1073
1074impl IntoZval for ZBox<ZendHashTable> {
1075    const TYPE: DataType = DataType::Array;
1076    const NULLABLE: bool = false;
1077
1078    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1079        zv.set_hashtable(self);
1080        Ok(())
1081    }
1082}
1083
1084impl<'a> FromZval<'a> for &'a ZendHashTable {
1085    const TYPE: DataType = DataType::Array;
1086
1087    fn from_zval(zval: &'a Zval) -> Option<Self> {
1088        zval.array()
1089    }
1090}
1091
1092///////////////////////////////////////////
1093// HashMap
1094///////////////////////////////////////////
1095
1096// TODO: Generalize hasher
1097#[allow(clippy::implicit_hasher)]
1098impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
1099where
1100    V: FromZval<'a>,
1101{
1102    type Error = Error;
1103
1104    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
1105        let mut hm = HashMap::with_capacity(value.len());
1106
1107        for (key, val) in value {
1108            hm.insert(
1109                key.to_string(),
1110                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
1111            );
1112        }
1113
1114        Ok(hm)
1115    }
1116}
1117
1118impl<K, V> TryFrom<HashMap<K, V>> for ZBox<ZendHashTable>
1119where
1120    K: AsRef<str>,
1121    V: IntoZval,
1122{
1123    type Error = Error;
1124
1125    fn try_from(value: HashMap<K, V>) -> Result<Self> {
1126        let mut ht = ZendHashTable::with_capacity(
1127            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
1128        );
1129
1130        for (k, v) in value {
1131            ht.insert(k.as_ref(), v)?;
1132        }
1133
1134        Ok(ht)
1135    }
1136}
1137
1138// TODO: Generalize hasher
1139#[allow(clippy::implicit_hasher)]
1140impl<K, V> IntoZval for HashMap<K, V>
1141where
1142    K: AsRef<str>,
1143    V: IntoZval,
1144{
1145    const TYPE: DataType = DataType::Array;
1146    const NULLABLE: bool = false;
1147
1148    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1149        let arr = self.try_into()?;
1150        zv.set_hashtable(arr);
1151        Ok(())
1152    }
1153}
1154
1155// TODO: Generalize hasher
1156#[allow(clippy::implicit_hasher)]
1157impl<'a, T> FromZval<'a> for HashMap<String, T>
1158where
1159    T: FromZval<'a>,
1160{
1161    const TYPE: DataType = DataType::Array;
1162
1163    fn from_zval(zval: &'a Zval) -> Option<Self> {
1164        zval.array().and_then(|arr| arr.try_into().ok())
1165    }
1166}
1167
1168///////////////////////////////////////////
1169// Vec
1170///////////////////////////////////////////
1171
1172impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1173where
1174    T: FromZval<'a>,
1175{
1176    type Error = Error;
1177
1178    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
1179        let mut vec = Vec::with_capacity(value.len());
1180
1181        for (_, val) in value {
1182            vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
1183        }
1184
1185        Ok(vec)
1186    }
1187}
1188
1189impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1190where
1191    T: IntoZval,
1192{
1193    type Error = Error;
1194
1195    fn try_from(value: Vec<T>) -> Result<Self> {
1196        let mut ht = ZendHashTable::with_capacity(
1197            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
1198        );
1199
1200        for val in value {
1201            ht.push(val)?;
1202        }
1203
1204        Ok(ht)
1205    }
1206}
1207
1208impl<T> IntoZval for Vec<T>
1209where
1210    T: IntoZval,
1211{
1212    const TYPE: DataType = DataType::Array;
1213    const NULLABLE: bool = false;
1214
1215    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
1216        let arr = self.try_into()?;
1217        zv.set_hashtable(arr);
1218        Ok(())
1219    }
1220}
1221
1222impl<'a, T> FromZval<'a> for Vec<T>
1223where
1224    T: FromZval<'a>,
1225{
1226    const TYPE: DataType = DataType::Array;
1227
1228    fn from_zval(zval: &'a Zval) -> Option<Self> {
1229        zval.array().and_then(|arr| arr.try_into().ok())
1230    }
1231}
1232
1233impl FromIterator<Zval> for ZBox<ZendHashTable> {
1234    fn from_iter<T: IntoIterator<Item = Zval>>(iter: T) -> Self {
1235        let mut ht = ZendHashTable::new();
1236        for item in iter {
1237            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1238            // `val` to a zval fails.
1239            let _ = ht.push(item);
1240        }
1241        ht
1242    }
1243}
1244
1245impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
1246    fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
1247        let mut ht = ZendHashTable::new();
1248        for (key, val) in iter {
1249            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1250            // `val` to a zval fails.
1251            let _ = ht.insert_at_index(key, val);
1252        }
1253        ht
1254    }
1255}
1256
1257impl<'a> FromIterator<(&'a str, Zval)> for ZBox<ZendHashTable> {
1258    fn from_iter<T: IntoIterator<Item = (&'a str, Zval)>>(iter: T) -> Self {
1259        let mut ht = ZendHashTable::new();
1260        for (key, val) in iter {
1261            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1262            // `val` to a zval fails.
1263            let _ = ht.insert(key, val);
1264        }
1265        ht
1266    }
1267}