shadow_crypt_shell/decryption/
validation.rs1use 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
65fn 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 assert!(result.is_err()); }
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()); }
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 let mut header = b"SHADOW".to_vec();
186 header.push(1); header.extend_from_slice(&[0u8; 4]); 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); }
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 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 let mut header = b"SHADOW".to_vec();
229 header.push(99); 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 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 }
260
261 #[test]
262 fn test_validate_input_directory_instead_of_file() {
263 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}