sf_cli/
models.rs

1//! Data models for the secure file encryption tool
2
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Operation type for file processing
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum OperationType {
9    /// Encrypt files/folders with password
10    Encrypt,
11    /// Decrypt files/folders with password
12    Decrypt,
13    /// Encrypt files/folders with hybrid encryption (public key + symmetric)
14    HybridEncrypt,
15    /// Decrypt files/folders with hybrid encryption (private key + symmetric)
16    HybridDecrypt,
17}
18
19impl std::fmt::Display for OperationType {
20    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21        match self {
22            Self::Encrypt => write!(f, "Encrypt"),
23            Self::Decrypt => write!(f, "Decrypt"),
24            Self::HybridEncrypt => write!(f, "Hybrid Encrypt"),
25            Self::HybridDecrypt => write!(f, "Hybrid Decrypt"),
26        }
27    }
28}
29
30/// Target type for operations
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub enum TargetType {
33    /// Single file
34    File,
35    /// Directory/folder
36    Directory,
37}
38
39impl std::fmt::Display for TargetType {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            Self::File => write!(f, "File"),
43            Self::Directory => write!(f, "Directory"),
44        }
45    }
46}
47
48/// Operation parameters
49#[derive(Debug, Clone)]
50pub struct OperationParams {
51    /// Type of operation (encrypt/decrypt)
52    pub operation: OperationType,
53    /// Target type (file/directory)
54    pub target_type: TargetType,
55    /// Source path
56    pub source: PathBuf,
57    /// Destination path (optional, defaults based on operation)
58    pub destination: Option<PathBuf>,
59    /// Whether to enable compression
60    pub compress: bool,
61    /// Whether to show progress
62    pub show_progress: bool,
63    /// Buffer size for file operations
64    pub buffer_size: usize,
65    /// Whether to preserve original filename (new feature)
66    pub preserve_filename: bool,
67    /// Whether to delete source file after operation
68    pub delete_source: bool,
69    /// Whether to verify checksum after decryption
70    pub verify_checksum: bool,
71    /// Public key path for hybrid encryption (optional, will auto-discover if None)
72    pub public_key_path: Option<PathBuf>,
73    /// Private key path for hybrid decryption (optional, will auto-discover if None)
74    pub private_key_path: Option<PathBuf>,
75}
76
77impl OperationParams {
78    /// Create new operation parameters
79    pub fn new(
80        operation: OperationType,
81        target_type: TargetType,
82        source: PathBuf,
83    ) -> Self {
84        Self {
85            operation,
86            target_type,
87            source,
88            destination: None,
89            compress: false,
90            show_progress: true,
91            buffer_size: 64 * 1024, // 64KB
92            preserve_filename: true, // Default to preserving filenames
93            delete_source: false,    // Default to keeping source files
94            verify_checksum: true,   // Default to verifying checksums
95            public_key_path: None,   // Auto-discover by default
96            private_key_path: None,  // Auto-discover by default
97        }
98    }
99
100    /// Set destination path
101    pub fn with_destination(mut self, destination: PathBuf) -> Self {
102        self.destination = Some(destination);
103        self
104    }
105
106    /// Enable compression
107    pub fn with_compression(mut self, compress: bool) -> Self {
108        self.compress = compress;
109        self
110    }
111
112    /// Set progress visibility
113    pub fn with_progress(mut self, show_progress: bool) -> Self {
114        self.show_progress = show_progress;
115        self
116    }
117
118    /// Set buffer size
119    pub fn with_buffer_size(mut self, buffer_size: usize) -> Self {
120        self.buffer_size = buffer_size;
121        self
122    }
123
124    /// Set filename preservation
125    pub fn with_preserve_filename(mut self, preserve_filename: bool) -> Self {
126        self.preserve_filename = preserve_filename;
127        self
128    }
129
130    /// Set source deletion after operation
131    pub fn with_delete_source(mut self, delete_source: bool) -> Self {
132        self.delete_source = delete_source;
133        self
134    }
135
136    /// Set checksum verification
137    pub fn with_verify_checksum(mut self, verify_checksum: bool) -> Self {
138        self.verify_checksum = verify_checksum;
139        self
140    }
141
142    /// Set public key path for hybrid encryption
143    pub fn with_public_key_path(mut self, public_key_path: PathBuf) -> Self {
144        self.public_key_path = Some(public_key_path);
145        self
146    }
147
148    /// Set private key path for hybrid decryption
149    pub fn with_private_key_path(mut self, private_key_path: PathBuf) -> Self {
150        self.private_key_path = Some(private_key_path);
151        self
152    }
153
154    /// Get the default destination path based on source and operation
155    pub fn get_destination(&self) -> PathBuf {
156        if let Some(dest) = &self.destination {
157            dest.clone()
158        } else {
159            match self.operation {
160                OperationType::Encrypt => {
161                    if self.preserve_filename {
162                        // Keep original filename, just add .sf extension
163                        let original_ext = self.source.extension()
164                            .and_then(|s| s.to_str())
165                            .unwrap_or("")
166                            .to_string();
167                        
168                        if self.compress {
169                            if original_ext.is_empty() {
170                                self.source.with_extension("sf")
171                            } else {
172                                self.source.with_extension(format!("{}.sf", original_ext))
173                            }
174                        } else {
175                            if original_ext.is_empty() {
176                                self.source.with_extension("sf")
177                            } else {
178                                self.source.with_extension(format!("{}.sf", original_ext))
179                            }
180                        }
181                    } else {
182                        // Legacy behavior
183                        if self.compress {
184                            self.source.with_extension("sf.gz")
185                        } else {
186                            self.source.with_extension("sf")
187                        }
188                    }
189                }
190                OperationType::HybridEncrypt => {
191                    // Use .hsf (hybrid secure file) extension
192                    if self.preserve_filename {
193                        let original_ext = self.source.extension()
194                            .and_then(|s| s.to_str())
195                            .unwrap_or("")
196                            .to_string();
197                        
198                        if original_ext.is_empty() {
199                            self.source.with_extension("hsf")
200                        } else {
201                            self.source.with_extension(format!("{}.hsf", original_ext))
202                        }
203                    } else {
204                        self.source.with_extension("hsf")
205                    }
206                }
207                OperationType::Decrypt => {
208                    if self.preserve_filename {
209                        // Will be determined from metadata during decryption
210                        // For now, just remove .sf or .sf.gz extension
211                        let source_str = self.source.to_string_lossy();
212                        if source_str.ends_with(".sf.gz") {
213                            PathBuf::from(source_str.trim_end_matches(".sf.gz"))
214                        } else if source_str.ends_with(".sf") {
215                            PathBuf::from(source_str.trim_end_matches(".sf"))
216                        } else {
217                            self.source.with_extension("decrypted")
218                        }
219                    } else {
220                        // Legacy behavior
221                        let source_str = self.source.to_string_lossy();
222                        if source_str.ends_with(".sf.gz") {
223                            PathBuf::from(source_str.trim_end_matches(".sf.gz"))
224                        } else if source_str.ends_with(".sf") {
225                            PathBuf::from(source_str.trim_end_matches(".sf"))
226                        } else {
227                            self.source.with_extension("decrypted")
228                        }
229                    }
230                }
231                OperationType::HybridDecrypt => {
232                    // Remove .hsf extension
233                    let source_str = self.source.to_string_lossy();
234                    if source_str.ends_with(".hsf") {
235                        PathBuf::from(source_str.trim_end_matches(".hsf"))
236                    } else {
237                        self.source.with_extension("decrypted")
238                    }
239                }
240            }
241        }
242    }
243}
244
245/// Result of a file operation
246#[derive(Debug, Clone)]
247pub struct OperationResult {
248    /// Whether the operation was successful
249    pub success: bool,
250    /// Source path that was processed
251    pub source: PathBuf,
252    /// Destination path where result was saved
253    pub destination: PathBuf,
254    /// Number of bytes processed
255    pub bytes_processed: u64,
256    /// Error message if operation failed
257    pub error: Option<String>,
258    /// Operation type that was performed
259    pub operation: OperationType,
260    /// Whether compression was used
261    pub compressed: bool,
262    /// Original filename (for decryption operations)
263    pub original_filename: Option<String>,
264    /// Whether checksum verification passed (for decryption)
265    pub checksum_verified: Option<bool>,
266}
267
268impl OperationResult {
269    /// Create a successful operation result
270    pub fn success(
271        source: PathBuf,
272        destination: PathBuf,
273        bytes_processed: u64,
274        operation: OperationType,
275        compressed: bool,
276    ) -> Self {
277        Self {
278            success: true,
279            source,
280            destination,
281            bytes_processed,
282            error: None,
283            operation,
284            compressed,
285            original_filename: None,
286            checksum_verified: None,
287        }
288    }
289
290    /// Create a successful operation result with metadata
291    pub fn success_with_metadata(
292        source: PathBuf,
293        destination: PathBuf,
294        bytes_processed: u64,
295        operation: OperationType,
296        compressed: bool,
297        original_filename: Option<String>,
298        checksum_verified: Option<bool>,
299    ) -> Self {
300        Self {
301            success: true,
302            source,
303            destination,
304            bytes_processed,
305            error: None,
306            operation,
307            compressed,
308            original_filename,
309            checksum_verified,
310        }
311    }
312
313    /// Create a failed operation result
314    pub fn failure(
315        source: PathBuf,
316        operation: OperationType,
317        error: String,
318    ) -> Self {
319        Self {
320            success: false,
321            source,
322            destination: PathBuf::new(),
323            bytes_processed: 0,
324            error: Some(error),
325            operation,
326            compressed: false,
327            original_filename: None,
328            checksum_verified: None,
329        }
330    }
331}
332
333impl std::fmt::Display for OperationResult {
334    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335        if self.success {
336            let mut result = format!(
337                "✓ {} {} -> {} ({} bytes{})",
338                self.operation,
339                self.source.display(),
340                self.destination.display(),
341                self.bytes_processed,
342                if self.compressed { ", compressed" } else { "" }
343            );
344
345            if let Some(filename) = &self.original_filename {
346                result.push_str(&format!(", original: {}", filename));
347            }
348
349            if let Some(verified) = self.checksum_verified {
350                result.push_str(&format!(", checksum: {}", if verified { "✓" } else { "✗" }));
351            }
352
353            write!(f, "{}", result)
354        } else {
355            write!(
356                f,
357                "✗ {} {} failed: {}",
358                self.operation,
359                self.source.display(),
360                self.error.as_ref().unwrap_or(&"Unknown error".to_string())
361            )
362        }
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_operation_params() {
372        let params = OperationParams::new(
373            OperationType::Encrypt,
374            TargetType::File,
375            PathBuf::from("test.txt"),
376        );
377
378        assert_eq!(params.operation, OperationType::Encrypt);
379        assert_eq!(params.target_type, TargetType::File);
380        assert_eq!(params.source, PathBuf::from("test.txt"));
381        assert!(!params.compress);
382        assert!(params.show_progress);
383    }
384
385    #[test]
386    fn test_destination_generation() {
387        // Test encryption destination with filename preservation (new default behavior)
388        let params = OperationParams::new(
389            OperationType::Encrypt,
390            TargetType::File,
391            PathBuf::from("test.txt"),
392        );
393        assert_eq!(params.get_destination(), PathBuf::from("test.txt.sf"));
394
395        // Test encryption with compression and filename preservation
396        let params = params.with_compression(true);
397        assert_eq!(params.get_destination(), PathBuf::from("test.txt.sf"));
398
399        // Test legacy behavior (no filename preservation)
400        let params = params.with_preserve_filename(false);
401        assert_eq!(params.get_destination(), PathBuf::from("test.sf.gz"));
402
403        // Test decryption
404        let params = OperationParams::new(
405            OperationType::Decrypt,
406            TargetType::File,
407            PathBuf::from("test.txt.sf"),
408        );
409        assert_eq!(params.get_destination(), PathBuf::from("test.txt"));
410
411        // Test legacy decryption
412        let params = OperationParams::new(
413            OperationType::Decrypt,
414            TargetType::File,
415            PathBuf::from("test.sf.gz"),
416        );
417        assert_eq!(params.get_destination(), PathBuf::from("test"));
418    }
419
420    #[test]
421    fn test_operation_result() {
422        let success = OperationResult::success(
423            PathBuf::from("source.txt"),
424            PathBuf::from("dest.sf"),
425            1024,
426            OperationType::Encrypt,
427            false,
428        );
429
430        assert!(success.success);
431        assert_eq!(success.bytes_processed, 1024);
432        assert!(success.error.is_none());
433
434        let failure = OperationResult::failure(
435            PathBuf::from("source.txt"),
436            OperationType::Encrypt,
437            "Test error".to_string(),
438        );
439
440        assert!(!failure.success);
441        assert_eq!(failure.bytes_processed, 0);
442        assert_eq!(failure.error.as_ref().unwrap(), "Test error");
443    }
444}