artichoke_backend/extn/core/kernel/
require.rs

1//! [`Kernel#require`](https://ruby-doc.org/core-3.1.2/Kernel.html#method-i-require)
2
3use std::path::{Path, PathBuf};
4
5use artichoke_core::load::{Loaded, Required};
6use bstr::ByteSlice;
7use scolapasta_path::bytes_to_os_str;
8
9use crate::convert::implicitly_convert_to_string;
10use crate::extn::prelude::*;
11use crate::state::parser::Context;
12
13pub fn load(interp: &mut Artichoke, mut filename: Value) -> Result<Loaded, Error> {
14    // SAFETY: The extracted byte slice is converted to an owned `Vec<u8>`
15    // before the interpreter is used again which protects against a garbage
16    // collection invalidating the pointer.
17    let filename = unsafe { implicitly_convert_to_string(interp, &mut filename)? };
18    if filename.find_byte(b'\0').is_some() {
19        return Err(ArgumentError::with_message("path name contains null byte").into());
20    }
21    let filename = filename.to_vec();
22    let file = bytes_to_os_str(&filename)?;
23    let path = Path::new(file);
24
25    if let Some(mut context) = interp.resolve_source_path(path)? {
26        for byte in &mut context {
27            if *byte == b'\\' {
28                *byte = b'/';
29            }
30        }
31        let context =
32            Context::new(context).ok_or_else(|| ArgumentError::with_message("path name contains null byte"))?;
33        interp.push_context(context)?;
34        let result = interp.load_source(path);
35        interp.pop_context()?;
36        return result;
37    }
38    let mut message = b"cannot load such file -- ".to_vec();
39    message.extend(filename);
40    Err(LoadError::from(message).into())
41}
42
43pub fn require(interp: &mut Artichoke, mut filename: Value) -> Result<Required, Error> {
44    // SAFETY: The extracted byte slice is converted to an owned `Vec<u8>`
45    // before the interpreter is used again which protects against a garbage
46    // collection invalidating the pointer.
47    let filename = unsafe { implicitly_convert_to_string(interp, &mut filename)? };
48    if filename.find_byte(b'\0').is_some() {
49        return Err(ArgumentError::with_message("path name contains null byte").into());
50    }
51    let filename = filename.to_vec();
52    let file = bytes_to_os_str(&filename)?;
53    let path = Path::new(file);
54
55    if let Some(mut context) = interp.resolve_source_path(path)? {
56        for byte in &mut context {
57            if *byte == b'\\' {
58                *byte = b'/';
59            }
60        }
61        let context =
62            Context::new(context).ok_or_else(|| ArgumentError::with_message("path name contains null byte"))?;
63        interp.push_context(context)?;
64        let result = interp.require_source(path);
65        interp.pop_context()?;
66        return result;
67    }
68    let mut message = b"cannot load such file -- ".to_vec();
69    message.extend(filename);
70    Err(LoadError::from(message).into())
71}
72
73pub fn require_relative(interp: &mut Artichoke, mut filename: Value, base: RelativePath) -> Result<Required, Error> {
74    // SAFETY: The extracted byte slice is converted to an owned `Vec<u8>`
75    // before the interpreter is used again which protects against a garbage
76    // collection invalidating the pointer.
77    let filename = unsafe { implicitly_convert_to_string(interp, &mut filename)? };
78    if filename.find_byte(b'\0').is_some() {
79        return Err(ArgumentError::with_message("path name contains null byte").into());
80    }
81    let filename = filename.to_vec();
82    let file = bytes_to_os_str(&filename)?;
83    let path = base.join(Path::new(file));
84
85    if let Some(mut context) = interp.resolve_source_path(&path)? {
86        for byte in &mut context {
87            if *byte == b'\\' {
88                *byte = b'/';
89            }
90        }
91        let context =
92            Context::new(context).ok_or_else(|| ArgumentError::with_message("path name contains null byte"))?;
93        interp.push_context(context)?;
94        let result = interp.require_source(&path);
95        interp.pop_context()?;
96        return result;
97    }
98    let mut message = b"cannot load such file -- ".to_vec();
99    message.extend(filename);
100    Err(LoadError::from(message).into())
101}
102
103#[derive(Default, Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
104pub struct RelativePath(PathBuf);
105
106impl From<PathBuf> for RelativePath {
107    fn from(path: PathBuf) -> Self {
108        Self(path)
109    }
110}
111
112impl From<&Path> for RelativePath {
113    fn from(path: &Path) -> Self {
114        Self(path.into())
115    }
116}
117
118impl From<String> for RelativePath {
119    fn from(path: String) -> Self {
120        Self(path.into())
121    }
122}
123
124impl From<&str> for RelativePath {
125    fn from(path: &str) -> Self {
126        Self(path.into())
127    }
128}
129
130impl RelativePath {
131    pub fn join<P: AsRef<Path>>(&self, path: P) -> PathBuf {
132        self.0.join(path.as_ref())
133    }
134
135    pub fn try_from_interp(interp: &mut Artichoke) -> Result<Self, Error> {
136        let context = interp
137            .peek_context()?
138            .ok_or_else(|| Fatal::from("relative require with no context stack"))?;
139        let path = bytes_to_os_str(context.filename())?;
140        let path = Path::new(path);
141        if let Some(base) = path.parent() {
142            Ok(Self::from(base))
143        } else {
144            Ok(Self::from("/"))
145        }
146    }
147}
148
149#[cfg(test)]
150mod test {
151    use bstr::ByteSlice;
152
153    use crate::test::prelude::*;
154
155    #[derive(Debug)]
156    struct MockSourceFile;
157
158    impl File for MockSourceFile {
159        type Artichoke = Artichoke;
160
161        type Error = Error;
162
163        fn require(interp: &mut Artichoke) -> Result<(), Self::Error> {
164            interp.eval(b"@i = 255").unwrap();
165            Ok(())
166        }
167    }
168
169    #[derive(Debug)]
170    struct MockExtensionAndSourceFile;
171
172    impl File for MockExtensionAndSourceFile {
173        type Artichoke = Artichoke;
174
175        type Error = Error;
176
177        fn require(interp: &mut Artichoke) -> Result<(), Self::Error> {
178            interp.eval(b"module Foo; RUST = 7; end").unwrap();
179            Ok(())
180        }
181    }
182
183    // Functional test for `Kernel::require`:
184    //
185    // - require side effects (e.g. ivar set or class def) effect the interpreter
186    // - Successful first require returns `true`.
187    // - Second require returns `false`.
188    // - Second require does not cause require side effects.
189    // - Require non-existing file raises and returns `nil`.
190    #[test]
191    fn functional() {
192        let mut interp = interpreter();
193        interp.def_file_for_type::<_, MockSourceFile>("file.rb").unwrap();
194        let result = interp.eval(b"require 'file'").unwrap();
195        let require_result = result.try_convert_into::<bool>(&interp).unwrap();
196        assert!(require_result);
197        let result = interp.eval(b"@i").unwrap();
198        let i_result = result.try_convert_into::<i64>(&interp).unwrap();
199        assert_eq!(i_result, 255);
200        let result = interp.eval(b"@i = 1000; require 'file'").unwrap();
201        let second_require_result = result.try_convert_into::<bool>(&interp).unwrap();
202        assert!(!second_require_result);
203        let result = interp.eval(b"@i").unwrap();
204        let second_i_result = result.try_convert_into::<i64>(&interp).unwrap();
205        assert_eq!(second_i_result, 1000);
206        let err = interp.eval(b"require 'non-existent-source'").unwrap_err();
207        assert_eq!(
208            b"cannot load such file -- non-existent-source".as_bstr(),
209            err.message().as_ref().as_bstr()
210        );
211        let expected_backtrace = b"(eval):1:in require\n(eval):1".to_vec();
212        let actual_backtrace = bstr::join("\n", err.vm_backtrace(&mut interp).unwrap());
213        assert_eq!(expected_backtrace.as_bstr(), actual_backtrace.as_bstr());
214    }
215
216    #[test]
217    fn absolute_path() {
218        let mut interp = interpreter();
219        let (path, require_code) = if cfg!(windows) {
220            (
221                "c:/artichoke/virtual_root/src/lib/foo/bar/source.rb",
222                &b"require 'c:/artichoke/virtual_root/src/lib/foo/bar/source.rb'"[..],
223            )
224        } else {
225            (
226                "/artichoke/virtual_root/src/lib/foo/bar/source.rb",
227                &b"require '/artichoke/virtual_root/src/lib/foo/bar/source.rb'"[..],
228            )
229        };
230
231        interp.def_rb_source_file(path, &b"# a source file"[..]).unwrap();
232        let result = interp.eval(require_code).unwrap();
233        assert!(result.try_convert_into::<bool>(&interp).unwrap());
234        let result = interp.eval(require_code).unwrap();
235        assert!(!result.try_convert_into::<bool>(&interp).unwrap());
236    }
237
238    #[test]
239    fn relative_with_dotted_path() {
240        let mut interp = interpreter();
241        if cfg!(windows) {
242            interp
243                .def_rb_source_file(
244                    "c:/artichoke/virtual_root/src/lib/foo/bar/source.rb",
245                    &b"require_relative '../bar.rb'"[..],
246                )
247                .unwrap();
248            interp
249                .def_rb_source_file("c:/artichoke/virtual_root/src/lib/foo/bar.rb", &b"# a source file"[..])
250                .unwrap();
251            let result = interp
252                .eval(b"require 'c:/artichoke/virtual_root/src/lib/foo/bar/source.rb'")
253                .unwrap();
254            assert!(result.try_convert_into::<bool>(&interp).unwrap());
255            let result = interp
256                .eval(b"require 'c:/artichoke/virtual_root/src/lib/foo/bar.rb'")
257                .unwrap();
258            assert!(!result.try_convert_into::<bool>(&interp).unwrap());
259        } else {
260            interp
261                .def_rb_source_file(
262                    "/artichoke/virtual_root/src/lib/foo/bar/source.rb",
263                    &b"require_relative '../bar.rb'"[..],
264                )
265                .unwrap();
266            interp
267                .def_rb_source_file("/artichoke/virtual_root/src/lib/foo/bar.rb", &b"# a source file"[..])
268                .unwrap();
269            let result = interp
270                .eval(b"require '/artichoke/virtual_root/src/lib/foo/bar/source.rb'")
271                .unwrap();
272            assert!(result.try_convert_into::<bool>(&interp).unwrap());
273            let result = interp
274                .eval(b"require '/artichoke/virtual_root/src/lib/foo/bar.rb'")
275                .unwrap();
276            assert!(!result.try_convert_into::<bool>(&interp).unwrap());
277        };
278    }
279
280    #[test]
281    fn directory_err() {
282        let mut interp = interpreter();
283        let err = interp.eval(b"require '/src'").unwrap_err();
284        assert_eq!(
285            b"cannot load such file -- /src".as_bstr(),
286            err.message().as_ref().as_bstr()
287        );
288        let expected_backtrace = b"(eval):1:in require\n(eval):1".to_vec();
289        let actual_backtrace = bstr::join("\n", err.vm_backtrace(&mut interp).unwrap());
290        assert_eq!(expected_backtrace.as_bstr(), actual_backtrace.as_bstr());
291    }
292
293    #[test]
294    fn path_defined_as_source_then_extension_file() {
295        let mut interp = interpreter();
296        interp
297            .def_rb_source_file("foo.rb", &b"module Foo; RUBY = 3; end"[..])
298            .unwrap();
299        interp
300            .def_file_for_type::<_, MockExtensionAndSourceFile>("foo.rb")
301            .unwrap();
302        let result = interp.eval(b"require 'foo'").unwrap();
303        let result = result.try_convert_into::<bool>(&interp).unwrap();
304        assert!(result, "successfully required foo.rb");
305        let result = interp.eval(b"Foo::RUBY + Foo::RUST").unwrap();
306        let result = result.try_convert_into::<i64>(&interp).unwrap();
307        assert_eq!(result, 10, "defined Ruby and Rust sources from single require");
308    }
309
310    #[test]
311    fn path_defined_as_extension_file_then_source() {
312        let mut interp = interpreter();
313        interp
314            .def_file_for_type::<_, MockExtensionAndSourceFile>("foo.rb")
315            .unwrap();
316        interp
317            .def_rb_source_file("foo.rb", &b"module Foo; RUBY = 3; end"[..])
318            .unwrap();
319        let result = interp.eval(b"require 'foo'").unwrap();
320        let result = result.try_convert_into::<bool>(&interp).unwrap();
321        assert!(result, "successfully required foo.rb");
322        let result = interp.eval(b"Foo::RUBY + Foo::RUST").unwrap();
323        let result = result.try_convert_into::<i64>(&interp).unwrap();
324        assert_eq!(result, 10, "defined Ruby and Rust sources from single require");
325    }
326}