Skip to content

Commit c4fbd91

Browse files
kakserpomXenira
andauthored
fix(array): cast numeric string keys into zend_ulong and allow negative keys
* feat(array): allow negative indices * test(array): add tests for array keys --------- Fixes: #453, #454 Refs: #456 Co-authored-by: Xenira <[email protected]>
1 parent 8dbed1b commit c4fbd91

File tree

5 files changed

+250
-31
lines changed

5 files changed

+250
-31
lines changed

src/types/array.rs

Lines changed: 219 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ use std::{
88
fmt::{Debug, Display},
99
iter::FromIterator,
1010
ptr,
11+
str::FromStr,
1112
};
1213

1314
use crate::{
1415
boxed::{ZBox, ZBoxable},
1516
convert::{FromZval, IntoZval},
1617
error::{Error, Result},
18+
ffi::zend_ulong,
1719
ffi::{
1820
_zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean,
1921
zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex,
@@ -188,9 +190,46 @@ impl ZendHashTable {
188190
/// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world"));
189191
/// ```
190192
#[must_use]
191-
pub fn get(&self, key: &'_ str) -> Option<&Zval> {
192-
let str = CString::new(key).ok()?;
193-
unsafe { zend_hash_str_find(self, str.as_ptr(), key.len() as _).as_ref() }
193+
pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
194+
where
195+
K: Into<ArrayKey<'a>>,
196+
{
197+
match key.into() {
198+
ArrayKey::Long(index) => unsafe {
199+
#[allow(clippy::cast_sign_loss)]
200+
zend_hash_index_find(self, index as zend_ulong).as_ref()
201+
},
202+
ArrayKey::String(key) => {
203+
if let Ok(index) = i64::from_str(key.as_str()) {
204+
#[allow(clippy::cast_sign_loss)]
205+
unsafe {
206+
zend_hash_index_find(self, index as zend_ulong).as_ref()
207+
}
208+
} else {
209+
unsafe {
210+
zend_hash_str_find(
211+
self,
212+
CString::new(key.as_str()).ok()?.as_ptr(),
213+
key.len() as _,
214+
)
215+
.as_ref()
216+
}
217+
}
218+
}
219+
ArrayKey::Str(key) => {
220+
if let Ok(index) = i64::from_str(key) {
221+
#[allow(clippy::cast_sign_loss)]
222+
unsafe {
223+
zend_hash_index_find(self, index as zend_ulong).as_ref()
224+
}
225+
} else {
226+
unsafe {
227+
zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
228+
.as_ref()
229+
}
230+
}
231+
}
232+
}
194233
}
195234

196235
/// Attempts to retrieve a value from the hash table with a string key.
@@ -219,9 +258,46 @@ impl ZendHashTable {
219258
// hashtable while only having a reference to it. #461
220259
#[allow(clippy::mut_from_ref)]
221260
#[must_use]
222-
pub fn get_mut(&self, key: &'_ str) -> Option<&mut Zval> {
223-
let str = CString::new(key).ok()?;
224-
unsafe { zend_hash_str_find(self, str.as_ptr(), key.len() as _).as_mut() }
261+
pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
262+
where
263+
K: Into<ArrayKey<'a>>,
264+
{
265+
match key.into() {
266+
ArrayKey::Long(index) => unsafe {
267+
#[allow(clippy::cast_sign_loss)]
268+
zend_hash_index_find(self, index as zend_ulong).as_mut()
269+
},
270+
ArrayKey::String(key) => {
271+
if let Ok(index) = i64::from_str(key.as_str()) {
272+
#[allow(clippy::cast_sign_loss)]
273+
unsafe {
274+
zend_hash_index_find(self, index as zend_ulong).as_mut()
275+
}
276+
} else {
277+
unsafe {
278+
zend_hash_str_find(
279+
self,
280+
CString::new(key.as_str()).ok()?.as_ptr(),
281+
key.len() as _,
282+
)
283+
.as_mut()
284+
}
285+
}
286+
}
287+
ArrayKey::Str(key) => {
288+
if let Ok(index) = i64::from_str(key) {
289+
#[allow(clippy::cast_sign_loss)]
290+
unsafe {
291+
zend_hash_index_find(self, index as zend_ulong).as_mut()
292+
}
293+
} else {
294+
unsafe {
295+
zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
296+
.as_mut()
297+
}
298+
}
299+
}
300+
}
225301
}
226302

227303
/// Attempts to retrieve a value from the hash table with an index.
@@ -247,8 +323,11 @@ impl ZendHashTable {
247323
/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100));
248324
/// ```
249325
#[must_use]
250-
pub fn get_index(&self, key: u64) -> Option<&Zval> {
251-
unsafe { zend_hash_index_find(self, key).as_ref() }
326+
pub fn get_index(&self, key: i64) -> Option<&Zval> {
327+
#[allow(clippy::cast_sign_loss)]
328+
unsafe {
329+
zend_hash_index_find(self, key as zend_ulong).as_ref()
330+
}
252331
}
253332

254333
/// Attempts to retrieve a value from the hash table with an index.
@@ -277,8 +356,11 @@ impl ZendHashTable {
277356
// hashtable while only having a reference to it. #461
278357
#[allow(clippy::mut_from_ref)]
279358
#[must_use]
280-
pub fn get_index_mut(&self, key: u64) -> Option<&mut Zval> {
281-
unsafe { zend_hash_index_find(self, key).as_mut() }
359+
pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
360+
unsafe {
361+
#[allow(clippy::cast_sign_loss)]
362+
zend_hash_index_find(self, key as zend_ulong).as_mut()
363+
}
282364
}
283365

284366
/// Attempts to remove a value from the hash table with a string key.
@@ -305,9 +387,44 @@ impl ZendHashTable {
305387
/// ht.remove("test");
306388
/// assert_eq!(ht.len(), 0);
307389
/// ```
308-
pub fn remove(&mut self, key: &str) -> Option<()> {
309-
let result =
310-
unsafe { zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _) };
390+
pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
391+
where
392+
K: Into<ArrayKey<'a>>,
393+
{
394+
let result = match key.into() {
395+
ArrayKey::Long(index) => unsafe {
396+
#[allow(clippy::cast_sign_loss)]
397+
zend_hash_index_del(self, index as zend_ulong)
398+
},
399+
ArrayKey::String(key) => {
400+
if let Ok(index) = i64::from_str(key.as_str()) {
401+
#[allow(clippy::cast_sign_loss)]
402+
unsafe {
403+
zend_hash_index_del(self, index as zend_ulong)
404+
}
405+
} else {
406+
unsafe {
407+
zend_hash_str_del(
408+
self,
409+
CString::new(key.as_str()).ok()?.as_ptr(),
410+
key.len() as _,
411+
)
412+
}
413+
}
414+
}
415+
ArrayKey::Str(key) => {
416+
if let Ok(index) = i64::from_str(key) {
417+
#[allow(clippy::cast_sign_loss)]
418+
unsafe {
419+
zend_hash_index_del(self, index as zend_ulong)
420+
}
421+
} else {
422+
unsafe {
423+
zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _)
424+
}
425+
}
426+
}
427+
};
311428

312429
if result < 0 {
313430
None
@@ -340,8 +457,11 @@ impl ZendHashTable {
340457
/// ht.remove_index(0);
341458
/// assert_eq!(ht.len(), 0);
342459
/// ```
343-
pub fn remove_index(&mut self, key: u64) -> Option<()> {
344-
let result = unsafe { zend_hash_index_del(self, key) };
460+
pub fn remove_index(&mut self, key: i64) -> Option<()> {
461+
let result = unsafe {
462+
#[allow(clippy::cast_sign_loss)]
463+
zend_hash_index_del(self, key as zend_ulong)
464+
};
345465

346466
if result < 0 {
347467
None
@@ -379,12 +499,54 @@ impl ZendHashTable {
379499
/// ht.insert("c", "C");
380500
/// assert_eq!(ht.len(), 3);
381501
/// ```
382-
pub fn insert<V>(&mut self, key: &str, val: V) -> Result<()>
502+
pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
383503
where
504+
K: Into<ArrayKey<'a>>,
384505
V: IntoZval,
385506
{
386507
let mut val = val.into_zval(false)?;
387-
unsafe { zend_hash_str_update(self, CString::new(key)?.as_ptr(), key.len(), &raw mut val) };
508+
match key.into() {
509+
ArrayKey::Long(index) => {
510+
unsafe {
511+
#[allow(clippy::cast_sign_loss)]
512+
zend_hash_index_update(self, index as zend_ulong, &raw mut val)
513+
};
514+
}
515+
ArrayKey::String(key) => {
516+
if let Ok(index) = i64::from_str(&key) {
517+
unsafe {
518+
#[allow(clippy::cast_sign_loss)]
519+
zend_hash_index_update(self, index as zend_ulong, &raw mut val)
520+
};
521+
} else {
522+
unsafe {
523+
zend_hash_str_update(
524+
self,
525+
CString::new(key.as_str())?.as_ptr(),
526+
key.len(),
527+
&raw mut val,
528+
)
529+
};
530+
}
531+
}
532+
ArrayKey::Str(key) => {
533+
if let Ok(index) = i64::from_str(key) {
534+
unsafe {
535+
#[allow(clippy::cast_sign_loss)]
536+
zend_hash_index_update(self, index as zend_ulong, &raw mut val)
537+
};
538+
} else {
539+
unsafe {
540+
zend_hash_str_update(
541+
self,
542+
CString::new(key)?.as_ptr(),
543+
key.len(),
544+
&raw mut val,
545+
)
546+
};
547+
}
548+
}
549+
}
388550
val.release();
389551
Ok(())
390552
}
@@ -417,12 +579,15 @@ impl ZendHashTable {
417579
/// ht.insert_at_index(0, "C"); // notice overriding index 0
418580
/// assert_eq!(ht.len(), 2);
419581
/// ```
420-
pub fn insert_at_index<V>(&mut self, key: u64, val: V) -> Result<()>
582+
pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
421583
where
422584
V: IntoZval,
423585
{
424586
let mut val = val.into_zval(false)?;
425-
unsafe { zend_hash_index_update(self, key, &raw mut val) };
587+
unsafe {
588+
#[allow(clippy::cast_sign_loss)]
589+
zend_hash_index_update(self, key as zend_ulong, &raw mut val)
590+
};
426591
val.release();
427592
Ok(())
428593
}
@@ -560,6 +725,8 @@ impl ZendHashTable {
560725
/// }
561726
/// ArrayKey::String(key) => {
562727
/// }
728+
/// ArrayKey::Str(key) => {
729+
/// }
563730
/// }
564731
/// dbg!(key, val);
565732
/// }
@@ -612,15 +779,25 @@ pub struct Iter<'a> {
612779
}
613780

614781
/// Represents the key of a PHP array, which can be either a long or a string.
615-
#[derive(Debug, PartialEq)]
616-
pub enum ArrayKey {
782+
#[derive(Debug, Clone, PartialEq)]
783+
pub enum ArrayKey<'a> {
617784
/// 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.
618787
Long(i64),
619788
/// A string key.
620789
String(String),
790+
/// A string key by reference.
791+
Str(&'a str),
621792
}
622793

623-
impl ArrayKey {
794+
impl From<String> for ArrayKey<'_> {
795+
fn from(value: String) -> Self {
796+
Self::String(value)
797+
}
798+
}
799+
800+
impl ArrayKey<'_> {
624801
/// Check if the key is an integer.
625802
///
626803
/// # Returns
@@ -630,21 +807,34 @@ impl ArrayKey {
630807
pub fn is_long(&self) -> bool {
631808
match self {
632809
ArrayKey::Long(_) => true,
633-
ArrayKey::String(_) => false,
810+
ArrayKey::String(_) | ArrayKey::Str(_) => false,
634811
}
635812
}
636813
}
637814

638-
impl Display for ArrayKey {
815+
impl Display for ArrayKey<'_> {
639816
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640817
match self {
641818
ArrayKey::Long(key) => write!(f, "{key}"),
642819
ArrayKey::String(key) => write!(f, "{key}"),
820+
ArrayKey::Str(key) => write!(f, "{key}"),
643821
}
644822
}
645823
}
646824

647-
impl<'a> FromZval<'a> for ArrayKey {
825+
impl<'a> From<&'a str> for ArrayKey<'a> {
826+
fn from(key: &'a str) -> ArrayKey<'a> {
827+
ArrayKey::Str(key)
828+
}
829+
}
830+
831+
impl<'a> From<i64> for ArrayKey<'a> {
832+
fn from(index: i64) -> ArrayKey<'a> {
833+
ArrayKey::Long(index)
834+
}
835+
}
836+
837+
impl<'a> FromZval<'a> for ArrayKey<'_> {
648838
const TYPE: DataType = DataType::String;
649839

650840
fn from_zval(zval: &'a Zval) -> Option<Self> {
@@ -686,7 +876,7 @@ impl<'a> Iter<'a> {
686876
}
687877

688878
impl<'a> IntoIterator for &'a ZendHashTable {
689-
type Item = (ArrayKey, &'a Zval);
879+
type Item = (ArrayKey<'a>, &'a Zval);
690880
type IntoIter = Iter<'a>;
691881

692882
/// Returns an iterator over the key(s) and value contained inside the
@@ -713,7 +903,7 @@ impl<'a> IntoIterator for &'a ZendHashTable {
713903
}
714904

715905
impl<'a> Iterator for Iter<'a> {
716-
type Item = (ArrayKey, &'a Zval);
906+
type Item = (ArrayKey<'a>, &'a Zval);
717907

718908
fn next(&mut self) -> Option<Self::Item> {
719909
self.next_zval()
@@ -1052,8 +1242,8 @@ impl FromIterator<Zval> for ZBox<ZendHashTable> {
10521242
}
10531243
}
10541244

1055-
impl FromIterator<(u64, Zval)> for ZBox<ZendHashTable> {
1056-
fn from_iter<T: IntoIterator<Item = (u64, Zval)>>(iter: T) -> Self {
1245+
impl FromIterator<(i64, Zval)> for ZBox<ZendHashTable> {
1246+
fn from_iter<T: IntoIterator<Item = (i64, Zval)>>(iter: T) -> Self {
10571247
let mut ht = ZendHashTable::new();
10581248
for (key, val) in iter {
10591249
// Inserting a zval cannot fail, as `push` only returns `Err` if converting

src/zend/handlers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ impl ZendObjectHandlers {
181181
let self_ = &mut *obj;
182182
let struct_props = T::get_metadata().get_properties();
183183

184-
for (name, val) in struct_props {
184+
for (&name, val) in struct_props {
185185
let mut zv = Zval::new();
186186
if val.prop.get(self_, &mut zv).is_err() {
187187
continue;

0 commit comments

Comments
 (0)