1use 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#[derive(Debug, Clone)]
16pub struct ZipSecurityConfig {
17 pub max_uncompressed_size: u64,
19 pub max_file_size: u64,
21 pub max_file_count: usize,
23 pub allow_path_traversal: bool,
25 pub memory_buffer_size: u64,
27}
28
29impl Default for ZipSecurityConfig {
30 fn default() -> Self {
31 Self {
32 max_uncompressed_size: 100 * 1024 * 1024, max_file_size: 50 * 1024 * 1024, max_file_count: 1000, allow_path_traversal: false,
36 memory_buffer_size: 50 * 1024 * 1024, }
38 }
39}
40
41impl ZipSecurityConfig {
42 pub fn permissive() -> Self {
44 Self {
45 max_uncompressed_size: 1024 * 1024 * 1024, max_file_size: 500 * 1024 * 1024, max_file_count: 10000,
48 allow_path_traversal: false,
49 memory_buffer_size: 500 * 1024 * 1024, }
51 }
52
53 pub fn strict() -> Self {
55 Self {
56 max_uncompressed_size: 10 * 1024 * 1024, max_file_size: 5 * 1024 * 1024, max_file_count: 100,
59 allow_path_traversal: false,
60 memory_buffer_size: 5 * 1024 * 1024, }
62 }
63}
64
65#[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
75fn 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 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 pub fn is_file(&self) -> bool {
108 !self.is_directory
109 }
110
111 pub fn extension(&self) -> Option<&str> {
113 Path::new(&self.name)
114 .extension()
115 .and_then(|ext| ext.to_str())
116 }
117
118 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 pub fn parent_path(&self) -> Option<&str> {
127 Path::new(&self.name)
128 .parent()
129 .and_then(|path| path.to_str())
130 }
131}
132
133pub 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 pub fn open_file<P: AsRef<Path>>(path: P) -> Result<Self> {
144 Self::open_file_with_config(path, ZipSecurityConfig::default())
145 }
146
147 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 pub fn new(reader: R) -> Result<Self> {
170 Self::new_with_config(reader, ZipSecurityConfig::default())
171 }
172
173 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 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 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 validate_zip_path(file_name, config.allow_path_traversal)?;
207
208 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 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 pub fn entries(&self) -> &HashMap<String, ZipEntry> {
263 &self.entries
264 }
265
266 pub fn contains_file(&self, name: &str) -> bool {
268 self.entries.contains_key(name)
269 }
270
271 pub fn get_entry(&self, name: &str) -> Option<&ZipEntry> {
273 self.entries.get(name)
274 }
275
276 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 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 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 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 pub fn extract_file<P: AsRef<Path>>(&mut self, name: &str, output_path: P) -> Result<()> {
326 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 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 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 let mut processed_files = std::collections::HashSet::new();
378
379 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 if !processed_files.insert(name.clone()) {
389 continue;
390 }
391
392 let entry = &self.entries[&name];
393
394 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 let output_path_str = output_path.to_string_lossy();
412 validate_zip_path(&output_path_str, self.security_config.allow_path_traversal)?;
413
414 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 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 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
460pub struct ZipWriter<W: Write + Seek> {
462 writer: zip::ZipWriter<W>,
463 written_files: Vec<String>,
464}
465
466impl ZipWriter<File> {
467 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 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 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 pub fn add_file_from_string(&mut self, name: &str, content: &str) -> Result<()> {
520 self.add_file(name, content.as_bytes())
521 }
522
523 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 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 pub fn written_files(&self) -> &[String] {
569 &self.written_files
570 }
571
572 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
583pub mod utils {
585 use super::*;
586
587 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 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 pub fn validate_zip<P: AsRef<Path>>(path: P) -> Result<bool> {
617 let mut reader = ZipReader::open_file(path)?;
618
619 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 for name in file_names {
629 let _data = reader.read_file(&name)?;
630 }
632
633 Ok(true)
634 }
635
636 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 pub fn read_memory_zip(data: &[u8]) -> Result<ZipReader<Cursor<&[u8]>>> {
655 let cursor = Cursor::new(data);
656 ZipReader::new(cursor)
657 }
658
659 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 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 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}