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    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()) // 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 the complete raw header bytes of a shadow file. Deserialization
71/// happens in the workflow, per format version.
72pub fn load_file_header_bytes(file: &ShadowFile) -> WorkflowResult<SecureBytes> {
73    // The file's own format version reads the header length out of the fixed
74    // header fields; the shell does not know the header layout.
75    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        // Add some dummy content after header
120        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 files of both versions
146        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 some non-shadow files
151        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        // Should find exactly 3 shadow files
157        assert_eq!(result.len(), 3);
158
159        // Check filenames
160        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); // header min + filename + dummy content
185    }
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}