use crate::cli::RepairArgs;
use crate::password::read_archive_path_prompting;
use crate::volumes::{infer_part_index, parse_rar3_rev_volume, path_has_extension};
use crate::{resolve_password_args, CliError, CliResult};
use rars::Error;
use rars_crc32::crc32;
use std::fs;
use std::path::{Path, PathBuf};
const RAR50_SIGNATURE: &[u8] = b"Rar!\x1a\x07\x01\x00";
pub(crate) fn cmd_repair(args: RepairArgs) -> CliResult<()> {
let mut password = resolve_password_args(&args.password)?;
let paths = args.paths;
if paths.len() > 2 {
if password.is_some() {
return Err("REV repair does not use archive passwords".into());
}
return cmd_repair_volumes(&paths);
}
let archive = read_archive_path_prompting(&paths[0], &mut password);
match archive {
Ok(archive) => {
let mut output = fs::File::create(&paths[1])?;
archive
.repair_recovery_to(&mut output)
.map_err(|err| format!("failed to repair archive '{}': {err}", paths[0]))?;
}
Err(parse_error) => {
if !path_starts_with(&paths[0], RAR50_SIGNATURE)? {
return Err(parse_error);
}
let bytes = fs::read(&paths[0])?;
let repaired =
rars::rar50::repair_inline_recovery_bytes(&bytes).map_err(|repair_error| {
format!(
"failed to parse archive '{}': {}; raw inline recovery repair also failed: {}",
paths[0], parse_error, repair_error
)
})?;
fs::write(&paths[1], repaired)?;
}
}
println!("repaired {}", paths[1]);
Ok(())
}
fn path_starts_with(path: &str, prefix: &[u8]) -> CliResult<bool> {
let mut file = fs::File::open(path)?;
let mut buf = vec![0; prefix.len()];
let read = std::io::Read::read(&mut file, &mut buf)?;
Ok(read == prefix.len() && buf == prefix)
}
fn cmd_repair_volumes(paths: &[String]) -> CliResult<()> {
let input_paths = &paths[..paths.len() - 1];
for path in input_paths {
if path_has_extension(path, "rev") {
let bytes = fs::read(path)?;
if rars::rar50::Rev5Volume::parse(&bytes).is_ok() {
return cmd_repair_rev5(paths);
}
}
}
cmd_repair_rev3(paths)
}
fn cmd_repair_rev5(paths: &[String]) -> CliResult<()> {
let out_dir = PathBuf::from(paths.last().expect("outdir"));
fs::create_dir_all(&out_dir)?;
let input_paths = &paths[..paths.len() - 1];
let mut data_inputs = Vec::new();
let mut recovery = Vec::new();
for path in input_paths {
let bytes = fs::read(path)?;
match rars::rar50::Rev5Volume::parse(&bytes) {
Ok(rev) => recovery.push(rev),
Err(Error::UnsupportedSignature) => data_inputs.push((PathBuf::from(path), bytes)),
Err(error) => {
return Err(CliError::general(format!(
"failed to parse REV volume '{path}': {error}"
)))
}
}
}
let first = recovery
.first()
.ok_or("RAR 5 REV repair requires at least one .rev file")?;
let mut slots: Vec<Option<&[u8]>> = vec![None; usize::from(first.data_count)];
for (path, bytes) in &data_inputs {
if let Some(index) = infer_part_index(path, first.data_count) {
slots[index] = Some(bytes.as_slice());
continue;
}
if let Some((index, _)) =
first.data_volumes.iter().enumerate().find(|(_, meta)| {
bytes.len() as u64 == meta.file_size && crc32(bytes) == meta.crc32
})
{
slots[index] = Some(bytes.as_slice());
}
}
rars::rar50::repair_rev5_volumes_to(&slots, &recovery, |index, bytes| {
let path =
repaired_volume_path(&out_dir, &data_inputs, index, usize::from(first.data_count));
fs::write(&path, bytes)?;
println!("repaired {}", path.display());
Ok(())
})
.map_err(|err| CliError::general(format!("failed to repair RAR 5 REV volume set: {err}")))?;
Ok(())
}
fn cmd_repair_rev3(paths: &[String]) -> CliResult<()> {
let out_dir = PathBuf::from(paths.last().expect("outdir"));
fs::create_dir_all(&out_dir)?;
let input_paths = &paths[..paths.len() - 1];
let mut data_inputs = Vec::new();
let mut recovery_inputs = Vec::new();
let mut data_count = None;
let mut recovery_count = None;
for path in input_paths {
let bytes = fs::read(path)?;
if path_has_extension(path, "rev") {
let (recovery_index, rec_count, dat_count, payload) =
parse_rar3_rev_volume(Path::new(path), &bytes).ok_or_else(|| {
CliError::general(format!("failed to parse RAR 3 REV volume name '{path}'"))
})?;
if recovery_count
.replace(rec_count)
.is_some_and(|count| count != rec_count)
|| data_count
.replace(dat_count)
.is_some_and(|count| count != dat_count)
{
return Err("RAR 3 REV volume metadata differs across files".into());
}
recovery_inputs.push((recovery_index, payload));
} else {
data_inputs.push((PathBuf::from(path), bytes));
}
}
let data_count = data_count.ok_or("RAR 3 REV repair requires at least one .rev file")?;
let recovery_count =
recovery_count.ok_or("RAR 3 REV repair requires at least one .rev file")?;
let mut slots: Vec<Option<&[u8]>> = vec![None; data_count];
for (path, bytes) in &data_inputs {
if let Some(index) = infer_part_index(path, data_count as u16) {
slots[index] = Some(bytes.as_slice());
}
}
let recovery: Vec<(usize, &[u8])> = recovery_inputs
.iter()
.map(|(index, bytes)| (*index, bytes.as_slice()))
.collect();
rars::rar15_40::repair_rev3_volumes_to(&slots, recovery_count, &recovery, |index, bytes| {
let path = repaired_volume_path(&out_dir, &data_inputs, index, data_count);
fs::write(&path, bytes)?;
println!("repaired {}", path.display());
Ok(())
})
.map_err(|err| CliError::general(format!("failed to repair RAR 3 REV volume set: {err}")))?;
Ok(())
}
fn repaired_volume_path(
out_dir: &Path,
data_inputs: &[(PathBuf, Vec<u8>)],
index: usize,
data_count: usize,
) -> PathBuf {
let Some(first_path) = data_inputs.first().map(|(path, _)| path) else {
return out_dir.join(format!("repaired.part{}.rar", index + 1));
};
let Some(file_name) = first_path.file_name().and_then(|name| name.to_str()) else {
return out_dir.join(format!("repaired.part{}.rar", index + 1));
};
let lower = file_name.to_ascii_lowercase();
if let Some(part_pos) = lower.rfind(".part") {
if lower.ends_with(".rar") {
let digits = &file_name[part_pos + ".part".len()..file_name.len() - 4];
if !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit()) {
let width = digits.len().max(data_count.to_string().len());
return out_dir.join(format!(
"{}.part{:0width$}.rar",
&file_name[..part_pos],
index + 1,
width = width
));
}
}
}
if lower.ends_with(".rar")
|| lower.rsplit_once('.').is_some_and(|(_, ext)| {
ext.len() == 3
&& ext.starts_with('r')
&& ext[1..].bytes().all(|byte| byte.is_ascii_digit())
})
{
let first_out = out_dir.join(file_name);
return crate::volumes::volume_part_path(&first_out, index)
.unwrap_or_else(|_| out_dir.join(format!("repaired.part{}.rar", index + 1)));
}
out_dir.join(format!("repaired.part{}.rar", index + 1))
}