use std::fs::File;
use std::io::{self, Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use bytes::Bytes;
use crate::checksum::Md5State;
use crate::error::{Par2Error, Result};
use crate::types::{CancellationToken, RecoveryExponent};
#[derive(Debug, Clone)]
pub enum RecoverySliceData {
InMemory(Bytes),
FileBacked {
path: PathBuf,
offset: u64,
len: usize,
packet_hash: Option<[u8; 16]>,
},
}
impl RecoverySliceData {
pub fn in_memory(data: Bytes) -> Self {
Self::InMemory(data)
}
pub fn file_backed(path: PathBuf, offset: u64, len: usize) -> Self {
Self::FileBacked {
path,
offset,
len,
packet_hash: None,
}
}
pub fn file_backed_with_hash(
path: PathBuf,
offset: u64,
len: usize,
packet_hash: [u8; 16],
) -> Self {
Self::FileBacked {
path,
offset,
len,
packet_hash: Some(packet_hash),
}
}
pub fn validate_packet_hash(
&self,
recovery_set_id: &[u8; 16],
exponent: RecoveryExponent,
) -> io::Result<bool> {
self.validate_packet_hash_inner(recovery_set_id, exponent, None)
.map_err(|error| match error {
Par2Error::Io(error) => error,
Par2Error::Cancelled => io::Error::new(
io::ErrorKind::Interrupted,
"recovery packet hash validation was cancelled",
),
error => io::Error::other(error.to_string()),
})
}
pub(crate) fn validate_packet_hash_cancellable(
&self,
recovery_set_id: &[u8; 16],
exponent: RecoveryExponent,
cancellation: &CancellationToken,
) -> Result<bool> {
self.validate_packet_hash_inner(recovery_set_id, exponent, Some(cancellation))
}
fn validate_packet_hash_inner(
&self,
recovery_set_id: &[u8; 16],
exponent: RecoveryExponent,
cancellation: Option<&CancellationToken>,
) -> Result<bool> {
let Self::FileBacked {
path,
offset,
len,
packet_hash: Some(expected),
} = self
else {
return Ok(true);
};
let mut hasher = Md5State::new();
hasher.update(recovery_set_id);
hasher.update(super::header::TYPE_RECOVERY);
hasher.update(&exponent.to_le_bytes());
let mut file = File::open(path)?;
file.seek(SeekFrom::Start(*offset))?;
let mut remaining = *len;
let mut buf = vec![0u8; remaining.clamp(1, 256 * 1024)];
while remaining > 0 {
if cancellation.is_some_and(CancellationToken::is_cancelled) {
return Err(Par2Error::Cancelled);
}
let take = remaining.min(buf.len());
file.read_exact(&mut buf[..take]).map_err(Par2Error::Io)?;
hasher.update(&buf[..take]);
remaining -= take;
}
let file_len = file
.metadata()
.ok()
.map_or(*offset + *len as u64, |metadata| metadata.len());
crate::file_cache::drop_touched_file_cache(&file, path, file_len, *offset, *len as u64);
Ok(hasher.finalize() == *expected)
}
pub fn len(&self) -> usize {
match self {
Self::InMemory(data) => data.len(),
Self::FileBacked { len, .. } => *len,
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Self::InMemory(data) => Some(data.as_ref()),
Self::FileBacked { .. } => None,
}
}
pub fn file_span(&self) -> Option<(&Path, u64, usize)> {
match self {
Self::InMemory(_) => None,
Self::FileBacked {
path, offset, len, ..
} => Some((path.as_path(), *offset, *len)),
}
}
pub fn to_vec(&self) -> io::Result<Vec<u8>> {
let mut out = vec![0u8; self.len()];
self.read_range_padded(0, &mut out)?;
Ok(out)
}
pub fn read_range_padded(&self, start: usize, dst: &mut [u8]) -> io::Result<()> {
dst.fill(0);
match self {
Self::InMemory(data) => {
if start >= data.len() {
return Ok(());
}
let end = (start + dst.len()).min(data.len());
let copy_len = end - start;
dst[..copy_len].copy_from_slice(&data[start..end]);
Ok(())
}
Self::FileBacked {
path, offset, len, ..
} => {
if start >= *len {
return Ok(());
}
let read_len = dst.len().min(*len - start);
let mut file = File::open(path)?;
read_exact_at_fallback(&mut file, offset + start as u64, &mut dst[..read_len])?;
let file_len = file
.metadata()
.ok()
.map_or(*len as u64, |metadata| metadata.len());
crate::file_cache::drop_touched_file_cache(
&file,
path,
file_len,
offset + start as u64,
read_len as u64,
);
Ok(())
}
}
}
}
impl From<Bytes> for RecoverySliceData {
fn from(value: Bytes) -> Self {
Self::InMemory(value)
}
}
impl From<Vec<u8>> for RecoverySliceData {
fn from(value: Vec<u8>) -> Self {
Self::InMemory(Bytes::from(value))
}
}
fn read_exact_at_fallback(file: &mut File, offset: u64, buf: &mut [u8]) -> io::Result<()> {
file.seek(SeekFrom::Start(offset))?;
file.read_exact(buf)
}
#[derive(Debug, Clone)]
pub struct RecoverySlicePacket {
pub exponent: RecoveryExponent,
pub data: RecoverySliceData,
}
impl RecoverySlicePacket {
pub fn parse(body: &[u8]) -> Result<Self> {
if body.len() <= 4 {
return Err(Par2Error::InvalidRecoveryPacket {
reason: format!("body too short: {} bytes, need more than 4", body.len()),
});
}
let exponent = u32::from_le_bytes(body[0..4].try_into().unwrap());
let data = RecoverySliceData::in_memory(Bytes::copy_from_slice(&body[4..]));
Ok(RecoverySlicePacket { exponent, data })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_recovery() {
let mut body = Vec::new();
body.extend_from_slice(&42u32.to_le_bytes());
body.extend_from_slice(&[0xAB; 128]);
let pkt = RecoverySlicePacket::parse(&body).unwrap();
assert_eq!(pkt.exponent, 42);
assert_eq!(pkt.data.len(), 128);
assert!(pkt.data.as_bytes().unwrap().iter().all(|&b| b == 0xAB));
}
#[test]
fn reject_recovery_empty_data() {
let body = 0u32.to_le_bytes();
let err = RecoverySlicePacket::parse(&body).unwrap_err();
assert!(matches!(err, Par2Error::InvalidRecoveryPacket { .. }));
}
#[test]
fn reject_too_short() {
let body = [0u8; 2];
let err = RecoverySlicePacket::parse(&body).unwrap_err();
assert!(matches!(err, Par2Error::InvalidRecoveryPacket { .. }));
}
#[test]
fn file_backed_hash_validation_honors_cancellation() {
let file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(file.path(), vec![0xA5; 512 * 1024]).unwrap();
let data = RecoverySliceData::file_backed_with_hash(
file.path().to_path_buf(),
0,
512 * 1024,
[0; 16],
);
let cancellation = CancellationToken::new();
cancellation.cancel();
let error = data
.validate_packet_hash_cancellable(&[0; 16], 0, &cancellation)
.unwrap_err();
assert!(matches!(error, Par2Error::Cancelled));
}
}