Skip to main content

ruff_db/
system.rs

1pub use command::{Command, CommandExecutor};
2pub use memory_fs::MemoryFileSystem;
3
4#[cfg(all(feature = "testing", feature = "os"))]
5pub use os::testing::UserConfigDirectoryOverrideGuard;
6
7#[cfg(feature = "os")]
8pub use os::OsSystem;
9
10use filetime::FileTime;
11use ruff_notebook::{Notebook, NotebookError};
12use ruff_python_ast::PySourceType;
13use std::error::Error;
14use std::fmt;
15use std::fmt::Debug;
16use std::process::Output;
17pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
18use walk_directory::WalkDirectoryBuilder;
19
20pub use self::path::{
21    DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
22    SystemVirtualPathBuf, deduplicate_nested_paths,
23};
24use crate::file_revision::FileRevision;
25
26mod command;
27mod memory_fs;
28#[cfg(feature = "os")]
29mod os;
30mod path;
31mod test;
32pub mod walk_directory;
33
34pub type Result<T> = std::io::Result<T>;
35pub type WhichResult = std::result::Result<SystemPathBuf, WhichError>;
36
37/// The system on which Ruff runs.
38///
39/// Ruff supports running on the CLI, in a language server, and in a browser (WASM). Each of these
40/// host-systems differ in what system operations they support and how they interact with the file system:
41/// * Language server:
42///    * Reading a file's content should take into account that it might have unsaved changes because it's open in the editor.
43///    * Use structured representations for notebooks, making deserializing a notebook from a string unnecessary.
44///    * Use their own file watching infrastructure.
45/// * WASM (Browser):
46///    * There are ways to emulate a file system in WASM but a native memory-filesystem is more efficient.
47///    * Doesn't support a current working directory
48///    * File watching isn't supported.
49///
50/// Abstracting the system also enables tests to use a more efficient in-memory file system.
51pub trait System: Debug + Sync + Send {
52    /// Reads the metadata of the file or directory at `path`.
53    ///
54    /// This function will traverse symbolic links to query information about the destination file.
55    fn path_metadata(&self, path: &SystemPath) -> Result<Metadata>;
56
57    /// Returns the canonical, absolute form of a path with all intermediate components normalized
58    /// and symbolic links resolved.
59    ///
60    /// # Errors
61    /// This function will return an error in the following situations, but is not limited to just these cases:
62    /// * `path` does not exist.
63    /// * A non-final component in `path` is not a directory.
64    /// * the symlink target path is not valid Unicode.
65    ///
66    /// ## Windows long-paths
67    /// Unlike `std::fs::canonicalize`, this function does remove UNC prefixes if possible.
68    /// See [dunce::canonicalize] for more information.
69    fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
70
71    /// Returns `true` if both paths refer to the same file.
72    fn is_same_file(&self, first: &SystemPath, second: &SystemPath) -> Result<bool>;
73
74    /// Returns the source type for `path` if known or `None`.
75    ///
76    /// The default is to always return `None`, assuming the system
77    /// has no additional information and that the caller should
78    /// rely on the file extension instead.
79    ///
80    /// This is primarily used for the LSP integration to respect
81    /// the chosen language (or the fact that it is a notebook) in
82    /// the editor.
83    fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
84        let _ = path;
85        None
86    }
87
88    /// Returns the source type for `path` if known or `None`.
89    ///
90    /// The default is to always return `None`, assuming the system
91    /// has no additional information and that the caller should
92    /// rely on the file extension instead.
93    ///
94    /// This is primarily used for the LSP integration to respect
95    /// the chosen language (or the fact that it is a notebook) in
96    /// the editor.
97    fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
98        let _ = path;
99
100        None
101    }
102
103    /// Find an executable binary's path by name.
104    fn which(&self, binary_name: &str) -> WhichResult;
105
106    /// Runs `command` and captures its standard output and standard error.
107    fn run_command(&self, command: Command) -> Result<Output> {
108        let Some(executor) = self.command_executor() else {
109            return Err(std::io::Error::new(
110                std::io::ErrorKind::Unsupported,
111                "running commands is not supported by this system",
112            ));
113        };
114
115        executor.execute(command)
116    }
117
118    /// Returns the system's command executor, if it supports running commands.
119    fn command_executor(&self) -> Option<&dyn CommandExecutor> {
120        None
121    }
122
123    /// Reads the content of the file at `path` into a [`String`].
124    fn read_to_string(&self, path: &SystemPath) -> Result<String>;
125
126    /// Reads the content of the file at `path` as a Notebook.
127    ///
128    /// This method optimizes for the case where the system holds a structured representation of a [`Notebook`],
129    /// allowing to skip the notebook deserialization. Systems that don't use a structured
130    /// representation fall-back to deserializing the notebook from a string.
131    fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError>;
132
133    /// Reads the content of the virtual file at `path` into a [`String`].
134    fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String>;
135
136    /// Reads the content of the virtual file at `path` as a [`Notebook`].
137    fn read_virtual_path_to_notebook(
138        &self,
139        path: &SystemVirtualPath,
140    ) -> std::result::Result<Notebook, NotebookError>;
141
142    /// Returns `true` if `path` exists.
143    fn path_exists(&self, path: &SystemPath) -> bool {
144        self.path_metadata(path).is_ok()
145    }
146
147    /// Returns `true` if `path` exists and is a directory.
148    fn is_directory(&self, path: &SystemPath) -> bool {
149        self.path_metadata(path)
150            .is_ok_and(|metadata| metadata.file_type.is_directory())
151    }
152
153    /// Returns `true` if `path` exists and is a file.
154    fn is_file(&self, path: &SystemPath) -> bool {
155        self.path_metadata(path)
156            .is_ok_and(|metadata| metadata.file_type.is_file())
157    }
158
159    /// Returns the current working directory
160    fn current_directory(&self) -> &SystemPath;
161
162    /// Returns the directory path where user configurations are stored.
163    ///
164    /// Returns `None` if no such convention exists for the system.
165    fn user_config_directory(&self) -> Option<SystemPathBuf>;
166
167    /// Returns the directory path where cached files are stored.
168    ///
169    /// Returns `None` if no such convention exists for the system.
170    fn cache_dir(&self) -> Option<SystemPathBuf>;
171
172    /// Iterate over the contents of the directory at `path`.
173    ///
174    /// The returned iterator must have the following properties:
175    /// - It only iterates over the top level of the directory,
176    ///   i.e., it does not recurse into subdirectories.
177    /// - It skips the current and parent directories (`.` and `..`
178    ///   respectively).
179    /// - The iterator yields `std::io::Result<DirEntry>` instances.
180    ///   For each instance, an `Err` variant may signify that the path
181    ///   of the entry was not valid UTF8, in which case it should be an
182    ///   [`std::io::Error`] with the ErrorKind set to
183    ///   [`std::io::ErrorKind::InvalidData`] and the payload set to a
184    ///   [`camino::FromPathBufError`]. It may also indicate that
185    ///   "some sort of intermittent IO error occurred during iteration"
186    ///   (language taken from the [`std::fs::read_dir`] documentation).
187    ///
188    /// # Errors
189    /// Returns an error:
190    /// - if `path` does not exist in the system,
191    /// - if `path` does not point to a directory,
192    /// - if the process does not have sufficient permissions to
193    ///   view the contents of the directory at `path`
194    /// - May also return an error in some other situations as well.
195    fn read_directory<'a>(
196        &'a self,
197        path: &SystemPath,
198    ) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>>;
199
200    /// Recursively walks the content of `path`.
201    ///
202    /// It is allowed to pass a `path` that points to a file. In this case, the walker
203    /// yields a single entry for that file.
204    fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder;
205
206    /// Fetches the environment variable `key` from the current process.
207    ///
208    /// # Errors
209    ///
210    /// Returns [`std::env::VarError::NotPresent`] if:
211    /// - The variable is not set.
212    /// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`).
213    ///
214    /// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid
215    /// Unicode.
216    fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
217        let _ = name;
218        Err(std::env::VarError::NotPresent)
219    }
220
221    /// Returns a handle to a [`WritableSystem`] if this system is writable.
222    fn as_writable(&self) -> Option<&dyn WritableSystem>;
223
224    fn as_any(&self) -> &dyn std::any::Any;
225
226    fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
227
228    fn dyn_clone(&self) -> Box<dyn System>;
229}
230
231/// System trait for non-readonly systems.
232pub trait WritableSystem: System {
233    /// Creates a file at the given path.
234    ///
235    /// Returns an error if the file already exists.
236    fn create_new_file(&self, path: &SystemPath) -> Result<()>;
237
238    /// Writes the given content to the file at the given path.
239    fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
240        self.write_file_bytes(path, content.as_bytes())
241    }
242
243    /// Writes the given content to the file at the given path.
244    fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()>;
245
246    /// Creates a directory at `path` as well as any intermediate directories.
247    fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
248
249    /// Reads the provided file from the system cache, or creates the file if necessary.
250    ///
251    /// Returns `Ok(None)` if the system does not expose a suitable cache directory.
252    fn get_or_cache(
253        &self,
254        path: &SystemPath,
255        read_contents: &dyn Fn() -> Result<String>,
256    ) -> Result<Option<SystemPathBuf>> {
257        let Some(cache_dir) = self.cache_dir() else {
258            return Ok(None);
259        };
260
261        let cache_path = cache_dir.join(path);
262
263        // The file has already been cached.
264        if self.is_file(&cache_path) {
265            return Ok(Some(cache_path));
266        }
267
268        // Read the file contents.
269        let contents = read_contents()?;
270
271        // Create the parent directory.
272        self.create_directory_all(cache_path.parent().unwrap())?;
273
274        // Create and write to the file on the system.
275        //
276        // Note that `create_new_file` will fail if the file has already been created. This
277        // ensures that only one thread/process ever attempts to write to it to avoid corrupting
278        // the cache.
279        self.create_new_file(&cache_path)?;
280        self.write_file(&cache_path, &contents)?;
281
282        Ok(Some(cache_path))
283    }
284
285    fn dyn_clone(&self) -> Box<dyn WritableSystem>;
286}
287
288#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct Metadata {
290    revision: FileRevision,
291    permissions: Option<u32>,
292    file_type: FileType,
293}
294
295impl Metadata {
296    pub fn new(revision: FileRevision, permissions: Option<u32>, file_type: FileType) -> Self {
297        Self {
298            revision,
299            permissions,
300            file_type,
301        }
302    }
303
304    pub fn revision(&self) -> FileRevision {
305        self.revision
306    }
307
308    pub fn permissions(&self) -> Option<u32> {
309        self.permissions
310    }
311
312    pub fn file_type(&self) -> FileType {
313        self.file_type
314    }
315}
316
317#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, get_size2::GetSize)]
318pub enum FileType {
319    File,
320    Directory,
321    Symlink,
322}
323
324impl FileType {
325    pub const fn is_file(self) -> bool {
326        matches!(self, FileType::File)
327    }
328
329    pub const fn is_directory(self) -> bool {
330        matches!(self, FileType::Directory)
331    }
332
333    pub const fn is_symlink(self) -> bool {
334        matches!(self, FileType::Symlink)
335    }
336}
337
338#[derive(Debug, PartialEq, Eq)]
339pub struct DirectoryEntry {
340    path: SystemPathBuf,
341    file_type: FileType,
342}
343
344impl DirectoryEntry {
345    pub fn new(path: SystemPathBuf, file_type: FileType) -> Self {
346        Self { path, file_type }
347    }
348
349    pub fn into_path(self) -> SystemPathBuf {
350        self.path
351    }
352
353    pub fn path(&self) -> &SystemPath {
354        &self.path
355    }
356
357    pub fn file_type(&self) -> FileType {
358        self.file_type
359    }
360}
361
362#[cfg(not(target_arch = "wasm32"))]
363pub fn file_time_now() -> FileTime {
364    FileTime::now()
365}
366
367#[cfg(target_arch = "wasm32")]
368pub fn file_time_now() -> FileTime {
369    // Copied from FileTime::from_system_time()
370    let time = web_time::SystemTime::now();
371
372    time.duration_since(web_time::UNIX_EPOCH)
373        .map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
374        .unwrap_or_else(|e| {
375            let until_epoch = e.duration();
376            let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
377                (0, 0)
378            } else {
379                (-1, 1_000_000_000 - until_epoch.subsec_nanos())
380            };
381
382            FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
383        })
384}
385
386#[derive(Copy, Clone, Eq, PartialEq, Debug)]
387pub enum WhichError {
388    /// An executable binary with that name was not found
389    CannotFindBinaryPath,
390
391    /// There was nowhere to search and the provided name wasn't an absolute path
392    CannotGetCurrentDirAndPathListEmpty,
393
394    /// Failed to canonicalize the path found
395    CannotCanonicalize,
396
397    /// The executable exists but its path contains non UTF8 characters.
398    NonUtf8Path,
399}
400
401impl Error for WhichError {}
402
403impl fmt::Display for WhichError {
404    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        match self {
406            WhichError::CannotFindBinaryPath => write!(f, "cannot find binary path"),
407            WhichError::CannotGetCurrentDirAndPathListEmpty => write!(
408                f,
409                "no path to search and provided name is not an absolute path"
410            ),
411            WhichError::CannotCanonicalize => write!(f, "cannot canonicalize path"),
412            WhichError::NonUtf8Path => write!(f, "non UTF-8 path"),
413        }
414    }
415}