1use crate::time_compat::SystemTime;
50use async_trait::async_trait;
51use std::io::Error as IoError;
52use std::path::{Path, PathBuf};
53use std::sync::Arc;
54
55use super::backend::FsBackend;
56use super::limits::{FsLimits, FsUsage};
57use super::normalize_path;
58use super::traits::{DirEntry, FileSystem, FileSystemExt, Metadata, fs_errors};
59use crate::error::Result;
60
61pub struct PosixFs<B: FsBackend> {
91 backend: B,
92}
93
94impl<B: FsBackend> PosixFs<B> {
95 pub fn new(backend: B) -> Self {
97 Self { backend }
98 }
99
100 pub fn backend(&self) -> &B {
102 &self.backend
103 }
104
105 fn normalize(path: &Path) -> PathBuf {
107 normalize_path(path)
108 }
109
110 async fn check_parent_exists(&self, path: &Path) -> Result<()> {
112 if let Some(parent) = path.parent()
113 && parent != Path::new("/")
114 && parent != Path::new("")
115 && !self.backend.exists(parent).await?
116 {
117 return Err(fs_errors::parent_not_found());
118 }
119 Ok(())
120 }
121}
122
123#[async_trait]
124impl<B: FsBackend + 'static> FileSystem for PosixFs<B> {
125 async fn read_file(&self, path: &Path) -> Result<Vec<u8>> {
126 let path = Self::normalize(path);
127 if let Ok(meta) = self.backend.stat(&path).await
129 && meta.file_type.is_dir()
130 {
131 return Err(fs_errors::is_a_directory());
132 }
133 self.backend.read(&path).await
134 }
135
136 async fn write_file(&self, path: &Path, content: &[u8]) -> Result<()> {
137 let path = Self::normalize(path);
138 self.check_parent_exists(&path).await?;
140
141 if let Ok(meta) = self.backend.stat(&path).await
143 && meta.file_type.is_dir()
144 {
145 return Err(fs_errors::is_a_directory());
146 }
147
148 self.backend.write(&path, content).await
149 }
150
151 async fn append_file(&self, path: &Path, content: &[u8]) -> Result<()> {
152 let path = Self::normalize(path);
153 self.check_parent_exists(&path).await?;
155
156 if let Ok(meta) = self.backend.stat(&path).await
158 && meta.file_type.is_dir()
159 {
160 return Err(fs_errors::is_a_directory());
161 }
162
163 self.backend.append(&path, content).await
164 }
165
166 async fn mkdir(&self, path: &Path, recursive: bool) -> Result<()> {
167 let path = Self::normalize(path);
168 if let Ok(meta) = self.backend.stat(&path).await {
170 if meta.file_type.is_dir() {
171 if recursive {
173 return Ok(()); } else {
175 return Err(fs_errors::already_exists("directory exists"));
176 }
177 } else {
178 return Err(fs_errors::already_exists("file exists"));
180 }
181 }
182
183 if recursive {
184 if let Some(parent) = path.parent() {
186 let mut current = PathBuf::from("/");
187 for component in parent.components().skip(1) {
188 current.push(component);
189 if let Ok(meta) = self.backend.stat(¤t).await
190 && !meta.file_type.is_dir()
191 {
192 return Err(fs_errors::already_exists("file exists"));
193 }
194 }
195 }
196 } else {
197 self.check_parent_exists(&path).await?;
199 }
200
201 self.backend.mkdir(&path, recursive).await
202 }
203
204 async fn remove(&self, path: &Path, recursive: bool) -> Result<()> {
205 let path = Self::normalize(path);
206 self.backend.remove(&path, recursive).await
207 }
208
209 async fn stat(&self, path: &Path) -> Result<Metadata> {
210 let path = Self::normalize(path);
211 self.backend.stat(&path).await
212 }
213
214 async fn read_dir(&self, path: &Path) -> Result<Vec<DirEntry>> {
215 let path = Self::normalize(path);
216 if let Ok(meta) = self.backend.stat(&path).await
218 && !meta.file_type.is_dir()
219 {
220 return Err(fs_errors::not_a_directory());
221 }
222 self.backend.read_dir(&path).await
223 }
224
225 async fn exists(&self, path: &Path) -> Result<bool> {
226 let path = Self::normalize(path);
227 self.backend.exists(&path).await
228 }
229
230 async fn rename(&self, from: &Path, to: &Path) -> Result<()> {
231 let from = Self::normalize(from);
232 let to = Self::normalize(to);
233 self.backend.rename(&from, &to).await
234 }
235
236 async fn copy(&self, from: &Path, to: &Path) -> Result<()> {
237 let from = Self::normalize(from);
238 let to = Self::normalize(to);
239 if let Ok(meta) = self.backend.stat(&from).await
241 && meta.file_type.is_dir()
242 {
243 return Err(IoError::other("cannot copy directory").into());
244 }
245 self.backend.copy(&from, &to).await
246 }
247
248 async fn symlink(&self, target: &Path, link: &Path) -> Result<()> {
249 let link = Self::normalize(link);
252 self.backend.symlink(target, &link).await
253 }
254
255 async fn read_link(&self, path: &Path) -> Result<PathBuf> {
256 let path = Self::normalize(path);
257 self.backend.read_link(&path).await
258 }
259
260 async fn chmod(&self, path: &Path, mode: u32) -> Result<()> {
261 let path = Self::normalize(path);
262 self.backend.chmod(&path, mode).await
263 }
264
265 async fn set_modified_time(&self, path: &Path, time: SystemTime) -> Result<()> {
266 let path = Self::normalize(path);
267 self.backend.set_modified_time(&path, time).await
268 }
269}
270
271#[async_trait]
272impl<B: FsBackend + 'static> FileSystemExt for PosixFs<B> {
273 fn usage(&self) -> FsUsage {
274 self.backend.usage()
275 }
276
277 fn limits(&self) -> FsLimits {
278 self.backend.limits()
279 }
280}
281
282impl<B: FsBackend + 'static> From<PosixFs<B>> for Arc<dyn FileSystem> {
284 fn from(fs: PosixFs<B>) -> Self {
285 Arc::new(fs)
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::error::Result;
293 use crate::fs::InMemoryFs;
294 use crate::fs::{DirEntry, FileType, FsBackend};
295 use std::collections::HashSet;
296 use std::path::{Path, PathBuf};
297 use std::sync::Mutex;
298
299 struct AppendCreatesFileBackend {
300 files: Mutex<HashSet<PathBuf>>,
301 }
302
303 impl AppendCreatesFileBackend {
304 fn new() -> Self {
305 let mut files = HashSet::new();
306 files.insert(PathBuf::from("/"));
307 files.insert(PathBuf::from("/tmp"));
308 Self {
309 files: Mutex::new(files),
310 }
311 }
312 }
313
314 #[async_trait]
315 impl FsBackend for AppendCreatesFileBackend {
316 async fn read(&self, _path: &Path) -> Result<Vec<u8>> {
317 Ok(Vec::new())
318 }
319
320 async fn write(&self, path: &Path, _content: &[u8]) -> Result<()> {
321 self.files
322 .lock()
323 .expect("backend lock poisoned")
324 .insert(path.to_path_buf());
325 Ok(())
326 }
327
328 async fn append(&self, path: &Path, content: &[u8]) -> Result<()> {
329 self.write(path, content).await
330 }
331
332 async fn mkdir(&self, path: &Path, _recursive: bool) -> Result<()> {
333 self.files
334 .lock()
335 .expect("backend lock poisoned")
336 .insert(path.to_path_buf());
337 Ok(())
338 }
339
340 async fn remove(&self, _path: &Path, _recursive: bool) -> Result<()> {
341 Ok(())
342 }
343
344 async fn stat(&self, path: &Path) -> Result<Metadata> {
345 if self
346 .files
347 .lock()
348 .expect("backend lock poisoned")
349 .contains(path)
350 {
351 Ok(Metadata {
352 file_type: FileType::File,
353 ..Metadata::default()
354 })
355 } else {
356 Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
357 }
358 }
359
360 async fn read_dir(&self, _path: &Path) -> Result<Vec<DirEntry>> {
361 Ok(Vec::new())
362 }
363
364 async fn exists(&self, path: &Path) -> Result<bool> {
365 Ok(self
366 .files
367 .lock()
368 .expect("backend lock poisoned")
369 .contains(path))
370 }
371
372 async fn rename(&self, _from: &Path, _to: &Path) -> Result<()> {
373 Ok(())
374 }
375
376 async fn copy(&self, _from: &Path, _to: &Path) -> Result<()> {
377 Ok(())
378 }
379
380 async fn symlink(&self, _target: &Path, _link: &Path) -> Result<()> {
381 Ok(())
382 }
383
384 async fn read_link(&self, _path: &Path) -> Result<PathBuf> {
385 Err(std::io::Error::from(std::io::ErrorKind::NotFound).into())
386 }
387
388 async fn chmod(&self, _path: &Path, _mode: u32) -> Result<()> {
389 Ok(())
390 }
391 }
392
393 #[tokio::test]
394 async fn test_posix_write_to_directory_fails() {
395 let fs = InMemoryFs::new();
398
399 fs.mkdir(Path::new("/tmp/testdir"), false)
401 .await
402 .expect("mkdir should succeed");
403
404 let result = fs.write_file(Path::new("/tmp/testdir"), b"test").await;
406 assert!(result.is_err());
407 assert!(
408 result
409 .expect_err("write_file should fail")
410 .to_string()
411 .contains("directory")
412 );
413 }
414
415 #[tokio::test]
416 async fn test_posix_mkdir_on_file_fails() {
417 let fs = InMemoryFs::new();
418
419 fs.write_file(Path::new("/tmp/testfile"), b"test")
421 .await
422 .expect("write_file should succeed");
423
424 let result = fs.mkdir(Path::new("/tmp/testfile"), false).await;
426 assert!(result.is_err());
427 }
428
429 #[tokio::test]
430 async fn test_posix_normalize_dot_slash_prefix() {
431 let fs = InMemoryFs::new();
433
434 fs.mkdir(Path::new("/tmp/dir"), true).await.unwrap();
436 fs.write_file(Path::new("/tmp/dir/file.txt"), b"content")
437 .await
438 .unwrap();
439
440 let dot_path = Path::new("/tmp/dir/./file.txt");
442 assert!(
443 fs.exists(dot_path).await.unwrap(),
444 "exists with ./ should work"
445 );
446
447 let content = fs.read_file(dot_path).await.unwrap();
448 assert_eq!(content, b"content");
449
450 let meta = fs.stat(dot_path).await;
452 assert!(meta.is_ok(), "stat with ./ should work");
453
454 fs.write_file(Path::new("/tmp/dir/./new.txt"), b"new")
456 .await
457 .unwrap();
458 let content = fs.read_file(Path::new("/tmp/dir/new.txt")).await.unwrap();
459 assert_eq!(content, b"new");
460 }
461
462 #[tokio::test]
463 async fn test_posix_normalize_preserves_semantics() {
464 let fs = InMemoryFs::new();
466
467 let result = fs
469 .write_file(Path::new("/tmp/nonexistent/./file.txt"), b"content")
470 .await;
471 assert!(result.is_err(), "should fail when parent doesn't exist");
472 }
473
474 #[tokio::test]
475 async fn test_posix_append_requires_parent_directory() {
476 let fs = PosixFs::new(AppendCreatesFileBackend::new());
477 let result = fs
478 .append_file(Path::new("/tmp/missing-parent/file.txt"), b"content")
479 .await;
480 assert!(
481 result.is_err(),
482 "append should fail when parent doesn't exist"
483 );
484 }
485}