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 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
73fn 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 assert!(result.is_err()); }
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()); }
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 let mut header = b"SHADOW".to_vec();
199 header.push(1); header.extend_from_slice(&[0u8; 4]); 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); }
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 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 let mut header = b"SHADOW".to_vec();
244 header.push(99); 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 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 }
277
278 #[test]
279 fn test_validate_input_directory_instead_of_file() {
280 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}