use super::*;
use crate::io_util::align16 as checked_align16;
use crate::x86_filter_scan::auto_x86_filter_ranges;
use rars_codec::rar13::{
unpack15_encode_with_options, EncodeOptions as Rar15EncodeOptions, Unpack15Encoder,
};
use rars_codec::rar20::{
unpack20_encode_auto_with_options, EncodeOptions as Rar20EncodeOptions, Unpack20Encoder,
};
use rars_codec::rar29::{
unpack29_encode_literals, unpack29_encode_literals_with_options, unpack29_encode_ppmd,
unpack29_encode_ppmd_with_filter, EncodeOptions as Rar29EncodeOptions, Unpack29Encoder,
};
pub use rars_codec::rar29::{Rar29FilterKind as FilterKind, Rar29FilterSpec as FilterSpec};
const AUTO_RGB_WIDTHS: [usize; 4] = [24, 48, 96, 192];
const AUTO_DELTA_EDGE_SKIP: usize = 64;
const MIN_STORE_FALLBACK_SIZE: usize = 1024;
const RAR29_LARGE_TEXT_PPMD_THRESHOLD: usize = 16 * 1024 * 1024;
const RAR29_TEXT_SAMPLE_SIZE: usize = 8192;
const RAR29_AUDIO_SAMPLE_SIZE: usize = 8192;
const RAR29_LZ_BLOCK_SIZE: usize = 1024 * 1024;
const RAR15_ALIGN_OVERFLOW: &str = "RAR 1.5 block size overflows usize";
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>> {
let has_file_comment = entries.iter().any(|entry| entry.file_comment.is_some());
validate_stored_writer_options(options, archive_comment.is_some(), has_file_comment)?;
if options.features.header_encryption {
return write_header_encrypted_stored_archive(entries, options, archive_comment);
}
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
write_main_header(
&mut out,
if archive_comment.is_some() && uses_old_style_archive_comment(options.target) {
MHD_COMMENT
} else {
0
},
);
write_archive_comment(&mut out, archive_comment, options.target)?;
for entry in entries {
write_stored_entry(&mut out, entry, options)?;
}
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>> {
let has_file_comment = entries.iter().any(|entry| entry.file_comment.is_some());
validate_compressed_writer_options(options, archive_comment.is_some(), has_file_comment)?;
if options.features.header_encryption {
return write_header_encrypted_compressed_archive(entries, options, archive_comment);
}
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
let mut main_flags = if options.features.solid { MHD_SOLID } else { 0 };
if archive_comment.is_some() && uses_old_style_archive_comment(options.target) {
main_flags |= MHD_COMMENT;
}
write_main_header(&mut out, main_flags);
write_archive_comment(&mut out, archive_comment, options.target)?;
let mut solid_encoder = SolidEncoder::for_target(options, options.features.solid)?;
let mut solid_run_has_member = false;
for entry in entries {
let payload = encode_or_store_payload(entry.data, options, &mut solid_encoder)?;
let solid_continuation =
options.features.solid && payload.method != 0x30 && solid_run_has_member;
write_compressed_entry(
&mut out,
entry,
&payload.data,
payload.method,
options.target,
dictionary_flags_for_options(options)?,
solid_continuation,
)?;
if options.features.solid {
solid_run_has_member = payload.method != 0x30;
}
}
Ok(out)
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum FilterPolicy {
Lz,
Auto,
Explicit(FilterSpec),
Ppmd,
PpmdFiltered(FilterSpec),
}
pub fn write_rar29_compressed_archive_with_filter_policy(
entries: &[FileEntry<'_>],
options: WriterOptions,
policy: FilterPolicy,
) -> Result<Vec<u8>> {
validate_rar29_filter_policy(&policy)?;
let encode_options = rar29_encode_options_for_options(options)?;
let lz_method = compression_method_for_level(options)?;
write_rar29_filtered_archive(entries, options, |entry| {
encode_rar29_policy_filtered_payload(entry.data, &policy, encode_options, lz_method)
})
}
fn encode_rar29_policy_filtered_payload(
data: &[u8],
policy: &FilterPolicy,
options: Rar29EncodeOptions,
lz_method: u8,
) -> Result<EncodedPayload> {
if lz_method == 0x30 {
return Ok(EncodedPayload {
data: data.to_vec(),
method: 0x30,
});
}
match policy {
FilterPolicy::Lz => encode_rar29_lz_member(data, options, lz_method),
FilterPolicy::Auto => encode_rar29_auto_filtered_member(data, options, lz_method, true),
FilterPolicy::Explicit(filter) => Ok(EncodedPayload {
data: encode_rar29_filtered_member(data, filter.clone(), options)?,
method: lz_method,
}),
FilterPolicy::Ppmd => Ok(EncodedPayload {
data: unpack29_encode_ppmd(data).map_err(Error::from)?,
method: 0x35,
}),
FilterPolicy::PpmdFiltered(filter) => Ok(EncodedPayload {
data: unpack29_encode_ppmd_with_filter(data, filter.clone()).map_err(Error::from)?,
method: 0x35,
}),
}
}
fn validate_rar29_filter_policy(policy: &FilterPolicy) -> Result<()> {
let filter = match policy {
FilterPolicy::Explicit(filter) | FilterPolicy::PpmdFiltered(filter) => filter,
FilterPolicy::Lz | FilterPolicy::Auto | FilterPolicy::Ppmd => return Ok(()),
};
match filter.kind {
FilterKind::Delta { channels } => {
if channels == 0 || channels > 32 {
return Err(Error::InvalidHeader(
"RAR 2.9 DELTA filter channel count is invalid",
));
}
}
FilterKind::Audio { channels } => {
if channels == 0 || channels > 32 {
return Err(Error::InvalidHeader(
"RAR 2.9 AUDIO filter channel count is invalid",
));
}
}
FilterKind::Rgb { width, pos_r } => {
if width == 0 || !width.is_multiple_of(3) || pos_r > 2 {
return Err(Error::InvalidHeader(
"RAR 2.9 RGB filter parameters are invalid",
));
}
}
FilterKind::E8 | FilterKind::E8E9 | FilterKind::Itanium => {}
}
Ok(())
}
fn encode_rar29_lz_member(
data: &[u8],
options: Rar29EncodeOptions,
method: u8,
) -> Result<EncodedPayload> {
let compressed = unpack29_encode_literals_with_options(data, options).map_err(Error::from)?;
if compressed.len() >= data.len() {
return Ok(EncodedPayload {
data: data.to_vec(),
method: 0x30,
});
}
Ok(EncodedPayload {
data: compressed,
method,
})
}
fn encode_rar29_filtered_member(
data: &[u8],
filter: FilterSpec,
options: Rar29EncodeOptions,
) -> Result<Vec<u8>> {
Unpack29Encoder::with_options(options)
.encode_member_with_filter(data, filter)
.map_err(Error::from)
}
fn encode_rar29_filtered_members(
data: &[u8],
filters: &[FilterSpec],
options: Rar29EncodeOptions,
) -> Result<Vec<u8>> {
Unpack29Encoder::with_options(options)
.encode_member_with_filters(data, filters)
.map_err(Error::from)
}
fn encode_rar29_auto_filtered_member(
data: &[u8],
options: Rar29EncodeOptions,
lz_method: u8,
include_ppmd: bool,
) -> Result<EncodedPayload> {
if data.is_empty() {
return Ok(EncodedPayload {
data: Vec::new(),
method: 0x30,
});
}
if include_ppmd && is_large_text_ppmd_candidate(data) {
return Ok(EncodedPayload {
data: unpack29_encode_ppmd(data).map_err(Error::from)?,
method: 0x35,
});
}
let mut best = EncodedPayload {
data: unpack29_encode_literals_with_options(data, options).map_err(Error::from)?,
method: lz_method,
};
if include_ppmd && data.len() <= 1024 * 1024 && is_text_ppmd_candidate(data) {
let ppmd = EncodedPayload {
data: unpack29_encode_ppmd(data).map_err(Error::from)?,
method: 0x35,
};
if ppmd.data.len() < best.data.len() {
best = ppmd;
}
return Ok(best);
}
if is_text_ppmd_candidate(data) {
return Ok(best);
}
let mut candidates = Vec::new();
if include_ppmd && is_auto_ppmd_candidate(data) {
candidates.push(EncodedPayload {
data: unpack29_encode_ppmd(data).map_err(Error::from)?,
method: 0x35,
});
}
candidates.extend([
EncodedPayload {
data: encode_rar29_filtered_member(data, FilterSpec::whole(FilterKind::E8), options)?,
method: lz_method,
},
EncodedPayload {
data: encode_rar29_filtered_member(data, FilterSpec::whole(FilterKind::E8E9), options)?,
method: lz_method,
},
EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::whole(FilterKind::Itanium),
options,
)?,
method: lz_method,
},
]);
let e8_candidates = auto_x86_filter_ranges(data, false);
for range in e8_candidates.iter().cloned() {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::range(FilterKind::E8, range),
options,
)?,
method: lz_method,
});
}
let e8_ranges = disjoint_filter_ranges(e8_candidates);
if e8_ranges.len() > 1 {
let filters: Vec<_> = e8_ranges
.into_iter()
.map(|range| FilterSpec::range(FilterKind::E8, range))
.collect();
candidates.push(EncodedPayload {
data: encode_rar29_filtered_members(data, &filters, options)?,
method: lz_method,
});
}
let e8e9_candidates = auto_x86_filter_ranges(data, true);
for range in e8e9_candidates.iter().cloned() {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::range(FilterKind::E8E9, range),
options,
)?,
method: lz_method,
});
}
let e8e9_ranges = disjoint_filter_ranges(e8e9_candidates);
if e8e9_ranges.len() > 1 {
let filters: Vec<_> = e8e9_ranges
.into_iter()
.map(|range| FilterSpec::range(FilterKind::E8E9, range))
.collect();
candidates.push(EncodedPayload {
data: encode_rar29_filtered_members(data, &filters, options)?,
method: lz_method,
});
}
for channels in 1..=4 {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::whole(FilterKind::Delta { channels }),
options,
)?,
method: lz_method,
});
if is_audio_filter_candidate(data, channels) {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::whole(FilterKind::Audio { channels }),
options,
)?,
method: lz_method,
});
}
}
for channels in 1..=4 {
if let Some(range) = auto_delta_filter_range(data, channels) {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::range(FilterKind::Delta { channels }, range),
options,
)?,
method: lz_method,
});
}
}
for width in AUTO_RGB_WIDTHS {
if data.len() >= width {
candidates.push(EncodedPayload {
data: encode_rar29_filtered_member(
data,
FilterSpec::whole(FilterKind::Rgb { width, pos_r: 0 }),
options,
)?,
method: lz_method,
});
}
}
for candidate in candidates {
if candidate.data.len() < best.data.len() {
best = candidate;
}
}
if best.data.len() >= data.len() {
return Ok(EncodedPayload {
data: data.to_vec(),
method: 0x30,
});
}
Ok(best)
}
fn is_large_text_ppmd_candidate(data: &[u8]) -> bool {
data.len() >= RAR29_LARGE_TEXT_PPMD_THRESHOLD && is_text_ppmd_candidate(data)
}
fn is_auto_ppmd_candidate(data: &[u8]) -> bool {
is_text_ppmd_candidate(data)
}
fn is_text_ppmd_candidate(data: &[u8]) -> bool {
let mut printable = 0usize;
let mut nul = 0usize;
let mut total = 0usize;
for start in text_sample_offsets(data.len()) {
let end = start.saturating_add(RAR29_TEXT_SAMPLE_SIZE).min(data.len());
for &byte in &data[start..end] {
total += 1;
if byte == 0 {
nul += 1;
}
if byte.is_ascii_graphic() || matches!(byte, b'\n' | b'\r' | b'\t' | b' ') {
printable += 1;
}
}
}
total != 0 && nul * 100 <= total && printable * 100 >= total * 85
}
fn text_sample_offsets(len: usize) -> [usize; 3] {
[
0,
len.saturating_sub(RAR29_TEXT_SAMPLE_SIZE) / 2,
len.saturating_sub(RAR29_TEXT_SAMPLE_SIZE),
]
}
fn is_audio_filter_candidate(data: &[u8], channels: usize) -> bool {
if channels == 0 || channels > 4 || data.len() < channels * 64 {
return false;
}
let mut total_delta = 0usize;
let mut small_delta = 0usize;
let mut compared = 0usize;
for start in text_sample_offsets(data.len()) {
let end = start
.saturating_add(RAR29_AUDIO_SAMPLE_SIZE)
.min(data.len());
let aligned_start = start + ((channels - start % channels) % channels);
if aligned_start + channels >= end {
continue;
}
for channel in 0..channels {
let mut previous = None;
let mut index = aligned_start + channel;
while index < end {
let byte = data[index];
if let Some(previous) = previous {
let delta = usize::from(byte.abs_diff(previous));
let delta = delta.min(256 - delta);
total_delta += delta;
small_delta += usize::from(delta <= 8);
compared += 1;
}
previous = Some(byte);
index += channels;
}
}
}
compared != 0 && total_delta <= compared * 24 && small_delta * 100 >= compared * 55
}
fn auto_delta_filter_range(data: &[u8], channels: usize) -> Option<std::ops::Range<usize>> {
if channels == 0 || data.len() <= AUTO_DELTA_EDGE_SKIP * 2 + channels * 8 {
return None;
}
let start = AUTO_DELTA_EDGE_SKIP;
let end = data.len() - AUTO_DELTA_EDGE_SKIP;
let aligned_start = start + ((channels - start % channels) % channels);
let aligned_end = end - (end - aligned_start) % channels;
(aligned_start + channels * 8 <= aligned_end).then_some(aligned_start..aligned_end)
}
fn disjoint_filter_ranges(mut ranges: Vec<std::ops::Range<usize>>) -> Vec<std::ops::Range<usize>> {
ranges.sort_by_key(|range| (range.start, range.end));
let mut disjoint: Vec<std::ops::Range<usize>> = Vec::new();
for range in ranges {
if disjoint.last().is_some_and(|last| range.start < last.end) {
continue;
}
disjoint.push(range);
}
disjoint
}
fn write_rar29_filtered_archive(
entries: &[FileEntry<'_>],
options: WriterOptions,
mut encode: impl FnMut(&FileEntry<'_>) -> Result<EncodedPayload>,
) -> Result<Vec<u8>> {
validate_rar29_filtered_writer_options(options)?;
if options.features.header_encryption {
validate_header_encrypted_archive_options(options, false, options.features.solid)?;
}
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
let main_flags = if options.features.solid { MHD_SOLID } else { 0 }
| if options.features.header_encryption {
MHD_PASSWORD
} else {
0
};
write_main_header(&mut out, main_flags);
let header_password = if options.features.header_encryption {
Some(header_encryption_password(
entries.iter().map(|entry| entry.password),
)?)
} else {
None
};
let mut solid_run_has_member = false;
for entry in entries {
let payload = encode(entry)?;
let solid_continuation =
options.features.solid && payload.method != 0x30 && solid_run_has_member;
if let Some(password) = header_password {
write_header_encrypted_compressed_entry(
&mut out,
entry,
&payload.data,
payload.method,
options,
solid_continuation,
password,
)?;
} else {
write_compressed_entry(
&mut out,
entry,
&payload.data,
payload.method,
options.target,
dictionary_flags_for_options(options)?,
solid_continuation,
)?;
}
solid_run_has_member = payload.method != 0x30;
}
Ok(out)
}
fn write_header_encrypted_stored_archive(
entries: &[StoredEntry<'_>],
options: WriterOptions,
archive_comment: Option<&[u8]>,
) -> Result<Vec<u8>> {
validate_header_encrypted_archive_options(options, archive_comment.is_some(), false)?;
let password = header_encryption_password(entries.iter().map(|entry| entry.password))?;
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
write_main_header(&mut out, MHD_PASSWORD);
for entry in entries {
write_header_encrypted_stored_entry(&mut out, entry, options, password)?;
}
Ok(out)
}
fn write_header_encrypted_compressed_archive(
entries: &[FileEntry<'_>],
options: WriterOptions,
archive_comment: Option<&[u8]>,
) -> Result<Vec<u8>> {
validate_header_encrypted_archive_options(
options,
archive_comment.is_some(),
options.features.solid,
)?;
let password = header_encryption_password(entries.iter().map(|entry| entry.password))?;
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
let main_flags = MHD_PASSWORD | if options.features.solid { MHD_SOLID } else { 0 };
write_main_header(&mut out, main_flags);
let mut solid_encoder = SolidEncoder::for_target(options, options.features.solid)?;
let mut solid_run_has_member = false;
for entry in entries {
let payload = encode_or_store_payload(entry.data, options, &mut solid_encoder)?;
let solid_continuation =
options.features.solid && payload.method != 0x30 && solid_run_has_member;
write_header_encrypted_compressed_entry(
&mut out,
entry,
&payload.data,
payload.method,
options,
solid_continuation,
password,
)?;
if options.features.solid {
solid_run_has_member = payload.method != 0x30;
}
}
Ok(out)
}
pub fn write_stored_volumes(
entry: StoredEntry<'_>,
options: WriterOptions,
max_packed_per_volume: usize,
) -> Result<Vec<Vec<u8>>> {
validate_stored_writer_options(options, false, false)?;
validate_volume_writer_inputs(
entry.name,
entry.data,
entry.password,
entry.file_comment,
options,
)?;
if options.features.header_encryption {
return write_header_encrypted_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: entry.data,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: options.target,
method: 0x30,
dictionary_flags: dictionary_flags_for_options(options)?,
base_flags: writer_file_flags(entry.password, None, false),
main_flags: 0,
password: entry.password,
max_packed_per_volume,
});
}
write_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: entry.data,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: options.target,
method: 0x30,
dictionary_flags: dictionary_flags_for_options(options)?,
base_flags: writer_file_flags(entry.password, None, false),
main_flags: 0,
password: entry.password,
max_packed_per_volume,
})
}
pub fn write_compressed_volumes(
entry: FileEntry<'_>,
options: WriterOptions,
max_packed_per_volume: usize,
) -> Result<Vec<Vec<u8>>> {
validate_compressed_writer_options(options, false, false)?;
validate_volume_writer_inputs(
entry.name,
entry.data,
entry.password,
entry.file_comment,
options,
)?;
let mut solid_encoder = None;
let payload = encode_or_store_payload(entry.data, options, &mut solid_encoder)?;
if options.features.header_encryption {
return write_header_encrypted_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: &payload.data,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: options.target,
method: payload.method,
dictionary_flags: dictionary_flags_for_options(options)?,
base_flags: writer_file_flags(entry.password, None, false),
main_flags: if options.features.solid { MHD_SOLID } else { 0 },
password: entry.password,
max_packed_per_volume,
});
}
write_split_volumes(SplitVolumeRecord {
name: entry.name,
unpacked: entry.data,
packed: &payload.data,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: options.target,
method: payload.method,
dictionary_flags: dictionary_flags_for_options(options)?,
base_flags: writer_file_flags(entry.password, None, false),
main_flags: if options.features.solid { MHD_SOLID } else { 0 },
password: entry.password,
max_packed_per_volume,
})
}
fn validate_stored_writer_options(
options: WriterOptions,
has_archive_comment: bool,
has_file_comment: bool,
) -> Result<()> {
validate_compression_level(options)?;
if !matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) {
return Err(Error::UnsupportedVersion(options.target));
}
let mut allowed = FeatureSet::store_only();
allowed.file_encryption =
writer_supports_file_encryption(options.target) && options.features.file_encryption;
allowed.header_encryption =
writer_supports_header_encryption(options.target) && options.features.header_encryption;
allowed.archive_comment = matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) && has_archive_comment;
allowed.file_comment = matches!(
options.target,
ArchiveVersion::Rar15 | ArchiveVersion::Rar20 | ArchiveVersion::Rar29
) && has_file_comment;
if options.features != allowed {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 1.5 writer feature",
});
}
Ok(())
}
fn validate_compressed_writer_options(
options: WriterOptions,
has_archive_comment: bool,
has_file_comment: bool,
) -> Result<()> {
validate_compression_level(options)?;
if !matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) {
return Err(Error::UnsupportedVersion(options.target));
}
let mut allowed = FeatureSet::store_only();
allowed.solid = matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) && options.features.solid;
allowed.file_encryption =
writer_supports_file_encryption(options.target) && options.features.file_encryption;
allowed.header_encryption =
writer_supports_header_encryption(options.target) && options.features.header_encryption;
allowed.archive_comment = matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) && has_archive_comment;
allowed.file_comment = matches!(
options.target,
ArchiveVersion::Rar15 | ArchiveVersion::Rar20 | ArchiveVersion::Rar29
) && has_file_comment;
if options.features != allowed {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 1.5 writer feature",
});
}
Ok(())
}
fn validate_rar29_filtered_writer_options(options: WriterOptions) -> Result<()> {
validate_compression_level(options)?;
if !matches!(
options.target,
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40
) {
return Err(Error::UnsupportedVersion(options.target));
}
let mut allowed = FeatureSet::store_only();
allowed.solid = options.features.solid;
allowed.file_encryption =
writer_supports_file_encryption(options.target) && options.features.file_encryption;
allowed.header_encryption =
writer_supports_header_encryption(options.target) && options.features.header_encryption;
if options.features != allowed {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 2.9 RARVM-filtered compressed writer feature",
});
}
Ok(())
}
fn validate_header_encrypted_archive_options(
options: WriterOptions,
has_archive_comment: bool,
_solid: bool,
) -> Result<()> {
if has_archive_comment {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 3.x header-encrypted archive comments",
});
}
if !options.features.file_encryption {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 3.x header encryption without file encryption",
});
}
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",
));
}
let _ = dictionary_flags_for_options(options)?;
Ok(())
}
fn rar29_encode_options_for_level(level: Option<u8>) -> Result<Rar29EncodeOptions> {
let level = level.unwrap_or(5);
let candidates = match level {
0 => 0,
1 => 8,
2 => 32,
3 => 64,
4 => 96,
5 => 128,
_ => {
return Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
))
}
};
Ok(Rar29EncodeOptions::new(candidates)
.with_lazy_matching(level >= 4)
.with_lazy_lookahead(1)
.with_block_size(RAR29_LZ_BLOCK_SIZE))
}
fn rar29_encode_options_for_options(options: WriterOptions) -> Result<Rar29EncodeOptions> {
Ok(rar29_encode_options_for_level(options.compression_level)?
.with_max_match_distance(dictionary_size_for_options(options)?))
}
fn rar20_encode_options_for_level(level: Option<u8>) -> Result<Rar20EncodeOptions> {
match level {
None | Some(3) => Ok(Rar20EncodeOptions::default()),
Some(1) => Ok(Rar20EncodeOptions::new(16).with_try_audio(false)),
Some(2) => Ok(Rar20EncodeOptions::new(64)),
Some(4) => Ok(Rar20EncodeOptions::new(96).with_lazy_matching(false)),
Some(5) => Ok(Rar20EncodeOptions::new(128).with_lazy_matching(false)),
Some(0) => Ok(Rar20EncodeOptions::new(0)),
Some(_) => Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
)),
}
}
fn rar20_encode_options_for_options(options: WriterOptions) -> Result<Rar20EncodeOptions> {
rar20_encode_options_for_level(options.compression_level)
}
fn rar15_encode_options_for_level(level: Option<u8>) -> Result<Rar15EncodeOptions> {
let level = level.unwrap_or(5);
match level {
0 => Ok(Rar15EncodeOptions::new()
.with_old_distance_tokens(false)
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(0)),
1 => Ok(Rar15EncodeOptions::new()
.with_old_distance_tokens(false)
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(4 * 1024)),
2 => Ok(Rar15EncodeOptions::new()
.with_lazy_matching(false)
.with_stmode_literal_runs(false)
.with_max_long_match_distance(8 * 1024)),
3 => Ok(Rar15EncodeOptions::new()
.with_lazy_matching(false)
.with_max_long_match_distance(16 * 1024)),
4 => Ok(Rar15EncodeOptions::new()
.with_lazy_matching(false)
.with_max_long_match_distance(24 * 1024)),
5 => Ok(Rar15EncodeOptions::new().with_lazy_matching(false)),
_ => Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
)),
}
}
fn rar29_default_dictionary_size(target: ArchiveVersion) -> usize {
match target {
ArchiveVersion::Rar29 => 1024 * 1024,
ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => 128 * 1024,
_ => 64 * 1024,
}
}
fn dictionary_size_for_options(options: WriterOptions) -> Result<usize> {
options
.dictionary_size
.map(Ok)
.unwrap_or_else(|| Ok(rar29_default_dictionary_size(options.target)))
}
fn dictionary_flags_for_options(options: WriterOptions) -> Result<u16> {
dictionary_flags_for_size(dictionary_size_for_options(options)?)
}
fn dictionary_flags_for_size(size: usize) -> Result<u16> {
let bits =
match size {
0x1_0000 => 0,
0x2_0000 => 1,
0x4_0000 => 2,
0x8_0000 => 3,
0x10_0000 => 4,
0x20_0000 => 5,
0x40_0000 => 6,
_ => return Err(Error::InvalidHeader(
"RAR 1.5-4.x dictionary size must be one of 64K, 128K, 256K, 512K, 1M, 2M, or 4M",
)),
};
Ok((bits as u16) << 5)
}
fn compression_method_for_level(options: WriterOptions) -> Result<u8> {
let Some(level) = options.compression_level else {
return Ok(0x33);
};
if level > 5 {
return Err(Error::InvalidHeader(
"RAR compression level must be in the range 0..5",
));
}
if level == 0 {
return Ok(0x30);
}
if matches!(
options.target,
ArchiveVersion::Rar20
| ArchiveVersion::Rar15
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
) {
return Ok(0x30 + level);
}
Ok(0x33)
}
enum SolidEncoder {
Rar15(Box<Unpack15Encoder>),
Rar20(Unpack20Encoder),
Rar29(Unpack29Encoder),
}
impl SolidEncoder {
fn for_target(options: WriterOptions, solid: bool) -> Result<Option<Self>> {
if !solid {
return Ok(None);
}
let encoder = match options.target {
ArchiveVersion::Rar15 => Self::Rar15(Box::new(Unpack15Encoder::with_options(
rar15_encode_options_for_level(options.compression_level)?,
))),
ArchiveVersion::Rar20 => Self::Rar20(Unpack20Encoder::with_options(
rar20_encode_options_for_options(options)?,
)),
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => Self::Rar29(
Unpack29Encoder::with_options(rar29_encode_options_for_options(options)?),
),
_ => return Ok(None),
};
Ok(Some(encoder))
}
}
struct EncodedPayload {
data: Vec<u8>,
method: u8,
}
fn encode_or_store_payload(
data: &[u8],
options: WriterOptions,
solid_encoder: &mut Option<SolidEncoder>,
) -> Result<EncodedPayload> {
let target = options.target;
if options.compression_level == Some(0) {
return Ok(EncodedPayload {
data: data.to_vec(),
method: 0x30,
});
}
let solid = solid_encoder.is_some();
if !solid
&& matches!(
target,
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40
)
{
let encode_options = rar29_encode_options_for_options(options)?;
let lz_method = compression_method_for_level(options)?;
if matches!(options.compression_level, Some(1..=4)) {
return encode_rar29_auto_filtered_member(data, encode_options, lz_method, false);
}
return encode_rar29_auto_filtered_member(data, encode_options, lz_method, true);
}
let compressed = encode_compressed_payload(data, options, solid_encoder.as_mut())?;
if should_store_fallback(target, solid, data.len(), compressed.len()) {
if solid {
*solid_encoder = SolidEncoder::for_target(options, true)?;
}
return Ok(EncodedPayload {
data: data.to_vec(),
method: 0x30,
});
}
Ok(EncodedPayload {
data: compressed,
method: compression_method_for_level(options)?,
})
}
fn encode_compressed_payload(
data: &[u8],
options: WriterOptions,
solid_encoder: Option<&mut SolidEncoder>,
) -> Result<Vec<u8>> {
let target = options.target;
match (target, solid_encoder) {
(ArchiveVersion::Rar15, Some(SolidEncoder::Rar15(encoder))) => {
encoder.encode_member(data).map_err(Error::from)
}
(ArchiveVersion::Rar15, None) => unpack15_encode_with_options(
data,
rar15_encode_options_for_level(options.compression_level)?,
)
.map_err(Error::from),
(ArchiveVersion::Rar20, None) => {
unpack20_encode_auto_with_options(data, rar20_encode_options_for_options(options)?)
.map_err(Error::from)
}
(ArchiveVersion::Rar20, Some(SolidEncoder::Rar20(encoder))) => {
encoder.encode_member(data).map_err(Error::from)
}
(ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40, None) => {
unpack29_encode_literals(data).map_err(Error::from)
}
(
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40,
Some(SolidEncoder::Rar29(encoder)),
) => encoder.encode_member(data).map_err(Error::from),
_ => Err(Error::UnsupportedVersion(target)),
}
}
fn should_store_fallback(
target: ArchiveVersion,
solid: bool,
unpacked_len: usize,
packed_len: usize,
) -> bool {
if packed_len < unpacked_len {
return false;
}
if !solid
&& matches!(
target,
ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
)
{
return true;
}
unpacked_len >= MIN_STORE_FALLBACK_SIZE
}
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()
&& !matches!(
options.target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
)
{
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "RAR 2.9 encrypted volume writer",
});
}
if file_comment.is_some() {
return Err(Error::UnsupportedFeature {
version: options.target,
feature: "volume_file_comment",
});
}
Ok(())
}
fn writer_file_flags(
password: Option<&[u8]>,
file_comment: Option<&[u8]>,
solid_continuation: bool,
) -> u16 {
let mut flags = 0;
if password.is_some() {
flags |= FHD_PASSWORD;
}
if file_comment.is_some() {
flags |= FHD_COMMENT;
}
if solid_continuation {
flags |= FHD_SOLID;
}
flags
}
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.5 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 encrypt_split_packed_data(
data: &mut Vec<u8>,
target: ArchiveVersion,
password: &[u8],
) -> Result<Option<[u8; 8]>> {
match target {
ArchiveVersion::Rar15 => {
Rar15Cipher::new(password).crypt_in_place(data);
Ok(None)
}
ArchiveVersion::Rar20 => {
let padded_len = checked_align16(data.len(), RAR15_ALIGN_OVERFLOW)?;
data.resize(padded_len, 0);
Rar20Cipher::new(password).encrypt_in_place(data)?;
Ok(None)
}
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => {
let salt = random_rar30_salt()?;
let padded_len = checked_align16(data.len(), RAR15_ALIGN_OVERFLOW)?;
data.resize(padded_len, 0);
Rar30Cipher::new(password, Some(salt))
.map_err(super::map_rar30_crypto_error)?
.encrypt_in_place(data)
.map_err(super::map_rar30_crypto_error)?;
Ok(Some(salt))
}
_ => Err(Error::UnsupportedVersion(target)),
}
}
fn writer_supports_file_encryption(target: ArchiveVersion) -> bool {
matches!(
target,
ArchiveVersion::Rar15
| ArchiveVersion::Rar20
| ArchiveVersion::Rar29
| ArchiveVersion::Rar30
| ArchiveVersion::Rar40
)
}
fn writer_supports_header_encryption(target: ArchiveVersion) -> bool {
matches!(target, ArchiveVersion::Rar30 | ArchiveVersion::Rar40)
}
fn header_encryption_password<'a>(
mut passwords: impl Iterator<Item = Option<&'a [u8]>>,
) -> Result<&'a [u8]> {
let first = passwords.next().flatten().ok_or(Error::NeedPassword)?;
for password in passwords {
if password != Some(first) {
return Err(Error::InvalidHeader(
"RAR 3.x header-encrypted writer needs one shared password",
));
}
}
Ok(first)
}
fn encrypt_packed_data_for_writer(
data: &mut Vec<u8>,
target: ArchiveVersion,
password: Option<&[u8]>,
) -> Result<Option<[u8; 8]>> {
let Some(password) = password else {
return Ok(None);
};
validate_writer_password(target, Some(password))?;
match target {
ArchiveVersion::Rar15 => {
Rar15Cipher::new(password).crypt_in_place(data);
Ok(None)
}
ArchiveVersion::Rar20 => {
let padded_len = checked_align16(data.len(), RAR15_ALIGN_OVERFLOW)?;
data.resize(padded_len, 0);
Rar20Cipher::new(password).encrypt_in_place(data)?;
Ok(None)
}
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => {
let salt = random_rar30_salt()?;
let padded_len =
data.len()
.checked_add(15)
.map(|len| len & !15)
.ok_or(Error::InvalidHeader(
"RAR 3.x encrypted data size overflows",
))?;
data.resize(padded_len, 0);
Rar30Cipher::new(password, Some(salt))
.map_err(super::map_rar30_crypto_error)?
.encrypt_in_place(data)
.map_err(super::map_rar30_crypto_error)?;
Ok(Some(salt))
}
_ => Err(Error::UnsupportedFeature {
version: target,
feature: "RAR writer file encryption",
}),
}
}
fn random_rar30_salt() -> Result<[u8; 8]> {
let mut salt = [0; 8];
getrandom::fill(&mut salt)
.map_err(|_| Error::InvalidHeader("RAR 3.x writer could not generate encryption salt"))?;
Ok(salt)
}
fn write_main_header(out: &mut Vec<u8>, flags: u16) {
let start = out.len();
out.extend_from_slice(&0u16.to_le_bytes());
out.push(MAIN_HEAD);
out.extend_from_slice(&flags.to_le_bytes());
out.extend_from_slice(&13u16.to_le_bytes());
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes());
write_header_crc(out, start);
}
fn write_comment_header(out: &mut Vec<u8>, comment: Option<&[u8]>) -> Result<()> {
let Some(comment) = comment else {
return Ok(());
};
let unp_size = u16::try_from(comment.len())
.map_err(|_| Error::InvalidHeader("RAR 1.5 archive comment is too long"))?;
let head_size = 13usize
.checked_add(comment.len())
.ok_or(Error::InvalidHeader(
"RAR 1.5 comment header size overflows",
))?;
let head_size = u16::try_from(head_size)
.map_err(|_| Error::InvalidHeader("RAR 1.5 comment header size overflows"))?;
let start = out.len();
out.extend_from_slice(&0u16.to_le_bytes());
out.push(COMM_HEAD);
out.extend_from_slice(&0u16.to_le_bytes());
out.extend_from_slice(&head_size.to_le_bytes());
out.extend_from_slice(&unp_size.to_le_bytes());
out.push(15);
out.push(0x30);
out.extend_from_slice(&((crc32(comment) & 0xffff) as u16).to_le_bytes());
out.extend_from_slice(comment);
write_comment_header_crc(out, start);
Ok(())
}
fn uses_old_style_archive_comment(target: ArchiveVersion) -> bool {
matches!(
target,
ArchiveVersion::Rar15 | ArchiveVersion::Rar20 | ArchiveVersion::Rar29
)
}
fn write_archive_comment(
out: &mut Vec<u8>,
comment: Option<&[u8]>,
target: ArchiveVersion,
) -> Result<()> {
if uses_old_style_archive_comment(target) {
return write_comment_header(out, comment);
}
match target {
ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => write_newsub_archive_comment(out, comment),
_ => Err(Error::UnsupportedVersion(target)),
}
}
fn write_newsub_archive_comment(out: &mut Vec<u8>, comment: Option<&[u8]>) -> Result<()> {
let Some(comment) = comment else {
return Ok(());
};
let packed = unpack29_encode_literals(comment)?;
write_file_header_and_data(
out,
FileRecord {
head_type: NEWSUB_HEAD,
name: b"CMT",
unpacked_size: comment.len(),
file_crc: crc32(comment),
packed: &packed,
file_time: 0,
file_attr: 0,
host_os: 3,
target: ArchiveVersion::Rar30,
method: 0x33,
dictionary_flags: dictionary_flags_for_target(ArchiveVersion::Rar30),
flags: 0,
salt: None,
extra: &[],
},
)
}
fn write_stored_entry(
out: &mut Vec<u8>,
entry: &StoredEntry<'_>,
options: WriterOptions,
) -> Result<()> {
let target = options.target;
validate_stored_entry(entry)?;
validate_writer_password(target, entry.password)?;
let mut packed = entry.data.to_vec();
let salt = encrypt_packed_data_for_writer(&mut packed, target, entry.password)?;
let mut flags = writer_file_flags(entry.password, entry.file_comment, false);
if salt.is_some() {
flags |= FHD_SALT;
}
let file_comment = encode_file_comment(entry.file_comment)?;
write_file_header_and_data(
out,
FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.data.len(),
file_crc: crc32(entry.data),
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target,
method: 0x30,
dictionary_flags: dictionary_flags_for_options(options)?,
flags,
salt,
extra: &file_comment,
},
)
}
fn write_compressed_entry(
out: &mut Vec<u8>,
entry: &FileEntry<'_>,
packed: &[u8],
method: u8,
target: ArchiveVersion,
dictionary_flags: u16,
solid_continuation: bool,
) -> Result<()> {
validate_file_entry(entry.name, entry.data)?;
validate_writer_password(target, entry.password)?;
let mut packed = packed.to_vec();
let salt = encrypt_packed_data_for_writer(&mut packed, target, entry.password)?;
let mut flags = writer_file_flags(entry.password, entry.file_comment, solid_continuation);
if salt.is_some() {
flags |= FHD_SALT;
}
let file_comment = encode_file_comment(entry.file_comment)?;
write_file_header_and_data(
out,
FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.data.len(),
file_crc: crc32(entry.data),
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target,
method,
dictionary_flags,
flags,
salt,
extra: &file_comment,
},
)
}
fn write_header_encrypted_stored_entry(
out: &mut Vec<u8>,
entry: &StoredEntry<'_>,
options: WriterOptions,
header_password: &[u8],
) -> Result<()> {
let target = options.target;
validate_stored_entry(entry)?;
validate_writer_password(target, entry.password)?;
let mut packed = entry.data.to_vec();
let salt = encrypt_packed_data_for_writer(&mut packed, target, entry.password)?;
let mut flags = writer_file_flags(entry.password, entry.file_comment, false);
if salt.is_some() {
flags |= FHD_SALT;
}
let file_comment = encode_file_comment(entry.file_comment)?;
let mut header = Vec::new();
write_file_header(
&mut header,
&FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.data.len(),
file_crc: crc32(entry.data),
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target,
method: 0x30,
dictionary_flags: dictionary_flags_for_options(options)?,
flags,
salt,
extra: &file_comment,
},
)?;
write_encrypted_header_and_data(out, &header, &packed, header_password)
}
fn write_header_encrypted_compressed_entry(
out: &mut Vec<u8>,
entry: &FileEntry<'_>,
packed: &[u8],
method: u8,
options: WriterOptions,
solid_continuation: bool,
header_password: &[u8],
) -> Result<()> {
let target = options.target;
validate_file_entry(entry.name, entry.data)?;
validate_writer_password(target, entry.password)?;
let mut packed = packed.to_vec();
let salt = encrypt_packed_data_for_writer(&mut packed, target, entry.password)?;
let mut flags = writer_file_flags(entry.password, entry.file_comment, solid_continuation);
if salt.is_some() {
flags |= FHD_SALT;
}
let file_comment = encode_file_comment(entry.file_comment)?;
let mut header = Vec::new();
write_file_header(
&mut header,
&FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.data.len(),
file_crc: crc32(entry.data),
packed: &packed,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target,
method,
dictionary_flags: dictionary_flags_for_options(options)?,
flags,
salt,
extra: &file_comment,
},
)?;
write_encrypted_header_and_data(out, &header, &packed, header_password)
}
fn write_encrypted_header_and_data(
out: &mut Vec<u8>,
header: &[u8],
data: &[u8],
password: &[u8],
) -> Result<()> {
let salt = random_rar30_salt()?;
let encrypted_size = checked_align16(header.len(), RAR15_ALIGN_OVERFLOW)?;
let mut encrypted_header = Vec::with_capacity(encrypted_size);
encrypted_header.extend_from_slice(header);
encrypted_header.resize(encrypted_size, 0);
Rar30Cipher::new(password, Some(salt))
.map_err(super::map_rar30_crypto_error)?
.encrypt_in_place(&mut encrypted_header)
.map_err(super::map_rar30_crypto_error)?;
out.extend_from_slice(&salt);
out.extend_from_slice(&encrypted_header);
out.extend_from_slice(data);
Ok(())
}
fn validate_writer_password(target: ArchiveVersion, password: Option<&[u8]>) -> Result<()> {
if password.is_some() && !writer_supports_file_encryption(target) {
return Err(Error::UnsupportedFeature {
version: target,
feature: "RAR writer file encryption",
});
}
Ok(())
}
fn validate_stored_entry(entry: &StoredEntry<'_>) -> Result<()> {
if entry.name.is_empty() {
return Err(Error::InvalidHeader("RAR 1.5 file name is empty"));
}
if entry.name.len() > u16::MAX as usize {
return Err(Error::InvalidHeader("RAR 1.5 file name is too long"));
}
if entry.data.len() > u32::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.5 store-only writer does not support large files",
));
}
Ok(())
}
fn validate_file_entry(name: &[u8], data: &[u8]) -> Result<()> {
if name.is_empty() {
return Err(Error::InvalidHeader("RAR 1.5 file name is empty"));
}
if name.len() > u16::MAX as usize {
return Err(Error::InvalidHeader("RAR 1.5 file name is too long"));
}
if data.len() > u32::MAX as usize {
return Err(Error::InvalidHeader(
"RAR 1.5 writer does not support large files",
));
}
Ok(())
}
struct FileRecord<'a> {
head_type: u8,
name: &'a [u8],
unpacked_size: usize,
file_crc: u32,
packed: &'a [u8],
file_time: u32,
file_attr: u32,
host_os: u8,
target: ArchiveVersion,
method: u8,
dictionary_flags: u16,
flags: u16,
salt: Option<[u8; 8]>,
extra: &'a [u8],
}
fn write_file_header_and_data(out: &mut Vec<u8>, record: FileRecord<'_>) -> Result<()> {
write_file_header(out, &record)?;
out.extend_from_slice(record.packed);
Ok(())
}
fn write_file_header(out: &mut Vec<u8>, record: &FileRecord<'_>) -> Result<()> {
let start = out.len();
let flags = record.flags | record.dictionary_flags;
let (host_os, file_attr) =
rar15_compatible_metadata(record.target, record.host_os, record.file_attr);
let packed_size = u32::try_from(record.packed.len())
.map_err(|_| Error::InvalidHeader("RAR 1.5 packed size overflows u32"))?;
let unpacked_size = u32::try_from(record.unpacked_size)
.map_err(|_| Error::InvalidHeader("RAR 1.5 unpacked size overflows u32"))?;
let head_size = 32usize
.checked_add(record.name.len())
.and_then(|size| size.checked_add(if record.salt.is_some() { 8 } else { 0 }))
.and_then(|size| size.checked_add(record.extra.len()))
.ok_or(Error::InvalidHeader("RAR 1.5 file header size overflows"))?;
let head_size = u16::try_from(head_size)
.map_err(|_| Error::InvalidHeader("RAR 1.5 file header size overflows"))?;
let unp_ver = match record.target {
ArchiveVersion::Rar15 => 15,
ArchiveVersion::Rar20 => 20,
ArchiveVersion::Rar29 | ArchiveVersion::Rar30 | ArchiveVersion::Rar40 => 29,
_ => return Err(Error::UnsupportedVersion(record.target)),
};
out.extend_from_slice(&0u16.to_le_bytes());
out.push(record.head_type);
out.extend_from_slice(&(LONG_BLOCK | flags).to_le_bytes());
out.extend_from_slice(&head_size.to_le_bytes());
out.extend_from_slice(&packed_size.to_le_bytes());
out.extend_from_slice(&unpacked_size.to_le_bytes());
out.push(host_os);
out.extend_from_slice(&record.file_crc.to_le_bytes());
out.extend_from_slice(&record.file_time.to_le_bytes());
out.push(unp_ver);
out.push(record.method);
out.extend_from_slice(&(record.name.len() as u16).to_le_bytes());
out.extend_from_slice(&file_attr.to_le_bytes());
out.extend_from_slice(record.name);
if let Some(salt) = record.salt {
out.extend_from_slice(&salt);
}
out.extend_from_slice(record.extra);
write_file_header_crc(out, start, record.name.len(), flags);
Ok(())
}
fn rar15_compatible_metadata(target: ArchiveVersion, host_os: u8, file_attr: u32) -> (u8, u32) {
const HOST_DOS: u8 = 0;
const HOST_UNIX: u8 = 3;
const DOS_ARCHIVE: u32 = 0x20;
const DOS_DIRECTORY: u32 = 0x10;
const UNIX_FILE_TYPE_MASK: u32 = 0o170000;
const UNIX_DIRECTORY: u32 = 0o040000;
if target == ArchiveVersion::Rar15 && host_os == HOST_UNIX {
let dos_attr = if file_attr & UNIX_FILE_TYPE_MASK == UNIX_DIRECTORY {
DOS_DIRECTORY
} else {
DOS_ARCHIVE
};
return (HOST_DOS, dos_attr);
}
(host_os, file_attr)
}
fn dictionary_flags_for_target(target: ArchiveVersion) -> u16 {
((rar29_default_dictionary_size(target).trailing_zeros() - 16) as u16) << 5
}
struct SplitVolumeRecord<'a> {
name: &'a [u8],
unpacked: &'a [u8],
packed: &'a [u8],
file_time: u32,
file_attr: u32,
host_os: u8,
target: ArchiveVersion,
method: u8,
dictionary_flags: u16,
base_flags: u16,
main_flags: u16,
password: Option<&'a [u8]>,
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.5 volume payload size must be non-zero",
));
}
if entry.packed.is_empty() {
return Err(Error::InvalidHeader(
"RAR 1.5 volume writer needs a non-empty packed payload",
));
}
let mut packed = entry.packed.to_vec();
let split_salt = if let Some(password) = entry.password {
encrypt_split_packed_data(&mut packed, entry.target, password)?
} else {
None
};
let base_flags = entry.base_flags | if split_salt.is_some() { FHD_SALT } else { 0 };
let chunks: Vec<&[u8]> = packed.chunks(entry.max_packed_per_volume).collect();
if chunks.len() < 2 {
return Err(Error::InvalidHeader(
"RAR 1.5 volume writer needs at least two volumes",
));
}
let mut volumes = Vec::with_capacity(chunks.len());
let unpacked_crc = crc32(entry.unpacked);
for (index, chunk) in chunks.iter().enumerate() {
let split_before = index > 0;
let split_after = index + 1 < chunks.len();
let mut file_flags = base_flags;
if split_before {
file_flags |= FHD_SPLIT_BEFORE;
}
if split_after {
file_flags |= FHD_SPLIT_AFTER;
}
let mut main_flags = MHD_VOLUME | entry.main_flags;
if index == 0 {
main_flags |= MHD_FIRSTVOLUME;
}
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
write_main_header(&mut out, main_flags);
write_file_header_and_data(
&mut out,
FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.unpacked.len(),
file_crc: if split_after {
crc32(chunk)
} else {
unpacked_crc
},
packed: chunk,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: entry.target,
method: entry.method,
dictionary_flags: entry.dictionary_flags,
flags: file_flags,
salt: split_salt,
extra: &[],
},
)?;
volumes.push(out);
}
Ok(volumes)
}
fn write_header_encrypted_split_volumes(entry: SplitVolumeRecord<'_>) -> Result<Vec<Vec<u8>>> {
validate_header_encrypted_archive_options(
WriterOptions {
target: entry.target,
features: {
let mut features = FeatureSet::store_only();
features.file_encryption = entry.password.is_some();
features.header_encryption = true;
features.solid = entry.main_flags & MHD_SOLID != 0;
features
},
compression_level: None,
dictionary_size: None,
},
false,
entry.main_flags & MHD_SOLID != 0,
)?;
let password = entry.password.ok_or(Error::NeedPassword)?;
if entry.max_packed_per_volume == 0 {
return Err(Error::InvalidHeader(
"RAR 1.5 volume payload size must be non-zero",
));
}
if entry.packed.is_empty() {
return Err(Error::InvalidHeader(
"RAR 1.5 volume writer needs a non-empty packed payload",
));
}
let mut packed = entry.packed.to_vec();
let split_salt = encrypt_split_packed_data(&mut packed, entry.target, password)?;
let base_flags = entry.base_flags | FHD_SALT;
let chunks: Vec<&[u8]> = packed.chunks(entry.max_packed_per_volume).collect();
if chunks.len() < 2 {
return Err(Error::InvalidHeader(
"RAR 1.5 volume writer needs at least two volumes",
));
}
let mut volumes = Vec::with_capacity(chunks.len());
let unpacked_crc = crc32(entry.unpacked);
for (index, chunk) in chunks.iter().enumerate() {
let split_before = index > 0;
let split_after = index + 1 < chunks.len();
let mut file_flags = base_flags;
if split_before {
file_flags |= FHD_SPLIT_BEFORE;
}
if split_after {
file_flags |= FHD_SPLIT_AFTER;
}
let mut main_flags = MHD_VOLUME | MHD_PASSWORD | entry.main_flags;
if index == 0 {
main_flags |= MHD_FIRSTVOLUME;
}
let mut out = Vec::new();
out.extend_from_slice(RAR15_SIGNATURE);
write_main_header(&mut out, main_flags);
let mut header = Vec::new();
write_file_header(
&mut header,
&FileRecord {
head_type: FILE_HEAD,
name: entry.name,
unpacked_size: entry.unpacked.len(),
file_crc: if split_after {
crc32(chunk)
} else {
unpacked_crc
},
packed: chunk,
file_time: entry.file_time,
file_attr: entry.file_attr,
host_os: entry.host_os,
target: entry.target,
method: entry.method,
dictionary_flags: entry.dictionary_flags,
flags: file_flags,
salt: split_salt,
extra: &[],
},
)?;
write_encrypted_header_and_data(&mut out, &header, chunk, password)?;
volumes.push(out);
}
Ok(volumes)
}
fn write_header_crc(out: &mut [u8], start: usize) {
let crc = (crc32(&out[start + 2..]) & 0xffff) as u16;
out[start..start + 2].copy_from_slice(&crc.to_le_bytes());
}
fn write_file_header_crc(out: &mut [u8], start: usize, name_len: usize, flags: u16) {
let end = if flags & FHD_COMMENT != 0 {
start + 32 + name_len
} else {
out.len()
};
let crc = (crc32(&out[start + 2..end]) & 0xffff) as u16;
out[start..start + 2].copy_from_slice(&crc.to_le_bytes());
}
fn write_comment_header_crc(out: &mut [u8], start: usize) {
let end = start + 13;
let crc = (crc32(&out[start + 2..end]) & 0xffff) as u16;
out[start..start + 2].copy_from_slice(&crc.to_le_bytes());
}
#[cfg(test)]
mod tests {
use super::{
auto_delta_filter_range, auto_x86_filter_ranges, disjoint_filter_ranges,
encode_rar29_auto_filtered_member, encode_rar29_filtered_member,
encode_rar29_filtered_members, is_audio_filter_candidate, rar29_encode_options_for_options,
FilterKind, FilterSpec, AUTO_DELTA_EDGE_SKIP, RAR29_LARGE_TEXT_PPMD_THRESHOLD,
};
use crate::{ArchiveVersion, FeatureSet};
use rars_codec::rar29::{unpack29_decode, EncodeOptions};
#[test]
fn auto_x86_filter_ranges_select_dense_opcode_clusters() {
let mut data = vec![0x41; 20_000];
for pos in [1024, 1050, 1090, 1130] {
data[pos] = 0xe8;
}
for pos in [12_000, 12_040, 12_080] {
data[pos] = 0xe9;
}
let e8_ranges = auto_x86_filter_ranges(&data, false);
assert_eq!(e8_ranges.len(), 1);
assert!(e8_ranges[0].contains(&1024));
assert!(e8_ranges[0].contains(&(1130 + 4)));
assert!(!e8_ranges[0].contains(&12_000));
let e8e9_ranges = auto_x86_filter_ranges(&data, true);
assert_eq!(e8e9_ranges.len(), 3);
assert!(e8e9_ranges[0].contains(&1024));
assert!(e8e9_ranges[0].contains(&12_000));
assert!(e8e9_ranges.iter().any(|range| range.contains(&1024)));
assert!(e8e9_ranges.iter().any(|range| range.contains(&12_000)));
}
#[test]
fn large_text_ppmd_candidate_accepts_html_like_payloads() {
let mut data = vec![b'a'; RAR29_LARGE_TEXT_PPMD_THRESHOLD + 1];
for index in (0..data.len()).step_by(79) {
data[index] = b'\n';
}
data[..32].copy_from_slice(b"<html><body>RAR PPMd text sample");
assert!(super::is_large_text_ppmd_candidate(&data));
}
#[test]
fn large_text_ppmd_candidate_rejects_binary_payloads() {
let mut data = vec![0u8; RAR29_LARGE_TEXT_PPMD_THRESHOLD + 1];
for index in (0..data.len()).step_by(257) {
data[index] = b'A';
}
assert!(!super::is_large_text_ppmd_candidate(&data));
}
#[test]
fn auto_ppmd_candidate_rejects_binary_audio_shaped_payloads() {
let mut data = Vec::new();
for sample in 0..8192i16 {
let left = sample.wrapping_mul(5).wrapping_add(200);
let right = sample.wrapping_mul(7).wrapping_sub(200);
data.extend_from_slice(&left.to_le_bytes());
data.extend_from_slice(&right.to_le_bytes());
}
assert!(!super::is_auto_ppmd_candidate(&data));
}
#[test]
fn auto_ppmd_candidate_accepts_text_payloads() {
let data = b"fn main() {\n println!(\"rar ppmd text candidate\");\n}\n".repeat(256);
assert!(super::is_auto_ppmd_candidate(&data));
}
#[test]
fn audio_filter_candidate_accepts_interleaved_pcm_like_payloads() {
let mut data = Vec::new();
for sample in 0..4096i16 {
let left = sample.wrapping_mul(3).wrapping_add(200);
let right = sample.wrapping_mul(3).wrapping_sub(200);
data.extend_from_slice(&left.to_le_bytes());
data.extend_from_slice(&right.to_le_bytes());
}
assert!(is_audio_filter_candidate(&data, 4));
assert!(!is_audio_filter_candidate(&data, 3));
}
#[test]
fn audio_filter_candidate_rejects_high_entropy_binary_payloads() {
let mut state = 0xfeed_faceu32;
let data: Vec<_> = (0..16_384)
.map(|_| {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state as u8
})
.collect();
for channels in 1..=4 {
assert!(!is_audio_filter_candidate(&data, channels));
}
}
#[test]
fn auto_filtered_rar29_large_text_uses_ppmd_before_lz_candidates() {
let mut data = vec![b'x'; RAR29_LARGE_TEXT_PPMD_THRESHOLD + 1];
for index in (0..data.len()).step_by(97) {
data[index] = b' ';
}
assert!(super::is_large_text_ppmd_candidate(&data));
}
#[test]
fn auto_x86_filter_ranges_include_code_section_spans() {
let mut data = vec![0x41; 32_000];
for pos in [4096, 4128, 4160] {
data[pos] = 0xe8;
}
for pos in [14_000, 14_032, 14_064] {
data[pos] = 0xe8;
}
let ranges = auto_x86_filter_ranges(&data, false);
assert!(ranges[0].contains(&4096));
assert!(ranges[0].contains(&14_064));
assert!(ranges.iter().any(|range| range.contains(&4096)));
assert!(ranges.iter().any(|range| range.contains(&14_064)));
}
#[test]
fn auto_x86_policy_can_encode_multiple_disjoint_ranges() {
let mut data = vec![0x41u8; 80_000];
for cluster_start in [8_000, 60_000] {
for index in 0..8 {
let pos = cluster_start + index * 64;
data[pos] = 0xe8;
data[pos + 1..pos + 5].copy_from_slice(&(0x2000u32 + index as u32).to_le_bytes());
}
}
let filters: Vec<_> = disjoint_filter_ranges(auto_x86_filter_ranges(&data, false))
.into_iter()
.map(|range| FilterSpec::range(FilterKind::E8, range))
.collect();
let packed = encode_rar29_filtered_members(&data, &filters, EncodeOptions::default())
.expect("multi-filter RAR29 member should encode");
let decoded = unpack29_decode(&packed, data.len()).unwrap();
assert_eq!(filters.len(), 2);
assert!(
decoded == data,
"RAR 2.9 auto multi-filter E8 round-trip failed"
);
}
#[test]
fn auto_x86_policy_considers_tight_ranges_inside_sparse_spans() {
let mut data = Vec::new();
data.extend((0..1024).map(|index| (index * 37 + 11) as u8));
let first_cluster_start = data.len();
for index in 0..16usize {
data.extend_from_slice(&[0x55, 0x8b, 0xec, 0x83, 0xec, (index & 0x7f) as u8]);
let call_pos = data.len();
data.push(0xe8);
let target = first_cluster_start + 0x500;
let relative = (target as i64 - (call_pos + 5) as i64) as i32;
data.extend_from_slice(&relative.to_le_bytes());
data.extend_from_slice(&[0x83, 0xc4, 0x04, 0x5d, 0xc3]);
}
data.extend((0..3200).map(|index| (index * 251 + 17) as u8));
let second_cluster_start = data.len();
for index in 0..16usize {
data.extend_from_slice(&[0x56, 0x8b, 0xf1, 0x83, 0xec, (index & 0x7f) as u8]);
let call_pos = data.len();
data.push(0xe8);
let target = second_cluster_start + 0x500;
let relative = (target as i64 - (call_pos + 5) as i64) as i32;
data.extend_from_slice(&relative.to_le_bytes());
data.extend_from_slice(&[0x83, 0xc4, 0x04, 0x5e, 0xc3]);
}
data.extend((0..1024).map(|index| (index * 53 + 7) as u8));
let ranges = disjoint_filter_ranges(auto_x86_filter_ranges(&data, false));
let filters: Vec<_> = ranges
.iter()
.cloned()
.map(|range| FilterSpec::range(FilterKind::E8, range))
.collect();
let broad_range = first_cluster_start..second_cluster_start + 16 * 16;
let broad = encode_rar29_filtered_member(
&data,
FilterSpec::range(FilterKind::E8, broad_range),
EncodeOptions::default(),
)
.unwrap();
let tight = encode_rar29_filtered_members(&data, &filters, EncodeOptions::default())
.expect("tight sparse x86 filters should encode");
let auto = encode_rar29_auto_filtered_member(&data, EncodeOptions::default(), 0x35, false)
.unwrap();
assert!(
tight.len() < broad.len(),
"tight x86 ranges should avoid filtering sparse data gaps"
);
assert!(auto.data.len() <= tight.len());
assert_eq!(unpack29_decode(&auto.data, data.len()).unwrap(), data);
}
#[test]
fn auto_delta_filter_range_skips_container_edges_and_aligns_channels() {
let data = vec![0u8; 512];
let range = auto_delta_filter_range(&data, 3).unwrap();
assert!(range.start >= AUTO_DELTA_EDGE_SKIP);
assert!(range.end <= data.len() - AUTO_DELTA_EDGE_SKIP);
assert_eq!(range.start % 3, 0);
assert_eq!((range.end - range.start) % 3, 0);
assert!(auto_delta_filter_range(&data[..80], 3).is_none());
}
#[test]
fn auto_filter_policy_considers_ranged_delta_candidates() {
let mut data = vec![0x55u8; AUTO_DELTA_EDGE_SKIP];
for sample in 0..256u16 {
let left = sample as u8;
let right = left.wrapping_add(1);
data.extend_from_slice(&[left, right]);
}
data.extend(std::iter::repeat_n(0xaa, AUTO_DELTA_EDGE_SKIP));
let options = EncodeOptions::default();
let plain =
rars_codec::rar29::unpack29_encode_literals_with_options(&data, options).unwrap();
let ranged = encode_rar29_filtered_member(
&data,
FilterSpec::range(
FilterKind::Delta { channels: 2 },
auto_delta_filter_range(&data, 2).unwrap(),
),
options,
)
.unwrap();
let auto = encode_rar29_auto_filtered_member(&data, options, 0x35, true).unwrap();
assert!(ranged.len() < plain.len());
assert!(auto.data.len() <= ranged.len());
}
#[test]
fn rar29_options_cap_match_distance_to_target_dictionary() {
assert_eq!(
rar29_encode_options_for_options(
super::WriterOptions::new(ArchiveVersion::Rar29, FeatureSet::store_only())
.with_compression_level(5)
)
.unwrap()
.max_match_distance,
1024 * 1024
);
assert_eq!(
rar29_encode_options_for_options(
super::WriterOptions::new(ArchiveVersion::Rar40, FeatureSet::store_only())
.with_compression_level(5)
)
.unwrap()
.max_match_distance,
128 * 1024
);
assert_eq!(
rar29_encode_options_for_options(
super::WriterOptions::new(ArchiveVersion::Rar40, FeatureSet::store_only())
.with_compression_level(5)
.with_dictionary_size(4 * 1024 * 1024)
)
.unwrap()
.max_match_distance,
4 * 1024 * 1024
);
}
}