1use std::fs;
2use std::os::unix::fs::PermissionsExt;
3use std::path::{Path, PathBuf};
4
5use crate::error::fs_err;
6use crate::fs::{DirEntry, Fs, FsMetadata};
7use crate::Result;
8
9#[derive(Debug, Clone, Copy)]
14pub struct OsFs;
15
16impl OsFs {
17 pub fn new() -> Self {
18 Self
19 }
20}
21
22impl Default for OsFs {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl Fs for OsFs {
29 fn stat(&self, path: &Path) -> Result<FsMetadata> {
30 let meta = fs::metadata(path).map_err(|e| fs_err(path, e))?;
31 Ok(metadata_from_std(&meta, false))
32 }
33
34 fn lstat(&self, path: &Path) -> Result<FsMetadata> {
35 let meta = fs::symlink_metadata(path).map_err(|e| fs_err(path, e))?;
36 let is_symlink = meta.file_type().is_symlink();
37 Ok(metadata_from_std(&meta, is_symlink))
38 }
39
40 fn open_read(&self, path: &Path) -> Result<Box<dyn std::io::Read + Send + Sync>> {
41 let f = fs::File::open(path).map_err(|e| fs_err(path, e))?;
42 Ok(Box::new(f))
43 }
44
45 fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
46 fs::read(path).map_err(|e| fs_err(path, e))
47 }
48
49 fn read_to_string(&self, path: &Path) -> Result<String> {
50 fs::read_to_string(path).map_err(|e| fs_err(path, e))
51 }
52
53 fn write_file(&self, path: &Path, contents: &[u8]) -> Result<()> {
54 fs::write(path, contents).map_err(|e| fs_err(path, e))
55 }
56
57 fn mkdir_all(&self, path: &Path) -> Result<()> {
58 fs::create_dir_all(path).map_err(|e| fs_err(path, e))
59 }
60
61 fn symlink(&self, original: &Path, link: &Path) -> Result<()> {
62 std::os::unix::fs::symlink(original, link).map_err(|e| fs_err(link, e))
63 }
64
65 fn readlink(&self, path: &Path) -> Result<PathBuf> {
66 fs::read_link(path).map_err(|e| fs_err(path, e))
67 }
68
69 fn remove_file(&self, path: &Path) -> Result<()> {
70 fs::remove_file(path).map_err(|e| fs_err(path, e))
71 }
72
73 fn remove_dir_all(&self, path: &Path) -> Result<()> {
74 fs::remove_dir_all(path).map_err(|e| fs_err(path, e))
75 }
76
77 fn exists(&self, path: &Path) -> bool {
78 path.exists()
79 }
80
81 fn is_symlink(&self, path: &Path) -> bool {
82 path.symlink_metadata()
83 .map(|m| m.file_type().is_symlink())
84 .unwrap_or(false)
85 }
86
87 fn is_dir(&self, path: &Path) -> bool {
88 path.is_dir()
89 }
90
91 fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
92 let entries = fs::read_dir(path).map_err(|e| fs_err(path, e))?;
93
94 let mut result = Vec::new();
95 for entry in entries {
96 let entry = entry.map_err(|e| fs_err(path, e))?;
97 let file_type = entry.file_type().map_err(|e| fs_err(entry.path(), e))?;
98 let name = entry.file_name().to_string_lossy().into_owned();
99
100 result.push(DirEntry {
101 path: entry.path(),
102 name,
103 is_dir: file_type.is_dir(),
104 is_file: file_type.is_file(),
105 is_symlink: file_type.is_symlink(),
106 });
107 }
108
109 result.sort_by(|a, b| a.name.cmp(&b.name));
110 Ok(result)
111 }
112
113 fn rename(&self, from: &Path, to: &Path) -> Result<()> {
114 fs::rename(from, to).map_err(|e| fs_err(from, e))
115 }
116
117 fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
118 fs::copy(from, to).map(|_| ()).map_err(|e| fs_err(from, e))
119 }
120
121 fn set_permissions(&self, path: &Path, mode: u32) -> Result<()> {
122 let perms = fs::Permissions::from_mode(mode);
123 fs::set_permissions(path, perms).map_err(|e| fs_err(path, e))
124 }
125
126 fn modified(&self, path: &Path) -> Result<std::time::SystemTime> {
127 fs::metadata(path)
128 .and_then(|m| m.modified())
129 .map_err(|e| fs_err(path, e))
130 }
131
132 fn set_modified(&self, path: &Path, time: std::time::SystemTime) -> Result<()> {
133 let file = fs::OpenOptions::new()
138 .write(true)
139 .open(path)
140 .map_err(|e| fs_err(path, e))?;
141 file.set_modified(time).map_err(|e| fs_err(path, e))
142 }
143}
144
145fn metadata_from_std(meta: &fs::Metadata, is_symlink: bool) -> FsMetadata {
146 FsMetadata {
147 is_file: meta.is_file(),
148 is_dir: meta.is_dir(),
149 is_symlink,
150 len: meta.len(),
151 mode: meta.permissions().mode(),
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use tempfile::TempDir;
159
160 #[test]
161 fn write_and_read_file() {
162 let tmp = TempDir::new().unwrap();
163 let fs = OsFs::new();
164 let path = tmp.path().join("hello.txt");
165
166 fs.write_file(&path, b"hello world").unwrap();
167 let contents = fs.read_to_string(&path).unwrap();
168 assert_eq!(contents, "hello world");
169 }
170
171 #[test]
172 fn read_file_bytes() {
173 let tmp = TempDir::new().unwrap();
174 let fs = OsFs::new();
175 let path = tmp.path().join("data.bin");
176
177 let data = vec![0u8, 1, 2, 255];
178 fs.write_file(&path, &data).unwrap();
179 let read_back = fs.read_file(&path).unwrap();
180 assert_eq!(read_back, data);
181 }
182
183 #[test]
184 fn mkdir_all_creates_nested_dirs() {
185 let tmp = TempDir::new().unwrap();
186 let fs = OsFs::new();
187 let deep = tmp.path().join("a").join("b").join("c");
188
189 fs.mkdir_all(&deep).unwrap();
190 assert!(fs.is_dir(&deep));
191 }
192
193 #[test]
194 fn symlink_and_readlink_roundtrip() {
195 let tmp = TempDir::new().unwrap();
196 let fs = OsFs::new();
197 let original = tmp.path().join("original.txt");
198 let link = tmp.path().join("link.txt");
199
200 fs.write_file(&original, b"content").unwrap();
201 fs.symlink(&original, &link).unwrap();
202
203 assert!(fs.is_symlink(&link));
204 assert_eq!(fs.readlink(&link).unwrap(), original);
205
206 let content = fs.read_to_string(&link).unwrap();
208 assert_eq!(content, "content");
209 }
210
211 #[test]
212 fn stat_follows_symlinks() {
213 let tmp = TempDir::new().unwrap();
214 let fs = OsFs::new();
215 let original = tmp.path().join("file.txt");
216 let link = tmp.path().join("link.txt");
217
218 fs.write_file(&original, b"data").unwrap();
219 fs.symlink(&original, &link).unwrap();
220
221 let meta = fs.stat(&link).unwrap();
222 assert!(meta.is_file);
223 assert!(!meta.is_symlink);
224 }
225
226 #[test]
227 fn lstat_does_not_follow_symlinks() {
228 let tmp = TempDir::new().unwrap();
229 let fs = OsFs::new();
230 let original = tmp.path().join("file.txt");
231 let link = tmp.path().join("link.txt");
232
233 fs.write_file(&original, b"data").unwrap();
234 fs.symlink(&original, &link).unwrap();
235
236 let meta = fs.lstat(&link).unwrap();
237 assert!(meta.is_symlink);
238 }
239
240 #[test]
241 fn exists_and_is_dir() {
242 let tmp = TempDir::new().unwrap();
243 let fs = OsFs::new();
244
245 assert!(fs.exists(tmp.path()));
246 assert!(fs.is_dir(tmp.path()));
247 assert!(!fs.exists(&tmp.path().join("nope")));
248 }
249
250 #[test]
251 fn read_dir_sorted() {
252 let tmp = TempDir::new().unwrap();
253 let fs = OsFs::new();
254
255 fs.write_file(&tmp.path().join("c.txt"), b"").unwrap();
256 fs.write_file(&tmp.path().join("a.txt"), b"").unwrap();
257 fs.write_file(&tmp.path().join("b.txt"), b"").unwrap();
258
259 let entries = fs.read_dir(tmp.path()).unwrap();
260 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
261 assert_eq!(names, vec!["a.txt", "b.txt", "c.txt"]);
262 }
263
264 #[test]
265 fn remove_file_and_remove_dir_all() {
266 let tmp = TempDir::new().unwrap();
267 let fs = OsFs::new();
268
269 let file = tmp.path().join("file.txt");
270 fs.write_file(&file, b"x").unwrap();
271 assert!(fs.exists(&file));
272 fs.remove_file(&file).unwrap();
273 assert!(!fs.exists(&file));
274
275 let dir = tmp.path().join("subdir");
276 fs.mkdir_all(&dir.join("nested")).unwrap();
277 fs.write_file(&dir.join("nested").join("f.txt"), b"y")
278 .unwrap();
279 assert!(fs.exists(&dir));
280 fs.remove_dir_all(&dir).unwrap();
281 assert!(!fs.exists(&dir));
282 }
283
284 #[test]
285 fn rename_file() {
286 let tmp = TempDir::new().unwrap();
287 let fs = OsFs::new();
288
289 let from = tmp.path().join("old.txt");
290 let to = tmp.path().join("new.txt");
291 fs.write_file(&from, b"moved").unwrap();
292 fs.rename(&from, &to).unwrap();
293
294 assert!(!fs.exists(&from));
295 assert_eq!(fs.read_to_string(&to).unwrap(), "moved");
296 }
297
298 #[test]
299 fn copy_file_preserves_content() {
300 let tmp = TempDir::new().unwrap();
301 let fs = OsFs::new();
302
303 let from = tmp.path().join("src.txt");
304 let to = tmp.path().join("dst.txt");
305 fs.write_file(&from, b"copied").unwrap();
306 fs.copy_file(&from, &to).unwrap();
307
308 assert!(fs.exists(&from));
309 assert_eq!(fs.read_to_string(&to).unwrap(), "copied");
310 }
311
312 #[test]
313 fn error_contains_path() {
314 let fs = OsFs::new();
315 let bad_path = Path::new("/nonexistent/path/to/file.txt");
316
317 let err = fs.read_file(bad_path).unwrap_err();
318 let msg = err.to_string();
319 assert!(
320 msg.contains("/nonexistent/path/to/file.txt"),
321 "error should contain the path: {msg}"
322 );
323 }
324
325 #[test]
326 fn set_permissions_works() {
327 let tmp = TempDir::new().unwrap();
328 let fs = OsFs::new();
329
330 let file = tmp.path().join("script.sh");
331 fs.write_file(&file, b"#!/bin/sh").unwrap();
332 fs.set_permissions(&file, 0o755).unwrap();
333
334 let meta = std::fs::metadata(&file).unwrap();
335 assert_eq!(meta.permissions().mode() & 0o777, 0o755);
336 }
337
338 #[allow(dead_code)]
340 fn assert_object_safe(_: &dyn Fs) {}
341}