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 compression_level: 0,
79 ..Default::default()
80 }
81 }
82
83 pub fn deflate(level: u8) -> Self {
84 Self {
85 compression_method: 1,
86 compression_level: level.min(9),
87 ..Default::default()
88 }
89 }
90
91 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
92 self.comment = comment.into();
93 self
94 }
95}
96
97#[derive(Debug, Clone)]
103#[repr(C)]
104pub struct ZipPathEntry {
105 pub path: String,
107 pub is_directory: bool,
109 pub size: u64,
111 pub compressed_size: u64,
113 pub crc32: u32,
115}
116
117pub type ZipPathEntryVec = Vec<ZipPathEntry>;
119
120#[derive(Debug, Clone)]
122#[repr(C)]
123pub struct ZipFileEntry {
124 pub path: String,
126 pub data: Vec<u8>,
128 pub is_directory: bool,
130}
131
132impl ZipFileEntry {
133 pub fn file(path: impl Into<String>, data: Vec<u8>) -> Self {
135 Self {
136 path: path.into(),
137 data,
138 is_directory: false,
139 }
140 }
141
142 pub fn directory(path: impl Into<String>) -> Self {
144 Self {
145 path: path.into(),
146 data: Vec::new(),
147 is_directory: true,
148 }
149 }
150}
151
152pub type ZipFileEntryVec = Vec<ZipFileEntry>;
154
155#[derive(Debug, Clone, PartialEq)]
161#[repr(C, u8)]
162pub enum ZipReadError {
163 InvalidFormat(String),
165 FileNotFound(String),
167 IoError(String),
169 UnsafePath(String),
171 EncryptedFile(String),
173 FileTooLarge { path: String, size: u64, max_size: u64 },
175}
176
177impl fmt::Display for ZipReadError {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179 match self {
180 ZipReadError::InvalidFormat(msg) => write!(f, "Invalid ZIP format: {}", msg),
181 ZipReadError::FileNotFound(path) => write!(f, "File not found: {}", path),
182 ZipReadError::IoError(msg) => write!(f, "I/O error: {}", msg),
183 ZipReadError::UnsafePath(path) => write!(f, "Unsafe path: {}", path),
184 ZipReadError::EncryptedFile(path) => write!(f, "Encrypted file: {}", path),
185 ZipReadError::FileTooLarge { path, size, max_size } => {
186 write!(f, "File too large: {} ({} > {})", path, size, max_size)
187 }
188 }
189 }
190}
191
192#[cfg(feature = "std")]
193impl std::error::Error for ZipReadError {}
194
195#[derive(Debug, Clone, PartialEq)]
197#[repr(C, u8)]
198pub enum ZipWriteError {
199 IoError(String),
201 InvalidPath(String),
203 CompressionError(String),
205}
206
207impl fmt::Display for ZipWriteError {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 match self {
210 ZipWriteError::IoError(msg) => write!(f, "I/O error: {}", msg),
211 ZipWriteError::InvalidPath(path) => write!(f, "Invalid path: {}", path),
212 ZipWriteError::CompressionError(msg) => write!(f, "Compression error: {}", msg),
213 }
214 }
215}
216
217#[cfg(feature = "std")]
218impl std::error::Error for ZipWriteError {}
219
220#[derive(Debug, Clone, Default)]
226#[repr(C)]
227pub struct ZipFile {
228 pub entries: ZipFileEntryVec,
230}
231
232impl ZipFile {
233 pub fn new() -> Self {
235 Self {
236 entries: Vec::new(),
237 }
238 }
239
240 #[cfg(feature = "zip_support")]
249 pub fn list(data: &[u8], config: &ZipReadConfig) -> Result<ZipPathEntryVec, ZipReadError> {
250 use std::io::Cursor;
251
252 let cursor = Cursor::new(data);
253 let mut archive = zip::ZipArchive::new(cursor)
254 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
255
256 let mut entries = Vec::new();
257
258 for i in 0..archive.len() {
259 let file = archive.by_index(i)
260 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
261
262 let path = file.name().to_string();
263
264 if !config.allow_path_traversal && path.contains("..") {
266 return Err(ZipReadError::UnsafePath(path));
267 }
268
269 entries.push(ZipPathEntry {
270 path,
271 is_directory: file.is_dir(),
272 size: file.size(),
273 compressed_size: file.compressed_size(),
274 crc32: file.crc32(),
275 });
276 }
277
278 Ok(entries)
279 }
280
281 #[cfg(feature = "zip_support")]
291 pub fn get_single_file(
292 data: &[u8],
293 entry: &ZipPathEntry,
294 config: &ZipReadConfig,
295 ) -> Result<Option<Vec<u8>>, ZipReadError> {
296 use std::io::{Cursor, Read};
297
298 if config.max_file_size > 0 && entry.size > config.max_file_size {
300 return Err(ZipReadError::FileTooLarge {
301 path: entry.path.clone(),
302 size: entry.size,
303 max_size: config.max_file_size,
304 });
305 }
306
307 let cursor = Cursor::new(data);
308 let mut archive = zip::ZipArchive::new(cursor)
309 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
310
311 let mut file = match archive.by_name(&entry.path) {
312 Ok(f) => f,
313 Err(zip::result::ZipError::FileNotFound) => return Ok(None),
314 Err(e) => return Err(ZipReadError::IoError(e.to_string())),
315 };
316
317 if file.is_dir() {
318 return Ok(Some(Vec::new()));
319 }
320
321 let mut contents = Vec::with_capacity(entry.size as usize);
322 file.read_to_end(&mut contents)
323 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
324
325 Ok(Some(contents))
326 }
327
328 #[cfg(feature = "zip_support")]
334 pub fn from_bytes(data: Vec<u8>, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
335 use std::io::{Cursor, Read};
336
337 let cursor = Cursor::new(&data);
338 let mut archive = zip::ZipArchive::new(cursor)
339 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
340
341 let mut entries = Vec::new();
342
343 for i in 0..archive.len() {
344 let mut file = archive.by_index(i)
345 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
346
347 let path = file.name().to_string();
348
349 if !config.allow_path_traversal && path.contains("..") {
351 return Err(ZipReadError::UnsafePath(path));
352 }
353
354 if config.max_file_size > 0 && file.size() > config.max_file_size {
356 return Err(ZipReadError::FileTooLarge {
357 path,
358 size: file.size(),
359 max_size: config.max_file_size,
360 });
361 }
362
363 let is_directory = file.is_dir();
364 let mut file_data = Vec::new();
365
366 if !is_directory {
367 file.read_to_end(&mut file_data)
368 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
369 }
370
371 entries.push(ZipFileEntry {
372 path,
373 data: file_data,
374 is_directory,
375 });
376 }
377
378 Ok(Self { entries })
379 }
380
381 #[cfg(all(feature = "zip_support", feature = "std"))]
383 pub fn from_file(path: &Path, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
384 let data = std::fs::read(path)
385 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
386 Self::from_bytes(data, config)
387 }
388
389 #[cfg(feature = "zip_support")]
394 pub fn to_bytes(&self, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
395 use std::io::{Cursor, Write};
396 use zip::write::SimpleFileOptions;
397
398 let buffer = Vec::new();
399 let cursor = Cursor::new(buffer);
400 let mut writer = zip::ZipWriter::new(cursor);
401
402 if !config.comment.is_empty() {
404 writer.set_comment(config.comment.clone());
405 }
406
407 let compression = match config.compression_method {
408 0 => zip::CompressionMethod::Stored,
409 _ => zip::CompressionMethod::Deflated,
410 };
411
412 let options = SimpleFileOptions::default()
413 .compression_method(compression)
414 .compression_level(Some(config.compression_level as i64))
415 .unix_permissions(config.unix_permissions);
416
417 for entry in &self.entries {
418 if entry.is_directory {
419 writer.add_directory(&entry.path, options)
420 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
421 } else {
422 writer.start_file(&entry.path, options)
423 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
424 writer.write_all(&entry.data)
425 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
426 }
427 }
428
429 let result = writer.finish()
430 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
431
432 Ok(result.into_inner())
433 }
434
435 #[cfg(all(feature = "zip_support", feature = "std"))]
437 pub fn to_file(&self, path: &Path, config: &ZipWriteConfig) -> Result<(), ZipWriteError> {
438 let data = self.to_bytes(config)?;
439 std::fs::write(path, data)
440 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
441 Ok(())
442 }
443
444 pub fn add_file(&mut self, path: impl Into<String>, data: Vec<u8>) {
450 let path = path.into();
451 self.entries.retain(|e| e.path != path);
453 self.entries.push(ZipFileEntry::file(path, data));
454 }
455
456 pub fn add_directory(&mut self, path: impl Into<String>) {
458 let path = path.into();
459 self.entries.retain(|e| e.path != path);
460 self.entries.push(ZipFileEntry::directory(path));
461 }
462
463 pub fn remove(&mut self, path: &str) {
465 self.entries.retain(|e| e.path != path);
466 }
467
468 pub fn get(&self, path: &str) -> Option<&ZipFileEntry> {
470 self.entries.iter().find(|e| e.path == path)
471 }
472
473 pub fn contains(&self, path: &str) -> bool {
475 self.entries.iter().any(|e| e.path == path)
476 }
477
478 pub fn paths(&self) -> Vec<&str> {
480 self.entries.iter().map(|e| e.path.as_str()).collect()
481 }
482
483 pub fn filter_by_suffix(&self, suffix: &str) -> Vec<&ZipFileEntry> {
485 self.entries.iter()
486 .filter(|e| !e.is_directory && e.path.ends_with(suffix))
487 .collect()
488 }
489}
490
491#[cfg(feature = "zip_support")]
497pub fn zip_create(entries: Vec<ZipFileEntry>, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
498 let zip = ZipFile { entries };
499 zip.to_bytes(config)
500}
501
502#[cfg(feature = "zip_support")]
504pub fn zip_create_from_files(
505 files: Vec<(String, Vec<u8>)>,
506 config: &ZipWriteConfig,
507) -> Result<Vec<u8>, ZipWriteError> {
508 let entries: Vec<ZipFileEntry> = files
509 .into_iter()
510 .map(|(path, data)| ZipFileEntry::file(path, data))
511 .collect();
512 zip_create(entries, config)
513}
514
515#[cfg(feature = "zip_support")]
517pub fn zip_extract_all(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipFileEntry>, ZipReadError> {
518 let zip = ZipFile::from_bytes(data.to_vec(), config)?;
519 Ok(zip.entries)
520}
521
522#[cfg(feature = "zip_support")]
524pub fn zip_list_contents(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipPathEntry>, ZipReadError> {
525 ZipFile::list(data, config)
526}
527
528#[cfg(test)]
533mod tests {
534 use super::*;
535
536 #[test]
537 fn test_zip_config_defaults() {
538 let read_config = ZipReadConfig::default();
539 assert_eq!(read_config.max_file_size, 0);
540 assert!(!read_config.allow_path_traversal);
541
542 let write_config = ZipWriteConfig::default();
543 assert_eq!(write_config.compression_method, 1);
544 assert_eq!(write_config.compression_level, 6);
545 }
546
547 #[test]
548 fn test_zip_file_entry_creation() {
549 let file = ZipFileEntry::file("test.txt", b"Hello".to_vec());
550 assert_eq!(file.path, "test.txt");
551 assert!(!file.is_directory);
552 assert_eq!(file.data, b"Hello");
553
554 let dir = ZipFileEntry::directory("subdir/");
555 assert!(dir.is_directory);
556 assert!(dir.data.is_empty());
557 }
558
559 #[cfg(feature = "zip_support")]
560 #[test]
561 fn test_zip_roundtrip() {
562 let files = vec![
563 ("hello.txt".to_string(), b"Hello, World!".to_vec()),
564 ("sub/nested.txt".to_string(), b"Nested file".to_vec()),
565 ];
566
567 let write_config = ZipWriteConfig::default();
568 let zip_data = zip_create_from_files(files, &write_config).expect("Failed to create ZIP");
569
570 let read_config = ZipReadConfig::default();
571 let entries = zip_extract_all(&zip_data, &read_config).expect("Failed to extract");
572
573 assert_eq!(entries.len(), 2);
574 assert!(entries.iter().any(|e| e.path == "hello.txt"));
575 assert!(entries.iter().any(|e| e.path == "sub/nested.txt"));
576 }
577
578 #[cfg(feature = "zip_support")]
579 #[test]
580 fn test_zip_file_manipulation() {
581 let mut zip = ZipFile::new();
582
583 zip.add_file("a.txt", b"AAA".to_vec());
584 zip.add_file("b.txt", b"BBB".to_vec());
585
586 assert_eq!(zip.entries.len(), 2);
587 assert!(zip.contains("a.txt"));
588 assert!(zip.contains("b.txt"));
589
590 zip.remove("a.txt");
591 assert_eq!(zip.entries.len(), 1);
592 assert!(!zip.contains("a.txt"));
593
594 zip.add_file("b.txt", b"NEW".to_vec());
596 assert_eq!(zip.entries.len(), 1);
597 assert_eq!(zip.get("b.txt").unwrap().data, b"NEW");
598 }
599}