Skip to main content

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}