Skip to main content

shadow_crypt_shell/listing/
file_ops.rs

1use 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()) // Skip entries we can't read
25        .map(|entry| entry.path())
26        .filter(|path| path.is_file()) // Only regular files
27        .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
70/// Reads enough leading bytes of a shadow file to cover its complete header,
71/// whatever its format version. Parsing happens in the workflow via
72/// [`shadow_crypt_core::vault::ParsedFile`], which tolerates trailing
73/// ciphertext bytes, so no version-specific length probing is needed here.
74pub 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        // Add some dummy content after header
110        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 files of both versions
136        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 some non-shadow files
141        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        // Should find exactly 3 shadow files
147        assert_eq!(result.len(), 3);
148
149        // Check filenames
150        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); // header min + filename + dummy content
175    }
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}