aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-08 22:14:12 +0900
committernsfisis <nsfisis@gmail.com>2026-08-08 22:14:12 +0900
commitf4cad2123b2af0de72bda4ce039e16e74f163f4e (patch)
tree21803308c5ff41e23c9d3b117433eea16b4ff663 /crates/shirabe-php-shim
parent0209f63210e5b547b5c6b73367bb80ea86c255ec (diff)
downloadphp-shirabe-f4cad2123b2af0de72bda4ce039e16e74f163f4e.tar.gz
php-shirabe-f4cad2123b2af0de72bda4ce039e16e74f163f4e.tar.zst
php-shirabe-f4cad2123b2af0de72bda4ce039e16e74f163f4e.zip
feat(php-shim): give ported exceptions PHP's class hierarchy
Ported exceptions were flat structs reached with `downcast_ref`, so Composer's `catch (\RuntimeException $e)` only matched the exact leaf type and `get_class($e)` had nothing to report. Each exception now embeds an instance of the class it extends and travels inside an `AnyThrowable`; `Catch::catch`/`catch_mut` walk that chain, and `PhpClass::php_class_name` yields the PHP FQCN. Dropping the `std::error::Error` impls from the exception types leaves `AnyThrowable` as the only route into an `anyhow::Error`, so the walk cannot be bypassed. A `no_exception_downcast` linter catches the `downcast::<X>()` calls that would now silently answer `None`. Three sites change behavior as a result: the `TransportException` exit-code override reaches `MaxFileSizeExceededException`, the `catch (\LogicException)` in findSimilar() reaches its subclasses, and rendered exception titles carry the real class name rather than a guess. `get_class_err()` is no longer a `todo!()`, which re-enables FilesystemRepositoryTest::testCorruptedRepositoryFile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/exception.rs541
-rw-r--r--crates/shirabe-php-shim/src/fs.rs18
-rw-r--r--crates/shirabe-php-shim/src/phar.rs137
-rw-r--r--crates/shirabe-php-shim/src/var.rs8
-rw-r--r--crates/shirabe-php-shim/src/zip.rs11
5 files changed, 516 insertions, 199 deletions
diff --git a/crates/shirabe-php-shim/src/exception.rs b/crates/shirabe-php-shim/src/exception.rs
index c57a3451..803d5b64 100644
--- a/crates/shirabe-php-shim/src/exception.rs
+++ b/crates/shirabe-php-shim/src/exception.rs
@@ -1,133 +1,416 @@
-use crate::PharException;
+use crate::PhpClass;
-#[derive(Debug)]
-pub struct Exception {
- pub message: String,
- pub code: i64,
+/// The fields a PHP `\Throwable` carries: its message and code, and the exception it wraps. Ported
+/// exception types embed this, either directly or through the parent exception they extend.
+#[derive(Debug, Clone)]
+pub struct ThrowableFields {
+ message: String,
+ code: i64,
+ previous: Option<std::sync::Arc<AnyThrowable>>,
}
-impl std::fmt::Display for Exception {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+impl ThrowableFields {
+ pub fn get_message(&self) -> &str {
+ &self.message
}
-}
-impl std::error::Error for Exception {}
+ pub fn get_code(&self) -> i64 {
+ self.code
+ }
-#[derive(Debug)]
-pub struct RuntimeException {
- pub message: String,
- pub code: i64,
+ /// PHP's `code` property is protected with no setter; Composer writes it through reflection.
+ pub fn set_code(&mut self, code: i64) {
+ self.code = code;
+ }
+
+ pub fn get_previous(&self) -> Option<&AnyThrowable> {
+ self.previous.as_deref()
+ }
}
-impl std::fmt::Display for RuntimeException {
+impl std::fmt::Display for ThrowableFields {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
-impl std::error::Error for RuntimeException {}
+crate::impl_php_class!(ThrowableFields, r"Throwable");
-#[derive(Debug)]
-pub struct UnexpectedValueException {
- pub message: String,
- pub code: i64,
+/// A ported PHP exception class, seen as the object PHP `throw`s: its [`ThrowableFields`], its
+/// concrete Rust type, and the instance of its parent class it embeds.
+/// [`impl_php_exception!`] implements this for every ported exception.
+///
+/// Every ported exception embeds an instance of the class it extends, so [`Self::parent`] walks
+/// exactly PHP's chain of superclasses and bottoms out at the [`ThrowableFields`].
+pub trait Throwable:
+ PhpClass + std::fmt::Debug + std::fmt::Display + Send + Sync + 'static
+{
+ fn fields(&self) -> &ThrowableFields;
+ fn as_any(&self) -> &(dyn std::any::Any + 'static);
+ fn as_any_mut(&mut self) -> &mut (dyn std::any::Any + 'static);
+ fn parent(&self) -> Option<&dyn Throwable>;
+ fn parent_mut(&mut self) -> Option<&mut dyn Throwable>;
}
-impl std::fmt::Display for UnexpectedValueException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+impl Throwable for ThrowableFields {
+ fn fields(&self) -> &ThrowableFields {
+ self
}
-}
-impl std::error::Error for UnexpectedValueException {}
+ fn as_any(&self) -> &(dyn std::any::Any + 'static) {
+ self
+ }
-#[derive(Debug)]
-pub struct InvalidArgumentException {
- pub message: String,
- pub code: i64,
-}
+ fn as_any_mut(&mut self) -> &mut (dyn std::any::Any + 'static) {
+ self
+ }
-impl std::fmt::Display for InvalidArgumentException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+ fn parent(&self) -> Option<&dyn Throwable> {
+ None
}
-}
-impl std::error::Error for InvalidArgumentException {}
+ fn parent_mut(&mut self) -> Option<&mut dyn Throwable> {
+ None
+ }
+}
+/// The form a thrown PHP exception takes while it travels as a Rust error. Ported exception types
+/// deliberately do not implement [`std::error::Error`], so this box is the only way one reaches an
+/// `anyhow::Error`: an error either carries an `AnyThrowable`, and PHP would see a `\Throwable`,
+/// or it does not, and PHP would see nothing catchable.
+///
+/// This is what makes `catch (\RuntimeException $e)` portable: [`Catch::catch`] answers over the
+/// whole class hierarchy, rather than over the Rust type, which is a leaf of it.
#[derive(Debug)]
-pub struct TypeError {
- pub message: String,
- pub code: i64,
+pub struct AnyThrowable(Box<dyn Throwable>);
+
+impl AnyThrowable {
+ pub fn new(exception: impl Throwable) -> Self {
+ Self(Box::new(exception))
+ }
+
+ /// The exception a Rust error carries, or `None` if it carries none.
+ // TODO(phase-c): this matches only an error that *is* the exception, where [`Catch`]'s
+ // `anyhow::Error` impl also sees one behind an `anyhow::Context` layer. Nothing in the port
+ // adds context to an error yet, so an exception wrapped that way would go silently unseen.
+ pub fn of<'e>(error: &'e (dyn std::error::Error + 'static)) -> Option<&'e Self> {
+ error.downcast_ref::<Self>()
+ }
+
+ /// PHP's `catch (T $e)`: the exception seen as an instance of `T`, or `None` if it is not one.
+ /// A subclass answers through the instance of `T` it embeds, so `T`'s own state is reachable
+ /// the way PHP reaches an inherited property.
+ fn downcast_ref<T: Throwable>(&self) -> Option<&T> {
+ let mut class: &dyn Throwable = &*self.0;
+ loop {
+ if let Some(instance) = class.as_any().downcast_ref::<T>() {
+ return Some(instance);
+ }
+ class = class.parent()?;
+ }
+ }
+
+ /// [`AnyThrowable::downcast_ref`] for a caught exception that is about to be mutated, the way
+ /// PHP writes to a property of the object it caught.
+ fn downcast_mut<T: Throwable>(&mut self) -> Option<&mut T> {
+ let mut superclasses = 0;
+ let mut class: &dyn Throwable = &*self.0;
+ while !class.as_any().is::<T>() {
+ class = class.parent()?;
+ superclasses += 1;
+ }
+
+ let mut class: &mut dyn Throwable = &mut *self.0;
+ for _ in 0..superclasses {
+ class = class.parent_mut().expect("walked immutably just above");
+ }
+ class.as_any_mut().downcast_mut::<T>()
+ }
+
+ pub fn get_message(&self) -> &str {
+ self.0.fields().get_message()
+ }
+
+ pub fn get_code(&self) -> i64 {
+ self.0.fields().get_code()
+ }
+
+ pub fn get_previous(&self) -> Option<&AnyThrowable> {
+ self.0.fields().get_previous()
+ }
}
-impl std::fmt::Display for TypeError {
+impl std::fmt::Display for AnyThrowable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+ std::fmt::Display::fmt(&self.0, f)
}
}
-impl std::error::Error for TypeError {}
+impl std::error::Error for AnyThrowable {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ self.get_previous()
+ .map(|previous| previous as &(dyn std::error::Error + 'static))
+ }
+}
-#[derive(Debug)]
-pub struct LogicException {
- pub message: String,
- pub code: i64,
+impl PhpClass for AnyThrowable {
+ fn php_class_name(&self) -> String {
+ self.0.php_class_name()
+ }
}
-impl std::fmt::Display for LogicException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+/// PHP's `catch` applied to a Rust error.
+pub trait Catch {
+ /// The exception the error carries, seen as an instance of `T`, or `None` if it carries no
+ /// exception or one of an unrelated class.
+ fn catch<T: Throwable>(&self) -> Option<&T>;
+
+ /// [`Catch::catch`] for a caught exception that is about to be mutated, the way PHP writes to
+ /// a property of the object it caught.
+ fn catch_mut<T: Throwable>(&mut self) -> Option<&mut T>;
+
+ /// PHP's `$e instanceof T`.
+ fn is_instanceof<T: Throwable>(&self) -> bool {
+ self.catch::<T>().is_some()
}
+
+ /// PHP's `get_class($e) === T::class`: the class the exception was thrown as, rather than
+ /// [`Catch::is_instanceof`]'s walk over its superclasses.
+ fn is_class<T: Throwable>(&self) -> bool;
}
-impl std::error::Error for LogicException {}
+impl Catch for anyhow::Error {
+ fn catch<T: Throwable>(&self) -> Option<&T> {
+ self.downcast_ref::<AnyThrowable>()?.downcast_ref::<T>()
+ }
-#[derive(Debug)]
-pub struct BadMethodCallException {
- pub message: String,
- pub code: i64,
+ fn catch_mut<T: Throwable>(&mut self) -> Option<&mut T> {
+ self.downcast_mut::<AnyThrowable>()?.downcast_mut::<T>()
+ }
+
+ fn is_class<T: Throwable>(&self) -> bool {
+ self.downcast_ref::<AnyThrowable>()
+ .is_some_and(|e| e.is_class::<T>())
+ }
}
-impl std::fmt::Display for BadMethodCallException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+impl Catch for AnyThrowable {
+ fn catch<T: Throwable>(&self) -> Option<&T> {
+ self.downcast_ref::<T>()
+ }
+
+ fn catch_mut<T: Throwable>(&mut self) -> Option<&mut T> {
+ self.downcast_mut::<T>()
+ }
+
+ fn is_class<T: Throwable>(&self) -> bool {
+ self.0.as_any().is::<T>()
}
}
-impl std::error::Error for BadMethodCallException {}
+/// Implements the `\Throwable` surface for a ported exception type, given the field holding the
+/// fields it inherits — a [`ThrowableFields`] for a type that extends a PHP built-in directly, or
+/// the embedded parent exception otherwise — and the fully-qualified name of the PHP class.
+///
+/// ```ignore
+/// impl_php_exception!(SolverBugException, 0, r"Composer\DependencyResolver\SolverBugException");
+/// ```
+///
+/// The type is deliberately left without a [`std::error::Error`] impl, so that the only route from
+/// it to an `anyhow::Error` is the [`AnyThrowable`] this generates a conversion to.
+#[macro_export]
+macro_rules! impl_php_exception {
+ ($ty:ty, $field:tt, $class_name:expr) => {
+ $crate::impl_php_exception!(@accessors $ty, $field, $class_name);
-#[derive(Debug)]
-pub struct OutOfBoundsException {
- pub message: String,
- pub code: i64,
+ impl $crate::Throwable for $ty {
+ fn fields(&self) -> &$crate::ThrowableFields {
+ $crate::Throwable::fields(&self.$field)
+ }
+
+ fn as_any(&self) -> &(dyn std::any::Any + 'static) {
+ self
+ }
+
+ fn as_any_mut(&mut self) -> &mut (dyn std::any::Any + 'static) {
+ self
+ }
+
+ fn parent(&self) -> Option<&dyn $crate::Throwable> {
+ Some(&self.$field)
+ }
+
+ fn parent_mut(&mut self) -> Option<&mut dyn $crate::Throwable> {
+ Some(&mut self.$field)
+ }
+ }
+
+ impl From<$ty> for $crate::AnyThrowable {
+ fn from(exception: $ty) -> Self {
+ $crate::AnyThrowable::new(exception)
+ }
+ }
+
+ impl From<$ty> for ::anyhow::Error {
+ fn from(exception: $ty) -> Self {
+ ::anyhow::Error::new($crate::AnyThrowable::new(exception))
+ }
+ }
+ };
+ // For an exception the port cannot let travel as a Rust error, because its state is not
+ // `Send + Sync`. It gets the accessors but no [`Throwable`], so asking for it in a `catch`
+ // does not compile, rather than silently never matching.
+ ($ty:ty, $field:tt, $class_name:expr, !Send) => {
+ $crate::impl_php_exception!(@accessors $ty, $field, $class_name);
+ };
+ (@accessors $ty:ty, $field:tt, $class_name:expr) => {
+ impl $ty {
+ pub fn get_message(&self) -> &str {
+ self.$field.get_message()
+ }
+
+ pub fn get_code(&self) -> i64 {
+ self.$field.get_code()
+ }
+
+ pub fn set_code(&mut self, code: i64) {
+ self.$field.set_code(code);
+ }
+
+ pub fn get_previous(&self) -> Option<&$crate::AnyThrowable> {
+ self.$field.get_previous()
+ }
+ }
+
+ impl std::fmt::Display for $ty {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ std::fmt::Display::fmt(&self.$field, f)
+ }
+ }
+
+ impl $crate::PhpClass for $ty {
+ fn php_class_name(&self) -> String {
+ $class_name.to_string()
+ }
+ }
+ };
}
-impl std::fmt::Display for OutOfBoundsException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
- }
+/// Defines a PHP built-in exception class as a struct carrying nothing but the instance of the
+/// class it extends, or the [`ThrowableFields`] itself for a class that extends nothing.
+macro_rules! define_php_exception {
+ ($ty:ident, ThrowableFields, $class_name:expr) => {
+ define_php_exception!(@shared $ty, ThrowableFields, $class_name);
+
+ impl $ty {
+ pub fn with_code_and_previous(
+ message: String,
+ code: i64,
+ previous: Option<std::sync::Arc<AnyThrowable>>,
+ ) -> Self {
+ Self {
+ inner: ThrowableFields {
+ message,
+ code,
+ previous,
+ },
+ }
+ }
+ }
+ };
+ ($ty:ident, $parent:ty, $class_name:expr) => {
+ define_php_exception!(@shared $ty, $parent, $class_name);
+
+ impl $ty {
+ pub fn with_code_and_previous(
+ message: String,
+ code: i64,
+ previous: Option<std::sync::Arc<AnyThrowable>>,
+ ) -> Self {
+ Self {
+ inner: <$parent>::with_code_and_previous(message, code, previous),
+ }
+ }
+ }
+ };
+ (@shared $ty:ident, $parent:ty, $class_name:expr) => {
+ #[derive(Debug, Clone)]
+ pub struct $ty {
+ inner: $parent,
+ }
+
+ impl $ty {
+ pub fn new(message: String) -> Self {
+ Self::with_code_and_previous(message, 0, None)
+ }
+
+ pub fn with_code(message: String, code: i64) -> Self {
+ Self::with_code_and_previous(message, code, None)
+ }
+ }
+
+ crate::impl_php_exception!($ty, inner, $class_name);
+ };
}
-impl std::error::Error for OutOfBoundsException {}
+define_php_exception!(Exception, ThrowableFields, r"Exception");
+define_php_exception!(Error, ThrowableFields, r"Error");
+define_php_exception!(TypeError, Error, r"TypeError");
+define_php_exception!(RuntimeException, Exception, r"RuntimeException");
+define_php_exception!(
+ UnexpectedValueException,
+ RuntimeException,
+ r"UnexpectedValueException"
+);
+define_php_exception!(
+ OutOfBoundsException,
+ RuntimeException,
+ r"OutOfBoundsException"
+);
+define_php_exception!(LogicException, Exception, r"LogicException");
+define_php_exception!(
+ InvalidArgumentException,
+ LogicException,
+ r"InvalidArgumentException"
+);
+define_php_exception!(
+ BadFunctionCallException,
+ LogicException,
+ r"BadFunctionCallException"
+);
+define_php_exception!(
+ BadMethodCallException,
+ BadFunctionCallException,
+ r"BadMethodCallException"
+);
#[derive(Debug)]
pub struct ErrorException {
- pub message: String,
- pub code: i64,
+ inner: Exception,
pub severity: i64,
pub filename: String,
pub lineno: i64,
}
-impl std::fmt::Display for ErrorException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+impl ErrorException {
+ pub fn new(
+ message: String,
+ code: i64,
+ severity: i64,
+ filename: String,
+ lineno: i64,
+ previous: Option<std::sync::Arc<AnyThrowable>>,
+ ) -> Self {
+ Self {
+ inner: Exception::with_code_and_previous(message, code, previous),
+ severity,
+ filename,
+ lineno,
+ }
}
}
-impl std::error::Error for ErrorException {}
+crate::impl_php_exception!(ErrorException, inner, r"ErrorException");
/// Models PHP's `exit`/`die` language construct propagated as a recoverable error so the actual
/// process termination happens at a single top-level site instead of deep in the call stack.
@@ -148,39 +431,103 @@ impl std::fmt::Display for ExitException {
impl std::error::Error for ExitException {}
-pub fn php_exception_get_code(_error: &anyhow::Error) -> i32 {
- // PHP's Throwable::getCode(). anyhow::Error carries the concrete exception type, so enumerate
- // the flat standard exception structs and read their `code` field; everything else defaults to
- // 0, matching PHP's default exception code.
- if let Some(e) = _error.downcast_ref::<Exception>() {
- return e.code as i32;
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[derive(Debug)]
+ struct Subclass {
+ inner: UnexpectedValueException,
+ detail: i64,
}
- if let Some(e) = _error.downcast_ref::<RuntimeException>() {
- return e.code as i32;
+
+ impl Subclass {
+ fn new(detail: i64) -> Self {
+ Self {
+ inner: UnexpectedValueException::new("boom".to_string()),
+ detail,
+ }
+ }
}
- if let Some(e) = _error.downcast_ref::<UnexpectedValueException>() {
- return e.code as i32;
+
+ crate::impl_php_exception!(Subclass, inner, r"Vendor\Subclass");
+
+ #[test]
+ fn catch_reaches_every_superclass() {
+ let error: anyhow::Error = Subclass::new(7).into();
+
+ assert_eq!(error.catch::<Subclass>().map(|e| e.detail), Some(7));
+ assert!(error.catch::<UnexpectedValueException>().is_some());
+ assert!(error.catch::<RuntimeException>().is_some());
+ assert!(error.catch::<Exception>().is_some());
+ assert!(error.catch::<ThrowableFields>().is_some());
}
- if let Some(e) = _error.downcast_ref::<InvalidArgumentException>() {
- return e.code as i32;
+
+ #[test]
+ fn catch_reaches_no_sibling_or_subclass() {
+ let error: anyhow::Error = RuntimeException::new("boom".to_string()).into();
+
+ assert!(error.catch::<Subclass>().is_none());
+ assert!(error.catch::<LogicException>().is_none());
+ assert!(error.catch::<Error>().is_none());
}
- if let Some(e) = _error.downcast_ref::<TypeError>() {
- return e.code as i32;
+
+ #[test]
+ fn an_error_is_not_an_exception() {
+ let error: anyhow::Error = TypeError::new("boom".to_string()).into();
+
+ assert!(error.catch::<Error>().is_some());
+ assert!(error.catch::<ThrowableFields>().is_some());
+ assert!(error.catch::<Exception>().is_none());
}
- if let Some(e) = _error.downcast_ref::<LogicException>() {
- return e.code as i32;
+
+ #[test]
+ fn catch_reaches_nothing_in_a_plain_rust_error() {
+ let error = anyhow::anyhow!("boom");
+
+ assert!(error.catch::<ThrowableFields>().is_none());
}
- if let Some(e) = _error.downcast_ref::<BadMethodCallException>() {
- return e.code as i32;
+
+ #[test]
+ fn is_class_reaches_no_superclass() {
+ let error: anyhow::Error = Subclass::new(7).into();
+
+ assert!(error.is_class::<Subclass>());
+ assert!(error.is_instanceof::<UnexpectedValueException>());
+ assert!(!error.is_class::<UnexpectedValueException>());
+ assert!(!error.is_class::<ThrowableFields>());
}
- if let Some(e) = _error.downcast_ref::<OutOfBoundsException>() {
- return e.code as i32;
+
+ #[test]
+ fn is_class_reaches_nothing_in_a_plain_rust_error() {
+ let error = anyhow::anyhow!("boom");
+
+ assert!(!error.is_class::<ThrowableFields>());
}
- if let Some(e) = _error.downcast_ref::<ErrorException>() {
- return e.code as i32;
+
+ #[test]
+ fn catch_mut_writes_through_to_the_superclass_state() {
+ let mut error: anyhow::Error = Subclass::new(7).into();
+
+ error
+ .catch_mut::<UnexpectedValueException>()
+ .unwrap()
+ .set_code(42);
+
+ assert_eq!(error.catch::<Subclass>().unwrap().get_code(), 42);
}
- if let Some(e) = _error.downcast_ref::<PharException>() {
- return e.code as i32;
+
+ #[test]
+ fn the_previous_exception_is_the_error_source() {
+ let previous = std::sync::Arc::new(AnyThrowable::new(RuntimeException::new(
+ "cause".to_string(),
+ )));
+ let error: anyhow::Error =
+ Exception::with_code_and_previous("boom".to_string(), 0, Some(previous)).into();
+
+ let source = std::error::Error::source(
+ error.downcast_ref::<AnyThrowable>().unwrap() as &dyn std::error::Error
+ );
+ assert_eq!(source.map(ToString::to_string), Some("cause".to_string()));
}
- 0
}
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs
index c3f131b3..00ed6de3 100644
--- a/crates/shirabe-php-shim/src/fs.rs
+++ b/crates/shirabe-php-shim/src/fs.rs
@@ -184,13 +184,10 @@ pub fn recursive_directory_iterator(
) -> Result<RecursiveDirectoryIterator, UnexpectedValueException> {
let root = _path.as_ref().to_path_buf();
if !root.is_dir() {
- return Err(UnexpectedValueException {
- message: format!(
- "RecursiveDirectoryIterator::__construct({}): Failed to open directory",
- root.display()
- ),
- code: 0,
- });
+ return Err(UnexpectedValueException::new(format!(
+ "RecursiveDirectoryIterator::__construct({}): Failed to open directory",
+ root.display()
+ )));
}
Ok(RecursiveDirectoryIterator {
root,
@@ -255,12 +252,11 @@ pub fn directory_iterator(
path: impl AsRef<std::path::Path>,
) -> Result<Vec<DirectoryIteratorEntry>, UnexpectedValueException> {
let base = path.as_ref();
- let rd = std::fs::read_dir(base).map_err(|_| UnexpectedValueException {
- message: format!(
+ let rd = std::fs::read_dir(base).map_err(|_| {
+ UnexpectedValueException::new(format!(
"DirectoryIterator::__construct({}): Failed to open directory",
base.display()
- ),
- code: 0,
+ ))
})?;
// PHP's DirectoryIterator yields the "." and ".." entries before the real ones.
let mut entries = vec![
diff --git a/crates/shirabe-php-shim/src/phar.rs b/crates/shirabe-php-shim/src/phar.rs
index 0387a0de..a0299a84 100644
--- a/crates/shirabe-php-shim/src/phar.rs
+++ b/crates/shirabe-php-shim/src/phar.rs
@@ -23,14 +23,12 @@ struct PharEntry {
}
fn corruption_error(path: &std::path::Path, detail: &str) -> anyhow::Error {
- anyhow::anyhow!(UnexpectedValueException {
- message: format!(
- "internal corruption of phar \"{}\" ({})",
- path.display(),
- detail
- ),
- code: 0,
- })
+ UnexpectedValueException::new(format!(
+ "internal corruption of phar \"{}\" ({})",
+ path.display(),
+ detail
+ ))
+ .into()
}
/// Reads a tar- or zip-based archive (optionally gzip/bzip2 compressed as a whole)
@@ -152,14 +150,12 @@ fn extract_entries(
overwrite: bool,
) -> anyhow::Result<()> {
let extract_error = |detail: String| {
- anyhow::anyhow!(PharException {
- message: format!(
- "Extracting from phar \"{}\" failed: {}",
- archive_path.display(),
- detail
- ),
- code: 0,
- })
+ PharException::new(format!(
+ "Extracting from phar \"{}\" failed: {}",
+ archive_path.display(),
+ detail
+ ))
+ .into()
};
std::fs::create_dir_all(directory).map_err(|e| extract_error(e.to_string()))?;
@@ -441,17 +437,18 @@ impl Phar {
#[derive(Debug)]
pub struct PharException {
- pub message: String,
- pub code: i64,
+ inner: crate::Exception,
}
-impl std::fmt::Display for PharException {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- f.write_str(&self.message)
+impl PharException {
+ pub fn new(message: String) -> Self {
+ Self {
+ inner: crate::Exception::new(message),
+ }
}
}
-impl std::error::Error for PharException {}
+crate::impl_php_exception!(PharException, inner, r"PharException");
#[derive(Debug)]
pub struct PharFileInfo {
@@ -514,13 +511,10 @@ impl PharData {
None => false,
};
if !parent_exists {
- return Err(anyhow::anyhow!(UnexpectedValueException {
- message: format!(
- "Cannot create phar '{}', file extension (or combination) not recognised or the directory does not exist",
- path.display()
- ),
- code: 0,
- }));
+ return Err(UnexpectedValueException::new(format!(
+ "Cannot create phar '{}', file extension (or combination) not recognised or the directory does not exist",
+ path.display()
+ )).into());
}
let format = format.unwrap_or(if path.to_string_lossy().ends_with(".zip") {
Phar::ZIP
@@ -647,15 +641,13 @@ impl PharData {
for file in iter {
let localname = file
.strip_prefix(base_directory)
- .map_err(|_| {
- anyhow::anyhow!(UnexpectedValueException {
- message: format!(
- "Iterator returned a path \"{}\" that is not in the base directory \"{}\"",
- file.display(),
- base_directory.display()
- ),
- code: 0,
- })
+ .map_err(|_| -> anyhow::Error {
+ UnexpectedValueException::new(format!(
+ "Iterator returned a path \"{}\" that is not in the base directory \"{}\"",
+ file.display(),
+ base_directory.display()
+ ))
+ .into()
})?
.to_string_lossy()
.into_owned();
@@ -679,15 +671,13 @@ impl PharData {
"PharData::compress: only tar-based archives can be compressed as a whole"
);
let tar_bytes = self.build_tar_bytes()?;
- let write_error = |e: std::io::Error| {
- anyhow::anyhow!(PharException {
- message: format!(
- "Unable to compress phar archive \"{}\": {}",
- self.path.display(),
- e
- ),
- code: 0,
- })
+ let write_error = |e: std::io::Error| -> anyhow::Error {
+ PharException::new(format!(
+ "Unable to compress phar archive \"{}\": {}",
+ self.path.display(),
+ e
+ ))
+ .into()
};
let (target, compressed) = match algo {
Phar::GZ => {
@@ -725,27 +715,23 @@ impl PharData {
}
let bytes = self.build_tar_bytes()?;
std::fs::write(&self.path, bytes).map_err(|e| {
- anyhow::anyhow!(PharException {
- message: format!(
- "Unable to write phar archive \"{}\": {}",
- self.path.display(),
- e
- ),
- code: 0,
- })
+ PharException::new(format!(
+ "Unable to write phar archive \"{}\": {}",
+ self.path.display(),
+ e
+ ))
+ .into()
})
}
fn build_tar_bytes(&self) -> anyhow::Result<Vec<u8>> {
let write_error = |e: std::io::Error| {
- anyhow::anyhow!(PharException {
- message: format!(
- "Unable to write phar archive \"{}\": {}",
- self.path.display(),
- e
- ),
- code: 0,
- })
+ PharException::new(format!(
+ "Unable to write phar archive \"{}\": {}",
+ self.path.display(),
+ e
+ ))
+ .into()
};
let mut builder = tar::Builder::new(Vec::new());
for entry in self.entries.borrow().iter() {
@@ -802,15 +788,13 @@ impl PharData {
}
fn write_zip(&self) -> anyhow::Result<()> {
- let write_error = |e: String| {
- anyhow::anyhow!(PharException {
- message: format!(
- "Unable to write phar archive \"{}\": {}",
- self.path.display(),
- e
- ),
- code: 0,
- })
+ let write_error = |e: String| -> anyhow::Error {
+ PharException::new(format!(
+ "Unable to write phar archive \"{}\": {}",
+ self.path.display(),
+ e
+ ))
+ .into()
};
let file = std::fs::File::create(&self.path).map_err(|e| write_error(e.to_string()))?;
let mut writer = zip::ZipWriter::new(file);
@@ -860,6 +844,7 @@ impl PharData {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::Catch as _;
fn write_file(dir: &std::path::Path, name: &str, content: &[u8]) -> std::path::PathBuf {
let path = dir.join(name);
@@ -934,9 +919,9 @@ mod tests {
let error = PharData::new("/nonexistent-dir/foo.tar").unwrap_err();
assert!(
error
- .downcast_ref::<UnexpectedValueException>()
+ .catch::<UnexpectedValueException>()
.unwrap()
- .message
+ .get_message()
.starts_with("Cannot create phar")
);
}
@@ -1030,9 +1015,9 @@ mod tests {
let error = Phar::new(&phar_path).unwrap_err();
assert!(
error
- .downcast_ref::<UnexpectedValueException>()
+ .catch::<UnexpectedValueException>()
.unwrap()
- .message
+ .get_message()
.contains("broken signature")
);
}
diff --git a/crates/shirabe-php-shim/src/var.rs b/crates/shirabe-php-shim/src/var.rs
index dfbd2f03..8697097d 100644
--- a/crates/shirabe-php-shim/src/var.rs
+++ b/crates/shirabe-php-shim/src/var.rs
@@ -222,14 +222,6 @@ pub fn get_class(_object: &PhpMixed) -> String {
todo!()
}
-// Overload accepting an `anyhow::Error` (PHP's `get_class($e)` is commonly used on exceptions).
-pub fn get_class_err(_e: &anyhow::Error) -> String {
- // TODO(phase-c): PHP returns the exception's class name. anyhow::Error carries the concrete
- // exception type, but mapping each ported exception struct to its PHP class name is not yet
- // wired up (cf. php_exception_get_code which downcasts case by case).
- todo!()
-}
-
pub fn get_debug_type(value: &PhpMixed) -> String {
match value {
PhpMixed::Null => "null".to_string(),
diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs
index 6a088419..7284b029 100644
--- a/crates/shirabe-php-shim/src/zip.rs
+++ b/crates/shirabe-php-shim/src/zip.rs
@@ -146,13 +146,10 @@ impl ZipArchive {
pub fn extract_to(&self, path: impl AsRef<std::path::Path>) -> Result<bool, ErrorException> {
if let Some(mock) = &self.mock {
- return mock.extract_to.clone().map_err(|message| ErrorException {
- message,
- code: 0,
- severity: 1,
- filename: String::new(),
- lineno: 0,
- });
+ return mock
+ .extract_to
+ .clone()
+ .map_err(|message| ErrorException::new(message, 0, 1, String::new(), 0, None));
}
let mut state = self.state.borrow_mut();
let ZipState::Reader(archive) = &mut *state else {