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

davidcole1340 / ext-php-rs / 15999711057

01 Jul 2025 12:45PM UTC coverage: 20.197% (-0.3%) from 20.518%
15999711057

Pull #456

github

web-flow
Merge c511bd2bc into 8dbed1bf1
Pull Request #456: fix(array): cast numeric keys into zend_ulong when inserting string keys

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

98 existing lines in 1 file now uncovered.

760 of 3763 relevant lines covered (20.2%)

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
    // TODO: Verify if this is safe to use, as it allows mutating the
257
    // hashtable while only having a reference to it. #461
258
    #[allow(clippy::mut_from_ref)]
259
    #[must_use]
260
    #[allow(clippy::mut_from_ref)]
NEW
261
    pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
×
262
    where
263
        K: Into<ArrayKey<'a>>,
264
    {
NEW
265
        match key.into() {
×
266
            ArrayKey::Long(index) => unsafe {
267
                #[allow(clippy::cast_sign_loss)]
NEW
268
                zend_hash_index_find(self, index as zend_ulong).as_mut()
×
269
            },
NEW
270
            ArrayKey::String(key) => {
×
NEW
271
                if let Ok(index) = i64::from_str(key.as_str()) {
×
NEW
272
                    #[allow(clippy::cast_sign_loss)]
×
273
                    unsafe {
NEW
274
                        zend_hash_index_find(self, index as zend_ulong).as_mut()
×
275
                    }
276
                } else {
277
                    unsafe {
278
                        zend_hash_str_find(
NEW
279
                            self,
×
NEW
280
                            CString::new(key.as_str()).ok()?.as_ptr(),
×
NEW
281
                            key.len() as _,
×
282
                        )
283
                        .as_mut()
284
                    }
285
                }
286
            }
NEW
287
            ArrayKey::Str(key) => {
×
NEW
288
                if let Ok(index) = i64::from_str(key) {
×
NEW
289
                    #[allow(clippy::cast_sign_loss)]
×
290
                    unsafe {
NEW
291
                        zend_hash_index_find(self, index as zend_ulong).as_mut()
×
292
                    }
293
                } else {
294
                    unsafe {
NEW
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]
NEW
326
    pub fn get_index(&self, key: i64) -> Option<&Zval> {
×
327
        #[allow(clippy::cast_sign_loss)]
328
        unsafe {
NEW
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
    #[allow(clippy::mut_from_ref)]
NEW
360
    pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
×
361
        unsafe {
362
            #[allow(clippy::cast_sign_loss)]
NEW
363
            zend_hash_index_find(self, key as zend_ulong).as_mut()
×
364
        }
365
    }
366

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

430
        if result < 0 {
×
UNCOV
431
            None
×
432
        } else {
UNCOV
433
            Some(())
×
434
        }
435
    }
436

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

467
        if result < 0 {
×
UNCOV
468
            None
×
469
        } else {
UNCOV
470
            Some(())
×
471
        }
472
    }
473

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

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

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

UNCOV
631
        Ok(())
×
632
    }
633

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

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

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

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

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

748
impl Debug for ZendHashTable {
749
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
750
        f.debug_map()
×
UNCOV
751
            .entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
×
752
            .finish()
753
    }
754
}
755

756
impl ToOwned for ZendHashTable {
757
    type Owned = ZBox<ZendHashTable>;
758

UNCOV
759
    fn to_owned(&self) -> Self::Owned {
×
760
        unsafe {
761
            // SAFETY: FFI call does not modify `self`, returns a new hashtable.
UNCOV
762
            let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
×
763

764
            // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not.
765
            ZBox::from_raw(
766
                ptr.as_mut()
×
UNCOV
767
                    .expect("Failed to allocate memory for hashtable"),
×
768
            )
769
        }
770
    }
771
}
772

773
/// Immutable iterator upon a reference to a hashtable.
774
pub struct Iter<'a> {
775
    ht: &'a ZendHashTable,
776
    current_num: i64,
777
    end_num: i64,
778
    pos: HashPosition,
779
    end_pos: HashPosition,
780
}
781

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

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

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

814
impl Display for ArrayKey<'_> {
815
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
816
        match self {
×
817
            ArrayKey::Long(key) => write!(f, "{key}"),
×
818
            ArrayKey::String(key) => write!(f, "{key}"),
×
NEW
819
            ArrayKey::Str(key) => write!(f, "{key}"),
×
820
        }
821
    }
822
}
823

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

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

836
impl<'a> FromZval<'a> for ArrayKey<'_> {
837
    const TYPE: DataType = DataType::String;
838

UNCOV
839
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
840
        if let Some(key) = zval.long() {
×
841
            return Some(ArrayKey::Long(key));
×
842
        }
UNCOV
843
        if let Some(key) = zval.string() {
×
844
            return Some(ArrayKey::String(key));
×
845
        }
UNCOV
846
        None
×
847
    }
848
}
849

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

867
        Self {
868
            ht,
869
            current_num: 0,
870
            end_num,
871
            pos: 0,
872
            end_pos,
873
        }
874
    }
875
}
876

877
impl<'a> IntoIterator for &'a ZendHashTable {
878
    type Item = (ArrayKey<'a>, &'a Zval);
879
    type IntoIter = Iter<'a>;
880

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

904
impl<'a> Iterator for Iter<'a> {
905
    type Item = (ArrayKey<'a>, &'a Zval);
906

UNCOV
907
    fn next(&mut self) -> Option<Self::Item> {
×
908
        self.next_zval()
×
909
            .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v))
×
910
    }
911

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

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

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

932
        let key_type = unsafe {
UNCOV
933
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
934
        };
935

UNCOV
936
        if key_type == -1 {
×
937
            return None;
×
938
        }
939

UNCOV
940
        let key = Zval::new();
×
941

942
        unsafe {
943
            zend_hash_get_current_key_zval_ex(
UNCOV
944
                ptr::from_ref(self.ht).cast_mut(),
×
945
                (&raw const key).cast_mut(),
×
946
                &raw mut self.end_pos,
×
947
            );
948
        }
949
        let value = unsafe {
UNCOV
950
            &*zend_hash_get_current_data_ex(
×
951
                ptr::from_ref(self.ht).cast_mut(),
×
952
                &raw mut self.end_pos,
×
953
            )
954
        };
955

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

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

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

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

976
        let key_type = unsafe {
UNCOV
977
            zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos)
×
978
        };
979

980
        // Key type `-1` is ???
981
        // Key type `1` is string
982
        // Key type `2` is long
983
        // Key type `3` is null meaning the end of the array
UNCOV
984
        if key_type == -1 || key_type == 3 {
×
985
            return None;
×
986
        }
987

UNCOV
988
        let mut key = Zval::new();
×
989

990
        unsafe {
991
            zend_hash_get_current_key_zval_ex(
UNCOV
992
                ptr::from_ref(self.ht).cast_mut(),
×
993
                (&raw const key).cast_mut(),
×
994
                &raw mut self.pos,
×
995
            );
996
        }
997
        let value = unsafe {
UNCOV
998
            let val_ptr =
×
999
                zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos);
×
1000

UNCOV
1001
            if val_ptr.is_null() {
×
1002
                return None;
×
1003
            }
1004

UNCOV
1005
            &*val_ptr
×
1006
        };
1007

UNCOV
1008
        if !key.is_long() && !key.is_string() {
×
1009
            key.set_long(self.current_num);
×
1010
        }
1011

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

UNCOV
1015
        Some((key, value))
×
1016
    }
1017
}
1018

1019
/// Immutable iterator which iterates over the values of the hashtable, as it
1020
/// was a set or list.
1021
pub struct Values<'a>(Iter<'a>);
1022

1023
impl<'a> Values<'a> {
1024
    /// Creates a new iterator over a hashtables values.
1025
    ///
1026
    /// # Parameters
1027
    ///
1028
    /// * `ht` - The hashtable to iterate.
UNCOV
1029
    pub fn new(ht: &'a ZendHashTable) -> Self {
×
1030
        Self(Iter::new(ht))
×
1031
    }
1032
}
1033

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

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

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

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

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

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

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

1073
impl IntoZval for ZBox<ZendHashTable> {
1074
    const TYPE: DataType = DataType::Array;
1075
    const NULLABLE: bool = false;
1076

UNCOV
1077
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1078
        zv.set_hashtable(self);
×
1079
        Ok(())
×
1080
    }
1081
}
1082

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

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

1091
///////////////////////////////////////////
1092
// HashMap
1093
///////////////////////////////////////////
1094

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

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

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

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

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

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

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

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

1137
// TODO: Generalize hasher
1138
#[allow(clippy::implicit_hasher)]
1139
impl<K, V> IntoZval for HashMap<K, V>
1140
where
1141
    K: AsRef<str>,
1142
    V: IntoZval,
1143
{
1144
    const TYPE: DataType = DataType::Array;
1145
    const NULLABLE: bool = false;
1146

UNCOV
1147
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1148
        let arr = self.try_into()?;
×
1149
        zv.set_hashtable(arr);
×
1150
        Ok(())
×
1151
    }
1152
}
1153

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

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

1167
///////////////////////////////////////////
1168
// Vec
1169
///////////////////////////////////////////
1170

1171
impl<'a, T> TryFrom<&'a ZendHashTable> for Vec<T>
1172
where
1173
    T: FromZval<'a>,
1174
{
1175
    type Error = Error;
1176

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

UNCOV
1180
        for (_, val) in value {
×
1181
            vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?);
×
1182
        }
1183

UNCOV
1184
        Ok(vec)
×
1185
    }
1186
}
1187

1188
impl<T> TryFrom<Vec<T>> for ZBox<ZendHashTable>
1189
where
1190
    T: IntoZval,
1191
{
1192
    type Error = Error;
1193

UNCOV
1194
    fn try_from(value: Vec<T>) -> Result<Self> {
×
1195
        let mut ht = ZendHashTable::with_capacity(
UNCOV
1196
            value.len().try_into().map_err(|_| Error::IntegerOverflow)?,
×
1197
        );
1198

UNCOV
1199
        for val in value {
×
1200
            ht.push(val)?;
×
1201
        }
1202

UNCOV
1203
        Ok(ht)
×
1204
    }
1205
}
1206

1207
impl<T> IntoZval for Vec<T>
1208
where
1209
    T: IntoZval,
1210
{
1211
    const TYPE: DataType = DataType::Array;
1212
    const NULLABLE: bool = false;
1213

UNCOV
1214
    fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
×
1215
        let arr = self.try_into()?;
×
1216
        zv.set_hashtable(arr);
×
1217
        Ok(())
×
1218
    }
1219
}
1220

1221
impl<'a, T> FromZval<'a> for Vec<T>
1222
where
1223
    T: FromZval<'a>,
1224
{
1225
    const TYPE: DataType = DataType::Array;
1226

UNCOV
1227
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
1228
        zval.array().and_then(|arr| arr.try_into().ok())
×
1229
    }
1230
}
1231

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

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

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