use crate::error::{Result, ValidationError};
use crate::security::{Permission, PermissionModel};
use std::fs;
use std::path::Path;
#[derive(Debug)]
pub struct IoValidator {
permission_model: PermissionModel,
}
impl Default for IoValidator {
fn default() -> Self {
Self::new()
}
}
impl IoValidator {
#[must_use]
pub fn new() -> Self {
Self {
permission_model: PermissionModel::new(),
}
}
#[must_use]
pub fn with_permissions(permission_model: PermissionModel) -> Self {
Self { permission_model }
}
pub fn validate_read(&self, path: &Path) -> Result<()> {
self.permission_model
.check_permission(path, Permission::Read)?;
if !path.exists() {
return Err(ValidationError::FileNotFound {
path: path.display().to_string(),
});
}
if !path.is_file() {
return Err(ValidationError::InvalidPath {
path: path.display().to_string(),
reason: "Path is not a file".to_string(),
});
}
fs::File::open(path).map_err(|e| ValidationError::ReadFailed {
path: path.display().to_string(),
reason: e.to_string(),
})?;
Ok(())
}
pub fn validate_write(&self, path: &Path) -> Result<()> {
self.permission_model
.check_permission(path, Permission::Write)?;
if let Some(parent) = path.parent() {
if !parent.exists() {
return Err(ValidationError::InvalidPath {
path: path.display().to_string(),
reason: format!("Parent directory {} does not exist", parent.display()),
});
}
if let Ok(metadata) = fs::metadata(parent) {
if metadata.permissions().readonly() {
return Err(ValidationError::WriteFailed {
path: path.display().to_string(),
reason: "Parent directory is read-only".to_string(),
});
}
}
}
Ok(())
}
pub fn validate_reads(&self, paths: &[&Path]) -> Result<Vec<PathValidation>> {
Ok(paths
.iter()
.map(|path| {
let result = self.validate_read(path);
PathValidation {
path: path.to_path_buf(),
valid: result.is_ok(),
error: result.err(),
}
})
.collect())
}
pub fn validate_writes(&self, paths: &[&Path]) -> Result<Vec<PathValidation>> {
Ok(paths
.iter()
.map(|path| {
let result = self.validate_write(path);
PathValidation {
path: path.to_path_buf(),
valid: result.is_ok(),
error: result.err(),
}
})
.collect())
}
}
#[derive(Debug)]
pub struct PathValidation {
pub path: std::path::PathBuf,
pub valid: bool,
pub error: Option<ValidationError>,
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::io::Write;
use tempfile::tempdir;
#[allow(clippy::expect_used)]
#[test]
fn test_validate_read_existing_file() {
let dir = tempdir().expect("Failed to create temp dir");
let file_path = dir.path().join("test.txt");
let mut file = File::create(&file_path).expect("Failed to create file");
writeln!(file, "test content").expect("Failed to write");
let validator = IoValidator::new();
assert!(validator.validate_read(&file_path).is_ok());
}
#[allow(clippy::expect_used)]
#[test]
fn test_validate_read_missing_file() {
let validator = IoValidator::new();
let result = validator.validate_read(Path::new("/nonexistent/file.txt"));
assert!(result.is_err());
assert!(matches!(result, Err(ValidationError::FileNotFound { .. })));
}
#[allow(clippy::expect_used)]
#[test]
fn test_validate_write_existing_directory() {
let dir = tempdir().expect("Failed to create temp dir");
let file_path = dir.path().join("output.txt");
let validator = IoValidator::new();
assert!(validator.validate_write(&file_path).is_ok());
}
#[allow(clippy::expect_used)]
#[test]
fn test_validate_write_missing_parent() {
let validator = IoValidator::new();
let result = validator.validate_write(Path::new("/nonexistent/dir/file.txt"));
assert!(result.is_err());
}
#[allow(clippy::expect_used)]
#[test]
fn test_batch_read_validation() {
let dir = tempdir().expect("Failed to create temp dir");
let file1 = dir.path().join("file1.txt");
let file2 = dir.path().join("file2.txt");
File::create(&file1).expect("Failed to create file1");
File::create(&file2).expect("Failed to create file2");
let validator = IoValidator::new();
let paths = vec![file1.as_path(), file2.as_path()];
let results = validator.validate_reads(&paths);
assert!(results.is_ok());
let validations = results.expect("Validation should succeed");
assert_eq!(validations.len(), 2);
assert!(validations.iter().all(|v| v.valid));
}
}