From f4cad2123b2af0de72bda4ce039e16e74f163f4e Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sat, 8 Aug 2026 22:14:12 +0900 Subject: 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::()` 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) --- crates/shirabe-php-shim/src/exception.rs | 541 +++++++++++++++++++++++++------ crates/shirabe-php-shim/src/fs.rs | 18 +- crates/shirabe-php-shim/src/phar.rs | 137 ++++---- crates/shirabe-php-shim/src/var.rs | 8 - crates/shirabe-php-shim/src/zip.rs | 11 +- 5 files changed, 516 insertions(+), 199 deletions(-) (limited to 'crates/shirabe-php-shim') 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>, } -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); + +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::() + } + + /// 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(&self) -> Option<&T> { + let mut class: &dyn Throwable = &*self.0; + loop { + if let Some(instance) = class.as_any().downcast_ref::() { + 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(&mut self) -> Option<&mut T> { + let mut superclasses = 0; + let mut class: &dyn Throwable = &*self.0; + while !class.as_any().is::() { + 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::() + } + + 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(&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(&mut self) -> Option<&mut T>; + + /// PHP's `$e instanceof T`. + fn is_instanceof(&self) -> bool { + self.catch::().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(&self) -> bool; } -impl std::error::Error for LogicException {} +impl Catch for anyhow::Error { + fn catch(&self) -> Option<&T> { + self.downcast_ref::()?.downcast_ref::() + } -#[derive(Debug)] -pub struct BadMethodCallException { - pub message: String, - pub code: i64, + fn catch_mut(&mut self) -> Option<&mut T> { + self.downcast_mut::()?.downcast_mut::() + } + + fn is_class(&self) -> bool { + self.downcast_ref::() + .is_some_and(|e| e.is_class::()) + } } -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(&self) -> Option<&T> { + self.downcast_ref::() + } + + fn catch_mut(&mut self) -> Option<&mut T> { + self.downcast_mut::() + } + + fn is_class(&self) -> bool { + self.0.as_any().is::() } } -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>, + ) -> 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>, + ) -> 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>, + ) -> 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::() { - 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::() { - 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::() { - 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::().map(|e| e.detail), Some(7)); + assert!(error.catch::().is_some()); + assert!(error.catch::().is_some()); + assert!(error.catch::().is_some()); + assert!(error.catch::().is_some()); } - if let Some(e) = _error.downcast_ref::() { - 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::().is_none()); + assert!(error.catch::().is_none()); + assert!(error.catch::().is_none()); } - if let Some(e) = _error.downcast_ref::() { - 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::().is_some()); + assert!(error.catch::().is_some()); + assert!(error.catch::().is_none()); } - if let Some(e) = _error.downcast_ref::() { - return e.code as i32; + + #[test] + fn catch_reaches_nothing_in_a_plain_rust_error() { + let error = anyhow::anyhow!("boom"); + + assert!(error.catch::().is_none()); } - if let Some(e) = _error.downcast_ref::() { - return e.code as i32; + + #[test] + fn is_class_reaches_no_superclass() { + let error: anyhow::Error = Subclass::new(7).into(); + + assert!(error.is_class::()); + assert!(error.is_instanceof::()); + assert!(!error.is_class::()); + assert!(!error.is_class::()); } - if let Some(e) = _error.downcast_ref::() { - 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::()); } - if let Some(e) = _error.downcast_ref::() { - 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::() + .unwrap() + .set_code(42); + + assert_eq!(error.catch::().unwrap().get_code(), 42); } - if let Some(e) = _error.downcast_ref::() { - 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::().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 { 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, ) -> Result, 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> { 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::() + .catch::() .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::() + .catch::() .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) -> Result { 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 { -- cgit v1.3.1-4-g156e