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    /// Count `\n` bytes in a file range without returning the data.
320    ///
321    /// Used by the line-feed scanner to count newlines in unloaded chunks.
322    /// Remote filesystem implementations can override this to count on the
323    /// server side, avoiding the transfer of chunk bytes over the network.
324    ///
325    /// The default implementation reads the range and counts locally.
326    fn count_line_feeds_in_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<usize> {
327        let data = self.read_range(path, offset, len)?;
328        Ok(data.iter().filter(|&&b| b == b'\n').count())
329    }
330
331    /// Write data to file atomically (temp file + rename)
332    fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()>;
333
334    /// Create a file for writing, returns a writer handle
335    fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
336
337    /// Open a file for reading, returns a reader handle
338    fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>>;
339
340    /// Open a file for writing in-place (truncating, preserves ownership on Unix)
341    fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
342
343    /// Open a file for appending (creates if doesn't exist)
344    fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>>;
345
346    /// Set file length (truncate or extend with zeros)
347    fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()>;
348
349    /// Write a file using a patch recipe (optimized for remote filesystems).
350    ///
351    /// This allows saving edited files by specifying which parts to copy from
352    /// the original and which parts are new content. For remote filesystems,
353    /// this avoids transferring unchanged portions over the network.
354    ///
355    /// # Arguments
356    /// * `src_path` - The original file to read from (for Copy operations)
357    /// * `dst_path` - The destination file (often same as src_path)
358    /// * `ops` - The sequence of operations to build the new file
359    ///
360    /// The default implementation flattens all operations into memory and
361    /// calls `write_file`. Remote implementations can override this to send
362    /// the recipe and let the remote host do the reconstruction.
363    fn write_patched(&self, src_path: &Path, dst_path: &Path, ops: &[WriteOp]) -> io::Result<()> {
364        // Default implementation: flatten to buffer and write
365        let mut buffer = Vec::new();
366        for op in ops {
367            match op {
368                WriteOp::Copy { offset, len } => {
369                    let data = self.read_range(src_path, *offset, *len as usize)?;
370                    buffer.extend_from_slice(&data);
371                }
372                WriteOp::Insert { data } => {
373                    buffer.extend_from_slice(data);
374                }
375            }
376        }
377        self.write_file(dst_path, &buffer)
378    }
379
380    // ========================================================================
381    // File Operations
382    // ========================================================================
383
384    /// Rename/move a file or directory atomically
385    fn rename(&self, from: &Path, to: &Path) -> io::Result<()>;
386
387    /// Copy a file (fallback when rename fails across filesystems)
388    fn copy(&self, from: &Path, to: &Path) -> io::Result<u64>;
389
390    /// Remove a file
391    fn remove_file(&self, path: &Path) -> io::Result<()>;
392
393    /// Remove an empty directory
394    fn remove_dir(&self, path: &Path) -> io::Result<()>;
395
396    // ========================================================================
397    // Metadata Operations
398    // ========================================================================
399
400    /// Get file/directory metadata
401    fn metadata(&self, path: &Path) -> io::Result<FileMetadata>;
402
403    /// Get symlink metadata (doesn't follow symlinks)
404    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata>;
405
406    /// Check if path exists
407    fn exists(&self, path: &Path) -> bool {
408        self.metadata(path).is_ok()
409    }
410
411    /// Check if path exists, returns metadata if it does
412    fn metadata_if_exists(&self, path: &Path) -> Option<FileMetadata> {
413        self.metadata(path).ok()
414    }
415
416    /// Check if path is a directory
417    fn is_dir(&self, path: &Path) -> io::Result<bool>;
418
419    /// Check if path is a file
420    fn is_file(&self, path: &Path) -> io::Result<bool>;
421
422    /// Set file permissions
423    fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()>;
424
425    // ========================================================================
426    // Directory Operations
427    // ========================================================================
428
429    /// List entries in a directory (non-recursive)
430    fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>>;
431
432    /// Create a directory
433    fn create_dir(&self, path: &Path) -> io::Result<()>;
434
435    /// Create a directory and all parent directories
436    fn create_dir_all(&self, path: &Path) -> io::Result<()>;
437
438    // ========================================================================
439    // Path Operations
440    // ========================================================================
441
442    /// Get canonical (absolute, normalized) path
443    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf>;
444
445    // ========================================================================
446    // Utility Methods
447    // ========================================================================
448
449    /// Get the current user's UID (Unix only, returns 0 on other platforms)
450    fn current_uid(&self) -> u32;
451
452    /// Check if the current user is the owner of the file
453    fn is_owner(&self, path: &Path) -> bool {
454        #[cfg(unix)]
455        {
456            if let Ok(meta) = self.metadata(path) {
457                if let Some(uid) = meta.uid {
458                    return uid == self.current_uid();
459                }
460            }
461            true
462        }
463        #[cfg(not(unix))]
464        {
465            let _ = path;
466            true
467        }
468    }
469
470    /// Get a temporary file path for atomic writes
471    fn temp_path_for(&self, path: &Path) -> PathBuf {
472        path.with_extension("tmp")
473    }
474
475    /// Get a unique temporary file path (using timestamp and PID)
476    fn unique_temp_path(&self, dest_path: &Path) -> PathBuf {
477        let temp_dir = std::env::temp_dir();
478        let file_name = dest_path
479            .file_name()
480            .unwrap_or_else(|| std::ffi::OsStr::new("fresh-save"));
481        let timestamp = std::time::SystemTime::now()
482            .duration_since(std::time::UNIX_EPOCH)
483            .map(|d| d.as_nanos())
484            .unwrap_or(0);
485        temp_dir.join(format!(
486            "{}-{}-{}.tmp",
487            file_name.to_string_lossy(),
488            std::process::id(),
489            timestamp
490        ))
491    }
492
493    // ========================================================================
494    // Remote Connection Info
495    // ========================================================================
496
497    /// Get remote connection info if this is a remote filesystem
498    ///
499    /// Returns `Some("user@host")` for remote filesystems, `None` for local.
500    /// Used to display remote connection status in the UI.
501    fn remote_connection_info(&self) -> Option<&str> {
502        None
503    }
504
505    /// Get the home directory for this filesystem
506    ///
507    /// For local filesystems, returns the local home directory.
508    /// For remote filesystems, returns the remote home directory.
509    fn home_dir(&self) -> io::Result<PathBuf> {
510        dirs::home_dir()
511            .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "home directory not found"))
512    }
513
514    /// Write file using sudo (for root-owned files).
515    ///
516    /// This writes the file with elevated privileges, preserving the specified
517    /// permissions and ownership. Used when normal write fails due to permissions.
518    ///
519    /// - `path`: Destination file path
520    /// - `data`: File contents to write
521    /// - `mode`: File permissions (e.g., 0o644)
522    /// - `uid`: Owner user ID
523    /// - `gid`: Owner group ID
524    fn sudo_write(&self, path: &Path, data: &[u8], mode: u32, uid: u32, gid: u32)
525        -> io::Result<()>;
526}
527
528// ============================================================================
529// FileSystemExt - Async Extension Trait
530// ============================================================================
531
532/// Async extension trait for FileSystem
533///
534/// This trait provides async versions of FileSystem methods using native
535/// Rust async fn (no async_trait crate needed). Default implementations
536/// simply call the sync methods, which works for local filesystem operations.
537///
538/// For truly async backends (network FS, remote agents), implementations
539/// can override these methods with actual async implementations.
540///
541/// Note: This trait is NOT object-safe due to async fn. Use generics
542/// (`impl FileSystem` or `F: FileSystem`) instead of `dyn FileSystem`
543/// when async methods are needed.
544///
545/// # Example
546///
547/// ```ignore
548/// async fn list_files<F: FileSystem>(fs: &F, path: &Path) -> io::Result<Vec<DirEntry>> {
549///     fs.read_dir_async(path).await
550/// }
551/// ```
552pub trait FileSystemExt: FileSystem {
553    /// Async version of read_file
554    fn read_file_async(
555        &self,
556        path: &Path,
557    ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
558        async { self.read_file(path) }
559    }
560
561    /// Async version of read_range
562    fn read_range_async(
563        &self,
564        path: &Path,
565        offset: u64,
566        len: usize,
567    ) -> impl std::future::Future<Output = io::Result<Vec<u8>>> + Send {
568        async move { self.read_range(path, offset, len) }
569    }
570
571    /// Async version of count_line_feeds_in_range
572    fn count_line_feeds_in_range_async(
573        &self,
574        path: &Path,
575        offset: u64,
576        len: usize,
577    ) -> impl std::future::Future<Output = io::Result<usize>> + Send {
578        async move { self.count_line_feeds_in_range(path, offset, len) }
579    }
580
581    /// Async version of write_file
582    fn write_file_async(
583        &self,
584        path: &Path,
585        data: &[u8],
586    ) -> impl std::future::Future<Output = io::Result<()>> + Send {
587        async { self.write_file(path, data) }
588    }
589
590    /// Async version of metadata
591    fn metadata_async(
592        &self,
593        path: &Path,
594    ) -> impl std::future::Future<Output = io::Result<FileMetadata>> + Send {
595        async { self.metadata(path) }
596    }
597
598    /// Async version of exists
599    fn exists_async(&self, path: &Path) -> impl std::future::Future<Output = bool> + Send {
600        async { self.exists(path) }
601    }
602
603    /// Async version of is_dir
604    fn is_dir_async(
605        &self,
606        path: &Path,
607    ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
608        async { self.is_dir(path) }
609    }
610
611    /// Async version of is_file
612    fn is_file_async(
613        &self,
614        path: &Path,
615    ) -> impl std::future::Future<Output = io::Result<bool>> + Send {
616        async { self.is_file(path) }
617    }
618
619    /// Async version of read_dir
620    fn read_dir_async(
621        &self,
622        path: &Path,
623    ) -> impl std::future::Future<Output = io::Result<Vec<DirEntry>>> + Send {
624        async { self.read_dir(path) }
625    }
626
627    /// Async version of canonicalize
628    fn canonicalize_async(
629        &self,
630        path: &Path,
631    ) -> impl std::future::Future<Output = io::Result<PathBuf>> + Send {
632        async { self.canonicalize(path) }
633    }
634}
635
636/// Blanket implementation: all FileSystem types automatically get async methods
637impl<T: FileSystem> FileSystemExt for T {}
638
639// ============================================================================
640// StdFileSystem Implementation
641// ============================================================================
642
643/// Standard filesystem implementation using `std::fs`
644///
645/// This is the default implementation for native builds.
646#[derive(Debug, Clone, Copy, Default)]
647pub struct StdFileSystem;
648
649impl StdFileSystem {
650    /// Check if a file is hidden (platform-specific)
651    fn is_hidden(path: &Path) -> bool {
652        path.file_name()
653            .and_then(|n| n.to_str())
654            .is_some_and(|n| n.starts_with('.'))
655    }
656
657    /// Build FileMetadata from std::fs::Metadata
658    fn build_metadata(path: &Path, meta: &std::fs::Metadata) -> FileMetadata {
659        #[cfg(unix)]
660        {
661            use std::os::unix::fs::MetadataExt;
662            FileMetadata {
663                size: meta.len(),
664                modified: meta.modified().ok(),
665                permissions: Some(FilePermissions::from_std(meta.permissions())),
666                is_hidden: Self::is_hidden(path),
667                is_readonly: meta.permissions().readonly(),
668                uid: Some(meta.uid()),
669                gid: Some(meta.gid()),
670            }
671        }
672        #[cfg(not(unix))]
673        {
674            FileMetadata {
675                size: meta.len(),
676                modified: meta.modified().ok(),
677                permissions: Some(FilePermissions::from_std(meta.permissions())),
678                is_hidden: Self::is_hidden(path),
679                is_readonly: meta.permissions().readonly(),
680            }
681        }
682    }
683}
684
685impl FileSystem for StdFileSystem {
686    // File Content Operations
687    fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
688        std::fs::read(path)
689    }
690
691    fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
692        let mut file = std::fs::File::open(path)?;
693        file.seek(io::SeekFrom::Start(offset))?;
694        let mut buffer = vec![0u8; len];
695        file.read_exact(&mut buffer)?;
696        Ok(buffer)
697    }
698
699    fn write_file(&self, path: &Path, data: &[u8]) -> io::Result<()> {
700        let original_metadata = self.metadata_if_exists(path);
701        let temp_path = self.temp_path_for(path);
702        {
703            let mut file = self.create_file(&temp_path)?;
704            file.write_all(data)?;
705            file.sync_all()?;
706        }
707        if let Some(ref meta) = original_metadata {
708            if let Some(ref perms) = meta.permissions {
709                // Best-effort permission restore; rename will proceed regardless
710                #[allow(clippy::let_underscore_must_use)]
711                let _ = self.set_permissions(&temp_path, perms);
712            }
713        }
714        self.rename(&temp_path, path)?;
715        Ok(())
716    }
717
718    fn create_file(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
719        let file = std::fs::File::create(path)?;
720        Ok(Box::new(StdFileWriter(file)))
721    }
722
723    fn open_file(&self, path: &Path) -> io::Result<Box<dyn FileReader>> {
724        let file = std::fs::File::open(path)?;
725        Ok(Box::new(StdFileReader(file)))
726    }
727
728    fn open_file_for_write(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
729        let file = std::fs::OpenOptions::new()
730            .write(true)
731            .truncate(true)
732            .open(path)?;
733        Ok(Box::new(StdFileWriter(file)))
734    }
735
736    fn open_file_for_append(&self, path: &Path) -> io::Result<Box<dyn FileWriter>> {
737        let file = std::fs::OpenOptions::new()
738            .create(true)
739            .append(true)
740            .open(path)?;
741        Ok(Box::new(StdFileWriter(file)))
742    }
743
744    fn set_file_length(&self, path: &Path, len: u64) -> io::Result<()> {
745        let file = std::fs::OpenOptions::new().write(true).open(path)?;
746        file.set_len(len)
747    }
748
749    // File Operations
750    fn rename(&self, from: &Path, to: &Path) -> io::Result<()> {
751        std::fs::rename(from, to)
752    }
753
754    fn copy(&self, from: &Path, to: &Path) -> io::Result<u64> {
755        std::fs::copy(from, to)
756    }
757
758    fn remove_file(&self, path: &Path) -> io::Result<()> {
759        std::fs::remove_file(path)
760    }
761
762    fn remove_dir(&self, path: &Path) -> io::Result<()> {
763        std::fs::remove_dir(path)
764    }
765
766    // Metadata Operations
767    fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
768        let meta = std::fs::metadata(path)?;
769        Ok(Self::build_metadata(path, &meta))
770    }
771
772    fn symlink_metadata(&self, path: &Path) -> io::Result<FileMetadata> {
773        let meta = std::fs::symlink_metadata(path)?;
774        Ok(Self::build_metadata(path, &meta))
775    }
776
777    fn is_dir(&self, path: &Path) -> io::Result<bool> {
778        Ok(std::fs::metadata(path)?.is_dir())
779    }
780
781    fn is_file(&self, path: &Path) -> io::Result<bool> {
782        Ok(std::fs::metadata(path)?.is_file())
783    }
784
785    fn set_permissions(&self, path: &Path, permissions: &FilePermissions) -> io::Result<()> {
786        std::fs::set_permissions(path, permissions.to_std())
787    }
788
789    // Directory Operations
790    fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
791        let mut entries = Vec::new();
792        for entry in std::fs::read_dir(path)? {
793            let entry = entry?;
794            let path = entry.path();
795            let name = entry.file_name().to_string_lossy().into_owned();
796            let file_type = entry.file_type()?;
797
798            let entry_type = if file_type.is_dir() {
799                EntryType::Directory
800            } else if file_type.is_symlink() {
801                EntryType::Symlink
802            } else {
803                EntryType::File
804            };
805
806            let mut dir_entry = DirEntry::new(path.clone(), name, entry_type);
807
808            // For symlinks, check if target is a directory
809            if file_type.is_symlink() {
810                dir_entry.symlink_target_is_dir = std::fs::metadata(&path)
811                    .map(|m| m.is_dir())
812                    .unwrap_or(false);
813            }
814
815            entries.push(dir_entry);
816        }
817        Ok(entries)
818    }
819
820    fn create_dir(&self, path: &Path) -> io::Result<()> {
821        std::fs::create_dir(path)
822    }
823
824    fn create_dir_all(&self, path: &Path) -> io::Result<()> {
825        std::fs::create_dir_all(path)
826    }
827
828    // Path Operations
829    fn canonicalize(&self, path: &Path) -> io::Result<PathBuf> {
830        std::fs::canonicalize(path)
831    }
832
833    // Utility
834    fn current_uid(&self) -> u32 {
835        #[cfg(all(unix, feature = "runtime"))]
836        {
837            // SAFETY: getuid() is a simple syscall with no arguments
838            unsafe { libc::getuid() }
839        }
840        #[cfg(not(all(unix, feature = "runtime")))]
841        {
842            0
843        }
844    }
845
846    fn sudo_write(
847        &self,
848        path: &Path,
849        data: &[u8],
850        mode: u32,
851        uid: u32,
852        gid: u32,
853    ) -> io::Result<()> {
854        use std::process::{Command, Stdio};
855
856        // Write data via sudo tee
857        let mut child = Command::new("sudo")
858            .args(["tee", &path.to_string_lossy()])
859            .stdin(Stdio::piped())
860            .stdout(Stdio::null())
861            .stderr(Stdio::piped())
862            .spawn()
863            .map_err(|e| io::Error::other(format!("failed to spawn sudo: {}", e)))?;
864
865        if let Some(mut stdin) = child.stdin.take() {
866            use std::io::Write;
867            stdin.write_all(data)?;
868        }
869
870        let output = child.wait_with_output()?;
871        if !output.status.success() {
872            let stderr = String::from_utf8_lossy(&output.stderr);
873            return Err(io::Error::new(
874                io::ErrorKind::PermissionDenied,
875                format!("sudo tee failed: {}", stderr.trim()),
876            ));
877        }
878
879        // Set permissions via sudo chmod
880        let status = Command::new("sudo")
881            .args(["chmod", &format!("{:o}", mode), &path.to_string_lossy()])
882            .status()?;
883        if !status.success() {
884            return Err(io::Error::other("sudo chmod failed"));
885        }
886
887        // Set ownership via sudo chown
888        let status = Command::new("sudo")
889            .args([
890                "chown",
891                &format!("{}:{}", uid, gid),
892                &path.to_string_lossy(),
893            ])
894            .status()?;
895        if !status.success() {
896            return Err(io::Error::other("sudo chown failed"));
897        }
898
899        Ok(())
900    }
901}
902
903// ============================================================================
904// NoopFileSystem Implementation
905// ============================================================================
906
907/// No-op filesystem that returns errors for all operations
908///
909/// Used as a placeholder or in WASM builds where a VirtualFileSystem
910/// should be used instead.
911#[derive(Debug, Clone, Copy, Default)]
912pub struct NoopFileSystem;
913
914impl NoopFileSystem {
915    fn unsupported<T>() -> io::Result<T> {
916        Err(io::Error::new(
917            io::ErrorKind::Unsupported,
918            "Filesystem not available",
919        ))
920    }
921}
922
923impl FileSystem for NoopFileSystem {
924    fn read_file(&self, _path: &Path) -> io::Result<Vec<u8>> {
925        Self::unsupported()
926    }
927
928    fn read_range(&self, _path: &Path, _offset: u64, _len: usize) -> io::Result<Vec<u8>> {
929        Self::unsupported()
930    }
931
932    fn write_file(&self, _path: &Path, _data: &[u8]) -> io::Result<()> {
933        Self::unsupported()
934    }
935
936    fn create_file(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
937        Self::unsupported()
938    }
939
940    fn open_file(&self, _path: &Path) -> io::Result<Box<dyn FileReader>> {
941        Self::unsupported()
942    }
943
944    fn open_file_for_write(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
945        Self::unsupported()
946    }
947
948    fn open_file_for_append(&self, _path: &Path) -> io::Result<Box<dyn FileWriter>> {
949        Self::unsupported()
950    }
951
952    fn set_file_length(&self, _path: &Path, _len: u64) -> io::Result<()> {
953        Self::unsupported()
954    }
955
956    fn rename(&self, _from: &Path, _to: &Path) -> io::Result<()> {
957        Self::unsupported()
958    }
959
960    fn copy(&self, _from: &Path, _to: &Path) -> io::Result<u64> {
961        Self::unsupported()
962    }
963
964    fn remove_file(&self, _path: &Path) -> io::Result<()> {
965        Self::unsupported()
966    }
967
968    fn remove_dir(&self, _path: &Path) -> io::Result<()> {
969        Self::unsupported()
970    }
971
972    fn metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
973        Self::unsupported()
974    }
975
976    fn symlink_metadata(&self, _path: &Path) -> io::Result<FileMetadata> {
977        Self::unsupported()
978    }
979
980    fn is_dir(&self, _path: &Path) -> io::Result<bool> {
981        Self::unsupported()
982    }
983
984    fn is_file(&self, _path: &Path) -> io::Result<bool> {
985        Self::unsupported()
986    }
987
988    fn set_permissions(&self, _path: &Path, _permissions: &FilePermissions) -> io::Result<()> {
989        Self::unsupported()
990    }
991
992    fn read_dir(&self, _path: &Path) -> io::Result<Vec<DirEntry>> {
993        Self::unsupported()
994    }
995
996    fn create_dir(&self, _path: &Path) -> io::Result<()> {
997        Self::unsupported()
998    }
999
1000    fn create_dir_all(&self, _path: &Path) -> io::Result<()> {
1001        Self::unsupported()
1002    }
1003
1004    fn canonicalize(&self, _path: &Path) -> io::Result<PathBuf> {
1005        Self::unsupported()
1006    }
1007
1008    fn current_uid(&self) -> u32 {
1009        0
1010    }
1011
1012    fn sudo_write(
1013        &self,
1014        _path: &Path,
1015        _data: &[u8],
1016        _mode: u32,
1017        _uid: u32,
1018        _gid: u32,
1019    ) -> io::Result<()> {
1020        Self::unsupported()
1021    }
1022}
1023
1024// ============================================================================
1025// Tests
1026// ============================================================================
1027
1028#[cfg(test)]
1029mod tests {
1030    use super::*;
1031    use tempfile::NamedTempFile;
1032
1033    #[test]
1034    fn test_std_filesystem_read_write() {
1035        let fs = StdFileSystem;
1036        let mut temp = NamedTempFile::new().unwrap();
1037        let path = temp.path().to_path_buf();
1038
1039        std::io::Write::write_all(&mut temp, b"Hello, World!").unwrap();
1040        std::io::Write::flush(&mut temp).unwrap();
1041
1042        let content = fs.read_file(&path).unwrap();
1043        assert_eq!(content, b"Hello, World!");
1044
1045        let range = fs.read_range(&path, 7, 5).unwrap();
1046        assert_eq!(range, b"World");
1047
1048        let meta = fs.metadata(&path).unwrap();
1049        assert_eq!(meta.size, 13);
1050    }
1051
1052    #[test]
1053    fn test_noop_filesystem() {
1054        let fs = NoopFileSystem;
1055        let path = Path::new("/some/path");
1056
1057        assert!(fs.read_file(path).is_err());
1058        assert!(fs.read_range(path, 0, 10).is_err());
1059        assert!(fs.write_file(path, b"data").is_err());
1060        assert!(fs.metadata(path).is_err());
1061        assert!(fs.read_dir(path).is_err());
1062    }
1063
1064    #[test]
1065    fn test_create_and_write_file() {
1066        let fs = StdFileSystem;
1067        let temp_dir = tempfile::tempdir().unwrap();
1068        let path = temp_dir.path().join("test.txt");
1069
1070        {
1071            let mut writer = fs.create_file(&path).unwrap();
1072            writer.write_all(b"test content").unwrap();
1073            writer.sync_all().unwrap();
1074        }
1075
1076        let content = fs.read_file(&path).unwrap();
1077        assert_eq!(content, b"test content");
1078    }
1079
1080    #[test]
1081    fn test_read_dir() {
1082        let fs = StdFileSystem;
1083        let temp_dir = tempfile::tempdir().unwrap();
1084
1085        // Create some files and directories
1086        fs.create_dir(&temp_dir.path().join("subdir")).unwrap();
1087        fs.write_file(&temp_dir.path().join("file1.txt"), b"content1")
1088            .unwrap();
1089        fs.write_file(&temp_dir.path().join("file2.txt"), b"content2")
1090            .unwrap();
1091
1092        let entries = fs.read_dir(temp_dir.path()).unwrap();
1093        assert_eq!(entries.len(), 3);
1094
1095        let names: Vec<_> = entries.iter().map(|e| e.name.as_str()).collect();
1096        assert!(names.contains(&"subdir"));
1097        assert!(names.contains(&"file1.txt"));
1098        assert!(names.contains(&"file2.txt"));
1099    }
1100
1101    #[test]
1102    fn test_dir_entry_types() {
1103        let file = DirEntry::new(PathBuf::from("/file"), "file".to_string(), EntryType::File);
1104        assert!(file.is_file());
1105        assert!(!file.is_dir());
1106
1107        let dir = DirEntry::new(
1108            PathBuf::from("/dir"),
1109            "dir".to_string(),
1110            EntryType::Directory,
1111        );
1112        assert!(dir.is_dir());
1113        assert!(!dir.is_file());
1114
1115        let link_to_dir = DirEntry::new_symlink(PathBuf::from("/link"), "link".to_string(), true);
1116        assert!(link_to_dir.is_symlink());
1117        assert!(link_to_dir.is_dir());
1118    }
1119
1120    #[test]
1121    fn test_metadata_builder() {
1122        let meta = FileMetadata::default()
1123            .with_hidden(true)
1124            .with_readonly(true);
1125        assert!(meta.is_hidden);
1126        assert!(meta.is_readonly);
1127    }
1128
1129    #[test]
1130    fn test_atomic_write() {
1131        let fs = StdFileSystem;
1132        let temp_dir = tempfile::tempdir().unwrap();
1133        let path = temp_dir.path().join("atomic_test.txt");
1134
1135        fs.write_file(&path, b"initial").unwrap();
1136        assert_eq!(fs.read_file(&path).unwrap(), b"initial");
1137
1138        fs.write_file(&path, b"updated").unwrap();
1139        assert_eq!(fs.read_file(&path).unwrap(), b"updated");
1140    }
1141
1142    #[test]
1143    fn test_write_patched_default_impl() {
1144        // Test that the default write_patched implementation works correctly
1145        let fs = StdFileSystem;
1146        let temp_dir = tempfile::tempdir().unwrap();
1147        let src_path = temp_dir.path().join("source.txt");
1148        let dst_path = temp_dir.path().join("dest.txt");
1149
1150        // Create source file with known content
1151        fs.write_file(&src_path, b"AAABBBCCC").unwrap();
1152
1153        // Apply patch: copy first 3 bytes, insert "XXX", copy last 3 bytes
1154        let ops = vec![
1155            WriteOp::Copy { offset: 0, len: 3 }, // "AAA"
1156            WriteOp::Insert { data: b"XXX" },    // "XXX"
1157            WriteOp::Copy { offset: 6, len: 3 }, // "CCC"
1158        ];
1159
1160        fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1161
1162        let result = fs.read_file(&dst_path).unwrap();
1163        assert_eq!(result, b"AAAXXXCCC");
1164    }
1165
1166    #[test]
1167    fn test_write_patched_same_file() {
1168        // Test patching a file in-place (src == dst)
1169        let fs = StdFileSystem;
1170        let temp_dir = tempfile::tempdir().unwrap();
1171        let path = temp_dir.path().join("file.txt");
1172
1173        // Create file
1174        fs.write_file(&path, b"Hello World").unwrap();
1175
1176        // Replace "World" with "Rust"
1177        let ops = vec![
1178            WriteOp::Copy { offset: 0, len: 6 }, // "Hello "
1179            WriteOp::Insert { data: b"Rust" },   // "Rust"
1180        ];
1181
1182        fs.write_patched(&path, &path, &ops).unwrap();
1183
1184        let result = fs.read_file(&path).unwrap();
1185        assert_eq!(result, b"Hello Rust");
1186    }
1187
1188    #[test]
1189    fn test_write_patched_insert_only() {
1190        // Test a patch with only inserts (new file)
1191        let fs = StdFileSystem;
1192        let temp_dir = tempfile::tempdir().unwrap();
1193        let src_path = temp_dir.path().join("empty.txt");
1194        let dst_path = temp_dir.path().join("new.txt");
1195
1196        // Create empty source (won't be read from)
1197        fs.write_file(&src_path, b"").unwrap();
1198
1199        let ops = vec![WriteOp::Insert {
1200            data: b"All new content",
1201        }];
1202
1203        fs.write_patched(&src_path, &dst_path, &ops).unwrap();
1204
1205        let result = fs.read_file(&dst_path).unwrap();
1206        assert_eq!(result, b"All new content");
1207    }
1208}