mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:47 +00:00
1218 lines
36 KiB
Rust
1218 lines
36 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::iter::FusedIterator;
|
|
use std::sync::{Arc, RwLock, RwLockWriteGuard};
|
|
|
|
use camino::{Utf8Path, Utf8PathBuf};
|
|
use filetime::FileTime;
|
|
use rustc_hash::FxHashMap;
|
|
|
|
use ruff_notebook::{Notebook, NotebookError};
|
|
|
|
use crate::system::{
|
|
walk_directory, DirectoryEntry, FileType, GlobError, GlobErrorKind, Metadata, Result,
|
|
SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf,
|
|
};
|
|
|
|
use super::walk_directory::{
|
|
DirectoryWalker, ErrorKind, WalkDirectoryBuilder, WalkDirectoryConfiguration,
|
|
WalkDirectoryVisitor, WalkDirectoryVisitorBuilder, WalkState,
|
|
};
|
|
|
|
/// File system that stores all content in memory.
|
|
///
|
|
/// The file system supports files and directories. Paths are case-sensitive.
|
|
///
|
|
/// The implementation doesn't aim at fully capturing the behavior of a real file system.
|
|
/// The implementation intentionally doesn't support:
|
|
/// * symlinks
|
|
/// * hardlinks
|
|
/// * permissions: All files and directories have the permission 0755.
|
|
///
|
|
/// Use a tempdir with the real file system to test these advanced file system features and behavior.
|
|
#[derive(Clone)]
|
|
pub struct MemoryFileSystem {
|
|
inner: Arc<MemoryFileSystemInner>,
|
|
}
|
|
|
|
impl MemoryFileSystem {
|
|
/// Permission used by all files and directories
|
|
const PERMISSION: u32 = 0o755;
|
|
|
|
pub fn new() -> Self {
|
|
Self::with_current_directory("/")
|
|
}
|
|
|
|
/// Creates a new, empty in memory file system with the given current working directory.
|
|
pub fn with_current_directory(cwd: impl AsRef<SystemPath>) -> Self {
|
|
let cwd = cwd.as_ref().to_path_buf();
|
|
|
|
assert!(
|
|
cwd.starts_with("/"),
|
|
"The current working directory must be an absolute path."
|
|
);
|
|
|
|
let fs = Self {
|
|
inner: Arc::new(MemoryFileSystemInner {
|
|
by_path: RwLock::new(BTreeMap::default()),
|
|
virtual_files: RwLock::new(FxHashMap::default()),
|
|
cwd: cwd.clone(),
|
|
}),
|
|
};
|
|
|
|
fs.create_directory_all(&cwd).unwrap();
|
|
|
|
fs
|
|
}
|
|
|
|
pub fn current_directory(&self) -> &SystemPath {
|
|
&self.inner.cwd
|
|
}
|
|
|
|
pub fn metadata(&self, path: impl AsRef<SystemPath>) -> Result<Metadata> {
|
|
fn metadata(fs: &MemoryFileSystem, path: &SystemPath) -> Result<Metadata> {
|
|
let by_path = fs.inner.by_path.read().unwrap();
|
|
let normalized = fs.normalize_path(path);
|
|
|
|
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
|
|
|
let metadata = match entry {
|
|
Entry::File(file) => Metadata {
|
|
revision: file.last_modified.into(),
|
|
permissions: Some(MemoryFileSystem::PERMISSION),
|
|
file_type: FileType::File,
|
|
},
|
|
Entry::Directory(directory) => Metadata {
|
|
revision: directory.last_modified.into(),
|
|
permissions: Some(MemoryFileSystem::PERMISSION),
|
|
file_type: FileType::Directory,
|
|
},
|
|
};
|
|
|
|
Ok(metadata)
|
|
}
|
|
|
|
metadata(self, path.as_ref())
|
|
}
|
|
|
|
pub fn canonicalize(&self, path: impl AsRef<SystemPath>) -> Result<SystemPathBuf> {
|
|
let path = path.as_ref();
|
|
// Mimic the behavior of a real FS where canonicalize errors if the `path` doesn't exist
|
|
self.metadata(path)?;
|
|
Ok(SystemPathBuf::from_utf8_path_buf(self.normalize_path(path)))
|
|
}
|
|
|
|
pub fn is_file(&self, path: impl AsRef<SystemPath>) -> bool {
|
|
let by_path = self.inner.by_path.read().unwrap();
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
|
|
matches!(by_path.get(&normalized), Some(Entry::File(_)))
|
|
}
|
|
|
|
pub fn is_directory(&self, path: impl AsRef<SystemPath>) -> bool {
|
|
let by_path = self.inner.by_path.read().unwrap();
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
|
|
matches!(by_path.get(&normalized), Some(Entry::Directory(_)))
|
|
}
|
|
|
|
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
|
|
fn read_to_string(fs: &MemoryFileSystem, path: &SystemPath) -> Result<String> {
|
|
let by_path = fs.inner.by_path.read().unwrap();
|
|
let normalized = fs.normalize_path(path);
|
|
|
|
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
|
|
|
match entry {
|
|
Entry::File(file) => Ok(file.content.clone()),
|
|
Entry::Directory(_) => Err(is_a_directory()),
|
|
}
|
|
}
|
|
|
|
read_to_string(self, path.as_ref())
|
|
}
|
|
|
|
pub(crate) fn read_to_notebook(
|
|
&self,
|
|
path: impl AsRef<SystemPath>,
|
|
) -> std::result::Result<ruff_notebook::Notebook, ruff_notebook::NotebookError> {
|
|
let content = self.read_to_string(path)?;
|
|
ruff_notebook::Notebook::from_source_code(&content)
|
|
}
|
|
|
|
pub(crate) fn read_virtual_path_to_string(
|
|
&self,
|
|
path: impl AsRef<SystemVirtualPath>,
|
|
) -> Result<String> {
|
|
let virtual_files = self.inner.virtual_files.read().unwrap();
|
|
let file = virtual_files
|
|
.get(&path.as_ref().to_path_buf())
|
|
.ok_or_else(not_found)?;
|
|
|
|
Ok(file.content.clone())
|
|
}
|
|
|
|
pub(crate) fn read_virtual_path_to_notebook(
|
|
&self,
|
|
path: &SystemVirtualPath,
|
|
) -> std::result::Result<Notebook, NotebookError> {
|
|
let content = self.read_virtual_path_to_string(path)?;
|
|
ruff_notebook::Notebook::from_source_code(&content)
|
|
}
|
|
|
|
pub fn exists(&self, path: &SystemPath) -> bool {
|
|
let by_path = self.inner.by_path.read().unwrap();
|
|
let normalized = self.normalize_path(path);
|
|
|
|
by_path.contains_key(&normalized)
|
|
}
|
|
|
|
pub fn virtual_path_exists(&self, path: &SystemVirtualPath) -> bool {
|
|
let virtual_files = self.inner.virtual_files.read().unwrap();
|
|
virtual_files.contains_key(&path.to_path_buf())
|
|
}
|
|
|
|
/// Writes the files to the file system.
|
|
///
|
|
/// The operation overrides existing files with the same normalized path.
|
|
///
|
|
/// Enclosing directories are automatically created if they don't exist.
|
|
pub fn write_files<P, C>(&self, files: impl IntoIterator<Item = (P, C)>) -> Result<()>
|
|
where
|
|
P: AsRef<SystemPath>,
|
|
C: ToString,
|
|
{
|
|
for (path, content) in files {
|
|
self.write_file(path.as_ref(), content.to_string())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stores a new file in the file system.
|
|
///
|
|
/// The operation overrides the content for an existing file with the same normalized `path`.
|
|
///
|
|
/// Enclosing directories are automatically created if they don't exist.
|
|
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
|
|
let mut by_path = self.inner.by_path.write().unwrap();
|
|
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
|
|
let file = get_or_create_file(&mut by_path, &normalized)?;
|
|
file.content = content.to_string();
|
|
file.last_modified = now();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Stores a new virtual file in the file system.
|
|
///
|
|
/// The operation overrides the content for an existing virtual file with the same `path`.
|
|
pub fn write_virtual_file(&self, path: impl AsRef<SystemVirtualPath>, content: impl ToString) {
|
|
let path = path.as_ref();
|
|
let mut virtual_files = self.inner.virtual_files.write().unwrap();
|
|
|
|
match virtual_files.entry(path.to_path_buf()) {
|
|
std::collections::hash_map::Entry::Vacant(entry) => {
|
|
entry.insert(File {
|
|
content: content.to_string(),
|
|
last_modified: now(),
|
|
});
|
|
}
|
|
std::collections::hash_map::Entry::Occupied(mut entry) => {
|
|
entry.get_mut().content = content.to_string();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns a builder for walking the directory tree of `path`.
|
|
///
|
|
/// The only files that are ignored when setting `WalkDirectoryBuilder::standard_filters`
|
|
/// are hidden files (files with a name starting with a `.`).
|
|
pub fn walk_directory(&self, path: impl AsRef<SystemPath>) -> WalkDirectoryBuilder {
|
|
WalkDirectoryBuilder::new(path, MemoryWalker { fs: self.clone() })
|
|
}
|
|
|
|
pub fn glob(
|
|
&self,
|
|
pattern: &str,
|
|
) -> std::result::Result<
|
|
impl Iterator<Item = std::result::Result<SystemPathBuf, GlobError>>,
|
|
glob::PatternError,
|
|
> {
|
|
// Very naive implementation that iterates over all files and collects all that match the given pattern.
|
|
|
|
let normalized = self.normalize_path(pattern);
|
|
let pattern = glob::Pattern::new(normalized.as_str())?;
|
|
let matches = std::sync::Mutex::new(Vec::new());
|
|
|
|
self.walk_directory("/").standard_filters(false).run(|| {
|
|
Box::new(|entry| {
|
|
match entry {
|
|
Ok(entry) => {
|
|
if pattern.matches_path(entry.path().as_std_path()) {
|
|
matches.lock().unwrap().push(Ok(entry.into_path()));
|
|
}
|
|
}
|
|
Err(error) => match error.kind {
|
|
ErrorKind::Loop { .. } => {
|
|
unreachable!("Loops aren't possible in the memory file system because it doesn't support symlinks.")
|
|
}
|
|
ErrorKind::Io { err, path } => {
|
|
matches.lock().unwrap().push(Err(GlobError { path: path.expect("walk_directory to always set a path").into_std_path_buf(), error: GlobErrorKind::IOError(err)}));
|
|
}
|
|
ErrorKind::NonUtf8Path { path } => {
|
|
matches.lock().unwrap().push(Err(GlobError { path, error: GlobErrorKind::NonUtf8Path}));
|
|
}
|
|
},
|
|
}
|
|
WalkState::Continue
|
|
})
|
|
});
|
|
|
|
Ok(matches.into_inner().unwrap().into_iter())
|
|
}
|
|
|
|
pub fn remove_file(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
|
fn remove_file(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
|
|
let mut by_path = fs.inner.by_path.write().unwrap();
|
|
let normalized = fs.normalize_path(path);
|
|
|
|
match by_path.entry(normalized) {
|
|
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
|
Entry::File(_) => {
|
|
entry.remove();
|
|
Ok(())
|
|
}
|
|
Entry::Directory(_) => Err(is_a_directory()),
|
|
},
|
|
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
|
}
|
|
}
|
|
|
|
remove_file(self, path.as_ref())
|
|
}
|
|
|
|
pub fn remove_virtual_file(&self, path: impl AsRef<SystemVirtualPath>) -> Result<()> {
|
|
let mut virtual_files = self.inner.virtual_files.write().unwrap();
|
|
match virtual_files.entry(path.as_ref().to_path_buf()) {
|
|
std::collections::hash_map::Entry::Occupied(entry) => {
|
|
entry.remove();
|
|
Ok(())
|
|
}
|
|
std::collections::hash_map::Entry::Vacant(_) => Err(not_found()),
|
|
}
|
|
}
|
|
|
|
/// Sets the last modified timestamp of the file stored at `path` to now.
|
|
///
|
|
/// Creates a new file if the file at `path` doesn't exist.
|
|
pub fn touch(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
|
let mut by_path = self.inner.by_path.write().unwrap();
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
|
|
get_or_create_file(&mut by_path, &normalized)?.last_modified = now();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Creates a directory at `path`. All enclosing directories are created if they don't exist.
|
|
pub fn create_directory_all(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
|
let mut by_path = self.inner.by_path.write().unwrap();
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
|
|
create_dir_all(&mut by_path, &normalized)
|
|
}
|
|
|
|
/// Deletes the directory at `path`.
|
|
///
|
|
/// ## Errors
|
|
/// * If the directory is not empty
|
|
/// * The `path` is not a directory
|
|
/// * The `path` does not exist
|
|
pub fn remove_directory(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
|
fn remove_directory(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
|
|
let mut by_path = fs.inner.by_path.write().unwrap();
|
|
let normalized = fs.normalize_path(path);
|
|
|
|
// Test if the directory is empty
|
|
// Skip the directory path itself
|
|
for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) {
|
|
if maybe_child.starts_with(&normalized) {
|
|
return Err(directory_not_empty());
|
|
} else if !maybe_child.as_str().starts_with(normalized.as_str()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
match by_path.entry(normalized.clone()) {
|
|
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
|
Entry::Directory(_) => {
|
|
entry.remove();
|
|
Ok(())
|
|
}
|
|
Entry::File(_) => Err(not_a_directory()),
|
|
},
|
|
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
|
}
|
|
}
|
|
|
|
remove_directory(self, path.as_ref())
|
|
}
|
|
|
|
fn normalize_path(&self, path: impl AsRef<SystemPath>) -> Utf8PathBuf {
|
|
let normalized = SystemPath::absolute(path, &self.inner.cwd);
|
|
normalized.into_utf8_path_buf()
|
|
}
|
|
|
|
pub fn read_directory(&self, path: impl AsRef<SystemPath>) -> Result<ReadDirectory> {
|
|
let by_path = self.inner.by_path.read().unwrap();
|
|
let normalized = self.normalize_path(path.as_ref());
|
|
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
|
if entry.is_file() {
|
|
return Err(not_a_directory());
|
|
};
|
|
|
|
// Collect the entries into a vector to avoid deadlocks when the
|
|
// consumer calls into other file system methods while iterating over the
|
|
// directory entries.
|
|
let collected = by_path
|
|
.range(normalized.clone()..)
|
|
.skip(1)
|
|
.take_while(|(path, _)| path.starts_with(&normalized))
|
|
.filter_map(|(path, entry)| {
|
|
if path.parent()? == normalized {
|
|
Some(Ok(DirectoryEntry {
|
|
path: SystemPathBuf::from_utf8_path_buf(path.to_owned()),
|
|
file_type: entry.file_type(),
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
Ok(ReadDirectory::new(collected))
|
|
}
|
|
|
|
/// Removes all files and directories except the current working directory.
|
|
pub fn remove_all(&self) {
|
|
self.inner.virtual_files.write().unwrap().clear();
|
|
|
|
self.inner
|
|
.by_path
|
|
.write()
|
|
.unwrap()
|
|
.retain(|key, _| key == self.inner.cwd.as_utf8_path());
|
|
}
|
|
}
|
|
|
|
impl Default for MemoryFileSystem {
|
|
fn default() -> Self {
|
|
MemoryFileSystem::new()
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for MemoryFileSystem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let paths = self.inner.by_path.read().unwrap();
|
|
|
|
f.debug_map().entries(paths.iter()).finish()
|
|
}
|
|
}
|
|
|
|
struct MemoryFileSystemInner {
|
|
by_path: RwLock<BTreeMap<Utf8PathBuf, Entry>>,
|
|
virtual_files: RwLock<FxHashMap<SystemVirtualPathBuf, File>>,
|
|
cwd: SystemPathBuf,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum Entry {
|
|
File(File),
|
|
Directory(Directory),
|
|
}
|
|
|
|
impl Entry {
|
|
const fn is_file(&self) -> bool {
|
|
matches!(self, Entry::File(_))
|
|
}
|
|
|
|
const fn file_type(&self) -> FileType {
|
|
match self {
|
|
Self::File(_) => FileType::File,
|
|
Self::Directory(_) => FileType::Directory,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct File {
|
|
content: String,
|
|
last_modified: FileTime,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Directory {
|
|
last_modified: FileTime,
|
|
}
|
|
|
|
fn not_found() -> std::io::Error {
|
|
std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory")
|
|
}
|
|
|
|
fn is_a_directory() -> std::io::Error {
|
|
// Note: Rust returns `ErrorKind::IsADirectory` for this error but this is a nightly only variant :(.
|
|
// So we have to use other for now.
|
|
std::io::Error::new(std::io::ErrorKind::Other, "Is a directory")
|
|
}
|
|
|
|
fn not_a_directory() -> std::io::Error {
|
|
// Note: Rust returns `ErrorKind::NotADirectory` for this error but this is a nightly only variant :(.
|
|
// So we have to use `Other` for now.
|
|
std::io::Error::new(std::io::ErrorKind::Other, "Not a directory")
|
|
}
|
|
|
|
fn directory_not_empty() -> std::io::Error {
|
|
std::io::Error::new(std::io::ErrorKind::Other, "directory not empty")
|
|
}
|
|
|
|
fn create_dir_all(
|
|
paths: &mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
|
|
normalized: &Utf8Path,
|
|
) -> Result<()> {
|
|
let mut path = Utf8PathBuf::new();
|
|
|
|
for component in normalized.components() {
|
|
path.push(component);
|
|
let entry = paths.entry(path.clone()).or_insert_with(|| {
|
|
Entry::Directory(Directory {
|
|
last_modified: now(),
|
|
})
|
|
});
|
|
|
|
if entry.is_file() {
|
|
return Err(not_a_directory());
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn get_or_create_file<'a>(
|
|
paths: &'a mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
|
|
normalized: &Utf8Path,
|
|
) -> Result<&'a mut File> {
|
|
if let Some(parent) = normalized.parent() {
|
|
create_dir_all(paths, parent)?;
|
|
}
|
|
|
|
let entry = paths.entry(normalized.to_path_buf()).or_insert_with(|| {
|
|
Entry::File(File {
|
|
content: String::new(),
|
|
last_modified: now(),
|
|
})
|
|
});
|
|
|
|
match entry {
|
|
Entry::File(file) => Ok(file),
|
|
Entry::Directory(_) => Err(is_a_directory()),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ReadDirectory {
|
|
entries: std::vec::IntoIter<Result<DirectoryEntry>>,
|
|
}
|
|
|
|
impl ReadDirectory {
|
|
fn new(entries: Vec<Result<DirectoryEntry>>) -> Self {
|
|
Self {
|
|
entries: entries.into_iter(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Iterator for ReadDirectory {
|
|
type Item = std::io::Result<DirectoryEntry>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.entries.next()
|
|
}
|
|
}
|
|
|
|
impl FusedIterator for ReadDirectory {}
|
|
|
|
/// Recursively walks a directory in the memory file system.
|
|
#[derive(Debug)]
|
|
struct MemoryWalker {
|
|
fs: MemoryFileSystem,
|
|
}
|
|
|
|
impl MemoryWalker {
|
|
fn visit_entry(
|
|
&self,
|
|
visitor: &mut dyn WalkDirectoryVisitor,
|
|
entry: walk_directory::DirectoryEntry,
|
|
queue: &mut Vec<WalkerState>,
|
|
ignore_hidden: bool,
|
|
) -> WalkState {
|
|
if entry.file_type().is_directory() {
|
|
let path = entry.path.clone();
|
|
let depth = entry.depth;
|
|
|
|
let state = visitor.visit(Ok(entry));
|
|
|
|
if matches!(state, WalkState::Continue) {
|
|
queue.push(WalkerState::Nested {
|
|
path,
|
|
depth: depth + 1,
|
|
});
|
|
}
|
|
|
|
state
|
|
} else if ignore_hidden
|
|
&& entry
|
|
.path
|
|
.file_name()
|
|
.is_some_and(|name| name.starts_with('.'))
|
|
{
|
|
WalkState::Skip
|
|
} else {
|
|
visitor.visit(Ok(entry))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl DirectoryWalker for MemoryWalker {
|
|
fn walk(
|
|
&self,
|
|
builder: &mut dyn WalkDirectoryVisitorBuilder,
|
|
configuration: WalkDirectoryConfiguration,
|
|
) {
|
|
let WalkDirectoryConfiguration {
|
|
paths,
|
|
ignore_hidden,
|
|
standard_filters: _,
|
|
} = configuration;
|
|
|
|
let mut visitor = builder.build();
|
|
let mut queue: Vec<_> = paths
|
|
.into_iter()
|
|
.map(|path| WalkerState::Start { path })
|
|
.collect();
|
|
|
|
while let Some(state) = queue.pop() {
|
|
let (path, depth) = match state {
|
|
WalkerState::Start { path } => {
|
|
match self.fs.metadata(&path) {
|
|
Ok(metadata) => {
|
|
let entry = walk_directory::DirectoryEntry {
|
|
file_type: metadata.file_type,
|
|
depth: 0,
|
|
path,
|
|
};
|
|
|
|
if self.visit_entry(&mut *visitor, entry, &mut queue, ignore_hidden)
|
|
== WalkState::Quit
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
Err(error) => {
|
|
visitor.visit(Err(walk_directory::Error {
|
|
depth: Some(0),
|
|
kind: walk_directory::ErrorKind::Io {
|
|
path: Some(path),
|
|
err: error,
|
|
},
|
|
}));
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
WalkerState::Nested { path, depth } => (path, depth),
|
|
};
|
|
|
|
// Use `read_directory` here instead of locking `by_path` to avoid deadlocks
|
|
// when the `visitor` calls any file system operations.
|
|
let entries = match self.fs.read_directory(&path) {
|
|
Ok(entries) => entries,
|
|
Err(error) => {
|
|
visitor.visit(Err(walk_directory::Error {
|
|
depth: Some(depth),
|
|
kind: walk_directory::ErrorKind::Io {
|
|
path: Some(path),
|
|
err: error,
|
|
},
|
|
}));
|
|
|
|
continue;
|
|
}
|
|
};
|
|
|
|
for entry in entries {
|
|
match entry {
|
|
Ok(entry) => {
|
|
let entry = walk_directory::DirectoryEntry {
|
|
file_type: entry.file_type,
|
|
depth,
|
|
path: entry.path,
|
|
};
|
|
|
|
if self.visit_entry(&mut *visitor, entry, &mut queue, ignore_hidden)
|
|
== WalkState::Quit
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
Err(error) => {
|
|
visitor.visit(Err(walk_directory::Error {
|
|
depth: Some(depth),
|
|
kind: walk_directory::ErrorKind::Io {
|
|
path: Some(path.clone()),
|
|
err: error,
|
|
},
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum WalkerState {
|
|
/// An entry path that was directly provided to the walker. Always has depth 0.
|
|
Start { path: SystemPathBuf },
|
|
|
|
/// Traverse into the directory with the given path at the given depth.
|
|
Nested { path: SystemPathBuf, depth: usize },
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn now() -> FileTime {
|
|
FileTime::now()
|
|
}
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
fn now() -> FileTime {
|
|
// Copied from FileTime::from_system_time()
|
|
let time = web_time::SystemTime::now();
|
|
|
|
time.duration_since(web_time::UNIX_EPOCH)
|
|
.map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
|
|
.unwrap_or_else(|e| {
|
|
let until_epoch = e.duration();
|
|
let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
|
|
(0, 0)
|
|
} else {
|
|
(-1, 1_000_000_000 - until_epoch.subsec_nanos())
|
|
};
|
|
|
|
FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::io::ErrorKind;
|
|
|
|
use std::time::Duration;
|
|
|
|
use crate::system::walk_directory::tests::DirectoryEntryToString;
|
|
use crate::system::walk_directory::WalkState;
|
|
use crate::system::{
|
|
DirectoryEntry, FileType, MemoryFileSystem, Result, SystemPath, SystemPathBuf,
|
|
SystemVirtualPath,
|
|
};
|
|
|
|
/// Creates a file system with the given files.
|
|
///
|
|
/// The content of all files will be empty.
|
|
fn with_files<P>(files: impl IntoIterator<Item = P>) -> super::MemoryFileSystem
|
|
where
|
|
P: AsRef<SystemPath>,
|
|
{
|
|
let fs = MemoryFileSystem::new();
|
|
fs.write_files(files.into_iter().map(|path| (path, "")))
|
|
.unwrap();
|
|
|
|
fs
|
|
}
|
|
|
|
#[test]
|
|
fn is_file() {
|
|
let path = SystemPath::new("a.py");
|
|
let fs = with_files([path]);
|
|
|
|
assert!(fs.is_file(path));
|
|
assert!(!fs.is_directory(path));
|
|
}
|
|
|
|
#[test]
|
|
fn exists() {
|
|
let fs = with_files(["a.py"]);
|
|
|
|
assert!(fs.exists(SystemPath::new("a.py")));
|
|
assert!(!fs.exists(SystemPath::new("b.py")));
|
|
}
|
|
|
|
#[test]
|
|
fn exists_directories() {
|
|
let fs = with_files(["a/b/c.py"]);
|
|
|
|
assert!(fs.exists(SystemPath::new("a")));
|
|
assert!(fs.exists(SystemPath::new("a/b")));
|
|
assert!(fs.exists(SystemPath::new("a/b/c.py")));
|
|
}
|
|
|
|
#[test]
|
|
fn path_normalization() {
|
|
let fs = with_files(["a.py"]);
|
|
|
|
assert!(fs.exists(SystemPath::new("a.py")));
|
|
assert!(fs.exists(SystemPath::new("/a.py")));
|
|
assert!(fs.exists(SystemPath::new("/b/./../a.py")));
|
|
}
|
|
|
|
#[test]
|
|
fn permissions() -> Result<()> {
|
|
let fs = with_files(["a.py"]);
|
|
|
|
// The default permissions match the default on Linux: 0755
|
|
assert_eq!(
|
|
fs.metadata(SystemPath::new("a.py"))?.permissions(),
|
|
Some(MemoryFileSystem::PERMISSION)
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn touch() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
let path = SystemPath::new("a.py");
|
|
|
|
// Creates a file if it doesn't exist
|
|
fs.touch(path)?;
|
|
|
|
assert!(fs.exists(path));
|
|
|
|
let timestamp1 = fs.metadata(path)?.revision();
|
|
|
|
// Sleep to ensure that the timestamp changes
|
|
std::thread::sleep(Duration::from_millis(1));
|
|
|
|
fs.touch(path)?;
|
|
|
|
let timestamp2 = fs.metadata(path)?.revision();
|
|
|
|
assert_ne!(timestamp1, timestamp2);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn create_dir_all() {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
|
|
|
|
assert!(fs.is_directory(SystemPath::new("a")));
|
|
assert!(fs.is_directory(SystemPath::new("a/b")));
|
|
assert!(fs.is_directory(SystemPath::new("a/b/c")));
|
|
|
|
// Should not fail if the directory already exists
|
|
fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn create_dir_all_fails_if_a_component_is_a_file() {
|
|
let fs = with_files(["a/b.py"]);
|
|
|
|
let error = fs
|
|
.create_directory_all(SystemPath::new("a/b.py/c"))
|
|
.unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
}
|
|
|
|
#[test]
|
|
fn write_file_fails_if_a_component_is_a_file() {
|
|
let fs = with_files(["a/b.py"]);
|
|
|
|
let error = fs
|
|
.write_file(SystemPath::new("a/b.py/c"), "content".to_string())
|
|
.unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
}
|
|
|
|
#[test]
|
|
fn write_file_fails_if_path_points_to_a_directory() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
fs.create_directory_all("a")?;
|
|
|
|
let error = fs
|
|
.write_file(SystemPath::new("a"), "content".to_string())
|
|
.unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn write_virtual_file() {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
fs.write_virtual_file("a", "content");
|
|
|
|
let error = fs.read_to_string("a").unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::NotFound);
|
|
|
|
assert_eq!(fs.read_virtual_path_to_string("a").unwrap(), "content");
|
|
}
|
|
|
|
#[test]
|
|
fn read() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
let path = SystemPath::new("a.py");
|
|
|
|
fs.write_file(path, "Test content".to_string())?;
|
|
|
|
assert_eq!(fs.read_to_string(path)?, "Test content");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_fails_if_path_is_a_directory() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
fs.create_directory_all("a")?;
|
|
|
|
let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_fails_if_path_doesnt_exist() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::NotFound);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn read_fails_if_virtual_path_doesnt_exit() {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
let error = fs.read_virtual_path_to_string("a").unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::NotFound);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_file() -> Result<()> {
|
|
let fs = with_files(["a/a.py", "b.py"]);
|
|
|
|
fs.remove_file("a/a.py")?;
|
|
|
|
assert!(!fs.exists(SystemPath::new("a/a.py")));
|
|
|
|
// It doesn't delete the enclosing directories
|
|
assert!(fs.exists(SystemPath::new("a")));
|
|
|
|
// It doesn't delete unrelated files.
|
|
assert!(fs.exists(SystemPath::new("b.py")));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn remove_virtual_file() {
|
|
let fs = MemoryFileSystem::new();
|
|
fs.write_virtual_file("a", "content");
|
|
fs.write_virtual_file("b", "content");
|
|
|
|
fs.remove_virtual_file("a").unwrap();
|
|
|
|
assert!(!fs.virtual_path_exists(SystemVirtualPath::new("a")));
|
|
assert!(fs.virtual_path_exists(SystemVirtualPath::new("b")));
|
|
}
|
|
|
|
#[test]
|
|
fn remove_non_existing_file() {
|
|
let fs = with_files(["b.py"]);
|
|
|
|
let error = fs.remove_file("a.py").unwrap_err();
|
|
|
|
assert_eq!(error.kind(), ErrorKind::NotFound);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_file_that_is_a_directory() -> Result<()> {
|
|
let fs = MemoryFileSystem::new();
|
|
fs.create_directory_all("a")?;
|
|
|
|
let error = fs.remove_file("a").unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn remove_directory() -> Result<()> {
|
|
let fs = with_files(["b.py"]);
|
|
fs.create_directory_all("a")?;
|
|
|
|
fs.remove_directory("a")?;
|
|
|
|
assert!(!fs.exists(SystemPath::new("a")));
|
|
|
|
// It doesn't delete unrelated files.
|
|
assert!(fs.exists(SystemPath::new("b.py")));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn remove_non_empty_directory() {
|
|
let fs = with_files(["a/a.py"]);
|
|
|
|
let error = fs.remove_directory("a").unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_directory_with_files_that_start_with_the_same_string() -> Result<()> {
|
|
let fs = with_files(["foo_bar.py", "foob.py"]);
|
|
fs.create_directory_all("foo")?;
|
|
|
|
fs.remove_directory("foo").unwrap();
|
|
|
|
assert!(!fs.exists(SystemPath::new("foo")));
|
|
assert!(fs.exists(SystemPath::new("foo_bar.py")));
|
|
assert!(fs.exists(SystemPath::new("foob.py")));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn remove_non_existing_directory() {
|
|
let fs = MemoryFileSystem::new();
|
|
|
|
let error = fs.remove_directory("a").unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::NotFound);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_directory_that_is_a_file() {
|
|
let fs = with_files(["a"]);
|
|
|
|
let error = fs.remove_directory("a").unwrap_err();
|
|
assert_eq!(error.kind(), ErrorKind::Other);
|
|
}
|
|
|
|
#[test]
|
|
fn read_directory() {
|
|
let fs = with_files(["b.ts", "a/bar.py", "d.rs", "a/foo/bar.py", "a/baz.pyi"]);
|
|
let contents: Vec<DirectoryEntry> = fs
|
|
.read_directory("a")
|
|
.unwrap()
|
|
.map(Result::unwrap)
|
|
.collect();
|
|
let expected_contents = vec![
|
|
DirectoryEntry::new(SystemPathBuf::from("/a/bar.py"), FileType::File),
|
|
DirectoryEntry::new(SystemPathBuf::from("/a/baz.pyi"), FileType::File),
|
|
DirectoryEntry::new(SystemPathBuf::from("/a/foo"), FileType::Directory),
|
|
];
|
|
assert_eq!(contents, expected_contents)
|
|
}
|
|
|
|
#[test]
|
|
fn read_directory_nonexistent() {
|
|
let fs = MemoryFileSystem::new();
|
|
let Err(error) = fs.read_directory("doesnt_exist") else {
|
|
panic!("Expected this to fail");
|
|
};
|
|
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
|
}
|
|
|
|
#[test]
|
|
fn read_directory_on_file() {
|
|
let fs = with_files(["a.py"]);
|
|
let Err(error) = fs.read_directory("a.py") else {
|
|
panic!("Expected this to fail");
|
|
};
|
|
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
|
assert!(error.to_string().contains("Not a directory"));
|
|
}
|
|
|
|
#[test]
|
|
fn walk_directory() -> std::io::Result<()> {
|
|
let root = SystemPath::new("/src");
|
|
let system = MemoryFileSystem::with_current_directory(root);
|
|
|
|
system.write_files([
|
|
(root.join("foo.py"), "print('foo')"),
|
|
(root.join("a/bar.py"), "print('bar')"),
|
|
(root.join("a/baz.py"), "print('baz')"),
|
|
(root.join("a/b/c.py"), "print('c')"),
|
|
])?;
|
|
|
|
let writer = DirectoryEntryToString::new(root.to_path_buf());
|
|
|
|
system.walk_directory(root).run(|| {
|
|
Box::new(|entry| {
|
|
writer.write_entry(entry);
|
|
|
|
WalkState::Continue
|
|
})
|
|
});
|
|
|
|
assert_eq!(
|
|
writer.to_string(),
|
|
r#"{
|
|
"": (
|
|
Directory,
|
|
0,
|
|
),
|
|
"a": (
|
|
Directory,
|
|
1,
|
|
),
|
|
"a/b": (
|
|
Directory,
|
|
2,
|
|
),
|
|
"a/b/c.py": (
|
|
File,
|
|
3,
|
|
),
|
|
"a/bar.py": (
|
|
File,
|
|
2,
|
|
),
|
|
"a/baz.py": (
|
|
File,
|
|
2,
|
|
),
|
|
"foo.py": (
|
|
File,
|
|
1,
|
|
),
|
|
}"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn walk_directory_hidden() -> std::io::Result<()> {
|
|
let root = SystemPath::new("/src");
|
|
let system = MemoryFileSystem::with_current_directory(root);
|
|
|
|
system.write_files([
|
|
(root.join("foo.py"), "print('foo')"),
|
|
(root.join("a/bar.py"), "print('bar')"),
|
|
(root.join("a/.baz.py"), "print('baz')"),
|
|
])?;
|
|
|
|
let writer = DirectoryEntryToString::new(root.to_path_buf());
|
|
|
|
system.walk_directory(root).run(|| {
|
|
Box::new(|entry| {
|
|
writer.write_entry(entry);
|
|
|
|
WalkState::Continue
|
|
})
|
|
});
|
|
|
|
assert_eq!(
|
|
writer.to_string(),
|
|
r#"{
|
|
"": (
|
|
Directory,
|
|
0,
|
|
),
|
|
"a": (
|
|
Directory,
|
|
1,
|
|
),
|
|
"a/bar.py": (
|
|
File,
|
|
2,
|
|
),
|
|
"foo.py": (
|
|
File,
|
|
1,
|
|
),
|
|
}"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn walk_directory_file() -> std::io::Result<()> {
|
|
let root = SystemPath::new("/src");
|
|
let system = MemoryFileSystem::with_current_directory(root);
|
|
|
|
system.write_file(root.join("foo.py"), "print('foo')")?;
|
|
|
|
let writer = DirectoryEntryToString::new(root.to_path_buf());
|
|
|
|
system.walk_directory(root.join("foo.py")).run(|| {
|
|
Box::new(|entry| {
|
|
writer.write_entry(entry);
|
|
|
|
WalkState::Continue
|
|
})
|
|
});
|
|
|
|
assert_eq!(
|
|
writer.to_string(),
|
|
r#"{
|
|
"foo.py": (
|
|
File,
|
|
0,
|
|
),
|
|
}"#
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn glob() -> std::io::Result<()> {
|
|
let root = SystemPath::new("/src");
|
|
let fs = MemoryFileSystem::with_current_directory(root);
|
|
|
|
fs.write_files([
|
|
(root.join("foo.py"), "print('foo')"),
|
|
(root.join("a/bar.py"), "print('bar')"),
|
|
(root.join("a/.baz.py"), "print('baz')"),
|
|
])?;
|
|
|
|
let mut matches = fs.glob("/src/a/**").unwrap().flatten().collect::<Vec<_>>();
|
|
matches.sort_unstable();
|
|
|
|
assert_eq!(matches, vec![root.join("a/.baz.py"), root.join("a/bar.py")]);
|
|
|
|
let matches = fs.glob("**/bar.py").unwrap().flatten().collect::<Vec<_>>();
|
|
assert_eq!(matches, vec![root.join("a/bar.py")]);
|
|
|
|
Ok(())
|
|
}
|
|
}
|