termesh_filesystem/
real.rs1use std::io;
5use std::path::{Path, PathBuf};
6
7use crate::service::{sort_entries, DirEntryInfo, EntryKind, FileSystemService, FsError, FsResult};
8
9#[derive(Debug, Clone, Copy, Default)]
12pub struct RealFileSystem;
13
14impl RealFileSystem {
15 pub const fn new() -> Self {
16 Self
17 }
18}
19
20fn map_err(path: &Path, e: io::Error) -> FsError {
22 match e.kind() {
23 io::ErrorKind::NotFound => FsError::NotFound(path.to_path_buf()),
24 io::ErrorKind::PermissionDenied => FsError::PermissionDenied(path.to_path_buf()),
25 io::ErrorKind::AlreadyExists => FsError::AlreadyExists(path.to_path_buf()),
26 _ => FsError::Other { path: path.to_path_buf(), message: e.to_string() },
27 }
28}
29
30impl FileSystemService for RealFileSystem {
31 fn read_dir(&self, path: &Path) -> FsResult<Vec<DirEntryInfo>> {
32 let mut out = Vec::new();
33 for entry in std::fs::read_dir(path).map_err(|e| map_err(path, e))? {
34 let Ok(entry) = entry else { continue };
37 let entry_path = entry.path();
38
39 let kind = match entry.file_type() {
42 Ok(ft) if ft.is_symlink() => EntryKind::Symlink,
43 Ok(ft) if ft.is_dir() => EntryKind::Dir,
44 Ok(_) => EntryKind::File,
45 Err(_) => continue,
46 };
47
48 out.push(DirEntryInfo { name: entry.file_name(), path: entry_path, kind });
49 }
50 sort_entries(&mut out);
51 Ok(out)
52 }
53
54 fn read_file(&self, path: &Path) -> FsResult<Vec<u8>> {
55 std::fs::read(path).map_err(|e| map_err(path, e))
56 }
57
58 fn create_file(&self, path: &Path) -> FsResult<()> {
59 std::fs::OpenOptions::new()
61 .write(true)
62 .create_new(true)
63 .open(path)
64 .map(|_| ())
65 .map_err(|e| map_err(path, e))
66 }
67
68 fn write_file(&self, path: &Path, contents: &[u8]) -> FsResult<()> {
69 std::fs::write(path, contents).map_err(|e| map_err(path, e))
70 }
71
72 fn create_dir(&self, path: &Path) -> FsResult<()> {
73 std::fs::create_dir_all(path).map_err(|e| map_err(path, e))
74 }
75
76 fn rename(&self, from: &Path, to: &Path) -> FsResult<()> {
77 std::fs::rename(from, to).map_err(|e| map_err(from, e))
78 }
79
80 fn remove_file(&self, path: &Path) -> FsResult<()> {
81 std::fs::remove_file(path).map_err(|e| map_err(path, e))
82 }
83
84 fn remove_dir_all(&self, path: &Path) -> FsResult<()> {
85 std::fs::remove_dir_all(path).map_err(|e| map_err(path, e))
86 }
87
88 fn canonicalize(&self, path: &Path) -> FsResult<PathBuf> {
89 std::fs::canonicalize(path).map_err(|e| map_err(path, e))
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 struct TempDir(PathBuf);
99
100 impl TempDir {
101 fn new(tag: &str) -> Self {
102 let stamp = std::time::SystemTime::now()
105 .duration_since(std::time::UNIX_EPOCH)
106 .unwrap()
107 .as_nanos();
108 let p = std::env::temp_dir().join(format!("termesh-fs-{tag}-{stamp}"));
109 std::fs::create_dir_all(&p).unwrap();
110 Self(p)
111 }
112 fn path(&self) -> &Path {
113 &self.0
114 }
115 }
116
117 impl Drop for TempDir {
118 fn drop(&mut self) {
119 let _ = std::fs::remove_dir_all(&self.0);
120 }
121 }
122
123 #[test]
124 fn read_dir_lists_one_level_sorted_dirs_first() {
125 let tmp = TempDir::new("readdir");
126 let fs = RealFileSystem::new();
127 fs.create_dir(&tmp.path().join("src/nested")).unwrap();
128 fs.create_file(&tmp.path().join("Cargo.toml")).unwrap();
129 fs.create_file(&tmp.path().join("README.md")).unwrap();
130
131 let entries = fs.read_dir(tmp.path()).unwrap();
132 let names: Vec<_> = entries.iter().map(|e| e.name.to_string_lossy().into_owned()).collect();
133 assert_eq!(names, ["src", "Cargo.toml", "README.md"], "one level only, dirs first");
134 assert_eq!(entries[0].kind, EntryKind::Dir);
135 }
136
137 #[test]
138 fn create_file_refuses_to_clobber() {
139 let tmp = TempDir::new("clobber");
140 let fs = RealFileSystem::new();
141 let f = tmp.path().join("a.txt");
142 fs.create_file(&f).unwrap();
143 std::fs::write(&f, b"precious").unwrap();
144
145 assert_eq!(fs.create_file(&f), Err(FsError::AlreadyExists(f.clone())));
146 assert_eq!(fs.read_file(&f).unwrap(), b"precious", "existing content is untouched");
147 }
148
149 #[test]
150 fn missing_paths_report_not_found() {
151 let tmp = TempDir::new("missing");
152 let fs = RealFileSystem::new();
153 let missing = tmp.path().join("nope");
154 assert_eq!(fs.read_dir(&missing), Err(FsError::NotFound(missing.clone())));
155 assert_eq!(fs.read_file(&missing), Err(FsError::NotFound(missing)));
156 }
157
158 #[test]
159 fn rename_and_remove_round_trip() {
160 let tmp = TempDir::new("rename");
161 let fs = RealFileSystem::new();
162 let (a, b) = (tmp.path().join("a.txt"), tmp.path().join("b.txt"));
163 fs.create_file(&a).unwrap();
164 fs.rename(&a, &b).unwrap();
165
166 let names: Vec<_> =
167 fs.read_dir(tmp.path()).unwrap().iter().map(|e| e.name.clone()).collect();
168 assert_eq!(names, ["b.txt"]);
169
170 fs.remove_file(&b).unwrap();
171 assert!(fs.read_dir(tmp.path()).unwrap().is_empty());
172 }
173
174 #[test]
175 fn symlinks_are_reported_as_symlinks_not_followed() {
176 #[cfg(unix)]
178 {
179 let tmp = TempDir::new("symlink");
180 let fs = RealFileSystem::new();
181 fs.create_dir(&tmp.path().join("target")).unwrap();
182 std::os::unix::fs::symlink(tmp.path().join("target"), tmp.path().join("link")).unwrap();
183
184 let entries = fs.read_dir(tmp.path()).unwrap();
185 let link = entries.iter().find(|e| e.name == "link").unwrap();
186 assert_eq!(link.kind, EntryKind::Symlink, "must not resolve to Dir");
187 }
188 }
189}