• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 16353567988

17 Jul 2025 06:57PM UTC coverage: 24.606% (+2.3%) from 22.325%
16353567988

push

github

Xenira
feat(array): support `Vec<(K,V)>` for hashtables

Refs: #425

35 of 37 new or added lines in 1 file covered. (94.59%)

87 existing lines in 1 file now uncovered.

968 of 3934 relevant lines covered (24.61%)

5.59 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

26.42
/src/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

4
use 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

14
use 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
/// ```
54
pub 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)]
59
impl 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> {
9✔
75
        Self::with_capacity(HT_MIN_SIZE)
9✔
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> {
13✔
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);
39✔
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()
39✔
106
                    .expect("Failed to allocate memory for hashtable"),
13✔
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 {
22✔
127
        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
66✔
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>
12✔
194
    where
195
        K: Into<ArrayKey<'a>>,
196
    {
197
        match key.into() {
12✔
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) => {
6✔
220
                if let Ok(index) = i64::from_str(key) {
6✔
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 _)
30✔
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<()>
35✔
503
    where
504
        K: Into<ArrayKey<'a>>,
505
        V: IntoZval,
506
    {
507
        let mut val = val.into_zval(false)?;
105✔
508
        match key.into() {
×
509
            ArrayKey::Long(index) => {
14✔
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) => {
21✔
533
                if let Ok(index) = i64::from_str(key) {
26✔
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,
16✔
542
                            CString::new(key)?.as_ptr(),
32✔
543
                            key.len(),
×
544
                            &raw mut val,
×
545
                        )
546
                    };
547
                }
548
            }
549
        }
550
        val.release();
35✔
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

740
unsafe impl ZBoxable for ZendHashTable {
741
    fn free(&mut self) {
7✔
742
        // SAFETY: ZBox has immutable access to `self`.
743
        unsafe { zend_array_destroy(self) }
14✔
744
    }
745
}
746

747
impl 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

755
impl 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.
773
pub 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)]
783
pub 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

794
impl From<String> for ArrayKey<'_> {
795
    fn from(value: String) -> Self {
×
796
        Self::String(value)
×
797
    }
798
}
799

800
impl TryFrom<ArrayKey<'_>> for String {
801
    type Error = Error;
802

803
    fn try_from(value: ArrayKey<'_>) -> std::result::Result<Self, Self::Error> {
12✔
804
        match value {
12✔
805
            ArrayKey::String(s) => Ok(s),
16✔
806
            ArrayKey::Str(s) => Ok(s.to_string()),
2✔
807
            ArrayKey::Long(_) => Err(Error::InvalidProperty),
3✔
808
        }
809
    }
810
}
811

812
impl TryFrom<ArrayKey<'_>> for i64 {
813
    type Error = Error;
814

815
    fn try_from(value: ArrayKey<'_>) -> std::result::Result<Self, Self::Error> {
11✔
816
        match value {
11✔
817
            ArrayKey::Long(i) => Ok(i),
6✔
818
            ArrayKey::String(s) => s.parse::<i64>().map_err(|_| Error::InvalidProperty),
9✔
819
            ArrayKey::Str(s) => s.parse::<i64>().map_err(|_| Error::InvalidProperty),
8✔
820
        }
821
    }
822
}
823

824
impl ArrayKey<'_> {
825
    /// Check if the key is an integer.
826
    ///
827
    /// # Returns
828
    ///
829
    /// Returns true if the key is an integer, false otherwise.
830
    #[must_use]
UNCOV
831
    pub fn is_long(&self) -> bool {
×
832
        match self {
×
833
            ArrayKey::Long(_) => true,
×
UNCOV
834
            ArrayKey::String(_) | ArrayKey::Str(_) => false,
×
835
        }
836
    }
837
}
838

839
impl Display for ArrayKey<'_> {
840
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
841
        match self {
×
842
            ArrayKey::Long(key) => write!(f, "{key}"),
×
UNCOV
843
            ArrayKey::String(key) => write!(f, "{key}"),
×
844
            ArrayKey::Str(key) => write!(f, "{key}"),
×
845
        }
846
    }
847
}
848

849
impl<'a> From<&'a str> for ArrayKey<'a> {
850
    fn from(key: &'a str) -> ArrayKey<'a> {
27✔
851
        ArrayKey::Str(key)
27✔
852
    }
853
}
854

855
impl<'a> From<i64> for ArrayKey<'a> {
856
    fn from(index: i64) -> ArrayKey<'a> {
20✔
857
        ArrayKey::Long(index)
20✔
858
    }
859
}
860

861
impl<'a> FromZval<'a> for ArrayKey<'_> {
862
    const TYPE: DataType = DataType::String;
863

864
    fn from_zval(zval: &'a Zval) -> Option<Self> {
20✔
865
        if let Some(key) = zval.long() {
31✔
UNCOV
866
            return Some(ArrayKey::Long(key));
×
867
        }
868
        if let Some(key) = zval.string() {
18✔
UNCOV
869
            return Some(ArrayKey::String(key));
×
870
        }
UNCOV
871
        None
×
872
    }
873
}
874

875
impl<'a> Iter<'a> {
876
    /// Creates a new iterator over a hashtable.
877
    ///
878
    /// # Parameters
879
    ///
880
    /// * `ht` - The hashtable to iterate.
881
    pub fn new(ht: &'a ZendHashTable) -> Self {
9✔
882
        let end_num: i64 = ht
27✔
883
            .len()
884
            .try_into()
885
            .expect("Integer overflow in hashtable length");
886
        let end_pos = if ht.nNumOfElements > 0 {
18✔
887
            ht.nNumOfElements - 1
9✔
888
        } else {
UNCOV
889
            0
×
890
        };
891

892
        Self {
893
            ht,
894
            current_num: 0,
895
            end_num,
896
            pos: 0,
897
            end_pos,
898
        }
899
    }
900
}
901

902
impl<'a> IntoIterator for &'a ZendHashTable {
903
    type Item = (ArrayKey<'a>, &'a Zval);
904
    type IntoIter = Iter<'a>;
905

906
    /// Returns an iterator over the key(s) and value contained inside the
907
    /// hashtable.
908
    ///
909
    /// # Example
910
    ///
911
    /// ```no_run
912
    /// use ext_php_rs::types::ZendHashTable;
913
    ///
914
    /// let mut ht = ZendHashTable::new();
915
    ///
916
    /// for (key, val) in ht.iter() {
917
    /// //   ^ Index if inserted at an index.
918
    /// //        ^ Optional string key, if inserted like a hashtable.
919
    /// //             ^ Inserted value.
920
    ///
921
    ///     dbg!(key, val);
922
    /// }
923
    #[inline]
924
    fn into_iter(self) -> Self::IntoIter {
9✔
925
        Iter::new(self)
18✔
926
    }
927
}
928

929
impl<'a> Iterator for Iter<'a> {
930
    type Item = (ArrayKey<'a>, &'a Zval);
931

932
    fn next(&mut self) -> Option<Self::Item> {
26✔
933
        self.next_zval()
52✔
934
            .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
126✔
935
    }
936

937
    fn count(self) -> usize
×
938
    where
939
        Self: Sized,
940
    {
941
        self.ht.len()
×
942
    }
943
}
944

945
impl ExactSizeIterator for Iter<'_> {
946
    fn len(&self) -> usize {
×
947
        self.ht.len()
×
948
    }
949
}
950

951
impl DoubleEndedIterator for Iter<'_> {
952
    fn next_back(&mut self) -> Option<Self::Item> {
×
953
        if self.end_num <= self.current_num {
×
UNCOV
954
            return None;
×
955
        }
956

957
        let key_type = unsafe {
958
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
959
        };
960

UNCOV
961
        if key_type == -1 {
×
UNCOV
962
            return None;
×
963
        }
964

965
        let key = Zval::new();
×
966

967
        unsafe {
968
            zend_hash_get_current_key_zval_ex(
UNCOV
969
                ptr::from_ref(self.ht).cast_mut(),
×
UNCOV
970
                (&raw const key).cast_mut(),
×
UNCOV
971
                &raw mut self.end_pos,
×
972
            );
973
        }
974
        let value = unsafe {
UNCOV
975
            &*zend_hash_get_current_data_ex(
×
UNCOV
976
                ptr::from_ref(self.ht).cast_mut(),
×
UNCOV
977
                &raw mut self.end_pos,
×
978
            )
979
        };
980

UNCOV
981
        let key = match ArrayKey::from_zval(&key) {
×
UNCOV
982
            Some(key) => key,
×
UNCOV
983
            None => ArrayKey::Long(self.end_num),
×
984
        };
985

986
        unsafe {
UNCOV
987
            zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos)
×
988
        };
989
        self.end_num -= 1;
×
990

UNCOV
991
        Some((key, value))
×
992
    }
993
}
994

995
impl<'a> Iter<'a> {
996
    pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> {
26✔
997
        if self.current_num >= self.end_num {
26✔
998
            return None;
6✔
999
        }
1000

1001
        let key_type = unsafe {
1002
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
1003
        };
1004

1005
        // Key type `-1` is ???
1006
        // Key type `1` is string
1007
        // Key type `2` is long
1008
        // Key type `3` is null meaning the end of the array
1009
        if key_type == -1 || key_type == 3 {
20✔
1010
            return None;
×
1011
        }
1012

1013
        let mut key = Zval::new();
×
1014

1015
        unsafe {
1016
            zend_hash_get_current_key_zval_ex(
UNCOV
1017
                ptr::from_ref(self.ht).cast_mut(),
×
UNCOV
1018
                (&raw const key).cast_mut(),
×
UNCOV
1019
                &raw mut self.pos,
×
1020
            );
1021
        }
1022
        let value = unsafe {
UNCOV
1023
            let val_ptr =
×
UNCOV
1024
                zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos);
×
1025

UNCOV
1026
            if val_ptr.is_null() {
×
UNCOV
1027
                return None;
×
1028
            }
1029

1030
            &*val_ptr
×
1031
        };
1032

1033
        if !key.is_long() && !key.is_string() {
9✔
UNCOV
1034
            key.set_long(self.current_num);
×
1035
        }
1036

UNCOV
1037
        unsafe { zend_hash_move_forward_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) };
×
1038
        self.current_num += 1;
×
1039

UNCOV
1040
        Some((key, value))
×
1041
    }
1042
}
1043

1044
/// Immutable iterator which iterates over the values of the hashtable, as it
1045
/// was a set or list.
1046
pub struct Values<'a>(Iter<'a>);
1047

1048
impl<'a> Values<'a> {
1049
    /// Creates a new iterator over a hashtables values.
1050
    ///
1051
    /// # Parameters
1052
    ///
1053
    /// * `ht` - The hashtable to iterate.
UNCOV
1054
    pub fn new(ht: &'a ZendHashTable) -> Self {
×
UNCOV
1055
        Self(Iter::new(ht))
×
1056
    }
1057
}
1058

1059
impl<'a> Iterator for Values<'a> {
1060
    type Item = &'a Zval;
1061

UNCOV
1062
    fn next(&mut self) -> Option<Self::Item> {
×
1063
        self.0.next().map(|(_, zval)| zval)
×
1064
    }
1065

UNCOV
1066
    fn count(self) -> usize
×
1067
    where
1068
        Self: Sized,
1069
    {
1070
        self.0.count()
×
1071
    }
1072
}
1073

1074
impl ExactSizeIterator for Values<'_> {
UNCOV
1075
    fn len(&self) -> usize {
×
UNCOV
1076
        self.0.len()
×
1077
    }
1078
}
1079

1080
impl DoubleEndedIterator for Values<'_> {
UNCOV
1081
    fn next_back(&mut self) -> Option<Self::Item> {
×
UNCOV
1082
        self.0.next_back().map(|(_, zval)| zval)
×
1083
    }
1084
}
1085

1086
impl Default for ZBox<ZendHashTable> {
1087
    fn default() -> Self {
×
1088
        ZendHashTable::new()
×
1089
    }
1090
}
1091

1092
impl Clone for ZBox<ZendHashTable> {
UNCOV
1093
    fn clone(&self) -> Self {
×
UNCOV
1094
        (**self).to_owned()
×
1095
    }
1096
}
1097

1098
impl IntoZval for ZBox<ZendHashTable> {
1099
    const TYPE: DataType = DataType::Array;
1100
    const NULLABLE: bool = false;
1101

UNCOV
1102
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
1103
        zv.set_hashtable(self);
×
1104
        Ok(())
×
1105
    }
1106
}
1107

1108
impl<'a> FromZval<'a> for &'a ZendHashTable {
1109
    const TYPE: DataType = DataType::Array;
1110

UNCOV
1111
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
1112
        zval.array()
×
1113
    }
1114
}
1115

1116
///////////////////////////////////////////
1117
// HashMap
1118
///////////////////////////////////////////
1119

1120
// TODO: Generalize hasher
1121
#[allow(clippy::implicit_hasher)]
1122
impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
1123
where
1124
    V: FromZval<'a>,
1125
{
1126
    type Error = Error;
1127

UNCOV
1128
    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
×
UNCOV
1129
        let mut hm = HashMap::with_capacity(value.len());
×
1130

1131
        for (key, val) in value {
×
UNCOV
1132
            hm.insert(
×
UNCOV
1133
                key.to_string(),
×
1134
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
×
1135
            );
1136
        }
1137

UNCOV
1138
        Ok(hm)
×
1139
    }
1140
}
1141

1142
impl<K, V> TryFrom<HashMap<K, V>> for ZBox<ZendHashTable>
1143
where
1144
    K: AsRef<str>,
1145
    V: IntoZval,
1146
{
1147
    type Error = Error;
1148

1149
    fn try_from(value: HashMap<K, V>) -> Result<Self> {
×
1150
        let mut ht = ZendHashTable::with_capacity(
1151
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1152
        );
1153

UNCOV
1154
        for (k, v) in value {
×
UNCOV
1155
            ht.insert(k.as_ref(), v)?;
×
1156
        }
1157

UNCOV
1158
        Ok(ht)
×
1159
    }
1160
}
1161

1162
impl<'a, K, V> TryFrom<&'a ZendHashTable> for Vec<(K, V)>
1163
where
1164
    K: TryFrom<ArrayKey<'a>, Error = Error>,
1165
    V: FromZval<'a>,
1166
{
1167
    type Error = Error;
1168

1169
    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
7✔
1170
        let mut vec = Vec::with_capacity(value.len());
28✔
1171

1172
        for (key, val) in value {
46✔
1173
            vec.push((
25✔
1174
                key.try_into()?,
31✔
1175
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
11✔
1176
            ));
1177
        }
1178

1179
        Ok(vec)
4✔
1180
    }
1181
}
1182

1183
impl<'a, V> TryFrom<&'a ZendHashTable> for Vec<(ArrayKey<'a>, V)>
1184
where
1185
    V: FromZval<'a>,
1186
{
1187
    type Error = Error;
1188

1189
    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
2✔
1190
        let mut vec = Vec::with_capacity(value.len());
8✔
1191

1192
        for (key, val) in value {
20✔
1193
            vec.push((
12✔
1194
                key,
6✔
1195
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
18✔
1196
            ));
1197
        }
1198

1199
        Ok(vec)
2✔
1200
    }
1201
}
1202

1203
impl<'a, K, V> TryFrom<Vec<(K, V)>> for ZBox<ZendHashTable>
1204
where
1205
    K: Into<ArrayKey<'a>>,
1206
    V: IntoZval,
1207
{
1208
    type Error = Error;
1209

1210
    fn try_from(value: Vec<(K, V)>) -> Result<Self> {
4✔
1211
        let mut ht = ZendHashTable::with_capacity(
1212
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
16✔
1213
        );
1214

1215
        for (k, v) in value {
40✔
1216
            ht.insert(k, v)?;
36✔
1217
        }
1218

1219
        Ok(ht)
4✔
1220
    }
1221
}
1222

1223
// TODO: Generalize hasher
1224
#[allow(clippy::implicit_hasher)]
1225
impl<K, V> IntoZval for HashMap<K, V>
1226
where
1227
    K: AsRef<str>,
1228
    V: IntoZval,
1229
{
1230
    const TYPE: DataType = DataType::Array;
1231
    const NULLABLE: bool = false;
1232

UNCOV
1233
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1234
        let arr = self.try_into()?;
×
1235
        zv.set_hashtable(arr);
×
1236
        Ok(())
×
1237
    }
1238
}
1239

1240
// TODO: Generalize hasher
1241
#[allow(clippy::implicit_hasher)]
1242
impl<'a, T> FromZval<'a> for HashMap<String, T>
1243
where
1244
    T: FromZval<'a>,
1245
{
1246
    const TYPE: DataType = DataType::Array;
1247

1248
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
1249
        zval.array().and_then(|arr| arr.try_into().ok())
×
1250
    }
1251
}
1252

1253
impl<'a, K, V> IntoZval for Vec<(K, V)>
1254
where
1255
    K: Into<ArrayKey<'a>>,
1256
    V: IntoZval,
1257
{
1258
    const TYPE: DataType = DataType::Array;
1259
    const NULLABLE: bool = false;
1260

1261
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
2✔
1262
        let arr = self.try_into()?;
6✔
NEW
1263
        zv.set_hashtable(arr);
×
NEW
UNCOV
1264
        Ok(())
×
1265
    }
1266
}
1267

1268
impl<'a, K, V> FromZval<'a> for Vec<(K, V)>
1269
where
1270
    K: TryFrom<ArrayKey<'a>, Error = Error>,
1271
    V: FromZval<'a>,
1272
{
1273
    const TYPE: DataType = DataType::Array;
1274

1275
    fn from_zval(zval: &'a Zval) -> Option<Self> {
3✔
1276
        zval.array().and_then(|arr| arr.try_into().ok())
18✔
1277
    }
1278
}
1279

1280
impl<'a, V> FromZval<'a> for Vec<(ArrayKey<'a>, V)>
1281
where
1282
    V: FromZval<'a>,
1283
{
1284
    const TYPE: DataType = DataType::Array;
1285

1286
    fn from_zval(zval: &'a Zval) -> Option<Self> {
1✔
1287
        zval.array().and_then(|arr| arr.try_into().ok())
6✔
1288
    }
1289
}
1290

1291
///////////////////////////////////////////
1292
// Vec
1293
///////////////////////////////////////////
1294

1295
impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1296
where
1297
    T: FromZval<'a>,
1298
{
1299
    type Error = Error;
1300

UNCOV
1301
    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
×
UNCOV
1302
        let mut vec = Vec::with_capacity(value.len());
×
1303

UNCOV
1304
        for (_, val) in value {
×
UNCOV
1305
            vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
×
1306
        }
1307

UNCOV
1308
        Ok(vec)
×
1309
    }
1310
}
1311

1312
impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1313
where
1314
    T: IntoZval,
1315
{
1316
    type Error = Error;
1317

UNCOV
1318
    fn try_from(value: Vec<T>) -> Result<Self> {
×
1319
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1320
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1321
        );
1322

UNCOV
1323
        for val in value {
×
UNCOV
1324
            ht.push(val)?;
×
1325
        }
1326

UNCOV
1327
        Ok(ht)
×
1328
    }
1329
}
1330

1331
impl<T> IntoZval for Vec<T>
1332
where
1333
    T: IntoZval,
1334
{
1335
    const TYPE: DataType = DataType::Array;
1336
    const NULLABLE: bool = false;
1337

UNCOV
1338
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
1339
        let arr = self.try_into()?;
×
UNCOV
1340
        zv.set_hashtable(arr);
×
UNCOV
1341
        Ok(())
×
1342
    }
1343
}
1344

1345
impl<'a, T> FromZval<'a> for Vec<T>
1346
where
1347
    T: FromZval<'a>,
1348
{
1349
    const TYPE: DataType = DataType::Array;
1350

UNCOV
1351
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
1352
        zval.array().and_then(|arr| arr.try_into().ok())
×
1353
    }
1354
}
1355

1356
impl FromIterator<Zval> for ZBox<ZendHashTable> {
UNCOV
1357
    fn from_iter<T: IntoIterator<Item = Zval>>(iter: T) -> Self {
×
UNCOV
1358
        let mut ht = ZendHashTable::new();
×
UNCOV
1359
        for item in iter {
×
1360
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1361
            // `val` to a zval fails.
UNCOV
1362
            let _ = ht.push(item);
×
1363
        }
UNCOV
1364
        ht
×
1365
    }
1366
}
1367

1368
impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
UNCOV
1369
    fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
×
UNCOV
1370
        let mut ht = ZendHashTable::new();
×
UNCOV
1371
        for (key, val) in iter {
×
1372
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1373
            // `val` to a zval fails.
UNCOV
1374
            let _ = ht.insert_at_index(key, val);
×
1375
        }
UNCOV
1376
        ht
×
1377
    }
1378
}
1379

1380
impl<'a> FromIterator<(&'a str, Zval)> for ZBox<ZendHashTable> {
UNCOV
1381
    fn from_iter<T: IntoIterator<Item = (&'a str, Zval)>>(iter: T) -> Self {
×
UNCOV
1382
        let mut ht = ZendHashTable::new();
×
UNCOV
1383
        for (key, val) in iter {
×
1384
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1385
            // `val` to a zval fails.
UNCOV
1386
            let _ = ht.insert(key, val);
×
1387
        }
UNCOV
1388
        ht
×
1389
    }
1390
}
1391

1392
#[cfg(test)]
1393
#[cfg(feature = "embed")]
1394
#[allow(clippy::unwrap_used)]
1395
mod tests {
1396
    use super::*;
1397
    use crate::embed::Embed;
1398

1399
    #[test]
1400
    fn test_string_try_from_array_key() {
1401
        let key = ArrayKey::String("test".to_string());
1402
        let result: Result<String, _> = key.try_into();
1403
        assert!(result.is_ok());
1404
        assert_eq!(result.unwrap(), "test".to_string());
1405

1406
        let key = ArrayKey::Str("test");
1407
        let result: Result<String, _> = key.try_into();
1408
        assert!(result.is_ok());
1409
        assert_eq!(result.unwrap(), "test".to_string());
1410

1411
        let key = ArrayKey::Long(42);
1412
        let result: Result<String, _> = key.try_into();
1413
        assert!(result.is_err());
1414
        assert!(matches!(result.unwrap_err(), Error::InvalidProperty));
1415

1416
        let key = ArrayKey::String("42".to_string());
1417
        let result: Result<String, _> = key.try_into();
1418
        assert!(result.is_ok());
1419
        assert_eq!(result.unwrap(), "42".to_string());
1420

1421
        let key = ArrayKey::Str("123");
1422
        let result: Result<i64, _> = key.try_into();
1423
        assert!(result.is_ok());
1424
        assert_eq!(result.unwrap(), 123);
1425
    }
1426

1427
    #[test]
1428
    fn test_i64_try_from_array_key() {
1429
        let key = ArrayKey::Long(42);
1430
        let result: Result<i64, _> = key.try_into();
1431
        assert!(result.is_ok());
1432
        assert_eq!(result.unwrap(), 42);
1433

1434
        let key = ArrayKey::String("42".to_string());
1435
        let result: Result<i64, _> = key.try_into();
1436
        assert!(result.is_ok());
1437
        assert_eq!(result.unwrap(), 42);
1438

1439
        let key = ArrayKey::Str("123");
1440
        let result: Result<i64, _> = key.try_into();
1441
        assert!(result.is_ok());
1442
        assert_eq!(result.unwrap(), 123);
1443

1444
        let key = ArrayKey::String("not a number".to_string());
1445
        let result: Result<i64, _> = key.try_into();
1446
        assert!(result.is_err());
1447
        assert!(matches!(result.unwrap_err(), Error::InvalidProperty));
1448
    }
1449

1450
    #[test]
1451
    fn test_vec_string_v_try_from_hash_table() {
1452
        Embed::run(|| {
1453
            let mut ht = ZendHashTable::new();
1454
            ht.insert("key1", "value1").unwrap();
1455
            ht.insert("key2", "value2").unwrap();
1456

1457
            let vec: Vec<(String, String)> = ht.as_ref().try_into().unwrap();
1458
            assert_eq!(vec.len(), 2);
1459
            assert_eq!(vec[0].0, "key1");
1460
            assert_eq!(vec[0].1, "value1");
1461
            assert_eq!(vec[1].0, "key2");
1462
            assert_eq!(vec[1].1, "value2");
1463

1464
            let mut ht2 = ZendHashTable::new();
1465
            ht2.insert(1, "value1").unwrap();
1466
            ht2.insert(2, "value2").unwrap();
1467

1468
            let vec2: Result<Vec<(String, String)>> = ht2.as_ref().try_into();
1469
            assert!(vec2.is_err());
1470
            assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty));
1471
        });
1472
    }
1473

1474
    #[test]
1475
    fn test_vec_i64_v_try_from_hash_table() {
1476
        Embed::run(|| {
1477
            let mut ht = ZendHashTable::new();
1478
            ht.insert(1, "value1").unwrap();
1479
            ht.insert("2", "value2").unwrap();
1480

1481
            let vec: Vec<(i64, String)> = ht.as_ref().try_into().unwrap();
1482
            assert_eq!(vec.len(), 2);
1483
            assert_eq!(vec[0].0, 1);
1484
            assert_eq!(vec[0].1, "value1");
1485
            assert_eq!(vec[1].0, 2);
1486
            assert_eq!(vec[1].1, "value2");
1487

1488
            let mut ht2 = ZendHashTable::new();
1489
            ht2.insert("key1", "value1").unwrap();
1490
            ht2.insert("key2", "value2").unwrap();
1491

1492
            let vec2: Result<Vec<(i64, String)>> = ht2.as_ref().try_into();
1493
            assert!(vec2.is_err());
1494
            assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty));
1495
        });
1496
    }
1497

1498
    #[test]
1499
    fn test_vec_array_key_v_try_from_hash_table() {
1500
        Embed::run(|| {
1501
            let mut ht = ZendHashTable::new();
1502
            ht.insert("key1", "value1").unwrap();
1503
            ht.insert(2, "value2").unwrap();
1504
            ht.insert("3", "value3").unwrap();
1505

1506
            let vec: Vec<(ArrayKey, String)> = ht.as_ref().try_into().unwrap();
1507
            assert_eq!(vec.len(), 3);
1508
            assert_eq!(vec[0].0, ArrayKey::String("key1".to_string()));
1509
            assert_eq!(vec[0].1, "value1");
1510
            assert_eq!(vec[1].0, ArrayKey::Long(2));
1511
            assert_eq!(vec[1].1, "value2");
1512
            assert_eq!(vec[2].0, ArrayKey::Long(3));
1513
            assert_eq!(vec[2].1, "value3");
1514
        });
1515
    }
1516

1517
    #[test]
1518
    fn test_hash_table_try_from_vec() {
1519
        Embed::run(|| {
1520
            let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")];
1521

1522
            let ht: ZBox<ZendHashTable> = vec.try_into().unwrap();
1523
            assert_eq!(ht.len(), 3);
1524
            assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1");
1525
            assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2");
1526
            assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3");
1527

1528
            let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")];
1529

1530
            let ht_i64: ZBox<ZendHashTable> = vec_i64.try_into().unwrap();
1531
            assert_eq!(ht_i64.len(), 3);
1532
            assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1");
1533
            assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2");
1534
            assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3");
1535
        });
1536
    }
1537

1538
    #[test]
1539
    fn test_vec_k_v_into_zval() {
1540
        Embed::run(|| {
1541
            let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")];
1542

1543
            let zval = vec.into_zval(false).unwrap();
1544
            assert!(zval.is_array());
1545
            let ht: &ZendHashTable = zval.array().unwrap();
1546
            assert_eq!(ht.len(), 3);
1547
            assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1");
1548
            assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2");
1549
            assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3");
1550

1551
            let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")];
1552
            let zval_i64 = vec_i64.into_zval(false).unwrap();
1553
            assert!(zval_i64.is_array());
1554
            let ht_i64: &ZendHashTable = zval_i64.array().unwrap();
1555
            assert_eq!(ht_i64.len(), 3);
1556
            assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1");
1557
            assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2");
1558
            assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3");
1559
        });
1560
    }
1561

1562
    #[test]
1563
    fn test_vec_k_v_from_zval() {
1564
        Embed::run(|| {
1565
            let mut ht = ZendHashTable::new();
1566
            ht.insert("key1", "value1").unwrap();
1567
            ht.insert("key2", "value2").unwrap();
1568
            ht.insert("key3", "value3").unwrap();
1569
            let mut zval = Zval::new();
1570
            zval.set_hashtable(ht);
1571

1572
            let vec: Vec<(String, String)> = Vec::<(String, String)>::from_zval(&zval).unwrap();
1573
            assert_eq!(vec.len(), 3);
1574
            assert_eq!(vec[0].0, "key1");
1575
            assert_eq!(vec[0].1, "value1");
1576
            assert_eq!(vec[1].0, "key2");
1577
            assert_eq!(vec[1].1, "value2");
1578
            assert_eq!(vec[2].0, "key3");
1579
            assert_eq!(vec[2].1, "value3");
1580

1581
            let mut ht_i64 = ZendHashTable::new();
1582
            ht_i64.insert(1, "value1").unwrap();
1583
            ht_i64.insert("2", "value2").unwrap();
1584
            ht_i64.insert(3, "value3").unwrap();
1585
            let mut zval_i64 = Zval::new();
1586
            zval_i64.set_hashtable(ht_i64);
1587

1588
            let vec_i64: Vec<(i64, String)> = Vec::<(i64, String)>::from_zval(&zval_i64).unwrap();
1589
            assert_eq!(vec_i64.len(), 3);
1590
            assert_eq!(vec_i64[0].0, 1);
1591
            assert_eq!(vec_i64[0].1, "value1");
1592
            assert_eq!(vec_i64[1].0, 2);
1593
            assert_eq!(vec_i64[1].1, "value2");
1594
            assert_eq!(vec_i64[2].0, 3);
1595
            assert_eq!(vec_i64[2].1, "value3");
1596

1597
            let mut ht_mixed = ZendHashTable::new();
1598
            ht_mixed.insert("key1", "value1").unwrap();
1599
            ht_mixed.insert(2, "value2").unwrap();
1600
            ht_mixed.insert("3", "value3").unwrap();
1601
            let mut zval_mixed = Zval::new();
1602
            zval_mixed.set_hashtable(ht_mixed);
1603

1604
            let vec_mixed: Option<Vec<(String, String)>> =
1605
                Vec::<(String, String)>::from_zval(&zval_mixed);
1606
            assert!(vec_mixed.is_none());
1607
        });
1608
    }
1609

1610
    #[test]
1611
    fn test_vec_array_key_v_from_zval() {
1612
        Embed::run(|| {
1613
            let mut ht = ZendHashTable::new();
1614
            ht.insert("key1", "value1").unwrap();
1615
            ht.insert(2, "value2").unwrap();
1616
            ht.insert("3", "value3").unwrap();
1617
            let mut zval = Zval::new();
1618
            zval.set_hashtable(ht);
1619

1620
            let vec: Vec<(ArrayKey, String)> = Vec::<(ArrayKey, String)>::from_zval(&zval).unwrap();
1621
            assert_eq!(vec.len(), 3);
1622
            assert_eq!(vec[0].0, ArrayKey::String("key1".to_string()));
1623
            assert_eq!(vec[0].1, "value1");
1624
            assert_eq!(vec[1].0, ArrayKey::Long(2));
1625
            assert_eq!(vec[1].1, "value2");
1626
            assert_eq!(vec[2].0, ArrayKey::Long(3));
1627
            assert_eq!(vec[2].1, "value3");
1628
        });
1629
    }
1630
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc