use std::io::{Read, Seek, SeekFrom};
use std::path::PathBuf;
use crate::date_time::DosDateTime;
use crate::error::{ArchiveError, Result};
#[derive(Debug, Clone)]
pub struct RarFileHeader {
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 struct RarArchive<T: Read + Seek> {
#[allow(dead_code)]
reader: T,
entries: Vec<RarFileHeader>,
current_index: usize,
archive_path: PathBuf,
is_temp_file: bool,
password: Option<String>,
}
impl<T: Read + Seek> RarArchive<T> {
pub fn new(mut reader: T) -> Result<Self> {
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir.join(format!("unarc_rar_{}.rar", std::process::id()));
reader.seek(SeekFrom::Start(0))?;
let mut data = Vec::new();
reader.read_to_end(&mut data)?;
std::fs::write(&temp_path, &data)?;
reader.seek(SeekFrom::Start(0))?;
let entries = Self::parse_entries_from_path(&temp_path)?;
Ok(Self {
reader,
entries,
current_index: 0,
archive_path: temp_path,
is_temp_file: true,
password: None,
})
}
pub fn from_path(path: &std::path::Path) -> Result<Self>
where
T: Default,
{
let entries = Self::parse_entries_from_path(path)?;
Ok(Self {
reader: T::default(),
entries,
current_index: 0,
archive_path: path.to_path_buf(),
is_temp_file: false,
password: None,
})
}
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;
}
fn parse_entries_from_path(path: &std::path::Path) -> Result<Vec<RarFileHeader>> {
let archive = unrar::Archive::new(path)
.open_for_listing()
.map_err(|e| ArchiveError::external_library("unrar", format!("Failed to open RAR archive: {:?}", e)))?;
let mut entries = Vec::new();
for entry_result in archive {
match entry_result {
Ok(entry) => {
let date_time = {
let ft = entry.file_time;
Some(DosDateTime::new(ft))
};
entries.push(RarFileHeader {
name: entry.filename.to_string_lossy().to_string(),
compressed_size: entry.unpacked_size, original_size: entry.unpacked_size,
compression_method: if entry.is_file() { "Compressed".to_string() } else { "Directory".to_string() },
date_time,
crc32: entry.file_crc,
is_directory: entry.is_directory(),
is_encrypted: entry.is_encrypted(),
});
}
Err(e) => {
return Err(ArchiveError::external_library("unrar", format!("Failed to read RAR entry: {:?}", e)));
}
}
}
Ok(entries)
}
pub fn get_next_entry(&mut self) -> Result<Option<RarFileHeader>> {
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: &RarFileHeader) -> Result<()> {
Ok(())
}
pub fn read(&mut self, header: &RarFileHeader) -> Result<Vec<u8>> {
self.read_with_password(header, self.password.clone())
}
pub fn read_with_password(&mut self, header: &RarFileHeader, password: Option<String>) -> Result<Vec<u8>> {
if header.is_directory {
return Ok(Vec::new());
}
if header.is_encrypted && password.is_none() {
return Err(ArchiveError::encryption_required(&header.name, "RAR"));
}
let temp_dir = std::env::temp_dir().join(format!("unarc_rar_extract_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir)?;
let archive = if let Some(ref pwd) = password {
unrar::Archive::with_password(&self.archive_path, pwd)
.open_for_processing()
.map_err(|e| ArchiveError::external_library("unrar", format!("Failed to open RAR for extraction: {:?}", e)))?
} else {
unrar::Archive::new(&self.archive_path)
.open_for_processing()
.map_err(|e| ArchiveError::external_library("unrar", format!("Failed to open RAR for extraction: {:?}", e)))?
};
let mut result_data = None;
let mut current = archive;
loop {
match current.read_header() {
Ok(Some(header_cursor)) => {
let entry_name = header_cursor.entry().filename.to_string_lossy().to_string();
if entry_name == header.name {
let (data, _next) = header_cursor
.read()
.map_err(|e| ArchiveError::decompression_failed(&header.name, format!("Failed to extract file: {:?}", e)))?;
result_data = Some(data);
break;
} else {
current = header_cursor
.skip()
.map_err(|e| ArchiveError::external_library("unrar", format!("Failed to skip file: {:?}", e)))?;
}
}
Ok(None) => break,
Err(e) => {
let _ = std::fs::remove_dir_all(&temp_dir);
return Err(ArchiveError::external_library("unrar", format!("Failed to read RAR header: {:?}", e)));
}
}
}
let _ = std::fs::remove_dir_all(&temp_dir);
result_data.ok_or_else(|| ArchiveError::corrupted_entry_named("RAR", &header.name, "File not found in archive"))
}
}
impl<T: Read + Seek> Drop for RarArchive<T> {
fn drop(&mut self) {
if self.is_temp_file {
let _ = std::fs::remove_file(&self.archive_path);
}
}
}
impl<T: Read + Seek> RarArchive<T> {
pub fn create_password_verifier(&self, header: &RarFileHeader) -> Result<super::password_verifier::RarPasswordVerifier> {
if !header.is_encrypted {
return Err(ArchiveError::unsupported_method("RAR", "entry is not encrypted"));
}
Ok(super::password_verifier::RarPasswordVerifier::new(
self.archive_path.clone(),
header.name.clone(),
header.crc32,
header.original_size,
))
}
}