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

extphprs / ext-php-rs / 26832618561

02 Jun 2026 04:11PM UTC coverage: 72.397% (-0.08%) from 72.479%
26832618561

push

github

ptondereau
fix(tests): wrap PHP function handlers in zend_fastcall! for windows

FunctionHandler resolves to extern "C" on unix and to
extern "vectorcall" on windows. The new compound-type integration
handlers (class union, intersection, DNF, primitive union) and the
src/builders/function.rs noop_handler test helper were declared
as plain extern "C", which only matches the unix alias and so
failed to type-check on windows with E0308.

Wrap each handler in zend_fastcall! { ... }, the same macro
closure.rs and builders/class.rs already use. The macro rewrites
the ABI to vectorcall on windows and stays C on unix.

Also gates the noop_handler helper behind cfg(php83) since the
tests that consume it are all PHP 8.3+ (class union, intersection,
DNF return types).

Drops a redundant `#![cfg_attr(windows, feature(abi_vectorcall))]`
attribute inside src/describe/mod.rs's tests module: inner
`feature` attributes only take effect at the crate root, so it was
a no-op that produced "the `#![feature]` attribute can only be
used at the crate root" on every windows compile. The crate root
already enables the feature in src/lib.rs.

0 of 1 new or added line in 1 file covered. (0.0%)

257 existing lines in 10 files now uncovered.

11564 of 15973 relevant lines covered (72.4%)

33.46 hits per line

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

81.12
/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✔
UNCOV
27
            if value == "0" || !value.starts_with('0') {
×
UNCOV
28
                Self::Long(index)
×
29
            } else {
UNCOV
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✔
UNCOV
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✔
UNCOV
59
            ArrayKey::ZendString(s) => s
×
UNCOV
60
                .as_str()
×
UNCOV
61
                .map_err(|_| Error::InvalidProperty)?
×
UNCOV
62
                .parse::<i64>()
×
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]
UNCOV
75
    pub fn is_long(&self) -> bool {
×
76
        match self {
×
UNCOV
77
            ArrayKey::Long(_) => true,
×
UNCOV
78
            ArrayKey::String(_) | ArrayKey::Str(_) | ArrayKey::ZendString(_) => false,
×
79
        }
UNCOV
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✔
UNCOV
86
            ArrayKey::Long(key) => write!(f, "{key}"),
×
87
            ArrayKey::String(key) => write!(f, "{key}"),
3✔
UNCOV
88
            ArrayKey::Str(key) => write!(f, "{key}"),
×
UNCOV
89
            ArrayKey::ZendString(key) => match key.as_str() {
×
UNCOV
90
                Ok(key) => write!(f, "{key}"),
×
UNCOV
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> {
140✔
99
        if let Ok(index) = i64::from_str(value) {
140✔
100
            if value == "0" || !value.starts_with('0') {
25✔
101
                ArrayKey::Long(index)
23✔
102
            } else {
103
                ArrayKey::Str(value)
2✔
104
            }
105
        } else {
106
            ArrayKey::Str(value)
115✔
107
        }
108
    }
140✔
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> {
UNCOV
124
    fn from(index: u64) -> ArrayKey<'a> {
×
UNCOV
125
        if let Ok(index) = i64::try_from(index) {
×
UNCOV
126
            ArrayKey::Long(index)
×
127
        } else {
UNCOV
128
            ArrayKey::String(index.to_string())
×
129
        }
UNCOV
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 {
UNCOV
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 {
6✔
145
        ArrayKey::from(value.as_ref())
6✔
146
    }
6✔
147
}
148

149
impl<'a> From<&'a ZendStr> for ArrayKey<'a> {
150
    fn from(value: &'a ZendStr) -> Self {
6✔
151
        if let Ok(text) = value.as_str()
6✔
152
            && let Ok(index) = i64::from_str(text)
6✔
153
            && (text == "0" || !text.starts_with('0'))
3✔
154
        {
155
            return ArrayKey::Long(index);
3✔
156
        }
3✔
157
        ArrayKey::ZendString(value)
3✔
158
    }
6✔
159
}
160

161
impl<'a> FromZval<'a> for ArrayKey<'_> {
162
    const TYPE: DataType = DataType::String;
163

164
    fn from_zval(zval: &'a Zval) -> Option<Self> {
128✔
165
        if let Some(key) = zval.long() {
128✔
166
            return Some(ArrayKey::Long(key));
79✔
167
        }
49✔
168
        if let Some(key) = zval.string() {
49✔
169
            return Some(ArrayKey::String(key));
49✔
UNCOV
170
        }
×
UNCOV
171
        None
×
172
    }
128✔
173
}
174

175
#[cfg(test)]
176
#[cfg(feature = "embed")]
177
#[allow(clippy::unwrap_used)]
178
mod tests {
179
    use crate::error::Error;
180
    use crate::types::ArrayKey;
181

182
    #[test]
183
    fn test_string_try_from_array_key() {
1✔
184
        let key = ArrayKey::String("test".to_string());
1✔
185
        let result: crate::error::Result<String, _> = key.try_into();
1✔
186
        assert!(result.is_ok());
1✔
187
        assert_eq!(result.unwrap(), "test".to_string());
1✔
188

189
        let key = ArrayKey::Str("test");
1✔
190
        let result: crate::error::Result<String, _> = key.try_into();
1✔
191
        assert!(result.is_ok());
1✔
192
        assert_eq!(result.unwrap(), "test".to_string());
1✔
193

194
        let key = ArrayKey::Long(42);
1✔
195
        let result: crate::error::Result<String, _> = key.try_into();
1✔
196
        assert_eq!(result.unwrap(), "42".to_string());
1✔
197

198
        let key = ArrayKey::String("42".to_string());
1✔
199
        let result: crate::error::Result<String, _> = key.try_into();
1✔
200
        assert!(result.is_ok());
1✔
201
        assert_eq!(result.unwrap(), "42".to_string());
1✔
202

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

209
    #[test]
210
    fn test_i64_try_from_array_key() {
1✔
211
        let key = ArrayKey::Long(42);
1✔
212
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
213
        assert!(result.is_ok());
1✔
214
        assert_eq!(result.unwrap(), 42);
1✔
215

216
        let key = ArrayKey::String("42".to_string());
1✔
217
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
218
        assert!(result.is_ok());
1✔
219
        assert_eq!(result.unwrap(), 42);
1✔
220

221
        let key = ArrayKey::Str("123");
1✔
222
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
223
        assert!(result.is_ok());
1✔
224
        assert_eq!(result.unwrap(), 123);
1✔
225

226
        let key = ArrayKey::String("not a number".to_string());
1✔
227
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
228
        assert!(result.is_err());
1✔
229
        assert!(matches!(result.unwrap_err(), Error::InvalidProperty));
1✔
230
    }
1✔
231

232
    #[test]
233
    fn test_from_str_with_leading_zeros() {
1✔
234
        let key: ArrayKey = "00".into();
1✔
235
        assert_eq!(key, ArrayKey::Str("00"));
1✔
236
        let key: ArrayKey = "071".into();
1✔
237
        assert_eq!(key, ArrayKey::Str("071"));
1✔
238
        let key: ArrayKey = "0".into();
1✔
239
        assert_eq!(key, ArrayKey::Long(0));
1✔
240
    }
1✔
241

242
    #[test]
243
    fn test_from_string_with_leading_zeros() {
1✔
244
        let key = ArrayKey::String("042".to_string());
1✔
245
        let result: crate::error::Result<String, _> = key.try_into();
1✔
246
        assert!(result.is_ok());
1✔
247
        assert_eq!(result.unwrap(), "042");
1✔
248
        let key = ArrayKey::String("00".to_string());
1✔
249
        let result: crate::error::Result<String, _> = key.try_into();
1✔
250
        assert!(result.is_ok());
1✔
251
        assert_eq!(result.unwrap(), "00");
1✔
252
        let key = ArrayKey::String("0".to_string());
1✔
253
        let result: crate::error::Result<i64, _> = key.try_into();
1✔
254
        assert!(result.is_ok());
1✔
255
        assert_eq!(result.unwrap(), 0);
1✔
256
    }
1✔
257
}
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