use super::headers::{block_header_image, resolved_main_extra, stored_file_specific, write_vint};
use super::ArchiveMetadataEntry;
use crate::detect::RAR50_SIGNATURE;
use crate::rar50::{FHEXTRA_SUBDATA, HEAD_MAIN, HEAD_SERVICE, HFL_DATA, HFL_EXTRA};
use crate::{Error, Result};
const MAX_LAYOUT_PASSES: usize = 16;
#[derive(Debug, Clone, Copy)]
pub(super) struct LayoutInputs<'a> {
pub(super) header_encrypted: bool,
pub(super) head_crypt_len: u64,
pub(super) main_flags: u64,
pub(super) volume_number: Option<u64>,
pub(super) archive_metadata: Option<ArchiveMetadataEntry<'a>>,
pub(super) body_len: u64,
pub(super) quick_open_payload_len: Option<u64>,
pub(super) recovery_percent: Option<u64>,
}
#[derive(Debug, Clone)]
pub(super) struct ResolvedLayout {
pub(super) main_extra: Vec<u8>,
pub(super) main_header_len: u64,
pub(super) recovery_offset: Option<u64>,
pub(super) recovery_prefix_len: Option<u64>,
}
pub(super) fn resolve_layout(inputs: &LayoutInputs<'_>) -> Result<ResolvedLayout> {
let signature_len = RAR50_SIGNATURE.len() as u64;
let quick_open_block_len = match inputs.quick_open_payload_len {
Some(payload_len) => {
stored_service_block_len(b"QO", payload_len, &[], inputs.header_encrypted)?
}
None => 0,
};
let mut quick_open_offset = inputs.quick_open_payload_len.map(|_| 0);
let mut recovery_offset = inputs.recovery_percent.map(|_| 0);
for _ in 0..MAX_LAYOUT_PASSES {
let main_extra =
resolved_main_extra(inputs.archive_metadata, quick_open_offset, recovery_offset)?;
let main_header_len = main_header_len(inputs, &main_extra)?;
let quick_open_position = signature_len
.checked_add(inputs.head_crypt_len)
.and_then(|value| value.checked_add(main_header_len))
.and_then(|value| value.checked_add(inputs.body_len))
.ok_or(Error::InvalidHeader("RAR 5 archive layout overflows"))?;
let recovery_position = quick_open_position
.checked_add(quick_open_block_len)
.ok_or(Error::InvalidHeader("RAR 5 archive layout overflows"))?;
let next_quick_open = quick_open_offset.map(|_| quick_open_position - signature_len);
let next_recovery = recovery_offset.map(|_| recovery_position - signature_len);
if next_quick_open == quick_open_offset && next_recovery == recovery_offset {
return Ok(ResolvedLayout {
main_extra,
main_header_len,
recovery_offset,
recovery_prefix_len: inputs.recovery_percent.map(|_| recovery_position),
});
}
quick_open_offset = next_quick_open;
recovery_offset = next_recovery;
}
Err(Error::InvalidHeader(
"RAR 5 writer could not resolve archive layout offsets",
))
}
fn main_header_len(inputs: &LayoutInputs<'_>, extra: &[u8]) -> Result<u64> {
let mut specific = Vec::new();
write_vint(&mut specific, inputs.main_flags);
if let Some(volume_number) = inputs.volume_number {
write_vint(&mut specific, volume_number);
}
let header = block_header_image(
HEAD_MAIN,
if extra.is_empty() { 0 } else { HFL_EXTRA },
None,
&specific,
extra,
)?;
Ok(emitted_header_len(
header.len() as u64,
inputs.header_encrypted,
))
}
pub(super) fn stored_service_block_len(
name: &[u8],
data_len: u64,
service_data: &[u8],
header_encrypted: bool,
) -> Result<u64> {
let mut extra = Vec::new();
super::headers::write_extra_record(&mut extra, FHEXTRA_SUBDATA, service_data);
let specific = stored_file_specific(name, data_len, Some(0), 0, None, 0)?;
let header = block_header_image(
HEAD_SERVICE,
HFL_EXTRA | HFL_DATA,
Some(data_len),
&specific,
&extra,
)?;
emitted_header_len(header.len() as u64, header_encrypted)
.checked_add(data_len)
.ok_or(Error::InvalidHeader("RAR 5 service block size overflows"))
}
fn emitted_header_len(plain_len: u64, header_encrypted: bool) -> u64 {
if header_encrypted {
16 + plain_len.div_ceil(16) * 16
} else {
plain_len
}
}
#[cfg(test)]
mod tests {
use super::*;
fn inputs(body_len: u64) -> LayoutInputs<'static> {
LayoutInputs {
header_encrypted: false,
head_crypt_len: 0,
main_flags: 0,
volume_number: None,
archive_metadata: None,
body_len,
quick_open_payload_len: None,
recovery_percent: Some(5),
}
}
fn assert_self_consistent(inputs: &LayoutInputs<'_>, layout: &ResolvedLayout) {
let signature_len = RAR50_SIGNATURE.len() as u64;
let quick_open_position =
signature_len + inputs.head_crypt_len + layout.main_header_len + inputs.body_len;
let rebuilt = super::main_header_len(inputs, &layout.main_extra).unwrap();
assert_eq!(rebuilt, layout.main_header_len, "main header size moved");
if let Some(offset) = layout.recovery_offset {
let quick_open_block_len = match inputs.quick_open_payload_len {
Some(len) => {
stored_service_block_len(b"QO", len, &[], inputs.header_encrypted).unwrap()
}
None => 0,
};
assert_eq!(
offset,
quick_open_position + quick_open_block_len - signature_len
);
assert_eq!(
layout.recovery_prefix_len,
Some(offset + signature_len),
"recovery protects everything before its own block"
);
}
}
#[test]
fn layout_settles_across_vint_width_boundaries() {
for boundary in [0x7fu64, 0x3fff, 0x1f_ffff, 0x0fff_ffff] {
for delta in [-3i64, -2, -1, 0, 1, 2, 3] {
let body_len = (boundary as i64 + delta).max(0) as u64;
let inputs = inputs(body_len);
let layout = resolve_layout(&inputs).unwrap();
assert_self_consistent(&inputs, &layout);
}
}
}
#[test]
fn layout_settles_with_quick_open_and_recovery_together() {
for body_len in [0u64, 100, 0x3ffe, 0x4001, 1 << 20] {
let mut inputs = inputs(body_len);
inputs.quick_open_payload_len = Some(4096);
let layout = resolve_layout(&inputs).unwrap();
assert!(layout.recovery_offset.is_some());
assert_self_consistent(&inputs, &layout);
}
}
#[test]
fn layout_accounts_for_encrypted_header_padding() {
let mut inputs = inputs(1024);
inputs.header_encrypted = true;
inputs.head_crypt_len = 60;
let layout = resolve_layout(&inputs).unwrap();
assert_eq!(
layout.main_header_len % 16,
0,
"an IV plus padded ciphertext is a multiple of the block size"
);
assert_self_consistent(&inputs, &layout);
}
#[test]
fn layout_without_locator_features_has_no_offsets() {
let mut inputs = inputs(4096);
inputs.recovery_percent = None;
let layout = resolve_layout(&inputs).unwrap();
assert_eq!(layout.recovery_offset, None);
assert_eq!(layout.recovery_prefix_len, None);
assert!(layout.main_extra.is_empty());
}
}