use std::{convert::TryFrom, fmt::Debug, ptr};
use crate::{
boxed::{ZBox, ZBoxable},
convert::{FromZval, FromZvalMut, IntoZval},
error::Result,
ffi::zend_ulong,
ffi::{
_zend_new_array, GC_FLAGS_MASK, GC_FLAGS_SHIFT, HT_MIN_SIZE, zend_array_count,
zend_array_destroy, zend_array_dup, zend_empty_array, zend_hash_clean, zend_hash_index_del,
zend_hash_index_find, zend_hash_index_update, zend_hash_next_index_insert,
zend_hash_str_del, zend_hash_str_find, zend_hash_str_update,
},
flags::{DataType, ZvalTypeFlags},
types::Zval,
};
mod array_key;
mod conversions;
mod entry;
mod iterators;
pub use array_key::ArrayKey;
pub use entry::{Entry, OccupiedEntry, VacantEntry};
pub use iterators::{Iter, Values};
pub type ZendHashTable = crate::ffi::HashTable;
#[allow(clippy::len_without_is_empty)]
impl ZendHashTable {
#[must_use]
pub fn new() -> ZBox<Self> {
Self::with_capacity(HT_MIN_SIZE)
}
#[must_use]
pub fn with_capacity(size: u32) -> ZBox<Self> {
unsafe {
#[allow(clippy::used_underscore_items)]
let ptr = _zend_new_array(size);
ZBox::from_raw(
ptr.as_mut()
.expect("Failed to allocate memory for hashtable"),
)
}
}
#[must_use]
pub fn len(&self) -> usize {
unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize }
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn clear(&mut self) {
unsafe { zend_hash_clean(self) }
}
#[must_use]
pub fn get<'a, K>(&self, key: K) -> Option<&Zval>
where
K: Into<ArrayKey<'a>>,
{
match key.into() {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_find(self, index as zend_ulong).as_ref()
},
ArrayKey::String(key) => unsafe {
zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_ref()
},
}
}
#[allow(clippy::mut_from_ref)]
#[must_use]
pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval>
where
K: Into<ArrayKey<'a>>,
{
match key.into() {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_find(self, index as zend_ulong).as_mut()
},
ArrayKey::String(key) => unsafe {
zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).as_mut()
},
}
}
#[must_use]
pub fn get_index(&self, key: i64) -> Option<&Zval> {
#[allow(clippy::cast_sign_loss)]
unsafe {
zend_hash_index_find(self, key as zend_ulong).as_ref()
}
}
#[allow(clippy::mut_from_ref)]
#[must_use]
pub fn get_index_mut(&self, key: i64) -> Option<&mut Zval> {
unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_find(self, key as zend_ulong).as_mut()
}
}
pub fn remove<'a, K>(&mut self, key: K) -> Option<()>
where
K: Into<ArrayKey<'a>>,
{
let result = match key.into() {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_del(self, index as zend_ulong)
},
ArrayKey::String(key) => unsafe {
zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
},
ArrayKey::Str(key) => unsafe {
zend_hash_str_del(self, key.as_ptr().cast(), key.len() as _)
},
};
if result < 0 { None } else { Some(()) }
}
pub fn remove_index(&mut self, key: i64) -> Option<()> {
let result = unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_del(self, key as zend_ulong)
};
if result < 0 { None } else { Some(()) }
}
pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()>
where
K: Into<ArrayKey<'a>>,
V: IntoZval,
{
let mut val = val.into_zval(false)?;
match key.into() {
ArrayKey::Long(index) => {
unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_update(self, index as zend_ulong, &raw mut val)
};
}
ArrayKey::String(key) => {
unsafe {
zend_hash_str_update(
self,
key.as_str().as_ptr().cast(),
key.len(),
&raw mut val,
)
};
}
ArrayKey::Str(key) => {
unsafe {
zend_hash_str_update(self, key.as_ptr().cast(), key.len(), &raw mut val)
};
}
}
val.release();
Ok(())
}
pub fn insert_at_index<V>(&mut self, key: i64, val: V) -> Result<()>
where
V: IntoZval,
{
let mut val = val.into_zval(false)?;
unsafe {
#[allow(clippy::cast_sign_loss)]
zend_hash_index_update(self, key as zend_ulong, &raw mut val)
};
val.release();
Ok(())
}
pub fn push<V>(&mut self, val: V) -> Result<()>
where
V: IntoZval,
{
let mut val = val.into_zval(false)?;
unsafe { zend_hash_next_index_insert(self, &raw mut val) };
val.release();
Ok(())
}
#[must_use]
pub fn has_numerical_keys(&self) -> bool {
!self.into_iter().any(|(k, _)| !k.is_long())
}
#[must_use]
pub fn has_sequential_keys(&self) -> bool {
!self
.into_iter()
.enumerate()
.any(|(i, (k, _))| ArrayKey::Long(i64::try_from(i).expect("Integer overflow")) != k)
}
#[inline]
#[must_use]
pub fn values(&self) -> Values<'_> {
Values::new(self)
}
#[inline]
#[must_use]
pub fn iter(&self) -> Iter<'_> {
self.into_iter()
}
#[must_use]
pub fn entry<'a, 'k, K>(&'a mut self, key: K) -> Entry<'a, 'k>
where
K: Into<ArrayKey<'k>>,
{
let key = key.into();
if self.has_key(&key) {
Entry::Occupied(OccupiedEntry::new(self, key))
} else {
Entry::Vacant(VacantEntry::new(self, key))
}
}
#[must_use]
pub fn has_key(&self, key: &ArrayKey<'_>) -> bool {
match key {
ArrayKey::Long(index) => unsafe {
#[allow(clippy::cast_sign_loss)]
!zend_hash_index_find(self, *index as zend_ulong).is_null()
},
ArrayKey::String(key) => unsafe {
!zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
},
ArrayKey::Str(key) => unsafe {
!zend_hash_str_find(self, key.as_ptr().cast(), key.len() as _).is_null()
},
}
}
#[must_use]
pub fn is_immutable(&self) -> bool {
let gc_type_info = unsafe { self.gc.u.type_info };
let gc_flags = (gc_type_info >> GC_FLAGS_SHIFT) & (GC_FLAGS_MASK >> GC_FLAGS_SHIFT);
gc_flags & ZvalTypeFlags::Immutable.bits() != 0
}
}
unsafe impl ZBoxable for ZendHashTable {
fn free(&mut self) {
if self.is_immutable() {
return;
}
unsafe { zend_array_destroy(self) }
}
}
impl Debug for ZendHashTable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_map()
.entries(self.into_iter().map(|(k, v)| (k.to_string(), v)))
.finish()
}
}
impl ToOwned for ZendHashTable {
type Owned = ZBox<ZendHashTable>;
fn to_owned(&self) -> Self::Owned {
unsafe {
let ptr = zend_array_dup(ptr::from_ref(self).cast_mut());
ZBox::from_raw(
ptr.as_mut()
.expect("Failed to allocate memory for hashtable"),
)
}
}
}
impl Default for ZBox<ZendHashTable> {
fn default() -> Self {
ZendHashTable::new()
}
}
impl Clone for ZBox<ZendHashTable> {
fn clone(&self) -> Self {
(**self).to_owned()
}
}
impl IntoZval for ZBox<ZendHashTable> {
const TYPE: DataType = DataType::Array;
const NULLABLE: bool = false;
fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> {
zv.set_hashtable(self);
Ok(())
}
}
impl<'a> FromZval<'a> for &'a ZendHashTable {
const TYPE: DataType = DataType::Array;
fn from_zval(zval: &'a Zval) -> Option<Self> {
zval.array()
}
}
impl<'a> FromZvalMut<'a> for &'a mut ZendHashTable {
const TYPE: DataType = DataType::Array;
fn from_zval_mut(zval: &'a mut Zval) -> Option<Self> {
zval.array_mut()
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ZendEmptyArray;
impl ZendEmptyArray {
#[must_use]
pub fn as_hashtable(&self) -> &ZendHashTable {
unsafe { &zend_empty_array }
}
}
impl IntoZval for ZendEmptyArray {
const TYPE: DataType = DataType::Array;
const NULLABLE: bool = false;
fn set_zval(self, zv: &mut Zval, _persistent: bool) -> Result<()> {
zv.u1.type_info = ZvalTypeFlags::Array.bits();
zv.value.arr = ptr::from_ref(self.as_hashtable()).cast_mut();
Ok(())
}
}
#[cfg(test)]
#[cfg(feature = "embed")]
mod tests {
use super::*;
use crate::embed::Embed;
#[test]
fn test_has_key_string() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.insert("test", "value");
assert!(ht.has_key(&ArrayKey::from("test")));
assert!(!ht.has_key(&ArrayKey::from("missing")));
});
}
#[test]
fn test_has_key_long() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.push(42i64);
assert!(ht.has_key(&ArrayKey::Long(0)));
assert!(!ht.has_key(&ArrayKey::Long(1)));
});
}
#[test]
fn test_has_key_str_ref() {
Embed::run(|| {
let mut ht = ZendHashTable::new();
let _ = ht.insert("hello", "world");
let key = ArrayKey::Str("hello");
assert!(ht.has_key(&key));
assert!(ht.has_key(&key));
assert!(!ht.has_key(&ArrayKey::Str("missing")));
});
}
}