Skip to main content

easy_archive/archive/zip/
encode.rs

1/// ZIP encoding implementation
2use crate::{
3    File,
4    error::{ArchiveError, Result},
5    traits::Encode,
6    utils::check_duplicate_files,
7};
8use std::collections::HashSet;
9use std::io::{Cursor, Write};
10use time::OffsetDateTime;
11use zip::DateTime;
12
13use super::Zip;
14
15impl Encode for Zip {
16    fn encode(files: Vec<File>) -> Result<Vec<u8>> {
17        // Check for duplicate files before encoding (fail fast)
18        check_duplicate_files(&files)?;
19
20        // Pre-allocate output buffer with estimated size
21        // ZIP typically achieves 40-60% compression with Zstd
22        let estimated_size: usize = files.iter().map(|f| f.buffer.len()).sum::<usize>() / 2;
23        let mut output = Vec::with_capacity(estimated_size);
24        let cursor = Cursor::new(&mut output);
25        let mut zip = zip::ZipWriter::new(cursor);
26        let mut dir_set = HashSet::with_capacity(files.len() / 4); // Estimate directory count
27
28        // Helper function to create file options with timestamp
29        // Performance: Zstd provides excellent compression speed and ratio
30        let create_options = |last_modified: Option<u64>| -> zip::write::FullFileOptions {
31            let mut options = zip::write::FullFileOptions::default()
32                // Use Zstd for better compression/speed balance (faster than LZMA, better than Deflate)
33                .compression_method(zip::CompressionMethod::Zstd);
34
35            if let Some(timestamp) = last_modified
36                && let Ok(offset_time) = OffsetDateTime::from_unix_timestamp(timestamp as i64)
37                && let Ok(datetime) = DateTime::try_from(
38                    time::PrimitiveDateTime::new(offset_time.date(), offset_time.time())
39                )
40            {
41                options = options.last_modified_time(datetime);
42            }
43
44            options
45        };
46
47        // First pass: Create all explicit directories
48        for file in files.iter().filter(|f| f.is_dir) {
49            if dir_set.contains(&file.path) {
50                continue;
51            }
52
53            dir_set.insert(file.path.clone());
54            let options = create_options(file.last_modified);
55
56            zip.add_directory(&file.path, options)
57                .map_err(|e| ArchiveError::EncodeFailed {
58                    format: "zip".to_string(),
59                    reason: format!("Failed to add directory '{}': {}", file.path, e),
60                })?;
61        }
62
63        // Second pass: Create implicit parent directories for files
64        for file in files.iter().filter(|f| !f.is_dir) {
65            if !file.path.contains('/') {
66                continue;
67            }
68
69            if let Some(parent) = std::path::Path::new(&file.path).parent() {
70                let parent_path = parent.to_string_lossy().to_string();
71                if !parent_path.is_empty() && !dir_set.contains(&parent_path) {
72                    dir_set.insert(parent_path.clone());
73                    let options = create_options(file.last_modified);
74
75                    zip.add_directory(&parent_path, options).map_err(|e| {
76                        ArchiveError::EncodeFailed {
77                            format: "zip".to_string(),
78                            reason: format!(
79                                "Failed to add parent directory '{}': {}",
80                                parent_path, e
81                            ),
82                        }
83                    })?;
84                }
85            }
86        }
87
88        // Third pass: Add all files
89        for file in files.iter().filter(|f| !f.is_dir) {
90            let mode = file.mode.unwrap_or(0o755);
91            let options = create_options(file.last_modified).unix_permissions(mode);
92
93            zip.start_file(&file.path, options)
94                .map_err(|e| ArchiveError::EncodeFailed {
95                    format: "zip".to_string(),
96                    reason: format!("Failed to start file '{}': {}", file.path, e),
97                })?;
98
99            zip.write_all(&file.buffer)
100                .map_err(|e| ArchiveError::EncodeFailed {
101                    format: "zip".to_string(),
102                    reason: format!("Failed to write file '{}': {}", file.path, e),
103                })?;
104        }
105
106        zip.finish().map_err(|e| ArchiveError::EncodeFailed {
107            format: "zip".to_string(),
108            reason: format!("Failed to finalize zip archive: {}", e),
109        })?;
110
111        Ok(output)
112    }
113}