Skip to main content

soar_utils/
fs.rs

1use std::{
2    fs::{self, File},
3    io::{BufReader, Read},
4    os,
5    path::Path,
6};
7
8use crate::error::{FileSystemError, FileSystemResult, IoOperation, IoResultExt};
9
10/// Removes the specified file or directory safely.
11///
12/// If the path does not exist, this function returns `Ok(())` without error. If the path
13/// points to a directory, it and all of its contents are removed recursively, equivalent to
14/// [`std::fs::remove_dir_all`]. If the path points to a file, it is removed with
15/// [`std::fs::remove_file`].
16///
17/// # Errors
18///
19/// Returns a [`FileSystemError::File`] if the removal fails for any reason other than
20/// the path not existing (e.g., permission denied, path is in use, etc.).
21///
22/// # Example
23///
24/// ```no_run
25/// use soar_utils::error::FileSystemResult;
26/// use soar_utils::fs::safe_remove;
27///
28/// fn main() -> FileSystemResult<()> {
29///     safe_remove("/tmp/some_path")?;
30///     Ok(())
31/// }
32/// ```
33pub fn safe_remove<P: AsRef<Path>>(path: P) -> FileSystemResult<()> {
34    let path = path.as_ref();
35
36    let metadata = match fs::symlink_metadata(path) {
37        Ok(m) => m,
38        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(()),
39        Err(e) => {
40            return Err(FileSystemError::RemoveFile {
41                path: path.to_path_buf(),
42                source: e,
43            })
44        }
45    };
46
47    let result = if metadata.is_dir() {
48        fs::remove_dir_all(path)
49    } else {
50        fs::remove_file(path)
51    };
52
53    result.with_path(path, IoOperation::RemoveFile)?;
54
55    Ok(())
56}
57
58/// Creates a directory structure if it doesn't exist.
59///
60/// If the directory already exists, this function does nothing. If the directory structure
61/// exists but is not a directory, this function returns an error.
62///
63/// # Arguments
64///
65/// * `path` - The path to create.
66///
67/// # Errors
68///
69/// * [`FileSystemError::Directory`] if the directory could not be created.
70/// * [`FileSystemError::NotADirectory`] if the path exists but is not a directory.
71///
72/// # Example
73///
74/// ```no_run
75/// use soar_utils::error::FileSystemResult;
76/// use soar_utils::fs::ensure_dir_exists;
77///
78/// fn main() -> FileSystemResult<()> {
79///     let dir = "/tmp/soar-doc/internal/dir";
80///     ensure_dir_exists(dir)?;
81///     Ok(())
82/// }
83/// ```
84pub fn ensure_dir_exists<P: AsRef<Path>>(path: P) -> FileSystemResult<()> {
85    let path = path.as_ref();
86    if !path.exists() {
87        std::fs::create_dir_all(path).with_path(path, IoOperation::CreateDirectory)?;
88    } else if !path.is_dir() {
89        return Err(FileSystemError::NotADirectory {
90            path: path.to_path_buf(),
91        });
92    }
93
94    Ok(())
95}
96
97/// Creates symlink from `source` to `target`
98/// If `target` is a file, it will be removed before creating the symlink.
99///
100/// # Arguments
101///
102/// * `source` - The path to the file or directory to symlink
103/// * `target` - The path to the symlink
104///
105/// # Errors
106///
107/// Returns a [`FileSystemError::Symlink`] if the symlink could not be created.
108/// Returns a [`FileSystemError::File`] if the symlink could not be removed.
109///
110/// # Example
111///
112/// ```no_run
113/// use soar_utils::error::FileSystemResult;
114/// use soar_utils::fs::create_symlink;
115///
116/// fn main() -> FileSystemResult<()> {
117///     create_symlink("/tmp/source", "/tmp/target")?;
118///     Ok(())
119/// }
120/// ```
121pub fn create_symlink<P: AsRef<Path>, Q: AsRef<Path>>(
122    source: P,
123    target: Q,
124) -> FileSystemResult<()> {
125    let source = source.as_ref();
126    let target = target.as_ref();
127
128    if let Some(parent) = target.parent() {
129        ensure_dir_exists(parent)?;
130    }
131
132    if target.is_file() {
133        fs::remove_file(target).with_path(target, IoOperation::RemoveFile)?;
134    }
135
136    os::unix::fs::symlink(source, target).with_path(
137        source,
138        IoOperation::CreateSymlink {
139            target: target.into(),
140        },
141    )
142}
143
144/// Walks a directory recursively and calls the provided function on each file or directory.
145///
146/// # Arguments
147///
148/// * `dir` - The directory to walk
149/// * `action` - The function to call on each file or directory
150///
151/// # Errors
152///
153/// Returns a [`FileSystemError::Directory`] if the directory could not be read.
154/// Returns a [`FileSystemError::NotADirectory`] if the path is not a directory.
155///
156/// # Example
157///
158/// ```no_run
159/// use std::path::Path;
160///
161/// use soar_utils::error::FileSystemResult;
162/// use soar_utils::fs::walk_dir;
163///
164/// fn main() -> FileSystemResult<()> {
165///     let _ = walk_dir("/tmp/dir", &mut |path: &Path| -> FileSystemResult<()> {
166///         println!("Found file or directory: {}", path.display());
167///         Ok(())
168///     })?;
169///     Ok(())
170/// }
171/// ```
172pub fn walk_dir<P, F, E>(dir: P, action: &mut F) -> Result<(), E>
173where
174    P: AsRef<Path>,
175    F: FnMut(&Path) -> Result<(), E>,
176    FileSystemError: Into<E>,
177{
178    let dir = dir.as_ref();
179
180    if !dir.is_dir() {
181        return Err(FileSystemError::NotADirectory {
182            path: dir.to_path_buf(),
183        }
184        .into());
185    }
186
187    for entry in fs::read_dir(dir)
188        .with_path(dir, IoOperation::ReadDirectory)
189        .map_err(|e| e.into())?
190    {
191        let Ok(entry) = entry else {
192            continue;
193        };
194
195        let path = entry.path();
196
197        if path.is_dir() {
198            walk_dir(&path, action)?;
199            continue;
200        }
201
202        action(&path)?;
203    }
204
205    Ok(())
206}
207
208/// Reads the first `bytes` bytes from a file and returns the signature.
209///
210/// # Arguments
211/// * `path` - The path to the file
212/// * `bytes` - The number of bytes to read from the file
213///
214/// # Returns
215/// Returns a byte array of the first `bytes` bytes from the file.
216///
217/// # Errors
218/// Returns a [`FileSystemError::File`] if the file could not be opened or read.
219///
220/// # Example
221/// ```no_run
222/// use soar_utils::fs::read_file_signature;
223/// use soar_utils::error::FileSystemResult;
224///
225/// fn main() -> FileSystemResult<()> {
226///     let signature = read_file_signature("/tmp/file", 1024)?;
227///     println!("File signature: {:?}", signature);
228///     Ok(())
229/// }
230pub fn read_file_signature<P: AsRef<Path>>(path: P, bytes: usize) -> FileSystemResult<Vec<u8>> {
231    let path = path.as_ref();
232    let file = File::open(path).with_path(path, IoOperation::ReadFile)?;
233
234    let mut reader = BufReader::new(file);
235    let mut buffer = vec![0u8; bytes];
236    reader
237        .read_exact(&mut buffer)
238        .with_path(path, IoOperation::ReadFile)?;
239    Ok(buffer)
240}
241
242/// Calculate the total size in bytes of a directory and all files contained within it.
243///
244/// Skips entries whose directory entry or metadata cannot be read. Recurses into subdirectories
245/// and accumulates file sizes.
246///
247/// # Returns
248///
249/// The total size in bytes of the directory and its contents.
250///
251/// # Errors
252///
253/// Returns a [`FileSystemError::Directory`] if the directory itself cannot be read.
254///
255/// # Examples
256///
257/// ```
258/// use soar_utils::fs::dir_size;
259///
260/// let size = dir_size("/tmp/dir").unwrap_or(0);
261/// println!("Directory size: {}", size);
262/// ```
263pub fn dir_size<P: AsRef<Path>>(path: P) -> FileSystemResult<u64> {
264    let path = path.as_ref();
265    let mut total_size = 0;
266
267    for entry in fs::read_dir(path).with_path(path, IoOperation::ReadDirectory)? {
268        let Ok(entry) = entry else {
269            continue;
270        };
271
272        let Ok(metadata) = entry.metadata() else {
273            continue;
274        };
275
276        if metadata.is_file() {
277            total_size += metadata.len();
278        } else if metadata.is_dir() {
279            total_size += dir_size(entry.path())?;
280        }
281    }
282
283    Ok(total_size)
284}
285
286/// Determine whether the file at the given path is an ELF binary.
287///
288/// Checks the file's first four bytes for the ELF magic sequence (0x7F, 'E', 'L', 'F') and
289/// returns `true` if they match, `false` otherwise.
290///
291/// # Examples
292///
293/// ```
294/// use std::fs::File;
295/// use std::io::Write;
296/// use tempfile::tempdir;
297/// use soar_utils::fs::is_elf;
298///
299/// let dir = tempdir().unwrap();
300/// let path = dir.path().join("example_elf");
301/// let mut f = File::create(&path).unwrap();
302/// f.write_all(&[0x7f, b'E', b'L', b'F', 0x00]).unwrap();
303///
304/// assert!(is_elf(&path));
305/// ```
306pub fn is_elf<P: AsRef<Path>>(path: P) -> bool {
307    read_file_signature(path, 4)
308        .ok()
309        .map(|magic| magic == [0x7f, 0x45, 0x4c, 0x46])
310        .unwrap_or(false)
311}
312
313#[cfg(test)]
314mod tests {
315    use std::{fs::Permissions, os::unix::fs::PermissionsExt};
316
317    use tempfile::tempdir;
318
319    use super::*;
320
321    #[test]
322    fn test_safe_remove_file() {
323        let dir = tempdir().unwrap();
324        let file_path = dir.path().join("test_file.txt");
325        fs::write(&file_path, "hello").unwrap();
326        safe_remove(&file_path).unwrap();
327        assert!(!file_path.exists());
328    }
329
330    #[test]
331    fn test_safe_remove_dir() {
332        let dir = tempdir().unwrap();
333        let sub_dir = dir.path().join("sub");
334        fs::create_dir(&sub_dir).unwrap();
335        safe_remove(&sub_dir).unwrap();
336        assert!(!sub_dir.exists());
337    }
338
339    #[test]
340    fn test_safe_remove_non_existent() {
341        let dir = tempdir().unwrap();
342        let file_path = dir.path().join("non_existent.txt");
343        safe_remove(&file_path).unwrap();
344    }
345
346    #[test]
347    fn test_ensure_dir_exists() {
348        let dir = tempdir().unwrap();
349        let new_dir = dir.path().join("new_dir");
350        ensure_dir_exists(&new_dir).unwrap();
351        assert!(new_dir.is_dir());
352    }
353
354    #[test]
355    fn test_ensure_dir_exists_already_exists() {
356        let dir = tempdir().unwrap();
357        ensure_dir_exists(dir.path()).unwrap();
358        assert!(dir.path().is_dir());
359    }
360
361    #[test]
362    fn test_ensure_dir_exists_file_collision() {
363        let dir = tempdir().unwrap();
364        let file_path = dir.path().join("file.txt");
365        fs::write(&file_path, "hello").unwrap();
366        assert!(ensure_dir_exists(&file_path).is_err());
367    }
368
369    #[test]
370    fn test_ensure_dir_exists_permission_denied() {
371        let dir = tempdir().unwrap();
372        let read_only_dir = dir.path().join("read_only");
373        fs::create_dir(&read_only_dir).unwrap();
374
375        // Set read-only permissions on the directory.
376        let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
377        perms.set_readonly(true);
378        fs::set_permissions(&read_only_dir, perms).unwrap();
379
380        let new_dir = read_only_dir.join("new_dir");
381        let result = ensure_dir_exists(&new_dir);
382        assert!(result.is_err());
383
384        // Cleanup: Set back to writable to allow tempdir to be removed.
385        let mut perms = fs::metadata(&read_only_dir).unwrap().permissions();
386        perms.set_mode(0o755);
387        fs::set_permissions(&read_only_dir, perms).unwrap();
388    }
389
390    #[test]
391    fn test_standard_safe_remove_permission_denied() {
392        let dir = tempdir().unwrap();
393        let sub_dir = dir.path().join("read_only_dir");
394        fs::create_dir(&sub_dir).unwrap();
395        let file_path = sub_dir.join("file.txt");
396        fs::write(&file_path, "content").unwrap();
397
398        // Set read-only permissions on the parent directory.
399        let mut perms = fs::metadata(&sub_dir).unwrap().permissions();
400        perms.set_readonly(true);
401        fs::set_permissions(&sub_dir, perms).unwrap();
402
403        let result = safe_remove(&file_path);
404        assert!(result.is_err());
405
406        // Cleanup: Set back to writable to allow tempdir to be removed.
407        let mut perms = fs::metadata(&sub_dir).unwrap().permissions();
408        perms.set_mode(0o755);
409        fs::set_permissions(&sub_dir, perms).unwrap();
410    }
411
412    #[test]
413    fn test_safe_remove_dir_permission_denied() {
414        let dir = tempdir().unwrap();
415        let sub_dir = dir.path().join("read_only_dir");
416        fs::create_dir(&sub_dir).unwrap();
417        let file_path = sub_dir.join("file.txt");
418        fs::write(&file_path, "content").unwrap();
419
420        // Set read-only permissions on the parent directory.
421        let mut perms = fs::metadata(&sub_dir).unwrap().permissions();
422        perms.set_readonly(true);
423        fs::set_permissions(&sub_dir, perms).unwrap();
424
425        let result = safe_remove(&sub_dir);
426        assert!(result.is_err());
427
428        // Cleanup: Set back to writable to allow tempdir to be removed.
429        let mut perms = fs::metadata(&sub_dir).unwrap().permissions();
430        perms.set_mode(0o755);
431        fs::set_permissions(&sub_dir, perms).unwrap();
432    }
433
434    #[test]
435    fn test_create_symlink() {
436        let dir = tempdir().unwrap();
437        let source = dir.path().join("source");
438        let target = dir.path().join("target");
439        fs::write(&source, "content").unwrap();
440        create_symlink(&source, &target).unwrap();
441        assert!(target.is_symlink());
442        assert_eq!(fs::read_link(&target).unwrap(), source);
443    }
444
445    #[test]
446    fn test_create_symlink_already_exists() {
447        let dir = tempdir().unwrap();
448        let source = dir.path().join("source");
449        let target = dir.path().join("target");
450        fs::write(&source, "content").unwrap();
451        fs::write(&target, "content").unwrap();
452        create_symlink(&source, &target).unwrap();
453        assert!(target.is_symlink());
454        assert_eq!(fs::read_link(&target).unwrap(), source);
455    }
456
457    #[test]
458    fn test_create_symlink_permission_denied() {
459        let dir = tempdir().unwrap();
460        let source = dir.path().join("source");
461        let target = dir.path().join("target");
462        fs::write(&source, "content").unwrap();
463
464        // Set read-only permissions on the parent directory.
465        let mut perms = fs::metadata(dir.path()).unwrap().permissions();
466        perms.set_readonly(true);
467        fs::set_permissions(dir.path(), perms).unwrap();
468
469        let result = create_symlink(&source, &target);
470        assert!(result.is_err());
471
472        // Cleanup: Set back to writable to allow tempdir to be removed.
473        let mut perms = fs::metadata(dir.path()).unwrap().permissions();
474        perms.set_mode(0o755);
475        fs::set_permissions(dir.path(), perms).unwrap();
476    }
477
478    #[test]
479    fn test_walk_dir() {
480        let tempdir = tempfile::tempdir().unwrap();
481        let dir = tempdir.path().join("dir");
482        fs::create_dir(&dir).unwrap();
483        let file = dir.join("file");
484        fs::File::create(&file).unwrap();
485
486        let mut results = Vec::new();
487        walk_dir(&dir, &mut |path| -> FileSystemResult<()> {
488            results.push(path.to_path_buf());
489            Ok(())
490        })
491        .unwrap();
492
493        assert_eq!(results, vec![file]);
494    }
495
496    #[test]
497    fn test_walk_dir_not_a_dir() {
498        let tempdir = tempfile::tempdir().unwrap();
499        let file = tempdir.path().join("file");
500        fs::File::create(&file).unwrap();
501
502        let result = walk_dir(&file, &mut |_| -> FileSystemResult<()> { Ok(()) });
503        assert!(result.is_err());
504    }
505
506    #[test]
507    fn test_walk_recursive_dir() {
508        let tempdir = tempfile::tempdir().unwrap();
509        let dir = tempdir.path().join("dir");
510        fs::create_dir(&dir).unwrap();
511        let file = dir.join("file");
512        File::create(&file).unwrap();
513
514        let nested_dir = dir.join("nested");
515        fs::create_dir(&nested_dir).unwrap();
516        let nested_file = nested_dir.join("file");
517        File::create(&nested_file).unwrap();
518
519        let mut results = Vec::new();
520        walk_dir(&dir, &mut |path| -> FileSystemResult<()> {
521            results.push(path.to_path_buf());
522            Ok(())
523        })
524        .unwrap();
525
526        results.sort();
527        let mut expected = vec![file, nested_file];
528        expected.sort();
529        assert_eq!(results, expected);
530    }
531
532    #[test]
533    fn test_walk_failing_entry() {
534        let tempdir = tempfile::tempdir().unwrap();
535        let dir = tempdir.path().join("dir");
536        fs::create_dir(&dir).unwrap();
537        let file = dir.join("file");
538        File::create(&file).unwrap();
539
540        let mut results = Vec::new();
541        walk_dir(&dir, &mut |path| {
542            results.push(path.to_path_buf());
543            Err(FileSystemError::ReadFile {
544                path: path.to_path_buf(),
545                source: std::io::Error::from(std::io::ErrorKind::Other),
546            })
547        })
548        .ok();
549
550        assert_eq!(results, vec![file]);
551    }
552
553    #[test]
554    fn test_walk_invalid_dir() {
555        let result = walk_dir("/this/path/does/not/exist", &mut |_| -> FileSystemResult<
556            (),
557        > { Ok(()) });
558        assert!(result.is_err());
559    }
560
561    #[test]
562    fn test_walk_dir_permission_denied() {
563        let tempdir = tempfile::tempdir().unwrap();
564        let dir = tempdir.path();
565
566        fs::set_permissions(dir, Permissions::from_mode(0o000)).unwrap();
567
568        let result = walk_dir(dir, &mut |_| -> FileSystemResult<()> { Ok(()) });
569
570        fs::set_permissions(dir, Permissions::from_mode(0o755)).unwrap();
571        assert!(result.is_err());
572    }
573
574    #[test]
575    fn test_walk_dir_permission_denied_recursive() {
576        let tempdir = tempfile::tempdir().unwrap();
577        let dir = tempdir.path();
578        let nested_dir = dir.join("nested");
579        fs::create_dir(&nested_dir).unwrap();
580
581        fs::set_permissions(&nested_dir, Permissions::from_mode(0o000)).unwrap();
582
583        let result = walk_dir(dir, &mut |_| -> FileSystemResult<()> { Ok(()) });
584
585        fs::set_permissions(nested_dir, Permissions::from_mode(0o755)).unwrap();
586        assert!(result.is_err());
587    }
588
589    #[test]
590    fn test_read_file_signature() {
591        let tempdir = tempfile::tempdir().unwrap();
592        let file = tempdir.path().join("file");
593        File::create(&file).unwrap();
594        fs::write(&file, b"sample test content").unwrap();
595
596        let signature = read_file_signature(&file, 8).unwrap();
597        assert_eq!(signature.len(), 8);
598        assert_eq!(signature, b"sample t");
599    }
600
601    #[test]
602    fn test_read_file_signature_empty() {
603        let tempdir = tempfile::tempdir().unwrap();
604        let file = tempdir.path().join("file");
605        File::create(&file).unwrap();
606
607        let signature = read_file_signature(&file, 0).unwrap();
608        assert!(signature.is_empty());
609    }
610
611    #[test]
612    fn test_read_file_signature_invalid() {
613        let tempdir = tempfile::tempdir().unwrap();
614        let file = tempdir.path().join("file");
615        File::create(&file).unwrap();
616
617        let result = read_file_signature(&file, 1024);
618        assert!(result.is_err());
619    }
620
621    #[test]
622    fn test_read_file_signature_non_existent() {
623        let result = read_file_signature("/this/path/does/not/exist", 1024);
624        assert!(result.is_err());
625    }
626
627    #[test]
628    fn test_calculate_directory_size() {
629        let tempdir = tempfile::tempdir().unwrap();
630        let dir = tempdir.path().join("dir");
631        fs::create_dir(&dir).unwrap();
632
633        let file = dir.join("file");
634        File::create(&file).unwrap();
635        fs::write(&file, b"sample test content").unwrap(); // 19 bytes
636
637        let nested_dir = dir.join("nested");
638        fs::create_dir(&nested_dir).unwrap();
639
640        let nested_file = nested_dir.join("file");
641        File::create(&nested_file).unwrap();
642        fs::write(&nested_file, b"sample test content").unwrap();
643
644        let size = dir_size(&dir).unwrap();
645        assert_eq!(size, 38);
646    }
647
648    #[test]
649    fn test_calculate_directory_size_empty() {
650        let tempdir = tempfile::tempdir().unwrap();
651        let dir = tempdir.path().join("dir");
652        fs::create_dir(&dir).unwrap();
653
654        let size = dir_size(&dir).unwrap();
655        assert_eq!(size, 0);
656    }
657
658    #[test]
659    fn test_calculate_directory_size_invalid() {
660        let result = dir_size("/this/path/does/not/exist");
661        assert!(result.is_err());
662    }
663
664    #[test]
665    fn test_calculate_directory_size_inner_permission_denied() {
666        let tempdir = tempfile::tempdir().unwrap();
667        let dir = tempdir.path();
668        let inner_dir = dir.join("inner");
669        ensure_dir_exists(&inner_dir).unwrap();
670
671        fs::set_permissions(&inner_dir, Permissions::from_mode(0o000)).unwrap();
672
673        let result = dir_size(dir);
674        assert!(result.is_err());
675
676        // Cleanup: Set back to writable to allow tempdir to be removed.
677        fs::set_permissions(inner_dir, Permissions::from_mode(0o755)).unwrap();
678    }
679
680    #[test]
681    fn test_create_symlink_inner_target() {
682        let tempdir = tempfile::tempdir().unwrap();
683        let source = tempdir.path().join("source");
684        let target = tempdir.path().join("inner").join("target");
685
686        let result = create_symlink(&source, &target);
687        assert!(result.is_ok());
688    }
689
690    #[test]
691    fn test_create_symlink_target_invalid_parent() {
692        let tempdir = tempfile::tempdir().unwrap();
693        let source = tempdir.path().join("source");
694
695        let file = tempdir.path().join("file");
696        File::create(&file).unwrap();
697        let target = tempdir.path().join("file").join("target");
698
699        let result = create_symlink(&source, &target);
700        assert!(result.is_err());
701    }
702
703    #[test]
704    fn test_create_symlink_target_no_permissions() {
705        let tempdir = tempfile::tempdir().unwrap();
706        let source = tempdir.path().join("source");
707        let target = tempdir.path().join("target");
708        File::create(&target).unwrap();
709
710        // Set read-only permissions on the parent directory.
711        let mut perms = fs::metadata(tempdir.path()).unwrap().permissions();
712        perms.set_readonly(true);
713        fs::set_permissions(tempdir.path(), perms).unwrap();
714
715        let result = create_symlink(&source, &target);
716        assert!(result.is_err());
717
718        // Cleanup: Set back to writable to allow tempdir to be removed.
719        let mut perms = fs::metadata(tempdir.path()).unwrap().permissions();
720        perms.set_mode(0o755);
721        fs::set_permissions(tempdir.path(), perms).unwrap();
722    }
723}