Skip to main content

office_rs/common/
zip_utils.rs

1//! ZIP文件处理工具模块
2//! 提供Office文档ZIP压缩包操作的通用功能
3
4use crate::context::ErrorContext;
5use crate::error::{ OfficeError, Result };
6use std::collections::HashMap;
7use std::fs::File;
8use std::io::{ BufReader, Cursor, Read, Seek, Write };
9use std::path::{ Component, Path };
10use time::OffsetDateTime;
11use zip::write::FileOptions;
12use zip::{ CompressionMethod, ZipArchive };
13
14/// ZIP操作安全配置
15#[derive(Debug, Clone)]
16pub struct ZipSecurityConfig {
17    /// 最大解压缩大小(字节)
18    pub max_uncompressed_size: u64,
19    /// 最大单个文件大小(字节)
20    pub max_file_size: u64,
21    /// 最大文件数量
22    pub max_file_count: usize,
23    /// 是否允许路径遍历
24    pub allow_path_traversal: bool,
25    /// 内存缓冲区大小(字节)
26    pub memory_buffer_size: u64,
27}
28
29impl Default for ZipSecurityConfig {
30    fn default() -> Self {
31        Self {
32            max_uncompressed_size: 100 * 1024 * 1024, // 100MB
33            max_file_size: 50 * 1024 * 1024, // 50MB
34            max_file_count: 1000, // 1000个文件
35            allow_path_traversal: false,
36            memory_buffer_size: 50 * 1024 * 1024, // 50MB缓冲区
37        }
38    }
39}
40
41impl ZipSecurityConfig {
42    /// 创建宽松的安全配置(用于可信文件)
43    pub fn permissive() -> Self {
44        Self {
45            max_uncompressed_size: 1024 * 1024 * 1024, // 1GB
46            max_file_size: 500 * 1024 * 1024, // 500MB
47            max_file_count: 10000,
48            allow_path_traversal: false,
49            memory_buffer_size: 500 * 1024 * 1024, // 500MB
50        }
51    }
52
53    /// 创建严格的安全配置(用于不可信文件)
54    pub fn strict() -> Self {
55        Self {
56            max_uncompressed_size: 10 * 1024 * 1024, // 10MB
57            max_file_size: 5 * 1024 * 1024, // 5MB
58            max_file_count: 100,
59            allow_path_traversal: false,
60            memory_buffer_size: 5 * 1024 * 1024, // 5MB
61        }
62    }
63}
64
65/// ZIP文档条目信息
66#[derive(Debug, Clone)]
67pub struct ZipEntry {
68    pub name: String,
69    pub size: u64,
70    pub compressed_size: u64,
71    pub is_directory: bool,
72    pub last_modified: Option<std::time::SystemTime>,
73}
74
75/// 路径安全验证函数
76fn validate_zip_path(path: &str, allow_traversal: bool) -> Result<()> {
77    if !allow_traversal {
78        let path_obj = Path::new(path);
79        for component in path_obj.components() {
80            match component {
81                Component::ParentDir => {
82                    return Err(OfficeError::Other(format!("检测到路径遍历攻击: {}", path)));
83                }
84                Component::RootDir => {
85                    return Err(OfficeError::Other(format!("检测到绝对路径: {}", path)));
86                }
87                _ => {}
88            }
89        }
90    }
91    Ok(())
92}
93
94impl ZipEntry {
95    /// 创建新的ZIP条目
96    pub fn new(name: String) -> Self {
97        Self {
98            name,
99            size: 0,
100            compressed_size: 0,
101            is_directory: false,
102            last_modified: None,
103        }
104    }
105
106    /// 检查是否为文件
107    pub fn is_file(&self) -> bool {
108        !self.is_directory
109    }
110
111    /// 获取文件扩展名
112    pub fn extension(&self) -> Option<&str> {
113        Path::new(&self.name)
114            .extension()
115            .and_then(|ext| ext.to_str())
116    }
117
118    /// 获取文件名(不含路径)
119    pub fn file_name(&self) -> Option<&str> {
120        Path::new(&self.name)
121            .file_name()
122            .and_then(|name| name.to_str())
123    }
124
125    /// 获取目录路径
126    pub fn parent_path(&self) -> Option<&str> {
127        Path::new(&self.name)
128            .parent()
129            .and_then(|path| path.to_str())
130    }
131}
132
133/// ZIP文档读取器
134pub struct ZipReader<R: Read + Seek> {
135    archive: ZipArchive<R>,
136    entries: HashMap<String, ZipEntry>,
137    security_config: ZipSecurityConfig,
138    total_uncompressed_size: u64,
139}
140
141impl ZipReader<BufReader<File>> {
142    /// 从文件路径打开ZIP文档(使用默认安全配置)
143    pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
144        Self::open_file_with_config(path, ZipSecurityConfig::default())
145    }
146
147    /// 从文件路径打开ZIP文档(使用自定义安全配置)
148    pub fn open_file_with_config<P: AsRef<Path>>(
149        path: P,
150        config: ZipSecurityConfig
151    ) -> Result<Self> {
152        let file = File::open(&path).map_err(|_e| {
153            OfficeError::file_not_found_with_context(
154                path.as_ref().to_string_lossy().to_string(),
155                ErrorContext {
156                    operation: Some("打开ZIP文件".to_string()),
157                    ..Default::default()
158                }
159            )
160        })?;
161
162        let reader = BufReader::new(file);
163        Self::new_with_config(reader, config)
164    }
165}
166
167impl<R: Read + Seek> ZipReader<R> {
168    /// 创建新的ZIP读取器(使用默认安全配置)
169    pub fn new(reader: R) -> Result<Self> {
170        Self::new_with_config(reader, ZipSecurityConfig::default())
171    }
172
173    /// 创建新的ZIP读取器(使用自定义安全配置)
174    pub fn new_with_config(reader: R, config: ZipSecurityConfig) -> Result<Self> {
175        let mut archive = ZipArchive::new(reader).map_err(|e| {
176            OfficeError::Zip(e).with_context(ErrorContext {
177                operation: Some("创建ZIP读取器".to_string()),
178                ..Default::default()
179            })
180        })?;
181
182        // 检查文件数量限制
183        if archive.len() > config.max_file_count {
184            return Err(
185                OfficeError::Other(
186                    format!("ZIP文件包含过多文件: {} > {}", archive.len(), config.max_file_count)
187                )
188            );
189        }
190
191        let mut entries = HashMap::new();
192        let mut total_uncompressed_size = 0u64;
193
194        // 读取所有条目信息
195        for i in 0..archive.len() {
196            let file = archive.by_index(i).map_err(|e| {
197                OfficeError::Zip(e).with_context(ErrorContext {
198                    operation: Some("读取ZIP条目".to_string()),
199                    ..Default::default()
200                })
201            })?;
202
203            let file_name = file.name();
204
205            // 验证路径安全性
206            validate_zip_path(file_name, config.allow_path_traversal)?;
207
208            // 检查单个文件大小限制
209            if file.size() > config.max_file_size {
210                return Err(
211                    OfficeError::Other(
212                        format!(
213                            "文件过大: {} ({} 字节) > {} 字节",
214                            file_name,
215                            file.size(),
216                            config.max_file_size
217                        )
218                    )
219                );
220            }
221
222            total_uncompressed_size = total_uncompressed_size.saturating_add(file.size());
223
224            let mut entry = ZipEntry::new(file_name.to_string());
225            entry.size = file.size();
226            entry.compressed_size = file.compressed_size();
227            entry.is_directory = file.is_dir();
228            entry.last_modified = file.last_modified().and_then(|dt| {
229                OffsetDateTime::try_from(dt)
230                    .ok()
231                    .map(|offset_dt| {
232                        std::time::SystemTime::UNIX_EPOCH +
233                            std::time::Duration::from_secs(offset_dt.unix_timestamp() as u64)
234                    })
235            });
236
237            entries.insert(file_name.to_string(), entry);
238        }
239
240        // 检查总解压缩大小限制
241        if total_uncompressed_size > config.max_uncompressed_size {
242            return Err(
243                OfficeError::Other(
244                    format!(
245                        "ZIP文件解压缩后过大: {} 字节 > {} 字节",
246                        total_uncompressed_size,
247                        config.max_uncompressed_size
248                    )
249                )
250            );
251        }
252
253        Ok(Self {
254            archive,
255            entries,
256            security_config: config,
257            total_uncompressed_size,
258        })
259    }
260
261    /// 获取所有条目列表
262    pub fn entries(&self) -> &HashMap<String, ZipEntry> {
263        &self.entries
264    }
265
266    /// 检查文件是否存在
267    pub fn contains_file(&self, name: &str) -> bool {
268        self.entries.contains_key(name)
269    }
270
271    /// 获取文件条目信息
272    pub fn get_entry(&self, name: &str) -> Option<&ZipEntry> {
273        self.entries.get(name)
274    }
275
276    /// 读取文件内容为字节数组
277    pub fn read_file(&mut self, name: &str) -> Result<Vec<u8>> {
278        let mut file = self.archive.by_name(name).map_err(|e| {
279            OfficeError::Zip(e).with_context(ErrorContext {
280                operation: Some(format!("读取ZIP文件: {}", name)),
281                file_path: Some(name.to_string()),
282                ..Default::default()
283            })
284        })?;
285
286        // 检查文件大小是否超过内存缓冲区限制
287        if file.size() > self.security_config.memory_buffer_size {
288            return Err(
289                OfficeError::Other(
290                    format!(
291                        "文件过大,无法加载到内存: {} ({} 字节) > {} 字节",
292                        name,
293                        file.size(),
294                        self.security_config.memory_buffer_size
295                    )
296                )
297            );
298        }
299
300        // 安全地创建缓冲区,避免整数溢出
301        let size = file.size() as usize;
302        if size > (isize::MAX as usize) {
303            return Err(OfficeError::Other(format!("文件大小超出系统限制: {} 字节", size)));
304        }
305
306        let mut contents = Vec::with_capacity(size);
307        file.read_to_end(&mut contents).map_err(|e| {
308            OfficeError::Io(e).with_context(ErrorContext {
309                operation: Some(format!("读取文件内容: {}", name)),
310                file_path: Some(name.to_string()),
311                ..Default::default()
312            })
313        })?;
314
315        Ok(contents)
316    }
317
318    /// 读取文件内容为字符串
319    pub fn read_file_to_string(&mut self, name: &str) -> Result<String> {
320        let bytes = self.read_file(name)?;
321        String::from_utf8(bytes).map_err(|e| OfficeError::Other(format!("UTF-8解码错误: {}", e)))
322    }
323
324    /// 提取文件到指定路径
325    pub fn extract_file<P: AsRef<Path>>(&mut self, name: &str, output_path: P) -> Result<()> {
326        // 验证输出路径安全性
327        let output_path_str = output_path.as_ref().to_string_lossy();
328        validate_zip_path(&output_path_str, self.security_config.allow_path_traversal)?;
329
330        let mut file = self.archive.by_name(name).map_err(|e| {
331            OfficeError::Zip(e).with_context(ErrorContext {
332                operation: Some(format!("提取ZIP文件: {}", name)),
333                file_path: Some(name.to_string()),
334                ..Default::default()
335            })
336        })?;
337
338        // 检查文件大小限制
339        if file.size() > self.security_config.max_file_size {
340            return Err(
341                OfficeError::Other(
342                    format!(
343                        "文件过大,无法提取: {} ({} 字节) > {} 字节",
344                        name,
345                        file.size(),
346                        self.security_config.max_file_size
347                    )
348                )
349            );
350        }
351
352        let mut output_file = File::create(&output_path).map_err(|e| {
353            OfficeError::Io(e).with_context(ErrorContext {
354                operation: Some("创建输出文件".to_string()),
355                file_path: Some(output_path.as_ref().to_string_lossy().to_string()),
356                ..Default::default()
357            })
358        })?;
359
360        std::io::copy(&mut file, &mut output_file).map_err(|e| {
361            OfficeError::Io(e).with_context(ErrorContext {
362                operation: Some("复制文件内容".to_string()),
363                file_path: Some(name.to_string()),
364                ..Default::default()
365            })
366        })?;
367
368        Ok(())
369    }
370
371    /// 提取所有文件到指定目录
372    pub fn extract_all<P: AsRef<Path>>(&mut self, output_dir: P) -> Result<()> {
373        let output_dir = output_dir.as_ref();
374        let mut total_extracted_size = 0u64;
375
376        // 使用HashSet避免重复文件名收集的性能问题
377        let mut processed_files = std::collections::HashSet::new();
378
379        // 先收集所有需要提取的文件名
380        let file_names: Vec<String> = self.entries
381            .iter()
382            .filter(|(_, entry)| !entry.is_directory)
383            .map(|(name, _)| name.clone())
384            .collect();
385
386        for name in file_names {
387            // 避免重复处理
388            if !processed_files.insert(name.clone()) {
389                continue;
390            }
391
392            let entry = &self.entries[&name];
393
394            // 检查累计提取大小
395            total_extracted_size = total_extracted_size.saturating_add(entry.size);
396            if total_extracted_size > self.security_config.max_uncompressed_size {
397                return Err(
398                    OfficeError::Other(
399                        format!(
400                            "提取的文件总大小超过限制: {} 字节 > {} 字节",
401                            total_extracted_size,
402                            self.security_config.max_uncompressed_size
403                        )
404                    )
405                );
406            }
407
408            let output_path = output_dir.join(&name);
409
410            // 验证输出路径安全性
411            let output_path_str = output_path.to_string_lossy();
412            validate_zip_path(&output_path_str, self.security_config.allow_path_traversal)?;
413
414            // 创建父目录
415            if let Some(parent) = output_path.parent() {
416                std::fs::create_dir_all(parent).map_err(|e| {
417                    OfficeError::Io(e).with_context(ErrorContext {
418                        operation: Some("创建目录".to_string()),
419                        file_path: Some(parent.to_string_lossy().to_string()),
420                        ..Default::default()
421                    })
422                })?;
423            }
424
425            self.extract_file(&name, &output_path)?;
426        }
427
428        Ok(())
429    }
430
431    /// 列出指定目录下的文件
432    pub fn list_files_in_directory(&self, dir_path: &str) -> Vec<&ZipEntry> {
433        let normalized_dir = if dir_path.is_empty() {
434            "".to_string()
435        } else if dir_path.ends_with('/') {
436            dir_path.to_string()
437        } else {
438            format!("{}/", dir_path)
439        };
440
441        self.entries
442            .values()
443            .filter(|entry| {
444                entry.name.starts_with(&normalized_dir) &&
445                    entry.name != normalized_dir &&
446                    !entry.name[normalized_dir.len()..].contains('/')
447            })
448            .collect()
449    }
450
451    /// 查找匹配模式的文件
452    pub fn find_files(&self, pattern: &str) -> Vec<&ZipEntry> {
453        self.entries
454            .values()
455            .filter(|entry| entry.name.contains(pattern))
456            .collect()
457    }
458}
459
460/// ZIP文档写入器
461pub struct ZipWriter<W: Write + Seek> {
462    writer: zip::ZipWriter<W>,
463    written_files: Vec<String>,
464}
465
466impl ZipWriter<File> {
467    /// 创建新的ZIP文件
468    pub fn create_file<P: AsRef<Path>>(path: P) -> Result<Self> {
469        let file = File::create(&path).map_err(|e| {
470            OfficeError::Io(e).with_context(ErrorContext {
471                operation: Some("创建ZIP文件".to_string()),
472                file_path: Some(path.as_ref().to_string_lossy().to_string()),
473                ..Default::default()
474            })
475        })?;
476
477        Self::new(file)
478    }
479}
480
481impl<W: Write + Seek> ZipWriter<W> {
482    /// 创建新的ZIP写入器
483    pub fn new(writer: W) -> Result<Self> {
484        let zip_writer = zip::ZipWriter::new(writer);
485        Ok(Self {
486            writer: zip_writer,
487            written_files: Vec::new(),
488        })
489    }
490
491    /// 添加文件到ZIP
492    pub fn add_file(&mut self, name: &str, data: &[u8]) -> Result<()> {
493        let options = FileOptions::<()>
494            ::default()
495            .compression_method(CompressionMethod::Deflated)
496            .unix_permissions(0o644);
497
498        self.writer.start_file(name, options).map_err(|e| {
499            OfficeError::Zip(e).with_context(ErrorContext {
500                operation: Some(format!("开始写入文件: {}", name)),
501                file_path: Some(name.to_string()),
502                ..Default::default()
503            })
504        })?;
505
506        self.writer.write_all(data).map_err(|e| {
507            OfficeError::Io(e).with_context(ErrorContext {
508                operation: Some(format!("写入文件数据: {}", name)),
509                file_path: Some(name.to_string()),
510                ..Default::default()
511            })
512        })?;
513
514        self.written_files.push(name.to_string());
515        Ok(())
516    }
517
518    /// 添加字符串文件到ZIP
519    pub fn add_file_from_string(&mut self, name: &str, content: &str) -> Result<()> {
520        self.add_file(name, content.as_bytes())
521    }
522
523    /// 添加目录到ZIP
524    pub fn add_directory(&mut self, name: &str) -> Result<()> {
525        let dir_name = if name.ends_with('/') { name.to_string() } else { format!("{}/", name) };
526
527        let options = FileOptions::<()>::default().compression_method(CompressionMethod::Stored);
528
529        self.writer.start_file(&dir_name, options).map_err(|e| {
530            OfficeError::Zip(e).with_context(ErrorContext {
531                operation: Some(format!("创建目录: {}", name)),
532                file_path: Some(name.to_string()),
533                ..Default::default()
534            })
535        })?;
536
537        self.written_files.push(dir_name);
538        Ok(())
539    }
540
541    /// 从现有文件添加到ZIP
542    pub fn add_file_from_path<P: AsRef<Path>>(
543        &mut self,
544        zip_path: &str,
545        file_path: P
546    ) -> Result<()> {
547        let mut file = File::open(&file_path).map_err(|e| {
548            OfficeError::Io(e).with_context(ErrorContext {
549                operation: Some("打开源文件".to_string()),
550                file_path: Some(file_path.as_ref().to_string_lossy().to_string()),
551                ..Default::default()
552            })
553        })?;
554
555        let mut buffer = Vec::new();
556        file.read_to_end(&mut buffer).map_err(|e| {
557            OfficeError::Io(e).with_context(ErrorContext {
558                operation: Some("读取源文件".to_string()),
559                file_path: Some(file_path.as_ref().to_string_lossy().to_string()),
560                ..Default::default()
561            })
562        })?;
563
564        self.add_file(zip_path, &buffer)
565    }
566
567    /// 获取已写入的文件列表
568    pub fn written_files(&self) -> &[String] {
569        &self.written_files
570    }
571
572    /// 完成ZIP文件写入
573    pub fn finish(self) -> Result<W> {
574        self.writer.finish().map_err(|e| {
575            OfficeError::Zip(e).with_context(ErrorContext {
576                operation: Some("完成ZIP文件写入".to_string()),
577                ..Default::default()
578            })
579        })
580    }
581}
582
583/// ZIP工具函数
584pub mod utils {
585    use super::*;
586
587    /// 检查文件是否为ZIP格式
588    pub fn is_zip_file<P: AsRef<Path>>(path: P) -> bool {
589        if let Ok(file) = File::open(path) {
590            let reader = BufReader::new(file);
591            ZipArchive::new(reader).is_ok()
592        } else {
593            false
594        }
595    }
596
597    /// 获取ZIP文件信息
598    pub fn get_zip_info<P: AsRef<Path>>(path: P) -> Result<(usize, u64, u64)> {
599        let reader = ZipReader::open_file(path)?;
600        let entries = reader.entries();
601
602        let file_count = entries.len();
603        let total_size = entries
604            .values()
605            .map(|e| e.size)
606            .sum();
607        let total_compressed_size = entries
608            .values()
609            .map(|e| e.compressed_size)
610            .sum();
611
612        Ok((file_count, total_size, total_compressed_size))
613    }
614
615    /// 验证ZIP文件完整性
616    pub fn validate_zip<P: AsRef<Path>>(path: P) -> Result<bool> {
617        let mut reader = ZipReader::open_file(path)?;
618
619        // 收集所有文件名
620        let file_names: Vec<String> = reader
621            .entries()
622            .iter()
623            .filter(|(_, entry)| entry.is_file())
624            .map(|(name, _)| name.clone())
625            .collect();
626
627        // 尝试读取所有文件
628        for name in file_names {
629            let _data = reader.read_file(&name)?;
630            // 如果能成功读取,说明文件完整
631        }
632
633        Ok(true)
634    }
635
636    /// 创建内存中的ZIP
637    pub fn create_memory_zip(files: &[(String, Vec<u8>)]) -> Result<Vec<u8>> {
638        let mut buffer = Vec::new();
639        {
640            let cursor = Cursor::new(&mut buffer);
641            let mut writer = ZipWriter::new(cursor)?;
642
643            for (name, data) in files {
644                writer.add_file(name, data)?;
645            }
646
647            writer.finish()?;
648        }
649
650        Ok(buffer)
651    }
652
653    /// 从内存中读取ZIP
654    pub fn read_memory_zip(data: &[u8]) -> Result<ZipReader<Cursor<&[u8]>>> {
655        let cursor = Cursor::new(data);
656        ZipReader::new(cursor)
657    }
658
659    /// 复制ZIP文件中的特定文件到新ZIP
660    pub fn copy_zip_files<P1: AsRef<Path>, P2: AsRef<Path>>(
661        source_path: P1,
662        target_path: P2,
663        file_patterns: &[&str]
664    ) -> Result<()> {
665        let mut source_reader = ZipReader::open_file(source_path)?;
666        let mut target_writer = ZipWriter::create_file(target_path)?;
667
668        // 收集匹配的文件名
669        let mut files_to_copy = Vec::new();
670        for pattern in file_patterns {
671            let matching_files = source_reader.find_files(pattern);
672            for entry in matching_files {
673                if entry.is_file() {
674                    files_to_copy.push(entry.name.clone());
675                }
676            }
677        }
678
679        // 复制文件
680        for file_name in files_to_copy {
681            let data = source_reader.read_file(&file_name)?;
682            target_writer.add_file(&file_name, &data)?;
683        }
684
685        target_writer.finish()?;
686        Ok(())
687    }
688}
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693    use std::io::Cursor;
694
695    #[test]
696    fn test_memory_zip_creation() {
697        let files = vec![
698            ("test1.txt".to_string(), b"Hello World".to_vec()),
699            ("test2.txt".to_string(), b"Goodbye World".to_vec())
700        ];
701
702        let zip_data = utils::create_memory_zip(&files).unwrap();
703        assert!(!zip_data.is_empty());
704
705        let mut reader = utils::read_memory_zip(&zip_data).unwrap();
706        assert!(reader.contains_file("test1.txt"));
707        assert!(reader.contains_file("test2.txt"));
708
709        let content1 = reader.read_file_to_string("test1.txt").unwrap();
710        assert_eq!(content1, "Hello World");
711    }
712
713    #[test]
714    fn test_zip_entry() {
715        let mut entry = ZipEntry::new("folder/test.xml".to_string());
716        entry.size = 1024;
717
718        assert_eq!(entry.file_name(), Some("test.xml"));
719        assert_eq!(entry.extension(), Some("xml"));
720        assert_eq!(entry.parent_path(), Some("folder"));
721        assert!(entry.is_file());
722    }
723}