Skip to main content

fresh/model/
filesystem.rs

1//! Unified filesystem abstraction for platform-independent file and directory operations
2//!
3//! This module provides a single trait for all filesystem operations, allowing the editor
4//! to work with different backends:
5//! - `StdFileSystem`: Native filesystem using `std::fs`
6//! - `VirtualFileSystem`: In-memory filesystem for WASM/browser (to be implemented)
7//! - Custom implementations for remote agents, network filesystems, etc.
8//!
9//! The trait is synchronous. For async UI operations (like the file explorer),
10//! callers should use `spawn_blocking` or similar patterns.
11
12use std::io::{self, Read, Seek, Write};
13use std::path::{Path, PathBuf};
14use std::time::SystemTime;
15
16// ============================================================================
17// Directory Entry Types
18// ============================================================================
19
20/// Type of filesystem entry
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum EntryType {
23    File,
24    Directory,
25    Symlink,
26}
27
28/// A directory entry returned by `read_dir`
29#[derive(Debug, Clone)]
30pub struct DirEntry {
31    /// Full path to the entry
32    pub path: PathBuf,
33    /// File/directory name (last component of path)
34    pub name: String,
35    /// Type of entry
36    pub entry_type: EntryType,
37    /// Optional metadata (can be populated lazily)
38    pub metadata: Option<FileMetadata>,
39    /// For symlinks, whether the target is a directory
40    pub symlink_target_is_dir: bool,
41}
42
43impl DirEntry {
44    /// Create a new directory entry
45    pub fn new(path: PathBuf, name: String, entry_type: EntryType) -> Self {
46        Self {
47            path,
48            name,
49            entry_type,
50            metadata: None,
51            symlink_target_is_dir: false,
52        }
53    }
54
55    /// Create a symlink entry with target info
56    pub fn new_symlink(path: PathBuf, name: String, target_is_dir: bool) -> Self {
57        Self {
58            path,
59            name,
60            entry_type: EntryType::Symlink,
61            metadata: None,
62            symlink_target_is_dir: target_is_dir,
63        }
64    }
65
66    /// Add metadata to this entry
67    pub fn with_metadata(mut self, metadata: FileMetadata) -> Self {
68        self.metadata = Some(metadata);
69        self
70    }
71
72    /// Returns true if this entry is a directory OR a symlink pointing to a directory
73    pub fn is_dir(&self) -> bool {
74        self.entry_type == EntryType::Directory
75            || (self.entry_type == EntryType::Symlink && self.symlink_target_is_dir)
76    }
77
78    /// Returns true if this is a regular file (or symlink to file)
79    pub fn is_file(&self) -> bool {
80        self.entry_type == EntryType::File
81            || (self.entry_type == EntryType::Symlink && !self.symlink_target_is_dir)
82    }
83
84    /// Returns true if this is a symlink
85    pub fn is_symlink(&self) -> bool {
86        self.entry_type == EntryType::Symlink
87    }
88}
89
90// ============================================================================
91// Metadata Types
92// ============================================================================
93
94/// Metadata about a file or directory
95#[derive(Debug, Clone)]
96pub struct FileMetadata {
97    /// Size in bytes (0 for directories)
98    pub size: u64,
99    /// Last modification time
100    pub modified: Option<SystemTime>,
101    /// File permissions (opaque, platform-specific)
102    pub permissions: Option<FilePermissions>,
103    /// Whether this is a hidden file (starts with . on Unix, hidden attribute on Windows)
104    pub is_hidden: bool,
105    /// Whether the file is read-only
106    pub is_readonly: bool,
107    /// File owner UID (Unix only)
108    #[cfg(unix)]
109    pub uid: Option<u32>,
110    /// File owner GID (Unix only)
111    #[cfg(unix)]
112    pub gid: Option<u32>,
113}
114
115impl FileMetadata {
116    /// Create minimal metadata with just size
117    pub fn new(size: u64) -> Self {
118        Self {
119            size,
120            modified: None,
121            permissions: None,
122            is_hidden: false,
123            is_readonly: false,
124            #[cfg(unix)]
125            uid: None,
126            #[cfg(unix)]
127            gid: None,
128        }
129    }
130
131    /// Builder: set modified time
132    pub fn with_modified(mut self, modified: SystemTime) -> Self {
133        self.modified = Some(modified);
134        self
135    }
136
137    /// Builder: set hidden flag
138    pub fn with_hidden(mut self, hidden: bool) -> Self {
139        self.is_hidden = hidden;
140        self
141    }
142
143    /// Builder: set readonly flag
144    pub fn with_readonly(mut self, readonly: bool) -> Self {
145        self.is_readonly = readonly;
146        self
147    }
148
149    /// Builder: set permissions
150    pub fn with_permissions(mut self, permissions: FilePermissions) -> Self {
151        self.permissions = Some(permissions);
152        self
153    }
154}
155
156impl Default for FileMetadata {
157    fn default() -> Self {
158        Self::new(0)
159    }
160}
161
162/// Opaque file permissions wrapper
163#[derive(Debug, Clone)]
164pub struct FilePermissions {
165    #[cfg(unix)]
166    mode: u32,
167    #[cfg(not(unix))]
168    readonly: bool,
169}
170
171impl FilePermissions {
172    /// Create from raw Unix mode bits
173    #[cfg(unix)]
174    pub fn from_mode(mode: u32) -> Self {
175        Self { mode }
176    }
177
178    /// Create from raw mode (non-Unix: treated as readonly if no write bits)
179    #[cfg(not(unix))]
180    pub fn from_mode(mode: u32) -> Self {
181        Self {
182            readonly: mode & 0o222 == 0,
183        }
184    }
185
186    /// Create from std::fs::Permissions
187    #[cfg(unix)]
188    pub fn from_std(perms: std::fs::Permissions) -> Self {
189        use std::os::unix::fs::PermissionsExt;
190        Self { mode: perms.mode() }
191    }
192
193    #[cfg(not(unix))]
194    pub fn from_std(perms: std::fs::Permissions) -> Self {
195        Self {
196            readonly: perms.readonly(),
197        }
198    }
199
200    /// Convert to std::fs::Permissions
201    #[cfg(unix)]
202    pub fn to_std(&self) -> std::fs::Permissions {
203        use std::os::unix::fs::PermissionsExt;
204        std::fs::Permissions::from_mode(self.mode)
205    }
206
207    #[cfg(not(unix))]
208    pub fn to_std(&self) -> std::fs::Permissions {
209        let mut perms = std::fs::Permissions::from(std::fs::metadata(".").unwrap().permissions());
210        perms.set_readonly(self.readonly);
211        perms
212    }
213
214    /// Get the Unix mode (if available)
215    #[cfg(unix)]
216    pub fn mode(&self) -> u32 {
217        self.mode
218    }
219
220    /// Check if readonly
221    pub fn is_readonly(&self) -> bool {
222        #[cfg(unix)]
223        {
224            self.mode & 0o222 == 0
225        }
226        #[cfg(not(unix))]
227        {
228            self.readonly
229        }
230    }
231}
232
233// ============================================================================
234// File Handle Traits
235// ============================================================================
236
237/// A writable file handle
238pub trait FileWriter: Write + Send {
239    /// Sync all data to disk
240    fn sync_all(&self) -> io::Result<()>;
241}
242
243// ============================================================================
244// Patch Operations for Efficient Remote Saves
245// ============================================================================
246
247/// An operation in a patched write - either copy from source or insert new data
248#[derive(Debug, Clone)]
249pub enum WriteOp<'a> {
250    /// Copy bytes from the source file at the given offset
251    Copy { offset: u64, len: u64 },
252    /// Insert new data
253    Insert { data: &'a [u8] },
254}
255
256/// Wrapper around std::fs::File that implements FileWriter
257struct StdFileWriter(std::fs::File);
258
259impl Write for StdFileWriter {
260    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
261        self.0.write(buf)
262    }
263
264    fn flush(&mut self) -> io::Result<()> {
265        self.0.flush()
266    }
267}
268
269impl FileWriter for StdFileWriter {
270    fn sync_all(&self) -> io::Result<()> {
271        self.0.sync_all()
272    }
273}
274
275/// A readable and seekable file handle
276pub trait FileReader: Read + Seek + Send {}
277
278/// Wrapper around std::fs::File that implements FileReader
279struct StdFileReader(std::fs::File);
280
281impl Read for StdFileReader {
282    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
283        self.0.read(buf)
284    }
285}
286
287impl Seek for StdFileReader {
288    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
289        self.0.seek(pos)
290    }
291}
292
293impl FileReader for StdFileReader {}
294
295// ============================================================================
296// FileSystem Trait
297// ============================================================================
298
299/// Unified trait for all filesystem operations
300///
301/// This trait provides both file content I/O and directory operations.
302/// Implementations can be:
303/// - `StdFileSystem`: Native filesystem using `std::fs`
304/// - `VirtualFileSystem`: In-memory for WASM/browser
305/// - Custom backends for remote agents, network filesystems, etc.
306///
307/// All methods are synchronous. For async UI operations, use `spawn_blocking`.
308pub trait FileSystem: Send + Sync {
309    // ========================================================================
310    // File Content Operations
311    // ========================================================================
312
313    /// Read entire file into memory
314    fn read_file(&self, path: &Path) -> io::Result<Vec<u8>>;
315
316    /// Read a range of bytes from a file (for lazy loading large files)
317    fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>>;
318
319    /// Write data to file atomically (temp file + rename)
320    fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()>;
321
322    /// Create a file for writing, returns a writer handle
323    fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
324
325    /// Open a file for reading, returns a reader handle
326    fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>>;
327
328    /// Open a file for writing in-place (truncating, preserves ownership on Unix)
329    fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
330
331    /// Open a file for appending (creates if doesn't exist)
332    fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
333
334    /// Set file length (truncate or extend with zeros)
335    fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()>;
336
337    /// Write a file using a patch recipe (optimized for remote filesystems).
338    ///
339    /// This allows saving edited files by specifying which parts to copy from
340    /// the original and which parts are new content. For remote filesystems,
341    /// this avoids transferring unchanged portions over the network.
342    ///
343    /// # Arguments
344    /// * `src_path` - The original file to read from (for Copy operations)
345    /// * `dst_path` - The destination file (often same as src_path)
346    /// * `ops` - The sequence of operations to build the new file
347    ///
348    /// The default implementation flattens all operations into memory and
349    /// calls `write_file`. Remote implementations can override this to send
350    /// the recipe and let the remote host do the reconstruction.
351    fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
352        // Default implementation: flatten to buffer and write
353        let mut buffer = Vec::new();
354        for op in ops {
355            match op {
356                WriteOp::Copy { offset, len } => {
357                    let data = self.read_range(src_path, *offset, *len as usize)?;
358                    buffer.extend_from_slice(&data);
359                }
360                WriteOp::Insert { data } => {
361                    buffer.extend_from_slice(data);
362                }
363            }
364        }
365        self.write_file(dst_path, &buffer)
366    }
367
368    // ========================================================================
369    // File Operations
370    // ========================================================================
371
372    /// Rename/move a file or directory atomically
373    fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
374
375    /// Copy a file (fallback when rename fails across filesystems)
376    fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
377
378    /// Remove a file
379    fn remove_file(&self, path: &Path) -> io::Result<()>;
380
381    /// Remove an empty directory
382    fn remove_dir(&self, path: &Path) -> io::Result<()>;
383
384    // ========================================================================
385    // Metadata Operations
386    // ========================================================================
387
388    /// Get file/directory metadata
389    fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
390
391    /// Get symlink metadata (doesn't follow symlinks)
392    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
393
394    /// Check if path exists
395    fn exists(&self, path: &Path) -> bool {
396        self.metadata(path).is_ok()
397    }
398
399    /// Check if path exists, returns metadata if it does
400    fn metadata_if_exists(&self, path: &Path) -> Option<FileMetadata> {
401        self.metadata(path).ok()
402    }
403
404    /// Check if path is a directory
405    fn is_dir(&self, path: &Path) -> io::Result<bool>;
406
407    /// Check if path is a file
408    fn is_file(&self, path: &Path) -> io::Result<bool>;
409
410    /// Set file permissions
411    fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()>;
412
413    // ========================================================================
414    // Directory Operations
415    // ========================================================================
416
417    /// List entries in a directory (non-recursive)
418    fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
419
420    /// Create a directory
421    fn create_dir(&self, path: &Path) -> io::Result<()>;
422
423    /// Create a directory and all parent directories
424    fn create_dir_all(&self, path: &Path) -> io::Result<()>;
425
426    // ========================================================================
427    // Path Operations
428    // ========================================================================
429
430    /// Get canonical (absolute, normalized) path
431    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
432
433    // ========================================================================
434    // Utility Methods
435    // ========================================================================
436
437    /// Get the current user's UID (Unix only, returns 0 on other platforms)
438    fn current_uid(&self) -> u32;
439
440    /// Check if the current user is the owner of the file
441    fn is_owner(&self, path: &Path) -> bool {
442        #[cfg(unix)]
443        {
444            if let Ok(meta) = self.metadata(path) {
445                if let Some(uid) = meta.uid {
446                    return uid == self.current_uid();
447                }
448            }
449            true
450        }
451        #[cfg(not(unix))]
452        {
453            let _ = path;
454            true
455        }
456    }
457
458    /// Get a temporary file path for atomic writes
459    fn temp_path_for(&self, path: &Path) -> PathBuf {
460        path.with_extension("tmp")
461    }
462
463    /// Get a unique temporary file path (using timestamp and PID)
464    fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
465        let temp_dir = std::env::temp_dir();
466        let file_name = dest_path
467            .file_name()
468            .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
469        let timestamp = std::time::SystemTime::now()
470            .duration_since(std::time::UNIX_EPOCH)
471            .map(|d| d.as_nanos())
472            .unwrap_or(0);
473        temp_dir.join(format!(
474            "{}-{}-{}.tmp",
475            file_name.to_string_lossy(),
476            std::process::id(),
477            timestamp
478        ))
479    }
480
481    // ========================================================================
482    // Remote Connection Info
483    // ========================================================================
484
485    /// Get remote connection info if this is a remote filesystem
486    ///
487    /// Returns `Some("user@host")` for remote filesystems, `None` for local.
488    /// Used to display remote connection status in the UI.
489    fn remote_connection_info(&self) -> Option<&str> {
490        None
491    }
492
493    /// Get the home directory for this filesystem
494    ///
495    /// For local filesystems, returns the local home directory.
496    /// For remote filesystems, returns the remote home directory.
497    fn home_dir(&self) -> io::Result<PathBuf> {
498        dirs::home_dir()
499            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
500    }
501
502    /// Write file using sudo (for root-owned files).
503    ///
504    /// This writes the file with elevated privileges, preserving the specified
505    /// permissions and ownership. Used when normal write fails due to permissions.
506    ///
507    /// - `path`: Destination file path
508    /// - `data`: File contents to write
509    /// - `mode`: File permissions (e.g., 0o644)
510    /// - `uid`: Owner user ID
511    /// - `gid`: Owner group ID
512    fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
513        -> io::Result<()>;
514}
515
516// ============================================================================
517// FileSystemExt - Async Extension Trait
518// ============================================================================
519
520/// Async extension trait for FileSystem
521///
522/// This trait provides async versions of FileSystem methods using native
523/// Rust async fn (no async_trait crate needed). Default implementations
524/// simply call the sync methods, which works for local filesystem operations.
525///
526/// For truly async backends (network FS, remote agents), implementations
527/// can override these methods with actual async implementations.
528///
529/// Note: This trait is NOT object-safe due to async fn. Use generics
530/// (`impl FileSystem` or `F: FileSystem`) instead of `dyn FileSystem`
531/// when async methods are needed.
532///
533/// # Example
534///
535/// ```ignore
536/// async fn list_files<F: FileSystem>(fs: &F, path: &Path) -> io::Result<Vec<DirEntry>> {
537///     fs.read_dir_async(path).await
538/// }
539/// ```
540pub trait FileSystemExt: FileSystem {
541    /// Async version of read_file
542    fn read_file_async(
543        &self,
544        path: &Path,
545    ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
546        async { self.read_file(path) }
547    }
548
549    /// Async version of read_range
550    fn read_range_async(
551        &self,
552        path: &Path,
553        offset: u64,
554        len: usize,
555    ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
556        async move { self.read_range(path, offset, len) }
557    }
558
559    /// Async version of write_file
560    fn write_file_async(
561        &self,
562        path: &Path,
563        data: &[u8],
564    ) -> impl std::future::Future<Output = io::Result<()>> + Send {
565        async { self.write_file(path, data) }
566    }
567
568    /// Async version of metadata
569    fn metadata_async(
570        &self,
571        path: &Path,
572    ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
573        async { self.metadata(path) }
574    }
575
576    /// Async version of exists
577    fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
578        async { self.exists(path) }
579    }
580
581    /// Async version of is_dir
582    fn is_dir_async(
583        &self,
584        path: &Path,
585    ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
586        async { self.is_dir(path) }
587    }
588
589    /// Async version of is_file
590    fn is_file_async(
591        &self,
592        path: &Path,
593    ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
594        async { self.is_file(path) }
595    }
596
597    /// Async version of read_dir
598    fn read_dir_async(
599        &self,
600        path: &Path,
601    ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
602        async { self.read_dir(path) }
603    }
604
605    /// Async version of canonicalize
606    fn canonicalize_async(
607        &self,
608        path: &Path,
609    ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
610        async { self.canonicalize(path) }
611    }
612}
613
614/// Blanket implementation: all FileSystem types automatically get async methods
615impl<T: FileSystem> FileSystemExt for T {}
616
617// ============================================================================
618// StdFileSystem Implementation
619// ============================================================================
620
621/// Standard filesystem implementation using `std::fs`
622///
623/// This is the default implementation for native builds.
624#[derive(Debug, Clone, Copy, Default)]
625pub struct StdFileSystem;
626
627impl StdFileSystem {
628    /// Check if a file is hidden (platform-specific)
629    fn is_hidden(path: &Path) -> bool {
630        path.file_name()
631            .and_then(|n| n.to_str())
632            .is_some_and(|n| n.starts_with('.'))
633    }
634
635    /// Build FileMetadata from std::fs::Metadata
636    fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
637        #[cfg(unix)]
638        {
639            use std::os::unix::fs::MetadataExt;
640            FileMetadata {
641                size: meta.len(),
642                modified: meta.modified().ok(),
643                permissions: Some(FilePermissions::from_std(meta.permissions())),
644                is_hidden: Self::is_hidden(path),
645                is_readonly: meta.permissions().readonly(),
646                uid: Some(meta.uid()),
647                gid: Some(meta.gid()),
648            }
649        }
650        #[cfg(not(unix))]
651        {
652            FileMetadata {
653                size: meta.len(),
654                modified: meta.modified().ok(),
655                permissions: Some(FilePermissions::from_std(meta.permissions())),
656                is_hidden: Self::is_hidden(path),
657                is_readonly: meta.permissions().readonly(),
658            }
659        }
660    }
661}
662
663impl FileSystem for StdFileSystem {
664    // File Content Operations
665    fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
666        std::fs::read(path)
667    }
668
669    fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
670        let mut file = std::fs::File::open(path)?;
671        file.seek(io::SeekFrom::Start(offset))?;
672        let mut buffer = vec![0u8; len];
673        file.read_exact(&mut buffer)?;
674        Ok(buffer)
675    }
676
677    fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
678        let original_metadata = self.metadata_if_exists(path);
679        let temp_path = self.temp_path_for(path);
680        {
681            let mut file = self.create_file(&temp_path)?;
682            file.write_all(data)?;
683            file.sync_all()?;
684        }
685        if let Some(ref meta) = original_metadata {
686            if let Some(ref perms) = meta.permissions {
687                let _ = self.set_permissions(&temp_path, perms);
688            }
689        }
690        self.rename(&temp_path, path)?;
691        Ok(())
692    }
693
694    fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
695        let file = std::fs::File::create(path)?;
696        Ok(Box::new(StdFileWriter(file)))
697    }
698
699    fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
700        let file = std::fs::File::open(path)?;
701        Ok(Box::new(StdFileReader(file)))
702    }
703
704    fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
705        let file = std::fs::OpenOptions::new()
706            .write(true)
707            .truncate(true)
708            .open(path)?;
709        Ok(Box::new(StdFileWriter(file)))
710    }
711
712    fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
713        let file = std::fs::OpenOptions::new()
714            .create(true)
715            .append(true)
716            .open(path)?;
717        Ok(Box::new(StdFileWriter(file)))
718    }
719
720    fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
721        let file = std::fs::OpenOptions::new().write(true).open(path)?;
722        file.set_len(len)
723    }
724
725    // File Operations
726    fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
727        std::fs::rename(from, to)
728    }
729
730    fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
731        std::fs::copy(from, to)
732    }
733
734    fn remove_file(&self, path: &Path) -> io::Result<()> {
735        std::fs::remove_file(path)
736    }
737
738    fn remove_dir(&self, path: &Path) -> io::Result<()> {
739        std::fs::remove_dir(path)
740    }
741
742    // Metadata Operations
743    fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
744        let meta = std::fs::metadata(path)?;
745        Ok(Self::build_metadata(path, &meta))
746    }
747
748    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
749        let meta = std::fs::symlink_metadata(path)?;
750        Ok(Self::build_metadata(path, &meta))
751    }
752
753    fn is_dir(&self, path: &Path) -> io::Result<bool> {
754        Ok(std::fs::metadata(path)?.is_dir())
755    }
756
757    fn is_file(&self, path: &Path) -> io::Result<bool> {
758        Ok(std::fs::metadata(path)?.is_file())
759    }
760
761    fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
762        std::fs::set_permissions(path, permissions.to_std())
763    }
764
765    // Directory Operations
766    fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
767        let mut entries = Vec::new();
768        for entry in std::fs::read_dir(path)? {
769            let entry = entry?;
770            let path = entry.path();
771            let name = entry.file_name().to_string_lossy().into_owned();
772            let file_type = entry.file_type()?;
773
774            let entry_type = if file_type.is_dir() {
775                EntryType::Directory
776            } else if file_type.is_symlink() {
777                EntryType::Symlink
778            } else {
779                EntryType::File
780            };
781
782            let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
783
784            // For symlinks, check if target is a directory
785            if file_type.is_symlink() {
786                dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
787                    .map(|m| m.is_dir())
788                    .unwrap_or(false);
789            }
790
791            entries.push(dir_entry);
792        }
793        Ok(entries)
794    }
795
796    fn create_dir(&self, path: &Path) -> io::Result<()> {
797        std::fs::create_dir(path)
798    }
799
800    fn create_dir_all(&self, path: &Path) -> io::Result<()> {
801        std::fs::create_dir_all(path)
802    }
803
804    // Path Operations
805    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
806        std::fs::canonicalize(path)
807    }
808
809    // Utility
810    fn current_uid(&self) -> u32 {
811        #[cfg(all(unix, feature = "runtime"))]
812        {
813            // SAFETY: getuid() is a simple syscall with no arguments
814            unsafe { libc::getuid() }
815        }
816        #[cfg(not(all(unix, feature = "runtime")))]
817        {
818            0
819        }
820    }
821
822    fn sudo_write(
823        &self,
824        path: &Path,
825        data: &[u8],
826        mode: u32,
827        uid: u32,
828        gid: u32,
829    ) -> io::Result<()> {
830        use std::process::{Command, Stdio};
831
832        // Write data via sudo tee
833        let mut child = Command::new("sudo")
834            .args(["tee", &path.to_string_lossy()])
835            .stdin(Stdio::piped())
836            .stdout(Stdio::null())
837            .stderr(Stdio::piped())
838            .spawn()
839            .map_err(|e| {
840                io::Error::new(io::ErrorKind::Other, format!("failed to spawn sudo: {}", e))
841            })?;
842
843        if let Some(mut stdin) = child.stdin.take() {
844            use std::io::Write;
845            stdin.write_all(data)?;
846        }
847
848        let output = child.wait_with_output()?;
849        if !output.status.success() {
850            let stderr = String::from_utf8_lossy(&output.stderr);
851            return Err(io::Error::new(
852                io::ErrorKind::PermissionDenied,
853                format!("sudo tee failed: {}", stderr.trim()),
854            ));
855        }
856
857        // Set permissions via sudo chmod
858        let status = Command::new("sudo")
859            .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
860            .status()?;
861        if !status.success() {
862            return Err(io::Error::new(io::ErrorKind::Other, "sudo chmod failed"));
863        }
864
865        // Set ownership via sudo chown
866        let status = Command::new("sudo")
867            .args([
868                "chown",
869                &format!("{}:{}", uid, gid),
870                &path.to_string_lossy(),
871            ])
872            .status()?;
873        if !status.success() {
874            return Err(io::Error::new(io::ErrorKind::Other, "sudo chown failed"));
875        }
876
877        Ok(())
878    }
879}
880
881// ============================================================================
882// NoopFileSystem Implementation
883// ============================================================================
884
885/// No-op filesystem that returns errors for all operations
886///
887/// Used as a placeholder or in WASM builds where a VirtualFileSystem
888/// should be used instead.
889#[derive(Debug, Clone, Copy, Default)]
890pub struct NoopFileSystem;
891
892impl NoopFileSystem {
893    fn unsupported<T>() -> io::Result<T> {
894        Err(io::Error::new(
895            io::ErrorKind::Unsupported,
896            "Filesystem not available",
897        ))
898    }
899}
900
901impl FileSystem for NoopFileSystem {
902    fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
903        Self::unsupported()
904    }
905
906    fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
907        Self::unsupported()
908    }
909
910    fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
911        Self::unsupported()
912    }
913
914    fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
915        Self::unsupported()
916    }
917
918    fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
919        Self::unsupported()
920    }
921
922    fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
923        Self::unsupported()
924    }
925
926    fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
927        Self::unsupported()
928    }
929
930    fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
931        Self::unsupported()
932    }
933
934    fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
935        Self::unsupported()
936    }
937
938    fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
939        Self::unsupported()
940    }
941
942    fn remove_file(&self, _path: &Path) -> io::Result<()> {
943        Self::unsupported()
944    }
945
946    fn remove_dir(&self, _path: &Path) -> io::Result<()> {
947        Self::unsupported()
948    }
949
950    fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
951        Self::unsupported()
952    }
953
954    fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
955        Self::unsupported()
956    }
957
958    fn is_dir(&self, _path: &Path) -> io::Result<bool> {
959        Self::unsupported()
960    }
961
962    fn is_file(&self, _path: &Path) -> io::Result<bool> {
963        Self::unsupported()
964    }
965
966    fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
967        Self::unsupported()
968    }
969
970    fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
971        Self::unsupported()
972    }
973
974    fn create_dir(&self, _path: &Path) -> io::Result<()> {
975        Self::unsupported()
976    }
977
978    fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
979        Self::unsupported()
980    }
981
982    fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
983        Self::unsupported()
984    }
985
986    fn current_uid(&self) -> u32 {
987        0
988    }
989
990    fn sudo_write(
991        &self,
992        _path: &Path,
993        _data: &[u8],
994        _mode: u32,
995        _uid: u32,
996        _gid: u32,
997    ) -> io::Result<()> {
998        Self::unsupported()
999    }
1000}
1001
1002// ============================================================================
1003// Tests
1004// ============================================================================
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use tempfile::NamedTempFile;
1010
1011    #[test]
1012    fn test_std_filesystem_read_write() {
1013        let fs = StdFileSystem;
1014        let mut temp = NamedTempFile::new().unwrap();
1015        let path = temp.path().to_path_buf();
1016
1017        std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1018        std::io::Write::flush(&mut temp).unwrap();
1019
1020        let content = fs.read_file(&path).unwrap();
1021        assert_eq!(content, b"Hello, World!");
1022
1023        let range = fs.read_range(&path, 7, 5).unwrap();
1024        assert_eq!(range, b"World");
1025
1026        let meta = fs.metadata(&path).unwrap();
1027        assert_eq!(meta.size, 13);
1028    }
1029
1030    #[test]
1031    fn test_noop_filesystem() {
1032        let fs = NoopFileSystem;
1033        let path = Path::new("/some/path");
1034
1035        assert!(fs.read_file(path).is_err());
1036        assert!(fs.read_range(path, 0, 10).is_err());
1037        assert!(fs.write_file(path, b"data").is_err());
1038        assert!(fs.metadata(path).is_err());
1039        assert!(fs.read_dir(path).is_err());
1040    }
1041
1042    #[test]
1043    fn test_create_and_write_file() {
1044        let fs = StdFileSystem;
1045        let temp_dir = tempfile::tempdir().unwrap();
1046        let path = temp_dir.path().join("test.txt");
1047
1048        {
1049            let mut writer = fs.create_file(&path).unwrap();
1050            writer.write_all(b"test content").unwrap();
1051            writer.sync_all().unwrap();
1052        }
1053
1054        let content = fs.read_file(&path).unwrap();
1055        assert_eq!(content, b"test content");
1056    }
1057
1058    #[test]
1059    fn test_read_dir() {
1060        let fs = StdFileSystem;
1061        let temp_dir = tempfile::tempdir().unwrap();
1062
1063        // Create some files and directories
1064        fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1065        fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1066            .unwrap();
1067        fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1068            .unwrap();
1069
1070        let entries = fs.read_dir(temp_dir.path()).unwrap();
1071        assert_eq!(entries.len(), 3);
1072
1073        let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1074        assert!(names.contains(&"subdir"));
1075        assert!(names.contains(&"file1.txt"));
1076        assert!(names.contains(&"file2.txt"));
1077    }
1078
1079    #[test]
1080    fn test_dir_entry_types() {
1081        let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1082        assert!(file.is_file());
1083        assert!(!file.is_dir());
1084
1085        let dir = DirEntry::new(
1086            PathBuf::from("/dir"),
1087            "dir".to_string(),
1088            EntryType::Directory,
1089        );
1090        assert!(dir.is_dir());
1091        assert!(!dir.is_file());
1092
1093        let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1094        assert!(link_to_dir.is_symlink());
1095        assert!(link_to_dir.is_dir());
1096    }
1097
1098    #[test]
1099    fn test_metadata_builder() {
1100        let meta = FileMetadata::default()
1101            .with_hidden(true)
1102            .with_readonly(true);
1103        assert!(meta.is_hidden);
1104        assert!(meta.is_readonly);
1105    }
1106
1107    #[test]
1108    fn test_atomic_write() {
1109        let fs = StdFileSystem;
1110        let temp_dir = tempfile::tempdir().unwrap();
1111        let path = temp_dir.path().join("atomic_test.txt");
1112
1113        fs.write_file(&path, b"initial").unwrap();
1114        assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1115
1116        fs.write_file(&path, b"updated").unwrap();
1117        assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1118    }
1119
1120    #[test]
1121    fn test_write_patched_default_impl() {
1122        // Test that the default write_patched implementation works correctly
1123        let fs = StdFileSystem;
1124        let temp_dir = tempfile::tempdir().unwrap();
1125        let src_path = temp_dir.path().join("source.txt");
1126        let dst_path = temp_dir.path().join("dest.txt");
1127
1128        // Create source file with known content
1129        fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1130
1131        // Apply patch: copy first 3 bytes, insert "XXX", copy last 3 bytes
1132        let ops = vec![
1133            WriteOp::Copy { offset: 0, len: 3 }, // "AAA"
1134            WriteOp::Insert { data: b"XXX" },    // "XXX"
1135            WriteOp::Copy { offset: 6, len: 3 }, // "CCC"
1136        ];
1137
1138        fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1139
1140        let result = fs.read_file(&dst_path).unwrap();
1141        assert_eq!(result, b"AAAXXXCCC");
1142    }
1143
1144    #[test]
1145    fn test_write_patched_same_file() {
1146        // Test patching a file in-place (src == dst)
1147        let fs = StdFileSystem;
1148        let temp_dir = tempfile::tempdir().unwrap();
1149        let path = temp_dir.path().join("file.txt");
1150
1151        // Create file
1152        fs.write_file(&path, b"Hello World").unwrap();
1153
1154        // Replace "World" with "Rust"
1155        let ops = vec![
1156            WriteOp::Copy { offset: 0, len: 6 }, // "Hello "
1157            WriteOp::Insert { data: b"Rust" },   // "Rust"
1158        ];
1159
1160        fs.write_patched(&path, &path, &ops).unwrap();
1161
1162        let result = fs.read_file(&path).unwrap();
1163        assert_eq!(result, b"Hello Rust");
1164    }
1165
1166    #[test]
1167    fn test_write_patched_insert_only() {
1168        // Test a patch with only inserts (new file)
1169        let fs = StdFileSystem;
1170        let temp_dir = tempfile::tempdir().unwrap();
1171        let src_path = temp_dir.path().join("empty.txt");
1172        let dst_path = temp_dir.path().join("new.txt");
1173
1174        // Create empty source (won't be read from)
1175        fs.write_file(&src_path, b"").unwrap();
1176
1177        let ops = vec![WriteOp::Insert {
1178            data: b"All new content",
1179        }];
1180
1181        fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1182
1183        let result = fs.read_file(&dst_path).unwrap();
1184        assert_eq!(result, b"All new content");
1185    }
1186}