artichoke_backend/
eval.rs

1use std::ffi::OsStr;
2use std::path::Path;
3
4use scolapasta_path::os_str_to_bytes;
5use spinoso_exception::{ArgumentError, Fatal, LoadError};
6
7use crate::Artichoke;
8use crate::core::{Eval, LoadSources, Parser};
9use crate::error::Error;
10use crate::ffi::InterpreterExtractError;
11use crate::state::parser::Context;
12use crate::sys;
13use crate::sys::protect;
14use crate::value::Value;
15use crate::{RubyException, exception_handler};
16
17impl Eval for Artichoke {
18    type Value = Value;
19
20    type Error = Error;
21
22    fn eval(&mut self, code: &[u8]) -> Result<Self::Value, Self::Error> {
23        // SAFETY: `protect::eval` requires an initialized mruby interpreter
24        // which is guaranteed by the `Artichoke` type. All dereferenced
25        // pointers are valid by `Artichoke` invariants.
26        let result = unsafe {
27            let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
28            let parser = state.parser.as_mut().ok_or_else(InterpreterExtractError::new)?;
29            let context: *mut sys::mrbc_context = parser.context_mut();
30            self.with_ffi_boundary(|mrb| protect::eval(mrb, context, code))?
31        };
32
33        let result = result.map(Value::from).map_err(Value::from);
34
35        match result {
36            Ok(value) if value.is_unreachable() => {
37                // Unreachable values are internal to the mruby interpreter and
38                // interacting with them via the C API is unspecified and may
39                // result in a segfault.
40                //
41                // See: <https://github.com/mruby/mruby/issues/4460>
42                emit_fatal_warning!("eval returned an unreachable Ruby value");
43                Err(Fatal::from("eval returned an unreachable Ruby value").into())
44            }
45            Ok(value) => Ok(self.protect(value)),
46            Err(exception) => {
47                let exception = self.protect(exception);
48                Err(exception_handler::last_error(self, exception)?)
49            }
50        }
51    }
52
53    fn eval_os_str(&mut self, code: &OsStr) -> Result<Self::Value, Self::Error> {
54        let code = os_str_to_bytes(code)?;
55        self.eval(code)
56    }
57
58    fn eval_file(&mut self, file: &Path) -> Result<Self::Value, Self::Error> {
59        let context = Context::new(os_str_to_bytes(file.as_os_str())?.to_vec())
60            .ok_or_else(|| ArgumentError::with_message("path name contains null byte"))?;
61        self.push_context(context)?;
62        let code = self
63            .read_source_file_contents(file)
64            .map_err(|err| {
65                let mut message = b"ruby: ".to_vec();
66                message.extend_from_slice(err.message().as_ref());
67                if let Ok(bytes) = os_str_to_bytes(file.as_os_str()) {
68                    message.extend_from_slice(b" -- ");
69                    message.extend_from_slice(bytes);
70                }
71                LoadError::from(message)
72            })?
73            .into_owned();
74        let result = self.eval(code.as_slice());
75        self.pop_context()?;
76        result
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    #[cfg(unix)]
83    use std::ffi::OsStr;
84    #[cfg(unix)]
85    use std::os::unix::ffi::OsStrExt;
86    use std::path::Path;
87
88    use bstr::ByteSlice;
89
90    use crate::test::prelude::*;
91
92    #[test]
93    fn root_eval_context() {
94        let mut interp = interpreter();
95        let result = interp.eval(b"__FILE__").unwrap();
96        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
97        assert_eq!(result, "(eval)");
98    }
99
100    #[test]
101    fn context_is_restored_after_eval() {
102        let mut interp = interpreter();
103        let context = Context::new(&b"context.rb"[..]).unwrap();
104        interp.push_context(context).unwrap();
105        interp.eval(b"15").unwrap();
106        let context = interp.peek_context().unwrap();
107        let filename = context.unwrap().filename();
108        assert_eq!(filename.as_bstr(), b"context.rb".as_bstr());
109    }
110
111    #[test]
112    fn root_context_is_not_pushed_after_eval() {
113        let mut interp = interpreter();
114        interp.eval(b"15").unwrap();
115        let context = interp.peek_context().unwrap();
116        assert!(context.is_none());
117    }
118
119    mod nested {
120        use crate::test::prelude::*;
121
122        #[derive(Debug)]
123        struct NestedEval;
124
125        unsafe extern "C-unwind" fn nested_eval_file(
126            mrb: *mut sys::mrb_state,
127            _slf: sys::mrb_value,
128        ) -> sys::mrb_value {
129            unwrap_interpreter!(mrb, to => guard);
130            let Ok(value) = guard.eval(b"__FILE__") else {
131                return Value::nil().inner();
132            };
133            value.inner()
134        }
135
136        impl File for NestedEval {
137            type Artichoke = Artichoke;
138
139            type Error = Error;
140
141            fn require(interp: &mut Artichoke) -> Result<(), Self::Error> {
142                let spec = module::Spec::new(interp, "NestedEval", c"NestedEval", None)?;
143                module::Builder::for_spec(interp, &spec)
144                    .add_self_method("file", nested_eval_file, sys::mrb_args_none())?
145                    .define()?;
146                interp.def_module::<Self>(spec)?;
147                Ok(())
148            }
149        }
150
151        #[test]
152        #[should_panic]
153        #[expect(clippy::should_panic_without_expect, reason = "this test is known broken")]
154        fn eval_context_is_a_stack() {
155            let mut interp = interpreter();
156            interp.def_file_for_type::<_, NestedEval>("nested_eval.rb").unwrap();
157            let code = b"require 'nested_eval'; NestedEval.file";
158            let result = interp.eval(code).unwrap();
159            let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
160            assert_eq!(result, "/src/lib/nested_eval.rb");
161        }
162    }
163
164    #[test]
165    fn eval_with_context() {
166        let mut interp = interpreter();
167
168        let context = Context::new(b"source.rb".as_ref()).unwrap();
169        interp.push_context(context).unwrap();
170        let result = interp.eval(b"__FILE__").unwrap();
171        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
172        assert_eq!(result, "source.rb");
173        interp.pop_context().unwrap();
174
175        let context = Context::new(b"source.rb".as_ref()).unwrap();
176        interp.push_context(context).unwrap();
177        let result = interp.eval(b"__FILE__").unwrap();
178        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
179        assert_eq!(result, "source.rb");
180        interp.pop_context().unwrap();
181
182        let context = Context::new(b"main.rb".as_ref()).unwrap();
183        interp.push_context(context).unwrap();
184        let result = interp.eval(b"__FILE__").unwrap();
185        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
186        assert_eq!(result, "main.rb");
187        interp.pop_context().unwrap();
188    }
189
190    #[test]
191    fn unparseable_code_returns_err_syntax_error() {
192        let mut interp = interpreter();
193        let err = interp.eval(b"'a").unwrap_err();
194        assert_eq!("SyntaxError", err.name().as_ref());
195    }
196
197    #[test]
198    fn interpreter_is_usable_after_syntax_error() {
199        let mut interp = interpreter();
200        let err = interp.eval(b"'a").unwrap_err();
201        assert_eq!("SyntaxError", err.name().as_ref());
202        // Ensure interpreter is usable after evaling unparseable code
203        let result = interp.eval(b"'a' * 10 ").unwrap();
204        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
205        assert_eq!(result, "a".repeat(10));
206    }
207
208    #[test]
209    fn file_magic_constant() {
210        let file = if cfg!(windows) {
211            "c:/artichoke/virtual_root/src/lib/source.rb"
212        } else {
213            "/artichoke/virtual_root/src/lib/source.rb"
214        };
215        let mut interp = interpreter();
216        interp
217            .def_rb_source_file("source.rb", &b"def file; __FILE__; end"[..])
218            .unwrap();
219        let result = interp.eval(b"require 'source'; file").unwrap();
220        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
221        assert_eq!(result, file);
222    }
223
224    #[test]
225    fn file_not_persistent() {
226        let mut interp = interpreter();
227        interp
228            .def_rb_source_file("source.rb", &b"def file; __FILE__; end"[..])
229            .unwrap();
230        let result = interp.eval(b"require 'source'; __FILE__").unwrap();
231        let result = result.try_convert_into_mut::<&str>(&mut interp).unwrap();
232        assert_eq!(result, "(eval)");
233    }
234
235    #[test]
236    fn return_syntax_error() {
237        let mut interp = interpreter();
238        interp
239            .def_rb_source_file("fail.rb", &b"def bad; 'as'.scan(; end"[..])
240            .unwrap();
241        let err = interp.eval(b"require 'fail'").unwrap_err();
242        assert_eq!("SyntaxError", err.name().as_ref());
243    }
244
245    #[test]
246    fn eval_file_error_file_not_found() {
247        let mut interp = interpreter();
248        let err = interp.eval_file(Path::new("no/such/file.rb")).unwrap_err();
249        assert_eq!("LoadError", err.name().as_ref());
250        assert_eq!(
251            b"ruby: file not found in virtual file system -- no/such/file.rb",
252            err.message().as_ref()
253        );
254    }
255
256    #[test]
257    #[cfg(unix)]
258    fn eval_file_error_invalid_path() {
259        let mut interp = interpreter();
260        let err = interp
261            .eval_file(Path::new(OsStr::from_bytes(b"not/valid/utf8/\xff.rb")))
262            .unwrap_err();
263        assert_eq!("LoadError", err.name().as_ref());
264        assert_eq!(
265            b"ruby: file not found in virtual file system -- not/valid/utf8/\xFF.rb",
266            err.message().as_ref()
267        );
268    }
269}