ggen_cli_validation/
io_validator.rs1use crate::error::{Result, ValidationError};
7use crate::security::{Permission, PermissionModel};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug)]
13pub struct IoValidator {
14 permission_model: PermissionModel,
16}
17
18impl Default for IoValidator {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl IoValidator {
25 #[must_use]
27 pub fn new() -> Self {
28 Self {
29 permission_model: PermissionModel::new(),
30 }
31 }
32
33 #[must_use]
35 pub fn with_permissions(permission_model: PermissionModel) -> Self {
36 Self { permission_model }
37 }
38
39 pub fn validate_read(&self, path: &Path) -> Result<()> {
46 self.permission_model
48 .check_permission(path, Permission::Read)?;
49
50 if !path.exists() {
52 return Err(ValidationError::FileNotFound {
53 path: path.display().to_string(),
54 });
55 }
56
57 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 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 pub fn validate_write(&self, path: &Path) -> Result<()> {
81 self.permission_model
83 .check_permission(path, Permission::Write)?;
84
85 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 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 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 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#[derive(Debug)]
141pub struct PathValidation {
142 pub path: std::path::PathBuf,
144 pub valid: bool,
146 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}