1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum FileKind {
8 File,
10 Directory,
12 Symlink,
14}
15
16#[derive(Debug, Clone)]
18pub struct FileMetadata {
19 pub path: String,
21
22 pub is_dir: bool,
24
25 pub size: u64,
27
28 pub permissions: FilePermissions,
30
31 pub is_symlink: bool,
33
34 pub symlink_target: Option<String>,
36
37 pub modified: i64,
39
40 pub hash: Option<String>,
42
43 pub kind: FileKind,
45}
46
47impl FileMetadata {
48 pub fn is_readable(&self) -> bool {
50 self.permissions.owner_read
51 }
52
53 pub fn is_writable(&self) -> bool {
55 self.permissions.owner_write
56 }
57
58 pub fn is_executable(&self) -> bool {
60 self.permissions.owner_exec
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct FilePermissions {
67 pub owner_read: bool,
69 pub owner_write: bool,
71 pub owner_exec: bool,
73 pub group_read: bool,
75 pub group_write: bool,
77 pub group_exec: bool,
79 pub other_read: bool,
81 pub other_write: bool,
83 pub other_exec: bool,
85}
86
87impl FilePermissions {
88 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 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 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 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 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 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#[derive(Debug, Clone)]
221pub enum FsOperation {
222 WriteFile { path: String, content: Vec<u8> },
224
225 CopyFile { src: String, dst: String },
227
228 CopyDir { src: String, dst: String },
230
231 MoveFile { src: String, dst: String },
233
234 MoveDir { src: String, dst: String },
236
237 DeleteFile { path: String },
239
240 DeleteDir { path: String },
242
243 Chmod {
245 path: String,
246 permissions: FilePermissions,
247 recursive: bool,
248 },
249
250 MakeExecutable { path: String },
252
253 Symlink {
255 link_path: String,
256 target_path: String,
257 },
258}
259
260impl FsOperation {
261 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#[derive(Debug, Clone)]
295pub struct FindResults {
296 pub files: Vec<String>,
298
299 pub count: usize,
301
302 pub dirs_traversed: usize,
304}
305
306impl FindResults {
307 pub fn new() -> Self {
309 Self {
310 files: Vec::new(),
311 count: 0,
312 dirs_traversed: 0,
313 }
314 }
315
316 pub fn dirs_only(self) -> Vec<String> {
318 self.files
319 .into_iter()
320 .filter(|p| p.ends_with('/'))
321 .collect()
322 }
323
324 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#[derive(Debug, Clone)]
341pub struct DirectoryEntry {
342 pub name: String,
344
345 pub is_dir: bool,
347
348 pub size: u64,
350
351 pub permissions: FilePermissions,
353
354 pub modified: i64,
356}
357
358#[derive(Debug, Clone)]
360pub struct OperationSummary {
361 pub operations: Vec<FsOperation>,
363
364 pub files_affected: usize,
366
367 pub dirs_affected: usize,
369
370 pub bytes_changed: u64,
372}
373
374impl OperationSummary {
375 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 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}