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