heroforge_core/fs/
operations.rs

1//! Filesystem operations and metadata types
2//!
3//! This module defines the core types for filesystem operations and file metadata.
4
5/// File kind/type
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FileKind {
8    /// Regular file
9    File,
10    /// Directory
11    Directory,
12    /// Symbolic link
13    Symlink,
14}
15
16/// File metadata
17#[derive(Debug, Clone)]
18pub struct FileMetadata {
19    /// File path
20    pub path: String,
21
22    /// Whether this is a directory
23    pub is_dir: bool,
24
25    /// File size in bytes
26    pub size: u64,
27
28    /// File permissions
29    pub permissions: FilePermissions,
30
31    /// Whether path is a symlink
32    pub is_symlink: bool,
33
34    /// Symlink target (if is_symlink)
35    pub symlink_target: Option<String>,
36
37    /// Last modified time (Unix timestamp)
38    pub modified: i64,
39
40    /// File hash (for versioning)
41    pub hash: Option<String>,
42
43    /// File kind
44    pub kind: FileKind,
45}
46
47impl FileMetadata {
48    /// Check if file is readable
49    pub fn is_readable(&self) -> bool {
50        self.permissions.owner_read
51    }
52
53    /// Check if file is writable
54    pub fn is_writable(&self) -> bool {
55        self.permissions.owner_write
56    }
57
58    /// Check if file is executable
59    pub fn is_executable(&self) -> bool {
60        self.permissions.owner_exec
61    }
62}
63
64/// File permission bits (Unix-style)
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct FilePermissions {
67    /// Owner read
68    pub owner_read: bool,
69    /// Owner write
70    pub owner_write: bool,
71    /// Owner execute
72    pub owner_exec: bool,
73    /// Group read
74    pub group_read: bool,
75    /// Group write
76    pub group_write: bool,
77    /// Group execute
78    pub group_exec: bool,
79    /// Other read
80    pub other_read: bool,
81    /// Other write
82    pub other_write: bool,
83    /// Other execute
84    pub other_exec: bool,
85}
86
87impl FilePermissions {
88    /// Default file permissions (644)
89    pub fn file() -> Self {
90        Self {
91            owner_read: true,
92            owner_write: true,
93            owner_exec: false,
94            group_read: true,
95            group_write: false,
96            group_exec: false,
97            other_read: true,
98            other_write: false,
99            other_exec: false,
100        }
101    }
102
103    /// Default executable permissions (755)
104    pub fn executable() -> Self {
105        Self {
106            owner_read: true,
107            owner_write: true,
108            owner_exec: true,
109            group_read: true,
110            group_write: false,
111            group_exec: true,
112            other_read: true,
113            other_write: false,
114            other_exec: true,
115        }
116    }
117
118    /// Read-only permissions (444)
119    pub fn readonly() -> Self {
120        Self {
121            owner_read: true,
122            owner_write: false,
123            owner_exec: false,
124            group_read: true,
125            group_write: false,
126            group_exec: false,
127            other_read: true,
128            other_write: false,
129            other_exec: false,
130        }
131    }
132
133    /// Convert from octal notation (e.g., 0o755)
134    pub fn from_octal(mode: u32) -> Self {
135        let owner = (mode >> 6) & 7;
136        let group = (mode >> 3) & 7;
137        let other = mode & 7;
138
139        Self {
140            owner_read: owner & 4 != 0,
141            owner_write: owner & 2 != 0,
142            owner_exec: owner & 1 != 0,
143            group_read: group & 4 != 0,
144            group_write: group & 2 != 0,
145            group_exec: group & 1 != 0,
146            other_read: other & 4 != 0,
147            other_write: other & 2 != 0,
148            other_exec: other & 1 != 0,
149        }
150    }
151
152    /// Convert to octal notation
153    pub fn to_octal(&self) -> u32 {
154        let mut mode = 0u32;
155        if self.owner_read {
156            mode |= 4 << 6;
157        }
158        if self.owner_write {
159            mode |= 2 << 6;
160        }
161        if self.owner_exec {
162            mode |= 1 << 6;
163        }
164        if self.group_read {
165            mode |= 4 << 3;
166        }
167        if self.group_write {
168            mode |= 2 << 3;
169        }
170        if self.group_exec {
171            mode |= 1 << 3;
172        }
173        if self.other_read {
174            mode |= 4;
175        }
176        if self.other_write {
177            mode |= 2;
178        }
179        if self.other_exec {
180            mode |= 1;
181        }
182        mode
183    }
184
185    /// Convert to string representation (e.g., "rwxr-xr-x")
186    pub fn to_string(&self) -> String {
187        let mut s = String::with_capacity(9);
188        s.push(if self.owner_read { 'r' } else { '-' });
189        s.push(if self.owner_write { 'w' } else { '-' });
190        s.push(if self.owner_exec { 'x' } else { '-' });
191        s.push(if self.group_read { 'r' } else { '-' });
192        s.push(if self.group_write { 'w' } else { '-' });
193        s.push(if self.group_exec { 'x' } else { '-' });
194        s.push(if self.other_read { 'r' } else { '-' });
195        s.push(if self.other_write { 'w' } else { '-' });
196        s.push(if self.other_exec { 'x' } else { '-' });
197        s
198    }
199}
200
201impl Default for FilePermissions {
202    fn default() -> Self {
203        Self::file()
204    }
205}
206
207impl From<u32> for FilePermissions {
208    fn from(mode: u32) -> Self {
209        Self::from_octal(mode)
210    }
211}
212
213impl From<FilePermissions> for u32 {
214    fn from(perms: FilePermissions) -> Self {
215        perms.to_octal()
216    }
217}
218
219/// Filesystem operations
220#[derive(Debug, Clone)]
221pub enum FsOperation {
222    /// Write file content
223    WriteFile { path: String, content: Vec<u8> },
224
225    /// Copy file
226    CopyFile { src: String, dst: String },
227
228    /// Copy directory recursively
229    CopyDir { src: String, dst: String },
230
231    /// Move file
232    MoveFile { src: String, dst: String },
233
234    /// Move directory recursively
235    MoveDir { src: String, dst: String },
236
237    /// Delete file
238    DeleteFile { path: String },
239
240    /// Delete directory recursively
241    DeleteDir { path: String },
242
243    /// Change file permissions
244    Chmod {
245        path: String,
246        permissions: FilePermissions,
247        recursive: bool,
248    },
249
250    /// Make file executable
251    MakeExecutable { path: String },
252
253    /// Create symbolic link
254    Symlink {
255        link_path: String,
256        target_path: String,
257    },
258}
259
260impl FsOperation {
261    /// Get human-readable description of operation
262    pub fn describe(&self) -> String {
263        match self {
264            FsOperation::WriteFile { path, .. } => format!("Write file: {}", path),
265            FsOperation::CopyFile { src, dst } => format!("Copy file: {} -> {}", src, dst),
266            FsOperation::CopyDir { src, dst } => format!("Copy directory: {} -> {}", src, dst),
267            FsOperation::MoveFile { src, dst } => format!("Move file: {} -> {}", src, dst),
268            FsOperation::MoveDir { src, dst } => format!("Move directory: {} -> {}", src, dst),
269            FsOperation::DeleteFile { path } => format!("Delete file: {}", path),
270            FsOperation::DeleteDir { path } => format!("Delete directory: {}", path),
271            FsOperation::Chmod {
272                path,
273                permissions,
274                recursive,
275            } => {
276                if *recursive {
277                    format!("Chmod {} (recursive): {}", path, permissions.to_string())
278                } else {
279                    format!("Chmod {}: {}", path, permissions.to_string())
280                }
281            }
282            FsOperation::MakeExecutable { path } => format!("Make executable: {}", path),
283            FsOperation::Symlink {
284                link_path,
285                target_path,
286            } => {
287                format!("Create symlink: {} -> {}", link_path, target_path)
288            }
289        }
290    }
291}
292
293/// Find operation results
294#[derive(Debug, Clone)]
295pub struct FindResults {
296    /// Matched file paths
297    pub files: Vec<String>,
298
299    /// Total count of matches
300    pub count: usize,
301
302    /// Directories traversed
303    pub dirs_traversed: usize,
304}
305
306impl FindResults {
307    /// Create new empty results
308    pub fn new() -> Self {
309        Self {
310            files: Vec::new(),
311            count: 0,
312            dirs_traversed: 0,
313        }
314    }
315
316    /// Filter results to only directories
317    pub fn dirs_only(self) -> Vec<String> {
318        self.files
319            .into_iter()
320            .filter(|p| p.ends_with('/'))
321            .collect()
322    }
323
324    /// Filter results to only files
325    pub fn files_only(self) -> Vec<String> {
326        self.files
327            .into_iter()
328            .filter(|p| !p.ends_with('/'))
329            .collect()
330    }
331}
332
333impl Default for FindResults {
334    fn default() -> Self {
335        Self::new()
336    }
337}
338
339/// Directory listing entry
340#[derive(Debug, Clone)]
341pub struct DirectoryEntry {
342    /// Entry name (not full path)
343    pub name: String,
344
345    /// Whether this is a directory
346    pub is_dir: bool,
347
348    /// File size (0 for directories)
349    pub size: u64,
350
351    /// File permissions
352    pub permissions: FilePermissions,
353
354    /// Last modified time
355    pub modified: i64,
356}
357
358/// Batch operation summary
359#[derive(Debug, Clone)]
360pub struct OperationSummary {
361    /// All operations to be performed
362    pub operations: Vec<FsOperation>,
363
364    /// Total files affected
365    pub files_affected: usize,
366
367    /// Total directories affected
368    pub dirs_affected: usize,
369
370    /// Estimated bytes changed
371    pub bytes_changed: u64,
372}
373
374impl OperationSummary {
375    /// Create new summary
376    pub fn new() -> Self {
377        Self {
378            operations: Vec::new(),
379            files_affected: 0,
380            dirs_affected: 0,
381            bytes_changed: 0,
382        }
383    }
384
385    /// Add operation to summary
386    pub fn add_operation(&mut self, op: FsOperation, bytes: u64) {
387        match &op {
388            FsOperation::WriteFile { .. } | FsOperation::CopyFile { .. } => {
389                self.files_affected += 1;
390            }
391            FsOperation::CopyDir { .. } => {
392                self.dirs_affected += 1;
393            }
394            FsOperation::DeleteFile { .. } | FsOperation::DeleteDir { .. } => {
395                self.files_affected += 1;
396            }
397            _ => {}
398        }
399        self.bytes_changed += bytes;
400        self.operations.push(op);
401    }
402}
403
404impl Default for OperationSummary {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413
414    #[test]
415    fn test_permissions_octal() {
416        let perms = FilePermissions::from_octal(0o755);
417        assert!(perms.owner_read);
418        assert!(perms.owner_write);
419        assert!(perms.owner_exec);
420        assert_eq!(perms.to_octal(), 0o755);
421    }
422
423    #[test]
424    fn test_permissions_string() {
425        let perms = FilePermissions::executable();
426        assert_eq!(perms.to_string(), "rwxr-xr-x");
427    }
428
429    #[test]
430    fn test_operation_describe() {
431        let op = FsOperation::WriteFile {
432            path: "/tmp/test.txt".to_string(),
433            content: vec![],
434        };
435        assert_eq!(op.describe(), "Write file: /tmp/test.txt");
436    }
437
438    #[test]
439    fn test_find_results() {
440        let results1 = FindResults {
441            files: vec!["file.txt".to_string(), "dir/".to_string()],
442            count: 2,
443            dirs_traversed: 1,
444        };
445        let results2 = FindResults {
446            files: vec!["file.txt".to_string(), "dir/".to_string()],
447            count: 2,
448            dirs_traversed: 1,
449        };
450        assert_eq!(results1.files_only().len(), 1);
451        assert_eq!(results2.dirs_only().len(), 1);
452    }
453}