artichoke_backend/load_path/hybrid.rs
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
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 scolapasta_path::{is_explicit_relative, os_string_to_bytes};
use super::{ExtensionHook, Memory, Native};
#[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))
}
}
}