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

davidcole1340 / ext-php-rs / 15931807656

27 Jun 2025 05:02PM UTC coverage: 20.223% (-0.3%) from 20.545%
15931807656

Pull #456

github

web-flow
Merge 1d2794899 into 688201af6
Pull Request #456: fix(array): cast numeric keys into zend_ulong when inserting string keys

2 of 84 new or added lines in 3 files covered. (2.38%)

266 existing lines in 5 files now uncovered.

761 of 3763 relevant lines covered (20.22%)

3.5 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 crate::ffi::zend_ulong;
5
use crate::{
6
    boxed::{ZBox, ZBoxable},
7
    convert::{FromZval, IntoZval},
8
    error::{Error, Result},
9
    ffi::{
10
        _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean,
11
        zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex,
12
        zend_hash_get_current_key_zval_ex, zend_hash_index_del, zend_hash_index_find,
13
        zend_hash_index_update, zend_hash_move_backwards_ex, zend_hash_move_forward_ex,
14
        zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
15
        HashPosition, HT_MIN_SIZE,
16
    },
17
    flags::DataType,
18
    types::Zval,
19
};
20
use std::str::FromStr;
21
use std::{
22
    collections::HashMap,
23
    convert::{TryFrom, TryInto},
24
    ffi::CString,
25
    fmt::{Debug, Display},
26
    iter::FromIterator,
27
    ptr,
28
};
29

30
/// A PHP hashtable.
31
///
32
/// In PHP, arrays are represented as hashtables. This allows you to push values
33
/// onto the end of the array like a vector, while also allowing you to insert
34
/// at arbitrary string key indexes.
35
///
36
/// A PHP hashtable stores values as [`Zval`]s. This allows you to insert
37
/// different types into the same hashtable. Types must implement [`IntoZval`]
38
/// to be able to be inserted into the hashtable.
39
///
40
/// # Examples
41
///
42
/// ```no_run
43
/// use ext_php_rs::types::ZendHashTable;
44
///
45
/// let mut ht = ZendHashTable::new();
46
/// ht.push(1);
47
/// ht.push("Hello, world!");
48
/// ht.insert("Like", "Hashtable");
49
///
50
/// assert_eq!(ht.len(), 3);
51
/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(1));
52
/// ```
53
pub type ZendHashTable = crate::ffi::HashTable;
54

55
// Clippy complains about there being no `is_empty` function when implementing
56
// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
57
#[allow(clippy::len_without_is_empty)]
58
impl ZendHashTable {
59
    /// Creates a new, empty, PHP hashtable, returned inside a [`ZBox`].
60
    ///
61
    /// # Example
62
    ///
63
    /// ```no_run
64
    /// use ext_php_rs::types::ZendHashTable;
65
    ///
66
    /// let ht = ZendHashTable::new();
67
    /// ```
68
    ///
69
    /// # Panics
70
    ///
71
    /// Panics if memory for the hashtable could not be allocated.
72
    #[must_use]
UNCOV
73
    pub fn new() -> ZBox<Self> {
×
74
        Self::with_capacity(HT_MIN_SIZE)
×
75
    }
76

77
    /// Creates a new, empty, PHP hashtable with an initial size, returned
78
    /// inside a [`ZBox`].
79
    ///
80
    /// # Parameters
81
    ///
82
    /// * `size` - The size to initialize the array with.
83
    ///
84
    /// # Example
85
    ///
86
    /// ```no_run
87
    /// use ext_php_rs::types::ZendHashTable;
88
    ///
89
    /// let ht = ZendHashTable::with_capacity(10);
90
    /// ```
91
    ///
92
    /// # Panics
93
    ///
94
    /// Panics if memory for the hashtable could not be allocated.
95
    #[must_use]
UNCOV
96
    pub fn with_capacity(size: u32) -> ZBox<Self> {
×
97
        unsafe {
98
            // SAFETY: PHP allocator handles the creation of the array.
99
            #[allow(clippy::used_underscore_items)]
UNCOV
100
            let ptr = _zend_new_array(size);
×
101

102
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
103
            ZBox::from_raw(
UNCOV
104
                ptr.as_mut()
×
105
                    .expect("Failed to allocate memory for hashtable"),
×
106
            )
107
        }
108
    }
109

110
    /// Returns the current number of elements in the array.
111
    ///
112
    /// # Example
113
    ///
114
    /// ```no_run
115
    /// use ext_php_rs::types::ZendHashTable;
116
    ///
117
    /// let mut ht = ZendHashTable::new();
118
    ///
119
    /// ht.push(1);
120
    /// ht.push("Hello, world");
121
    ///
122
    /// assert_eq!(ht.len(), 2);
123
    /// ```
124
    #[must_use]
UNCOV
125
    pub fn len(&self) -> usize {
×
126
        unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
×
127
    }
128

129
    /// Returns whether the hash table is empty.
130
    ///
131
    /// # Example
132
    ///
133
    /// ```no_run
134
    /// use ext_php_rs::types::ZendHashTable;
135
    ///
136
    /// let mut ht = ZendHashTable::new();
137
    ///
138
    /// assert_eq!(ht.is_empty(), true);
139
    ///
140
    /// ht.push(1);
141
    /// ht.push("Hello, world");
142
    ///
143
    /// assert_eq!(ht.is_empty(), false);
144
    /// ```
145
    #[must_use]
UNCOV
146
    pub fn is_empty(&self) -> bool {
×
147
        self.len() == 0
×
148
    }
149

150
    /// Clears the hash table, removing all values.
151
    ///
152
    /// # Example
153
    ///
154
    /// ```no_run
155
    /// use ext_php_rs::types::ZendHashTable;
156
    ///
157
    /// let mut ht = ZendHashTable::new();
158
    ///
159
    /// ht.insert("test", "hello world");
160
    /// assert_eq!(ht.is_empty(), false);
161
    ///
162
    /// ht.clear();
163
    /// assert_eq!(ht.is_empty(), true);
164
    /// ```
UNCOV
165
    pub fn clear(&mut self) {
×
166
        unsafe { zend_hash_clean(self) }
×
167
    }
168

169
    /// Attempts to retrieve a value from the hash table with a string key.
170
    ///
171
    /// # Parameters
172
    ///
173
    /// * `key` - The key to search for in the hash table.
174
    ///
175
    /// # Returns
176
    ///
177
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
178
    ///   table.
179
    /// * `None` - No value at the given position was found.
180
    ///
181
    /// # Example
182
    ///
183
    /// ```no_run
184
    /// use ext_php_rs::types::ZendHashTable;
185
    ///
186
    /// let mut ht = ZendHashTable::new();
187
    ///
188
    /// ht.insert("test", "hello world");
189
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
190
    /// ```
191
    #[must_use]
NEW
UNCOV
192
    pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
×
193
    where
194
        K: Into<ArrayKey<'a>>,
195
    {
NEW
196
        match key.into() {
×
197
            ArrayKey::Long(index) => unsafe {
198
                #[allow(clippy::cast_sign_loss)]
NEW
199
                zend_hash_index_find(self, index as zend_ulong).as_ref()
×
200
            },
NEW
201
            ArrayKey::String(key) => {
×
NEW
202
                if let Ok(index) = i64::from_str(key.as_str()) {
×
NEW
203
                    #[allow(clippy::cast_sign_loss)]
×
204
                    unsafe {
NEW
205
                        zend_hash_index_find(self, index as zend_ulong).as_ref()
×
206
                    }
207
                } else {
208
                    unsafe {
209
                        zend_hash_str_find(
NEW
210
                            self,
×
NEW
211
                            CString::new(key.as_str()).ok()?.as_ptr(),
×
NEW
212
                            key.len() as _,
×
213
                        )
214
                        .as_ref()
215
                    }
216
                }
217
            }
NEW
218
            ArrayKey::Str(key) => {
×
NEW
219
                if let Ok(index) = i64::from_str(key) {
×
NEW
220
                    #[allow(clippy::cast_sign_loss)]
×
221
                    unsafe {
NEW
222
                        zend_hash_index_find(self, index as zend_ulong).as_ref()
×
223
                    }
224
                } else {
225
                    unsafe {
NEW
226
                        zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
×
227
                            .as_ref()
228
                    }
229
                }
230
            }
231
        }
232
    }
233

234
    /// Attempts to retrieve a value from the hash table with a string key.
235
    ///
236
    /// # Parameters
237
    ///
238
    /// * `key` - The key to search for in the hash table.
239
    ///
240
    /// # Returns
241
    ///
242
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
243
    ///   table.
244
    /// * `None` - No value at the given position was found.
245
    ///
246
    /// # Example
247
    ///
248
    /// ```no_run
249
    /// use ext_php_rs::types::ZendHashTable;
250
    ///
251
    /// let mut ht = ZendHashTable::new();
252
    ///
253
    /// ht.insert("test", "hello world");
254
    /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
255
    /// ```
256
    #[must_use]
257
    #[allow(clippy::mut_from_ref)]
NEW
258
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
259
    where
260
        K: Into<ArrayKey<'a>>,
261
    {
NEW
262
        match key.into() {
×
263
            ArrayKey::Long(index) => unsafe {
264
                #[allow(clippy::cast_sign_loss)]
NEW
265
                zend_hash_index_find(self, index as zend_ulong).as_mut()
×
266
            },
NEW
267
            ArrayKey::String(key) => {
×
NEW
268
                if let Ok(index) = i64::from_str(key.as_str()) {
×
NEW
269
                    #[allow(clippy::cast_sign_loss)]
×
270
                    unsafe {
NEW
271
                        zend_hash_index_find(self, index as zend_ulong).as_mut()
×
272
                    }
273
                } else {
274
                    unsafe {
275
                        zend_hash_str_find(
NEW
276
                            self,
×
NEW
277
                            CString::new(key.as_str()).ok()?.as_ptr(),
×
NEW
278
                            key.len() as _,
×
279
                        )
280
                        .as_mut()
281
                    }
282
                }
283
            }
NEW
284
            ArrayKey::Str(key) => {
×
NEW
285
                if let Ok(index) = i64::from_str(key) {
×
NEW
286
                    #[allow(clippy::cast_sign_loss)]
×
287
                    unsafe {
NEW
288
                        zend_hash_index_find(self, index as zend_ulong).as_mut()
×
289
                    }
290
                } else {
291
                    unsafe {
NEW
292
                        zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
×
293
                            .as_mut()
294
                    }
295
                }
296
            }
297
        }
298
    }
299

300
    /// Attempts to retrieve a value from the hash table with an index.
301
    ///
302
    /// # Parameters
303
    ///
304
    /// * `key` - The key to search for in the hash table.
305
    ///
306
    /// # Returns
307
    ///
308
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
309
    ///   table.
310
    /// * `None` - No value at the given position was found.
311
    ///
312
    /// # Example
313
    ///
314
    /// ```no_run
315
    /// use ext_php_rs::types::ZendHashTable;
316
    ///
317
    /// let mut ht = ZendHashTable::new();
318
    ///
319
    /// ht.push(100);
320
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
321
    /// ```
322
    #[must_use]
NEW
323
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
×
324
        #[allow(clippy::cast_sign_loss)]
325
        unsafe {
NEW
326
            zend_hash_index_find(self, key as zend_ulong).as_ref()
×
327
        }
328
    }
329

330
    /// Attempts to retrieve a value from the hash table with an index.
331
    ///
332
    /// # Parameters
333
    ///
334
    /// * `key` - The key to search for in the hash table.
335
    ///
336
    /// # Returns
337
    ///
338
    /// * `Some(&Zval)` - A reference to the zval at the position in the hash
339
    ///   table.
340
    /// * `None` - No value at the given position was found.
341
    ///
342
    /// # Example
343
    ///
344
    /// ```no_run
345
    /// use ext_php_rs::types::ZendHashTable;
346
    ///
347
    /// let mut ht = ZendHashTable::new();
348
    ///
349
    /// ht.push(100);
350
    /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
351
    /// ```
352
    #[must_use]
353
    #[allow(clippy::mut_from_ref)]
NEW
354
    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
×
355
        unsafe {
356
            #[allow(clippy::cast_sign_loss)]
NEW
UNCOV
357
            zend_hash_index_find(self, key as zend_ulong).as_mut()
×
358
        }
359
    }
360

361
    /// Attempts to remove a value from the hash table with a string key.
362
    ///
363
    /// # Parameters
364
    ///
365
    /// * `key` - The key to remove from the hash table.
366
    ///
367
    /// # Returns
368
    ///
369
    /// * `Some(())` - Key was successfully removed.
370
    /// * `None` - No key was removed, did not exist.
371
    ///
372
    /// # Example
373
    ///
374
    /// ```no_run
375
    /// use ext_php_rs::types::ZendHashTable;
376
    ///
377
    /// let mut ht = ZendHashTable::new();
378
    ///
379
    /// ht.insert("test", "hello world");
380
    /// assert_eq!(ht.len(), 1);
381
    ///
382
    /// ht.remove("test");
383
    /// assert_eq!(ht.len(), 0);
384
    /// ```
NEW
385
    pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
×
386
    where
387
        K: Into<ArrayKey<'a>>,
388
    {
NEW
389
        let result = match key.into() {
×
390
            ArrayKey::Long(index) => unsafe {
391
                #[allow(clippy::cast_sign_loss)]
NEW
392
                zend_hash_index_del(self, index as zend_ulong)
×
393
            },
NEW
394
            ArrayKey::String(key) => {
×
NEW
395
                if let Ok(index) = i64::from_str(key.as_str()) {
×
NEW
396
                    #[allow(clippy::cast_sign_loss)]
×
397
                    unsafe {
NEW
398
                        zend_hash_index_del(self, index as zend_ulong)
×
399
                    }
400
                } else {
401
                    unsafe {
402
                        zend_hash_str_del(
NEW
403
                            self,
×
NEW
404
                            CString::new(key.as_str()).ok()?.as_ptr(),
×
NEW
405
                            key.len() as _,
×
406
                        )
407
                    }
408
                }
409
            }
NEW
410
            ArrayKey::Str(key) => {
×
NEW
411
                if let Ok(index) = i64::from_str(key) {
×
NEW
412
                    #[allow(clippy::cast_sign_loss)]
×
413
                    unsafe {
NEW
414
                        zend_hash_index_del(self, index as zend_ulong)
×
415
                    }
416
                } else {
417
                    unsafe {
NEW
418
                        zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
×
419
                    }
420
                }
421
            }
422
        };
423

424
        if result < 0 {
×
425
            None
×
426
        } else {
427
            Some(())
×
428
        }
429
    }
430

431
    /// Attempts to remove a value from the hash table with a string key.
432
    ///
433
    /// # Parameters
434
    ///
435
    /// * `key` - The key to remove from the hash table.
436
    ///
437
    /// # Returns
438
    ///
439
    /// * `Ok(())` - Key was successfully removed.
440
    /// * `None` - No key was removed, did not exist.
441
    ///
442
    /// # Example
443
    ///
444
    /// ```no_run
445
    /// use ext_php_rs::types::ZendHashTable;
446
    ///
447
    /// let mut ht = ZendHashTable::new();
448
    ///
449
    /// ht.push("hello");
450
    /// assert_eq!(ht.len(), 1);
451
    ///
452
    /// ht.remove_index(0);
453
    /// assert_eq!(ht.len(), 0);
454
    /// ```
NEW
455
    pub fn remove_index(&mut self, key: i64) -> Option<()> {
×
456
        let result = unsafe {
457
            #[allow(clippy::cast_sign_loss)]
NEW
458
            zend_hash_index_del(self, key as zend_ulong)
×
459
        };
460

461
        if result < 0 {
×
462
            None
×
463
        } else {
464
            Some(())
×
465
        }
466
    }
467

468
    /// Attempts to insert an item into the hash table, or update if the key
469
    /// already exists. Returns nothing in a result if successful.
470
    ///
471
    /// # Parameters
472
    ///
473
    /// * `key` - The key to insert the value at in the hash table.
474
    /// * `value` - The value to insert into the hash table.
475
    ///
476
    /// # Returns
477
    ///
478
    /// Returns nothing in a result on success.
479
    ///
480
    /// # Errors
481
    ///
482
    /// Returns an error if the key could not be converted into a [`CString`],
483
    /// or converting the value into a [`Zval`] failed.
484
    ///
485
    /// # Example
486
    ///
487
    /// ```no_run
488
    /// use ext_php_rs::types::ZendHashTable;
489
    ///
490
    /// let mut ht = ZendHashTable::new();
491
    ///
492
    /// ht.insert("a", "A");
493
    /// ht.insert("b", "B");
494
    /// ht.insert("c", "C");
495
    /// assert_eq!(ht.len(), 3);
496
    /// ```
NEW
UNCOV
497
    pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
×
498
    where
499
        K: Into<ArrayKey<'a>>,
500
        V: IntoZval,
501
    {
502
        let mut val = val.into_zval(false)?;
×
NEW
503
        match key.into() {
×
NEW
504
            ArrayKey::Long(index) => {
×
505
                unsafe {
506
                    #[allow(clippy::cast_sign_loss)]
NEW
UNCOV
507
                    zend_hash_index_update(self, index as zend_ulong, &raw mut val)
×
508
                };
509
            }
NEW
510
            ArrayKey::String(key) => {
×
NEW
511
                if let Ok(index) = i64::from_str(&key) {
×
512
                    unsafe {
513
                        #[allow(clippy::cast_sign_loss)]
NEW
514
                        zend_hash_index_update(self, index as zend_ulong, &raw mut val)
×
515
                    };
516
                } else {
517
                    unsafe {
518
                        zend_hash_str_update(
NEW
519
                            self,
×
NEW
520
                            CString::new(key.as_str())?.as_ptr(),
×
NEW
521
                            key.len(),
×
NEW
522
                            &raw mut val,
×
523
                        )
524
                    };
525
                }
526
            }
NEW
527
            ArrayKey::Str(key) => {
×
NEW
528
                if let Ok(index) = i64::from_str(key) {
×
529
                    unsafe {
530
                        #[allow(clippy::cast_sign_loss)]
NEW
531
                        zend_hash_index_update(self, index as zend_ulong, &raw mut val)
×
532
                    };
533
                } else {
534
                    unsafe {
535
                        zend_hash_str_update(
NEW
536
                            self,
×
NEW
537
                            CString::new(key)?.as_ptr(),
×
NEW
538
                            key.len(),
×
NEW
539
                            &raw mut val,
×
540
                        )
541
                    };
542
                }
543
            }
544
        }
545
        val.release();
×
546
        Ok(())
×
547
    }
548

549
    /// Inserts an item into the hash table at a specified index, or updates if
550
    /// the key already exists. Returns nothing in a result if successful.
551
    ///
552
    /// # Parameters
553
    ///
554
    /// * `key` - The index at which the value should be inserted.
555
    /// * `val` - The value to insert into the hash table.
556
    ///
557
    /// # Returns
558
    ///
559
    /// Returns nothing in a result on success.
560
    ///
561
    /// # Errors
562
    ///
563
    /// Returns an error if converting the value into a [`Zval`] failed.
564
    ///
565
    /// # Example
566
    ///
567
    /// ```no_run
568
    /// use ext_php_rs::types::ZendHashTable;
569
    ///
570
    /// let mut ht = ZendHashTable::new();
571
    ///
572
    /// ht.insert_at_index(0, "A");
573
    /// ht.insert_at_index(5, "B");
574
    /// ht.insert_at_index(0, "C"); // notice overriding index 0
575
    /// assert_eq!(ht.len(), 2);
576
    /// ```
NEW
UNCOV
577
    pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
×
578
    where
579
        V: IntoZval,
580
    {
581
        let mut val = val.into_zval(false)?;
×
582
        unsafe {
583
            #[allow(clippy::cast_sign_loss)]
NEW
UNCOV
584
            zend_hash_index_update(self, key as zend_ulong, &raw mut val)
×
585
        };
UNCOV
586
        val.release();
×
587
        Ok(())
×
588
    }
589

590
    /// Pushes an item onto the end of the hash table. Returns a result
591
    /// containing nothing if the element was successfully inserted.
592
    ///
593
    /// # Parameters
594
    ///
595
    /// * `val` - The value to insert into the hash table.
596
    ///
597
    /// # Returns
598
    ///
599
    /// Returns nothing in a result on success.
600
    ///
601
    /// # Errors
602
    ///
603
    /// Returns an error if converting the value into a [`Zval`] failed.
604
    ///
605
    /// # Example
606
    ///
607
    /// ```no_run
608
    /// use ext_php_rs::types::ZendHashTable;
609
    ///
610
    /// let mut ht = ZendHashTable::new();
611
    ///
612
    /// ht.push("a");
613
    /// ht.push("b");
614
    /// ht.push("c");
615
    /// assert_eq!(ht.len(), 3);
616
    /// ```
UNCOV
617
    pub fn push<V>(&mut self, val: V) -> Result<()>
×
618
    where
619
        V: IntoZval,
620
    {
621
        let mut val = val.into_zval(false)?;
×
622
        unsafe { zend_hash_next_index_insert(self, &raw mut val) };
×
UNCOV
623
        val.release();
×
624

UNCOV
625
        Ok(())
×
626
    }
627

628
    /// Checks if the hashtable only contains numerical keys.
629
    ///
630
    /// # Returns
631
    ///
632
    /// True if all keys on the hashtable are numerical.
633
    ///
634
    /// # Example
635
    ///
636
    /// ```no_run
637
    /// use ext_php_rs::types::ZendHashTable;
638
    ///
639
    /// let mut ht = ZendHashTable::new();
640
    ///
641
    /// ht.push(0);
642
    /// ht.push(3);
643
    /// ht.push(9);
644
    /// assert!(ht.has_numerical_keys());
645
    ///
646
    /// ht.insert("obviously not numerical", 10);
647
    /// assert!(!ht.has_numerical_keys());
648
    /// ```
649
    #[must_use]
650
    pub fn has_numerical_keys(&self) -> bool {
×
UNCOV
651
        !self.into_iter().any(|(k, _)| !k.is_long())
×
652
    }
653

654
    /// Checks if the hashtable has numerical, sequential keys.
655
    ///
656
    /// # Returns
657
    ///
658
    /// True if all keys on the hashtable are numerical and are in sequential
659
    /// order (i.e. starting at 0 and not skipping any keys).
660
    ///
661
    /// # Panics
662
    ///
663
    /// Panics if the number of elements in the hashtable exceeds `i64::MAX`.
664
    ///
665
    /// # Example
666
    ///
667
    /// ```no_run
668
    /// use ext_php_rs::types::ZendHashTable;
669
    ///
670
    /// let mut ht = ZendHashTable::new();
671
    ///
672
    /// ht.push(0);
673
    /// ht.push(3);
674
    /// ht.push(9);
675
    /// assert!(ht.has_sequential_keys());
676
    ///
677
    /// ht.insert_at_index(90, 10);
678
    /// assert!(!ht.has_sequential_keys());
679
    /// ```
680
    #[must_use]
681
    pub fn has_sequential_keys(&self) -> bool {
×
682
        !self
×
683
            .into_iter()
×
684
            .enumerate()
×
UNCOV
685
            .any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
×
686
    }
687

688
    /// Returns an iterator over the values contained inside the hashtable, as
689
    /// if it was a set or list.
690
    ///
691
    /// # Example
692
    ///
693
    /// ```no_run
694
    /// use ext_php_rs::types::ZendHashTable;
695
    ///
696
    /// let mut ht = ZendHashTable::new();
697
    ///
698
    /// for val in ht.values() {
699
    ///     dbg!(val);
700
    /// }
701
    #[inline]
702
    #[must_use]
703
    pub fn values(&self) -> Values<'_> {
×
UNCOV
704
        Values::new(self)
×
705
    }
706

707
    /// Returns an iterator over the key(s) and value contained inside the
708
    /// hashtable.
709
    ///
710
    /// # Example
711
    ///
712
    /// ```no_run
713
    /// use ext_php_rs::types::{ZendHashTable, ArrayKey};
714
    ///
715
    /// let mut ht = ZendHashTable::new();
716
    ///
717
    /// for (key, val) in ht.iter() {
718
    ///     match &key {
719
    ///         ArrayKey::Long(index) => {
720
    ///         }
721
    ///         ArrayKey::String(key) => {
722
    ///         }
723
    ///         ArrayKey::Str(key) => {
724
    ///         }
725
    ///     }
726
    ///     dbg!(key, val);
727
    /// }
728
    #[inline]
729
    #[must_use]
UNCOV
730
    pub fn iter(&self) -> Iter<'_> {
×
UNCOV
731
        self.into_iter()
×
732
    }
733
}
734

735
unsafe impl ZBoxable for ZendHashTable {
UNCOV
736
    fn free(&mut self) {
×
737
        // SAFETY: ZBox has immutable access to `self`.
UNCOV
738
        unsafe { zend_array_destroy(self) }
×
739
    }
740
}
741

742
impl Debug for ZendHashTable {
743
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
744
        f.debug_map()
×
UNCOV
745
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
746
            .finish()
747
    }
748
}
749

750
impl ToOwned for ZendHashTable {
751
    type Owned = ZBox<ZendHashTable>;
752

UNCOV
753
    fn to_owned(&self) -> Self::Owned {
×
754
        unsafe {
755
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
756
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
757

758
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
759
            ZBox::from_raw(
760
                ptr.as_mut()
×
UNCOV
761
                    .expect("Failed to allocate memory for hashtable"),
×
762
            )
763
        }
764
    }
765
}
766

767
/// Immutable iterator upon a reference to a hashtable.
768
pub struct Iter<'a> {
769
    ht: &'a ZendHashTable,
770
    current_num: i64,
771
    end_num: i64,
772
    pos: HashPosition,
773
    end_pos: HashPosition,
774
}
775

776
/// Represents the key of a PHP array, which can be either a long or a string.
777
#[derive(Debug, Clone, PartialEq)]
778
pub enum ArrayKey<'a> {
779
    /// A numerical key.
780
    Long(i64),
781
    /// A string key.
782
    String(String),
783
    /// A string key by reference.
784
    Str(&'a str),
785
}
786

787
impl From<String> for ArrayKey<'_> {
NEW
UNCOV
788
    fn from(value: String) -> Self {
×
NEW
UNCOV
789
        Self::String(value)
×
790
    }
791
}
792

793
impl ArrayKey<'_> {
794
    /// Check if the key is an integer.
795
    ///
796
    /// # Returns
797
    ///
798
    /// Returns true if the key is an integer, false otherwise.
799
    #[must_use]
800
    pub fn is_long(&self) -> bool {
×
801
        match self {
×
802
            ArrayKey::Long(_) => true,
×
NEW
803
            ArrayKey::String(_) | ArrayKey::Str(_) => false,
×
804
        }
805
    }
806
}
807

808
impl Display for ArrayKey<'_> {
UNCOV
809
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
810
        match self {
×
811
            ArrayKey::Long(key) => write!(f, "{key}"),
×
812
            ArrayKey::String(key) => write!(f, "{key}"),
×
NEW
813
            ArrayKey::Str(key) => write!(f, "{key}"),
×
814
        }
815
    }
816
}
817

818
impl<'a> From<&'a str> for ArrayKey<'a> {
NEW
UNCOV
819
    fn from(key: &'a str) -> ArrayKey<'a> {
×
NEW
820
        ArrayKey::Str(key)
×
821
    }
822
}
823

824
impl<'a> From<i64> for ArrayKey<'a> {
NEW
825
    fn from(index: i64) -> ArrayKey<'a> {
×
NEW
826
        ArrayKey::Long(index)
×
827
    }
828
}
829

830
impl<'a> FromZval<'a> for ArrayKey<'_> {
831
    const TYPE: DataType = DataType::String;
832

833
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
834
        if let Some(key) = zval.long() {
×
835
            return Some(ArrayKey::Long(key));
×
836
        }
837
        if let Some(key) = zval.string() {
×
838
            return Some(ArrayKey::String(key));
×
839
        }
UNCOV
840
        None
×
841
    }
842
}
843

844
impl<'a> Iter<'a> {
845
    /// Creates a new iterator over a hashtable.
846
    ///
847
    /// # Parameters
848
    ///
849
    /// * `ht` - The hashtable to iterate.
UNCOV
850
    pub fn new(ht: &'a ZendHashTable) -> Self {
×
851
        let end_num: i64 = ht
×
852
            .len()
853
            .try_into()
854
            .expect("Integer overflow in hashtable length");
UNCOV
855
        let end_pos = if ht.nNumOfElements > 0 {
×
856
            ht.nNumOfElements - 1
×
857
        } else {
UNCOV
858
            0
×
859
        };
860

861
        Self {
862
            ht,
863
            current_num: 0,
864
            end_num,
865
            pos: 0,
866
            end_pos,
867
        }
868
    }
869
}
870

871
impl<'a> IntoIterator for &'a ZendHashTable {
872
    type Item = (ArrayKey<'a>, &'a Zval);
873
    type IntoIter = Iter<'a>;
874

875
    /// Returns an iterator over the key(s) and value contained inside the
876
    /// hashtable.
877
    ///
878
    /// # Example
879
    ///
880
    /// ```no_run
881
    /// use ext_php_rs::types::ZendHashTable;
882
    ///
883
    /// let mut ht = ZendHashTable::new();
884
    ///
885
    /// for (key, val) in ht.iter() {
886
    /// //   ^ Index if inserted at an index.
887
    /// //        ^ Optional string key, if inserted like a hashtable.
888
    /// //             ^ Inserted value.
889
    ///
890
    ///     dbg!(key, val);
891
    /// }
892
    #[inline]
UNCOV
893
    fn into_iter(self) -> Self::IntoIter {
×
894
        Iter::new(self)
×
895
    }
896
}
897

898
impl<'a> Iterator for Iter<'a> {
899
    type Item = (ArrayKey<'a>, &'a Zval);
900

UNCOV
901
    fn next(&mut self) -> Option<Self::Item> {
×
902
        self.next_zval()
×
903
            .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
×
904
    }
905

906
    fn count(self) -> usize
×
907
    where
908
        Self: Sized,
909
    {
UNCOV
910
        self.ht.len()
×
911
    }
912
}
913

914
impl ExactSizeIterator for Iter<'_> {
UNCOV
915
    fn len(&self) -> usize {
×
916
        self.ht.len()
×
917
    }
918
}
919

920
impl DoubleEndedIterator for Iter<'_> {
UNCOV
921
    fn next_back(&mut self) -> Option<Self::Item> {
×
922
        if self.end_num <= self.current_num {
×
923
            return None;
×
924
        }
925

926
        let key_type = unsafe {
UNCOV
927
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
928
        };
929

UNCOV
930
        if key_type == -1 {
×
931
            return None;
×
932
        }
933

UNCOV
934
        let key = Zval::new();
×
935

936
        unsafe {
937
            zend_hash_get_current_key_zval_ex(
UNCOV
938
                ptr::from_ref(self.ht).cast_mut(),
×
939
                (&raw const key).cast_mut(),
×
940
                &raw mut self.end_pos,
×
941
            );
942
        }
943
        let value = unsafe {
UNCOV
944
            &*zend_hash_get_current_data_ex(
×
945
                ptr::from_ref(self.ht).cast_mut(),
×
946
                &raw mut self.end_pos,
×
947
            )
948
        };
949

UNCOV
950
        let key = match ArrayKey::from_zval(&key) {
×
951
            Some(key) => key,
×
952
            None => ArrayKey::Long(self.end_num),
×
953
        };
954

955
        unsafe {
UNCOV
956
            zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos)
×
957
        };
UNCOV
958
        self.end_num -= 1;
×
959

UNCOV
960
        Some((key, value))
×
961
    }
962
}
963

964
impl<'a> Iter<'a> {
UNCOV
965
    pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> {
×
966
        if self.current_num >= self.end_num {
×
967
            return None;
×
968
        }
969

970
        let key_type = unsafe {
UNCOV
971
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
972
        };
973

974
        // Key type `-1` is ???
975
        // Key type `1` is string
976
        // Key type `2` is long
977
        // Key type `3` is null meaning the end of the array
UNCOV
978
        if key_type == -1 || key_type == 3 {
×
979
            return None;
×
980
        }
981

UNCOV
982
        let mut key = Zval::new();
×
983

984
        unsafe {
985
            zend_hash_get_current_key_zval_ex(
UNCOV
986
                ptr::from_ref(self.ht).cast_mut(),
×
987
                (&raw const key).cast_mut(),
×
988
                &raw mut self.pos,
×
989
            );
990
        }
991
        let value = unsafe {
UNCOV
992
            let val_ptr =
×
993
                zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos);
×
994

UNCOV
995
            if val_ptr.is_null() {
×
996
                return None;
×
997
            }
998

UNCOV
999
            &*val_ptr
×
1000
        };
1001

UNCOV
1002
        if !key.is_long() && !key.is_string() {
×
1003
            key.set_long(self.current_num);
×
1004
        }
1005

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

UNCOV
1009
        Some((key, value))
×
1010
    }
1011
}
1012

1013
/// Immutable iterator which iterates over the values of the hashtable, as it
1014
/// was a set or list.
1015
pub struct Values<'a>(Iter<'a>);
1016

1017
impl<'a> Values<'a> {
1018
    /// Creates a new iterator over a hashtables values.
1019
    ///
1020
    /// # Parameters
1021
    ///
1022
    /// * `ht` - The hashtable to iterate.
UNCOV
1023
    pub fn new(ht: &'a ZendHashTable) -> Self {
×
1024
        Self(Iter::new(ht))
×
1025
    }
1026
}
1027

1028
impl<'a> Iterator for Values<'a> {
1029
    type Item = &'a Zval;
1030

UNCOV
1031
    fn next(&mut self) -> Option<Self::Item> {
×
1032
        self.0.next().map(|(_, zval)| zval)
×
1033
    }
1034

UNCOV
1035
    fn count(self) -> usize
×
1036
    where
1037
        Self: Sized,
1038
    {
UNCOV
1039
        self.0.count()
×
1040
    }
1041
}
1042

1043
impl ExactSizeIterator for Values<'_> {
UNCOV
1044
    fn len(&self) -> usize {
×
1045
        self.0.len()
×
1046
    }
1047
}
1048

1049
impl DoubleEndedIterator for Values<'_> {
UNCOV
1050
    fn next_back(&mut self) -> Option<Self::Item> {
×
1051
        self.0.next_back().map(|(_, zval)| zval)
×
1052
    }
1053
}
1054

1055
impl Default for ZBox<ZendHashTable> {
UNCOV
1056
    fn default() -> Self {
×
1057
        ZendHashTable::new()
×
1058
    }
1059
}
1060

1061
impl Clone for ZBox<ZendHashTable> {
UNCOV
1062
    fn clone(&self) -> Self {
×
1063
        (**self).to_owned()
×
1064
    }
1065
}
1066

1067
impl IntoZval for ZBox<ZendHashTable> {
1068
    const TYPE: DataType = DataType::Array;
1069
    const NULLABLE: bool = false;
1070

UNCOV
1071
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1072
        zv.set_hashtable(self);
×
1073
        Ok(())
×
1074
    }
1075
}
1076

1077
impl<'a> FromZval<'a> for &'a ZendHashTable {
1078
    const TYPE: DataType = DataType::Array;
1079

UNCOV
1080
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
1081
        zval.array()
×
1082
    }
1083
}
1084

1085
///////////////////////////////////////////
1086
// HashMap
1087
///////////////////////////////////////////
1088

1089
// TODO: Generalize hasher
1090
#[allow(clippy::implicit_hasher)]
1091
impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap<String, V>
1092
where
1093
    V: FromZval<'a>,
1094
{
1095
    type Error = Error;
1096

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

UNCOV
1100
        for (key, val) in value {
×
1101
            hm.insert(
×
1102
                key.to_string(),
×
1103
                V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?,
×
1104
            );
1105
        }
1106

UNCOV
1107
        Ok(hm)
×
1108
    }
1109
}
1110

1111
impl<K, V> TryFrom<HashMap<K, V>> for ZBox<ZendHashTable>
1112
where
1113
    K: AsRef<str>,
1114
    V: IntoZval,
1115
{
1116
    type Error = Error;
1117

UNCOV
1118
    fn try_from(value: HashMap<K, V>) -> Result<Self> {
×
1119
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1120
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1121
        );
1122

UNCOV
1123
        for (k, v) in value {
×
1124
            ht.insert(k.as_ref(), v)?;
×
1125
        }
1126

UNCOV
1127
        Ok(ht)
×
1128
    }
1129
}
1130

1131
// TODO: Generalize hasher
1132
#[allow(clippy::implicit_hasher)]
1133
impl<K, V> IntoZval for HashMap<K, V>
1134
where
1135
    K: AsRef<str>,
1136
    V: IntoZval,
1137
{
1138
    const TYPE: DataType = DataType::Array;
1139
    const NULLABLE: bool = false;
1140

UNCOV
1141
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1142
        let arr = self.try_into()?;
×
1143
        zv.set_hashtable(arr);
×
1144
        Ok(())
×
1145
    }
1146
}
1147

1148
// TODO: Generalize hasher
1149
#[allow(clippy::implicit_hasher)]
1150
impl<'a, T> FromZval<'a> for HashMap<String, T>
1151
where
1152
    T: FromZval<'a>,
1153
{
1154
    const TYPE: DataType = DataType::Array;
1155

UNCOV
1156
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
1157
        zval.array().and_then(|arr| arr.try_into().ok())
×
1158
    }
1159
}
1160

1161
///////////////////////////////////////////
1162
// Vec
1163
///////////////////////////////////////////
1164

1165
impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1166
where
1167
    T: FromZval<'a>,
1168
{
1169
    type Error = Error;
1170

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

UNCOV
1174
        for (_, val) in value {
×
1175
            vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
×
1176
        }
1177

UNCOV
1178
        Ok(vec)
×
1179
    }
1180
}
1181

1182
impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1183
where
1184
    T: IntoZval,
1185
{
1186
    type Error = Error;
1187

UNCOV
1188
    fn try_from(value: Vec<T>) -> Result<Self> {
×
1189
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1190
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1191
        );
1192

UNCOV
1193
        for val in value {
×
1194
            ht.push(val)?;
×
1195
        }
1196

UNCOV
1197
        Ok(ht)
×
1198
    }
1199
}
1200

1201
impl<T> IntoZval for Vec<T>
1202
where
1203
    T: IntoZval,
1204
{
1205
    const TYPE: DataType = DataType::Array;
1206
    const NULLABLE: bool = false;
1207

UNCOV
1208
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1209
        let arr = self.try_into()?;
×
1210
        zv.set_hashtable(arr);
×
1211
        Ok(())
×
1212
    }
1213
}
1214

1215
impl<'a, T> FromZval<'a> for Vec<T>
1216
where
1217
    T: FromZval<'a>,
1218
{
1219
    const TYPE: DataType = DataType::Array;
1220

UNCOV
1221
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
1222
        zval.array().and_then(|arr| arr.try_into().ok())
×
1223
    }
1224
}
1225

1226
impl FromIterator<Zval> for ZBox<ZendHashTable> {
UNCOV
1227
    fn from_iter<T: IntoIterator<Item = Zval>>(iter: T) -> Self {
×
1228
        let mut ht = ZendHashTable::new();
×
1229
        for item in iter {
×
1230
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1231
            // `val` to a zval fails.
UNCOV
1232
            let _ = ht.push(item);
×
1233
        }
UNCOV
1234
        ht
×
1235
    }
1236
}
1237

1238
impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
NEW
UNCOV
1239
    fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
×
1240
        let mut ht = ZendHashTable::new();
×
1241
        for (key, val) in iter {
×
1242
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1243
            // `val` to a zval fails.
UNCOV
1244
            let _ = ht.insert_at_index(key, val);
×
1245
        }
1246
        ht
×
1247
    }
1248
}
1249

1250
impl<'a> FromIterator<(&'a str, Zval)> for ZBox<ZendHashTable> {
UNCOV
1251
    fn from_iter<T: IntoIterator<Item = (&'a str, Zval)>>(iter: T) -> Self {
×
1252
        let mut ht = ZendHashTable::new();
×
1253
        for (key, val) in iter {
×
1254
            // Inserting a zval cannot fail, as `push` only returns `Err` if converting
1255
            // `val` to a zval fails.
UNCOV
1256
            let _ = ht.insert(key, val);
×
1257
        }
UNCOV
1258
        ht
×
1259
    }
1260
}
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