shadow_crypt_shell/listing/
file_ops.rs1use std::{fs, path::Path};
2
3use shadow_crypt_core::{
4 memory::SecureBytes,
5 vault::MAX_HEADER_LEN,
6 version::{PREAMBLE_LENGTH, 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> {
75 read_n_bytes_from_file(&file.path, MAX_HEADER_LEN)
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81 use shadow_crypt_core::{profile::SecurityProfile, v1, v2, version::Version};
82 use std::fs;
83 use tempfile::TempDir;
84
85 fn create_test_header_v1() -> v1::header::FileHeader {
86 v1::header::FileHeader::new(
87 [1u8; 16],
88 v1::key::KeyDerivationParams::from(SecurityProfile::Test),
89 [2u8; 24],
90 [3u8; 24],
91 vec![4, 5, 6, 7, 8],
92 )
93 }
94
95 fn create_test_header_v2() -> v2::header::FileHeader {
96 v2::header::FileHeader::new(
97 [1u8; 16],
98 v2::key::KeyDerivationParams::from(SecurityProfile::Test),
99 [2u8; 24],
100 [3u8; 24],
101 vec![4, 5, 6, 7, 8],
102 )
103 .unwrap()
104 }
105
106 fn create_shadow_file(dir: &TempDir, filename: &str) -> std::path::PathBuf {
107 let path = dir.path().join(filename);
108 let serialized = create_test_header_v1().serialize();
109 let mut content = serialized;
111 content.extend_from_slice(b"dummy content");
112 fs::write(&path, content).unwrap();
113 path
114 }
115
116 fn create_shadow_file_v2(dir: &TempDir, filename: &str) -> std::path::PathBuf {
117 let path = dir.path().join(filename);
118 let serialized = create_test_header_v2().serialize();
119 let mut content = serialized;
120 content.extend_from_slice(b"dummy content");
121 fs::write(&path, content).unwrap();
122 path
123 }
124
125 fn create_non_shadow_file(dir: &TempDir, filename: &str) -> std::path::PathBuf {
126 let path = dir.path().join(filename);
127 fs::write(&path, b"not a shadow file").unwrap();
128 path
129 }
130
131 #[test]
132 fn test_scan_directory_for_shadow_files() {
133 let temp_dir = TempDir::new().unwrap();
134
135 create_shadow_file(&temp_dir, "file1.shadow");
137 create_shadow_file(&temp_dir, "file2.shadow");
138 create_shadow_file_v2(&temp_dir, "file5.shadow");
139
140 create_non_shadow_file(&temp_dir, "file3.txt");
142 create_non_shadow_file(&temp_dir, "file4.dat");
143
144 let result = scan_directory_for_shadow_files(temp_dir.path()).unwrap();
145
146 assert_eq!(result.len(), 3);
148
149 let filenames: std::collections::HashSet<_> =
151 result.iter().map(|f| f.filename.as_str()).collect();
152 assert!(filenames.contains("file1.shadow"));
153 assert!(filenames.contains("file2.shadow"));
154 assert!(filenames.contains("file5.shadow"));
155 }
156
157 #[test]
158 fn test_scan_directory_nonexistent() {
159 let non_existent = std::path::Path::new("/non/existent/directory");
160 let result = scan_directory_for_shadow_files(non_existent);
161 assert!(result.is_err());
162 assert!(matches!(result.unwrap_err(), WorkflowError::Listing(_)));
163 }
164
165 #[test]
166 fn test_try_create_shadow_file_valid() {
167 let temp_dir = TempDir::new().unwrap();
168 let path = create_shadow_file(&temp_dir, "test.shadow");
169
170 let result = try_create_shadow_file(&path).unwrap();
171
172 assert_eq!(result.filename, "test.shadow");
173 assert_eq!(result.version, Version::V1);
174 assert_eq!(result.size, 90 + 5 + 13); }
176
177 #[test]
178 fn test_try_create_shadow_file_valid_v2() {
179 let temp_dir = TempDir::new().unwrap();
180 let path = create_shadow_file_v2(&temp_dir, "test.shadow");
181
182 let result = try_create_shadow_file(&path).unwrap();
183
184 assert_eq!(result.filename, "test.shadow");
185 assert_eq!(result.version, Version::V2);
186 }
187
188 #[test]
189 fn test_try_create_shadow_file_invalid_magic() {
190 let temp_dir = TempDir::new().unwrap();
191 let path = temp_dir.path().join("invalid.shadow");
192 fs::write(&path, b"NOTSHADOW").unwrap();
193
194 let result = try_create_shadow_file(&path);
195 assert!(result.is_err());
196 assert!(matches!(result.unwrap_err(), WorkflowError::Listing(_)));
197 }
198
199 #[test]
200 fn test_try_create_shadow_file_insufficient_bytes() {
201 let temp_dir = TempDir::new().unwrap();
202 let path = temp_dir.path().join("short.shadow");
203 fs::write(&path, b"SHORT").unwrap();
204
205 let result = try_create_shadow_file(&path);
206 assert!(result.is_err());
207 }
208
209 #[test]
210 fn test_get_file_size() {
211 let temp_dir = TempDir::new().unwrap();
212 let path = temp_dir.path().join("test.txt");
213 let content = b"Hello, world!";
214 fs::write(&path, content).unwrap();
215
216 let size = get_file_size(&path).unwrap();
217 assert_eq!(size, content.len() as u64);
218 }
219
220 #[test]
221 fn test_load_file_header_bytes_v1() {
222 let temp_dir = TempDir::new().unwrap();
223 let path = create_shadow_file(&temp_dir, "test.shadow");
224 let shadow_file = try_create_shadow_file(&path).unwrap();
225
226 let header_bytes = load_file_header_bytes(&shadow_file).unwrap();
227 let header = v1::header::FileHeader::try_deserialize(header_bytes.as_slice()).unwrap();
228
229 assert_eq!(header.salt(), &[1u8; 16]);
230 }
231
232 #[test]
233 fn test_load_file_header_bytes_v2() {
234 let temp_dir = TempDir::new().unwrap();
235 let path = create_shadow_file_v2(&temp_dir, "test.shadow");
236 let shadow_file = try_create_shadow_file(&path).unwrap();
237
238 let header_bytes = load_file_header_bytes(&shadow_file).unwrap();
239 let header = v2::header::FileHeader::try_deserialize(header_bytes.as_slice()).unwrap();
240
241 assert_eq!(header.salt(), &[1u8; 16]);
242 }
243}