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