use std::io::{Read, Seek, SeekFrom};
use crate::RAR5_MAGIC;
use crate::crypto::{Rar5EncryptionHeader, decrypt_rar5_headers, derive_rar5_key};
use crate::error::{RarError, Result};
use crate::header::{
ArchiveHeader, EndArchiveHeader, FileHeader, RarEncryption, RarHeader, ServiceHeader,
};
fn read_vint<R: Read>(reader: &mut R) -> Result<u64> {
let mut result: u64 = 0;
let mut shift = 0u32;
loop {
let mut byte = [0u8; 1];
reader.read_exact(&mut byte)?;
let b = byte[0];
result |= u64::from(b & 0x7F) << shift;
if b & 0x80 == 0 {
break;
}
shift += 7;
if shift > 63 {
return Err(RarError::TruncatedHeader(0));
}
}
Ok(result)
}
fn read_u32_le<R: Read>(reader: &mut R) -> Result<u32> {
let mut buf = [0u8; 4];
reader.read_exact(&mut buf)?;
Ok(u32::from_le_bytes(buf))
}
pub fn parse_rar5<R: Read + Seek>(
reader: &mut R,
password: Option<&str>,
) -> Result<Vec<RarHeader>> {
let mut magic = [0u8; 8];
reader.read_exact(&mut magic)?;
if magic != RAR5_MAGIC {
return Err(RarError::SignatureNotFound);
}
let mut headers = Vec::new();
loop {
let header_start = reader.stream_position()?;
let expected_crc = match read_u32_le(reader) {
Ok(v) => v,
Err(RarError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
};
let crc_data_start = reader.stream_position()?;
let header_size = read_vint(reader)?;
let header_size_vint_len = reader.stream_position()? - crc_data_start;
let total_header_bytes = header_size_vint_len + header_size;
let header_type = read_vint(reader)?;
let header_flags = read_vint(reader)?;
let extra_area_size = if header_flags & 0x0001 != 0 {
read_vint(reader)?
} else {
0
};
let data_area_size = if header_flags & 0x0002 != 0 {
read_vint(reader)?
} else {
0
};
let current_pos = reader.stream_position()?;
let consumed_from_crc_start = current_pos - crc_data_start;
let remaining_header = total_header_bytes.saturating_sub(consumed_from_crc_start);
let mut remaining_buf = vec![0u8; remaining_header as usize];
reader.read_exact(&mut remaining_buf)?;
{
let end_pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(crc_data_start))?;
let mut crc_buf = vec![0u8; total_header_bytes as usize];
reader.read_exact(&mut crc_buf)?;
let actual_crc = crc32fast::hash(&crc_buf);
if actual_crc != expected_crc {
return Err(RarError::InvalidHeaderCrc(header_start));
}
reader.seek(SeekFrom::Start(end_pos))?;
}
let mut cursor = std::io::Cursor::new(&remaining_buf);
match header_type {
4 => {
let enc_header = parse_encryption_header(&mut cursor)?;
let password = password.ok_or_else(|| {
tracing::warn!(
"RAR5 archive has encrypted headers — cannot parse without password"
);
RarError::EncryptedHeaders
})?;
tracing::debug!(
lg2_count = enc_header.lg2_count,
has_psw_check = enc_header.has_psw_check,
"decrypting RAR5 encrypted headers"
);
let derived = derive_rar5_key(password, &enc_header)?;
let mut encrypted_data = Vec::new();
reader.read_to_end(&mut encrypted_data)?;
if encrypted_data.len() < 16 {
return Err(RarError::DecryptionError(
"encrypted header data too short (need at least 16 bytes for IV)"
.to_string(),
));
}
let mut iv = [0u8; 16];
iv.copy_from_slice(&encrypted_data[..16]);
let ciphertext = &encrypted_data[16..];
let decrypted = decrypt_rar5_headers(ciphertext, &derived.key, &iv)?;
let mut dec_cursor = std::io::Cursor::new(decrypted);
let decrypted_headers = parse_rar5_headers_from_data(&mut dec_cursor)?;
headers.extend(decrypted_headers);
break;
}
1 => {
let archive_flags = read_vint(&mut cursor)?;
let is_volume = archive_flags & 0x0001 != 0;
let has_volume_number = archive_flags & 0x0002 != 0;
let volume_number = if has_volume_number {
Some(read_vint(&mut cursor)? as i32)
} else {
None
};
headers.push(RarHeader::Archive(ArchiveHeader {
is_first_volume: !is_volume || volume_number == Some(0),
volume_number,
}));
}
2 => {
let fh = parse_rar5_file_header(
&mut cursor,
extra_area_size,
data_area_size,
remaining_header,
reader,
)?;
headers.push(RarHeader::File(fh));
}
3 => {
let fh = parse_rar5_file_header(
&mut cursor,
extra_area_size,
data_area_size,
remaining_header,
reader,
)?;
headers.push(RarHeader::Service(ServiceHeader {
name: fh.filename,
data_size: fh.data_size,
}));
}
5 => {
let end_flags = read_vint(&mut cursor)?;
let has_next_volume = end_flags & 0x0001 != 0;
headers.push(RarHeader::EndArchive(EndArchiveHeader {
volume_number: if has_next_volume { Some(1) } else { None },
}));
break;
}
_ => {
tracing::warn!(
"unknown RAR5 header type {} at offset {}",
header_type,
header_start
);
}
}
if data_area_size > 0 {
let data_start = crc_data_start + total_header_bytes;
let target = data_start + data_area_size;
if reader.seek(SeekFrom::Start(target)).is_err() {
break; }
}
}
Ok(headers)
}
fn parse_encryption_header<R: Read>(reader: &mut R) -> Result<Rar5EncryptionHeader> {
let version = read_vint(reader)?;
if version != 0 {
return Err(RarError::UnsupportedEncryptionVersion(version));
}
let enc_flags = read_vint(reader)?;
let has_psw_check = enc_flags & 0x0001 != 0;
let mut lg2_count_buf = [0u8; 1];
reader.read_exact(&mut lg2_count_buf)?;
let lg2_count = lg2_count_buf[0];
let mut salt = vec![0u8; 16];
reader.read_exact(&mut salt)?;
let (psw_check, psw_check_sum) = if has_psw_check {
let mut check = vec![0u8; 8];
reader.read_exact(&mut check)?;
let mut sum = vec![0u8; 4];
reader.read_exact(&mut sum)?;
(check, sum)
} else {
(Vec::new(), Vec::new())
};
Ok(Rar5EncryptionHeader {
lg2_count,
salt,
has_psw_check,
psw_check,
psw_check_sum,
})
}
fn parse_rar5_headers_from_data<R: Read + Seek>(reader: &mut R) -> Result<Vec<RarHeader>> {
let mut headers = Vec::new();
loop {
let header_start = reader.stream_position()?;
let expected_crc = match read_u32_le(reader) {
Ok(v) => v,
Err(RarError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
};
let crc_data_start = reader.stream_position()?;
let header_size = match read_vint(reader) {
Ok(v) => v,
Err(RarError::Io(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e),
};
let header_size_vint_len = reader.stream_position()? - crc_data_start;
let total_header_bytes = header_size_vint_len + header_size;
let header_type = read_vint(reader)?;
let header_flags = read_vint(reader)?;
let extra_area_size = if header_flags & 0x0001 != 0 {
read_vint(reader)?
} else {
0
};
let data_area_size = if header_flags & 0x0002 != 0 {
read_vint(reader)?
} else {
0
};
let current_pos = reader.stream_position()?;
let consumed_from_crc_start = current_pos - crc_data_start;
let remaining_header = total_header_bytes.saturating_sub(consumed_from_crc_start);
let mut remaining_buf = vec![0u8; remaining_header as usize];
reader.read_exact(&mut remaining_buf)?;
{
let end_pos = reader.stream_position()?;
reader.seek(SeekFrom::Start(crc_data_start))?;
let mut crc_buf = vec![0u8; total_header_bytes as usize];
reader.read_exact(&mut crc_buf)?;
let actual_crc = crc32fast::hash(&crc_buf);
if actual_crc != expected_crc {
tracing::debug!(
"CRC mismatch in decrypted block at offset {} — likely padding, stopping",
header_start
);
break;
}
reader.seek(SeekFrom::Start(end_pos))?;
}
let mut cursor = std::io::Cursor::new(&remaining_buf);
match header_type {
1 => {
let archive_flags = read_vint(&mut cursor)?;
let is_volume = archive_flags & 0x0001 != 0;
let has_volume_number = archive_flags & 0x0002 != 0;
let volume_number = if has_volume_number {
Some(read_vint(&mut cursor)? as i32)
} else {
None
};
headers.push(RarHeader::Archive(ArchiveHeader {
is_first_volume: !is_volume || volume_number == Some(0),
volume_number,
}));
}
2 => {
let fh = parse_rar5_file_header(
&mut cursor,
extra_area_size,
data_area_size,
remaining_header,
reader,
)?;
headers.push(RarHeader::File(fh));
}
3 => {
let fh = parse_rar5_file_header(
&mut cursor,
extra_area_size,
data_area_size,
remaining_header,
reader,
)?;
headers.push(RarHeader::Service(ServiceHeader {
name: fh.filename,
data_size: fh.data_size,
}));
}
5 => {
let end_flags = read_vint(&mut cursor)?;
let has_next_volume = end_flags & 0x0001 != 0;
headers.push(RarHeader::EndArchive(EndArchiveHeader {
volume_number: if has_next_volume { Some(1) } else { None },
}));
break;
}
_ => {
tracing::debug!(
"unknown header type {} in decrypted block at offset {}",
header_type,
header_start
);
}
}
if data_area_size > 0 {
let data_start = crc_data_start + total_header_bytes;
let target = data_start + data_area_size;
if reader.seek(SeekFrom::Start(target)).is_err() {
break;
}
}
}
Ok(headers)
}
fn parse_rar5_file_header<R: Read + Seek>(
cursor: &mut std::io::Cursor<&Vec<u8>>,
extra_area_size: u64,
data_area_size: u64,
_remaining_header: u64,
outer_reader: &mut R,
) -> Result<FileHeader> {
let file_flags = read_vint(cursor)?;
let is_directory = file_flags & 0x0001 != 0;
let has_unix_timestamps = file_flags & 0x0002 != 0;
let has_crc32 = file_flags & 0x0004 != 0;
let has_unknown_unpacked_size = file_flags & 0x0008 != 0;
let unpacked_size = if has_unknown_unpacked_size {
0
} else {
read_vint(cursor)?
};
let _attributes = read_vint(cursor)?;
if has_unix_timestamps {
let mut _mtime_buf = [0u8; 4];
cursor.read_exact(&mut _mtime_buf)?;
}
if has_crc32 {
let mut _data_crc = [0u8; 4];
cursor.read_exact(&mut _data_crc)?;
}
let compression_info = read_vint(cursor)?;
let method = ((compression_info >> 7) & 0x0F) as u8;
let is_solid = compression_info & 0x0040 != 0;
let _host_os = read_vint(cursor)?;
let name_len = read_vint(cursor)?;
let mut name_buf = vec![0u8; name_len as usize];
cursor.read_exact(&mut name_buf)?;
let filename = String::from_utf8_lossy(&name_buf).into_owned();
let mut encryption = None;
let mut is_encrypted = false;
if extra_area_size > 0 {
let extra_start = cursor.position();
let extra_end = extra_start + extra_area_size;
while cursor.position() < extra_end {
let extra_size = match read_vint(cursor) {
Ok(v) => v,
Err(_) => break,
};
let extra_field_start = cursor.position();
let extra_type = match read_vint(cursor) {
Ok(v) => v,
Err(_) => break,
};
if extra_type == 1 {
is_encrypted = true;
let _version = read_vint(cursor)?;
let enc_flags = read_vint(cursor)?;
let has_psw_check = enc_flags & 0x0001 != 0;
let mut lg2_count = [0u8; 1];
cursor.read_exact(&mut lg2_count)?;
let mut salt = vec![0u8; 16];
cursor.read_exact(&mut salt)?;
let mut iv = vec![0u8; 16];
cursor.read_exact(&mut iv)?;
let psw_check = if has_psw_check {
let mut buf = vec![0u8; 12];
cursor.read_exact(&mut buf)?;
buf
} else {
Vec::new()
};
encryption = Some(RarEncryption::Rar5 {
lg2_count: lg2_count[0],
salt,
use_psw_check: has_psw_check,
psw_check,
iv,
});
}
cursor.set_position(extra_field_start + extra_size);
}
}
let data_start_position = outer_reader.stream_position()?;
Ok(FileHeader {
filename,
uncompressed_size: unpacked_size,
compressed_size: data_area_size,
compression_method: method,
data_start_position,
data_size: data_area_size,
is_directory,
is_encrypted,
is_solid,
volume_number: None,
encryption,
})
}
#[doc(hidden)]
pub mod tests_helper {
use crate::RAR5_MAGIC;
pub fn build_minimal_rar5_store() -> Vec<u8> {
build_rar5_with_method(0)
}
pub fn build_rar5_with_method(method: u8) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(RAR5_MAGIC);
{
let inner = vec![1u8, 0, 0]; let size_vint = vec![inner.len() as u8]; let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
}
{
let filename = b"test.txt";
let mut inner = vec![
2u8, 0x02, 5, 0x04, 5, 0, ];
let data_crc = crc32fast::hash(b"hello");
inner.extend_from_slice(&data_crc.to_le_bytes());
let compression_info: u64 = (method as u64) << 7;
let mut ci = compression_info;
loop {
let mut byte = (ci & 0x7F) as u8;
ci >>= 7;
if ci > 0 {
byte |= 0x80;
}
inner.push(byte);
if ci == 0 {
break;
}
}
inner.push(0u8); inner.push(filename.len() as u8); inner.extend_from_slice(filename);
let header_size_val = inner.len();
assert!(header_size_val < 128);
let size_vint = vec![header_size_val as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
out.extend_from_slice(b"hello");
}
{
let inner = vec![5u8, 0, 0]; let size_vint = vec![inner.len() as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn read_vint_single_byte() {
let data = [0x05u8];
let mut cursor = Cursor::new(&data[..]);
assert_eq!(read_vint(&mut cursor).unwrap(), 5);
}
#[test]
fn read_vint_multi_byte() {
let data = [0x80u8, 0x01];
let mut cursor = Cursor::new(&data[..]);
assert_eq!(read_vint(&mut cursor).unwrap(), 128);
}
#[test]
fn read_vint_value_255() {
let data = [0xFFu8, 0x01];
let mut cursor = Cursor::new(&data[..]);
assert_eq!(read_vint(&mut cursor).unwrap(), 255);
}
fn build_minimal_rar5() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(RAR5_MAGIC);
{
let inner = vec![1u8, 0, 0]; let size_vint = vec![inner.len() as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
}
{
let filename = b"test.txt";
let mut inner = vec![
2u8, 0x02, 5, 0x04, 5,
0, ];
let data_crc = crc32fast::hash(b"hello");
inner.extend_from_slice(&data_crc.to_le_bytes());
inner.push(0u8); inner.push(0u8); inner.push(filename.len() as u8);
inner.extend_from_slice(filename);
let header_size_val = inner.len();
assert!(header_size_val < 128);
let size_vint = vec![header_size_val as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
out.extend_from_slice(b"hello");
}
{
let inner = vec![5u8, 0, 0]; let size_vint = vec![inner.len() as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
}
out
}
#[test]
fn parse_minimal_rar5() {
let data = build_minimal_rar5();
let mut cursor = Cursor::new(&data[..]);
let headers = parse_rar5(&mut cursor, None).unwrap();
assert_eq!(headers.len(), 3);
match &headers[0] {
RarHeader::Archive(ah) => {
assert!(ah.volume_number.is_none());
}
other => panic!("expected Archive header, got {:?}", other),
}
match &headers[1] {
RarHeader::File(fh) => {
assert_eq!(fh.filename, "test.txt");
assert_eq!(fh.uncompressed_size, 5);
assert_eq!(fh.compressed_size, 5);
assert_eq!(fh.compression_method, 0);
assert!(!fh.is_directory);
assert!(!fh.is_encrypted);
assert!(!fh.is_solid);
}
other => panic!("expected File header, got {:?}", other),
}
match &headers[2] {
RarHeader::EndArchive(ea) => {
assert!(ea.volume_number.is_none());
}
other => panic!("expected EndArchive header, got {:?}", other),
}
}
fn build_rar5_with_encryption_header() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(RAR5_MAGIC);
let mut inner = Vec::new();
inner.push(4u8); inner.push(0u8); inner.push(0u8); inner.push(0u8); inner.push(15u8); inner.extend_from_slice(&[0u8; 16]);
let header_size_val = inner.len();
assert!(header_size_val < 128);
let size_vint = vec![header_size_val as u8];
let mut header_data = Vec::new();
header_data.extend_from_slice(&size_vint);
header_data.extend_from_slice(&inner);
let crc = crc32fast::hash(&header_data);
out.extend_from_slice(&crc.to_le_bytes());
out.extend_from_slice(&header_data);
out
}
#[test]
fn test_encrypted_headers_no_password() {
let data = build_rar5_with_encryption_header();
let mut cursor = Cursor::new(&data[..]);
let result = parse_rar5(&mut cursor, None);
assert!(result.is_err(), "should fail without password");
assert!(
matches!(result, Err(RarError::EncryptedHeaders)),
"expected EncryptedHeaders error, got: {result:?}"
);
}
#[test]
fn test_encrypted_headers_detected() {
let data = build_rar5_with_encryption_header();
let mut cursor = Cursor::new(&data[..]);
let err = parse_rar5(&mut cursor, None).unwrap_err();
assert_eq!(
format!("{err}"),
"archive has encrypted headers \u{2014} password required to read file list"
);
}
#[test]
fn invalid_crc_rejected() {
let mut data = build_minimal_rar5();
data[8] ^= 0xFF;
let mut cursor = Cursor::new(&data[..]);
let result = parse_rar5(&mut cursor, None);
assert!(matches!(result, Err(RarError::InvalidHeaderCrc(_))));
}
}