shadow_crypt_shell/listing/
file_ops.rs

1use 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()) // Skip entries we can't read
27        .map(|entry| entry.path())
28        .filter(|path| path.is_file()) // Only regular files
29        .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        // Add some dummy content after header
116        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 some shadow files
133        create_shadow_file(&temp_dir, "file1.shadow");
134        create_shadow_file(&temp_dir, "file2.shadow");
135
136        // Create some non-shadow files
137        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        // Should find exactly 2 shadow files
143        assert_eq!(result.len(), 2);
144
145        // Check filenames
146        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); // header min + filename + dummy content
170    }
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        // Check that it's the same as our test header
213        assert_eq!(header.magic, *b"SHADOW");
214        assert_eq!(header.version, 1);
215        assert_eq!(header.salt, [1u8; 16]);
216    }
217}