1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
use log::{debug, trace};
use std::ffi::c_void;
use std::fmt;

use crate::gc::MrbGarbageCollection;
use crate::sys;
use crate::value::{Value, ValueLike};
use crate::Mrb;
use crate::MrbError;

/// Metadata about a Ruby exception.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Exception {
    /// The result of calling `exception.class.name`.
    pub class: String,
    /// The result of calling `exception.message`.
    pub message: String,
    /// The result of calling `exception.backtrace`.
    ///
    /// Some exceptions, like `SyntaxError` which is thrown directly by the
    /// mruby VM, do not have backtraces, so this field is optional.
    pub backtrace: Option<Vec<String>>,
    /// The result of calling `exception.inspect`.
    pub inspect: String,
}

impl Exception {
    pub fn new(class: &str, message: &str, backtrace: Option<Vec<String>>, inspect: &str) -> Self {
        Self {
            class: class.to_owned(),
            message: message.to_owned(),
            backtrace,
            inspect: inspect.to_owned(),
        }
    }
}

impl fmt::Display for Exception {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.inspect)?;
        if let Some(ref backtrace) = self.backtrace {
            for frame in backtrace {
                write!(f, "\n{}", frame)?;
            }
        }
        Ok(())
    }
}

#[derive(Debug, PartialEq, Eq)]
pub enum LastError {
    Some(Exception),
    None,
    UnableToExtract(MrbError),
}

/// Extract the last exception thrown on the interpreter.
pub trait MrbExceptionHandler {
    /// Extract the last thrown exception on the mruby interpreter if there is
    /// one.
    ///
    /// If there is an error, return [`LastError::Some`], which contains the
    /// exception class name, message, and optional backtrace.
    fn last_error(&self) -> LastError;
}

impl MrbExceptionHandler for Mrb {
    fn last_error(&self) -> LastError {
        let _arena = self.create_arena_savepoint();
        let mrb = { self.borrow().mrb };
        let exc = unsafe {
            let exc = (*mrb).exc;
            // Clear the current exception from the mruby interpreter so
            // subsequent calls to the mruby VM are not tainted by an error they
            // did not generate.
            //
            // We must do this at the beginning of `current_exception` so we can
            // use the mruby VM to inspect the exception once we turn it into an
            // `mrb_value`. `ValueLike::funcall` handles errors by calling this
            // function, so not clearing the exception results in a stack
            // overflow.
            (*mrb).exc = std::ptr::null_mut();
            exc
        };
        if exc.is_null() {
            trace!("No last error present");
            return LastError::None;
        }
        // Generate exception metadata in by executing the following Ruby code:
        //
        // ```ruby
        // clazz = exception.class.name
        // message = exception.message
        // backtrace = exception.backtrace
        // ```
        let exception = Value::new(self, unsafe { sys::mrb_sys_obj_value(exc as *mut c_void) });
        let class = exception
            .funcall::<Value, _, _>("class", &[])
            .and_then(|exception| exception.funcall::<String, _, _>("name", &[]));
        let class = match class {
            Ok(class) => class,
            Err(err) => return LastError::UnableToExtract(err),
        };
        let message = match exception.funcall::<String, _, _>("message", &[]) {
            Ok(message) => message,
            Err(err) => return LastError::UnableToExtract(err),
        };
        let backtrace = match exception.funcall::<Option<Vec<String>>, _, _>("backtrace", &[]) {
            Ok(backtrace) => backtrace,
            Err(err) => return LastError::UnableToExtract(err),
        };
        let inspect = match exception.funcall::<String, _, _>("inspect", &[]) {
            Ok(inspect) => inspect,
            Err(err) => return LastError::UnableToExtract(err),
        };
        let exception = Exception {
            class,
            message,
            backtrace,
            inspect,
        };
        debug!("Extracted exception from interpreter: {}", exception);
        LastError::Some(exception)
    }
}

#[cfg(test)]
mod tests {
    use crate::eval::MrbEval;
    use crate::exception::Exception;
    use crate::value::ValueLike;
    use crate::MrbError;

    #[test]
    fn return_exception() {
        let interp = crate::interpreter().expect("mrb init");
        let result = interp
            .eval("raise ArgumentError.new('waffles')")
            .map(|_| ());
        let expected = Exception::new(
            "ArgumentError",
            "waffles",
            Some(vec!["(eval):1".to_owned()]),
            "(eval):1: waffles (ArgumentError)",
        );
        assert_eq!(result, Err(MrbError::Exec(expected.to_string())));
    }

    #[test]
    fn return_exception_with_no_backtrace() {
        let interp = crate::interpreter().expect("mrb init");
        let result = interp.eval("def bad; (; end").map(|_| ());
        let expected = Exception::new("SyntaxError", "waffles", None, "SyntaxError: syntax error");
        assert_eq!(result, Err(MrbError::Exec(expected.to_string())));
    }

    #[test]
    fn raise_does_not_panic_or_segfault() {
        let interp = crate::interpreter().expect("mrb init");
        let _ = interp.eval(r#"raise 'foo'"#);
        let _ = interp.eval(r#"raise 'foo'"#);
        let _ = interp.eval(r#"eval "raise 'foo'""#);
        let _ = interp.eval(r#"eval "raise 'foo'""#);
        let _ = interp.eval(r#"require 'foo'"#);
        let _ = interp.eval(r#"require 'foo'"#);
        let _ = interp.eval(r#"eval "require 'foo'""#);
        let _ = interp.eval(r#"eval "require 'foo'""#);
        let _ = interp.eval(r#"Regexp.compile(2)"#);
        let _ = interp.eval(r#"Regexp.compile(2)"#);
        let _ = interp.eval(r#"eval "Regexp.compile(2)""#);
        let _ = interp.eval(r#"eval "Regexp.compile(2)""#);
        let _ = interp.eval(
            r#"
def fail
  begin
    require 'foo'
  rescue LoadError
    require 'forwardable'
  end
end

fail
            "#,
        );
        let kernel = interp.eval(r#"Kernel"#).unwrap();
        let _ = kernel.funcall::<(), _, _>("raise", &[]);
        let _ = kernel.funcall::<(), _, _>("raise", &[]);
    }
}