1use alloc::string::String;
6use alloc::vec::Vec;
7use alloc::format;
8use core::fmt;
9
10#[cfg(feature = "std")]
11use std::path::Path;
12
13#[derive(Debug, Clone, Default)]
19#[repr(C)]
20pub struct ZipReadConfig {
21 pub max_file_size: u64,
23 pub allow_path_traversal: bool,
25 pub skip_encrypted: bool,
27}
28
29impl ZipReadConfig {
30 pub fn new() -> Self {
31 Self::default()
32 }
33
34 pub fn with_max_file_size(mut self, max_size: u64) -> Self {
35 self.max_file_size = max_size;
36 self
37 }
38
39 pub fn with_allow_path_traversal(mut self, allow: bool) -> Self {
40 self.allow_path_traversal = allow;
41 self
42 }
43}
44
45#[derive(Debug, Clone)]
47#[repr(C)]
48pub struct ZipWriteConfig {
49 pub compression_method: u8,
51 pub compression_level: u8,
53 pub unix_permissions: u32,
55 pub comment: String,
57}
58
59impl Default for ZipWriteConfig {
60 fn default() -> Self {
61 Self {
62 compression_method: 1, compression_level: 6, unix_permissions: 0o644,
65 comment: String::new(),
66 }
67 }
68}
69
70impl ZipWriteConfig {
71 pub fn new() -> Self {
72 Self::default()
73 }
74
75 pub fn store() -> Self {
76 Self {
77 compression_method: 0,
78 ..Default::default()
79 }
80 }
81
82 pub fn deflate(level: u8) -> Self {
83 Self {
84 compression_method: 1,
85 compression_level: level.min(9),
86 ..Default::default()
87 }
88 }
89
90 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
91 self.comment = comment.into();
92 self
93 }
94}
95
96#[derive(Debug, Clone)]
102#[repr(C)]
103pub struct ZipPathEntry {
104 pub path: String,
106 pub is_directory: bool,
108 pub size: u64,
110 pub compressed_size: u64,
112 pub crc32: u32,
114}
115
116pub type ZipPathEntryVec = Vec<ZipPathEntry>;
118
119#[derive(Debug, Clone)]
121#[repr(C)]
122pub struct ZipFileEntry {
123 pub path: String,
125 pub data: Vec<u8>,
127 pub is_directory: bool,
129}
130
131impl ZipFileEntry {
132 pub fn file(path: impl Into<String>, data: Vec<u8>) -> Self {
134 Self {
135 path: path.into(),
136 data,
137 is_directory: false,
138 }
139 }
140
141 pub fn directory(path: impl Into<String>) -> Self {
143 Self {
144 path: path.into(),
145 data: Vec::new(),
146 is_directory: true,
147 }
148 }
149}
150
151pub type ZipFileEntryVec = Vec<ZipFileEntry>;
153
154#[derive(Debug, Clone, PartialEq)]
160#[repr(C, u8)]
161pub enum ZipReadError {
162 InvalidFormat(String),
164 FileNotFound(String),
166 IoError(String),
168 UnsafePath(String),
170 EncryptedFile(String),
172 FileTooLarge { path: String, size: u64, max_size: u64 },
174}
175
176impl fmt::Display for ZipReadError {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match self {
179 ZipReadError::InvalidFormat(msg) => write!(f, "Invalid ZIP format: {}", msg),
180 ZipReadError::FileNotFound(path) => write!(f, "File not found: {}", path),
181 ZipReadError::IoError(msg) => write!(f, "I/O error: {}", msg),
182 ZipReadError::UnsafePath(path) => write!(f, "Unsafe path: {}", path),
183 ZipReadError::EncryptedFile(path) => write!(f, "Encrypted file: {}", path),
184 ZipReadError::FileTooLarge { path, size, max_size } => {
185 write!(f, "File too large: {} ({} > {})", path, size, max_size)
186 }
187 }
188 }
189}
190
191#[cfg(feature = "std")]
192impl std::error::Error for ZipReadError {}
193
194#[derive(Debug, Clone, PartialEq)]
196#[repr(C, u8)]
197pub enum ZipWriteError {
198 IoError(String),
200 InvalidPath(String),
202 CompressionError(String),
204}
205
206impl fmt::Display for ZipWriteError {
207 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208 match self {
209 ZipWriteError::IoError(msg) => write!(f, "I/O error: {}", msg),
210 ZipWriteError::InvalidPath(path) => write!(f, "Invalid path: {}", path),
211 ZipWriteError::CompressionError(msg) => write!(f, "Compression error: {}", msg),
212 }
213 }
214}
215
216#[cfg(feature = "std")]
217impl std::error::Error for ZipWriteError {}
218
219#[derive(Debug, Clone, Default)]
225#[repr(C)]
226pub struct ZipFile {
227 pub entries: ZipFileEntryVec,
229}
230
231impl ZipFile {
232 pub fn new() -> Self {
234 Self {
235 entries: Vec::new(),
236 }
237 }
238
239 #[cfg(feature = "zip_support")]
248 pub fn list(data: &[u8], config: &ZipReadConfig) -> Result<ZipPathEntryVec, ZipReadError> {
249 use std::io::Cursor;
250
251 let cursor = Cursor::new(data);
252 let mut archive = zip::ZipArchive::new(cursor)
253 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
254
255 let mut entries = Vec::new();
256
257 for i in 0..archive.len() {
258 let file = archive.by_index(i)
259 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
260
261 let path = file.name().to_string();
262
263 if !config.allow_path_traversal && path.contains("..") {
265 return Err(ZipReadError::UnsafePath(path));
266 }
267
268 entries.push(ZipPathEntry {
269 path,
270 is_directory: file.is_dir(),
271 size: file.size(),
272 compressed_size: file.compressed_size(),
273 crc32: file.crc32(),
274 });
275 }
276
277 Ok(entries)
278 }
279
280 #[cfg(feature = "zip_support")]
290 pub fn get_single_file(
291 data: &[u8],
292 entry: &ZipPathEntry,
293 config: &ZipReadConfig,
294 ) -> Result<Option<Vec<u8>>, ZipReadError> {
295 use std::io::{Cursor, Read};
296
297 if config.max_file_size > 0 && entry.size > config.max_file_size {
299 return Err(ZipReadError::FileTooLarge {
300 path: entry.path.clone(),
301 size: entry.size,
302 max_size: config.max_file_size,
303 });
304 }
305
306 let cursor = Cursor::new(data);
307 let mut archive = zip::ZipArchive::new(cursor)
308 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
309
310 let mut file = match archive.by_name(&entry.path) {
311 Ok(f) => f,
312 Err(zip::result::ZipError::FileNotFound) => return Ok(None),
313 Err(e) => return Err(ZipReadError::IoError(e.to_string())),
314 };
315
316 if file.is_dir() {
317 return Ok(Some(Vec::new()));
318 }
319
320 let mut contents = Vec::with_capacity(entry.size as usize);
321 file.read_to_end(&mut contents)
322 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
323
324 Ok(Some(contents))
325 }
326
327 #[cfg(feature = "zip_support")]
333 pub fn from_bytes(data: Vec<u8>, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
334 use std::io::{Cursor, Read};
335
336 let cursor = Cursor::new(&data);
337 let mut archive = zip::ZipArchive::new(cursor)
338 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
339
340 let mut entries = Vec::new();
341
342 for i in 0..archive.len() {
343 let mut file = archive.by_index(i)
344 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
345
346 let path = file.name().to_string();
347
348 if !config.allow_path_traversal && path.contains("..") {
350 return Err(ZipReadError::UnsafePath(path));
351 }
352
353 if config.max_file_size > 0 && file.size() > config.max_file_size {
355 return Err(ZipReadError::FileTooLarge {
356 path,
357 size: file.size(),
358 max_size: config.max_file_size,
359 });
360 }
361
362 let is_directory = file.is_dir();
363 let mut file_data = Vec::new();
364
365 if !is_directory {
366 file.read_to_end(&mut file_data)
367 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
368 }
369
370 entries.push(ZipFileEntry {
371 path,
372 data: file_data,
373 is_directory,
374 });
375 }
376
377 Ok(Self { entries })
378 }
379
380 #[cfg(all(feature = "zip_support", feature = "std"))]
382 pub fn from_file(path: &Path, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
383 let data = std::fs::read(path)
384 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
385 Self::from_bytes(data, config)
386 }
387
388 #[cfg(feature = "zip_support")]
393 pub fn to_bytes(&self, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
394 use std::io::{Cursor, Write};
395 use zip::write::SimpleFileOptions;
396
397 let buffer = Vec::new();
398 let cursor = Cursor::new(buffer);
399 let mut writer = zip::ZipWriter::new(cursor);
400
401 if !config.comment.is_empty() {
403 writer.set_comment(config.comment.clone());
404 }
405
406 let compression = match config.compression_method {
407 0 => zip::CompressionMethod::Stored,
408 _ => zip::CompressionMethod::Deflated,
409 };
410
411 let options = SimpleFileOptions::default()
412 .compression_method(compression)
413 .unix_permissions(config.unix_permissions);
414
415 for entry in &self.entries {
416 if entry.is_directory {
417 writer.add_directory(&entry.path, options)
418 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
419 } else {
420 writer.start_file(&entry.path, options)
421 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
422 writer.write_all(&entry.data)
423 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
424 }
425 }
426
427 let result = writer.finish()
428 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
429
430 Ok(result.into_inner())
431 }
432
433 #[cfg(all(feature = "zip_support", feature = "std"))]
435 pub fn to_file(&self, path: &Path, config: &ZipWriteConfig) -> Result<(), ZipWriteError> {
436 let data = self.to_bytes(config)?;
437 std::fs::write(path, data)
438 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
439 Ok(())
440 }
441
442 pub fn add_file(&mut self, path: impl Into<String>, data: Vec<u8>) {
448 let path = path.into();
449 self.entries.retain(|e| e.path != path);
451 self.entries.push(ZipFileEntry::file(path, data));
452 }
453
454 pub fn add_directory(&mut self, path: impl Into<String>) {
456 let path = path.into();
457 self.entries.retain(|e| e.path != path);
458 self.entries.push(ZipFileEntry::directory(path));
459 }
460
461 pub fn remove(&mut self, path: &str) {
463 self.entries.retain(|e| e.path != path);
464 }
465
466 pub fn get(&self, path: &str) -> Option<&ZipFileEntry> {
468 self.entries.iter().find(|e| e.path == path)
469 }
470
471 pub fn contains(&self, path: &str) -> bool {
473 self.entries.iter().any(|e| e.path == path)
474 }
475
476 pub fn paths(&self) -> Vec<&str> {
478 self.entries.iter().map(|e| e.path.as_str()).collect()
479 }
480
481 pub fn filter_by_suffix(&self, suffix: &str) -> Vec<&ZipFileEntry> {
483 self.entries.iter()
484 .filter(|e| !e.is_directory && e.path.ends_with(suffix))
485 .collect()
486 }
487}
488
489#[cfg(feature = "zip_support")]
495pub fn zip_create(entries: Vec<ZipFileEntry>, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
496 let zip = ZipFile { entries };
497 zip.to_bytes(config)
498}
499
500#[cfg(feature = "zip_support")]
502pub fn zip_create_from_files(
503 files: Vec<(String, Vec<u8>)>,
504 config: &ZipWriteConfig,
505) -> Result<Vec<u8>, ZipWriteError> {
506 let entries: Vec<ZipFileEntry> = files
507 .into_iter()
508 .map(|(path, data)| ZipFileEntry::file(path, data))
509 .collect();
510 zip_create(entries, config)
511}
512
513#[cfg(feature = "zip_support")]
515pub fn zip_extract_all(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipFileEntry>, ZipReadError> {
516 let zip = ZipFile::from_bytes(data.to_vec(), config)?;
517 Ok(zip.entries)
518}
519
520#[cfg(feature = "zip_support")]
522pub fn zip_list_contents(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipPathEntry>, ZipReadError> {
523 ZipFile::list(data, config)
524}
525
526#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_zip_config_defaults() {
536 let read_config = ZipReadConfig::default();
537 assert_eq!(read_config.max_file_size, 0);
538 assert!(!read_config.allow_path_traversal);
539
540 let write_config = ZipWriteConfig::default();
541 assert_eq!(write_config.compression_method, 1);
542 assert_eq!(write_config.compression_level, 6);
543 }
544
545 #[test]
546 fn test_zip_file_entry_creation() {
547 let file = ZipFileEntry::file("test.txt", b"Hello".to_vec());
548 assert_eq!(file.path, "test.txt");
549 assert!(!file.is_directory);
550 assert_eq!(file.data, b"Hello");
551
552 let dir = ZipFileEntry::directory("subdir/");
553 assert!(dir.is_directory);
554 assert!(dir.data.is_empty());
555 }
556
557 #[cfg(feature = "zip_support")]
558 #[test]
559 fn test_zip_roundtrip() {
560 let files = vec![
561 ("hello.txt".to_string(), b"Hello, World!".to_vec()),
562 ("sub/nested.txt".to_string(), b"Nested file".to_vec()),
563 ];
564
565 let write_config = ZipWriteConfig::default();
566 let zip_data = zip_create_from_files(files, &write_config).expect("Failed to create ZIP");
567
568 let read_config = ZipReadConfig::default();
569 let entries = zip_extract_all(&zip_data, &read_config).expect("Failed to extract");
570
571 assert_eq!(entries.len(), 2);
572 assert!(entries.iter().any(|e| e.path == "hello.txt"));
573 assert!(entries.iter().any(|e| e.path == "sub/nested.txt"));
574 }
575
576 #[cfg(feature = "zip_support")]
577 #[test]
578 fn test_zip_file_manipulation() {
579 let mut zip = ZipFile::new();
580
581 zip.add_file("a.txt", b"AAA".to_vec());
582 zip.add_file("b.txt", b"BBB".to_vec());
583
584 assert_eq!(zip.entries.len(), 2);
585 assert!(zip.contains("a.txt"));
586 assert!(zip.contains("b.txt"));
587
588 zip.remove("a.txt");
589 assert_eq!(zip.entries.len(), 1);
590 assert!(!zip.contains("a.txt"));
591
592 zip.add_file("b.txt", b"NEW".to_vec());
594 assert_eq!(zip.entries.len(), 1);
595 assert_eq!(zip.get("b.txt").unwrap().data, b"NEW");
596 }
597}