artichoke_backend/
load.rs

1use std::borrow::Cow;
2use std::ffi::OsStr;
3use std::path::Path;
4
5use artichoke_core::eval::Eval;
6use artichoke_core::file::File;
7use artichoke_core::load::{LoadSources, Loaded, Required};
8use scolapasta_path::os_str_to_bytes;
9use spinoso_exception::LoadError;
10
11use crate::Artichoke;
12use crate::error::Error;
13use crate::ffi::InterpreterExtractError;
14
15const RUBY_EXTENSION: &str = "rb";
16
17impl LoadSources for Artichoke {
18    type Artichoke = Self;
19    type Error = Error;
20    type Exception = Error;
21
22    fn def_file_for_type<P, T>(&mut self, path: P) -> Result<(), Self::Error>
23    where
24        P: AsRef<Path>,
25        T: File<Artichoke = Self::Artichoke, Error = Self::Exception>,
26    {
27        let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
28        let path = path.as_ref();
29        state.load_path_vfs.register_extension(path, T::require)?;
30        Ok(())
31    }
32
33    fn def_rb_source_file<P, T>(&mut self, path: P, contents: T) -> Result<(), Self::Error>
34    where
35        P: AsRef<Path>,
36        T: Into<Cow<'static, [u8]>>,
37    {
38        let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
39        let path = path.as_ref();
40        state.load_path_vfs.write_file(path, contents.into())?;
41        Ok(())
42    }
43
44    fn resolve_source_path<P>(&self, path: P) -> Result<Option<Vec<u8>>, Self::Error>
45    where
46        P: AsRef<Path>,
47    {
48        let state = self.state.as_deref().ok_or_else(InterpreterExtractError::new)?;
49        let path = path.as_ref();
50        if let Some(path) = state.load_path_vfs.resolve_file(path) {
51            return Ok(Some(path));
52        }
53        // If the given path did not end in `.rb`, try again with a `.rb` file
54        // extension.
55        if !matches!(path.extension(), Some(ext) if *ext == *OsStr::new(RUBY_EXTENSION)) {
56            let mut path = path.to_owned();
57            path.set_extension(RUBY_EXTENSION);
58            return Ok(state.load_path_vfs.resolve_file(&path));
59        }
60        Ok(None)
61    }
62
63    fn source_is_file<P>(&self, path: P) -> Result<bool, Self::Error>
64    where
65        P: AsRef<Path>,
66    {
67        let state = self.state.as_deref().ok_or_else(InterpreterExtractError::new)?;
68        let path = path.as_ref();
69        if state.load_path_vfs.is_file(path) {
70            return Ok(true);
71        }
72        // If the given path did not end in `.rb`, try again with a `.rb` file
73        // extension.
74        if !matches!(path.extension(), Some(ext) if *ext == *OsStr::new(RUBY_EXTENSION)) {
75            let mut path = path.to_owned();
76            path.set_extension(RUBY_EXTENSION);
77            if state.load_path_vfs.is_file(&path) {
78                return Ok(true);
79            }
80        }
81        Ok(false)
82    }
83
84    fn load_source<P>(&mut self, path: P) -> Result<Loaded, Self::Error>
85    where
86        P: AsRef<Path>,
87    {
88        let path = path.as_ref();
89        let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
90        // Require Rust `File` first because an File may define classes and
91        // modules with `LoadSources` and Ruby files can require arbitrary
92        // other files, including some child sources that may depend on these
93        // module definitions.
94        if let Some(hook) = state.load_path_vfs.get_extension(path) {
95            // dynamic, Rust-backed `File` require
96            hook(self)?;
97        }
98        let contents = self
99            .read_source_file_contents(path)
100            .map_err(|_| {
101                let mut message = b"cannot load such file".to_vec();
102                if let Ok(bytes) = os_str_to_bytes(path.as_os_str()) {
103                    message.extend_from_slice(b" -- ");
104                    message.extend_from_slice(bytes);
105                }
106                LoadError::from(message)
107            })?
108            .into_owned();
109        self.eval(contents.as_ref())?;
110        Ok(Loaded::Success)
111    }
112
113    fn require_source<P>(&mut self, path: P) -> Result<Required, Self::Error>
114    where
115        P: AsRef<Path>,
116    {
117        let path = path.as_ref();
118        let mut alternate_path;
119        let path = {
120            let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
121            // If a file is already required, short circuit.
122            if let Some(true) = state.load_path_vfs.is_required(path) {
123                return Ok(Required::AlreadyRequired);
124            }
125            // Require Rust `File` first because an File may define classes and
126            // modules with `LoadSources` and Ruby files can require arbitrary
127            // other files, including some child sources that may depend on these
128            // module definitions.
129            match state.load_path_vfs.get_extension(path) {
130                Some(hook) => {
131                    // dynamic, Rust-backed `File` require
132                    hook(self)?;
133                    path
134                }
135                None if matches!(path.extension(), Some(ext) if *ext == *OsStr::new(RUBY_EXTENSION)) => path,
136                None => {
137                    alternate_path = path.to_owned();
138                    alternate_path.set_extension(RUBY_EXTENSION);
139                    // If a file is already required, short circuit.
140                    if let Some(true) = state.load_path_vfs.is_required(&alternate_path) {
141                        return Ok(Required::AlreadyRequired);
142                    }
143                    if let Some(hook) = state.load_path_vfs.get_extension(&alternate_path) {
144                        // dynamic, Rust-backed `File` require
145                        hook(self)?;
146                    } else {
147                        // Try to load the source at the given path
148                        if let Ok(contents) = self.read_source_file_contents(path) {
149                            let contents = contents.into_owned();
150                            self.eval(&contents)?;
151                            let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
152                            state.load_path_vfs.mark_required(path)?;
153                            return Ok(Required::Success);
154                        }
155                        // else proceed with the alternate path
156                    }
157                    // This ensures that if we load the hook at an alternate
158                    // path, we use that alternate path to load the Ruby source.
159                    &alternate_path
160                }
161            }
162        };
163        let contents = self.read_source_file_contents(path)?.into_owned();
164        self.eval(contents.as_ref())?;
165        let state = self.state.as_deref_mut().ok_or_else(InterpreterExtractError::new)?;
166        state.load_path_vfs.mark_required(path)?;
167        Ok(Required::Success)
168    }
169
170    fn read_source_file_contents<P>(&self, path: P) -> Result<Cow<'_, [u8]>, Self::Error>
171    where
172        P: AsRef<Path>,
173    {
174        let state = self.state.as_deref().ok_or_else(InterpreterExtractError::new)?;
175        let path = path.as_ref();
176        let contents = state.load_path_vfs.read_file(path)?;
177        Ok(contents.into())
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use artichoke_core::load::{Loaded, Required};
184    use bstr::ByteSlice;
185
186    use crate::test::prelude::*;
187
188    const NON_IDEMPOTENT_LOAD: &[u8] = b"
189module LoadSources
190  class Counter
191    attr_reader :c
192
193    def initialize(c)
194      @c = c
195    end
196
197    def inc!
198      @c += 1
199    end
200
201    def self.instance
202      @instance ||= new(10)
203    end
204  end
205end
206
207LoadSources::Counter.instance.inc!
208    ";
209
210    #[test]
211    fn load_has_no_memory() {
212        let mut interp = interpreter();
213        interp.def_rb_source_file("counter.rb", NON_IDEMPOTENT_LOAD).unwrap();
214
215        let result = interp.load_source("./counter.rb").unwrap();
216        assert_eq!(result, Loaded::Success);
217        let count = interp
218            .eval(b"LoadSources::Counter.instance.c")
219            .unwrap()
220            .try_convert_into::<usize>(&interp)
221            .unwrap();
222        assert_eq!(count, 11);
223
224        // `Kernel#load` has no memory and will always execute
225        let result = interp.load_source("./counter.rb").unwrap();
226        assert_eq!(result, Loaded::Success);
227        let count = interp
228            .eval(b"LoadSources::Counter.instance.c")
229            .unwrap()
230            .try_convert_into::<usize>(&interp)
231            .unwrap();
232        assert_eq!(count, 12);
233    }
234
235    #[test]
236    fn load_has_no_memory_and_ignores_loaded_features() {
237        let mut interp = interpreter();
238        interp.def_rb_source_file("counter.rb", NON_IDEMPOTENT_LOAD).unwrap();
239
240        let result = interp.require_source("./counter.rb").unwrap();
241        assert_eq!(result, Required::Success);
242        let count = interp
243            .eval(b"LoadSources::Counter.instance.c")
244            .unwrap()
245            .try_convert_into::<usize>(&interp)
246            .unwrap();
247        assert_eq!(count, 11);
248
249        let result = interp.require_source("./counter.rb").unwrap();
250        assert_eq!(result, Required::AlreadyRequired);
251
252        let result = interp.load_source("./counter.rb").unwrap();
253        assert_eq!(result, Loaded::Success);
254        let count = interp
255            .eval(b"LoadSources::Counter.instance.c")
256            .unwrap()
257            .try_convert_into::<usize>(&interp)
258            .unwrap();
259        assert_eq!(count, 12);
260
261        // `Kernel#load` has no memory and will always execute
262        let result = interp.load_source("./counter.rb").unwrap();
263        assert_eq!(result, Loaded::Success);
264        let count = interp
265            .eval(b"LoadSources::Counter.instance.c")
266            .unwrap()
267            .try_convert_into::<usize>(&interp)
268            .unwrap();
269        assert_eq!(count, 13);
270    }
271
272    #[test]
273    fn load_does_not_discover_paths_from_loaded_features() {
274        let mut interp = interpreter();
275        interp.def_rb_source_file("counter.rb", NON_IDEMPOTENT_LOAD).unwrap();
276
277        let result = interp.require_source("./counter").unwrap();
278        assert_eq!(result, Required::Success);
279        let count = interp
280            .eval(b"LoadSources::Counter.instance.c")
281            .unwrap()
282            .try_convert_into::<usize>(&interp)
283            .unwrap();
284        assert_eq!(count, 11);
285
286        let exc = interp.load_source("./counter").unwrap_err();
287        assert_eq!(exc.message().as_bstr(), b"cannot load such file -- ./counter".as_bstr());
288        assert_eq!(exc.name(), "LoadError");
289    }
290}