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 /// Failed to make an object lazy (PHP 8.4+)
166 #[error("failed to make the object lazy")]
167 LazyObjectFailed,
168 /// The engine failed to load an auto-global.
169 ///
170 /// The enum carries the name of the auto-global.
171 #[error("the engine failed to load the `{0}` auto-global")]
172 AutoGlobalLoadFailed(&'static str),
173}
174
175impl Error {
176 /// Builds an [`Error::ExceptionPending`] from the exception the engine is
177 /// currently holding, leaving the exception where it is.
178 ///
179 /// Returns [`None`] when no exception is pending.
180 pub(crate) fn pending_exception() -> Option<Self> {
181 ExecutorGlobals::pending_exception_class().map(|class| Self::ExceptionPending { class })
182 }
183}
184
185impl From<TryFromIntError> for Error {
186 fn from(_: TryFromIntError) -> Self {
187 Self::IntegerOverflow
188 }
189}
190
191impl From<NulError> for Error {
192 fn from(value: NulError) -> Self {
193 Self::InvalidCString {
194 position: value.nul_position(),
195 }
196 }
197}
198
199impl From<Error> for PhpException {
200 fn from(err: Error) -> Self {
201 Self::from_message(err.to_string())
202 }
203}
204
205/// Trigger an error that is reported in PHP the same way `trigger_error()` is.
206///
207/// See specific error type descriptions at <https://www.php.net/manual/en/errorfunc.constants.php>.
208///
209/// Does nothing if `message` contains a NUL byte, or if the error type bits do not
210/// fit in a C `int`.
211pub fn php_error(type_: &ErrorType, message: &str) {
212 let Ok(c_string) = CString::new(message) else {
213 return;
214 };
215 let Ok(bits) = c_int::try_from(type_.bits()) else {
216 return;
217 };
218
219 // SAFETY: `php_error_docref` is declared `PHP_ATTRIBUTE_FORMAT(printf, 3, 4)`, so
220 // `message` must be passed as a `%s` argument and never as the format itself,
221 // which would interpret `%` sequences in it as varargs directives. Both pointers
222 // are NUL-terminated and outlive the call.
223 unsafe {
224 php_error_docref(std::ptr::null(), bits, c"%s".as_ptr(), c_string.as_ptr());
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use std::error::Error as _;
231
232 use super::*;
233
234 #[test]
235 fn exception_pending_names_the_class() {
236 let err = Error::ExceptionPending {
237 class: "RuntimeException".into(),
238 };
239
240 assert_eq!(
241 err.to_string(),
242 "a PHP exception of class RuntimeException is pending"
243 );
244 }
245
246 #[test]
247 fn catch_error_is_chained_as_a_source() {
248 let err: Error = CatchError::Bailout.into();
249
250 assert_eq!(
251 err.to_string(),
252 "try_catch did not run the closure to completion"
253 );
254 assert_eq!(
255 err.source().map(ToString::to_string),
256 Some("the engine bailed out".to_string())
257 );
258 }
259
260 #[test]
261 fn invalid_cstring_reports_the_nul_position() {
262 let err: Error = CString::new("a\0b")
263 .expect_err("the string has a NUL")
264 .into();
265
266 assert_eq!(err.to_string(), "string contains a NUL byte at position 1");
267 }
268
269 #[test]
270 fn invalid_property_names_the_property() {
271 let err = Error::InvalidProperty {
272 property: "age".into(),
273 };
274
275 assert_eq!(
276 err.to_string(),
277 "property `age` does not exist on the object"
278 );
279 }
280
281 #[test]
282 fn messages_are_lowercase_fragments() {
283 assert_eq!(Error::InvalidScope.to_string(), "invalid scope");
284 }
285}