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    /// 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}