1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! Error and result types returned from the library functions.
use std::{
ffi::{CString, NulError, c_int},
num::TryFromIntError,
};
use crate::{
exception::PhpException,
ffi::php_error_docref,
flags::{ClassFlags, DataType, ErrorType, ZvalTypeFlags},
zend::{CatchError, ExecutorGlobals},
};
/// The main result type which is passed by the library.
pub type Result<T, E = Error> = std::result::Result<T, E>;
const _: fn() = || {
fn assert_send_sync<T: Send + Sync + 'static>() {}
assert_send_sync::<Error>();
};
const _: () = assert!(
size_of::<Error>() == size_of::<String>() + size_of::<usize>(),
"Error is returned by every Result in the crate, including the conversion paths: a variant that grows it past a String payload is paid for on every call"
);
/// The main error type which is passed by the library inside the custom
/// [`Result`] type.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
/// An incorrect number of arguments was given to a PHP function.
///
/// The enum carries two integers - the first representing the number of
/// arguments that were received, and the second representing the minimum
/// number of arguments expected.
#[error("expected at least {1} arguments, got {0} arguments")]
IncorrectArguments(usize, usize),
/// There was an error converting a Zval into a primitive type.
///
/// The enum carries the data type of the Zval.
#[error("could not convert value of type {0}")]
ZvalConversion(DataType),
/// The function type read from the engine is not one this crate models.
///
/// The enum carries the raw value read from `zend_function.type`.
#[error("unknown function type {0}")]
UnknownFunctionType(u8),
/// Attempted to convert a [`ZvalTypeFlags`] struct to a [`DataType`].
/// The flags did not contain a datatype.
///
/// The enum carries the flags that were attempted to be converted to a
/// [`DataType`].
#[error("type flags did not contain a datatype: {0:?}")]
InvalidTypeToDatatype(ZvalTypeFlags),
/// The function called was called in an invalid scope (calling
/// class-related functions inside of a non-class bound function).
#[error("invalid scope")]
InvalidScope,
/// The pointer inside a given type was invalid, either null or pointing to
/// garbage.
#[error("invalid pointer")]
InvalidPointer,
/// A class declared two properties with the same name, for example a
/// `#[php(prop)]` field and a `#[php(getter)]` method.
#[error("property `{property}` is declared twice")]
DuplicateProperty {
/// `Class::$property` that was declared twice.
property: String,
},
/// A class or enum declared two methods whose names differ only by ASCII
/// case. PHP method names ignore case.
#[error("method `{method}` is declared twice")]
DuplicateMethod {
/// `Class::method` that was declared twice.
method: String,
},
/// A class declared two constants, or an enum two cases, with the same
/// name.
#[error("constant `{constant}` is declared twice")]
DuplicateConstant {
/// `Class::CONSTANT` that was declared twice.
constant: String,
},
/// A module declared two functions whose names differ only by ASCII case.
#[error("function `{function}` is declared twice")]
DuplicateFunction {
/// Name of the function that was declared twice.
function: String,
},
/// A class, interface or enum with this name, ignoring ASCII case, is
/// already registered with the engine.
#[error("class `{class}` is already registered")]
DuplicateClass {
/// Name of the class that was registered twice.
class: String,
},
/// The given property name does not exist.
#[error("property `{property}` does not exist on the object")]
InvalidProperty {
/// Name of the property that was looked up.
property: String,
},
/// The enum has no case matching the given name or discriminant.
#[error("enum has no case matching `{case}`")]
InvalidEnumCase {
/// Name or discriminant that was looked up.
case: String,
},
/// The string could not be converted into a C-string due to the presence of
/// a NUL character.
#[error("string contains a NUL byte at position {position}")]
InvalidCString {
/// Byte offset of the first NUL character.
position: usize,
},
/// The string could not be converted into a valid Utf8 string
#[error("invalid UTF-8 byte sequence")]
InvalidUtf8,
/// Could not call the given function.
#[error("could not call the given function")]
Callable,
/// An object was expected.
#[error("an object was expected")]
Object,
/// The object's class does not implement `__toString()`, so it cannot be
/// converted into a string.
///
/// The enum carries the name of the class.
#[error("{0} does not implement __toString()")]
NotStringable(String),
/// An invalid exception type was thrown.
#[error("invalid exception type was thrown: {0:?}")]
InvalidException(ClassFlags),
/// Converting integer arguments resulted in an overflow.
#[error("converting integer arguments resulted in an overflow")]
IntegerOverflow,
/// The engine did not run a [`try_catch`] closure to completion.
///
/// [`try_catch`]: crate::zend::try_catch
#[error("try_catch did not run the closure to completion")]
Catch(#[from] CatchError),
/// A PHP exception is pending in the executor globals.
///
/// The exception is left where the engine put it, so it propagates on its
/// own once control returns to PHP. Take ownership of it with
/// [`ExecutorGlobals::take_exception`] to handle it from Rust.
///
/// The enum carries the class name of the pending exception.
#[error("a PHP exception of class {class} is pending")]
ExceptionPending {
/// Class name of the pending exception.
class: String,
},
/// A failure occurred while registering the stream wrapper
#[error("a failure occurred while registering the stream wrapper")]
StreamWrapperRegistrationFailure,
/// A failure occurred while unregistering the stream wrapper
#[error("a failure occurred while unregistering the stream wrapper")]
StreamWrapperUnregistrationFailure,
/// The SAPI write function is not available
#[error("the SAPI write function is not available")]
SapiWriteUnavailable,
/// The engine refused to start an output handler: no request is active,
/// or a registered conflict check rejected the handler name.
#[error("the engine refused to start the output handler")]
OutputHandlerStartFailed,
/// The engine refused to change the response headers: no request is
/// active, a `SapiGlobals` guard is alive, or the header is invalid or
/// comes after the headers were sent.
#[error("the engine refused to change the response headers")]
ResponseHeaderFailed,
/// A module loaded with `dl()` has a `sapi_activate_function`. PHP unloads
/// such a module at the end of the request, while the SAPI keeps the hook.
#[error("a module loaded with dl() cannot have a sapi_activate_function")]
SapiActivateUnderDl,
/// Failed to make an object lazy (PHP 8.4+)
#[error("failed to make the object lazy")]
LazyObjectFailed,
/// The engine failed to load an auto-global.
///
/// The enum carries the name of the auto-global.
#[error("the engine failed to load the `{0}` auto-global")]
AutoGlobalLoadFailed(&'static str),
}
impl Error {
/// Builds an [`Error::ExceptionPending`] from the exception the engine is
/// currently holding, leaving the exception where it is.
///
/// Returns [`None`] when no exception is pending.
pub(crate) fn pending_exception() -> Option<Self> {
ExecutorGlobals::pending_exception_class().map(|class| Self::ExceptionPending { class })
}
}
impl From<TryFromIntError> for Error {
fn from(_: TryFromIntError) -> Self {
Self::IntegerOverflow
}
}
impl From<NulError> for Error {
fn from(value: NulError) -> Self {
Self::InvalidCString {
position: value.nul_position(),
}
}
}
impl From<Error> for PhpException {
fn from(err: Error) -> Self {
Self::from_message(err.to_string())
}
}
/// Trigger an error that is reported in PHP the same way `trigger_error()` is.
///
/// See specific error type descriptions at <https://www.php.net/manual/en/errorfunc.constants.php>.
///
/// Does nothing if `message` contains a NUL byte, or if the error type bits do not
/// fit in a C `int`.
pub fn php_error(type_: &ErrorType, message: &str) {
let Ok(c_string) = CString::new(message) else {
return;
};
let Ok(bits) = c_int::try_from(type_.bits()) else {
return;
};
// SAFETY: `php_error_docref` is declared `PHP_ATTRIBUTE_FORMAT(printf, 3, 4)`, so
// `message` must be passed as a `%s` argument and never as the format itself,
// which would interpret `%` sequences in it as varargs directives. Both pointers
// are NUL-terminated and outlive the call.
unsafe {
php_error_docref(std::ptr::null(), bits, c"%s".as_ptr(), c_string.as_ptr());
}
}
#[cfg(test)]
mod tests {
use std::error::Error as _;
use super::*;
#[test]
fn exception_pending_names_the_class() {
let err = Error::ExceptionPending {
class: "RuntimeException".into(),
};
assert_eq!(
err.to_string(),
"a PHP exception of class RuntimeException is pending"
);
}
#[test]
fn catch_error_is_chained_as_a_source() {
let err: Error = CatchError::Bailout.into();
assert_eq!(
err.to_string(),
"try_catch did not run the closure to completion"
);
assert_eq!(
err.source().map(ToString::to_string),
Some("the engine bailed out".to_string())
);
}
#[test]
fn invalid_cstring_reports_the_nul_position() {
let err: Error = CString::new("a\0b")
.expect_err("the string has a NUL")
.into();
assert_eq!(err.to_string(), "string contains a NUL byte at position 1");
}
#[test]
fn invalid_property_names_the_property() {
let err = Error::InvalidProperty {
property: "age".into(),
};
assert_eq!(
err.to_string(),
"property `age` does not exist on the object"
);
}
#[test]
fn messages_are_lowercase_fragments() {
assert_eq!(Error::InvalidScope.to_string(), "invalid scope");
}
}