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

extphprs / ext-php-rs / 26565713723

28 May 2026 09:11AM UTC coverage: 66.146% (-0.06%) from 66.207%
26565713723

Pull #743

github

web-flow
Merge 7dbb5b991 into cd7df3e47
Pull Request #743: feat: Support using ZendStr as ZendHashTable keys

36 of 68 new or added lines in 5 files covered. (52.94%)

1 existing line in 1 file now uncovered.

8685 of 13130 relevant lines covered (66.15%)

33.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.0
/src/types/array/array_key.rs
1
use crate::{
2
    boxed::ZBox, convert::FromZval, error::Error, flags::DataType, types::ZendStr, types::Zval,
3
};
4
use std::str::FromStr;
5
use std::{convert::TryFrom, fmt::Display};
6

7
/// Represents the key of a PHP array, which can be either a long or a string.
8
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
9
pub enum ArrayKey<'a> {
10
    /// A numerical key.
11
    /// In Zend API it's represented by `u64` (`zend_ulong`), so the value needs
12
    /// to be cast to `zend_ulong` before passing into Zend functions.
13
    Long(i64),
14
    /// A string key.
15
    String(String),
16
    /// A string key by reference.
17
    Str(&'a str),
18
    /// A PHP `zend_string` key.
19
    /// Allows bypassing repeated `zend_string_init` allocations and re-hashing
20
    /// when working with pre-existing or interned PHP strings
21
    ZendString(&'a ZendStr),
22
}
23

24
impl From<String> for ArrayKey<'_> {
25
    fn from(value: String) -> Self {
3✔
26
        if let Ok(index) = i64::from_str(value.as_str()) {
3✔
27
            if value == "0" || !value.starts_with('0') {
×
28
                Self::Long(index)
×
29
            } else {
30
                Self::String(value)
×
31
            }
32
        } else {
33
            Self::String(value)
3✔
34
        }
35
    }
3✔
36
}
37

38
impl TryFrom<ArrayKey<'_>> for String {
39
    type Error = Error;
40

41
    fn try_from(value: ArrayKey<'_>) -> Result<Self, Self::Error> {
52✔
42
        match value {
52✔
43
            ArrayKey::String(s) => Ok(s),
34✔
44
            ArrayKey::Str(s) => Ok(s.to_string()),
1✔
45
            ArrayKey::Long(l) => Ok(l.to_string()),
17✔
NEW
46
            ArrayKey::ZendString(s) => s.as_str().map(ToString::to_string),
×
47
        }
48
    }
52✔
49
}
50

51
impl TryFrom<ArrayKey<'_>> for i64 {
52
    type Error = Error;
53

54
    fn try_from(value: ArrayKey<'_>) -> Result<Self, Self::Error> {
30✔
55
        match value {
30✔
56
            ArrayKey::Long(i) => Ok(i),
21✔
57
            ArrayKey::String(s) => s.parse::<i64>().map_err(|_| Error::InvalidProperty),
7✔
58
            ArrayKey::Str(s) => s.parse::<i64>().map_err(|_| Error::InvalidProperty),
2✔
NEW
59
            ArrayKey::ZendString(s) => s
×
NEW
60
                .as_str()
×
NEW
61
                .map_err(|_| Error::InvalidProperty)?
×
NEW
62
                .parse::<i64>()
×
NEW
63
                .map_err(|_| Error::InvalidProperty),
×
64
        }
65
    }
30✔
66
}
67

68
impl ArrayKey<'_> {
69
    /// Check if the key is an integer.
70
    ///
71
    /// # Returns
72
    ///
73
    /// Returns true if the key is an integer, false otherwise.
74
    #[must_use]
75
    pub fn is_long(&self) -> bool {
×
76
        match self {
×
77
            ArrayKey::Long(_) => true,
×
NEW
78
            ArrayKey::String(_) | ArrayKey::Str(_) | ArrayKey::ZendString(_) => false,
×
79
        }
80
    }
×
81
}
82

83
impl Display for ArrayKey<'_> {
84
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3✔
85
        match self {
3✔
86
            ArrayKey::Long(key) => write!(f, "{key}"),
×
87
            ArrayKey::String(key) => write!(f, "{key}"),
3✔
88
            ArrayKey::Str(key) => write!(f, "{key}"),
×
NEW
89
            ArrayKey::ZendString(key) => match key.as_str() {
×
NEW
90
                Ok(key) => write!(f, "{key}"),
×
NEW
91
                Err(_) => write!(f, "{}", String::from_utf8_lossy(key.as_bytes())),
×
92
            },
93
        }
94
    }
3✔
95
}
96

97
impl<'a> From<&'a str> for ArrayKey<'a> {
98
    fn from(value: &'a str) -> ArrayKey<'a> {
139✔
99
        if let Ok(index) = i64::from_str(value) {
139✔
100
            if value == "0" || !value.starts_with('0') {
24✔
101
                ArrayKey::Long(index)
22✔
102
            } else {
103
                ArrayKey::Str(value)
2✔
104
            }
105
        } else {
106
            ArrayKey::Str(value)
115✔
107
        }
108
    }
139✔
109
}
110

111
impl<'a> From<i32> for ArrayKey<'a> {
112
    fn from(index: i32) -> ArrayKey<'a> {
100✔
113
        ArrayKey::Long(i64::from(index))
100✔
114
    }
100✔
115
}
116

117
impl<'a> From<i64> for ArrayKey<'a> {
118
    fn from(index: i64) -> ArrayKey<'a> {
1✔
119
        ArrayKey::Long(index)
1✔
120
    }
1✔
121
}
122

123
impl<'a> From<u64> for ArrayKey<'a> {
124
    fn from(index: u64) -> ArrayKey<'a> {
×
125
        if let Ok(index) = i64::try_from(index) {
×
126
            ArrayKey::Long(index)
×
127
        } else {
128
            ArrayKey::String(index.to_string())
×
129
        }
130
    }
×
131
}
132

133
impl<'a> From<usize> for ArrayKey<'a> {
134
    fn from(index: usize) -> ArrayKey<'a> {
11✔
135
        if let Ok(index) = i64::try_from(index) {
11✔
136
            ArrayKey::Long(index)
11✔
137
        } else {
138
            ArrayKey::String(index.to_string())
×
139
        }
140
    }
11✔
141
}
142

143
impl<'a> From<&'a ZBox<ZendStr>> for ArrayKey<'a> {
144
    fn from(value: &'a ZBox<ZendStr>) -> Self {
3✔
145
        ArrayKey::ZendString(value.as_ref())
3✔
146
    }
3✔
147
}
148

149
impl<'a> FromZval<'a> for ArrayKey<'_> {
150
    const TYPE: DataType = DataType::String;
151

152
    fn from_zval(zval: &'a Zval) -> Option<Self> {
128✔
153
        if let Some(key) = zval.long() {
128✔
154
            return Some(ArrayKey::Long(key));
79✔
155
        }
49✔
156
        if let Some(key) = zval.string() {
49✔
157
            return Some(ArrayKey::String(key));
49✔
158
        }
×
159
        None
×
160
    }
128✔
161
}
162

163
#[cfg(test)]
164
#[cfg(feature = "embed")]
165
#[allow(clippy::unwrap_used)]
166
mod tests {
167
    use crate::error::Error;
168
    use crate::types::ArrayKey;
169

170
    #[test]
171
    fn test_string_try_from_array_key() {
1✔
172
        let key = ArrayKey::String("test".to_string());
1✔
173
        let result: crate::error::Result<String, _> = key.try_into();
1✔
174
        assert!(result.is_ok());
1✔
175
        assert_eq!(result.unwrap(), "test".to_string());
1✔
176

177
        let key = ArrayKey::Str("test");
1✔
178
        let result: crate::error::Result<String, _> = key.try_into();
1✔
179
        assert!(result.is_ok());
1✔
180
        assert_eq!(result.unwrap(), "test".to_string());
1✔
181

182
        let key = ArrayKey::Long(42);
1✔
183
        let result: crate::error::Result<String, _> = key.try_into();
1✔
184
        assert_eq!(result.unwrap(), "42".to_string());
1✔
185

186
        let key = ArrayKey::String("42".to_string());
1✔
187
        let result: crate::error::Result<String, _> = key.try_into();
1✔
188
        assert!(result.is_ok());
1✔
189
        assert_eq!(result.unwrap(), "42".to_string());
1✔
190

191
        let key = ArrayKey::Str("123");
1✔
192
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
193
        assert!(result.is_ok());
1✔
194
        assert_eq!(result.unwrap(), 123);
1✔
195
    }
1✔
196

197
    #[test]
198
    fn test_i64_try_from_array_key() {
1✔
199
        let key = ArrayKey::Long(42);
1✔
200
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
201
        assert!(result.is_ok());
1✔
202
        assert_eq!(result.unwrap(), 42);
1✔
203

204
        let key = ArrayKey::String("42".to_string());
1✔
205
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
206
        assert!(result.is_ok());
1✔
207
        assert_eq!(result.unwrap(), 42);
1✔
208

209
        let key = ArrayKey::Str("123");
1✔
210
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
211
        assert!(result.is_ok());
1✔
212
        assert_eq!(result.unwrap(), 123);
1✔
213

214
        let key = ArrayKey::String("not a number".to_string());
1✔
215
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
216
        assert!(result.is_err());
1✔
217
        assert!(matches!(result.unwrap_err(), Error::InvalidProperty));
1✔
218
    }
1✔
219

220
    #[test]
221
    fn test_from_str_with_leading_zeros() {
1✔
222
        let key: ArrayKey = "00".into();
1✔
223
        assert_eq!(key, ArrayKey::Str("00"));
1✔
224
        let key: ArrayKey = "071".into();
1✔
225
        assert_eq!(key, ArrayKey::Str("071"));
1✔
226
        let key: ArrayKey = "0".into();
1✔
227
        assert_eq!(key, ArrayKey::Long(0));
1✔
228
    }
1✔
229

230
    #[test]
231
    fn test_from_string_with_leading_zeros() {
1✔
232
        let key = ArrayKey::String("042".to_string());
1✔
233
        let result: crate::error::Result<String, _> = key.try_into();
1✔
234
        assert!(result.is_ok());
1✔
235
        assert_eq!(result.unwrap(), "042");
1✔
236
        let key = ArrayKey::String("00".to_string());
1✔
237
        let result: crate::error::Result<String, _> = key.try_into();
1✔
238
        assert!(result.is_ok());
1✔
239
        assert_eq!(result.unwrap(), "00");
1✔
240
        let key = ArrayKey::String("0".to_string());
1✔
241
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
242
        assert!(result.is_ok());
1✔
243
        assert_eq!(result.unwrap(), 0);
1✔
244
    }
1✔
245
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc