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 write_file_with_mode(&self, path: &Path, contents: &[u8], mode: u32) -> Result<()> {
58 use std::io::Write as _;
59 use std::os::unix::fs::OpenOptionsExt;
60 let mut file = fs::OpenOptions::new()
71 .write(true)
72 .create(true)
73 .truncate(true)
74 .mode(mode)
75 .open(path)
76 .map_err(|e| fs_err(path, e))?;
77 let perms = fs::Permissions::from_mode(mode);
81 fs::set_permissions(path, perms).map_err(|e| fs_err(path, e))?;
82 file.write_all(contents).map_err(|e| fs_err(path, e))?;
83 file.sync_all().map_err(|e| fs_err(path, e))?;
84 Ok(())
85 }
86
87 fn mkdir_all(&self, path: &Path) -> Result<()> {
88 fs::create_dir_all(path).map_err(|e| fs_err(path, e))
89 }
90
91 fn symlink(&self, original: &Path, link: &Path) -> Result<()> {
92 std::os::unix::fs::symlink(original, link).map_err(|e| fs_err(link, e))
93 }
94
95 fn readlink(&self, path: &Path) -> Result<PathBuf> {
96 fs::read_link(path).map_err(|e| fs_err(path, e))
97 }
98
99 fn remove_file(&self, path: &Path) -> Result<()> {
100 fs::remove_file(path).map_err(|e| fs_err(path, e))
101 }
102
103 fn remove_dir_all(&self, path: &Path) -> Result<()> {
104 fs::remove_dir_all(path).map_err(|e| fs_err(path, e))
105 }
106
107 fn exists(&self, path: &Path) -> bool {
108 path.exists()
109 }
110
111 fn is_symlink(&self, path: &Path) -> bool {
112 path.symlink_metadata()
113 .map(|m| m.file_type().is_symlink())
114 .unwrap_or(false)
115 }
116
117 fn is_dir(&self, path: &Path) -> bool {
118 path.is_dir()
119 }
120
121 fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
122 let entries = fs::read_dir(path).map_err(|e| fs_err(path, e))?;
123
124 let mut result = Vec::new();
125 for entry in entries {
126 let entry = entry.map_err(|e| fs_err(path, e))?;
127 let file_type = entry.file_type().map_err(|e| fs_err(entry.path(), e))?;
128 let name = entry.file_name().to_string_lossy().into_owned();
129
130 result.push(DirEntry {
131 path: entry.path(),
132 name,
133 is_dir: file_type.is_dir(),
134 is_file: file_type.is_file(),
135 is_symlink: file_type.is_symlink(),
136 });
137 }
138
139 result.sort_by(|a, b| a.name.cmp(&b.name));
140 Ok(result)
141 }
142
143 fn rename(&self, from: &Path, to: &Path) -> Result<()> {
144 fs::rename(from, to).map_err(|e| fs_err(from, e))
145 }
146
147 fn copy_file(&self, from: &Path, to: &Path) -> Result<()> {
148 fs::copy(from, to).map(|_| ()).map_err(|e| fs_err(from, e))
149 }
150
151 fn set_permissions(&self, path: &Path, mode: u32) -> Result<()> {
152 let perms = fs::Permissions::from_mode(mode);
153 fs::set_permissions(path, perms).map_err(|e| fs_err(path, e))
154 }
155
156 fn modified(&self, path: &Path) -> Result<std::time::SystemTime> {
157 fs::metadata(path)
158 .and_then(|m| m.modified())
159 .map_err(|e| fs_err(path, e))
160 }
161
162 fn set_modified(&self, path: &Path, time: std::time::SystemTime) -> Result<()> {
163 let file = fs::OpenOptions::new()
168 .write(true)
169 .open(path)
170 .map_err(|e| fs_err(path, e))?;
171 file.set_modified(time).map_err(|e| fs_err(path, e))
172 }
173}
174
175fn metadata_from_std(meta: &fs::Metadata, is_symlink: bool) -> FsMetadata {
176 FsMetadata {
177 is_file: meta.is_file(),
178 is_dir: meta.is_dir(),
179 is_symlink,
180 len: meta.len(),
181 mode: meta.permissions().mode(),
182 }
183}
184
185#[cfg(test)]
186mod tests {
187 use super::*;
188 use tempfile::TempDir;
189
190 #[test]
191 fn write_and_read_file() {
192 let tmp = TempDir::new().unwrap();
193 let fs = OsFs::new();
194 let path = tmp.path().join("hello.txt");
195
196 fs.write_file(&path, b"hello world").unwrap();
197 let contents = fs.read_to_string(&path).unwrap();
198 assert_eq!(contents, "hello world");
199 }
200
201 #[test]
202 fn read_file_bytes() {
203 let tmp = TempDir::new().unwrap();
204 let fs = OsFs::new();
205 let path = tmp.path().join("data.bin");
206
207 let data = vec![0u8, 1, 2, 255];
208 fs.write_file(&path, &data).unwrap();
209 let read_back = fs.read_file(&path).unwrap();
210 assert_eq!(read_back, data);
211 }
212
213 #[test]
214 fn mkdir_all_creates_nested_dirs() {
215 let tmp = TempDir::new().unwrap();
216 let fs = OsFs::new();
217 let deep = tmp.path().join("a").join("b").join("c");
218
219 fs.mkdir_all(&deep).unwrap();
220 assert!(fs.is_dir(&deep));
221 }
222
223 #[test]
224 fn symlink_and_readlink_roundtrip() {
225 let tmp = TempDir::new().unwrap();
226 let fs = OsFs::new();
227 let original = tmp.path().join("original.txt");
228 let link = tmp.path().join("link.txt");
229
230 fs.write_file(&original, b"content").unwrap();
231 fs.symlink(&original, &link).unwrap();
232
233 assert!(fs.is_symlink(&link));
234 assert_eq!(fs.readlink(&link).unwrap(), original);
235
236 let content = fs.read_to_string(&link).unwrap();
238 assert_eq!(content, "content");
239 }
240
241 #[test]
242 fn stat_follows_symlinks() {
243 let tmp = TempDir::new().unwrap();
244 let fs = OsFs::new();
245 let original = tmp.path().join("file.txt");
246 let link = tmp.path().join("link.txt");
247
248 fs.write_file(&original, b"data").unwrap();
249 fs.symlink(&original, &link).unwrap();
250
251 let meta = fs.stat(&link).unwrap();
252 assert!(meta.is_file);
253 assert!(!meta.is_symlink);
254 }
255
256 #[test]
257 fn lstat_does_not_follow_symlinks() {
258 let tmp = TempDir::new().unwrap();
259 let fs = OsFs::new();
260 let original = tmp.path().join("file.txt");
261 let link = tmp.path().join("link.txt");
262
263 fs.write_file(&original, b"data").unwrap();
264 fs.symlink(&original, &link).unwrap();
265
266 let meta = fs.lstat(&link).unwrap();
267 assert!(meta.is_symlink);
268 }
269
270 #[test]
271 fn exists_and_is_dir() {
272 let tmp = TempDir::new().unwrap();
273 let fs = OsFs::new();
274
275 assert!(fs.exists(tmp.path()));
276 assert!(fs.is_dir(tmp.path()));
277 assert!(!fs.exists(&tmp.path().join("nope")));
278 }
279
280 #[test]
281 fn read_dir_sorted() {
282 let tmp = TempDir::new().unwrap();
283 let fs = OsFs::new();
284
285 fs.write_file(&tmp.path().join("c.txt"), b"").unwrap();
286 fs.write_file(&tmp.path().join("a.txt"), b"").unwrap();
287 fs.write_file(&tmp.path().join("b.txt"), b"").unwrap();
288
289 let entries = fs.read_dir(tmp.path()).unwrap();
290 let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
291 assert_eq!(names, vec!["a.txt", "b.txt", "c.txt"]);
292 }
293
294 #[test]
295 fn remove_file_and_remove_dir_all() {
296 let tmp = TempDir::new().unwrap();
297 let fs = OsFs::new();
298
299 let file = tmp.path().join("file.txt");
300 fs.write_file(&file, b"x").unwrap();
301 assert!(fs.exists(&file));
302 fs.remove_file(&file).unwrap();
303 assert!(!fs.exists(&file));
304
305 let dir = tmp.path().join("subdir");
306 fs.mkdir_all(&dir.join("nested")).unwrap();
307 fs.write_file(&dir.join("nested").join("f.txt"), b"y")
308 .unwrap();
309 assert!(fs.exists(&dir));
310 fs.remove_dir_all(&dir).unwrap();
311 assert!(!fs.exists(&dir));
312 }
313
314 #[test]
315 fn rename_file() {
316 let tmp = TempDir::new().unwrap();
317 let fs = OsFs::new();
318
319 let from = tmp.path().join("old.txt");
320 let to = tmp.path().join("new.txt");
321 fs.write_file(&from, b"moved").unwrap();
322 fs.rename(&from, &to).unwrap();
323
324 assert!(!fs.exists(&from));
325 assert_eq!(fs.read_to_string(&to).unwrap(), "moved");
326 }
327
328 #[test]
329 fn copy_file_preserves_content() {
330 let tmp = TempDir::new().unwrap();
331 let fs = OsFs::new();
332
333 let from = tmp.path().join("src.txt");
334 let to = tmp.path().join("dst.txt");
335 fs.write_file(&from, b"copied").unwrap();
336 fs.copy_file(&from, &to).unwrap();
337
338 assert!(fs.exists(&from));
339 assert_eq!(fs.read_to_string(&to).unwrap(), "copied");
340 }
341
342 #[test]
343 fn error_contains_path() {
344 let fs = OsFs::new();
345 let bad_path = Path::new("/nonexistent/path/to/file.txt");
346
347 let err = fs.read_file(bad_path).unwrap_err();
348 let msg = err.to_string();
349 assert!(
350 msg.contains("/nonexistent/path/to/file.txt"),
351 "error should contain the path: {msg}"
352 );
353 }
354
355 #[test]
356 fn set_permissions_works() {
357 let tmp = TempDir::new().unwrap();
358 let fs = OsFs::new();
359
360 let file = tmp.path().join("script.sh");
361 fs.write_file(&file, b"#!/bin/sh").unwrap();
362 fs.set_permissions(&file, 0o755).unwrap();
363
364 let meta = std::fs::metadata(&file).unwrap();
365 assert_eq!(meta.permissions().mode() & 0o777, 0o755);
366 }
367
368 #[allow(dead_code)]
370 fn assert_object_safe(_: &dyn Fs) {}
371}