diff --git a/src/types/array.rs b/src/types/array.rs deleted file mode 100644 index 602eda7d4..000000000 --- a/src/types/array.rs +++ /dev/null @@ -1,1630 +0,0 @@ -//! Represents an array in PHP. As all arrays in PHP are associative arrays, -//! they are represented by hash tables. - -use std::{ - collections::HashMap, - convert::{TryFrom, TryInto}, - ffi::CString, - fmt::{Debug, Display}, - iter::FromIterator, - ptr, - str::FromStr, -}; - -use crate::{ - boxed::{ZBox, ZBoxable}, - convert::{FromZval, IntoZval}, - error::{Error, Result}, - ffi::zend_ulong, - ffi::{ - _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, zend_hash_clean, - zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex, - zend_hash_get_current_key_zval_ex, zend_hash_index_del, zend_hash_index_find, - zend_hash_index_update, zend_hash_move_backwards_ex, zend_hash_move_forward_ex, - zend_hash_next_index_insert, zend_hash_str_del, zend_hash_str_find, zend_hash_str_update, - HashPosition, HT_MIN_SIZE, - }, - flags::DataType, - types::Zval, -}; - -/// A PHP hashtable. -/// -/// In PHP, arrays are represented as hashtables. This allows you to push values -/// onto the end of the array like a vector, while also allowing you to insert -/// at arbitrary string key indexes. -/// -/// A PHP hashtable stores values as [`Zval`]s. This allows you to insert -/// different types into the same hashtable. Types must implement [`IntoZval`] -/// to be able to be inserted into the hashtable. -/// -/// # Examples -/// -/// ```no_run -/// use ext_php_rs::types::ZendHashTable; -/// -/// let mut ht = ZendHashTable::new(); -/// ht.push(1); -/// ht.push("Hello, world!"); -/// ht.insert("Like", "Hashtable"); -/// -/// assert_eq!(ht.len(), 3); -/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(1)); -/// ``` -pub type ZendHashTable = crate::ffi::HashTable; - -// Clippy complains about there being no `is_empty` function when implementing -// on the alias `ZendStr` :( -#[allow(clippy::len_without_is_empty)] -impl ZendHashTable { - /// Creates a new, empty, PHP hashtable, returned inside a [`ZBox`]. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let ht = ZendHashTable::new(); - /// ``` - /// - /// # Panics - /// - /// Panics if memory for the hashtable could not be allocated. - #[must_use] - pub fn new() -> ZBox { - Self::with_capacity(HT_MIN_SIZE) - } - - /// Creates a new, empty, PHP hashtable with an initial size, returned - /// inside a [`ZBox`]. - /// - /// # Parameters - /// - /// * `size` - The size to initialize the array with. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let ht = ZendHashTable::with_capacity(10); - /// ``` - /// - /// # Panics - /// - /// Panics if memory for the hashtable could not be allocated. - #[must_use] - pub fn with_capacity(size: u32) -> ZBox { - unsafe { - // SAFETY: PHP allocator handles the creation of the array. - #[allow(clippy::used_underscore_items)] - let ptr = _zend_new_array(size); - - // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not. - ZBox::from_raw( - ptr.as_mut() - .expect("Failed to allocate memory for hashtable"), - ) - } - } - - /// Returns the current number of elements in the array. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push(1); - /// ht.push("Hello, world"); - /// - /// assert_eq!(ht.len(), 2); - /// ``` - #[must_use] - pub fn len(&self) -> usize { - unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize } - } - - /// Returns whether the hash table is empty. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// assert_eq!(ht.is_empty(), true); - /// - /// ht.push(1); - /// ht.push("Hello, world"); - /// - /// assert_eq!(ht.is_empty(), false); - /// ``` - #[must_use] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Clears the hash table, removing all values. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert("test", "hello world"); - /// assert_eq!(ht.is_empty(), false); - /// - /// ht.clear(); - /// assert_eq!(ht.is_empty(), true); - /// ``` - pub fn clear(&mut self) { - unsafe { zend_hash_clean(self) } - } - - /// Attempts to retrieve a value from the hash table with a string key. - /// - /// # Parameters - /// - /// * `key` - The key to search for in the hash table. - /// - /// # Returns - /// - /// * `Some(&Zval)` - A reference to the zval at the position in the hash - /// table. - /// * `None` - No value at the given position was found. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert("test", "hello world"); - /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world")); - /// ``` - #[must_use] - pub fn get<'a, K>(&self, key: K) -> Option<&Zval> - where - K: Into>, - { - 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) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_ref() - } - } else { - unsafe { - zend_hash_str_find( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - .as_ref() - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_ref() - } - } else { - unsafe { - zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - .as_ref() - } - } - } - } - } - - /// Attempts to retrieve a value from the hash table with a string key. - /// - /// # Parameters - /// - /// * `key` - The key to search for in the hash table. - /// - /// # Returns - /// - /// * `Some(&Zval)` - A reference to the zval at the position in the hash - /// table. - /// * `None` - No value at the given position was found. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert("test", "hello world"); - /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world")); - /// ``` - // TODO: Verify if this is safe to use, as it allows mutating the - // hashtable while only having a reference to it. #461 - #[allow(clippy::mut_from_ref)] - #[must_use] - pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval> - where - K: Into>, - { - 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) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_mut() - } - } else { - unsafe { - zend_hash_str_find( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - .as_mut() - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_find(self, index as zend_ulong).as_mut() - } - } else { - unsafe { - zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - .as_mut() - } - } - } - } - } - - /// Attempts to retrieve a value from the hash table with an index. - /// - /// # Parameters - /// - /// * `key` - The key to search for in the hash table. - /// - /// # Returns - /// - /// * `Some(&Zval)` - A reference to the zval at the position in the hash - /// table. - /// * `None` - No value at the given position was found. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push(100); - /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100)); - /// ``` - #[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() - } - } - - /// Attempts to retrieve a value from the hash table with an index. - /// - /// # Parameters - /// - /// * `key` - The key to search for in the hash table. - /// - /// # Returns - /// - /// * `Some(&Zval)` - A reference to the zval at the position in the hash - /// table. - /// * `None` - No value at the given position was found. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push(100); - /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100)); - /// ``` - // TODO: Verify if this is safe to use, as it allows mutating the - // hashtable while only having a reference to it. #461 - #[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() - } - } - - /// Attempts to remove a value from the hash table with a string key. - /// - /// # Parameters - /// - /// * `key` - The key to remove from the hash table. - /// - /// # Returns - /// - /// * `Some(())` - Key was successfully removed. - /// * `None` - No key was removed, did not exist. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert("test", "hello world"); - /// assert_eq!(ht.len(), 1); - /// - /// ht.remove("test"); - /// assert_eq!(ht.len(), 0); - /// ``` - pub fn remove<'a, K>(&mut self, key: K) -> Option<()> - where - K: Into>, - { - 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) => { - if let Ok(index) = i64::from_str(key.as_str()) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_del(self, index as zend_ulong) - } - } else { - unsafe { - zend_hash_str_del( - self, - CString::new(key.as_str()).ok()?.as_ptr(), - key.len() as _, - ) - } - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - #[allow(clippy::cast_sign_loss)] - unsafe { - zend_hash_index_del(self, index as zend_ulong) - } - } else { - unsafe { - zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _) - } - } - } - }; - - if result < 0 { - None - } else { - Some(()) - } - } - - /// Attempts to remove a value from the hash table with a string key. - /// - /// # Parameters - /// - /// * `key` - The key to remove from the hash table. - /// - /// # Returns - /// - /// * `Ok(())` - Key was successfully removed. - /// * `None` - No key was removed, did not exist. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push("hello"); - /// assert_eq!(ht.len(), 1); - /// - /// ht.remove_index(0); - /// assert_eq!(ht.len(), 0); - /// ``` - 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(()) - } - } - - /// Attempts to insert an item into the hash table, or update if the key - /// already exists. Returns nothing in a result if successful. - /// - /// # Parameters - /// - /// * `key` - The key to insert the value at in the hash table. - /// * `value` - The value to insert into the hash table. - /// - /// # Returns - /// - /// Returns nothing in a result on success. - /// - /// # Errors - /// - /// Returns an error if the key could not be converted into a [`CString`], - /// or converting the value into a [`Zval`] failed. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert("a", "A"); - /// ht.insert("b", "B"); - /// ht.insert("c", "C"); - /// assert_eq!(ht.len(), 3); - /// ``` - pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()> - where - K: Into>, - 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) => { - if let Ok(index) = i64::from_str(&key) { - unsafe { - #[allow(clippy::cast_sign_loss)] - zend_hash_index_update(self, index as zend_ulong, &raw mut val) - }; - } else { - unsafe { - zend_hash_str_update( - self, - CString::new(key.as_str())?.as_ptr(), - key.len(), - &raw mut val, - ) - }; - } - } - ArrayKey::Str(key) => { - if let Ok(index) = i64::from_str(key) { - unsafe { - #[allow(clippy::cast_sign_loss)] - zend_hash_index_update(self, index as zend_ulong, &raw mut val) - }; - } else { - unsafe { - zend_hash_str_update( - self, - CString::new(key)?.as_ptr(), - key.len(), - &raw mut val, - ) - }; - } - } - } - val.release(); - Ok(()) - } - - /// Inserts an item into the hash table at a specified index, or updates if - /// the key already exists. Returns nothing in a result if successful. - /// - /// # Parameters - /// - /// * `key` - The index at which the value should be inserted. - /// * `val` - The value to insert into the hash table. - /// - /// # Returns - /// - /// Returns nothing in a result on success. - /// - /// # Errors - /// - /// Returns an error if converting the value into a [`Zval`] failed. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.insert_at_index(0, "A"); - /// ht.insert_at_index(5, "B"); - /// ht.insert_at_index(0, "C"); // notice overriding index 0 - /// assert_eq!(ht.len(), 2); - /// ``` - pub fn insert_at_index(&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(()) - } - - /// Pushes an item onto the end of the hash table. Returns a result - /// containing nothing if the element was successfully inserted. - /// - /// # Parameters - /// - /// * `val` - The value to insert into the hash table. - /// - /// # Returns - /// - /// Returns nothing in a result on success. - /// - /// # Errors - /// - /// Returns an error if converting the value into a [`Zval`] failed. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push("a"); - /// ht.push("b"); - /// ht.push("c"); - /// assert_eq!(ht.len(), 3); - /// ``` - pub fn push(&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(()) - } - - /// Checks if the hashtable only contains numerical keys. - /// - /// # Returns - /// - /// True if all keys on the hashtable are numerical. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push(0); - /// ht.push(3); - /// ht.push(9); - /// assert!(ht.has_numerical_keys()); - /// - /// ht.insert("obviously not numerical", 10); - /// assert!(!ht.has_numerical_keys()); - /// ``` - #[must_use] - pub fn has_numerical_keys(&self) -> bool { - !self.into_iter().any(|(k, _)| !k.is_long()) - } - - /// Checks if the hashtable has numerical, sequential keys. - /// - /// # Returns - /// - /// True if all keys on the hashtable are numerical and are in sequential - /// order (i.e. starting at 0 and not skipping any keys). - /// - /// # Panics - /// - /// Panics if the number of elements in the hashtable exceeds `i64::MAX`. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// ht.push(0); - /// ht.push(3); - /// ht.push(9); - /// assert!(ht.has_sequential_keys()); - /// - /// ht.insert_at_index(90, 10); - /// assert!(!ht.has_sequential_keys()); - /// ``` - #[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) - } - - /// Returns an iterator over the values contained inside the hashtable, as - /// if it was a set or list. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// for val in ht.values() { - /// dbg!(val); - /// } - #[inline] - #[must_use] - pub fn values(&self) -> Values<'_> { - Values::new(self) - } - - /// Returns an iterator over the key(s) and value contained inside the - /// hashtable. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// for (key, val) in ht.iter() { - /// match &key { - /// ArrayKey::Long(index) => { - /// } - /// ArrayKey::String(key) => { - /// } - /// ArrayKey::Str(key) => { - /// } - /// } - /// dbg!(key, val); - /// } - #[inline] - #[must_use] - pub fn iter(&self) -> Iter<'_> { - self.into_iter() - } -} - -unsafe impl ZBoxable for ZendHashTable { - fn free(&mut self) { - // SAFETY: ZBox has immutable access to `self`. - 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; - - fn to_owned(&self) -> Self::Owned { - unsafe { - // SAFETY: FFI call does not modify `self`, returns a new hashtable. - let ptr = zend_array_dup(ptr::from_ref(self).cast_mut()); - - // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not. - ZBox::from_raw( - ptr.as_mut() - .expect("Failed to allocate memory for hashtable"), - ) - } - } -} - -/// Immutable iterator upon a reference to a hashtable. -pub struct Iter<'a> { - ht: &'a ZendHashTable, - current_num: i64, - end_num: i64, - pos: HashPosition, - end_pos: HashPosition, -} - -/// Represents the key of a PHP array, which can be either a long or a string. -#[derive(Debug, Clone, PartialEq)] -pub enum ArrayKey<'a> { - /// A numerical key. - /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs - /// to be cast to `zend_ulong` before passing into Zend functions. - Long(i64), - /// A string key. - String(String), - /// A string key by reference. - Str(&'a str), -} - -impl From for ArrayKey<'_> { - fn from(value: String) -> Self { - Self::String(value) - } -} - -impl TryFrom> for String { - type Error = Error; - - fn try_from(value: ArrayKey<'_>) -> std::result::Result { - match value { - ArrayKey::String(s) => Ok(s), - ArrayKey::Str(s) => Ok(s.to_string()), - ArrayKey::Long(_) => Err(Error::InvalidProperty), - } - } -} - -impl TryFrom> for i64 { - type Error = Error; - - fn try_from(value: ArrayKey<'_>) -> std::result::Result { - match value { - ArrayKey::Long(i) => Ok(i), - ArrayKey::String(s) => s.parse::().map_err(|_| Error::InvalidProperty), - ArrayKey::Str(s) => s.parse::().map_err(|_| Error::InvalidProperty), - } - } -} - -impl ArrayKey<'_> { - /// Check if the key is an integer. - /// - /// # Returns - /// - /// Returns true if the key is an integer, false otherwise. - #[must_use] - pub fn is_long(&self) -> bool { - match self { - ArrayKey::Long(_) => true, - ArrayKey::String(_) | ArrayKey::Str(_) => false, - } - } -} - -impl Display for ArrayKey<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - ArrayKey::Long(key) => write!(f, "{key}"), - ArrayKey::String(key) => write!(f, "{key}"), - ArrayKey::Str(key) => write!(f, "{key}"), - } - } -} - -impl<'a> From<&'a str> for ArrayKey<'a> { - fn from(key: &'a str) -> ArrayKey<'a> { - ArrayKey::Str(key) - } -} - -impl<'a> From for ArrayKey<'a> { - fn from(index: i64) -> ArrayKey<'a> { - ArrayKey::Long(index) - } -} - -impl<'a> FromZval<'a> for ArrayKey<'_> { - const TYPE: DataType = DataType::String; - - fn from_zval(zval: &'a Zval) -> Option { - if let Some(key) = zval.long() { - return Some(ArrayKey::Long(key)); - } - if let Some(key) = zval.string() { - return Some(ArrayKey::String(key)); - } - None - } -} - -impl<'a> Iter<'a> { - /// Creates a new iterator over a hashtable. - /// - /// # Parameters - /// - /// * `ht` - The hashtable to iterate. - pub fn new(ht: &'a ZendHashTable) -> Self { - let end_num: i64 = ht - .len() - .try_into() - .expect("Integer overflow in hashtable length"); - let end_pos = if ht.nNumOfElements > 0 { - ht.nNumOfElements - 1 - } else { - 0 - }; - - Self { - ht, - current_num: 0, - end_num, - pos: 0, - end_pos, - } - } -} - -impl<'a> IntoIterator for &'a ZendHashTable { - type Item = (ArrayKey<'a>, &'a Zval); - type IntoIter = Iter<'a>; - - /// Returns an iterator over the key(s) and value contained inside the - /// hashtable. - /// - /// # Example - /// - /// ```no_run - /// use ext_php_rs::types::ZendHashTable; - /// - /// let mut ht = ZendHashTable::new(); - /// - /// for (key, val) in ht.iter() { - /// // ^ Index if inserted at an index. - /// // ^ Optional string key, if inserted like a hashtable. - /// // ^ Inserted value. - /// - /// dbg!(key, val); - /// } - #[inline] - fn into_iter(self) -> Self::IntoIter { - Iter::new(self) - } -} - -impl<'a> Iterator for Iter<'a> { - type Item = (ArrayKey<'a>, &'a Zval); - - fn next(&mut self) -> Option { - self.next_zval() - .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v)) - } - - fn count(self) -> usize - where - Self: Sized, - { - self.ht.len() - } -} - -impl ExactSizeIterator for Iter<'_> { - fn len(&self) -> usize { - self.ht.len() - } -} - -impl DoubleEndedIterator for Iter<'_> { - fn next_back(&mut self) -> Option { - if self.end_num <= self.current_num { - return None; - } - - let key_type = unsafe { - zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) - }; - - if key_type == -1 { - return None; - } - - let key = Zval::new(); - - unsafe { - zend_hash_get_current_key_zval_ex( - ptr::from_ref(self.ht).cast_mut(), - (&raw const key).cast_mut(), - &raw mut self.end_pos, - ); - } - let value = unsafe { - &*zend_hash_get_current_data_ex( - ptr::from_ref(self.ht).cast_mut(), - &raw mut self.end_pos, - ) - }; - - let key = match ArrayKey::from_zval(&key) { - Some(key) => key, - None => ArrayKey::Long(self.end_num), - }; - - unsafe { - zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos) - }; - self.end_num -= 1; - - Some((key, value)) - } -} - -impl<'a> Iter<'a> { - pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> { - if self.current_num >= self.end_num { - return None; - } - - let key_type = unsafe { - zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) - }; - - // Key type `-1` is ??? - // Key type `1` is string - // Key type `2` is long - // Key type `3` is null meaning the end of the array - if key_type == -1 || key_type == 3 { - return None; - } - - let mut key = Zval::new(); - - unsafe { - zend_hash_get_current_key_zval_ex( - ptr::from_ref(self.ht).cast_mut(), - (&raw const key).cast_mut(), - &raw mut self.pos, - ); - } - let value = unsafe { - let val_ptr = - zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos); - - if val_ptr.is_null() { - return None; - } - - &*val_ptr - }; - - if !key.is_long() && !key.is_string() { - key.set_long(self.current_num); - } - - unsafe { zend_hash_move_forward_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) }; - self.current_num += 1; - - Some((key, value)) - } -} - -/// Immutable iterator which iterates over the values of the hashtable, as it -/// was a set or list. -pub struct Values<'a>(Iter<'a>); - -impl<'a> Values<'a> { - /// Creates a new iterator over a hashtables values. - /// - /// # Parameters - /// - /// * `ht` - The hashtable to iterate. - pub fn new(ht: &'a ZendHashTable) -> Self { - Self(Iter::new(ht)) - } -} - -impl<'a> Iterator for Values<'a> { - type Item = &'a Zval; - - fn next(&mut self) -> Option { - self.0.next().map(|(_, zval)| zval) - } - - fn count(self) -> usize - where - Self: Sized, - { - self.0.count() - } -} - -impl ExactSizeIterator for Values<'_> { - fn len(&self) -> usize { - self.0.len() - } -} - -impl DoubleEndedIterator for Values<'_> { - fn next_back(&mut self) -> Option { - self.0.next_back().map(|(_, zval)| zval) - } -} - -impl Default for ZBox { - fn default() -> Self { - ZendHashTable::new() - } -} - -impl Clone for ZBox { - fn clone(&self) -> Self { - (**self).to_owned() - } -} - -impl IntoZval for ZBox { - 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 { - zval.array() - } -} - -/////////////////////////////////////////// -// HashMap -/////////////////////////////////////////// - -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap -where - V: FromZval<'a>, -{ - type Error = Error; - - fn try_from(value: &'a ZendHashTable) -> Result { - let mut hm = HashMap::with_capacity(value.len()); - - for (key, val) in value { - hm.insert( - key.to_string(), - V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, - ); - } - - Ok(hm) - } -} - -impl TryFrom> for ZBox -where - K: AsRef, - V: IntoZval, -{ - type Error = Error; - - fn try_from(value: HashMap) -> Result { - let mut ht = ZendHashTable::with_capacity( - value.len().try_into().map_err(|_| Error::IntegerOverflow)?, - ); - - for (k, v) in value { - ht.insert(k.as_ref(), v)?; - } - - Ok(ht) - } -} - -impl<'a, K, V> TryFrom<&'a ZendHashTable> for Vec<(K, V)> -where - K: TryFrom, Error = Error>, - V: FromZval<'a>, -{ - type Error = Error; - - fn try_from(value: &'a ZendHashTable) -> Result { - let mut vec = Vec::with_capacity(value.len()); - - for (key, val) in value { - vec.push(( - key.try_into()?, - V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, - )); - } - - Ok(vec) - } -} - -impl<'a, V> TryFrom<&'a ZendHashTable> for Vec<(ArrayKey<'a>, V)> -where - V: FromZval<'a>, -{ - type Error = Error; - - fn try_from(value: &'a ZendHashTable) -> Result { - let mut vec = Vec::with_capacity(value.len()); - - for (key, val) in value { - vec.push(( - key, - V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, - )); - } - - Ok(vec) - } -} - -impl<'a, K, V> TryFrom> for ZBox -where - K: Into>, - V: IntoZval, -{ - type Error = Error; - - fn try_from(value: Vec<(K, V)>) -> Result { - let mut ht = ZendHashTable::with_capacity( - value.len().try_into().map_err(|_| Error::IntegerOverflow)?, - ); - - for (k, v) in value { - ht.insert(k, v)?; - } - - Ok(ht) - } -} - -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl IntoZval for HashMap -where - K: AsRef, - V: IntoZval, -{ - const TYPE: DataType = DataType::Array; - const NULLABLE: bool = false; - - fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { - let arr = self.try_into()?; - zv.set_hashtable(arr); - Ok(()) - } -} - -// TODO: Generalize hasher -#[allow(clippy::implicit_hasher)] -impl<'a, T> FromZval<'a> for HashMap -where - T: FromZval<'a>, -{ - const TYPE: DataType = DataType::Array; - - fn from_zval(zval: &'a Zval) -> Option { - zval.array().and_then(|arr| arr.try_into().ok()) - } -} - -impl<'a, K, V> IntoZval for Vec<(K, V)> -where - K: Into>, - V: IntoZval, -{ - const TYPE: DataType = DataType::Array; - const NULLABLE: bool = false; - - fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { - let arr = self.try_into()?; - zv.set_hashtable(arr); - Ok(()) - } -} - -impl<'a, K, V> FromZval<'a> for Vec<(K, V)> -where - K: TryFrom, Error = Error>, - V: FromZval<'a>, -{ - const TYPE: DataType = DataType::Array; - - fn from_zval(zval: &'a Zval) -> Option { - zval.array().and_then(|arr| arr.try_into().ok()) - } -} - -impl<'a, V> FromZval<'a> for Vec<(ArrayKey<'a>, V)> -where - V: FromZval<'a>, -{ - const TYPE: DataType = DataType::Array; - - fn from_zval(zval: &'a Zval) -> Option { - zval.array().and_then(|arr| arr.try_into().ok()) - } -} - -/////////////////////////////////////////// -// Vec -/////////////////////////////////////////// - -impl<'a, T> TryFrom<&'a ZendHashTable> for Vec -where - T: FromZval<'a>, -{ - type Error = Error; - - fn try_from(value: &'a ZendHashTable) -> Result { - let mut vec = Vec::with_capacity(value.len()); - - for (_, val) in value { - vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?); - } - - Ok(vec) - } -} - -impl TryFrom> for ZBox -where - T: IntoZval, -{ - type Error = Error; - - fn try_from(value: Vec) -> Result { - let mut ht = ZendHashTable::with_capacity( - value.len().try_into().map_err(|_| Error::IntegerOverflow)?, - ); - - for val in value { - ht.push(val)?; - } - - Ok(ht) - } -} - -impl IntoZval for Vec -where - T: IntoZval, -{ - const TYPE: DataType = DataType::Array; - const NULLABLE: bool = false; - - fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { - let arr = self.try_into()?; - zv.set_hashtable(arr); - Ok(()) - } -} - -impl<'a, T> FromZval<'a> for Vec -where - T: FromZval<'a>, -{ - const TYPE: DataType = DataType::Array; - - fn from_zval(zval: &'a Zval) -> Option { - zval.array().and_then(|arr| arr.try_into().ok()) - } -} - -impl FromIterator for ZBox { - fn from_iter>(iter: T) -> Self { - let mut ht = ZendHashTable::new(); - for item in iter { - // Inserting a zval cannot fail, as `push` only returns `Err` if converting - // `val` to a zval fails. - let _ = ht.push(item); - } - ht - } -} - -impl FromIterator<(i64, Zval)> for ZBox { - fn from_iter>(iter: T) -> Self { - let mut ht = ZendHashTable::new(); - for (key, val) in iter { - // Inserting a zval cannot fail, as `push` only returns `Err` if converting - // `val` to a zval fails. - let _ = ht.insert_at_index(key, val); - } - ht - } -} - -impl<'a> FromIterator<(&'a str, Zval)> for ZBox { - fn from_iter>(iter: T) -> Self { - let mut ht = ZendHashTable::new(); - for (key, val) in iter { - // Inserting a zval cannot fail, as `push` only returns `Err` if converting - // `val` to a zval fails. - let _ = ht.insert(key, val); - } - ht - } -} - -#[cfg(test)] -#[cfg(feature = "embed")] -#[allow(clippy::unwrap_used)] -mod tests { - use super::*; - use crate::embed::Embed; - - #[test] - fn test_string_try_from_array_key() { - let key = ArrayKey::String("test".to_string()); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "test".to_string()); - - let key = ArrayKey::Str("test"); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "test".to_string()); - - let key = ArrayKey::Long(42); - let result: Result = key.try_into(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), Error::InvalidProperty)); - - let key = ArrayKey::String("42".to_string()); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "42".to_string()); - - let key = ArrayKey::Str("123"); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 123); - } - - #[test] - fn test_i64_try_from_array_key() { - let key = ArrayKey::Long(42); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 42); - - let key = ArrayKey::String("42".to_string()); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 42); - - let key = ArrayKey::Str("123"); - let result: Result = key.try_into(); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), 123); - - let key = ArrayKey::String("not a number".to_string()); - let result: Result = key.try_into(); - assert!(result.is_err()); - assert!(matches!(result.unwrap_err(), Error::InvalidProperty)); - } - - #[test] - fn test_vec_string_v_try_from_hash_table() { - Embed::run(|| { - let mut ht = ZendHashTable::new(); - ht.insert("key1", "value1").unwrap(); - ht.insert("key2", "value2").unwrap(); - - let vec: Vec<(String, String)> = ht.as_ref().try_into().unwrap(); - assert_eq!(vec.len(), 2); - assert_eq!(vec[0].0, "key1"); - assert_eq!(vec[0].1, "value1"); - assert_eq!(vec[1].0, "key2"); - assert_eq!(vec[1].1, "value2"); - - let mut ht2 = ZendHashTable::new(); - ht2.insert(1, "value1").unwrap(); - ht2.insert(2, "value2").unwrap(); - - let vec2: Result> = ht2.as_ref().try_into(); - assert!(vec2.is_err()); - assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty)); - }); - } - - #[test] - fn test_vec_i64_v_try_from_hash_table() { - Embed::run(|| { - let mut ht = ZendHashTable::new(); - ht.insert(1, "value1").unwrap(); - ht.insert("2", "value2").unwrap(); - - let vec: Vec<(i64, String)> = ht.as_ref().try_into().unwrap(); - assert_eq!(vec.len(), 2); - assert_eq!(vec[0].0, 1); - assert_eq!(vec[0].1, "value1"); - assert_eq!(vec[1].0, 2); - assert_eq!(vec[1].1, "value2"); - - let mut ht2 = ZendHashTable::new(); - ht2.insert("key1", "value1").unwrap(); - ht2.insert("key2", "value2").unwrap(); - - let vec2: Result> = ht2.as_ref().try_into(); - assert!(vec2.is_err()); - assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty)); - }); - } - - #[test] - fn test_vec_array_key_v_try_from_hash_table() { - Embed::run(|| { - let mut ht = ZendHashTable::new(); - ht.insert("key1", "value1").unwrap(); - ht.insert(2, "value2").unwrap(); - ht.insert("3", "value3").unwrap(); - - let vec: Vec<(ArrayKey, String)> = ht.as_ref().try_into().unwrap(); - assert_eq!(vec.len(), 3); - assert_eq!(vec[0].0, ArrayKey::String("key1".to_string())); - assert_eq!(vec[0].1, "value1"); - assert_eq!(vec[1].0, ArrayKey::Long(2)); - assert_eq!(vec[1].1, "value2"); - assert_eq!(vec[2].0, ArrayKey::Long(3)); - assert_eq!(vec[2].1, "value3"); - }); - } - - #[test] - fn test_hash_table_try_from_vec() { - Embed::run(|| { - let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")]; - - let ht: ZBox = vec.try_into().unwrap(); - assert_eq!(ht.len(), 3); - assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); - assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); - assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); - - let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")]; - - let ht_i64: ZBox = vec_i64.try_into().unwrap(); - assert_eq!(ht_i64.len(), 3); - assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); - assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); - assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); - }); - } - - #[test] - fn test_vec_k_v_into_zval() { - Embed::run(|| { - let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")]; - - let zval = vec.into_zval(false).unwrap(); - assert!(zval.is_array()); - let ht: &ZendHashTable = zval.array().unwrap(); - assert_eq!(ht.len(), 3); - assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); - assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); - assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); - - let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")]; - let zval_i64 = vec_i64.into_zval(false).unwrap(); - assert!(zval_i64.is_array()); - let ht_i64: &ZendHashTable = zval_i64.array().unwrap(); - assert_eq!(ht_i64.len(), 3); - assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); - assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); - assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); - }); - } - - #[test] - fn test_vec_k_v_from_zval() { - Embed::run(|| { - let mut ht = ZendHashTable::new(); - ht.insert("key1", "value1").unwrap(); - ht.insert("key2", "value2").unwrap(); - ht.insert("key3", "value3").unwrap(); - let mut zval = Zval::new(); - zval.set_hashtable(ht); - - let vec: Vec<(String, String)> = Vec::<(String, String)>::from_zval(&zval).unwrap(); - assert_eq!(vec.len(), 3); - assert_eq!(vec[0].0, "key1"); - assert_eq!(vec[0].1, "value1"); - assert_eq!(vec[1].0, "key2"); - assert_eq!(vec[1].1, "value2"); - assert_eq!(vec[2].0, "key3"); - assert_eq!(vec[2].1, "value3"); - - let mut ht_i64 = ZendHashTable::new(); - ht_i64.insert(1, "value1").unwrap(); - ht_i64.insert("2", "value2").unwrap(); - ht_i64.insert(3, "value3").unwrap(); - let mut zval_i64 = Zval::new(); - zval_i64.set_hashtable(ht_i64); - - let vec_i64: Vec<(i64, String)> = Vec::<(i64, String)>::from_zval(&zval_i64).unwrap(); - assert_eq!(vec_i64.len(), 3); - assert_eq!(vec_i64[0].0, 1); - assert_eq!(vec_i64[0].1, "value1"); - assert_eq!(vec_i64[1].0, 2); - assert_eq!(vec_i64[1].1, "value2"); - assert_eq!(vec_i64[2].0, 3); - assert_eq!(vec_i64[2].1, "value3"); - - let mut ht_mixed = ZendHashTable::new(); - ht_mixed.insert("key1", "value1").unwrap(); - ht_mixed.insert(2, "value2").unwrap(); - ht_mixed.insert("3", "value3").unwrap(); - let mut zval_mixed = Zval::new(); - zval_mixed.set_hashtable(ht_mixed); - - let vec_mixed: Option> = - Vec::<(String, String)>::from_zval(&zval_mixed); - assert!(vec_mixed.is_none()); - }); - } - - #[test] - fn test_vec_array_key_v_from_zval() { - Embed::run(|| { - let mut ht = ZendHashTable::new(); - ht.insert("key1", "value1").unwrap(); - ht.insert(2, "value2").unwrap(); - ht.insert("3", "value3").unwrap(); - let mut zval = Zval::new(); - zval.set_hashtable(ht); - - let vec: Vec<(ArrayKey, String)> = Vec::<(ArrayKey, String)>::from_zval(&zval).unwrap(); - assert_eq!(vec.len(), 3); - assert_eq!(vec[0].0, ArrayKey::String("key1".to_string())); - assert_eq!(vec[0].1, "value1"); - assert_eq!(vec[1].0, ArrayKey::Long(2)); - assert_eq!(vec[1].1, "value2"); - assert_eq!(vec[2].0, ArrayKey::Long(3)); - assert_eq!(vec[2].1, "value3"); - }); - } -} diff --git a/src/types/array/array_key.rs b/src/types/array/array_key.rs new file mode 100644 index 000000000..d229ce3e8 --- /dev/null +++ b/src/types/array/array_key.rs @@ -0,0 +1,156 @@ +use std::{convert::TryFrom, fmt::Display}; + +use crate::{convert::FromZval, error::Error, flags::DataType, types::Zval}; + +/// Represents the key of a PHP array, which can be either a long or a string. +#[derive(Debug, Clone, PartialEq)] +pub enum ArrayKey<'a> { + /// A numerical key. + /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs + /// to be cast to `zend_ulong` before passing into Zend functions. + Long(i64), + /// A string key. + String(String), + /// A string key by reference. + Str(&'a str), +} + +impl From for ArrayKey<'_> { + fn from(value: String) -> Self { + Self::String(value) + } +} + +impl TryFrom> for String { + type Error = Error; + + fn try_from(value: ArrayKey<'_>) -> std::result::Result { + match value { + ArrayKey::String(s) => Ok(s), + ArrayKey::Str(s) => Ok(s.to_string()), + ArrayKey::Long(_) => Err(Error::InvalidProperty), + } + } +} + +impl TryFrom> for i64 { + type Error = Error; + + fn try_from(value: ArrayKey<'_>) -> std::result::Result { + match value { + ArrayKey::Long(i) => Ok(i), + ArrayKey::String(s) => s.parse::().map_err(|_| Error::InvalidProperty), + ArrayKey::Str(s) => s.parse::().map_err(|_| Error::InvalidProperty), + } + } +} + +impl ArrayKey<'_> { + /// Check if the key is an integer. + /// + /// # Returns + /// + /// Returns true if the key is an integer, false otherwise. + #[must_use] + pub fn is_long(&self) -> bool { + match self { + ArrayKey::Long(_) => true, + ArrayKey::String(_) | ArrayKey::Str(_) => false, + } + } +} + +impl Display for ArrayKey<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ArrayKey::Long(key) => write!(f, "{key}"), + ArrayKey::String(key) => write!(f, "{key}"), + ArrayKey::Str(key) => write!(f, "{key}"), + } + } +} + +impl<'a> From<&'a str> for ArrayKey<'a> { + fn from(key: &'a str) -> ArrayKey<'a> { + ArrayKey::Str(key) + } +} + +impl<'a> From for ArrayKey<'a> { + fn from(index: i64) -> ArrayKey<'a> { + ArrayKey::Long(index) + } +} + +impl<'a> FromZval<'a> for ArrayKey<'_> { + const TYPE: DataType = DataType::String; + + fn from_zval(zval: &'a Zval) -> Option { + if let Some(key) = zval.long() { + return Some(ArrayKey::Long(key)); + } + if let Some(key) = zval.string() { + return Some(ArrayKey::String(key)); + } + None + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use crate::error::Error; + use crate::types::ArrayKey; + + #[test] + fn test_string_try_from_array_key() { + let key = ArrayKey::String("test".to_string()); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "test".to_string()); + + let key = ArrayKey::Str("test"); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "test".to_string()); + + let key = ArrayKey::Long(42); + let result: crate::error::Result = key.try_into(); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::InvalidProperty)); + + let key = ArrayKey::String("42".to_string()); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "42".to_string()); + + let key = ArrayKey::Str("123"); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 123); + } + + #[test] + fn test_i64_try_from_array_key() { + let key = ArrayKey::Long(42); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 42); + + let key = ArrayKey::String("42".to_string()); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 42); + + let key = ArrayKey::Str("123"); + let result: crate::error::Result = key.try_into(); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 123); + + let key = ArrayKey::String("not a number".to_string()); + let result: crate::error::Result = key.try_into(); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), Error::InvalidProperty)); + } +} diff --git a/src/types/array/conversions/hash_map.rs b/src/types/array/conversions/hash_map.rs new file mode 100644 index 000000000..768c737a2 --- /dev/null +++ b/src/types/array/conversions/hash_map.rs @@ -0,0 +1,83 @@ +use std::{collections::HashMap, convert::TryFrom}; + +use crate::{ + boxed::ZBox, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + flags::DataType, + types::Zval, +}; + +use super::super::ZendHashTable; + +// TODO: Generalize hasher +#[allow(clippy::implicit_hasher)] +impl<'a, V> TryFrom<&'a ZendHashTable> for HashMap +where + V: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut hm = HashMap::with_capacity(value.len()); + + for (key, val) in value { + hm.insert( + key.to_string(), + V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, + ); + } + + Ok(hm) + } +} + +impl TryFrom> for ZBox +where + K: AsRef, + V: IntoZval, +{ + type Error = Error; + + fn try_from(value: HashMap) -> Result { + let mut ht = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for (k, v) in value { + ht.insert(k.as_ref(), v)?; + } + + Ok(ht) + } +} + +// TODO: Generalize hasher +#[allow(clippy::implicit_hasher)] +impl IntoZval for HashMap +where + K: AsRef, + V: IntoZval, +{ + const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; + + fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { + let arr = self.try_into()?; + zv.set_hashtable(arr); + Ok(()) + } +} + +// TODO: Generalize hasher +#[allow(clippy::implicit_hasher)] +impl<'a, T> FromZval<'a> for HashMap +where + T: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} diff --git a/src/types/array/conversions/mod.rs b/src/types/array/conversions/mod.rs new file mode 100644 index 000000000..6748bb599 --- /dev/null +++ b/src/types/array/conversions/mod.rs @@ -0,0 +1,13 @@ +//! Collection type conversions for `ZendHashTable`. +//! +//! This module provides conversions between Rust collection types and PHP arrays +//! (represented as `ZendHashTable`). Each collection type has its own module for +//! better organization and maintainability. +//! +//! ## Supported Collections +//! +//! - `HashMap` ↔ `ZendHashTable` (via `hash_map` module) +//! - `Vec` and `Vec<(K, V)>` ↔ `ZendHashTable` (via `vec` module) + +mod hash_map; +mod vec; diff --git a/src/types/array/conversions/vec.rs b/src/types/array/conversions/vec.rs new file mode 100644 index 000000000..0b61b8cc9 --- /dev/null +++ b/src/types/array/conversions/vec.rs @@ -0,0 +1,371 @@ +use std::convert::TryFrom; + +use crate::{ + boxed::ZBox, + convert::{FromZval, IntoZval}, + error::{Error, Result}, + flags::DataType, + types::Zval, +}; + +use super::super::{ArrayKey, ZendHashTable}; + +/////////////////////////////////////////// +// Vec<(K, V)> conversions +/////////////////////////////////////////// + +impl<'a, K, V> TryFrom<&'a ZendHashTable> for Vec<(K, V)> +where + K: TryFrom, Error = Error>, + V: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut vec = Vec::with_capacity(value.len()); + + for (key, val) in value { + vec.push(( + key.try_into()?, + V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, + )); + } + + Ok(vec) + } +} + +impl<'a, V> TryFrom<&'a ZendHashTable> for Vec<(ArrayKey<'a>, V)> +where + V: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut vec = Vec::with_capacity(value.len()); + + for (key, val) in value { + vec.push(( + key, + V::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?, + )); + } + + Ok(vec) + } +} + +impl<'a, K, V> TryFrom> for ZBox +where + K: Into>, + V: IntoZval, +{ + type Error = Error; + + fn try_from(value: Vec<(K, V)>) -> Result { + let mut ht = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for (k, v) in value { + ht.insert(k, v)?; + } + + Ok(ht) + } +} + +impl<'a, K, V> IntoZval for Vec<(K, V)> +where + K: Into>, + V: IntoZval, +{ + const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; + + fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { + let arr = self.try_into()?; + zv.set_hashtable(arr); + Ok(()) + } +} + +impl<'a, K, V> FromZval<'a> for Vec<(K, V)> +where + K: TryFrom, Error = Error>, + V: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +impl<'a, V> FromZval<'a> for Vec<(ArrayKey<'a>, V)> +where + V: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +/////////////////////////////////////////// +// Vec conversions +/////////////////////////////////////////// + +impl<'a, T> TryFrom<&'a ZendHashTable> for Vec +where + T: FromZval<'a>, +{ + type Error = Error; + + fn try_from(value: &'a ZendHashTable) -> Result { + let mut vec = Vec::with_capacity(value.len()); + + for (_, val) in value { + vec.push(T::from_zval(val).ok_or_else(|| Error::ZvalConversion(val.get_type()))?); + } + + Ok(vec) + } +} + +impl TryFrom> for ZBox +where + T: IntoZval, +{ + type Error = Error; + + fn try_from(value: Vec) -> Result { + let mut ht = ZendHashTable::with_capacity( + value.len().try_into().map_err(|_| Error::IntegerOverflow)?, + ); + + for val in value { + ht.push(val)?; + } + + Ok(ht) + } +} + +impl IntoZval for Vec +where + T: IntoZval, +{ + const TYPE: DataType = DataType::Array; + const NULLABLE: bool = false; + + fn set_zval(self, zv: &mut Zval, _: bool) -> Result<()> { + let arr = self.try_into()?; + zv.set_hashtable(arr); + Ok(()) + } +} + +impl<'a, T> FromZval<'a> for Vec +where + T: FromZval<'a>, +{ + const TYPE: DataType = DataType::Array; + + fn from_zval(zval: &'a Zval) -> Option { + zval.array().and_then(|arr| arr.try_into().ok()) + } +} + +#[cfg(test)] +#[cfg(feature = "embed")] +#[allow(clippy::unwrap_used)] +mod tests { + use crate::boxed::ZBox; + use crate::convert::{FromZval, IntoZval}; + use crate::embed::Embed; + use crate::error::Error; + use crate::types::{ArrayKey, ZendHashTable, Zval}; + + #[test] + fn test_hash_table_try_from_vec() { + Embed::run(|| { + let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")]; + + let ht: ZBox = vec.try_into().unwrap(); + assert_eq!(ht.len(), 3); + assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); + assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); + assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); + + let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")]; + + let ht_i64: ZBox = vec_i64.try_into().unwrap(); + assert_eq!(ht_i64.len(), 3); + assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); + assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); + assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); + }); + } + + #[test] + fn test_vec_k_v_into_zval() { + Embed::run(|| { + let vec = vec![("key1", "value1"), ("key2", "value2"), ("key3", "value3")]; + + let zval = vec.into_zval(false).unwrap(); + assert!(zval.is_array()); + let ht: &ZendHashTable = zval.array().unwrap(); + assert_eq!(ht.len(), 3); + assert_eq!(ht.get("key1").unwrap().string().unwrap(), "value1"); + assert_eq!(ht.get("key2").unwrap().string().unwrap(), "value2"); + assert_eq!(ht.get("key3").unwrap().string().unwrap(), "value3"); + + let vec_i64 = vec![(1, "value1"), (2, "value2"), (3, "value3")]; + let zval_i64 = vec_i64.into_zval(false).unwrap(); + assert!(zval_i64.is_array()); + let ht_i64: &ZendHashTable = zval_i64.array().unwrap(); + assert_eq!(ht_i64.len(), 3); + assert_eq!(ht_i64.get(1).unwrap().string().unwrap(), "value1"); + assert_eq!(ht_i64.get(2).unwrap().string().unwrap(), "value2"); + assert_eq!(ht_i64.get(3).unwrap().string().unwrap(), "value3"); + }); + } + + #[test] + fn test_vec_k_v_from_zval() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert("key2", "value2").unwrap(); + ht.insert("key3", "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let vec: Vec<(String, String)> = Vec::<(String, String)>::from_zval(&zval).unwrap(); + assert_eq!(vec.len(), 3); + assert_eq!(vec[0].0, "key1"); + assert_eq!(vec[0].1, "value1"); + assert_eq!(vec[1].0, "key2"); + assert_eq!(vec[1].1, "value2"); + assert_eq!(vec[2].0, "key3"); + assert_eq!(vec[2].1, "value3"); + + let mut ht_i64 = ZendHashTable::new(); + ht_i64.insert(1, "value1").unwrap(); + ht_i64.insert("2", "value2").unwrap(); + ht_i64.insert(3, "value3").unwrap(); + let mut zval_i64 = Zval::new(); + zval_i64.set_hashtable(ht_i64); + + let vec_i64: Vec<(i64, String)> = Vec::<(i64, String)>::from_zval(&zval_i64).unwrap(); + assert_eq!(vec_i64.len(), 3); + assert_eq!(vec_i64[0].0, 1); + assert_eq!(vec_i64[0].1, "value1"); + assert_eq!(vec_i64[1].0, 2); + assert_eq!(vec_i64[1].1, "value2"); + assert_eq!(vec_i64[2].0, 3); + assert_eq!(vec_i64[2].1, "value3"); + + let mut ht_mixed = ZendHashTable::new(); + ht_mixed.insert("key1", "value1").unwrap(); + ht_mixed.insert(2, "value2").unwrap(); + ht_mixed.insert("3", "value3").unwrap(); + let mut zval_mixed = Zval::new(); + zval_mixed.set_hashtable(ht_mixed); + + let vec_mixed: Option> = + Vec::<(String, String)>::from_zval(&zval_mixed); + assert!(vec_mixed.is_none()); + }); + } + + #[test] + fn test_vec_array_key_v_from_zval() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert(2, "value2").unwrap(); + ht.insert("3", "value3").unwrap(); + let mut zval = Zval::new(); + zval.set_hashtable(ht); + + let vec: Vec<(ArrayKey, String)> = Vec::<(ArrayKey, String)>::from_zval(&zval).unwrap(); + assert_eq!(vec.len(), 3); + assert_eq!(vec[0].0, ArrayKey::String("key1".to_string())); + assert_eq!(vec[0].1, "value1"); + assert_eq!(vec[1].0, ArrayKey::Long(2)); + assert_eq!(vec[1].1, "value2"); + assert_eq!(vec[2].0, ArrayKey::Long(3)); + assert_eq!(vec[2].1, "value3"); + }); + } + + #[test] + fn test_vec_i64_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert(1, "value1").unwrap(); + ht.insert("2", "value2").unwrap(); + + let vec: Vec<(i64, String)> = ht.as_ref().try_into().unwrap(); + assert_eq!(vec.len(), 2); + assert_eq!(vec[0].0, 1); + assert_eq!(vec[0].1, "value1"); + assert_eq!(vec[1].0, 2); + assert_eq!(vec[1].1, "value2"); + + let mut ht2 = ZendHashTable::new(); + ht2.insert("key1", "value1").unwrap(); + ht2.insert("key2", "value2").unwrap(); + + let vec2: crate::error::Result> = ht2.as_ref().try_into(); + assert!(vec2.is_err()); + assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty)); + }); + } + + #[test] + fn test_vec_string_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert("key2", "value2").unwrap(); + + let vec: Vec<(String, String)> = ht.as_ref().try_into().unwrap(); + assert_eq!(vec.len(), 2); + assert_eq!(vec[0].0, "key1"); + assert_eq!(vec[0].1, "value1"); + assert_eq!(vec[1].0, "key2"); + assert_eq!(vec[1].1, "value2"); + + let mut ht2 = ZendHashTable::new(); + ht2.insert(1, "value1").unwrap(); + ht2.insert(2, "value2").unwrap(); + + let vec2: crate::error::Result> = ht2.as_ref().try_into(); + assert!(vec2.is_err()); + assert!(matches!(vec2.unwrap_err(), Error::InvalidProperty)); + }); + } + + #[test] + fn test_vec_array_key_v_try_from_hash_table() { + Embed::run(|| { + let mut ht = ZendHashTable::new(); + ht.insert("key1", "value1").unwrap(); + ht.insert(2, "value2").unwrap(); + ht.insert("3", "value3").unwrap(); + + let vec: Vec<(ArrayKey, String)> = ht.as_ref().try_into().unwrap(); + assert_eq!(vec.len(), 3); + assert_eq!(vec[0].0, ArrayKey::String("key1".to_string())); + assert_eq!(vec[0].1, "value1"); + assert_eq!(vec[1].0, ArrayKey::Long(2)); + assert_eq!(vec[1].1, "value2"); + assert_eq!(vec[2].0, ArrayKey::Long(3)); + assert_eq!(vec[2].1, "value3"); + }); + } +} diff --git a/src/types/array/iterators.rs b/src/types/array/iterators.rs new file mode 100644 index 000000000..8bdcd11aa --- /dev/null +++ b/src/types/array/iterators.rs @@ -0,0 +1,271 @@ +use std::{ + convert::TryInto, + iter::{DoubleEndedIterator, ExactSizeIterator, Iterator}, + ptr, +}; + +use super::{ArrayKey, ZendHashTable}; +use crate::boxed::ZBox; +use crate::{ + convert::FromZval, + ffi::{ + zend_hash_get_current_data_ex, zend_hash_get_current_key_type_ex, + zend_hash_get_current_key_zval_ex, zend_hash_move_backwards_ex, zend_hash_move_forward_ex, + HashPosition, + }, + types::Zval, +}; + +/// Immutable iterator upon a reference to a hashtable. +pub struct Iter<'a> { + ht: &'a ZendHashTable, + current_num: i64, + end_num: i64, + pos: HashPosition, + end_pos: HashPosition, +} + +impl<'a> Iter<'a> { + /// Creates a new iterator over a hashtable. + /// + /// # Parameters + /// + /// * `ht` - The hashtable to iterate. + pub fn new(ht: &'a ZendHashTable) -> Self { + let end_num: i64 = ht + .len() + .try_into() + .expect("Integer overflow in hashtable length"); + let end_pos = if ht.nNumOfElements > 0 { + ht.nNumOfElements - 1 + } else { + 0 + }; + + Self { + ht, + current_num: 0, + end_num, + pos: 0, + end_pos, + } + } + + pub fn next_zval(&mut self) -> Option<(Zval, &'a Zval)> { + if self.current_num >= self.end_num { + return None; + } + + let key_type = unsafe { + zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) + }; + + // Key type `-1` is ??? + // Key type `1` is string + // Key type `2` is long + // Key type `3` is null meaning the end of the array + if key_type == -1 || key_type == 3 { + return None; + } + + let mut key = Zval::new(); + + unsafe { + zend_hash_get_current_key_zval_ex( + ptr::from_ref(self.ht).cast_mut(), + (&raw const key).cast_mut(), + &raw mut self.pos, + ); + } + let value = unsafe { + let val_ptr = + zend_hash_get_current_data_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos); + + if val_ptr.is_null() { + return None; + } + + &*val_ptr + }; + + if !key.is_long() && !key.is_string() { + key.set_long(self.current_num); + } + + unsafe { zend_hash_move_forward_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) }; + self.current_num += 1; + + Some((key, value)) + } +} + +impl<'a> IntoIterator for &'a ZendHashTable { + type Item = (ArrayKey<'a>, &'a Zval); + type IntoIter = Iter<'a>; + + /// Returns an iterator over the key(s) and value contained inside the + /// hashtable. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// for (key, val) in ht.iter() { + /// // ^ Index if inserted at an index. + /// // ^ Optional string key, if inserted like a hashtable. + /// // ^ Inserted value. + /// + /// dbg!(key, val); + /// } + #[inline] + fn into_iter(self) -> Self::IntoIter { + Iter::new(self) + } +} + +impl<'a> Iterator for Iter<'a> { + type Item = (ArrayKey<'a>, &'a Zval); + + fn next(&mut self) -> Option { + self.next_zval() + .map(|(k, v)| (ArrayKey::from_zval(&k).expect("Invalid array key!"), v)) + } + + fn count(self) -> usize + where + Self: Sized, + { + self.ht.len() + } +} + +impl ExactSizeIterator for Iter<'_> { + fn len(&self) -> usize { + self.ht.len() + } +} + +impl DoubleEndedIterator for Iter<'_> { + fn next_back(&mut self) -> Option { + if self.end_num <= self.current_num { + return None; + } + + let key_type = unsafe { + zend_hash_get_current_key_type_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.pos) + }; + + if key_type == -1 { + return None; + } + + let key = Zval::new(); + + unsafe { + zend_hash_get_current_key_zval_ex( + ptr::from_ref(self.ht).cast_mut(), + (&raw const key).cast_mut(), + &raw mut self.end_pos, + ); + } + let value = unsafe { + &*zend_hash_get_current_data_ex( + ptr::from_ref(self.ht).cast_mut(), + &raw mut self.end_pos, + ) + }; + + let key = match ArrayKey::from_zval(&key) { + Some(key) => key, + None => ArrayKey::Long(self.end_num), + }; + + unsafe { + zend_hash_move_backwards_ex(ptr::from_ref(self.ht).cast_mut(), &raw mut self.end_pos) + }; + self.end_num -= 1; + + Some((key, value)) + } +} + +/// Immutable iterator which iterates over the values of the hashtable, as it +/// was a set or list. +pub struct Values<'a>(Iter<'a>); + +impl<'a> Values<'a> { + /// Creates a new iterator over a hashtables values. + /// + /// # Parameters + /// + /// * `ht` - The hashtable to iterate. + pub fn new(ht: &'a ZendHashTable) -> Self { + Self(Iter::new(ht)) + } +} + +impl<'a> Iterator for Values<'a> { + type Item = &'a Zval; + + fn next(&mut self) -> Option { + self.0.next().map(|(_, zval)| zval) + } + + fn count(self) -> usize + where + Self: Sized, + { + self.0.count() + } +} + +impl ExactSizeIterator for Values<'_> { + fn len(&self) -> usize { + self.0.len() + } +} + +impl DoubleEndedIterator for Values<'_> { + fn next_back(&mut self) -> Option { + self.0.next_back().map(|(_, zval)| zval) + } +} + +impl FromIterator for ZBox { + fn from_iter>(iter: T) -> Self { + let mut ht = ZendHashTable::new(); + for item in iter { + // Inserting a zval cannot fail, as `push` only returns `Err` if converting + // `val` to a zval fails. + let _ = ht.push(item); + } + ht + } +} + +impl FromIterator<(i64, Zval)> for ZBox { + fn from_iter>(iter: T) -> Self { + let mut ht = ZendHashTable::new(); + for (key, val) in iter { + // Inserting a zval cannot fail, as `push` only returns `Err` if converting + // `val` to a zval fails. + let _ = ht.insert_at_index(key, val); + } + ht + } +} + +impl<'a> FromIterator<(&'a str, Zval)> for ZBox { + fn from_iter>(iter: T) -> Self { + let mut ht = ZendHashTable::new(); + for (key, val) in iter { + // Inserting a zval cannot fail, as `push` only returns `Err` if converting + // `val` to a zval fails. + let _ = ht.insert(key, val); + } + ht + } +} diff --git a/src/types/array/mod.rs b/src/types/array/mod.rs new file mode 100644 index 000000000..05658e19a --- /dev/null +++ b/src/types/array/mod.rs @@ -0,0 +1,797 @@ +//! Represents an array in PHP. As all arrays in PHP are associative arrays, +//! they are represented by hash tables. + +use std::{convert::TryFrom, ffi::CString, fmt::Debug, ptr, str::FromStr}; + +use crate::{ + boxed::{ZBox, ZBoxable}, + convert::{FromZval, IntoZval}, + error::Result, + ffi::zend_ulong, + ffi::{ + _zend_new_array, zend_array_count, zend_array_destroy, zend_array_dup, 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, + HT_MIN_SIZE, + }, + flags::DataType, + types::Zval, +}; + +mod array_key; +mod conversions; +mod iterators; + +pub use array_key::ArrayKey; +pub use iterators::{Iter, Values}; + +/// A PHP hashtable. +/// +/// In PHP, arrays are represented as hashtables. This allows you to push values +/// onto the end of the array like a vector, while also allowing you to insert +/// at arbitrary string key indexes. +/// +/// A PHP hashtable stores values as [`Zval`]s. This allows you to insert +/// different types into the same hashtable. Types must implement [`IntoZval`] +/// to be able to be inserted into the hashtable. +/// +/// # Examples +/// +/// ```no_run +/// use ext_php_rs::types::ZendHashTable; +/// +/// let mut ht = ZendHashTable::new(); +/// ht.push(1); +/// ht.push("Hello, world!"); +/// ht.insert("Like", "Hashtable"); +/// +/// assert_eq!(ht.len(), 3); +/// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(1)); +/// ``` +pub type ZendHashTable = crate::ffi::HashTable; + +// Clippy complains about there being no `is_empty` function when implementing +// on the alias `ZendStr` :( +#[allow(clippy::len_without_is_empty)] +impl ZendHashTable { + /// Creates a new, empty, PHP hashtable, returned inside a [`ZBox`]. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let ht = ZendHashTable::new(); + /// ``` + /// + /// # Panics + /// + /// Panics if memory for the hashtable could not be allocated. + #[must_use] + pub fn new() -> ZBox { + Self::with_capacity(HT_MIN_SIZE) + } + + /// Creates a new, empty, PHP hashtable with an initial size, returned + /// inside a [`ZBox`]. + /// + /// # Parameters + /// + /// * `size` - The size to initialize the array with. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let ht = ZendHashTable::with_capacity(10); + /// ``` + /// + /// # Panics + /// + /// Panics if memory for the hashtable could not be allocated. + #[must_use] + pub fn with_capacity(size: u32) -> ZBox { + unsafe { + // SAFETY: PHP allocator handles the creation of the array. + #[allow(clippy::used_underscore_items)] + let ptr = _zend_new_array(size); + + // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not. + ZBox::from_raw( + ptr.as_mut() + .expect("Failed to allocate memory for hashtable"), + ) + } + } + + /// Returns the current number of elements in the array. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push(1); + /// ht.push("Hello, world"); + /// + /// assert_eq!(ht.len(), 2); + /// ``` + #[must_use] + pub fn len(&self) -> usize { + unsafe { zend_array_count(ptr::from_ref(self).cast_mut()) as usize } + } + + /// Returns whether the hash table is empty. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// assert_eq!(ht.is_empty(), true); + /// + /// ht.push(1); + /// ht.push("Hello, world"); + /// + /// assert_eq!(ht.is_empty(), false); + /// ``` + #[must_use] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Clears the hash table, removing all values. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("test", "hello world"); + /// assert_eq!(ht.is_empty(), false); + /// + /// ht.clear(); + /// assert_eq!(ht.is_empty(), true); + /// ``` + pub fn clear(&mut self) { + unsafe { zend_hash_clean(self) } + } + + /// Attempts to retrieve a value from the hash table with a string key. + /// + /// # Parameters + /// + /// * `key` - The key to search for in the hash table. + /// + /// # Returns + /// + /// * `Some(&Zval)` - A reference to the zval at the position in the hash + /// table. + /// * `None` - No value at the given position was found. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("test", "hello world"); + /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world")); + /// ``` + #[must_use] + pub fn get<'a, K>(&self, key: K) -> Option<&Zval> + where + K: Into>, + { + 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) => { + if let Ok(index) = i64::from_str(key.as_str()) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_find(self, index as zend_ulong).as_ref() + } + } else { + unsafe { + zend_hash_str_find( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + .as_ref() + } + } + } + ArrayKey::Str(key) => { + if let Ok(index) = i64::from_str(key) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_find(self, index as zend_ulong).as_ref() + } + } else { + unsafe { + zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) + .as_ref() + } + } + } + } + } + + /// Attempts to retrieve a value from the hash table with a string key. + /// + /// # Parameters + /// + /// * `key` - The key to search for in the hash table. + /// + /// # Returns + /// + /// * `Some(&Zval)` - A reference to the zval at the position in the hash + /// table. + /// * `None` - No value at the given position was found. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("test", "hello world"); + /// assert_eq!(ht.get("test").and_then(|zv| zv.str()), Some("hello world")); + /// ``` + // TODO: Verify if this is safe to use, as it allows mutating the + // hashtable while only having a reference to it. #461 + #[allow(clippy::mut_from_ref)] + #[must_use] + pub fn get_mut<'a, K>(&self, key: K) -> Option<&mut Zval> + where + K: Into>, + { + 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) => { + if let Ok(index) = i64::from_str(key.as_str()) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_find(self, index as zend_ulong).as_mut() + } + } else { + unsafe { + zend_hash_str_find( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + .as_mut() + } + } + } + ArrayKey::Str(key) => { + if let Ok(index) = i64::from_str(key) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_find(self, index as zend_ulong).as_mut() + } + } else { + unsafe { + zend_hash_str_find(self, CString::new(key).ok()?.as_ptr(), key.len() as _) + .as_mut() + } + } + } + } + } + + /// Attempts to retrieve a value from the hash table with an index. + /// + /// # Parameters + /// + /// * `key` - The key to search for in the hash table. + /// + /// # Returns + /// + /// * `Some(&Zval)` - A reference to the zval at the position in the hash + /// table. + /// * `None` - No value at the given position was found. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push(100); + /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100)); + /// ``` + #[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() + } + } + + /// Attempts to retrieve a value from the hash table with an index. + /// + /// # Parameters + /// + /// * `key` - The key to search for in the hash table. + /// + /// # Returns + /// + /// * `Some(&Zval)` - A reference to the zval at the position in the hash + /// table. + /// * `None` - No value at the given position was found. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push(100); + /// assert_eq!(ht.get_index(0).and_then(|zv| zv.long()), Some(100)); + /// ``` + // TODO: Verify if this is safe to use, as it allows mutating the + // hashtable while only having a reference to it. #461 + #[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() + } + } + + /// Attempts to remove a value from the hash table with a string key. + /// + /// # Parameters + /// + /// * `key` - The key to remove from the hash table. + /// + /// # Returns + /// + /// * `Some(())` - Key was successfully removed. + /// * `None` - No key was removed, did not exist. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("test", "hello world"); + /// assert_eq!(ht.len(), 1); + /// + /// ht.remove("test"); + /// assert_eq!(ht.len(), 0); + /// ``` + pub fn remove<'a, K>(&mut self, key: K) -> Option<()> + where + K: Into>, + { + 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) => { + if let Ok(index) = i64::from_str(key.as_str()) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_del(self, index as zend_ulong) + } + } else { + unsafe { + zend_hash_str_del( + self, + CString::new(key.as_str()).ok()?.as_ptr(), + key.len() as _, + ) + } + } + } + ArrayKey::Str(key) => { + if let Ok(index) = i64::from_str(key) { + #[allow(clippy::cast_sign_loss)] + unsafe { + zend_hash_index_del(self, index as zend_ulong) + } + } else { + unsafe { + zend_hash_str_del(self, CString::new(key).ok()?.as_ptr(), key.len() as _) + } + } + } + }; + + if result < 0 { + None + } else { + Some(()) + } + } + + /// Attempts to remove a value from the hash table with a string key. + /// + /// # Parameters + /// + /// * `key` - The key to remove from the hash table. + /// + /// # Returns + /// + /// * `Ok(())` - Key was successfully removed. + /// * `None` - No key was removed, did not exist. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push("hello"); + /// assert_eq!(ht.len(), 1); + /// + /// ht.remove_index(0); + /// assert_eq!(ht.len(), 0); + /// ``` + 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(()) + } + } + + /// Attempts to insert an item into the hash table, or update if the key + /// already exists. Returns nothing in a result if successful. + /// + /// # Parameters + /// + /// * `key` - The key to insert the value at in the hash table. + /// * `value` - The value to insert into the hash table. + /// + /// # Returns + /// + /// Returns nothing in a result on success. + /// + /// # Errors + /// + /// Returns an error if the key could not be converted into a [`CString`], + /// or converting the value into a [`Zval`] failed. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert("a", "A"); + /// ht.insert("b", "B"); + /// ht.insert("c", "C"); + /// assert_eq!(ht.len(), 3); + /// ``` + pub fn insert<'a, K, V>(&mut self, key: K, val: V) -> Result<()> + where + K: Into>, + 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) => { + if let Ok(index) = i64::from_str(&key) { + unsafe { + #[allow(clippy::cast_sign_loss)] + zend_hash_index_update(self, index as zend_ulong, &raw mut val) + }; + } else { + unsafe { + zend_hash_str_update( + self, + CString::new(key.as_str())?.as_ptr(), + key.len(), + &raw mut val, + ) + }; + } + } + ArrayKey::Str(key) => { + if let Ok(index) = i64::from_str(key) { + unsafe { + #[allow(clippy::cast_sign_loss)] + zend_hash_index_update(self, index as zend_ulong, &raw mut val) + }; + } else { + unsafe { + zend_hash_str_update( + self, + CString::new(key)?.as_ptr(), + key.len(), + &raw mut val, + ) + }; + } + } + } + val.release(); + Ok(()) + } + + /// Inserts an item into the hash table at a specified index, or updates if + /// the key already exists. Returns nothing in a result if successful. + /// + /// # Parameters + /// + /// * `key` - The index at which the value should be inserted. + /// * `val` - The value to insert into the hash table. + /// + /// # Returns + /// + /// Returns nothing in a result on success. + /// + /// # Errors + /// + /// Returns an error if converting the value into a [`Zval`] failed. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.insert_at_index(0, "A"); + /// ht.insert_at_index(5, "B"); + /// ht.insert_at_index(0, "C"); // notice overriding index 0 + /// assert_eq!(ht.len(), 2); + /// ``` + pub fn insert_at_index(&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(()) + } + + /// Pushes an item onto the end of the hash table. Returns a result + /// containing nothing if the element was successfully inserted. + /// + /// # Parameters + /// + /// * `val` - The value to insert into the hash table. + /// + /// # Returns + /// + /// Returns nothing in a result on success. + /// + /// # Errors + /// + /// Returns an error if converting the value into a [`Zval`] failed. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push("a"); + /// ht.push("b"); + /// ht.push("c"); + /// assert_eq!(ht.len(), 3); + /// ``` + pub fn push(&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(()) + } + + /// Checks if the hashtable only contains numerical keys. + /// + /// # Returns + /// + /// True if all keys on the hashtable are numerical. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push(0); + /// ht.push(3); + /// ht.push(9); + /// assert!(ht.has_numerical_keys()); + /// + /// ht.insert("obviously not numerical", 10); + /// assert!(!ht.has_numerical_keys()); + /// ``` + #[must_use] + pub fn has_numerical_keys(&self) -> bool { + !self.into_iter().any(|(k, _)| !k.is_long()) + } + + /// Checks if the hashtable has numerical, sequential keys. + /// + /// # Returns + /// + /// True if all keys on the hashtable are numerical and are in sequential + /// order (i.e. starting at 0 and not skipping any keys). + /// + /// # Panics + /// + /// Panics if the number of elements in the hashtable exceeds `i64::MAX`. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// ht.push(0); + /// ht.push(3); + /// ht.push(9); + /// assert!(ht.has_sequential_keys()); + /// + /// ht.insert_at_index(90, 10); + /// assert!(!ht.has_sequential_keys()); + /// ``` + #[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) + } + + /// Returns an iterator over the values contained inside the hashtable, as + /// if it was a set or list. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::ZendHashTable; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// for val in ht.values() { + /// dbg!(val); + /// } + #[inline] + #[must_use] + pub fn values(&self) -> Values<'_> { + Values::new(self) + } + + /// Returns an iterator over the key(s) and value contained inside the + /// hashtable. + /// + /// # Example + /// + /// ```no_run + /// use ext_php_rs::types::{ZendHashTable, ArrayKey}; + /// + /// let mut ht = ZendHashTable::new(); + /// + /// for (key, val) in ht.iter() { + /// match &key { + /// ArrayKey::Long(index) => { + /// } + /// ArrayKey::String(key) => { + /// } + /// ArrayKey::Str(key) => { + /// } + /// } + /// dbg!(key, val); + /// } + #[inline] + #[must_use] + pub fn iter(&self) -> Iter<'_> { + self.into_iter() + } +} + +unsafe impl ZBoxable for ZendHashTable { + fn free(&mut self) { + // SAFETY: ZBox has immutable access to `self`. + 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; + + fn to_owned(&self) -> Self::Owned { + unsafe { + // SAFETY: FFI call does not modify `self`, returns a new hashtable. + let ptr = zend_array_dup(ptr::from_ref(self).cast_mut()); + + // SAFETY: `as_mut()` checks if the pointer is null, and panics if it is not. + ZBox::from_raw( + ptr.as_mut() + .expect("Failed to allocate memory for hashtable"), + ) + } + } +} + +impl Default for ZBox { + fn default() -> Self { + ZendHashTable::new() + } +} + +impl Clone for ZBox { + fn clone(&self) -> Self { + (**self).to_owned() + } +} + +impl IntoZval for ZBox { + 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 { + zval.array() + } +} diff --git a/src/util/mod.rs b/src/util/mod.rs index 76d36f21b..57314609f 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,3 +1,4 @@ mod cstring_scope; +#[allow(unused_imports)] pub use cstring_scope::CStringScope;