ggen_cli_validation/
io_validator.rs

1//! IO validation for file operations
2//!
3//! Validates read and write operations before execution,
4//! preventing common errors and security issues.
5
6use crate::error::{Result, ValidationError};
7use crate::security::{Permission, PermissionModel};
8use std::fs;
9use std::path::Path;
10
11/// IO validator for file operations
12#[derive(Debug)]
13pub struct IoValidator {
14    /// Permission model for security checks
15    permission_model: PermissionModel,
16}
17
18impl Default for IoValidator {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl IoValidator {
25    /// Create a new IO validator with default permission model
26    #[must_use]
27    pub fn new() -> Self {
28        Self {
29            permission_model: PermissionModel::new(),
30        }
31    }
32
33    /// Create an IO validator with custom permission model
34    #[must_use]
35    pub fn with_permissions(permission_model: PermissionModel) -> Self {
36        Self { permission_model }
37    }
38
39    /// Validate a read operation
40    ///
41    /// Checks:
42    /// - File exists
43    /// - Read permissions
44    /// - Path traversal prevention
45    pub fn validate_read(&self, path: &Path) -> Result<()> {
46        // Check permissions first
47        self.permission_model
48            .check_permission(path, Permission::Read)?;
49
50        // Check file exists
51        if !path.exists() {
52            return Err(ValidationError::FileNotFound {
53                path: path.display().to_string(),
54            });
55        }
56
57        // Check it's actually a file (not a directory)
58        if !path.is_file() {
59            return Err(ValidationError::InvalidPath {
60                path: path.display().to_string(),
61                reason: "Path is not a file".to_string(),
62            });
63        }
64
65        // Try to open for reading
66        fs::File::open(path).map_err(|e| ValidationError::ReadFailed {
67            path: path.display().to_string(),
68            reason: e.to_string(),
69        })?;
70
71        Ok(())
72    }
73
74    /// Validate a write operation
75    ///
76    /// Checks:
77    /// - Parent directory exists
78    /// - Write permissions
79    /// - Path traversal prevention
80    pub fn validate_write(&self, path: &Path) -> Result<()> {
81        // Check permissions first
82        self.permission_model
83            .check_permission(path, Permission::Write)?;
84
85        // Check parent directory exists
86        if let Some(parent) = path.parent() {
87            if !parent.exists() {
88                return Err(ValidationError::InvalidPath {
89                    path: path.display().to_string(),
90                    reason: format!("Parent directory {} does not exist", parent.display()),
91                });
92            }
93
94            // Check parent is writable
95            if let Ok(metadata) = fs::metadata(parent) {
96                if metadata.permissions().readonly() {
97                    return Err(ValidationError::WriteFailed {
98                        path: path.display().to_string(),
99                        reason: "Parent directory is read-only".to_string(),
100                    });
101                }
102            }
103        }
104
105        Ok(())
106    }
107
108    /// Validate multiple read operations
109    pub fn validate_reads(&self, paths: &[&Path]) -> Result<Vec<PathValidation>> {
110        Ok(paths
111            .iter()
112            .map(|path| {
113                let result = self.validate_read(path);
114                PathValidation {
115                    path: path.to_path_buf(),
116                    valid: result.is_ok(),
117                    error: result.err(),
118                }
119            })
120            .collect())
121    }
122
123    /// Validate multiple write operations
124    pub fn validate_writes(&self, paths: &[&Path]) -> Result<Vec<PathValidation>> {
125        Ok(paths
126            .iter()
127            .map(|path| {
128                let result = self.validate_write(path);
129                PathValidation {
130                    path: path.to_path_buf(),
131                    valid: result.is_ok(),
132                    error: result.err(),
133                }
134            })
135            .collect())
136    }
137}
138
139/// Result of path validation
140#[derive(Debug)]
141pub struct PathValidation {
142    /// The path being validated
143    pub path: std::path::PathBuf,
144    /// Whether the path is valid
145    pub valid: bool,
146    /// Error if validation failed
147    pub error: Option<ValidationError>,
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use std::fs::File;
154    use std::io::Write;
155    use tempfile::tempdir;
156
157    #[allow(clippy::expect_used)]
158    #[test]
159    fn test_validate_read_existing_file() {
160        let dir = tempdir().expect("Failed to create temp dir");
161        let file_path = dir.path().join("test.txt");
162        let mut file = File::create(&file_path).expect("Failed to create file");
163        writeln!(file, "test content").expect("Failed to write");
164
165        let validator = IoValidator::new();
166        assert!(validator.validate_read(&file_path).is_ok());
167    }
168
169    #[allow(clippy::expect_used)]
170    #[test]
171    fn test_validate_read_missing_file() {
172        let validator = IoValidator::new();
173        let result = validator.validate_read(Path::new("/nonexistent/file.txt"));
174        assert!(result.is_err());
175        assert!(matches!(result, Err(ValidationError::FileNotFound { .. })));
176    }
177
178    #[allow(clippy::expect_used)]
179    #[test]
180    fn test_validate_write_existing_directory() {
181        let dir = tempdir().expect("Failed to create temp dir");
182        let file_path = dir.path().join("output.txt");
183
184        let validator = IoValidator::new();
185        assert!(validator.validate_write(&file_path).is_ok());
186    }
187
188    #[allow(clippy::expect_used)]
189    #[test]
190    fn test_validate_write_missing_parent() {
191        let validator = IoValidator::new();
192        let result = validator.validate_write(Path::new("/nonexistent/dir/file.txt"));
193        assert!(result.is_err());
194    }
195
196    #[allow(clippy::expect_used)]
197    #[test]
198    fn test_batch_read_validation() {
199        let dir = tempdir().expect("Failed to create temp dir");
200        let file1 = dir.path().join("file1.txt");
201        let file2 = dir.path().join("file2.txt");
202
203        File::create(&file1).expect("Failed to create file1");
204        File::create(&file2).expect("Failed to create file2");
205
206        let validator = IoValidator::new();
207        let paths = vec![file1.as_path(), file2.as_path()];
208        let results = validator.validate_reads(&paths);
209
210        assert!(results.is_ok());
211        let validations = results.expect("Validation should succeed");
212        assert_eq!(validations.len(), 2);
213        assert!(validations.iter().all(|v| v.valid));
214    }
215}