pub mod error;
pub mod format;
pub mod reader;
pub mod writer;
#[cfg(feature = "encryption")]
pub mod encryption;
#[cfg(feature = "encryption")]
pub mod decrypt_reader;
#[cfg(feature = "async")]
pub mod async_writer;
#[cfg(feature = "async")]
pub mod async_reader;
#[cfg(feature = "async")]
pub mod parallel;
#[cfg(any(feature = "cloud-s3", feature = "cloud-gcs"))]
pub mod cloud;
pub use error::{Result, SZipError};
pub use format::ZipEntry;
pub use reader::StreamingZipReader;
pub use writer::{CompressionMethod, StreamingZipWriter};
#[derive(Debug, Clone, Default)]
pub struct EntryOptions {
pub mtime: Option<std::time::SystemTime>,
pub unix_mode: Option<u32>,
}
impl EntryOptions {
pub(crate) fn msdos_datetime(&self) -> (u16, u16) {
use std::time::{Duration, UNIX_EPOCH};
let Some(mtime) = self.mtime else {
return (0, 0);
};
let secs = mtime
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
let secs_per_day = 86400u64;
let days_since_epoch = secs / secs_per_day;
let time_of_day = secs % secs_per_day;
let hour = (time_of_day / 3600) as u16;
let minute = ((time_of_day % 3600) / 60) as u16;
let second = (time_of_day % 60) as u16;
let z = days_since_epoch as i64 + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = (z - era * 146_097) as u32;
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let day = doy - (153 * mp + 2) / 5 + 1;
let month = if mp < 10 { mp + 3 } else { mp - 9 };
let year = if month <= 2 { y + 1 } else { y };
let dos_year = (year.clamp(1980, 2107) - 1980) as u16;
let dos_time = (hour << 11) | (minute << 5) | (second / 2);
let dos_date = (dos_year << 9) | ((month as u16) << 5) | (day as u16);
(dos_time, dos_date)
}
pub(crate) fn unix_extra_field(&self) -> Vec<u8> {
let Some(mode) = self.unix_mode else {
return Vec::new();
};
let _ = mode;
let mut field = Vec::with_capacity(15);
field.extend_from_slice(&0x7875u16.to_le_bytes()); field.extend_from_slice(&11u16.to_le_bytes()); field.push(1); field.push(4); field.extend_from_slice(&0u32.to_le_bytes()); field.push(4); field.extend_from_slice(&0u32.to_le_bytes()); field
}
#[allow(dead_code)]
pub(crate) fn external_attrs(&self) -> u32 {
self.unix_mode.map(|m| m << 16).unwrap_or(0)
}
}
#[cfg(feature = "async")]
pub use async_writer::AsyncStreamingZipWriter;
#[cfg(feature = "encryption")]
pub use encryption::AesStrength;
#[cfg(feature = "async")]
pub use async_reader::{AsyncStreamingZipReader, GenericAsyncZipReader};
#[cfg(feature = "async")]
pub use parallel::{ParallelConfig, ParallelEntry};
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_basic_write_read_roundtrip() {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut writer = StreamingZipWriter::from_writer(cursor).unwrap();
writer.start_entry("test1.txt").unwrap();
writer.write_data(b"Hello, World!").unwrap();
writer.start_entry("test2.txt").unwrap();
writer.write_data(b"Testing s-zip library").unwrap();
let cursor = writer.finish().unwrap();
let zip_bytes = cursor.into_inner();
assert!(!zip_bytes.is_empty(), "ZIP should not be empty");
assert_eq!(
&zip_bytes[0..4],
b"PK\x03\x04",
"Should start with ZIP signature"
);
}
#[test]
fn test_compression_method_to_zip_method() {
assert_eq!(CompressionMethod::Stored.to_zip_method(), 0);
assert_eq!(CompressionMethod::Deflate.to_zip_method(), 8);
#[cfg(feature = "zstd-support")]
assert_eq!(CompressionMethod::Zstd.to_zip_method(), 93);
}
#[test]
fn test_empty_entry_name() {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut writer = StreamingZipWriter::from_writer(cursor).unwrap();
assert!(writer.start_entry("").is_ok());
}
#[test]
fn test_multiple_small_entries() {
let buffer = Vec::new();
let cursor = Cursor::new(buffer);
let mut writer = StreamingZipWriter::from_writer(cursor).unwrap();
for i in 0..10 {
let entry_name = format!("file_{}.txt", i);
let entry_data = format!("Content of file {}", i);
writer.start_entry(&entry_name).unwrap();
writer.write_data(entry_data.as_bytes()).unwrap();
}
let cursor = writer.finish().unwrap();
let zip_bytes = cursor.into_inner();
assert!(zip_bytes.len() > 100, "ZIP with 10 files should be larger");
}
#[test]
fn test_error_display() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let err = SZipError::from(io_err);
assert!(format!("{}", err).contains("I/O error"));
let invalid_err = SZipError::InvalidFormat("bad format".to_string());
assert!(format!("{}", invalid_err).contains("Invalid ZIP format"));
let not_found_err = SZipError::EntryNotFound("missing.txt".to_string());
assert!(format!("{}", not_found_err).contains("Entry not found"));
}
#[cfg(feature = "encryption")]
#[test]
fn test_aes_strength() {
assert_eq!(AesStrength::Aes256.salt_size(), 16);
assert_eq!(AesStrength::Aes256.key_size(), 32);
assert_eq!(AesStrength::Aes256.to_winzip_code(), 0x03);
}
}