artichoke_backend/load_path/
native.rs

1use std::collections::HashSet;
2use std::env;
3use std::fs::{self, File};
4use std::io;
5use std::path::{Path, PathBuf};
6
7use bstr::{BString, ByteSlice};
8use scolapasta_path::{ConvertBytesError, absolutize_relative_to, normalize_slashes};
9
10#[derive(Default, Debug, PartialEq, Eq)]
11pub struct Native {
12    loaded_features: HashSet<BString>,
13}
14
15impl Native {
16    /// Create a new native virtual file system.
17    ///
18    /// This file system grants access to the host file system.
19    #[must_use]
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Check whether `path` points to a file in the virtual file system and
25    /// return the absolute path if it exists.
26    ///
27    /// This API is infallible and will return [`None`] for non-existent paths.
28    #[must_use]
29    pub fn resolve_file(&self, path: &Path) -> Option<PathBuf> {
30        if File::open(path).is_ok() {
31            Some(path.to_owned())
32        } else {
33            None
34        }
35    }
36
37    /// Check whether `path` points to a file in the virtual file system.
38    ///
39    /// This API is infallible and will return `false` for non-existent paths.
40    #[must_use]
41    pub fn is_file(&self, path: &Path) -> bool {
42        if let Ok(metadata) = fs::metadata(path) {
43            !metadata.is_dir()
44        } else {
45            false
46        }
47    }
48
49    /// Read file contents for the file at `path`.
50    ///
51    /// Returns a byte slice of complete file contents. If `path` is relative,
52    /// it is absolutized relative to the current working directory of the
53    /// virtual file system.
54    ///
55    /// # Errors
56    ///
57    /// If `path` does not exist, an [`io::Error`] with error kind
58    /// [`io::ErrorKind::NotFound`] is returned.
59    pub fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
60        fs::read(path)
61    }
62
63    /// Check whether a file at `path` has been required already.
64    ///
65    /// This API is infallible and will return `false` for non-existent paths.
66    #[must_use]
67    pub fn is_required(&self, path: &Path) -> Option<bool> {
68        let path = if let Ok(cwd) = env::current_dir() {
69            absolutize_relative_to(path, cwd)
70        } else {
71            return None;
72        };
73        if let Ok(path) = normalize_slashes(path) {
74            Some(self.loaded_features.contains(path.as_bstr()))
75        } else {
76            None
77        }
78    }
79
80    /// Mark a source at `path` as required on the interpreter.
81    ///
82    /// This metadata is used by `Kernel#require` and friends to enforce that
83    /// Ruby sources are only loaded into the interpreter once to limit side
84    /// effects.
85    ///
86    /// # Errors
87    ///
88    /// If `path` does not exist, an [`io::Error`] with error kind
89    /// [`io::ErrorKind::NotFound`] is returned.
90    pub fn mark_required(&mut self, path: &Path) -> io::Result<()> {
91        let cwd = env::current_dir()?;
92        let path = absolutize_relative_to(path, cwd);
93        let path =
94            normalize_slashes(path).map_err(|_| io::Error::new(io::ErrorKind::NotFound, ConvertBytesError::new()))?;
95        self.loaded_features.insert(path.into());
96        Ok(())
97    }
98}