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