shadow_crypt_shell/listing/
file_ops.rs1use std::{fs, path::Path};
2
3use shadow_crypt_core::{
4 memory::SecureBytes,
5 v1, v2,
6 version::{PREAMBLE_LENGTH, Version, read_file_version},
7};
8
9use crate::{
10 errors::{WorkflowError, WorkflowResult},
11 listing::file::ShadowFile,
12 utils::read_n_bytes_from_file,
13};
14
15pub fn scan_directory_for_shadow_files(dir_path: &Path) -> WorkflowResult<Vec<ShadowFile>> {
16 if !dir_path.is_dir() {
17 return Err(WorkflowError::Listing(format!(
18 "The path '{}' is not a directory.",
19 dir_path.display()
20 )));
21 }
22
23 let files = fs::read_dir(dir_path)?
24 .filter_map(|entry| entry.ok()) .map(|entry| entry.path())
26 .filter(|path| path.is_file()) .collect::<Vec<_>>();
28
29 let shadow_files: Vec<ShadowFile> = files
30 .iter()
31 .filter_map(|path| try_create_shadow_file(path).ok())
32 .collect();
33
34 Ok(shadow_files)
35}
36
37fn try_create_shadow_file(path: &Path) -> WorkflowResult<ShadowFile> {
38 let preamble = read_n_bytes_from_file(path, PREAMBLE_LENGTH)?;
39
40 let version = read_file_version(preamble.as_slice()).map_err(|_| {
41 WorkflowError::Listing(format!(
42 "The file '{}' is not a supported Shadow file.",
43 path.display()
44 ))
45 })?;
46 let filename = path
47 .file_name()
48 .ok_or_else(|| {
49 WorkflowError::Listing(format!(
50 "Failed to get filename for file '{}'.",
51 path.display()
52 ))
53 })?
54 .to_string_lossy()
55 .to_string();
56
57 Ok(ShadowFile::new(
58 path.to_path_buf(),
59 filename,
60 version,
61 get_file_size(path)?,
62 ))
63}
64
65fn get_file_size(path: &Path) -> WorkflowResult<u64> {
66 let metadata = fs::metadata(path)?;
67 Ok(metadata.len())
68}
69
70pub fn load_file_header_bytes(file: &ShadowFile) -> WorkflowResult<SecureBytes> {
73 let header_length = match file.version {
76 Version::V1 => {
77 let fixed = read_n_bytes_from_file(&file.path, v1::header::FileHeader::min_length())?;
78 v1::header_ops::get_length_from_bytes(fixed.as_slice())?
79 }
80 Version::V2 => {
81 let fixed = read_n_bytes_from_file(&file.path, v2::header::FileHeader::min_length())?;
82 v2::header_ops::get_length_from_bytes(fixed.as_slice())?
83 }
84 };
85 read_n_bytes_from_file(&file.path, header_length as usize)
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use shadow_crypt_core::{profile::SecurityProfile, v1, v2, version::Version};
92 use std::fs;
93 use tempfile::TempDir;
94
95 fn create_test_header_v1() -> v1::header::FileHeader {
96 v1::header::FileHeader::new(
97 [1u8; 16],
98 v1::key::KeyDerivationParams::from(SecurityProfile::Test),
99 [2u8; 24],
100 [3u8; 24],
101 vec![4, 5, 6, 7, 8],
102 )
103 }
104
105 fn create_test_header_v2() -> v2::header::FileHeader {
106 v2::header::FileHeader::new(
107 [1u8; 16],
108 v2::key::KeyDerivationParams::from(SecurityProfile::Test),
109 [2u8; 24],
110 [3u8; 24],
111 vec![4, 5, 6, 7, 8],
112 )
113 .unwrap()
114 }
115
116 fn create_shadow_file(dir: &TempDir, filename: &str) -> std::path::PathBuf {
117 let path = dir.path().join(filename);
118 let serialized = v1::header_ops::serialize(&create_test_header_v1());
119 let mut content = serialized;
121 content.extend_from_slice(b"dummy content");
122 fs::write(&path, content).unwrap();
123 path
124 }
125
126 fn create_shadow_file_v2(dir: &TempDir, filename: &str) -> std::path::PathBuf {
127 let path = dir.path().join(filename);
128 let serialized = v2::header_ops::serialize(&create_test_header_v2());
129 let mut content = serialized;
130 content.extend_from_slice(b"dummy content");
131 fs::write(&path, content).unwrap();
132 path
133 }
134
135 fn create_non_shadow_file(dir: &TempDir, filename: &str) -> std::path::PathBuf {
136 let path = dir.path().join(filename);
137 fs::write(&path, b"not a shadow file").unwrap();
138 path
139 }
140
141 #[test]
142 fn test_scan_directory_for_shadow_files() {
143 let temp_dir = TempDir::new().unwrap();
144
145 create_shadow_file(&temp_dir, "file1.shadow");
147 create_shadow_file(&temp_dir, "file2.shadow");
148 create_shadow_file_v2(&temp_dir, "file5.shadow");
149
150 create_non_shadow_file(&temp_dir, "file3.txt");
152 create_non_shadow_file(&temp_dir, "file4.dat");
153
154 let result = scan_directory_for_shadow_files(temp_dir.path()).unwrap();
155
156 assert_eq!(result.len(), 3);
158
159 let filenames: std::collections::HashSet<_> =
161 result.iter().map(|f| f.filename.as_str()).collect();
162 assert!(filenames.contains("file1.shadow"));
163 assert!(filenames.contains("file2.shadow"));
164 assert!(filenames.contains("file5.shadow"));
165 }
166
167 #[test]
168 fn test_scan_directory_nonexistent() {
169 let non_existent = std::path::Path::new("/non/existent/directory");
170 let result = scan_directory_for_shadow_files(non_existent);
171 assert!(result.is_err());
172 assert!(matches!(result.unwrap_err(), WorkflowError::Listing(_)));
173 }
174
175 #[test]
176 fn test_try_create_shadow_file_valid() {
177 let temp_dir = TempDir::new().unwrap();
178 let path = create_shadow_file(&temp_dir, "test.shadow");
179
180 let result = try_create_shadow_file(&path).unwrap();
181
182 assert_eq!(result.filename, "test.shadow");
183 assert_eq!(result.version, Version::V1);
184 assert_eq!(result.size, 90 + 5 + 13); }
186
187 #[test]
188 fn test_try_create_shadow_file_valid_v2() {
189 let temp_dir = TempDir::new().unwrap();
190 let path = create_shadow_file_v2(&temp_dir, "test.shadow");
191
192 let result = try_create_shadow_file(&path).unwrap();
193
194 assert_eq!(result.filename, "test.shadow");
195 assert_eq!(result.version, Version::V2);
196 }
197
198 #[test]
199 fn test_try_create_shadow_file_invalid_magic() {
200 let temp_dir = TempDir::new().unwrap();
201 let path = temp_dir.path().join("invalid.shadow");
202 fs::write(&path, b"NOTSHADOW").unwrap();
203
204 let result = try_create_shadow_file(&path);
205 assert!(result.is_err());
206 assert!(matches!(result.unwrap_err(), WorkflowError::Listing(_)));
207 }
208
209 #[test]
210 fn test_try_create_shadow_file_insufficient_bytes() {
211 let temp_dir = TempDir::new().unwrap();
212 let path = temp_dir.path().join("short.shadow");
213 fs::write(&path, b"SHORT").unwrap();
214
215 let result = try_create_shadow_file(&path);
216 assert!(result.is_err());
217 }
218
219 #[test]
220 fn test_get_file_size() {
221 let temp_dir = TempDir::new().unwrap();
222 let path = temp_dir.path().join("test.txt");
223 let content = b"Hello, world!";
224 fs::write(&path, content).unwrap();
225
226 let size = get_file_size(&path).unwrap();
227 assert_eq!(size, content.len() as u64);
228 }
229
230 #[test]
231 fn test_load_file_header_bytes_v1() {
232 let temp_dir = TempDir::new().unwrap();
233 let path = create_shadow_file(&temp_dir, "test.shadow");
234 let shadow_file = try_create_shadow_file(&path).unwrap();
235
236 let header_bytes = load_file_header_bytes(&shadow_file).unwrap();
237 let header = v1::header_ops::try_deserialize(header_bytes.as_slice()).unwrap();
238
239 assert_eq!(header.magic, *b"SHADOW");
240 assert_eq!(header.version, 1);
241 assert_eq!(header.salt, [1u8; 16]);
242 }
243
244 #[test]
245 fn test_load_file_header_bytes_v2() {
246 let temp_dir = TempDir::new().unwrap();
247 let path = create_shadow_file_v2(&temp_dir, "test.shadow");
248 let shadow_file = try_create_shadow_file(&path).unwrap();
249
250 let header_bytes = load_file_header_bytes(&shadow_file).unwrap();
251 let header = v2::header_ops::try_deserialize(header_bytes.as_slice()).unwrap();
252
253 assert_eq!(header.magic, *b"SHADOW");
254 assert_eq!(header.version, 2);
255 assert_eq!(header.salt, [1u8; 16]);
256 }
257}