Skip to main content

shadow_crypt_shell/decryption/
validation.rs

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