Skip to main content

ruff_db/files/
directory.rs

1use compact_str::CompactString;
2
3use super::private::FileStatus;
4use super::{File, FilePath};
5use crate::Db;
6use crate::system::{FileType, SystemPath};
7
8/// A cached snapshot of the direct children in a directory.
9///
10/// The entries are sorted by name for efficient lookups.
11#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
12pub struct DirectoryListing(Box<[(CompactString, FileType)]>);
13
14impl DirectoryListing {
15    /// Returns the type of the entry named `name`, if present.
16    fn file_type(&self, name: &str) -> Option<FileType> {
17        self.0
18            .binary_search_by(|(candidate, _)| candidate.as_str().cmp(name))
19            .ok()
20            .map(|index| self.0[index].1)
21    }
22
23    /// Returns whether any entry name starts with `prefix`.
24    pub fn contains_name_with_prefix(&self, prefix: &str) -> bool {
25        let index = self
26            .0
27            .partition_point(|(candidate, _)| candidate.as_str() < prefix);
28        self.0
29            .get(index)
30            .is_some_and(|(name, _)| name.starts_with(prefix))
31    }
32
33    /// Returns whether `name` resolves to a file, following symbolic links.
34    pub fn entry_is_file(&self, db: &dyn Db, directory: &SystemPath, name: &str) -> bool {
35        match self.file_type(name) {
36            Some(FileType::File) => true,
37            Some(FileType::Directory) | None => false,
38            Some(FileType::Symlink) => super::system_path_to_file(db, directory.join(name)).is_ok(),
39        }
40    }
41
42    /// Returns whether `name` resolves to a directory, following symbolic links.
43    pub fn entry_is_directory(&self, db: &dyn Db, directory: &SystemPath, name: &str) -> bool {
44        match self.file_type(name) {
45            Some(FileType::Directory) => true,
46            Some(FileType::File) | None => false,
47            Some(FileType::Symlink) => db.system().is_directory(&directory.join(name)),
48        }
49    }
50
51    /// Iterates over the entries in the directory in name order.
52    pub fn iter(&self) -> impl Iterator<Item = (&str, FileType)> {
53        self.0
54            .iter()
55            .map(|(name, file_type)| (name.as_str(), *file_type))
56    }
57}
58
59#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize, thiserror::Error)]
60#[error("{message}")]
61pub struct DirectoryListingError {
62    #[get_size(ignore)]
63    kind: std::io::ErrorKind,
64    message: Box<str>,
65}
66
67impl From<std::io::Error> for DirectoryListingError {
68    fn from(error: std::io::Error) -> Self {
69        Self {
70            kind: error.kind(),
71            message: error.to_string().into_boxed_str(),
72        }
73    }
74}
75
76/// Interns a directory system path and returns a salsa `File` ingredient.
77///
78/// Returns `Err` if the path doesn't exist, isn't accessible, or if the path doesn't point to a directory.
79#[inline]
80pub fn system_path_to_directory(
81    db: &dyn Db,
82    path: impl AsRef<SystemPath>,
83) -> Result<File, DirectoryListingError> {
84    let file = db.files().system(db, path.as_ref());
85
86    match file.status(db) {
87        FileStatus::IsADirectory => Ok(file),
88        FileStatus::Exists => Err(std::io::Error::from(std::io::ErrorKind::NotADirectory).into()),
89        FileStatus::NotFound => Err(std::io::Error::from(std::io::ErrorKind::NotFound).into()),
90    }
91}
92
93#[inline]
94pub fn directory_listing<'db>(
95    db: &'db dyn Db,
96    path: &SystemPath,
97) -> Result<&'db DirectoryListing, DirectoryListingError> {
98    let directory = system_path_to_directory(db, path)?;
99    directory_listing_query(db, directory).map_err(Clone::clone)
100}
101
102#[salsa::tracked(returns(as_ref), heap_size=ruff_memory_usage::heap_size)]
103fn directory_listing_query(
104    db: &dyn Db,
105    directory: File,
106) -> Result<DirectoryListing, DirectoryListingError> {
107    let _ = directory.revision(db);
108    let _ = directory.permissions(db);
109
110    let path = match directory.path(db) {
111        FilePath::System(path) => path,
112        FilePath::Vendored(_) | FilePath::SystemVirtual(_) => {
113            return Err(std::io::Error::new(
114                std::io::ErrorKind::InvalidInput,
115                "directory listings are only supported for system paths",
116            )
117            .into());
118        }
119    };
120
121    let mut entries = db
122        .system()
123        .read_directory(path)?
124        .filter_map(|entry| {
125            let entry = entry.ok()?;
126            let file_type = entry.file_type();
127            let path = entry.into_path();
128            let name = path.file_name()?;
129            Some((CompactString::from(name), file_type))
130        })
131        .collect::<Vec<_>>();
132
133    entries.sort_unstable_by(|left, right| left.0.cmp(&right.0));
134    Ok(DirectoryListing(entries.into_boxed_slice()))
135}
136
137#[cfg(test)]
138mod tests {
139    use crate::files::directory_listing;
140    use crate::system::{DbWithWritableSystem as _, SystemPath};
141    use crate::tests::TestDb;
142
143    #[test]
144    fn listing_is_sorted() -> std::io::Result<()> {
145        let mut db = TestDb::new();
146        db.write_file("src/z.py", "")?;
147        db.write_file("src/a.py", "")?;
148
149        let path = SystemPath::new("src");
150        let listing = directory_listing(&db, path).unwrap();
151        assert_eq!(
152            listing.iter().map(|(name, _)| name).collect::<Vec<_>>(),
153            ["a.py", "z.py"]
154        );
155        assert!(listing.contains_name_with_prefix("a"));
156        assert!(!listing.contains_name_with_prefix("b"));
157
158        Ok(())
159    }
160
161    #[test]
162    fn empty_and_unavailable_listing() {
163        let db = TestDb::new();
164
165        assert_eq!(
166            directory_listing(&db, SystemPath::new("/"))
167                .unwrap()
168                .iter()
169                .next(),
170            None
171        );
172
173        assert_eq!(
174            directory_listing(&db, SystemPath::new("missing"))
175                .unwrap_err()
176                .kind,
177            std::io::ErrorKind::NotFound
178        );
179    }
180}