shadow_crypt_shell/decryption/
validation.rs1use 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 assert!(result.is_err()); }
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()); }
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 };
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); }
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 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 let mut header = b"SHADOW".to_vec();
242 header.push(99); 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 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 }
273
274 #[test]
275 fn test_validate_input_directory_instead_of_file() {
276 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}