use alloc::collections::BTreeMap;
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
use super::types::{ArchiveEntry, ArchiveError, CompressionMethod, ExtractResult};
const ZIP_LOCAL_SIGNATURE: u32 = 0x04034b50;
const ZIP_CD_SIGNATURE: u32 = 0x02014b50;
const ZIP_EOCD_SIGNATURE: u32 = 0x06054b50;
const ZIP_METHOD_STORE: u16 = 0;
const ZIP_METHOD_DEFLATE: u16 = 8;
const ZIP_METHOD_LZ4: u16 = 99;
#[derive(Debug, Clone)]
struct ZipLocalHeader {
version_needed: u16,
flags: u16,
compression: u16,
mod_time: u16,
mod_date: u16,
crc32: u32,
compressed_size: u32,
uncompressed_size: u32,
filename: String,
extra: Vec<u8>,
}
impl ZipLocalHeader {
fn to_bytes(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(30 + self.filename.len() + self.extra.len());
buf.extend_from_slice(&ZIP_LOCAL_SIGNATURE.to_le_bytes());
buf.extend_from_slice(&self.version_needed.to_le_bytes());
buf.extend_from_slice(&self.flags.to_le_bytes());
buf.extend_from_slice(&self.compression.to_le_bytes());
buf.extend_from_slice(&self.mod_time.to_le_bytes());
buf.extend_from_slice(&self.mod_date.to_le_bytes());
buf.extend_from_slice(&self.crc32.to_le_bytes());
buf.extend_from_slice(&self.compressed_size.to_le_bytes());
buf.extend_from_slice(&self.uncompressed_size.to_le_bytes());
buf.extend_from_slice(&(self.filename.len() as u16).to_le_bytes());
buf.extend_from_slice(&(self.extra.len() as u16).to_le_bytes());
buf.extend_from_slice(self.filename.as_bytes());
buf.extend_from_slice(&self.extra);
buf
}
fn from_bytes(data: &[u8]) -> Result<(Self, usize), ArchiveError> {
if data.len() < 30 {
return Err(ArchiveError::InvalidFormat);
}
let sig = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if sig != ZIP_LOCAL_SIGNATURE {
return Err(ArchiveError::InvalidFormat);
}
let version_needed = u16::from_le_bytes([data[4], data[5]]);
let flags = u16::from_le_bytes([data[6], data[7]]);
let compression = u16::from_le_bytes([data[8], data[9]]);
let mod_time = u16::from_le_bytes([data[10], data[11]]);
let mod_date = u16::from_le_bytes([data[12], data[13]]);
let crc32 = u32::from_le_bytes([data[14], data[15], data[16], data[17]]);
let compressed_size = u32::from_le_bytes([data[18], data[19], data[20], data[21]]);
let uncompressed_size = u32::from_le_bytes([data[22], data[23], data[24], data[25]]);
let filename_len = u16::from_le_bytes([data[26], data[27]]) as usize;
let extra_len = u16::from_le_bytes([data[28], data[29]]) as usize;
if data.len() < 30 + filename_len + extra_len {
return Err(ArchiveError::InvalidFormat);
}
let filename = String::from_utf8_lossy(&data[30..30 + filename_len]).into_owned();
let extra = data[30 + filename_len..30 + filename_len + extra_len].to_vec();
let header_size = 30 + filename_len + extra_len;
Ok((
Self {
version_needed,
flags,
compression,
mod_time,
mod_date,
crc32,
compressed_size,
uncompressed_size,
filename,
extra,
},
header_size,
))
}
}
#[derive(Debug, Clone)]
struct ZipCentralHeader {
version_made: u16,
version_needed: u16,
flags: u16,
compression: u16,
mod_time: u16,
mod_date: u16,
crc32: u32,
compressed_size: u32,
uncompressed_size: u32,
disk_start: u16,
internal_attr: u16,
external_attr: u32,
local_offset: u32,
filename: String,
extra: Vec<u8>,
comment: String,
}
impl ZipCentralHeader {
fn to_bytes(&self) -> Vec<u8> {
let mut buf =
Vec::with_capacity(46 + self.filename.len() + self.extra.len() + self.comment.len());
buf.extend_from_slice(&ZIP_CD_SIGNATURE.to_le_bytes());
buf.extend_from_slice(&self.version_made.to_le_bytes());
buf.extend_from_slice(&self.version_needed.to_le_bytes());
buf.extend_from_slice(&self.flags.to_le_bytes());
buf.extend_from_slice(&self.compression.to_le_bytes());
buf.extend_from_slice(&self.mod_time.to_le_bytes());
buf.extend_from_slice(&self.mod_date.to_le_bytes());
buf.extend_from_slice(&self.crc32.to_le_bytes());
buf.extend_from_slice(&self.compressed_size.to_le_bytes());
buf.extend_from_slice(&self.uncompressed_size.to_le_bytes());
buf.extend_from_slice(&(self.filename.len() as u16).to_le_bytes());
buf.extend_from_slice(&(self.extra.len() as u16).to_le_bytes());
buf.extend_from_slice(&(self.comment.len() as u16).to_le_bytes());
buf.extend_from_slice(&self.disk_start.to_le_bytes());
buf.extend_from_slice(&self.internal_attr.to_le_bytes());
buf.extend_from_slice(&self.external_attr.to_le_bytes());
buf.extend_from_slice(&self.local_offset.to_le_bytes());
buf.extend_from_slice(self.filename.as_bytes());
buf.extend_from_slice(&self.extra);
buf.extend_from_slice(self.comment.as_bytes());
buf
}
fn from_bytes(data: &[u8]) -> Result<(Self, usize), ArchiveError> {
if data.len() < 46 {
return Err(ArchiveError::InvalidFormat);
}
let sig = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if sig != ZIP_CD_SIGNATURE {
return Err(ArchiveError::InvalidFormat);
}
let version_made = u16::from_le_bytes([data[4], data[5]]);
let version_needed = u16::from_le_bytes([data[6], data[7]]);
let flags = u16::from_le_bytes([data[8], data[9]]);
let compression = u16::from_le_bytes([data[10], data[11]]);
let mod_time = u16::from_le_bytes([data[12], data[13]]);
let mod_date = u16::from_le_bytes([data[14], data[15]]);
let crc32 = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
let compressed_size = u32::from_le_bytes([data[20], data[21], data[22], data[23]]);
let uncompressed_size = u32::from_le_bytes([data[24], data[25], data[26], data[27]]);
let filename_len = u16::from_le_bytes([data[28], data[29]]) as usize;
let extra_len = u16::from_le_bytes([data[30], data[31]]) as usize;
let comment_len = u16::from_le_bytes([data[32], data[33]]) as usize;
let disk_start = u16::from_le_bytes([data[34], data[35]]);
let internal_attr = u16::from_le_bytes([data[36], data[37]]);
let external_attr = u32::from_le_bytes([data[38], data[39], data[40], data[41]]);
let local_offset = u32::from_le_bytes([data[42], data[43], data[44], data[45]]);
let total_len = 46 + filename_len + extra_len + comment_len;
if data.len() < total_len {
return Err(ArchiveError::InvalidFormat);
}
let filename = String::from_utf8_lossy(&data[46..46 + filename_len]).into_owned();
let extra = data[46 + filename_len..46 + filename_len + extra_len].to_vec();
let comment =
String::from_utf8_lossy(&data[46 + filename_len + extra_len..total_len]).into_owned();
Ok((
Self {
version_made,
version_needed,
flags,
compression,
mod_time,
mod_date,
crc32,
compressed_size,
uncompressed_size,
disk_start,
internal_attr,
external_attr,
local_offset,
filename,
extra,
comment,
},
total_len,
))
}
}
fn crc32(data: &[u8]) -> u32 {
let mut crc = 0xFFFFFFFFu32;
for &byte in data {
let index = ((crc ^ byte as u32) & 0xFF) as usize;
crc = CRC32_TABLE[index] ^ (crc >> 8);
}
!crc
}
static CRC32_TABLE: [u32; 256] = [
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD706B3, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
];
fn compress_lz4(data: &[u8]) -> Vec<u8> {
lz4_flex::compress_prepend_size(data)
}
fn decompress_lz4(data: &[u8]) -> Result<Vec<u8>, ArchiveError> {
lz4_flex::decompress_size_prepended(data).map_err(|_| ArchiveError::DecompressError)
}
pub fn create_zip(files: &[(&str, &[u8])]) -> Result<Vec<u8>, ArchiveError> {
let mut archive = Vec::new();
let mut central_directory = Vec::new();
let mut cd_entries: u16 = 0;
for (name, data) in files {
let local_offset = archive.len() as u32;
let compressed = compress_lz4(data);
let use_compression = compressed.len() < data.len();
let (stored_data, compression_method) = if use_compression {
(compressed.as_slice(), ZIP_METHOD_LZ4)
} else {
(*data, ZIP_METHOD_STORE)
};
let checksum = crc32(data);
let extra = if use_compression {
vec![0x4C, 0x5A, 0x34, 0x00] } else {
Vec::new()
};
let local_header = ZipLocalHeader {
version_needed: 20,
flags: 0,
compression: compression_method,
mod_time: 0,
mod_date: 0x5421, crc32: checksum,
compressed_size: stored_data.len() as u32,
uncompressed_size: data.len() as u32,
filename: (*name).to_string(),
extra: extra.clone(),
};
archive.extend_from_slice(&local_header.to_bytes());
archive.extend_from_slice(stored_data);
let cd_header = ZipCentralHeader {
version_made: 20,
version_needed: 20,
flags: 0,
compression: compression_method,
mod_time: 0,
mod_date: 0x5421,
crc32: checksum,
compressed_size: stored_data.len() as u32,
uncompressed_size: data.len() as u32,
disk_start: 0,
internal_attr: 0,
external_attr: 0o644 << 16, local_offset,
filename: (*name).to_string(),
extra,
comment: String::new(),
};
central_directory.extend_from_slice(&cd_header.to_bytes());
cd_entries += 1;
}
let cd_offset = archive.len() as u32;
let cd_size = central_directory.len() as u32;
archive.extend_from_slice(¢ral_directory);
archive.extend_from_slice(&ZIP_EOCD_SIGNATURE.to_le_bytes());
archive.extend_from_slice(&0u16.to_le_bytes()); archive.extend_from_slice(&0u16.to_le_bytes()); archive.extend_from_slice(&cd_entries.to_le_bytes()); archive.extend_from_slice(&cd_entries.to_le_bytes()); archive.extend_from_slice(&cd_size.to_le_bytes()); archive.extend_from_slice(&cd_offset.to_le_bytes()); archive.extend_from_slice(&0u16.to_le_bytes());
Ok(archive)
}
pub fn parse_zip(data: &[u8]) -> Result<BTreeMap<String, ArchiveEntry>, ArchiveError> {
let mut entries = BTreeMap::new();
let eocd_pos = find_eocd(data).ok_or(ArchiveError::InvalidFormat)?;
if data.len() < eocd_pos + 22 {
return Err(ArchiveError::InvalidFormat);
}
let cd_entries = u16::from_le_bytes([data[eocd_pos + 10], data[eocd_pos + 11]]) as usize;
let cd_offset = u32::from_le_bytes([
data[eocd_pos + 16],
data[eocd_pos + 17],
data[eocd_pos + 18],
data[eocd_pos + 19],
]) as usize;
let mut pos = cd_offset;
for _ in 0..cd_entries {
if pos >= data.len() {
break;
}
let (cd_header, header_size) = ZipCentralHeader::from_bytes(&data[pos..])?;
let compression = match cd_header.compression {
ZIP_METHOD_STORE => CompressionMethod::Store,
ZIP_METHOD_DEFLATE => CompressionMethod::Deflate,
ZIP_METHOD_LZ4 => CompressionMethod::Lzma, _ => CompressionMethod::Unknown,
};
let entry = ArchiveEntry {
name: cd_header.filename.clone(),
is_dir: cd_header.filename.ends_with('/'),
size: cd_header.uncompressed_size as u64,
compressed_size: cd_header.compressed_size as u64,
mtime: dos_to_unix_time(cd_header.mod_time, cd_header.mod_date),
mode: cd_header.external_attr >> 16,
offset: cd_header.local_offset as u64,
compression,
crc32: cd_header.crc32,
};
entries.insert(cd_header.filename, entry);
pos += header_size;
}
Ok(entries)
}
pub fn parse_zip_directory(path: &str) -> Result<BTreeMap<String, ArchiveEntry>, ArchiveError> {
let _ = path;
Ok(BTreeMap::new())
}
pub fn extract_file(archive_data: &[u8], entry: &ArchiveEntry) -> Result<Vec<u8>, ArchiveError> {
let offset = entry.offset as usize;
let (local_header, header_size) = ZipLocalHeader::from_bytes(&archive_data[offset..])?;
let data_start = offset + header_size;
let data_end = data_start + local_header.compressed_size as usize;
if data_end > archive_data.len() {
return Err(ArchiveError::InvalidFormat);
}
let compressed_data = &archive_data[data_start..data_end];
let decompressed = match local_header.compression {
ZIP_METHOD_STORE => compressed_data.to_vec(),
ZIP_METHOD_LZ4 => decompress_lz4(compressed_data)?,
_ => {
return Err(ArchiveError::UnsupportedType(
"Unknown compression".to_string(),
));
}
};
let computed_crc = crc32(&decompressed);
if computed_crc != local_header.crc32 {
return Err(ArchiveError::ChecksumMismatch);
}
Ok(decompressed)
}
pub fn extract_all(archive_data: &[u8]) -> Result<Vec<(String, Vec<u8>)>, ArchiveError> {
let entries = parse_zip(archive_data)?;
let mut files = Vec::new();
for (name, entry) in entries {
if !entry.is_dir {
let data = extract_file(archive_data, &entry)?;
files.push((name, data));
}
}
Ok(files)
}
fn find_eocd(data: &[u8]) -> Option<usize> {
let min_pos = data.len().saturating_sub(22 + 65535);
let start = if min_pos > 0 { min_pos } else { 0 };
for i in (start..=data.len().saturating_sub(22)).rev() {
if data.len() >= i + 4 && data[i..i + 4] == [0x50, 0x4B, 0x05, 0x06] {
return Some(i);
}
}
None
}
fn dos_to_unix_time(time: u16, date: u16) -> u64 {
let second = (time & 0x1F) * 2;
let minute = (time >> 5) & 0x3F;
let hour = (time >> 11) & 0x1F;
let day = date & 0x1F;
let month = (date >> 5) & 0x0F;
let year = ((date >> 9) & 0x7F) + 1980;
let days_since_epoch = days_from_ymd(year as i32, month as u32, day as u32);
let seconds = (hour as u64) * 3600 + (minute as u64) * 60 + (second as u64);
(days_since_epoch as u64) * 86400 + seconds
}
fn days_from_ymd(year: i32, month: u32, day: u32) -> i32 {
let y = year - (month <= 2) as i32;
let era = if y >= 0 { y } else { y - 399 } / 400;
let yoe = (y - era * 400) as u32;
let doy = (153 * (if month > 2 { month - 3 } else { month + 9 }) + 2) / 5 + day - 1;
let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
era * 146097 + doe as i32 - 719468
}
pub fn write_to_zip(
archive_path: &str,
inner_path: &str,
data: &[u8],
) -> Result<Vec<u8>, ArchiveError> {
let _ = archive_path; create_zip(&[(inner_path, data)])
}
pub fn add_to_archive(
archive_path: &str,
files: &[(&str, &[u8])],
) -> Result<Vec<u8>, ArchiveError> {
let _ = archive_path; create_zip(files)
}
pub fn extract_archive(
archive_data: &[u8],
target_dir: &str,
) -> Result<ExtractResult, ArchiveError> {
let _ = target_dir; let files = extract_all(archive_data)?;
let mut bytes_extracted = 0u64;
let mut directories_created = 0u64;
for (name, data) in &files {
bytes_extracted += data.len() as u64;
if name.ends_with('/') {
directories_created += 1;
}
}
Ok(ExtractResult {
files_extracted: files.len() as u64,
directories_created,
bytes_extracted,
})
}
pub fn extract_archive_contents(
archive_data: &[u8],
) -> Result<(ExtractResult, Vec<(String, Vec<u8>)>), ArchiveError> {
let files = extract_all(archive_data)?;
let mut bytes_extracted = 0u64;
let mut directories_created = 0u64;
for (name, data) in &files {
bytes_extracted += data.len() as u64;
if name.ends_with('/') {
directories_created += 1;
}
}
let result = ExtractResult {
files_extracted: files.len() as u64,
directories_created,
bytes_extracted,
};
Ok((result, files))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_crc32() {
let data = b"123456789";
assert_eq!(crc32(data), 0xCBF43926);
}
#[test]
fn test_lz4_roundtrip() {
let original = b"Hello, World! This is some test data that should compress well when repeated. Hello, World! This is some test data that should compress well when repeated.";
let compressed = compress_lz4(original);
let decompressed = decompress_lz4(&compressed).unwrap();
assert_eq!(original.as_slice(), decompressed.as_slice());
}
#[test]
fn test_create_and_parse_zip() {
let files = [
("hello.txt", b"Hello, World!".as_slice()),
("data.bin", &[0u8, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
];
let archive = create_zip(&files).unwrap();
let entries = parse_zip(&archive).unwrap();
assert_eq!(entries.len(), 2);
assert!(entries.contains_key("hello.txt"));
assert!(entries.contains_key("data.bin"));
let hello_entry = entries.get("hello.txt").unwrap();
assert_eq!(hello_entry.size, 13);
}
#[test]
fn test_extract_file() {
let original_data = b"This is test content for extraction!";
let files = [("test.txt", original_data.as_slice())];
let archive = create_zip(&files).unwrap();
let entries = parse_zip(&archive).unwrap();
let entry = entries.get("test.txt").unwrap();
let extracted = extract_file(&archive, entry).unwrap();
assert_eq!(extracted, original_data);
}
#[test]
fn test_compression_savings() {
let mut data = Vec::new();
for _ in 0..1000 {
data.extend_from_slice(b"AAAAAAAAAA");
}
let files = [("compressible.txt", data.as_slice())];
let archive = create_zip(&files).unwrap();
assert!(archive.len() < data.len());
let entries = parse_zip(&archive).unwrap();
let entry = entries.get("compressible.txt").unwrap();
let extracted = extract_file(&archive, entry).unwrap();
assert_eq!(extracted, data);
}
#[test]
fn test_dos_to_unix() {
let date = ((2025 - 1980) << 9) | (1 << 5) | 1;
let time = 0;
let unix = dos_to_unix_time(time, date);
assert!(unix > 0);
}
}