Skip to main content

cc_audit/types/
paths.rs

1//! Path-related NewType wrappers with validation.
2
3use std::path::{Path, PathBuf};
4use thiserror::Error;
5
6/// Error type for path validation failures.
7#[derive(Error, Debug, Clone)]
8pub enum PathValidationError {
9    #[error("Path not found: {0}")]
10    NotFound(PathBuf),
11
12    #[error("Path is not a file: {0}")]
13    NotAFile(PathBuf),
14
15    #[error("Path is not a directory: {0}")]
16    NotADirectory(PathBuf),
17
18    #[error("Path is not readable: {0}")]
19    NotReadable(PathBuf),
20}
21
22/// A validated scan target path.
23///
24/// Ensures the path exists at construction time.
25#[derive(Debug, Clone)]
26pub struct ScanTarget {
27    path: PathBuf,
28}
29
30impl ScanTarget {
31    /// Create a new ScanTarget after validating the path exists.
32    pub fn new(path: impl AsRef<Path>) -> Result<Self, PathValidationError> {
33        let path = path.as_ref().to_path_buf();
34        if !path.exists() {
35            return Err(PathValidationError::NotFound(path));
36        }
37        Ok(Self { path })
38    }
39
40    /// Create a ScanTarget for a file, validating it's actually a file.
41    pub fn file(path: impl AsRef<Path>) -> Result<Self, PathValidationError> {
42        let path = path.as_ref().to_path_buf();
43        if !path.exists() {
44            return Err(PathValidationError::NotFound(path));
45        }
46        if !path.is_file() {
47            return Err(PathValidationError::NotAFile(path));
48        }
49        Ok(Self { path })
50    }
51
52    /// Create a ScanTarget for a directory, validating it's actually a directory.
53    pub fn directory(path: impl AsRef<Path>) -> Result<Self, PathValidationError> {
54        let path = path.as_ref().to_path_buf();
55        if !path.exists() {
56            return Err(PathValidationError::NotFound(path));
57        }
58        if !path.is_dir() {
59            return Err(PathValidationError::NotADirectory(path));
60        }
61        Ok(Self { path })
62    }
63
64    /// Create a ScanTarget without validation (for testing or trusted paths).
65    ///
66    /// # Safety
67    /// The caller must ensure the path is valid for the intended use.
68    pub fn unchecked(path: impl AsRef<Path>) -> Self {
69        Self {
70            path: path.as_ref().to_path_buf(),
71        }
72    }
73
74    /// Get the underlying path reference.
75    pub fn path(&self) -> &Path {
76        &self.path
77    }
78
79    /// Get the path as a PathBuf.
80    pub fn to_path_buf(&self) -> PathBuf {
81        self.path.clone()
82    }
83
84    /// Consume self and return the inner PathBuf.
85    pub fn into_path_buf(self) -> PathBuf {
86        self.path
87    }
88
89    /// Check if the target is a file.
90    pub fn is_file(&self) -> bool {
91        self.path.is_file()
92    }
93
94    /// Check if the target is a directory.
95    pub fn is_dir(&self) -> bool {
96        self.path.is_dir()
97    }
98
99    /// Get the file name component if present.
100    pub fn file_name(&self) -> Option<&std::ffi::OsStr> {
101        self.path.file_name()
102    }
103
104    /// Get the parent directory if present.
105    pub fn parent(&self) -> Option<&Path> {
106        self.path.parent()
107    }
108}
109
110impl AsRef<Path> for ScanTarget {
111    fn as_ref(&self) -> &Path {
112        &self.path
113    }
114}
115
116impl From<ScanTarget> for PathBuf {
117    fn from(target: ScanTarget) -> Self {
118        target.path
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use std::fs;
126    use tempfile::tempdir;
127
128    #[test]
129    fn test_scan_target_valid_path() {
130        let dir = tempdir().unwrap();
131        let target = ScanTarget::new(dir.path());
132        assert!(target.is_ok());
133        assert!(target.unwrap().is_dir());
134    }
135
136    #[test]
137    fn test_scan_target_invalid_path() {
138        let result = ScanTarget::new("/nonexistent/path/12345");
139        assert!(result.is_err());
140        assert!(matches!(result, Err(PathValidationError::NotFound(_))));
141    }
142
143    #[test]
144    fn test_scan_target_file() {
145        let dir = tempdir().unwrap();
146        let file_path = dir.path().join("test.txt");
147        fs::write(&file_path, "test").unwrap();
148
149        let target = ScanTarget::file(&file_path);
150        assert!(target.is_ok());
151        assert!(target.unwrap().is_file());
152    }
153
154    #[test]
155    fn test_scan_target_file_on_directory() {
156        let dir = tempdir().unwrap();
157        let result = ScanTarget::file(dir.path());
158        assert!(result.is_err());
159        assert!(matches!(result, Err(PathValidationError::NotAFile(_))));
160    }
161
162    #[test]
163    fn test_scan_target_directory() {
164        let dir = tempdir().unwrap();
165        let target = ScanTarget::directory(dir.path());
166        assert!(target.is_ok());
167        assert!(target.unwrap().is_dir());
168    }
169
170    #[test]
171    fn test_scan_target_directory_on_file() {
172        let dir = tempdir().unwrap();
173        let file_path = dir.path().join("test.txt");
174        fs::write(&file_path, "test").unwrap();
175
176        let result = ScanTarget::directory(&file_path);
177        assert!(result.is_err());
178        assert!(matches!(result, Err(PathValidationError::NotADirectory(_))));
179    }
180
181    #[test]
182    fn test_scan_target_unchecked() {
183        let target = ScanTarget::unchecked("/any/path");
184        assert_eq!(target.path(), Path::new("/any/path"));
185    }
186
187    #[test]
188    fn test_scan_target_into_path_buf() {
189        let dir = tempdir().unwrap();
190        let target = ScanTarget::new(dir.path()).unwrap();
191        let path_buf: PathBuf = target.into();
192        assert_eq!(path_buf, dir.path());
193    }
194}