artichoke_backend/load_path/
hybrid.rs

1use std::borrow::Cow;
2use std::io;
3use std::path::Path;
4
5#[cfg(feature = "load-path-rubylib-native-file-system-loader")]
6use artichoke_load_path::Rubylib;
7use scolapasta_path::{is_explicit_relative, os_string_to_bytes};
8
9use super::{ExtensionHook, Memory, Native};
10
11#[derive(Debug)]
12pub struct Hybrid {
13    #[cfg(feature = "load-path-rubylib-native-file-system-loader")]
14    rubylib: Option<Rubylib>,
15    #[cfg(not(feature = "load-path-rubylib-native-file-system-loader"))]
16    rubylib: Option<Native>, // hard-coded to `None`
17    memory: Memory,
18    native: Native,
19}
20
21impl Default for Hybrid {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl Hybrid {
28    /// Create a new hybrid virtual file system.
29    ///
30    /// This file system allows access to the host file system with an in-memory
31    /// file system mounted at [`RUBY_LOAD_PATH`].
32    ///
33    /// [`RUBY_LOAD_PATH`]: super::RUBY_LOAD_PATH
34    #[must_use]
35    pub fn new() -> Self {
36        #[cfg(feature = "load-path-rubylib-native-file-system-loader")]
37        let rubylib = Rubylib::new();
38        #[cfg(not(feature = "load-path-rubylib-native-file-system-loader"))]
39        let rubylib = None;
40        let memory = Memory::new();
41        let native = Native::new();
42        Self {
43            rubylib,
44            memory,
45            native,
46        }
47    }
48
49    /// Check whether `path` points to a file in the virtual file system and
50    /// return the absolute path if it exists.
51    ///
52    /// This API is infallible and will return [`None`] for non-existent paths.
53    #[must_use]
54    pub fn resolve_file(&self, path: &Path) -> Option<Vec<u8>> {
55        if is_explicit_relative(path) {
56            return self.memory.resolve_file(path).or_else(|| {
57                self.native
58                    .resolve_file(path)
59                    .and_then(|path| os_string_to_bytes(path.into()).ok())
60            });
61        }
62        if let Some(ref rubylib) = self.rubylib {
63            rubylib
64                .resolve_file(path)
65                .and_then(|path| os_string_to_bytes(path.into()).ok())
66                .or_else(|| {
67                    self.memory.resolve_file(path).or_else(|| {
68                        self.native
69                            .resolve_file(path)
70                            .and_then(|path| os_string_to_bytes(path.into()).ok())
71                    })
72                })
73        } else {
74            self.memory.resolve_file(path).or_else(|| {
75                self.native
76                    .resolve_file(path)
77                    .and_then(|path| os_string_to_bytes(path.into()).ok())
78            })
79        }
80    }
81
82    /// Check whether `path` points to a file in the virtual file system.
83    ///
84    /// This API is infallible and will return `false` for non-existent paths.
85    #[must_use]
86    pub fn is_file(&self, path: &Path) -> bool {
87        if is_explicit_relative(path) {
88            return self.memory.is_file(path) || self.native.is_file(path);
89        }
90        if let Some(ref rubylib) = self.rubylib {
91            if rubylib.is_file(path) {
92                return true;
93            }
94        }
95        self.memory.is_file(path) || self.native.is_file(path)
96    }
97
98    /// Read file contents for the file at `path`.
99    ///
100    /// Returns a byte slice of complete file contents. If `path` is relative,
101    /// it is absolutized relative to the current working directory of the
102    /// virtual file system.
103    ///
104    /// # Errors
105    ///
106    /// If `path` does not exist, an [`io::Error`] with error kind
107    /// [`io::ErrorKind::NotFound`] is returned.
108    pub fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
109        if is_explicit_relative(path) {
110            return self.memory.read_file(path).or_else(|_| self.native.read_file(path));
111        }
112        if let Some(ref rubylib) = self.rubylib {
113            rubylib
114                .read_file(path)
115                .or_else(|_| self.memory.read_file(path).or_else(|_| self.native.read_file(path)))
116        } else {
117            self.memory.read_file(path).or_else(|_| self.native.read_file(path))
118        }
119    }
120
121    /// Write file contents into the virtual file system at `path`.
122    ///
123    /// Writes the full file contents. If any file contents already exist at
124    /// `path`, they are replaced. Extension hooks are preserved.
125    ///
126    /// Only the [`Memory`] file system at [`RUBY_LOAD_PATH`] is writable.
127    ///
128    /// # Errors
129    ///
130    /// If access to the [`Memory`] file system returns an error, the error is
131    /// returned. See [`Memory::write_file`].
132    ///
133    /// [`RUBY_LOAD_PATH`]: super::RUBY_LOAD_PATH
134    pub fn write_file(&mut self, path: &Path, buf: Cow<'static, [u8]>) -> io::Result<()> {
135        self.memory.write_file(path, buf)
136    }
137
138    /// Retrieve an extension hook for the file at `path`.
139    ///
140    /// This API is infallible and will return `None` for non-existent paths.
141    #[must_use]
142    pub fn get_extension(&self, path: &Path) -> Option<ExtensionHook> {
143        self.memory.get_extension(path)
144    }
145
146    /// Write extension hook into the virtual file system at `path`.
147    ///
148    /// If any extension hooks already exist at `path`, they are replaced. File
149    /// contents are preserved.
150    ///
151    /// This function writes all extensions to the virtual file system. If the
152    /// given path does not map to the virtual file system, the extension is
153    /// unreachable.
154    ///
155    /// # Errors
156    ///
157    /// If the given path does not resolve to the virtual file system, an error
158    /// is returned.
159    pub fn register_extension(&mut self, path: &Path, extension: ExtensionHook) -> io::Result<()> {
160        self.memory.register_extension(path, extension)
161    }
162
163    /// Check whether a file at `path` has been required already.
164    ///
165    /// This API is infallible and will return `false` for non-existent paths.
166    #[must_use]
167    pub fn is_required(&self, path: &Path) -> Option<bool> {
168        if is_explicit_relative(path) {
169            if let Some(required) = self.memory.is_required(path) {
170                return Some(required);
171            }
172            return self.native.is_required(path);
173        }
174        if let Some(ref rubylib) = self.rubylib {
175            if let Some(required) = rubylib.is_required(path) {
176                return Some(required);
177            }
178        }
179        if let Some(required) = self.memory.is_required(path) {
180            Some(required)
181        } else {
182            self.native.is_required(path)
183        }
184    }
185
186    /// Mark a source at `path` as required on the interpreter.
187    ///
188    /// This metadata is used by `Kernel#require` and friends to enforce that
189    /// Ruby sources are only loaded into the interpreter once to limit side
190    /// effects.
191    ///
192    /// # Errors
193    ///
194    /// If `path` does not exist, an [`io::Error`] with error kind
195    /// [`io::ErrorKind::NotFound`] is returned.
196    pub fn mark_required(&mut self, path: &Path) -> io::Result<()> {
197        if is_explicit_relative(path) {
198            return self
199                .memory
200                .mark_required(path)
201                .or_else(|_| self.native.mark_required(path));
202        }
203        if let Some(ref mut rubylib) = self.rubylib {
204            rubylib.mark_required(path).or_else(|_| {
205                self.memory
206                    .mark_required(path)
207                    .or_else(|_| self.native.mark_required(path))
208            })
209        } else {
210            self.memory
211                .mark_required(path)
212                .or_else(|_| self.native.mark_required(path))
213        }
214    }
215}