logo
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
use std::borrow::Cow;
use std::io;
use std::path::Path;

#[cfg(feature = "load-path-rubylib-native-file-system-loader")]
use artichoke_load_path::Rubylib;
use mezzaluna_feature_loader::paths::is_explicit_relative;

use super::{ExtensionHook, Memory, Native};
use crate::platform_string::os_string_to_bytes;

#[derive(Debug)]
pub struct Hybrid {
    #[cfg(feature = "load-path-rubylib-native-file-system-loader")]
    rubylib: Option<Rubylib>,
    #[cfg(not(feature = "load-path-rubylib-native-file-system-loader"))]
    rubylib: Option<Native>, // hard-coded to `None`
    memory: Memory,
    native: Native,
}

impl Default for Hybrid {
    fn default() -> Self {
        Self::new()
    }
}

impl Hybrid {
    /// Create a new hybrid virtual file system.
    ///
    /// This file system allows access to the host file system with an in-memory
    /// file system mounted at [`RUBY_LOAD_PATH`].
    ///
    /// [`RUBY_LOAD_PATH`]: super::RUBY_LOAD_PATH
    #[must_use]
    pub fn new() -> Self {
        #[cfg(feature = "load-path-rubylib-native-file-system-loader")]
        let rubylib = Rubylib::new();
        #[cfg(not(feature = "load-path-rubylib-native-file-system-loader"))]
        let rubylib = None;
        let memory = Memory::new();
        let native = Native::new();
        Self {
            rubylib,
            memory,
            native,
        }
    }

    /// Check whether `path` points to a file in the virtual file system and
    /// return the absolute path if it exists.
    ///
    /// This API is infallible and will return [`None`] for non-existent paths.
    #[must_use]
    pub fn resolve_file(&self, path: &Path) -> Option<Vec<u8>> {
        if is_explicit_relative(path) {
            return self.memory.resolve_file(path).or_else(|| {
                self.native
                    .resolve_file(path)
                    .and_then(|path| os_string_to_bytes(path.into()).ok())
            });
        }
        if let Some(ref rubylib) = self.rubylib {
            rubylib
                .resolve_file(path)
                .and_then(|path| os_string_to_bytes(path.into()).ok())
                .or_else(|| {
                    self.memory.resolve_file(path).or_else(|| {
                        self.native
                            .resolve_file(path)
                            .and_then(|path| os_string_to_bytes(path.into()).ok())
                    })
                })
        } else {
            self.memory.resolve_file(path).or_else(|| {
                self.native
                    .resolve_file(path)
                    .and_then(|path| os_string_to_bytes(path.into()).ok())
            })
        }
    }

    /// Check whether `path` points to a file in the virtual file system.
    ///
    /// This API is infallible and will return `false` for non-existent paths.
    #[must_use]
    pub fn is_file(&self, path: &Path) -> bool {
        if is_explicit_relative(path) {
            return self.memory.is_file(path) || self.native.is_file(path);
        }
        if let Some(ref rubylib) = self.rubylib {
            if rubylib.is_file(path) {
                return true;
            }
        }
        self.memory.is_file(path) || self.native.is_file(path)
    }

    /// Read file contents for the file at `path`.
    ///
    /// Returns a byte slice of complete file contents. If `path` is relative,
    /// it is absolutized relative to the current working directory of the
    /// virtual file system.
    ///
    /// # Errors
    ///
    /// If `path` does not exist, an [`io::Error`] with error kind
    /// [`io::ErrorKind::NotFound`] is returned.
    pub fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
        if is_explicit_relative(path) {
            return self.memory.read_file(path).or_else(|_| self.native.read_file(path));
        }
        if let Some(ref rubylib) = self.rubylib {
            rubylib
                .read_file(path)
                .or_else(|_| self.memory.read_file(path).or_else(|_| self.native.read_file(path)))
        } else {
            self.memory.read_file(path).or_else(|_| self.native.read_file(path))
        }
    }

    /// Write file contents into the virtual file system at `path`.
    ///
    /// Writes the full file contents. If any file contents already exist at
    /// `path`, they are replaced. Extension hooks are preserved.
    ///
    /// Only the [`Memory`] file system at [`RUBY_LOAD_PATH`] is writable.
    ///
    /// # Errors
    ///
    /// If access to the [`Memory`] file system returns an error, the error is
    /// returned. See [`Memory::write_file`].
    ///
    /// [`RUBY_LOAD_PATH`]: super::RUBY_LOAD_PATH
    pub fn write_file(&mut self, path: &Path, buf: Cow<'static, [u8]>) -> io::Result<()> {
        self.memory.write_file(path, buf)
    }

    /// Retrieve an extension hook for the file at `path`.
    ///
    /// This API is infallible and will return `None` for non-existent paths.
    #[must_use]
    pub fn get_extension(&self, path: &Path) -> Option<ExtensionHook> {
        self.memory.get_extension(path)
    }

    /// Write extension hook into the virtual file system at `path`.
    ///
    /// If any extension hooks already exist at `path`, they are replaced. File
    /// contents are preserved.
    ///
    /// This function writes all extensions to the virtual file system. If the
    /// given path does not map to the virtual file system, the extension is
    /// unreachable.
    ///
    /// # Errors
    ///
    /// If the given path does not resolve to the virtual file system, an error
    /// is returned.
    pub fn register_extension(&mut self, path: &Path, extension: ExtensionHook) -> io::Result<()> {
        self.memory.register_extension(path, extension)
    }

    /// Check whether a file at `path` has been required already.
    ///
    /// This API is infallible and will return `false` for non-existent paths.
    #[must_use]
    pub fn is_required(&self, path: &Path) -> Option<bool> {
        if is_explicit_relative(path) {
            if let Some(required) = self.memory.is_required(path) {
                return Some(required);
            }
            return self.native.is_required(path);
        }
        if let Some(ref rubylib) = self.rubylib {
            if let Some(required) = rubylib.is_required(path) {
                return Some(required);
            }
        }
        if let Some(required) = self.memory.is_required(path) {
            Some(required)
        } else {
            self.native.is_required(path)
        }
    }

    /// Mark a source at `path` as required on the interpreter.
    ///
    /// This metadata is used by `Kernel#require` and friends to enforce that
    /// Ruby sources are only loaded into the interpreter once to limit side
    /// effects.
    ///
    /// # Errors
    ///
    /// If `path` does not exist, an [`io::Error`] with error kind
    /// [`io::ErrorKind::NotFound`] is returned.
    pub fn mark_required(&mut self, path: &Path) -> io::Result<()> {
        if is_explicit_relative(path) {
            return self
                .memory
                .mark_required(path)
                .or_else(|_| self.native.mark_required(path));
        }
        if let Some(ref mut rubylib) = self.rubylib {
            rubylib.mark_required(path).or_else(|_| {
                self.memory
                    .mark_required(path)
                    .or_else(|_| self.native.mark_required(path))
            })
        } else {
            self.memory
                .mark_required(path)
                .or_else(|_| self.native.mark_required(path))
        }
    }
}