ruff_db/files/
directory.rs1use compact_str::CompactString;
2
3use super::private::FileStatus;
4use super::{File, FilePath};
5use crate::Db;
6use crate::system::{FileType, SystemPath};
7
8#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
12pub struct DirectoryListing(Box<[(CompactString, FileType)]>);
13
14impl DirectoryListing {
15 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 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 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 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 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#[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}