use std::collections::HashSet;
use std::io::{Read, Seek, SeekFrom};
use crate::date_time::DosDateTime;
use crate::error::{ArchiveError, Result};
const AES_METHOD_ID: &[u8] = &[0x06, 0xF1, 0x07, 0x01];
#[derive(Debug, Clone)]
pub struct SevenZFileHeader {
pub name: String,
pub compressed_size: u64,
pub original_size: u64,
pub compression_method: String,
pub date_time: Option<DosDateTime>,
pub crc32: u32,
pub is_directory: bool,
pub is_encrypted: bool,
pub index: usize,
}
pub struct SevenZArchive<T: Read + Seek> {
reader: T,
entries: Vec<SevenZFileHeader>,
current_index: usize,
password: Option<String>,
is_encrypted: bool,
}
impl<T: Read + Seek> SevenZArchive<T> {
pub fn new(reader: T) -> Result<Self> {
Self::new_with_password(reader, None)
}
fn get_encrypted_blocks(archive: &sevenz_rust2::Archive) -> HashSet<usize> {
let mut encrypted = HashSet::new();
for (idx, block) in archive.blocks.iter().enumerate() {
for coder in &block.coders {
if coder.encoder_method_id().starts_with(AES_METHOD_ID) {
encrypted.insert(idx);
break;
}
}
}
encrypted
}
pub fn new_with_password(mut reader: T, password: Option<String>) -> Result<Self> {
let pwd = match &password {
Some(p) => sevenz_rust2::Password::from(p.as_str()),
None => sevenz_rust2::Password::empty(),
};
let archive = sevenz_rust2::ArchiveReader::new(&mut reader, pwd).map_err(|e| ArchiveError::external_library("sevenz-rust2", e.to_string()))?;
let encrypted_blocks = Self::get_encrypted_blocks(archive.archive());
let is_encrypted = !encrypted_blocks.is_empty();
let mut entries = Vec::new();
for (index, entry) in archive.archive().files.iter().enumerate() {
let name = entry.name.clone();
let is_directory = entry.is_directory;
let original_size = entry.size;
let compressed_size = entry.compressed_size;
let crc32 = if entry.has_crc { entry.crc as u32 } else { 0 };
let date_time = if entry.has_last_modified_date {
let ts: u64 = entry.last_modified_date.into();
convert_nt_time_to_dos_datetime(ts)
} else {
None
};
let compression_method = "7z".to_string();
let file_encrypted = if is_directory {
false
} else {
archive
.archive()
.stream_map
.file_block_index
.get(index)
.and_then(|opt| *opt)
.map(|block_idx| encrypted_blocks.contains(&block_idx))
.unwrap_or(false)
};
entries.push(SevenZFileHeader {
name,
compressed_size,
original_size,
compression_method,
date_time,
crc32,
is_directory,
is_encrypted: file_encrypted,
index,
});
}
Ok(Self {
reader,
entries,
current_index: 0,
password,
is_encrypted,
})
}
pub fn is_encrypted(&self) -> bool {
self.is_encrypted
}
pub fn set_password<P: Into<String>>(&mut self, password: P) {
self.password = Some(password.into());
}
pub fn clear_password(&mut self) {
self.password = None;
}
pub fn get_next_entry(&mut self) -> Result<Option<SevenZFileHeader>> {
if self.current_index >= self.entries.len() {
return Ok(None);
}
let entry = self.entries[self.current_index].clone();
self.current_index += 1;
Ok(Some(entry))
}
pub fn skip(&mut self, _header: &SevenZFileHeader) -> Result<()> {
Ok(())
}
pub fn read(&mut self, header: &SevenZFileHeader) -> Result<Vec<u8>> {
self.read_with_password(header, self.password.clone())
}
pub fn read_with_password(&mut self, header: &SevenZFileHeader, password: Option<String>) -> Result<Vec<u8>> {
if header.is_directory {
return Ok(Vec::new());
}
let pwd = match &password {
Some(p) => sevenz_rust2::Password::from(p.as_str()),
None => sevenz_rust2::Password::empty(),
};
let mut archive = sevenz_rust2::ArchiveReader::new(&mut self.reader, pwd).map_err(|e| ArchiveError::external_library("sevenz-rust2", e.to_string()))?;
archive
.read_file(&header.name)
.map_err(|e| ArchiveError::decompression_failed(&header.name, e.to_string()))
}
}
fn convert_nt_time_to_dos_datetime(ts: u64) -> Option<DosDateTime> {
let nanos_per_sec = 10_000_000u64;
let secs_since_1601 = ts / nanos_per_sec;
let secs_1601_to_1970 = 11_644_473_600u64;
if secs_since_1601 >= secs_1601_to_1970 {
let unix_secs = secs_since_1601 - secs_1601_to_1970;
let days_since_epoch = unix_secs / 86400;
let time_of_day = unix_secs % 86400;
let hour = (time_of_day / 3600) as u32;
let minute = ((time_of_day % 3600) / 60) as u32;
let second = (time_of_day % 60) as u32;
let mut days = days_since_epoch as i64;
let mut year = 1970i32;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if days < days_in_year {
break;
}
days -= days_in_year;
year += 1;
}
let (month, day) = day_of_year_to_month_day(days as u32, is_leap_year(year));
if (1980..=2107).contains(&year) {
let dos_year = ((year - 1980) as u32) & 0x7F;
let dos_date = (dos_year << 9) | ((month as u32) << 5) | (day as u32);
let dos_time = (hour << 11) | (minute << 5) | (second / 2);
Some(DosDateTime::new((dos_date << 16) | dos_time))
} else {
None
}
} else {
None
}
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn day_of_year_to_month_day(day_of_year: u32, is_leap: bool) -> (u8, u8) {
let days_in_months: [u32; 12] = if is_leap {
[31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
} else {
[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
};
let mut remaining = day_of_year;
for (month, &days) in days_in_months.iter().enumerate() {
if remaining < days {
return ((month + 1) as u8, (remaining + 1) as u8);
}
remaining -= days;
}
(12, 31)
}
impl<T: Read + Seek> SevenZArchive<T> {
pub fn create_password_verifier(&mut self, header: &SevenZFileHeader) -> Result<super::password_verifier::SevenZPasswordVerifier> {
if !header.is_encrypted {
return Err(ArchiveError::unsupported_method("7z", "entry is not encrypted"));
}
self.reader.seek(SeekFrom::Start(0))?;
let mut archive_data = Vec::new();
self.reader.read_to_end(&mut archive_data)?;
Ok(super::password_verifier::SevenZPasswordVerifier::new(
archive_data,
header.name.clone(),
header.crc32,
header.original_size,
))
}
}