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(Copy, 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 #[must_use] pub fn new() -> Self {
31 Self::default()
32 }
33
34 #[must_use] pub const fn with_max_file_size(mut self, max_size: u64) -> Self {
35 self.max_file_size = max_size;
36 self
37 }
38
39 #[must_use] pub const 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 #[must_use] pub fn new() -> Self {
72 Self::default()
73 }
74
75 #[must_use] pub fn store() -> Self {
76 Self {
77 compression_method: 0,
78 compression_level: 0,
79 ..Default::default()
80 }
81 }
82
83 #[must_use] 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 #[must_use]
92 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
93 self.comment = comment.into();
94 self
95 }
96}
97
98#[derive(Debug, Clone)]
104#[repr(C)]
105pub struct ZipPathEntry {
106 pub path: String,
108 pub is_directory: bool,
110 pub size: u64,
112 pub compressed_size: u64,
114 pub crc32: u32,
116}
117
118pub type ZipPathEntryVec = Vec<ZipPathEntry>;
120
121#[derive(Debug, Clone)]
123#[repr(C)]
124pub struct ZipFileEntry {
125 pub path: String,
127 pub data: Vec<u8>,
129 pub is_directory: bool,
131}
132
133impl ZipFileEntry {
134 pub fn file(path: impl Into<String>, data: Vec<u8>) -> Self {
136 Self {
137 path: path.into(),
138 data,
139 is_directory: false,
140 }
141 }
142
143 pub fn directory(path: impl Into<String>) -> Self {
145 Self {
146 path: path.into(),
147 data: Vec::new(),
148 is_directory: true,
149 }
150 }
151}
152
153pub type ZipFileEntryVec = Vec<ZipFileEntry>;
155
156#[derive(Debug, Clone, PartialEq, Eq)]
162#[repr(C, u8)]
163pub enum ZipReadError {
164 InvalidFormat(String),
166 FileNotFound(String),
168 IoError(String),
170 UnsafePath(String),
172 EncryptedFile(String),
174 FileTooLarge { path: String, size: u64, max_size: u64 },
176}
177
178impl fmt::Display for ZipReadError {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 match self {
181 Self::InvalidFormat(msg) => write!(f, "Invalid ZIP format: {msg}"),
182 Self::FileNotFound(path) => write!(f, "File not found: {path}"),
183 Self::IoError(msg) => write!(f, "I/O error: {msg}"),
184 Self::UnsafePath(path) => write!(f, "Unsafe path: {path}"),
185 Self::EncryptedFile(path) => write!(f, "Encrypted file: {path}"),
186 Self::FileTooLarge { path, size, max_size } => {
187 write!(f, "File too large: {path} ({size} > {max_size})")
188 }
189 }
190 }
191}
192
193#[cfg(feature = "std")]
194impl std::error::Error for ZipReadError {}
195
196#[derive(Debug, Clone, PartialEq, Eq)]
198#[repr(C, u8)]
199pub enum ZipWriteError {
200 IoError(String),
202 InvalidPath(String),
204 CompressionError(String),
206}
207
208impl fmt::Display for ZipWriteError {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 Self::IoError(msg) => write!(f, "I/O error: {msg}"),
212 Self::InvalidPath(path) => write!(f, "Invalid path: {path}"),
213 Self::CompressionError(msg) => write!(f, "Compression error: {msg}"),
214 }
215 }
216}
217
218#[cfg(feature = "std")]
219impl std::error::Error for ZipWriteError {}
220
221#[derive(Debug, Clone, Default)]
227#[repr(C)]
228pub struct ZipFile {
229 pub entries: ZipFileEntryVec,
231}
232
233impl ZipFile {
234 #[must_use] pub const fn new() -> Self {
236 Self {
237 entries: Vec::new(),
238 }
239 }
240
241 #[cfg(feature = "zip")]
250 pub fn list(data: &[u8], config: &ZipReadConfig) -> Result<ZipPathEntryVec, ZipReadError> {
254 use std::io::Cursor;
255
256 let cursor = Cursor::new(data);
257 let mut archive = zip::ZipArchive::new(cursor)
258 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
259
260 let mut entries = Vec::new();
261
262 for i in 0..archive.len() {
263 let file = archive.by_index(i)
264 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
265
266 let path = file.name().to_string();
267
268 if !config.allow_path_traversal && path.contains("..") {
270 return Err(ZipReadError::UnsafePath(path));
271 }
272
273 entries.push(ZipPathEntry {
274 path,
275 is_directory: file.is_dir(),
276 size: file.size(),
277 compressed_size: file.compressed_size(),
278 crc32: file.crc32(),
279 });
280 }
281
282 Ok(entries)
283 }
284
285 #[cfg(feature = "zip")]
295 pub fn get_single_file(
299 data: &[u8],
300 entry: &ZipPathEntry,
301 config: &ZipReadConfig,
302 ) -> Result<Option<Vec<u8>>, ZipReadError> {
303 use std::io::{Cursor, Read};
304
305 if config.max_file_size > 0 && entry.size > config.max_file_size {
307 return Err(ZipReadError::FileTooLarge {
308 path: entry.path.clone(),
309 size: entry.size,
310 max_size: config.max_file_size,
311 });
312 }
313
314 let cursor = Cursor::new(data);
315 let mut archive = zip::ZipArchive::new(cursor)
316 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
317
318 let mut file = match archive.by_name(&entry.path) {
319 Ok(f) => f,
320 Err(zip::result::ZipError::FileNotFound) => return Ok(None),
321 Err(e) => return Err(ZipReadError::IoError(e.to_string())),
322 };
323
324 if file.is_dir() {
325 return Ok(Some(Vec::new()));
326 }
327
328 let mut contents = Vec::with_capacity(usize::try_from(entry.size).unwrap_or(0));
329 file.read_to_end(&mut contents)
330 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
331
332 Ok(Some(contents))
333 }
334
335 #[cfg(feature = "zip")]
341 pub fn from_bytes(data: &[u8], config: &ZipReadConfig) -> Result<Self, ZipReadError> {
345 use std::io::{Cursor, Read};
346
347 let cursor = Cursor::new(data);
348 let mut archive = zip::ZipArchive::new(cursor)
349 .map_err(|e| ZipReadError::InvalidFormat(e.to_string()))?;
350
351 let mut entries = Vec::new();
352
353 for i in 0..archive.len() {
354 let mut file = archive.by_index(i)
355 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
356
357 let path = file.name().to_string();
358
359 if !config.allow_path_traversal && path.contains("..") {
361 return Err(ZipReadError::UnsafePath(path));
362 }
363
364 if config.max_file_size > 0 && file.size() > config.max_file_size {
366 return Err(ZipReadError::FileTooLarge {
367 path,
368 size: file.size(),
369 max_size: config.max_file_size,
370 });
371 }
372
373 let is_directory = file.is_dir();
374 let mut file_data = Vec::new();
375
376 if !is_directory {
377 file.read_to_end(&mut file_data)
378 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
379 }
380
381 entries.push(ZipFileEntry {
382 path,
383 data: file_data,
384 is_directory,
385 });
386 }
387
388 Ok(Self { entries })
389 }
390
391 #[cfg(all(feature = "zip", feature = "std"))]
393 pub fn from_file(path: &Path, config: &ZipReadConfig) -> Result<Self, ZipReadError> {
397 let data = std::fs::read(path)
398 .map_err(|e| ZipReadError::IoError(e.to_string()))?;
399 Self::from_bytes(&data, config)
400 }
401
402 #[cfg(feature = "zip")]
407 pub fn to_bytes(&self, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
411 use std::io::{Cursor, Write};
412 use zip::write::SimpleFileOptions;
413
414 let buffer = Vec::new();
415 let cursor = Cursor::new(buffer);
416 let mut writer = zip::ZipWriter::new(cursor);
417
418 if !config.comment.is_empty() {
420 writer.set_comment(config.comment.clone());
421 }
422
423 let compression = match config.compression_method {
424 0 => zip::CompressionMethod::Stored,
425 _ => zip::CompressionMethod::Deflated,
426 };
427
428 let options = SimpleFileOptions::default()
429 .compression_method(compression)
430 .compression_level(Some(i64::from(config.compression_level)))
431 .unix_permissions(config.unix_permissions);
432
433 for entry in &self.entries {
434 if entry.is_directory {
435 writer.add_directory(&entry.path, options)
436 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
437 } else {
438 writer.start_file(&entry.path, options)
439 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
440 writer.write_all(&entry.data)
441 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
442 }
443 }
444
445 let result = writer.finish()
446 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
447
448 Ok(result.into_inner())
449 }
450
451 #[cfg(all(feature = "zip", feature = "std"))]
453 pub fn to_file(&self, path: &Path, config: &ZipWriteConfig) -> Result<(), ZipWriteError> {
457 let data = self.to_bytes(config)?;
458 std::fs::write(path, data)
459 .map_err(|e| ZipWriteError::IoError(e.to_string()))?;
460 Ok(())
461 }
462
463 pub fn add_file(&mut self, path: impl Into<String>, data: Vec<u8>) {
469 let path = path.into();
470 self.entries.retain(|e| e.path != path);
472 self.entries.push(ZipFileEntry::file(path, data));
473 }
474
475 pub fn add_directory(&mut self, path: impl Into<String>) {
477 let path = path.into();
478 self.entries.retain(|e| e.path != path);
479 self.entries.push(ZipFileEntry::directory(path));
480 }
481
482 pub fn remove(&mut self, path: &str) {
484 self.entries.retain(|e| e.path != path);
485 }
486
487 #[must_use] pub fn get(&self, path: &str) -> Option<&ZipFileEntry> {
489 self.entries.iter().find(|e| e.path == path)
490 }
491
492 #[must_use] pub fn contains(&self, path: &str) -> bool {
494 self.entries.iter().any(|e| e.path == path)
495 }
496
497 #[must_use] pub fn paths(&self) -> Vec<&str> {
499 self.entries.iter().map(|e| e.path.as_str()).collect()
500 }
501
502 #[must_use] pub fn filter_by_suffix(&self, suffix: &str) -> Vec<&ZipFileEntry> {
504 self.entries.iter()
505 .filter(|e| !e.is_directory && e.path.ends_with(suffix))
506 .collect()
507 }
508}
509
510#[cfg(feature = "zip")]
516pub fn zip_create(entries: Vec<ZipFileEntry>, config: &ZipWriteConfig) -> Result<Vec<u8>, ZipWriteError> {
520 let zip = ZipFile { entries };
521 zip.to_bytes(config)
522}
523
524#[cfg(feature = "zip")]
526pub fn zip_create_from_files(
530 files: Vec<(String, Vec<u8>)>,
531 config: &ZipWriteConfig,
532) -> Result<Vec<u8>, ZipWriteError> {
533 let entries: Vec<ZipFileEntry> = files
534 .into_iter()
535 .map(|(path, data)| ZipFileEntry::file(path, data))
536 .collect();
537 zip_create(entries, config)
538}
539
540#[cfg(feature = "zip")]
542pub fn zip_extract_all(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipFileEntry>, ZipReadError> {
546 let zip = ZipFile::from_bytes(data, config)?;
547 Ok(zip.entries)
548}
549
550#[cfg(feature = "zip")]
552pub fn zip_list_contents(data: &[u8], config: &ZipReadConfig) -> Result<Vec<ZipPathEntry>, ZipReadError> {
556 ZipFile::list(data, config)
557}
558
559#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_zip_config_defaults() {
569 let read_config = ZipReadConfig::default();
570 assert_eq!(read_config.max_file_size, 0);
571 assert!(!read_config.allow_path_traversal);
572
573 let write_config = ZipWriteConfig::default();
574 assert_eq!(write_config.compression_method, 1);
575 assert_eq!(write_config.compression_level, 6);
576 }
577
578 #[test]
579 fn test_zip_file_entry_creation() {
580 let file = ZipFileEntry::file("test.txt", b"Hello".to_vec());
581 assert_eq!(file.path, "test.txt");
582 assert!(!file.is_directory);
583 assert_eq!(file.data, b"Hello");
584
585 let dir = ZipFileEntry::directory("subdir/");
586 assert!(dir.is_directory);
587 assert!(dir.data.is_empty());
588 }
589
590 #[cfg(feature = "zip")]
591 #[test]
592 fn test_zip_roundtrip() {
593 let files = vec![
594 ("hello.txt".to_string(), b"Hello, World!".to_vec()),
595 ("sub/nested.txt".to_string(), b"Nested file".to_vec()),
596 ];
597
598 let write_config = ZipWriteConfig::default();
599 let zip_data = zip_create_from_files(files, &write_config).expect("Failed to create ZIP");
600
601 let read_config = ZipReadConfig::default();
602 let entries = zip_extract_all(&zip_data, &read_config).expect("Failed to extract");
603
604 assert_eq!(entries.len(), 2);
605 assert!(entries.iter().any(|e| e.path == "hello.txt"));
606 assert!(entries.iter().any(|e| e.path == "sub/nested.txt"));
607 }
608
609 #[cfg(feature = "zip")]
610 #[test]
611 fn test_zip_file_manipulation() {
612 let mut zip = ZipFile::new();
613
614 zip.add_file("a.txt", b"AAA".to_vec());
615 zip.add_file("b.txt", b"BBB".to_vec());
616
617 assert_eq!(zip.entries.len(), 2);
618 assert!(zip.contains("a.txt"));
619 assert!(zip.contains("b.txt"));
620
621 zip.remove("a.txt");
622 assert_eq!(zip.entries.len(), 1);
623 assert!(!zip.contains("a.txt"));
624
625 zip.add_file("b.txt", b"NEW".to_vec());
627 assert_eq!(zip.entries.len(), 1);
628 assert_eq!(zip.get("b.txt").unwrap().data, b"NEW");
629 }
630}