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

davidcole1340 / ext-php-rs / 16317721183

16 Jul 2025 11:03AM UTC coverage: 22.2% (-0.1%) from 22.325%
16317721183

Pull #520

github

web-flow
Merge 6c86b4975 into 1166e2910
Pull Request #520: Zval::null() and array btreemap conversion

0 of 22 new or added lines in 2 files covered. (0.0%)

276 existing lines in 2 files now uncovered.

870 of 3919 relevant lines covered (22.2%)

3.63 hits per line

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

0.0
/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
use std::collections::BTreeMap;
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> {
×
UNCOV
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]
UNCOV
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)]
UNCOV
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()
×
UNCOV
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 {
×
UNCOV
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 {
×
UNCOV
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) {
×
UNCOV
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]
UNCOV
193
    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
×
194
    where
195
        K: Into<ArrayKey<'a>>,
196
    {
UNCOV
197
        match key.into() {
×
198
            ArrayKey::Long(index) => unsafe {
199
                #[allow(clippy::cast_sign_loss)]
UNCOV
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()) {
×
UNCOV
204
                    #[allow(clippy::cast_sign_loss)]
×
205
                    unsafe {
UNCOV
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(),
×
UNCOV
213
                            key.len() as _,
×
214
                        )
215
                        .as_ref()
216
                    }
217
                }
218
            }
219
            ArrayKey::Str(key) => {
×
220
                if let Ok(index) = i64::from_str(key) {
×
UNCOV
221
                    #[allow(clippy::cast_sign_loss)]
×
222
                    unsafe {
UNCOV
223
                        zend_hash_index_find(self, index as zend_ulong).as_ref()
×
224
                    }
225
                } else {
226
                    unsafe {
UNCOV
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]
UNCOV
261
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
262
    where
263
        K: Into<ArrayKey<'a>>,
264
    {
UNCOV
265
        match key.into() {
×
266
            ArrayKey::Long(index) => unsafe {
267
                #[allow(clippy::cast_sign_loss)]
UNCOV
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()) {
×
UNCOV
272
                    #[allow(clippy::cast_sign_loss)]
×
273
                    unsafe {
UNCOV
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(),
×
UNCOV
281
                            key.len() as _,
×
282
                        )
283
                        .as_mut()
284
                    }
285
                }
286
            }
287
            ArrayKey::Str(key) => {
×
288
                if let Ok(index) = i64::from_str(key) {
×
UNCOV
289
                    #[allow(clippy::cast_sign_loss)]
×
290
                    unsafe {
UNCOV
291
                        zend_hash_index_find(self, index as zend_ulong).as_mut()
×
292
                    }
293
                } else {
294
                    unsafe {
UNCOV
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]
UNCOV
326
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
×
327
        #[allow(clippy::cast_sign_loss)]
328
        unsafe {
UNCOV
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]
UNCOV
359
    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
×
360
        unsafe {
361
            #[allow(clippy::cast_sign_loss)]
UNCOV
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
    /// ```
UNCOV
390
    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
×
391
    where
392
        K: Into<ArrayKey<'a>>,
393
    {
UNCOV
394
        let result = match key.into() {
×
395
            ArrayKey::Long(index) => unsafe {
396
                #[allow(clippy::cast_sign_loss)]
UNCOV
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()) {
×
UNCOV
401
                    #[allow(clippy::cast_sign_loss)]
×
402
                    unsafe {
UNCOV
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(),
×
UNCOV
410
                            key.len() as _,
×
411
                        )
412
                    }
413
                }
414
            }
415
            ArrayKey::Str(key) => {
×
416
                if let Ok(index) = i64::from_str(key) {
×
UNCOV
417
                    #[allow(clippy::cast_sign_loss)]
×
418
                    unsafe {
UNCOV
419
                        zend_hash_index_del(self, index as zend_ulong)
×
420
                    }
421
                } else {
422
                    unsafe {
UNCOV
423
                        zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
×
424
                    }
425
                }
426
            }
427
        };
428

429
        if result < 0 {
×
UNCOV
430
            None
×
431
        } else {
UNCOV
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
    /// ```
UNCOV
460
    pub fn remove_index(&mut self, key: i64) -> Option<()> {
×
461
        let result = unsafe {
462
            #[allow(clippy::cast_sign_loss)]
UNCOV
463
            zend_hash_index_del(self, key as zend_ulong)
×
464
        };
465

466
        if result < 0 {
×
UNCOV
467
            None
×
468
        } else {
UNCOV
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
    /// ```
UNCOV
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() {
×
UNCOV
509
            ArrayKey::Long(index) => {
×
510
                unsafe {
511
                    #[allow(clippy::cast_sign_loss)]
UNCOV
512
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
×
513
                };
514
            }
515
            ArrayKey::String(key) => {
×
UNCOV
516
                if let Ok(index) = i64::from_str(&key) {
×
517
                    unsafe {
518
                        #[allow(clippy::cast_sign_loss)]
UNCOV
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(),
×
UNCOV
527
                            &raw mut val,
×
528
                        )
529
                    };
530
                }
531
            }
532
            ArrayKey::Str(key) => {
×
UNCOV
533
                if let Ok(index) = i64::from_str(key) {
×
534
                    unsafe {
535
                        #[allow(clippy::cast_sign_loss)]
UNCOV
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(),
×
UNCOV
544
                            &raw mut val,
×
545
                        )
546
                    };
547
                }
548
            }
549
        }
550
        val.release();
×
UNCOV
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
    /// ```
UNCOV
582
    pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
×
583
    where
584
        V: IntoZval,
585
    {
UNCOV
586
        let mut val = val.into_zval(false)?;
×
587
        unsafe {
588
            #[allow(clippy::cast_sign_loss)]
UNCOV
589
            zend_hash_index_update(self, key as zend_ulong, &raw mut val)
×
590
        };
591
        val.release();
×
UNCOV
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
    /// ```
UNCOV
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) };
×
UNCOV
628
        val.release();
×
629

UNCOV
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 {
×
UNCOV
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()
×
UNCOV
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<'_> {
×
UNCOV
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<'_> {
×
UNCOV
736
        self.into_iter()
×
737
    }
738
}
739

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

747
impl Debug for ZendHashTable {
748
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
749
        f.debug_map()
×
UNCOV
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

UNCOV
758
    fn to_owned(&self) -> Self::Owned {
×
759
        unsafe {
760
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
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()
×
UNCOV
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 {
×
UNCOV
796
        Self::String(value)
×
797
    }
798
}
799

800
impl 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,
×
UNCOV
810
            ArrayKey::String(_) | ArrayKey::Str(_) => false,
×
811
        }
812
    }
813
}
814

815
impl 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}"),
×
UNCOV
820
            ArrayKey::Str(key) => write!(f, "{key}"),
×
821
        }
822
    }
823
}
824

825
impl<'a> From<&'a str> for ArrayKey<'a> {
826
    fn from(key: &'a str) -> ArrayKey<'a> {
×
UNCOV
827
        ArrayKey::Str(key)
×
828
    }
829
}
830

831
impl<'a> From<i64> for ArrayKey<'a> {
832
    fn from(index: i64) -> ArrayKey<'a> {
×
UNCOV
833
        ArrayKey::Long(index)
×
834
    }
835
}
836

837
impl<'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() {
×
UNCOV
842
            return Some(ArrayKey::Long(key));
×
843
        }
844
        if let Some(key) = zval.string() {
×
UNCOV
845
            return Some(ArrayKey::String(key));
×
846
        }
UNCOV
847
        None
×
848
    }
849
}
850

851
impl<'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 {
×
UNCOV
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 {
×
UNCOV
863
            ht.nNumOfElements - 1
×
864
        } else {
UNCOV
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

878
impl<'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 {
×
UNCOV
901
        Iter::new(self)
×
902
    }
903
}
904

905
impl<'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()
×
UNCOV
910
            .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
×
911
    }
912

UNCOV
913
    fn count(self) -> usize
×
914
    where
915
        Self: Sized,
916
    {
UNCOV
917
        self.ht.len()
×
918
    }
919
}
920

921
impl ExactSizeIterator for Iter<'_> {
922
    fn len(&self) -> usize {
×
UNCOV
923
        self.ht.len()
×
924
    }
925
}
926

927
impl DoubleEndedIterator for Iter<'_> {
928
    fn next_back(&mut self) -> Option<Self::Item> {
×
929
        if self.end_num <= self.current_num {
×
UNCOV
930
            return None;
×
931
        }
932

933
        let key_type = unsafe {
UNCOV
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 {
×
UNCOV
938
            return None;
×
939
        }
940

UNCOV
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(),
×
UNCOV
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(),
×
UNCOV
953
                &raw mut self.end_pos,
×
954
            )
955
        };
956

957
        let key = match ArrayKey::from_zval(&key) {
×
958
            Some(key) => key,
×
UNCOV
959
            None => ArrayKey::Long(self.end_num),
×
960
        };
961

962
        unsafe {
UNCOV
963
            zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos)
×
964
        };
UNCOV
965
        self.end_num -= 1;
×
966

UNCOV
967
        Some((key, value))
×
968
    }
969
}
970

971
impl<'a> Iter<'a> {
972
    pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> {
×
973
        if self.current_num >= self.end_num {
×
UNCOV
974
            return None;
×
975
        }
976

977
        let key_type = unsafe {
UNCOV
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 {
×
UNCOV
986
            return None;
×
987
        }
988

UNCOV
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(),
×
UNCOV
995
                &raw mut self.pos,
×
996
            );
997
        }
998
        let value = unsafe {
999
            let val_ptr =
×
UNCOV
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() {
×
UNCOV
1003
                return None;
×
1004
            }
1005

UNCOV
1006
            &*val_ptr
×
1007
        };
1008

1009
        if !key.is_long() && !key.is_string() {
×
UNCOV
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) };
×
UNCOV
1014
        self.current_num += 1;
×
1015

UNCOV
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.
1022
pub struct Values<'a>(Iter<'a>);
1023

1024
impl<'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 {
×
UNCOV
1031
        Self(Iter::new(ht))
×
1032
    }
1033
}
1034

1035
impl<'a> Iterator for Values<'a> {
1036
    type Item = &'a Zval;
1037

1038
    fn next(&mut self) -> Option<Self::Item> {
×
UNCOV
1039
        self.0.next().map(|(_, zval)| zval)
×
1040
    }
1041

UNCOV
1042
    fn count(self) -> usize
×
1043
    where
1044
        Self: Sized,
1045
    {
UNCOV
1046
        self.0.count()
×
1047
    }
1048
}
1049

1050
impl ExactSizeIterator for Values<'_> {
1051
    fn len(&self) -> usize {
×
UNCOV
1052
        self.0.len()
×
1053
    }
1054
}
1055

1056
impl DoubleEndedIterator for Values<'_> {
1057
    fn next_back(&mut self) -> Option<Self::Item> {
×
UNCOV
1058
        self.0.next_back().map(|(_, zval)| zval)
×
1059
    }
1060
}
1061

1062
impl Default for ZBox<ZendHashTable> {
1063
    fn default() -> Self {
×
UNCOV
1064
        ZendHashTable::new()
×
1065
    }
1066
}
1067

1068
impl Clone for ZBox<ZendHashTable> {
1069
    fn clone(&self) -> Self {
×
UNCOV
1070
        (**self).to_owned()
×
1071
    }
1072
}
1073

1074
impl 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);
×
UNCOV
1080
        Ok(())
×
1081
    }
1082
}
1083

1084
impl<'a> FromZval<'a> for &'a ZendHashTable {
1085
    const TYPE: DataType = DataType::Array;
1086

1087
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
1088
        zval.array()
×
1089
    }
1090
}
1091

1092
///////////////////////////////////////////
1093
// HashMap
1094
///////////////////////////////////////////
1095

1096
// TODO: Generalize hasher
1097
#[allow(clippy::implicit_hasher)]
1098
impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
1099
where
1100
    V: FromZval<'a>,
1101
{
1102
    type Error = Error;
1103

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

1107
        for (key, val) in value {
×
1108
            hm.insert(
×
1109
                key.to_string(),
×
UNCOV
1110
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
×
1111
            );
1112
        }
1113

UNCOV
1114
        Ok(hm)
×
1115
    }
1116
}
1117

1118
impl<K, V> TryFrom<HashMap<K, V>> for ZBox<ZendHashTable>
1119
where
1120
    K: AsRef<str>,
1121
    V: IntoZval,
1122
{
1123
    type Error = Error;
1124

UNCOV
1125
    fn try_from(value: HashMap<K, V>) -> Result<Self> {
×
1126
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1127
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1128
        );
1129

1130
        for (k, v) in value {
×
UNCOV
1131
            ht.insert(k.as_ref(), v)?;
×
1132
        }
1133

UNCOV
1134
        Ok(ht)
×
1135
    }
1136
}
1137

1138
// TODO: Generalize hasher
1139
#[allow(clippy::implicit_hasher)]
1140
impl<K, V> IntoZval for HashMap<K, V>
1141
where
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);
×
UNCOV
1151
        Ok(())
×
1152
    }
1153
}
1154

1155
// TODO: Generalize hasher
1156
#[allow(clippy::implicit_hasher)]
1157
impl<'a, T> FromZval<'a> for HashMap<String, T>
1158
where
1159
    T: FromZval<'a>,
1160
{
1161
    const TYPE: DataType = DataType::Array;
1162

1163
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
1164
        zval.array().and_then(|arr| arr.try_into().ok())
×
1165
    }
1166
}
1167

1168

1169
///////////////////////////////////////////
1170
// BTreeMap
1171
///////////////////////////////////////////
1172

1173
impl<'a, V> TryFrom<&'a ZendHashTable> for BTreeMap<String, V>
1174
where
1175
    V: FromZval<'a>,
1176
{
1177
    type Error = Error;
1178

NEW
UNCOV
1179
    fn try_from(value: &'a ZendHashTable) -> Result<Self> {
×
NEW
1180
        let mut hm = BTreeMap::new();
×
1181

NEW
UNCOV
1182
        for (key, val) in value {
×
NEW
UNCOV
1183
            hm.insert(
×
NEW
1184
                key.to_string(),
×
NEW
UNCOV
1185
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
×
1186
            );
1187
        }
1188

NEW
UNCOV
1189
        Ok(hm)
×
1190
    }
1191
}
1192

1193
impl<K, V> TryFrom<BTreeMap<K, V>> for ZBox<ZendHashTable>
1194
where
1195
    K: AsRef<str>,
1196
    V: IntoZval,
1197
{
1198
    type Error = Error;
1199

NEW
1200
    fn try_from(value: BTreeMap<K, V>) -> Result<Self> {
×
1201
        let mut ht = ZendHashTable::with_capacity(
NEW
UNCOV
1202
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1203
        );
1204

NEW
UNCOV
1205
        for (k, v) in value {
×
NEW
UNCOV
1206
            ht.insert(k.as_ref(), v)?;
×
1207
        }
1208

NEW
UNCOV
1209
        Ok(ht)
×
1210
    }
1211
}
1212

1213
impl<K, V> IntoZval for BTreeMap<K, V>
1214
where
1215
    K: AsRef<str>,
1216
    V: IntoZval,
1217
{
1218
    const TYPE: DataType = DataType::Array;
1219
    const NULLABLE: bool = false;
1220

NEW
UNCOV
1221
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
NEW
UNCOV
1222
        let arr = self.try_into()?;
×
NEW
UNCOV
1223
        zv.set_hashtable(arr);
×
NEW
UNCOV
1224
        Ok(())
×
1225
    }
1226
}
1227

1228
impl<'a, T> FromZval<'a> for BTreeMap<String, T>
1229
where
1230
    T: FromZval<'a>,
1231
{
1232
    const TYPE: DataType = DataType::Array;
1233

NEW
1234
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
NEW
1235
        zval.array().and_then(|arr| arr.try_into().ok())
×
1236
    }
1237
}
1238

1239
///////////////////////////////////////////
1240
// Vec
1241
///////////////////////////////////////////
1242

1243
impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1244
where
1245
    T: FromZval<'a>,
1246
{
1247
    type Error = Error;
1248

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

1252
        for (_, val) in value {
×
UNCOV
1253
            vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
×
1254
        }
1255

UNCOV
1256
        Ok(vec)
×
1257
    }
1258
}
1259

1260
impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1261
where
1262
    T: IntoZval,
1263
{
1264
    type Error = Error;
1265

UNCOV
1266
    fn try_from(value: Vec<T>) -> Result<Self> {
×
1267
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1268
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1269
        );
1270

UNCOV
1271
        for val in value {
×
UNCOV
1272
            ht.push(val)?;
×
1273
        }
1274

UNCOV
1275
        Ok(ht)
×
1276
    }
1277
}
1278

1279
impl<T> IntoZval for Vec<T>
1280
where
1281
    T: IntoZval,
1282
{
1283
    const TYPE: DataType = DataType::Array;
1284
    const NULLABLE: bool = false;
1285

UNCOV
1286
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
UNCOV
1287
        let arr = self.try_into()?;
×
UNCOV
1288
        zv.set_hashtable(arr);
×
UNCOV
1289
        Ok(())
×
1290
    }
1291
}
1292

1293
impl<'a, T> FromZval<'a> for Vec<T>
1294
where
1295
    T: FromZval<'a>,
1296
{
1297
    const TYPE: DataType = DataType::Array;
1298

1299
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
1300
        zval.array().and_then(|arr| arr.try_into().ok())
×
1301
    }
1302
}
1303

1304
impl FromIterator<Zval> for ZBox<ZendHashTable> {
1305
    fn from_iter<T: IntoIterator<Item = Zval>>(iter: T) -> Self {
×
1306
        let mut ht = ZendHashTable::new();
×
1307
        for item in iter {
×
1308
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1309
            // `val` to a zval fails.
1310
            let _ = ht.push(item);
×
1311
        }
1312
        ht
×
1313
    }
1314
}
1315

1316
impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
1317
    fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
×
1318
        let mut ht = ZendHashTable::new();
×
1319
        for (key, val) in iter {
×
1320
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1321
            // `val` to a zval fails.
1322
            let _ = ht.insert_at_index(key, val);
×
1323
        }
1324
        ht
×
1325
    }
1326
}
1327

1328
impl<'a> FromIterator<(&'a str, Zval)> for ZBox<ZendHashTable> {
1329
    fn from_iter<T: IntoIterator<Item = (&'a str, Zval)>>(iter: T) -> Self {
×
1330
        let mut ht = ZendHashTable::new();
×
1331
        for (key, val) in iter {
×
1332
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1333
            // `val` to a zval fails.
1334
            let _ = ht.insert(key, val);
×
1335
        }
1336
        ht
×
1337
    }
1338
}
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