shadow_crypt_shell/decryption/
validation.rs

1use std::path::PathBuf;
2
3use shadow_crypt_core::{
4    v1::header_ops::{get_version_from_bytes, is_shadow_file},
5    version::is_supported_version,
6};
7
8use crate::{
9    decryption::{cli::DecryptionCliArgs, file::DecryptionInputFile},
10    errors::{WorkflowError, WorkflowResult},
11    utils::read_n_bytes_from_file,
12};
13
14#[derive(Debug)]
15pub struct ValidDecryptionArgs {
16    pub files: Vec<DecryptionInputFile>,
17}
18
19pub fn validate_input(input: DecryptionCliArgs) -> WorkflowResult<ValidDecryptionArgs> {
20    ensure_not_empty(&input)?;
21
22    let validated_files: Vec<DecryptionInputFile> = input
23        .input_files
24        .iter()
25        .map(PathBuf::from)
26        .map(ensure_exists)
27        .map(ensure_is_regular_file)
28        .map(ensure_is_encrypted_shadow_file)
29        .map(ensure_version_supported)
30        .map(create_input_file)
31        .collect::<WorkflowResult<Vec<DecryptionInputFile>>>()?;
32
33    Ok(ValidDecryptionArgs {
34        files: validated_files,
35    })
36}
37
38fn ensure_not_empty(input: &DecryptionCliArgs) -> WorkflowResult<()> {
39    if input.input_files.is_empty() {
40        return Err(WorkflowError::UserInput(
41            "No input files provided".to_string(),
42        ));
43    }
44    Ok(())
45}
46
47fn ensure_exists(path: PathBuf) -> WorkflowResult<PathBuf> {
48    if !path.exists() {
49        return Err(WorkflowError::UserInput(format!(
50            "Input file does not exist: {}",
51            path.display()
52        )));
53    }
54    Ok(path)
55}
56
57fn ensure_is_regular_file(path: WorkflowResult<PathBuf>) -> WorkflowResult<PathBuf> {
58    if let Ok(path) = &path
59        && !path.is_file()
60    {
61        return Err(WorkflowError::UserInput(format!(
62            "Input path is not a file: {}",
63            path.display()
64        )));
65    }
66    path
67}
68
69fn ensure_is_encrypted_shadow_file(path: WorkflowResult<PathBuf>) -> WorkflowResult<PathBuf> {
70    let path = path?;
71    let first_bytes = read_n_bytes_from_file(&path, 10)?;
72    if !is_shadow_file(first_bytes.as_slice())? {
73        return Err(WorkflowError::UserInput(format!(
74            "File is not a valid Shadow encrypted file: {}",
75            path.display()
76        )));
77    }
78    Ok(path)
79}
80
81fn ensure_version_supported(path: WorkflowResult<PathBuf>) -> WorkflowResult<PathBuf> {
82    let path = path?;
83    let first_bytes = read_n_bytes_from_file(&path, 10)?;
84    let version: u8 = get_version_from_bytes(first_bytes.as_slice()).map_err(|_| {
85        WorkflowError::UserInput(format!(
86            "Unable to read version from file: {}",
87            path.display()
88        ))
89    })?;
90    if !is_supported_version(version) {
91        return Err(WorkflowError::UserInput(format!(
92            "Unsupported Shadow file version in file: {}",
93            path.display()
94        )));
95    }
96    Ok(path)
97}
98
99fn create_input_file(path: WorkflowResult<PathBuf>) -> WorkflowResult<DecryptionInputFile> {
100    let path = path?;
101    let name: String = path
102        .file_name()
103        .and_then(|n| n.to_str())
104        .ok_or_else(|| {
105            WorkflowError::UserInput(format!("Invalid filename for path: {}", path.display()))
106        })?
107        .to_string();
108    let size: u64 = path
109        .metadata()
110        .map_err(|_| {
111            WorkflowError::UserInput(format!(
112                "Unable to read metadata for file: {}",
113                path.display()
114            ))
115        })?
116        .len();
117
118    Ok(DecryptionInputFile {
119        path,
120        filename: name,
121        size,
122    })
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    use crate::decryption::cli::DecryptionCliArgs;
129    use std::fs;
130    use tempfile::TempDir;
131
132    #[test]
133    fn test_validate_input_no_files() {
134        let args = DecryptionCliArgs {
135            input_files: vec![],
136        };
137        let result = validate_input(args);
138        assert!(result.is_err());
139    }
140
141    #[test]
142    fn test_validate_input_file_does_not_exist() {
143        let args = DecryptionCliArgs {
144            input_files: vec!["nonexistent.txt".to_string()],
145        };
146        let result = validate_input(args);
147        assert!(result.is_err());
148    }
149
150    #[test]
151    fn test_validate_input_path_is_directory() {
152        let temp_dir = TempDir::new().unwrap();
153        let args = DecryptionCliArgs {
154            input_files: vec![temp_dir.path().to_str().unwrap().to_string()],
155        };
156        let result = validate_input(args);
157        assert!(result.is_err());
158    }
159
160    #[test]
161    fn test_validate_input_valid_file() {
162        let temp_dir = TempDir::new().unwrap();
163        let file_path = temp_dir.path().join("test.txt");
164        fs::write(&file_path, b"test").unwrap();
165
166        let args = DecryptionCliArgs {
167            input_files: vec![file_path.to_str().unwrap().to_string()],
168        };
169        let result = validate_input(args);
170        // This will fail because the file is not a shadow file, but that's tested elsewhere
171        // We just want to ensure the validation pipeline works for valid file paths
172        assert!(result.is_err()); // Expected to fail at shadow file check
173    }
174
175    #[test]
176    fn test_validate_input_multiple_files() {
177        let temp_dir = TempDir::new().unwrap();
178        let file1 = temp_dir.path().join("test1.txt");
179        let file2 = temp_dir.path().join("test2.txt");
180        fs::write(&file1, b"test1").unwrap();
181        fs::write(&file2, b"test2").unwrap();
182
183        let args = DecryptionCliArgs {
184            input_files: vec![
185                file1.to_str().unwrap().to_string(),
186                file2.to_str().unwrap().to_string(),
187            ],
188        };
189        let result = validate_input(args);
190        assert!(result.is_err()); // Expected to fail at shadow file check
191    }
192
193    #[test]
194    fn test_validate_input_valid_shadow_file() {
195        let temp_dir = TempDir::new().unwrap();
196        let file_path = temp_dir.path().join("test.shadow");
197        // Create a valid shadow file header: "SHADOW" + version 1 + some dummy data
198        let mut header = b"SHADOW".to_vec();
199        header.push(1); // version 1
200        // Add minimal header data to make it valid (at least 10 bytes total)
201        header.extend_from_slice(&[0u8; 4]); // dummy data
202        fs::write(&file_path, header).unwrap();
203
204        let args = DecryptionCliArgs {
205            input_files: vec![file_path.to_str().unwrap().to_string()],
206        };
207        let result = validate_input(args);
208        assert!(result.is_ok());
209        let valid_args = result.unwrap();
210        assert_eq!(valid_args.files.len(), 1);
211        assert_eq!(valid_args.files[0].filename, "test.shadow");
212        assert_eq!(valid_args.files[0].size, 11); // "SHADOW" (6) + version (1) + dummy (4)
213    }
214
215    #[test]
216    fn test_validate_input_invalid_magic_bytes() {
217        let temp_dir = TempDir::new().unwrap();
218        let file_path = temp_dir.path().join("invalid.txt");
219        // Write invalid magic bytes
220        fs::write(&file_path, b"INVALID").unwrap();
221
222        let args = DecryptionCliArgs {
223            input_files: vec![file_path.to_str().unwrap().to_string()],
224        };
225        let result = validate_input(args);
226        assert!(result.is_err());
227        let err = result.unwrap_err();
228        match err {
229            WorkflowError::UserInput(msg) => {
230                assert!(msg.contains("not a valid Shadow encrypted file"))
231            }
232            _ => panic!("Expected UserInput error"),
233        }
234    }
235
236    #[test]
237    fn test_validate_input_unsupported_version() {
238        let temp_dir = TempDir::new().unwrap();
239        let file_path = temp_dir.path().join("unsupported.shadow");
240        // Create file with "SHADOW" but unsupported version (e.g., 99)
241        let mut header = b"SHADOW".to_vec();
242        header.push(99); // unsupported version
243        fs::write(&file_path, header).unwrap();
244
245        let args = DecryptionCliArgs {
246            input_files: vec![file_path.to_str().unwrap().to_string()],
247        };
248        let result = validate_input(args);
249        assert!(result.is_err());
250        let err = result.unwrap_err();
251        match err {
252            WorkflowError::UserInput(msg) => {
253                assert!(msg.contains("Unsupported Shadow file version"))
254            }
255            _ => panic!("Expected UserInput error"),
256        }
257    }
258
259    #[test]
260    fn test_validate_input_insufficient_bytes() {
261        let temp_dir = TempDir::new().unwrap();
262        let file_path = temp_dir.path().join("short.txt");
263        // Write only 5 bytes, less than needed for header validation
264        fs::write(&file_path, b"SHORT").unwrap();
265
266        let args = DecryptionCliArgs {
267            input_files: vec![file_path.to_str().unwrap().to_string()],
268        };
269        let result = validate_input(args);
270        assert!(result.is_err());
271        // This should fail at the magic bytes check due to insufficient bytes
272    }
273
274    #[test]
275    fn test_validate_input_directory_instead_of_file() {
276        // Test with a directory path instead of a file
277        let temp_dir = TempDir::new().unwrap();
278
279        let args = DecryptionCliArgs {
280            input_files: vec![temp_dir.path().to_str().unwrap().to_string()],
281        };
282        let result = validate_input(args);
283        assert!(result.is_err());
284        let err = result.unwrap_err();
285        match err {
286            WorkflowError::UserInput(msg) => assert!(msg.contains("Input path is not a file")),
287            _ => panic!("Expected UserInput error"),
288        }
289    }
290}