artichoke_backend/
exception_handler.rs

1use std::borrow::Cow;
2use std::error;
3use std::fmt;
4
5use bstr::BString;
6use scolapasta_string_escape::format_debug_escape_into;
7
8use crate::Artichoke;
9use crate::core::{TryConvertMut, Value as _};
10use crate::error::{Error, RubyException};
11use crate::gc::MrbGarbageCollection;
12use crate::sys;
13use crate::value::Value;
14
15/// Incrementally construct a [`CaughtException`].
16///
17/// See also [`CaughtException::builder`].
18#[derive(Default, Debug)]
19pub struct Builder(CaughtException);
20
21impl Builder {
22    /// Construct a new, empty `Builder`.
23    #[must_use]
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    #[must_use]
29    pub fn with_value(mut self, value: Value) -> Self {
30        self.0.value = value;
31        self
32    }
33
34    #[must_use]
35    pub fn with_name(mut self, name: String) -> Self {
36        self.0.name = name;
37        self
38    }
39
40    #[must_use]
41    pub fn with_message(mut self, message: Vec<u8>) -> Self {
42        self.0.message = message.into();
43        self
44    }
45
46    #[must_use]
47    pub fn finish(self) -> CaughtException {
48        self.0
49    }
50}
51
52/// An `Exception` rescued with [`sys::mrb_protect`].
53///
54/// `CaughtException` is re-raiseable because it implements [`RubyException`].
55#[derive(Default, Debug, Clone)]
56pub struct CaughtException {
57    value: Value,
58    name: String,
59    message: BString,
60}
61
62impl CaughtException {
63    /// Incrementally construct a [`CaughtException`].
64    #[must_use]
65    pub fn builder() -> Builder {
66        Builder::new()
67    }
68
69    /// Construct a new `CaughtException`.
70    #[must_use]
71    pub fn with_value_class_and_message(value: Value, name: String, message: Vec<u8>) -> Self {
72        let message = message.into();
73        Self { value, name, message }
74    }
75}
76
77impl fmt::Display for CaughtException {
78    fn fmt(&self, mut f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        f.write_str(&self.name())?;
80        f.write_str(" (")?;
81        format_debug_escape_into(&mut f, self.message())?;
82        f.write_str(")")?;
83        Ok(())
84    }
85}
86
87impl error::Error for CaughtException {}
88
89impl RubyException for CaughtException {
90    fn message(&self) -> Cow<'_, [u8]> {
91        self.message.as_slice().into()
92    }
93
94    fn name(&self) -> Cow<'_, str> {
95        self.name.as_str().into()
96    }
97
98    fn vm_backtrace(&self, interp: &mut Artichoke) -> Option<Vec<Vec<u8>>> {
99        let backtrace = self.value.funcall(interp, "backtrace", &[], None).ok()?;
100        let backtrace = interp.try_convert_mut(backtrace).ok()?;
101        Some(backtrace)
102    }
103
104    fn as_mrb_value(&self, interp: &mut Artichoke) -> Option<sys::mrb_value> {
105        let _ = interp;
106        Some(self.value.inner())
107    }
108}
109
110impl From<CaughtException> for Box<dyn RubyException> {
111    fn from(exc: CaughtException) -> Self {
112        Box::new(exc)
113    }
114}
115
116impl From<CaughtException> for Error {
117    fn from(exc: CaughtException) -> Self {
118        Self::from(Box::<dyn RubyException>::from(exc))
119    }
120}
121
122/// Transform a `Exception` Ruby `Value` into an [`Error`].
123///
124/// # Errors
125///
126/// This function makes fallible calls into the Ruby VM to resolve the
127/// exception. If these calls return an error, that error is returned from this
128/// function.
129pub fn last_error(interp: &mut Artichoke, exception: Value) -> Result<Error, Error> {
130    let mut arena = interp.create_arena_savepoint()?;
131
132    // Clear the current exception from the mruby interpreter so subsequent
133    // calls to the mruby VM are not tainted by an error they did not
134    // generate.
135    //
136    // We must clear the pointer at the beginning of this function so we can
137    // use the mruby VM to inspect the exception once we turn it into an
138    // `mrb_value`. `Value::funcall` handles errors by calling this
139    // function, so not clearing the exception results in a stack overflow.
140
141    // Generate exception metadata in by executing the Ruby code:
142    //
143    // ```ruby
144    // clazz = exception.class.name
145    // message = exception.message
146    // ```
147
148    // Sometimes when hacking on `extn/core` it is possible to enter a
149    // crash loop where an exception is captured by this handler, but
150    // extracting the exception name or backtrace throws again.
151    // Un-commenting the following print statement will at least get you the
152    // exception class and message, which should help debugging.
153    //
154    // ```
155    // let message = exception.funcall(&mut arena, "message", &[], None)?;
156    // let message = message.try_convert_into_mut::<String>(&mut arena);
157    // println!("{:?}, {:?}", exception, message);
158    // ```
159
160    let class = exception.funcall(&mut arena, "class", &[], None)?;
161    let classname = class.funcall(&mut arena, "name", &[], None)?;
162    let classname = classname.try_convert_into_mut::<&str>(&mut arena)?;
163    let message = exception.funcall(&mut arena, "message", &[], None)?;
164    let message = message.try_convert_into_mut::<&[u8]>(&mut arena)?;
165
166    let exc = CaughtException::builder()
167        .with_value(exception)
168        .with_name(classname.into())
169        .with_message(message.to_vec())
170        .finish();
171    Ok(Error::from(exc))
172}
173
174#[cfg(test)]
175mod tests {
176    use bstr::ByteSlice;
177
178    use crate::test::prelude::*;
179
180    #[test]
181    fn return_exception() {
182        let mut interp = interpreter();
183        let err = interp.eval(b"raise ArgumentError.new('waffles')").unwrap_err();
184        assert_eq!("ArgumentError", err.name().as_ref());
185        assert_eq!(b"waffles".as_bstr(), err.message().as_ref().as_bstr());
186        let expected_backtrace = b"(eval):1".to_vec();
187        let backtrace = bstr::join("\n", err.vm_backtrace(&mut interp).unwrap());
188        assert_eq!(backtrace.as_bstr(), expected_backtrace.as_bstr());
189    }
190
191    #[test]
192    fn return_exception_with_no_backtrace() {
193        let mut interp = interpreter();
194        let err = interp.eval(b"def bad; (; end").unwrap_err();
195        assert_eq!("SyntaxError", err.name().as_ref());
196        assert_eq!(b"syntax error".as_bstr(), err.message().as_ref().as_bstr());
197        assert_eq!(None, err.vm_backtrace(&mut interp));
198    }
199
200    #[test]
201    fn raise_does_not_panic_or_segfault() {
202        let mut interp = interpreter();
203        interp.eval(b"raise 'foo'").unwrap_err();
204        interp.eval(b"raise 'foo'").unwrap_err();
205        interp.eval(br#"eval("raise 'foo'")"#).unwrap_err();
206        interp.eval(br#"eval("raise 'foo'")"#).unwrap_err();
207        interp.eval(b"require 'foo'").unwrap_err();
208        interp.eval(b"require 'foo'").unwrap_err();
209        interp.eval(br#"eval("require 'foo'")"#).unwrap_err();
210        interp.eval(br#"eval("require 'foo'")"#).unwrap_err();
211        interp.eval(b"Regexp.compile(2)").unwrap_err();
212        interp.eval(b"Regexp.compile(2)").unwrap_err();
213        #[cfg(feature = "core-regexp")]
214        {
215            interp.eval(br#"eval("Regexp.compile(2)")"#).unwrap_err();
216            interp.eval(br#"eval("Regexp.compile(2)")"#).unwrap_err();
217        }
218        #[cfg(feature = "stdlib-forwardable")]
219        {
220            const REQUIRE_TEST: &[u8] = b"\
221def fail
222  begin
223    require 'foo'
224  rescue LoadError
225    require 'forwardable'
226  end
227end
228
229fail
230";
231            interp.eval(REQUIRE_TEST).unwrap();
232        }
233        let kernel = interp.eval(b"Kernel").unwrap();
234        kernel.funcall(&mut interp, "raise", &[], None).unwrap_err();
235        kernel.funcall(&mut interp, "raise", &[], None).unwrap_err();
236    }
237}