use crate::detect::{find_archive_start, ArchiveSignature, RAR13_SIGNATURE};
use crate::error::{Error, Result};
use crate::features::FeatureSet;
use crate::io_util::{read_exact_at, read_u16, read_u32};
pub(crate) use crate::source::ArchiveSource;
use crate::version::{ArchiveFamily, ArchiveVersion};
use rars_codec::rar13::{
unpack15_decode, unpack15_encode, unpack15_encode_with_options,
EncodeOptions as Rar15EncodeOptions, Unpack15, Unpack15Encoder,
};
use rars_crypto::rar13::{Rar13Cipher, Rar13DecryptReader};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
const MAIN_HEAD_SIZE: u16 = 7;
const FILE_HEAD_BASE_SIZE: usize = 21;
const MHD_VOLUME: u8 = 0x01;
const MHD_COMMENT: u8 = 0x02;
const MHD_SOLID: u8 = 0x08;
const MHD_PACK_COMMENT: u8 = 0x10;
const MHD_AV: u8 = 0x20;
const MHD_ALWAYS_SET: u8 = 0x80;
const RAR13_AV_PREFIX: &[u8; 6] = b"\x1ai\x6d\x02\xda\xae";
const COPY_BUFFER_SIZE: usize = 64 * 1024;
const LHD_SPLIT_BEFORE: u8 = 0x01;
const LHD_SPLIT_AFTER: u8 = 0x02;
const LHD_PASSWORD: u8 = 0x04;
const LHD_COMMENT: u8 = 0x08;
const LHD_SOLID: u8 = 0x10;
const METHOD_STORE: u8 = 0;
const METHOD_BEST: u8 = 5;
const DEFAULT_UNP_VER: u8 = 2;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct MainHeader {
pub flags: u8,
pub head_size: u16,
pub extra: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct FileHeader {
pub flags: u8,
pub pack_size: u32,
pub unp_size: u32,
pub file_crc: u16,
pub file_time: u32,
pub file_attr: u8,
pub unp_ver: u8,
pub method: u8,
pub head_size: u16,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Entry {
pub header: FileHeader,
pub name: Vec<u8>,
pub extra: Vec<u8>,
pub packed_range: Range<usize>,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Archive {
pub sfx_offset: usize,
pub main: MainHeader,
pub entries: Vec<Entry>,
source: ArchiveSource,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct AuthenticityVerification {
pub size: u16,
pub prefix: [u8; 6],
pub cipher_body: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AuthenticityVerificationStatus {
Absent,
StructurallyPresent,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ExtractedEntryMeta {
pub name: Vec<u8>,
pub file_time: u32,
pub file_attr: u8,
pub is_directory: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct WriterOptions {
pub target: ArchiveVersion,
pub features: FeatureSet,
pub compression_level: Option<u8>,
}
impl WriterOptions {
pub const fn new(target: ArchiveVersion, features: FeatureSet) -> Self {
Self {
target,
features,
compression_level: None,
}
}
pub const fn with_compression_level(mut self, level: u8) -> Self {
self.compression_level = Some(level);
self
}
}
impl Default for WriterOptions {
fn default() -> Self {
Self {
target: ArchiveVersion::Rar14,
features: FeatureSet::store_only(),
compression_level: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StoredEntry<'a> {
pub name: &'a [u8],
pub data: &'a [u8],
pub file_time: u32,
pub file_attr: u8,
pub password: Option<&'a [u8]>,
pub file_comment: Option<&'a [u8]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FileEntry<'a> {
pub name: &'a [u8],
pub data: &'a [u8],
pub file_time: u32,
pub file_attr: u8,
pub password: Option<&'a [u8]>,
pub file_comment: Option<&'a [u8]>,
}
impl MainHeader {
pub fn is_volume(&self) -> bool {
self.flags & MHD_VOLUME != 0
}
pub fn has_archive_comment(&self) -> bool {
self.flags & MHD_COMMENT != 0
}
pub fn has_packed_comment(&self) -> bool {
self.flags & MHD_PACK_COMMENT != 0
}
pub fn is_solid(&self) -> bool {
self.flags & MHD_SOLID != 0
}
pub fn has_authenticity_verification(&self) -> bool {
self.flags & MHD_AV != 0
}
fn parse(input: &[u8]) -> Result<Self> {
if input.len() < MAIN_HEAD_SIZE as usize {
return Err(Error::TooShort);
}
if !input.starts_with(RAR13_SIGNATURE) {
return Err(Error::UnsupportedSignature);
}
let head_size = read_u16(input, 4)?;
let flags = input[6];
if head_size < MAIN_HEAD_SIZE {
return Err(Error::InvalidHeader(
"RAR 1.3 main header is shorter than 7 bytes",
));
}
if head_size as usize > input.len() {
return Err(Error::TooShort);
}
let extra = input[MAIN_HEAD_SIZE as usize..head_size as usize].to_vec();
Ok(Self {
flags,
head_size,
extra,
})
}
}
impl FileHeader {
fn parse(input: &[u8]) -> Result<(Self, Vec<u8>, Vec<u8>, usize)> {
if input.len() < FILE_HEAD_BASE_SIZE {
return Err(Error::TooShort);
}
let pack_size = read_u32(input, 0)?;
let unp_size = read_u32(input, 4)?;
let file_crc = read_u16(input, 8)?;
let head_size = read_u16(input, 10)?;
let file_time = read_u32(input, 12)?;
let file_attr = input[16];
let flags = input[17];
let unp_ver = input[18];
let name_size = input[19] as usize;
let method = input[20];
let minimum_size = FILE_HEAD_BASE_SIZE + name_size;
if (head_size as usize) < minimum_size {
return Err(Error::InvalidHeader(
"RAR 1.3 file header is shorter than its name",
));
}
if input.len() < head_size as usize {
return Err(Error::TooShort);
}
let name = input[FILE_HEAD_BASE_SIZE..FILE_HEAD_BASE_SIZE + name_size].to_vec();
let extra = input[minimum_size..head_size as usize].to_vec();
Ok((
Self {
flags,
pack_size,
unp_size,
file_crc,
file_time,
file_attr,
unp_ver,
method,
head_size,
},
name,
extra,
head_size as usize,
))
}
}
impl Archive {
pub fn parse(input: &[u8]) -> Result<Self> {
let data: Arc<[u8]> = Arc::from(input.to_vec().into_boxed_slice());
Self::parse_shared(data)
}
pub fn parse_owned(input: Vec<u8>) -> Result<Self> {
Self::parse_shared(Arc::from(input.into_boxed_slice()))
}
pub fn parse_path(path: impl AsRef<Path>) -> Result<Self> {
let path = Arc::new(path.as_ref().to_path_buf());
let mut file = File::open(path.as_ref())?;
let len = file.metadata()?.len();
let scan_len = len.min(128 * 1024) as usize;
let mut scan = vec![0; scan_len];
file.read_exact(&mut scan)?;
let sig = find_archive_start(&scan, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
if sig.family != ArchiveFamily::Rar13 {
return Err(Error::UnsupportedSignature);
}
Self::parse_seekable(file, len, sig.offset, ArchiveSource::File(path))
}
pub fn parse_path_with_signature(
path: impl AsRef<Path>,
signature: ArchiveSignature,
) -> Result<Self> {
if signature.family != ArchiveFamily::Rar13 {
return Err(Error::UnsupportedSignature);
}
let path = Arc::new(path.as_ref().to_path_buf());
let file = File::open(path.as_ref())?;
let len = file.metadata()?.len();
Self::parse_seekable(file, len, signature.offset, ArchiveSource::File(path))
}
fn parse_shared(input: Arc<[u8]>) -> Result<Self> {
let sig = find_archive_start(&input, 128 * 1024).ok_or(Error::UnsupportedSignature)?;
if sig.family != ArchiveFamily::Rar13 {
return Err(Error::UnsupportedSignature);
}
let archive = &input[sig.offset..];
let main = MainHeader::parse(archive)?;
let mut pos = main.head_size as usize;
let mut entries = Vec::new();
while pos < archive.len() {
if archive.len() - pos < FILE_HEAD_BASE_SIZE {
break;
}
let (header, name, extra, consumed) = FileHeader::parse(&archive[pos..])?;
let data_start = pos + consumed;
let data_end =
data_start
.checked_add(header.pack_size as usize)
.ok_or(Error::InvalidHeader(
"RAR 1.3 file data size overflows usize",
))?;
if data_end > archive.len() {
return Err(Error::TooShort);
}
entries.push(Entry {
header,
name,
extra,
packed_range: sig.offset + data_start..sig.offset + data_end,
});
pos = data_end;
}
Ok(Self {
sfx_offset: sig.offset,
main,
entries,
source: ArchiveSource::Memory(input),
})
}
fn parse_seekable(
mut file: File,
file_len: u64,
sfx_offset: usize,
source: ArchiveSource,
) -> Result<Self> {
let main_prefix = read_exact_at(&mut file, sfx_offset, MAIN_HEAD_SIZE as usize)?;
let head_size = read_u16(&main_prefix, 4)? as usize;
let main_bytes = read_exact_at(&mut file, sfx_offset, head_size)?;
let main = MainHeader::parse(&main_bytes)?;
let mut pos = main.head_size as usize;
let mut entries = Vec::new();
while (sfx_offset + pos) as u64 + FILE_HEAD_BASE_SIZE as u64 <= file_len {
let header_prefix = read_exact_at(&mut file, sfx_offset + pos, FILE_HEAD_BASE_SIZE)?;
let head_size = read_u16(&header_prefix, 10)? as usize;
let header_bytes = read_exact_at(&mut file, sfx_offset + pos, head_size)?;
let (header, name, extra, consumed) = FileHeader::parse(&header_bytes)?;
let data_start = pos + consumed;
let data_end =
data_start
.checked_add(header.pack_size as usize)
.ok_or(Error::InvalidHeader(
"RAR 1.3 file data size overflows usize",
))?;
if (sfx_offset + data_end) as u64 > file_len {
return Err(Error::TooShort);
}
entries.push(Entry {
header,
name,
extra,
packed_range: sfx_offset + data_start..sfx_offset + data_end,
});
pos = data_end;
}
Ok(Self {
sfx_offset,
main,
entries,
source,
})
}
fn copy_range_to(&self, range: Range<usize>, out: &mut impl Write) -> Result<()> {
self.source.copy_range_to(range, out)
}
fn range_reader(&self, range: Range<usize>) -> Result<Box<dyn Read + '_>> {
self.source.range_reader(range)
}
fn copy_decrypted_range_to(
&self,
range: Range<usize>,
mut cipher: Rar13Cipher,
out: &mut impl Write,
) -> Result<()> {
let mut buffer = [0u8; COPY_BUFFER_SIZE];
match &self.source {
ArchiveSource::Memory(data) => {
let data = data.get(range).ok_or(Error::TooShort)?;
for chunk in data.chunks(COPY_BUFFER_SIZE) {
buffer[..chunk.len()].copy_from_slice(chunk);
for byte in &mut buffer[..chunk.len()] {
*byte = cipher.decrypt_byte(*byte);
}
out.write_all(&buffer[..chunk.len()])?;
}
}
ArchiveSource::File(path) => {
let mut file = File::open(path.as_ref())?;
file.seek(SeekFrom::Start(range.start as u64))?;
let mut remaining = range.len();
while remaining > 0 {
let to_read = remaining.min(buffer.len());
file.read_exact(&mut buffer[..to_read])?;
for byte in &mut buffer[..to_read] {
*byte = cipher.decrypt_byte(*byte);
}
out.write_all(&buffer[..to_read])?;
remaining -= to_read;
}
}
}
Ok(())
}
pub fn extract_to<F>(&self, password: Option<&[u8]>, mut open: F) -> Result<()>
where
F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
{
let mut unpack15 = Unpack15::new();
let mut extracted_count = 0usize;
for entry in &self.entries {
if entry.is_split_before() || entry.is_split_after() {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry requires multivolume extraction",
));
}
let meta = entry.metadata();
if meta.is_directory {
let _ = open(&meta)?;
extracted_count += 1;
continue;
}
let mut writer = open(&meta)?;
if entry.is_stored() && !entry.is_encrypted() {
entry
.write_stored_to(self, password, &mut writer)
.map_err(|error| entry.entry_error("extracting", error))?;
} else {
entry
.write_compressed_to(
self,
password,
&mut unpack15,
self.main.is_solid() && extracted_count != 0,
&mut writer,
)
.map_err(|error| entry.entry_error("extracting", error))?;
}
extracted_count += 1;
}
Ok(())
}
pub fn archive_comment(&self) -> Result<Option<Vec<u8>>> {
if !self.main.has_archive_comment() {
return Ok(None);
}
let length = read_u16(&self.main.extra, 0)? as usize;
if self.main.has_packed_comment() {
if length < 2 {
return Err(Error::InvalidHeader(
"RAR 1.3 packed archive comment is shorter than size field",
));
}
let unpacked_len = read_u16(&self.main.extra, 2)? as usize;
let packed_len = length - 2;
let packed_start = 4usize;
let packed_end = packed_start
.checked_add(packed_len)
.ok_or(Error::InvalidHeader(
"RAR 1.3 archive comment size overflows",
))?;
if packed_end > self.main.extra.len() {
return Err(Error::TooShort);
}
let mut packed = self.main.extra[packed_start..packed_end].to_vec();
Rar13Cipher::new_comment().decrypt_in_place(&mut packed);
return Ok(Some(unpack15_decode(&packed, unpacked_len)?));
}
let comment_start = 2usize;
let comment_end = comment_start
.checked_add(length)
.ok_or(Error::InvalidHeader(
"RAR 1.3 archive comment size overflows",
))?;
if comment_end > self.main.extra.len() {
return Err(Error::TooShort);
}
Ok(Some(self.main.extra[comment_start..comment_end].to_vec()))
}
pub fn authenticity_verification(&self) -> Result<Option<AuthenticityVerification>> {
if !self.main.has_authenticity_verification() {
return Ok(None);
}
let size = read_u16(&self.main.extra, 0)?;
if size < RAR13_AV_PREFIX.len() as u16 {
return Err(Error::InvalidHeader("RAR 1.3 AV payload is too short"));
}
let payload_end = 2usize
.checked_add(size as usize)
.ok_or(Error::InvalidHeader("RAR 1.3 AV payload size overflows"))?;
if payload_end > self.main.extra.len() {
return Err(Error::TooShort);
}
let prefix_bytes = self
.main
.extra
.get(2..2 + RAR13_AV_PREFIX.len())
.ok_or(Error::TooShort)?;
let prefix: [u8; 6] = prefix_bytes
.try_into()
.expect("RAR 1.3 AV prefix slice has fixed length");
if &prefix != RAR13_AV_PREFIX {
return Err(Error::InvalidHeader("RAR 1.3 AV prefix mismatch"));
}
Ok(Some(AuthenticityVerification {
size,
prefix,
cipher_body: self.main.extra[2 + RAR13_AV_PREFIX.len()..payload_end].to_vec(),
}))
}
pub fn authenticity_verification_status(&self) -> Result<AuthenticityVerificationStatus> {
Ok(if self.authenticity_verification()?.is_some() {
AuthenticityVerificationStatus::StructurallyPresent
} else {
AuthenticityVerificationStatus::Absent
})
}
}
impl Entry {
pub fn name_bytes(&self) -> &[u8] {
&self.name
}
pub fn name_lossy(&self) -> String {
String::from_utf8_lossy(&self.name).into_owned()
}
pub fn is_encrypted(&self) -> bool {
self.header.flags & LHD_PASSWORD != 0
}
pub fn is_split_before(&self) -> bool {
self.header.flags & LHD_SPLIT_BEFORE != 0
}
pub fn is_split_after(&self) -> bool {
self.header.flags & LHD_SPLIT_AFTER != 0
}
pub fn is_directory(&self) -> bool {
self.header.file_attr & 0x10 != 0
}
pub fn has_file_comment(&self) -> bool {
self.header.flags & LHD_COMMENT != 0
}
pub fn file_comment(&self) -> Result<Option<Vec<u8>>> {
if !self.has_file_comment() {
return Ok(None);
}
let length = read_u16(&self.extra, 0)? as usize;
let comment_start = 2usize;
let comment_end = comment_start
.checked_add(length)
.ok_or(Error::InvalidHeader("RAR 1.3 file comment size overflows"))?;
if comment_end > self.extra.len() {
return Err(Error::TooShort);
}
Ok(Some(self.extra[comment_start..comment_end].to_vec()))
}
pub fn is_stored(&self) -> bool {
self.header.method == METHOD_STORE
}
pub fn packed_data<'a>(&self, archive: &'a Archive) -> Result<&'a [u8]> {
match &archive.source {
ArchiveSource::Memory(data) => {
data.get(self.packed_range.clone()).ok_or(Error::TooShort)
}
ArchiveSource::File(_) => Err(Error::InvalidHeader(
"RAR 1.3 file-backed packed data requires owned read",
)),
}
}
pub fn write_packed_data(&self, archive: &Archive, out: &mut impl Write) -> Result<()> {
archive.copy_range_to(self.packed_range.clone(), out)
}
pub fn verify_checksum(&self, data: &[u8]) -> Result<()> {
let actual = file_checksum(data);
if actual == self.header.file_crc {
Ok(())
} else {
Err(Error::CrcMismatch {
expected: self.header.file_crc,
actual,
})
}
}
pub fn metadata(&self) -> ExtractedEntryMeta {
ExtractedEntryMeta {
name: self.name.clone(),
file_time: self.header.file_time,
file_attr: self.header.file_attr,
is_directory: self.is_directory(),
}
}
fn write_stored_to(
&self,
archive: &Archive,
password: Option<&[u8]>,
out: &mut impl Write,
) -> Result<()> {
if !self.is_stored() {
return Err(Error::InvalidHeader("RAR 1.3 entry is not stored"));
}
if self.is_encrypted() {
let password = password.ok_or(Error::NeedPassword)?;
let mut checksum = Rar13Checksum::new();
let mut checksum_writer = Rar13ChecksumWriter {
inner: out,
checksum: &mut checksum,
};
archive.copy_decrypted_range_to(
self.packed_range.clone(),
Rar13Cipher::new(password),
&mut checksum_writer,
)?;
let actual = checksum.finish();
return if actual == self.header.file_crc {
Ok(())
} else {
Err(Error::CrcMismatch {
expected: self.header.file_crc,
actual,
})
};
}
let mut checksum = Rar13Checksum::new();
let mut checksum_writer = Rar13ChecksumWriter {
inner: out,
checksum: &mut checksum,
};
self.write_packed_data(archive, &mut checksum_writer)?;
let actual = checksum.finish();
if actual == self.header.file_crc {
Ok(())
} else {
Err(Error::CrcMismatch {
expected: self.header.file_crc,
actual,
})
}
}
fn write_compressed_to(
&self,
archive: &Archive,
password: Option<&[u8]>,
unpack15: &mut Unpack15,
solid: bool,
out: &mut impl Write,
) -> Result<()> {
if self.is_stored() || self.is_directory() {
return self.write_stored_to(archive, password, out);
}
let mut checksum = Rar13Checksum::new();
let mut checksum_writer = Rar13ChecksumWriter {
inner: out,
checksum: &mut checksum,
};
if self.is_encrypted() {
let password = password.ok_or(Error::NeedPassword)?;
let packed = archive.range_reader(self.packed_range.clone())?;
let mut packed = Rar13DecryptReader::new(packed, Rar13Cipher::new(password));
unpack15.decode_member_from_reader(
&mut packed,
self.header.unp_size as usize,
solid,
&mut checksum_writer,
)?;
} else {
let mut packed = archive.range_reader(self.packed_range.clone())?;
unpack15.decode_member_from_reader(
&mut packed,
self.header.unp_size as usize,
solid,
&mut checksum_writer,
)?;
}
let actual = checksum.finish();
if actual == self.header.file_crc {
Ok(())
} else {
Err(Error::CrcMismatch {
expected: self.header.file_crc,
actual,
})
}
}
pub fn write_to(
&self,
archive: &Archive,
password: Option<&[u8]>,
out: &mut impl Write,
) -> Result<()> {
self.write_compressed_to(archive, password, &mut Unpack15::new(), false, out)
}
fn entry_error(&self, operation: &'static str, error: Error) -> Error {
if matches!(
error,
Error::NeedPassword | Error::WrongPasswordOrCorruptData
) {
return error;
}
if self.is_encrypted()
&& matches!(
error,
Error::InvalidHeader(_)
| Error::Codec(_)
| Error::CrcMismatch { .. }
| Error::Crc32Mismatch { .. }
| Error::HashMismatch { .. }
)
{
return Error::WrongPasswordOrCorruptData;
}
error.at_entry(self.name.clone(), operation)
}
}
pub fn extract_volumes_to<F>(
volumes: &[Archive],
password: Option<&[u8]>,
mut open: F,
) -> Result<()>
where
F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
{
let mut pending: Option<PendingSplitRefs> = None;
let mut unpack15 = Unpack15::new();
let mut extracted_count = 0usize;
for (volume_index, archive) in volumes.iter().enumerate() {
for (entry_index, entry) in archive.entries.iter().enumerate() {
if !entry.is_split_before() && !entry.is_split_after() {
if pending.is_some() {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry is interrupted by a regular entry",
));
}
let meta = entry.metadata();
if meta.is_directory {
let _ = open(&meta)?;
extracted_count += 1;
continue;
}
let mut writer = open(&meta)?;
entry
.write_compressed_to(
archive,
password,
&mut unpack15,
archive.main.is_solid() && extracted_count != 0,
&mut writer,
)
.map_err(|error| entry.entry_error("extracting", error))?;
extracted_count += 1;
continue;
}
match (
&mut pending,
entry.is_split_before(),
entry.is_split_after(),
) {
(None, false, true) => {
pending = Some(PendingSplitRefs::new(entry, volume_index, entry_index));
}
(Some(current), true, true) => {
current.append(entry, volume_index, entry_index)?;
}
(Some(current), true, false) => {
current.append(entry, volume_index, entry_index)?;
let completed = pending.take().expect("pending split");
let solid = archive.main.is_solid() && extracted_count != 0;
completed
.write_to(volumes, entry, password, &mut unpack15, solid, &mut open)
.map_err(|error| entry.entry_error("extracting", error))?;
extracted_count += 1;
}
_ => {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry flags are inconsistent",
));
}
}
}
}
if pending.is_some() {
return Err(Error::InvalidHeader("RAR 1.3 split entry is incomplete"));
}
Ok(())
}
struct Rar13ChecksumWriter<'a, W: Write + ?Sized> {
inner: &'a mut W,
checksum: &'a mut Rar13Checksum,
}
impl<W: Write + ?Sized> Write for Rar13ChecksumWriter<'_, W> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let written = self.inner.write(buf)?;
self.checksum.update(&buf[..written]);
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
struct Rar13Checksum {
value: u16,
}
impl Rar13Checksum {
fn new() -> Self {
Self { value: 0 }
}
fn update(&mut self, input: &[u8]) {
for &byte in input {
self.value = self.value.wrapping_add(byte as u16).rotate_left(1);
}
}
fn finish(self) -> u16 {
self.value
}
}
struct PendingSplitRefs {
name: Vec<u8>,
fragments: Vec<(usize, usize)>,
file_time: u32,
file_attr: u8,
method: u8,
unp_ver: u8,
was_encrypted: bool,
}
impl PendingSplitRefs {
fn new(entry: &Entry, volume_index: usize, entry_index: usize) -> Self {
Self {
name: entry.name.clone(),
fragments: vec![(volume_index, entry_index)],
file_time: entry.header.file_time,
file_attr: entry.header.file_attr,
method: entry.header.method,
unp_ver: entry.header.unp_ver,
was_encrypted: entry.is_encrypted(),
}
}
fn append(&mut self, entry: &Entry, volume_index: usize, entry_index: usize) -> Result<()> {
if entry.name != self.name {
return Err(Error::InvalidHeader("RAR 1.3 split entry name changed"));
}
if entry.header.method != self.method {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry compression method changed",
));
}
if entry.header.unp_ver != self.unp_ver {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry unpack version changed",
));
}
if entry.is_encrypted() != self.was_encrypted {
return Err(Error::InvalidHeader(
"RAR 1.3 split entry encryption flag changed",
));
}
self.fragments.push((volume_index, entry_index));
Ok(())
}
fn write_to<F>(
self,
volumes: &[Archive],
final_entry: &Entry,
password: Option<&[u8]>,
unpack15: &mut Unpack15,
solid: bool,
open: &mut F,
) -> Result<()>
where
F: FnMut(&ExtractedEntryMeta) -> Result<Box<dyn Write>>,
{
let mut reader = self.fragment_reader(volumes, password)?;
let meta = ExtractedEntryMeta {
name: self.name,
file_time: self.file_time,
file_attr: self.file_attr,
is_directory: false,
};
let mut writer = open(&meta)?;
let mut checksum = Rar13Checksum::new();
let mut checksum_writer = Rar13ChecksumWriter {
inner: &mut writer,
checksum: &mut checksum,
};
if self.method == METHOD_STORE {
std::io::copy(&mut reader, &mut checksum_writer)?;
} else {
unpack15.decode_member_from_reader(
&mut reader,
final_entry.header.unp_size as usize,
solid,
&mut checksum_writer,
)?;
}
let actual = checksum.finish();
if actual == final_entry.header.file_crc {
Ok(())
} else {
Err(Error::CrcMismatch {
expected: final_entry.header.file_crc,
actual,
})
}
}
fn fragment_reader<'a>(
&self,
volumes: &'a [Archive],
password: Option<&'a [u8]>,
) -> Result<ChainedReader<'a>> {
let mut readers = Vec::with_capacity(self.fragments.len());
for &(volume_index, entry_index) in &self.fragments {
let archive = volumes
.get(volume_index)
.ok_or(Error::InvalidHeader("RAR 1.3 split volume is missing"))?;
let entry = archive
.entries
.get(entry_index)
.ok_or(Error::InvalidHeader("RAR 1.3 split entry is missing"))?;
let reader = archive.range_reader(entry.packed_range.clone())?;
if entry.is_encrypted() {
let password = password.ok_or(Error::NeedPassword)?;
readers.push(
Box::new(Rar13DecryptReader::new(reader, Rar13Cipher::new(password)))
as Box<dyn Read + 'a>,
);
} else {
readers.push(reader);
}
}
Ok(ChainedReader { readers, index: 0 })
}
}
struct ChainedReader<'a> {
readers: Vec<Box<dyn Read + 'a>>,
index: usize,
}
impl Read for ChainedReader<'_> {
fn read(&mut self, out: &mut [u8]) -> std::io::Result<usize> {
while let Some(reader) = self.readers.get_mut(self.index) {
let read = reader.read(out)?;
if read != 0 {
return Ok(read);
}
self.index += 1;
}
Ok(0)
}
}
pub fn write_stored_archive(
entries: &[StoredEntry<'_>],
options: WriterOptions,
) -> Result<Vec<u8>> {
write_stored_archive_with_comment(entries, options, None)
}
pub fn write_stored_archive_with_comment(
entries: &[StoredEntry<'_>],
options: WriterOptions,
archive_comment: Option<&[u8]>,
) -> Result<Vec<u8>> {
if !options.target.is_rar13_family() {
return Err(Error::UnsupportedVersion(options.target));
}
options.features.validate_for(options.target)?;
validate_stored_writer_features(options.target, options.features)?;
let mut out = Vec::new();
write_main_header(&mut out, options.features, archive_comment)?;
for entry in entries {
validate_stored_entry(entry)?;
write_stored_entry(&mut out, entry, options.features)?;
}
Ok(out)
}
pub fn write_compressed_archive(
entries: &[FileEntry<'_>],
options: WriterOptions,
) -> Result<Vec<u8>> {
write_compressed_archive_with_comment(entries, options, None)
}
pub fn write_compressed_archive_with_comment(
entries: &[FileEntry<'_>],
options: WriterOptions,
archive_comment: Option<&[u8]>,
) -> Result<Vec<u8>> {
if !options.target.is_rar13_family() {
return Err(Error::UnsupportedVersion(options.target));
}
options.features.validate_for(options.target)?;
validate_compressed_writer_features(options.target, options.features)?;
validate_compression_level(options)?;
let mut out = Vec::new();
write_main_header(&mut out, options.features, archive_comment)?;
let encode_options = rar15_encode_options_for_level(options.compression_level)?;
let mut solid_encoder = options
.features
.solid
.then(|| Unpack15Encoder::with_options(encode_options));
for entry in entries {
validate_file_entry(entry.name, entry.data)?;
let solid = solid_encoder.is_some();
let mut packed = if let Some(encoder) = solid_encoder.as_mut() {
encoder.encode_member(entry.data)?
} else if options.compression_level == Some(0) {
entry.data.to_vec()
} else {
encode_verified_rar15_payload(entry.data, encode_options)?
.unwrap_or_else(|| entry.data.to_vec())
};
let method = if options.compression_level == Some(0)
|| (!solid && packed.len() >= entry.data.len())
{
packed = entry.data.to_vec();
METHOD_STORE
} else {
METHOD_BEST
};
if let Some(password) = entry.password {
Rar13Cipher::new(password).encrypt_in_place(&mut packed);
}
let mut flags = 0;
if options.features.solid {
flags |= LHD_SOLID;
}
if entry.password.is_some() {
flags |= LHD_PASSWORD;
}
if entry.file_comment.is_some() {
flags |= LHD_COMMENT;
}
let file_extra = encode_file_comment(entry.file_comment)?;
write_file_entry(
&mut out,
FileEntryRecord {
name: entry.name,
unpacked_size: entry.data.len() as u32,
file_crc: file_checksum(entry.data),
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
flags,
unp_ver: DEFAULT_UNP_VER,
method,
extra: &file_extra,
},
)?;
}
Ok(out)
}
pub fn write_stored_volumes(
entry: StoredEntry<'_>,
options: WriterOptions,
max_packed_per_volume: usize,
) -> Result<Vec<Vec<u8>>> {
if !options.target.is_rar13_family() {
return Err(Error::UnsupportedVersion(options.target));
}
options.features.validate_for(options.target)?;
validate_stored_writer_features(options.target, options.features)?;
validate_volume_writer_inputs(
entry.name,
entry.data,
entry.password,
entry.file_comment,
options,
)?;
let body = entry.data.to_vec();
write_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: &body,
file_time: entry.file_time,
file_attr: entry.file_attr,
method: METHOD_STORE,
base_flags: 0,
features: options.features,
max_packed_per_volume,
})
}
pub fn write_compressed_volumes(
entry: FileEntry<'_>,
options: WriterOptions,
max_packed_per_volume: usize,
) -> Result<Vec<Vec<u8>>> {
if !options.target.is_rar13_family() {
return Err(Error::UnsupportedVersion(options.target));
}
options.features.validate_for(options.target)?;
validate_compressed_writer_features(options.target, options.features)?;
validate_volume_writer_inputs(
entry.name,
entry.data,
entry.password,
entry.file_comment,
options,
)?;
validate_compression_level(options)?;
let mut packed = encode_verified_rar15_payload(
entry.data,
rar15_encode_options_for_level(options.compression_level)?,
)?
.unwrap_or_else(|| entry.data.to_vec());
let method = if packed.len() >= entry.data.len() {
packed = entry.data.to_vec();
METHOD_STORE
} else {
METHOD_BEST
};
write_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
method,
base_flags: 0,
features: options.features,
max_packed_per_volume,
})
}
fn validate_stored_writer_features(version: ArchiveVersion, features: FeatureSet) -> Result<()> {
reject_writer_feature(version, features.sfx, "sfx")?;
reject_writer_feature(
version,
features.authenticity_verification,
"authenticity_verification",
)?;
Ok(())
}
fn validate_volume_writer_inputs(
name: &[u8],
data: &[u8],
password: Option<&[u8]>,
file_comment: Option<&[u8]>,
options: WriterOptions,
) -> Result<()> {
validate_file_entry(name, data)?;
if password.is_some() {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "volume_password",
});
}
if file_comment.is_some() || options.features.file_comment {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "volume_file_comment",
});
}
if options.features.archive_comment {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "volume_archive_comment",
});
}
Ok(())
}
fn validate_compressed_writer_features(
version: ArchiveVersion,
features: FeatureSet,
) -> Result<()> {
reject_writer_feature(version, features.sfx, "sfx")?;
reject_writer_feature(
version,
features.authenticity_verification,
"authenticity_verification",
)?;
Ok(())
}
fn validate_compression_level(options: WriterOptions) -> Result<()> {
if matches!(options.compression_level, Some(level) if level > 5) {
return Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
));
}
Ok(())
}
fn rar15_encode_options_for_level(level: Option<u8>) -> Result<Rar15EncodeOptions> {
let level = level.unwrap_or(5);
let compatible = Rar15EncodeOptions::new().with_old_distance_tokens(false);
match level {
0 => Ok(compatible
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(0)),
1 => Ok(compatible
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(4 * 1024)),
2 => Ok(compatible
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(8 * 1024)),
3 => Ok(compatible
.with_lazy_matching(false)
.with_max_long_match_distance(16 * 1024)),
4 => Ok(compatible
.with_lazy_matching(false)
.with_max_long_match_distance(24 * 1024)),
5 => Ok(compatible.with_lazy_matching(false)),
_ => Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
)),
}
}
fn encode_verified_rar15_payload(
data: &[u8],
options: Rar15EncodeOptions,
) -> Result<Option<Vec<u8>>> {
for candidate_options in rar15_encode_fallback_options(options) {
let packed = unpack15_encode_with_options(data, candidate_options)?;
if unpack15_payload_matches(&packed, data)? {
return Ok(Some(packed));
}
}
Ok(None)
}
fn rar15_encode_fallback_options(options: Rar15EncodeOptions) -> Vec<Rar15EncodeOptions> {
let mut candidates = vec![options];
let distance_limited = options.with_max_long_match_distance(24 * 1024);
if distance_limited != options {
candidates.push(distance_limited);
}
let conservative = options
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(8 * 1024);
if !candidates.contains(&conservative) {
candidates.push(conservative);
}
candidates
}
fn unpack15_payload_matches(packed: &[u8], data: &[u8]) -> Result<bool> {
match unpack15_decode(packed, data.len()) {
Ok(decoded) => Ok(decoded == data),
Err(_) => Ok(false),
}
}
fn reject_writer_feature(
version: ArchiveVersion,
enabled: bool,
feature: &'static str,
) -> Result<()> {
if enabled {
Err(Error::UnsupportedFeature { version, feature })
} else {
Ok(())
}
}
fn write_main_header(
out: &mut Vec<u8>,
features: FeatureSet,
archive_comment: Option<&[u8]>,
) -> Result<()> {
write_main_header_with_flags(out, features, archive_comment, 0)
}
fn write_main_header_with_flags(
out: &mut Vec<u8>,
features: FeatureSet,
archive_comment: Option<&[u8]>,
extra_flags: u8,
) -> Result<()> {
let comment_extra = encode_archive_comment(archive_comment)?;
let mut flags = MHD_ALWAYS_SET | extra_flags;
if archive_comment.is_some() {
flags |= MHD_COMMENT;
flags |= MHD_PACK_COMMENT;
}
if features.solid {
flags |= MHD_SOLID;
}
out.extend_from_slice(RAR13_SIGNATURE);
let head_size = MAIN_HEAD_SIZE as usize + comment_extra.len();
if head_size > u16::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 main header comment extension is too large",
));
}
out.extend_from_slice(&(head_size as u16).to_le_bytes());
out.push(flags);
out.extend_from_slice(&comment_extra);
Ok(())
}
fn write_stored_entry(
out: &mut Vec<u8>,
entry: &StoredEntry<'_>,
features: FeatureSet,
) -> Result<()> {
let mut flags = 0u8;
if entry.password.is_some() {
flags |= LHD_PASSWORD;
}
if entry.file_comment.is_some() {
flags |= LHD_COMMENT;
}
if features.solid {
flags |= LHD_SOLID;
}
let mut body = entry.data.to_vec();
if let Some(password) = entry.password {
Rar13Cipher::new(password).encrypt_in_place(&mut body);
}
let file_extra = encode_file_comment(entry.file_comment)?;
write_file_entry(
out,
FileEntryRecord {
name: entry.name,
unpacked_size: entry.data.len() as u32,
file_crc: file_checksum(entry.data),
packed: &body,
file_time: entry.file_time,
file_attr: entry.file_attr,
flags,
unp_ver: DEFAULT_UNP_VER,
method: METHOD_STORE,
extra: &file_extra,
},
)?;
Ok(())
}
fn validate_stored_entry(entry: &StoredEntry<'_>) -> Result<()> {
validate_file_entry(entry.name, entry.data)
}
struct FileEntryRecord<'a> {
name: &'a [u8],
unpacked_size: u32,
file_crc: u16,
packed: &'a [u8],
file_time: u32,
file_attr: u8,
flags: u8,
unp_ver: u8,
method: u8,
extra: &'a [u8],
}
fn write_file_entry(out: &mut Vec<u8>, entry: FileEntryRecord<'_>) -> Result<()> {
let head_size = FILE_HEAD_BASE_SIZE + entry.name.len() + entry.extra.len();
out.extend_from_slice(&(entry.packed.len() as u32).to_le_bytes());
out.extend_from_slice(&entry.unpacked_size.to_le_bytes());
out.extend_from_slice(&entry.file_crc.to_le_bytes());
out.extend_from_slice(&(head_size as u16).to_le_bytes());
out.extend_from_slice(&entry.file_time.to_le_bytes());
out.push(entry.file_attr);
out.push(entry.flags);
out.push(entry.unp_ver);
out.push(entry.name.len() as u8);
out.push(entry.method);
out.extend_from_slice(entry.name);
out.extend_from_slice(entry.extra);
out.extend_from_slice(entry.packed);
Ok(())
}
struct SplitVolumeRecord<'a> {
name: &'a [u8],
unpacked: &'a [u8],
packed: &'a [u8],
file_time: u32,
file_attr: u8,
method: u8,
base_flags: u8,
features: FeatureSet,
max_packed_per_volume: usize,
}
fn write_split_volumes(entry: SplitVolumeRecord<'_>) -> Result<Vec<Vec<u8>>> {
if entry.max_packed_per_volume == 0 {
return Err(Error::InvalidHeader(
"RAR 1.3 volume payload size must be non-zero",
));
}
if entry.packed.is_empty() {
return Err(Error::InvalidHeader(
"RAR 1.3 volume writer needs a non-empty packed payload",
));
}
let chunks: Vec<&[u8]> = entry.packed.chunks(entry.max_packed_per_volume).collect();
if chunks.len() < 2 {
return Err(Error::InvalidHeader(
"RAR 1.3 volume writer needs at least two volumes",
));
}
let mut volumes = Vec::with_capacity(chunks.len());
for (index, chunk) in chunks.iter().enumerate() {
let split_before = index > 0;
let split_after = index + 1 < chunks.len();
let mut flags = entry.base_flags;
if split_before {
flags |= LHD_SPLIT_BEFORE;
}
if split_after {
flags |= LHD_SPLIT_AFTER;
}
if entry.features.solid {
flags |= LHD_SOLID;
}
let mut out = Vec::new();
write_main_header_with_flags(&mut out, entry.features, None, MHD_VOLUME)?;
let checksum_data = if split_after { *chunk } else { entry.unpacked };
write_file_entry(
&mut out,
FileEntryRecord {
name: entry.name,
unpacked_size: entry.unpacked.len() as u32,
file_crc: file_checksum(checksum_data),
packed: chunk,
file_time: entry.file_time,
file_attr: entry.file_attr,
flags,
unp_ver: DEFAULT_UNP_VER,
method: entry.method,
extra: &[],
},
)?;
volumes.push(out);
}
Ok(volumes)
}
fn encode_archive_comment(comment: Option<&[u8]>) -> Result<Vec<u8>> {
let Some(comment) = comment else {
return Ok(Vec::new());
};
if comment.len() > u16::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 archive comment is longer than 65535 bytes",
));
}
let mut packed = unpack15_encode(comment)?;
Rar13Cipher::new_comment().encrypt_in_place(&mut packed);
let packed_field_len = packed.len().checked_add(2).ok_or(Error::InvalidHeader(
"RAR 1.3 archive comment size overflows",
))?;
if packed_field_len > u16::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 packed archive comment is longer than 65535 bytes",
));
}
let mut out = Vec::with_capacity(4 + packed.len());
out.extend_from_slice(&(packed_field_len as u16).to_le_bytes());
out.extend_from_slice(&(comment.len() as u16).to_le_bytes());
out.extend_from_slice(&packed);
Ok(out)
}
fn encode_file_comment(comment: Option<&[u8]>) -> Result<Vec<u8>> {
let Some(comment) = comment else {
return Ok(Vec::new());
};
if comment.len() > u16::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 file comment is longer than 65535 bytes",
));
}
let mut out = Vec::with_capacity(2 + comment.len());
out.extend_from_slice(&(comment.len() as u16).to_le_bytes());
out.extend_from_slice(comment);
Ok(out)
}
fn validate_file_entry(name: &[u8], data: &[u8]) -> Result<()> {
if name.is_empty() {
return Err(Error::InvalidHeader("RAR 1.3 file name is empty"));
}
if name.len() > u8::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 file name is longer than 255 bytes",
));
}
if data.len() > u32::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.3 file is larger than 32-bit size fields",
));
}
Ok(())
}
pub fn file_checksum(input: &[u8]) -> u16 {
let mut checksum = Rar13Checksum::new();
checksum.update(input);
checksum.finish()
}
#[cfg(test)]
mod tests {
use super::*;
use rars_codec::rar13::{find_long_lz, LongLz};
use std::cell::RefCell;
use std::rc::Rc;
struct CollectWriter(Rc<RefCell<Vec<u8>>>);
#[derive(Debug, Clone, PartialEq, Eq)]
struct CollectedEntry {
name: Vec<u8>,
data: Vec<u8>,
file_time: u32,
file_attr: u8,
is_directory: bool,
}
impl Write for CollectWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.borrow_mut().extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn collect_extract(archive: &Archive, password: Option<&[u8]>) -> Result<Vec<CollectedEntry>> {
let entries = RefCell::new(Vec::new());
archive.extract_to(password, |meta| {
let data = Rc::new(RefCell::new(Vec::new()));
entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
Ok(Box::new(CollectWriter(data)))
})?;
Ok(entries
.into_inner()
.into_iter()
.map(|(meta, data)| CollectedEntry {
name: meta.name,
data: data.borrow().clone(),
file_time: meta.file_time,
file_attr: meta.file_attr,
is_directory: meta.is_directory,
})
.collect())
}
fn collect_extract_volumes(
volumes: &[Archive],
password: Option<&[u8]>,
) -> Result<Vec<CollectedEntry>> {
let entries = RefCell::new(Vec::new());
extract_volumes_to(volumes, password, |meta| {
let data = Rc::new(RefCell::new(Vec::new()));
entries.borrow_mut().push((meta.clone(), Rc::clone(&data)));
Ok(Box::new(CollectWriter(data)))
})?;
Ok(entries
.into_inner()
.into_iter()
.map(|(meta, data)| CollectedEntry {
name: meta.name,
data: data.borrow().clone(),
file_time: meta.file_time,
file_attr: meta.file_attr,
is_directory: meta.is_directory,
})
.collect())
}
fn synthetic_log_payload(lines: usize) -> Vec<u8> {
let mut data = Vec::new();
for index in 0..lines {
data.extend_from_slice(
format!(
"2026-05-12T12:{:02}:{:02}.000Z INFO worker-{:02} request_id={:04x}-{:05} path=/api/v1/items/{} status={} elapsed_ms={} bytes={} message=processed archive chunk retry={} user=service-{}\n",
index % 60,
(index * 7) % 60,
index % 16,
index % 10000,
(index * 17) % 100000,
index % 2048,
200 + (index % 5),
(index * 37) % 5000,
(index * 911) % 65536,
index % 3,
index % 32
)
.as_bytes(),
);
}
data
}
#[test]
fn writes_and_reads_stored_archive() {
let input = [
StoredEntry {
name: b"README.md",
data: b"hello rar 1.3",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
StoredEntry {
name: b"docs",
data: b"",
file_time: 0,
file_attr: 0x10,
password: None,
file_comment: None,
},
];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.main.flags, 0x80);
assert_eq!(archive.entries.len(), 2);
assert_eq!(archive.entries[0].name_bytes(), b"README.md");
assert_eq!(archive.entries[0].name_lossy(), "README.md");
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, b"hello rar 1.3");
assert!(archive.entries[1].is_directory());
assert!(extracted[1].is_directory);
}
#[test]
fn rejects_malformed_main_header_boundaries() {
assert_eq!(MainHeader::parse(b"RE~"), Err(Error::TooShort));
let mut too_small = Vec::from(&b"RE~^"[..]);
too_small.extend_from_slice(&6u16.to_le_bytes());
too_small.push(0x80);
assert_eq!(
MainHeader::parse(&too_small),
Err(Error::InvalidHeader(
"RAR 1.3 main header is shorter than 7 bytes"
))
);
let mut truncated_extra = Vec::from(&b"RE~^"[..]);
truncated_extra.extend_from_slice(&8u16.to_le_bytes());
truncated_extra.push(0x80);
assert_eq!(MainHeader::parse(&truncated_extra), Err(Error::TooShort));
assert!(matches!(
Archive::parse(b"Rar!\x1a\x07\x00"),
Err(Error::UnsupportedSignature)
));
}
#[test]
fn rejects_file_header_shorter_than_its_name() {
let mut bytes = Vec::from(&b"RE~^"[..]);
bytes.extend_from_slice(&7u16.to_le_bytes());
bytes.push(0x80);
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.extend_from_slice(&0u16.to_le_bytes());
bytes.extend_from_slice(&(FILE_HEAD_BASE_SIZE as u16).to_le_bytes());
bytes.extend_from_slice(&0u32.to_le_bytes());
bytes.push(0x20);
bytes.push(0);
bytes.push(DEFAULT_UNP_VER);
bytes.push(10);
bytes.push(METHOD_STORE);
assert!(matches!(
Archive::parse(&bytes),
Err(Error::InvalidHeader(
"RAR 1.3 file header is shorter than its name"
))
));
}
#[test]
fn rejects_truncated_file_payload_during_parse() {
let input = [StoredEntry {
name: b"hello.txt",
data: b"hello",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let mut bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
bytes.pop();
assert!(matches!(Archive::parse(&bytes), Err(Error::TooShort)));
}
#[test]
fn returns_none_for_absent_archive_comment() {
let bytes = write_stored_archive(&[], WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.archive_comment().unwrap(), None);
}
#[test]
fn rejects_normal_extract_on_split_entries() {
let entry = StoredEntry {
name: b"split.bin",
data: b"abcdefghijklmnopqrstuvwxyz",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let volumes = write_stored_volumes(entry, WriterOptions::default(), 8).unwrap();
let first = Archive::parse(&volumes[0]).unwrap();
assert_eq!(
collect_extract(&first, None),
Err(Error::InvalidHeader(
"RAR 1.3 split entry requires multivolume extraction"
))
);
assert_eq!(
collect_extract(&first, None),
Err(Error::InvalidHeader(
"RAR 1.3 split entry requires multivolume extraction"
))
);
}
#[test]
fn rejects_malformed_comment_extensions() {
let packed_too_short = Archive {
sfx_offset: 0,
main: MainHeader {
flags: MHD_COMMENT | MHD_PACK_COMMENT,
head_size: MAIN_HEAD_SIZE,
extra: 1u16.to_le_bytes().to_vec(),
},
entries: Vec::new(),
source: ArchiveSource::Memory(Arc::new([])),
};
assert_eq!(
packed_too_short.archive_comment(),
Err(Error::InvalidHeader(
"RAR 1.3 packed archive comment is shorter than size field"
))
);
let unpacked_too_short = Archive {
sfx_offset: 0,
main: MainHeader {
flags: MHD_COMMENT,
head_size: MAIN_HEAD_SIZE,
extra: 4u16.to_le_bytes().to_vec(),
},
entries: Vec::new(),
source: ArchiveSource::Memory(Arc::new([])),
};
assert_eq!(unpacked_too_short.archive_comment(), Err(Error::TooShort));
}
#[test]
fn rejects_malformed_av_extensions() {
let too_short = Archive {
sfx_offset: 0,
main: MainHeader {
flags: MHD_AV,
head_size: MAIN_HEAD_SIZE,
extra: 5u16.to_le_bytes().to_vec(),
},
entries: Vec::new(),
source: ArchiveSource::Memory(Arc::new([])),
};
assert_eq!(
too_short.authenticity_verification(),
Err(Error::InvalidHeader("RAR 1.3 AV payload is too short"))
);
let bad_prefix = Archive {
sfx_offset: 0,
main: MainHeader {
flags: MHD_AV,
head_size: MAIN_HEAD_SIZE,
extra: {
let mut extra = 6u16.to_le_bytes().to_vec();
extra.extend_from_slice(b"badbad");
extra
},
},
entries: Vec::new(),
source: ArchiveSource::Memory(Arc::new([])),
};
assert_eq!(
bad_prefix.authenticity_verification(),
Err(Error::InvalidHeader("RAR 1.3 AV prefix mismatch"))
);
}
#[test]
fn writes_and_reads_encrypted_stored_archive() {
let input = [StoredEntry {
name: b"secret.txt",
data: b"secret bytes",
file_time: 0,
file_attr: 0x20,
password: Some(b"pass"),
file_comment: None,
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.entries[0].is_encrypted());
match collect_extract(&archive, None) {
Err(Error::NeedPassword) => {}
Err(Error::AtEntry { source, .. }) if matches!(*source, Error::NeedPassword) => {}
other => panic!("expected missing password error, got {other:?}"),
}
let extracted = collect_extract(&archive, Some(b"pass")).unwrap();
assert_eq!(extracted[0].data, b"secret bytes");
}
#[test]
fn writes_and_reads_archive_comment() {
let input = [StoredEntry {
name: b"README.md",
data: b"hello rar 1.3",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_stored_archive_with_comment(
&input,
WriterOptions::default(),
Some(b"This is an archive comment."),
)
.unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.main.has_archive_comment());
assert!(archive.main.has_packed_comment());
assert_eq!(
archive.archive_comment().unwrap().as_deref(),
Some(&b"This is an archive comment."[..])
);
assert_eq!(
collect_extract(&archive, None).unwrap()[0].data,
b"hello rar 1.3"
);
}
#[test]
fn writes_and_reads_file_comment() {
let input = [StoredEntry {
name: b"README.md",
data: b"hello rar 1.3",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: Some(b"file comment\r\n"),
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.entries[0].has_file_comment());
assert_eq!(
archive.entries[0].file_comment().unwrap().as_deref(),
Some(&b"file comment\r\n"[..])
);
assert_eq!(
collect_extract(&archive, None).unwrap()[0].data,
b"hello rar 1.3"
);
}
#[test]
fn writes_and_reads_literal_only_compressed_archive() {
let input = [FileEntry {
name: b"tiny.txt",
data: b"literal bytes over sixteen",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.main.flags, 0x80);
assert_eq!(archive.entries.len(), 1);
assert_eq!(archive.entries[0].name, b"tiny.txt");
assert!(archive.entries[0].is_stored());
assert_eq!(archive.entries[0].header.method, METHOD_STORE);
assert_eq!(
archive.entries[0].header.pack_size,
input[0].data.len() as u32
);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, b"literal bytes over sixteen");
}
#[test]
fn writes_and_reads_literal_only_compressed_archive_with_repeated_stmode() {
let data =
b"this literal-only payload is long enough to enter and exit stmode more than once";
let input = [FileEntry {
name: b"long.txt",
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_BEST);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data);
}
#[test]
fn compressed_writer_levels_control_rar15_encoder_policy() {
let mut data: Vec<_> = (0..5000).map(|index| (index * 73 + 19) as u8).collect();
data.extend_from_within(..256);
let input = [FileEntry {
name: b"level-policy.bin",
data: &data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let level_one =
write_compressed_archive(&input, WriterOptions::default().with_compression_level(1))
.unwrap();
let level_five =
write_compressed_archive(&input, WriterOptions::default().with_compression_level(5))
.unwrap();
let level_one = Archive::parse(&level_one).unwrap();
let level_five = Archive::parse(&level_five).unwrap();
let level_one_file = &level_one.entries[0];
let level_five_file = &level_five.entries[0];
assert_eq!(level_one_file.header.method, METHOD_BEST);
assert_eq!(level_five_file.header.method, METHOD_BEST);
assert!(level_five_file.header.pack_size < level_one_file.header.pack_size);
assert_eq!(collect_extract(&level_one, None).unwrap()[0].data, data);
assert_eq!(collect_extract(&level_five, None).unwrap()[0].data, data);
}
#[test]
fn rar14_writer_uses_dos_compatible_old_distance_policy() {
for level in 0..=5 {
let options = rar15_encode_options_for_level(Some(level)).unwrap();
assert!(
!options.old_distance_tokens_enabled(),
"RAR 1.4 level {level} must not emit old-distance tokens"
);
}
}
#[test]
fn compressed_writer_keeps_adaptive_lz_planning_in_sync_after_literals() {
let data = synthetic_log_payload(8000);
let input = [FileEntry {
name: b"synthetic.log",
data: &data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes =
write_compressed_archive(&input, WriterOptions::default().with_compression_level(2))
.unwrap();
let archive = Archive::parse(&bytes).unwrap();
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data);
}
#[test]
fn compressed_writer_emits_short_lz_matches() {
let data = b"abcabcabcabcabcabcabcabcabcabcabcabc";
let input = [FileEntry {
name: b"repeat.txt",
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_BEST);
assert!(
archive.entries[0].header.pack_size < data.len() as u32,
"ShortLZ should make the repeated payload smaller than stored data"
);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data);
}
#[test]
fn compressed_writer_emits_long_lz_matches() {
let mut data = short_lz_resistant_prefix(300);
data.extend_from_within(..32);
assert_eq!(
find_long_lz(&data, 300, 0x8000),
Some(LongLz {
distance: 300,
length: 32
})
);
let input = [FileEntry {
name: b"far.txt",
data: &data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let literal_only = Unpack15Encoder::new()
.encode_literals_only(&data)
.unwrap()
.len();
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_BEST);
assert!(
(archive.entries[0].header.pack_size as usize) < literal_only,
"LongLZ should make a >256-byte-distance repeat smaller than literal-only output"
);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data);
}
#[test]
fn compressed_writer_stores_incompressible_member_when_smaller() {
let mut state = 0x8765_4321u32;
let data: Vec<_> = (0..8192)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state as u8
})
.collect();
let input = [FileEntry {
name: b"randomish.bin",
data: &data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_STORE);
assert_eq!(archive.entries[0].header.pack_size, data.len() as u32);
assert_eq!(collect_extract(&archive, None).unwrap()[0].data, data);
}
#[test]
fn compressed_writer_stores_tiny_incompressible_member_when_smaller() {
let data = b"\x00\xff\x12\xed\x34\xcb\x56\xa9\x78\x87\x9a\x65\xbc\x43\xde\x21";
let input = [FileEntry {
name: b"tiny.bin",
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_STORE);
assert_eq!(archive.entries[0].header.pack_size, data.len() as u32);
assert_eq!(collect_extract(&archive, None).unwrap()[0].data, data);
}
#[test]
fn writes_and_reads_solid_compressed_archive() {
let input = [
FileEntry {
name: b"first.txt",
data: b"first member primes the adaptive unpack15 state",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
FileEntry {
name: b"second.txt",
data: b"second member is encoded without resetting that state",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
];
let mut features = FeatureSet::store_only();
features.solid = true;
let options = WriterOptions {
target: ArchiveVersion::Rar14,
features,
..WriterOptions::default()
};
let bytes = write_compressed_archive(&input, options).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.main.is_solid());
assert_eq!(archive.entries.len(), 2);
assert!(archive
.entries
.iter()
.all(|entry| entry.header.flags & LHD_SOLID != 0));
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, input[0].data);
assert_eq!(extracted[1].data, input[1].data);
}
#[test]
fn writes_and_reads_encrypted_compressed_archive() {
let input = [FileEntry {
name: b"secret.txt",
data: b"secret compressed bytes over sixteen",
file_time: 0,
file_attr: 0x20,
password: Some(b"pass"),
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.entries[0].is_encrypted());
assert_eq!(archive.entries[0].header.method, METHOD_STORE);
assert!(matches!(
collect_extract(&archive, None),
Err(Error::NeedPassword)
));
let extracted = collect_extract(&archive, Some(b"pass")).unwrap();
assert_eq!(extracted[0].data, input[0].data);
}
#[test]
fn writes_and_reads_compressed_file_comment() {
let input = [FileEntry {
name: b"commented.txt",
data: b"compressed member with file comment",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: Some(b"compressed file comment"),
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(
archive.entries[0].file_comment().unwrap().as_deref(),
Some(&b"compressed file comment"[..])
);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, input[0].data);
}
#[test]
fn writes_and_reads_stored_multivolume_archive() {
let entry = StoredEntry {
name: b"random.bin",
data: b"abcdefghijklmnopqrstuvwxyz0123456789",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let bytes = write_stored_volumes(entry, WriterOptions::default(), 10).unwrap();
assert_eq!(bytes.len(), 4);
let volumes: Vec<_> = bytes
.iter()
.map(|bytes| Archive::parse(bytes).unwrap())
.collect();
assert!(volumes.iter().all(|archive| archive.main.is_volume()));
assert!(!volumes[0].entries[0].is_split_before());
assert!(volumes[0].entries[0].is_split_after());
assert!(volumes[1].entries[0].is_split_before());
assert!(volumes[1].entries[0].is_split_after());
assert!(volumes[3].entries[0].is_split_before());
assert!(!volumes[3].entries[0].is_split_after());
assert!(volumes.iter().all(|archive| archive.entries[0].is_stored()));
let extracted = collect_extract_volumes(&volumes, None).unwrap();
assert_eq!(extracted.len(), 1);
assert_eq!(extracted[0].name, b"random.bin");
assert_eq!(extracted[0].data, entry.data);
}
#[test]
fn writes_and_reads_compressed_multivolume_archive() {
let data = b"abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc";
let entry = FileEntry {
name: b"repeat.txt",
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let bytes = write_compressed_volumes(entry, WriterOptions::default(), 8).unwrap();
assert!(bytes.len() >= 2);
let volumes: Vec<_> = bytes
.iter()
.map(|bytes| Archive::parse(bytes).unwrap())
.collect();
assert!(volumes.iter().all(|archive| archive.main.is_volume()));
assert!(!volumes[0].entries[0].is_stored());
assert!(volumes[0].entries[0].is_split_after());
assert!(volumes.last().unwrap().entries[0].is_split_before());
assert!(!volumes.last().unwrap().entries[0].is_split_after());
let extracted = collect_extract_volumes(&volumes, None).unwrap();
assert_eq!(extracted.len(), 1);
assert_eq!(extracted[0].name, b"repeat.txt");
assert_eq!(extracted[0].data, data);
}
fn short_lz_resistant_prefix(len: usize) -> Vec<u8> {
let mut data = Vec::with_capacity(len);
while data.len() < len {
let next = (0u8..=u8::MAX)
.find(|&candidate| {
if data.len() < 2 {
return true;
}
let start = data.len().saturating_sub(256);
!data[start..].windows(3).any(|window| {
window == [data[data.len() - 2], data[data.len() - 1], candidate]
})
})
.expect("byte alphabet can avoid local 3-byte repeats");
data.push(next);
}
data
}
#[test]
fn writes_empty_compressed_archive_member() {
let input = [FileEntry {
name: b"empty.bin",
data: b"",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_compressed_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert_eq!(archive.entries[0].header.method, METHOD_STORE);
assert_eq!(archive.entries[0].header.pack_size, 0);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, b"");
}
#[test]
fn rejects_rar5_only_features_for_rar13() {
let mut features = FeatureSet::store_only();
features.quick_open = true;
let options = WriterOptions {
target: ArchiveVersion::Rar13,
features,
..WriterOptions::default()
};
let err = write_stored_archive(&[], options).unwrap_err();
assert_eq!(
err,
Error::UnsupportedFeature {
version: ArchiveVersion::Rar13,
feature: "quick_open"
}
);
}
#[test]
fn rejects_unimplemented_rar13_writer_features() {
let mut features = FeatureSet::store_only();
features.sfx = true;
let options = WriterOptions {
target: ArchiveVersion::Rar14,
features,
..WriterOptions::default()
};
let err = write_stored_archive(&[], options).unwrap_err();
assert_eq!(
err,
Error::UnsupportedFeature {
version: ArchiveVersion::Rar14,
feature: "sfx"
}
);
}
#[test]
fn file_checksum_matches_rar13_algorithm() {
assert_eq!(file_checksum(b""), 0x0000);
assert_eq!(file_checksum(b"123456789"), 0xc78a);
}
#[test]
fn rar13_checksum_writer_flush_propagates_to_inner_writer() {
struct FlushSpy {
data: Vec<u8>,
flushed: usize,
}
impl Write for FlushSpy {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.data.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.flushed += 1;
Ok(())
}
}
let mut inner = FlushSpy {
data: Vec::new(),
flushed: 0,
};
let mut checksum = Rar13Checksum::new();
let mut writer = Rar13ChecksumWriter {
inner: &mut inner,
checksum: &mut checksum,
};
writer.write_all(b"hello").unwrap();
writer.flush().unwrap();
assert_eq!(inner.data, b"hello");
assert_eq!(inner.flushed, 1);
}
#[test]
fn entry_packed_data_returns_borrowed_slice_for_memory_archives() {
let payload = b"packed_data direct accessor coverage";
let input = [StoredEntry {
name: b"slice.bin",
data: payload,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
let entry = &archive.entries[0];
let packed = entry.packed_data(&archive).unwrap();
assert_eq!(packed, payload);
assert!(!packed.is_empty());
}
#[test]
fn extract_volumes_to_annotates_failed_non_split_entry_with_at_entry() {
let payload = b"corrupt-me-please";
let input = [StoredEntry {
name: b"plain.bin",
data: payload,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let mut bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
let range = archive.entries[0].packed_range.clone();
bytes[range.start] ^= 0xff;
let corrupted = Archive::parse(&bytes).unwrap();
let err = collect_extract_volumes(std::slice::from_ref(&corrupted), None).unwrap_err();
match err {
Error::AtEntry {
name,
operation,
source,
} => {
assert_eq!(name, b"plain.bin");
assert_eq!(operation, "extracting");
assert!(matches!(*source, Error::CrcMismatch { .. }));
}
other => panic!("expected AtEntry annotation, got {other:?}"),
}
}
#[test]
fn extract_volumes_to_annotates_failed_split_completion_with_at_entry() {
let entry = StoredEntry {
name: b"split.bin",
data: b"abcdefghijklmnopqrstuvwxyz0123456789",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let mut volume_bytes = write_stored_volumes(entry, WriterOptions::default(), 10).unwrap();
assert!(
volume_bytes.len() >= 2,
"need at least two volumes to exercise the split-completion path"
);
let last_index = volume_bytes.len() - 1;
let last_archive = Archive::parse(&volume_bytes[last_index]).unwrap();
let last_range = last_archive.entries[0].packed_range.clone();
volume_bytes[last_index][last_range.start] ^= 0x7f;
let volumes: Vec<_> = volume_bytes
.iter()
.map(|bytes| Archive::parse(bytes).unwrap())
.collect();
let err = collect_extract_volumes(&volumes, None).unwrap_err();
match err {
Error::AtEntry {
name,
operation,
source,
} => {
assert_eq!(name, b"split.bin");
assert_eq!(operation, "extracting");
assert!(
matches!(*source, Error::CrcMismatch { .. }),
"expected CrcMismatch source, got {source:?}"
);
}
other => panic!("expected AtEntry annotation, got {other:?}"),
}
}
#[test]
fn entry_packed_data_refuses_to_buffer_file_backed_archives() {
let payload = b"packed_data refuses file-backed";
let input = [StoredEntry {
name: b"file.bin",
data: payload,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let dir =
std::env::temp_dir().join(format!("rars-rar13-packed-data-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("entry.rar");
std::fs::write(&path, &bytes).unwrap();
let archive = Archive::parse_path(&path).unwrap();
let result = archive.entries[0].packed_data(&archive);
assert_eq!(
result,
Err(Error::InvalidHeader(
"RAR 1.3 file-backed packed data requires owned read"
))
);
std::fs::remove_file(&path).ok();
std::fs::remove_dir(&dir).ok();
}
fn parse_volumes(bytes: &[Vec<u8>]) -> Vec<Archive> {
bytes.iter().map(|b| Archive::parse(b).unwrap()).collect()
}
fn split_volumes_for(name: &[u8], data: &[u8]) -> Vec<Vec<u8>> {
write_stored_volumes(
StoredEntry {
name,
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
WriterOptions::default(),
10,
)
.unwrap()
}
#[test]
fn extract_volumes_to_rejects_pending_split_interrupted_by_regular_entry() {
let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
let mut volumes = parse_volumes(&bytes);
let mut intruder = volumes[0].entries[0].clone();
intruder.header.flags &= !(LHD_SPLIT_BEFORE | LHD_SPLIT_AFTER);
intruder.name = b"intruder.bin".to_vec();
volumes[0].entries.push(intruder);
let err = collect_extract_volumes(&volumes, None).unwrap_err();
assert_eq!(
err,
Error::InvalidHeader("RAR 1.3 split entry is interrupted by a regular entry"),
);
}
#[test]
fn extract_volumes_to_rejects_split_with_inconsistent_flags() {
let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
let volumes = parse_volumes(&bytes);
let middle = volumes.into_iter().nth(1).unwrap();
let err = collect_extract_volumes(std::slice::from_ref(&middle), None).unwrap_err();
assert_eq!(
err,
Error::InvalidHeader("RAR 1.3 split entry flags are inconsistent"),
);
}
#[test]
fn extract_volumes_to_rejects_pending_split_left_incomplete_at_end() {
let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
let volumes = parse_volumes(&bytes);
let err = collect_extract_volumes(std::slice::from_ref(&volumes[0]), None).unwrap_err();
assert_eq!(
err,
Error::InvalidHeader("RAR 1.3 split entry is incomplete")
);
}
#[test]
fn extract_volumes_to_rejects_split_fragments_with_drifted_attributes() {
let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
let mut volumes = parse_volumes(&bytes);
volumes[1].entries[0].name = b"different.bin".to_vec();
let err = collect_extract_volumes(&volumes, None).unwrap_err();
assert_eq!(
err,
Error::InvalidHeader("RAR 1.3 split entry name changed")
);
}
#[test]
fn extract_volumes_to_rejects_split_fragments_with_drifted_method() {
let bytes = split_volumes_for(b"split.bin", b"abcdefghijklmnopqrstuvwxyz");
let mut volumes = parse_volumes(&bytes);
volumes[1].entries[0].header.method = METHOD_BEST;
let err = collect_extract_volumes(&volumes, None).unwrap_err();
assert_eq!(
err,
Error::InvalidHeader("RAR 1.3 split entry compression method changed"),
);
}
#[test]
fn extract_volumes_to_carries_directory_entries_across_volume_array() {
let input = [StoredEntry {
name: b"docs",
data: b"",
file_time: 0,
file_attr: 0x10,
password: None,
file_comment: None,
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let archive = Archive::parse(&bytes).unwrap();
let extracted = collect_extract_volumes(std::slice::from_ref(&archive), None).unwrap();
assert_eq!(extracted.len(), 1);
assert!(extracted[0].is_directory);
assert_eq!(extracted[0].name, b"docs");
}
#[test]
fn extract_volumes_to_routes_pending_split_reader_through_fragment_chain() {
let payload: Vec<u8> = (0..96).map(|i| ((i * 53) ^ 0xa5) as u8).collect();
let bytes = split_volumes_for(b"chain.bin", &payload);
assert!(
bytes.len() >= 3,
"need at least three volumes for the chain"
);
let volumes = parse_volumes(&bytes);
let extracted = collect_extract_volumes(&volumes, None).unwrap();
assert_eq!(extracted.len(), 1);
assert_eq!(extracted[0].data, payload);
}
#[test]
fn write_compressed_archive_with_comment_round_trips_through_archive_comment() {
let data = b"compressed archive comment payload payload payload";
let comment = b"This is a compressed archive comment.";
let input = [FileEntry {
name: b"payload.txt",
data,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes =
write_compressed_archive_with_comment(&input, WriterOptions::default(), Some(comment))
.unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.main.has_archive_comment());
assert!(archive.main.has_packed_comment());
assert_eq!(
archive.archive_comment().unwrap().as_deref(),
Some(&comment[..])
);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data);
}
#[test]
fn write_compressed_archive_with_comment_emits_solid_compressed_archive() {
let data1 = b"solid compressed payload one with overlap overlap overlap";
let data2 = b"solid compressed payload two with overlap overlap overlap";
let mut features = FeatureSet::store_only();
features.solid = true;
let options = WriterOptions {
target: ArchiveVersion::Rar14,
features,
..WriterOptions::default()
};
let input = [
FileEntry {
name: b"a.txt",
data: data1,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
FileEntry {
name: b"b.txt",
data: data2,
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
];
let bytes = write_compressed_archive_with_comment(&input, options, None).unwrap();
let archive = Archive::parse(&bytes).unwrap();
assert!(archive.main.is_solid());
assert_eq!(archive.entries.len(), 2);
let extracted = collect_extract(&archive, None).unwrap();
assert_eq!(extracted[0].data, data1);
assert_eq!(extracted[1].data, data2);
}
#[test]
fn write_compressed_archive_with_comment_rejects_non_rar13_target() {
let options = WriterOptions {
target: ArchiveVersion::Rar15,
..WriterOptions::default()
};
let err = write_compressed_archive_with_comment(&[], options, None).unwrap_err();
assert_eq!(err, Error::UnsupportedVersion(ArchiveVersion::Rar15));
}
#[test]
fn parse_path_round_trips_multi_entry_archive_via_file_backed_seekable_path() {
let input = [
StoredEntry {
name: b"first.txt",
data: b"first payload",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
StoredEntry {
name: b"second.txt",
data: b"second payload",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
},
];
let bytes = write_stored_archive_with_comment(
&input,
WriterOptions::default(),
Some(b"file-backed comment"),
)
.unwrap();
let dir =
std::env::temp_dir().join(format!("rars-rar13-parse-seekable-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("multi.rar");
std::fs::write(&path, &bytes).unwrap();
let archive = Archive::parse_path(&path).unwrap();
assert_eq!(archive.entries.len(), 2);
assert_eq!(archive.entries[0].name, b"first.txt");
assert_eq!(archive.entries[1].name, b"second.txt");
assert_eq!(
archive.archive_comment().unwrap().as_deref(),
Some(&b"file-backed comment"[..])
);
std::fs::remove_file(&path).ok();
std::fs::remove_dir(&dir).ok();
}
#[test]
fn parse_path_rejects_files_without_rar13_signature() {
let dir =
std::env::temp_dir().join(format!("rars-rar13-parse-path-bad-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("not_a_rar.bin");
std::fs::write(&path, [0u8; 64]).unwrap();
let err = Archive::parse_path(&path).unwrap_err();
assert_eq!(err, Error::UnsupportedSignature);
std::fs::remove_file(&path).ok();
std::fs::remove_dir(&dir).ok();
}
#[test]
fn extract_to_encrypted_archive_reads_through_file_backed_decrypted_range() {
let input = [StoredEntry {
name: b"secret.bin",
data: b"file-backed secret payload",
file_time: 0,
file_attr: 0x20,
password: Some(b"pw"),
file_comment: None,
}];
let bytes = write_stored_archive(&input, WriterOptions::default()).unwrap();
let dir =
std::env::temp_dir().join(format!("rars-rar13-decrypt-file-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("encrypted.rar");
std::fs::write(&path, &bytes).unwrap();
let archive = Archive::parse_path(&path).unwrap();
let extracted = collect_extract(&archive, Some(b"pw")).unwrap();
assert_eq!(extracted[0].data, b"file-backed secret payload");
std::fs::remove_file(&path).ok();
std::fs::remove_dir(&dir).ok();
}
#[test]
fn write_stored_volumes_rejects_password_protected_entries() {
let entry = StoredEntry {
name: b"locked.bin",
data: b"data",
file_time: 0,
file_attr: 0x20,
password: Some(b"pw"),
file_comment: None,
};
let err = write_stored_volumes(entry, WriterOptions::default(), 16).unwrap_err();
assert_eq!(
err,
Error::UnsupportedFeature {
version: ArchiveVersion::Rar14,
feature: "volume_password",
}
);
}
#[test]
fn write_compressed_volumes_rejects_archive_comment_feature() {
let mut features = FeatureSet::store_only();
features.archive_comment = true;
let entry = FileEntry {
name: b"with-comment.bin",
data: b"data",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let err = write_compressed_volumes(
entry,
WriterOptions {
target: ArchiveVersion::Rar14,
features,
..WriterOptions::default()
},
16,
)
.unwrap_err();
assert_eq!(
err,
Error::UnsupportedFeature {
version: ArchiveVersion::Rar14,
feature: "volume_archive_comment",
}
);
}
#[test]
fn write_compressed_volumes_rejects_non_rar13_target() {
let options = WriterOptions {
target: ArchiveVersion::Rar20,
..WriterOptions::default()
};
let entry = FileEntry {
name: b"x.bin",
data: b"data",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
};
let err = write_compressed_volumes(entry, options, 16).unwrap_err();
assert_eq!(err, Error::UnsupportedVersion(ArchiveVersion::Rar20));
}
#[test]
fn file_header_parse_rejects_input_below_base_size() {
let err = FileHeader::parse(&[0u8; FILE_HEAD_BASE_SIZE - 1]).unwrap_err();
assert_eq!(err, Error::TooShort);
}
#[test]
fn file_header_parse_rejects_truncated_input_against_declared_head_size() {
let mut header = [0u8; FILE_HEAD_BASE_SIZE];
let declared_head_size: u16 = (FILE_HEAD_BASE_SIZE + 32) as u16;
header[10..12].copy_from_slice(&declared_head_size.to_le_bytes());
header[19] = 0;
let err = FileHeader::parse(&header).unwrap_err();
assert_eq!(err, Error::TooShort);
}
#[test]
fn archive_comment_rejects_size_field_shorter_than_two_bytes() {
let input = [StoredEntry {
name: b"file.bin",
data: b"data",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes =
write_stored_archive_with_comment(&input, WriterOptions::default(), Some(b"hi"))
.unwrap();
let mut archive = Archive::parse(&bytes).unwrap();
archive.main.extra[0] = 1;
archive.main.extra[1] = 0;
assert_eq!(
archive.archive_comment(),
Err(Error::InvalidHeader(
"RAR 1.3 packed archive comment is shorter than size field"
))
);
}
#[test]
fn archive_comment_rejects_packed_payload_extending_past_extra_buffer() {
let input = [StoredEntry {
name: b"file.bin",
data: b"data",
file_time: 0,
file_attr: 0x20,
password: None,
file_comment: None,
}];
let bytes =
write_stored_archive_with_comment(&input, WriterOptions::default(), Some(b"hi"))
.unwrap();
let mut archive = Archive::parse(&bytes).unwrap();
let inflated = (archive.main.extra.len() as u16 + 16).to_le_bytes();
archive.main.extra[0] = inflated[0];
archive.main.extra[1] = inflated[1];
assert_eq!(archive.archive_comment(), Err(Error::TooShort));
}
}