chore: initial sanitized public snapshot
This commit is contained in:
@@ -0,0 +1,474 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::model::{DownloadFile, PieceIndex, PieceState};
|
||||
|
||||
use super::{
|
||||
model::{ControlFileError, ControlFileVersion, ControlMetadata},
|
||||
text::{decode_control_metadata, encode_control_metadata},
|
||||
};
|
||||
|
||||
/// Magic trailer marker for text metadata appended to binary control files.
|
||||
const BINARY_CONTROL_TRAILER_MAGIC: &[u8; 8] = b"AR2RTXT1";
|
||||
/// Block length used by upstream binary control files for piece bitfields.
|
||||
const BINARY_PIECE_BLOCK_LENGTH: u64 = 16 * 1024;
|
||||
|
||||
/// Writes the simplified control-file representation used by the Rust implementation.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlFileError`] when the encoded control file cannot be written.
|
||||
pub fn write_aria2_control_file(
|
||||
path: &Path,
|
||||
metadata: &ControlMetadata,
|
||||
) -> Result<(), ControlFileError> {
|
||||
let encoded = encode_binary_control_prefix(metadata)
|
||||
.and_then(|mut prefix| {
|
||||
let trailer = encode_binary_control_trailer(metadata)?;
|
||||
prefix.extend_from_slice(&trailer);
|
||||
Some(prefix)
|
||||
})
|
||||
.unwrap_or_else(|| encode_control_metadata(metadata).into_bytes());
|
||||
fs::write(path, encoded)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reads the simplified control-file representation from disk.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlFileError`] when the control file cannot be read or decoded.
|
||||
pub fn read_aria2_control_file(path: &Path) -> Result<ControlMetadata, ControlFileError> {
|
||||
let content = fs::read(path)?;
|
||||
if looks_like_binary_control_file(&content) {
|
||||
return decode_binary_control_metadata(path, &content);
|
||||
}
|
||||
let text = String::from_utf8(content)
|
||||
.map_err(|_| ControlFileError::Parse("control file is not valid UTF-8".to_owned()))?;
|
||||
decode_control_metadata(&text)
|
||||
}
|
||||
|
||||
/// Reads an upstream-compatible binary `.aria2` control file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`ControlFileError`] when the control file cannot be read or decoded.
|
||||
pub fn read_aria2_binary_control_file(path: &Path) -> Result<ControlMetadata, ControlFileError> {
|
||||
let content = fs::read(path)?;
|
||||
decode_binary_control_metadata(path, &content)
|
||||
}
|
||||
|
||||
/// Detects whether the raw bytes look like an upstream binary control file.
|
||||
fn looks_like_binary_control_file(content: &[u8]) -> bool {
|
||||
matches!(
|
||||
content.get(0..2),
|
||||
Some(bytes) if bytes == [0x00, 0x00] || bytes == [0x00, 0x01]
|
||||
)
|
||||
}
|
||||
|
||||
/// Encodes the upstream-compatible binary control-file prefix when possible.
|
||||
fn encode_binary_control_prefix(metadata: &ControlMetadata) -> Option<Vec<u8>> {
|
||||
let piece_length = metadata.files.first()?.piece_length;
|
||||
if piece_length == 0 || piece_length > u64::from(u32::MAX) {
|
||||
return None;
|
||||
}
|
||||
let total_length = metadata.files.iter().map(|file| file.length).sum::<u64>();
|
||||
let piece_count = binary_piece_count(total_length, piece_length)?;
|
||||
let mut verified_bitfield = vec![0_u8; binary_bitfield_length(piece_count)?];
|
||||
|
||||
let mut normalized_states = std::collections::BTreeMap::<u32, PieceState>::new();
|
||||
for (index, state) in &metadata.piece_states {
|
||||
normalized_states.insert(index.0, *state);
|
||||
}
|
||||
|
||||
let mut inflight_pieces = Vec::new();
|
||||
for (index, state) in normalized_states {
|
||||
let span_length = control_piece_span_bytes(index, piece_length, total_length);
|
||||
if span_length > u64::from(u32::MAX) {
|
||||
return None;
|
||||
}
|
||||
match state {
|
||||
PieceState::Verified => set_binary_bit(&mut verified_bitfield, index),
|
||||
PieceState::InFlight => inflight_pieces.push((
|
||||
index,
|
||||
u32::try_from(span_length).ok()?,
|
||||
vec![0_u8; binary_bitfield_length(binary_piece_block_count(span_length)?)?],
|
||||
)),
|
||||
PieceState::Pending | PieceState::Failed => {}
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = Vec::new();
|
||||
push_u16_be(&mut bytes, 1_u16);
|
||||
push_u32_be(&mut bytes, 0_u32);
|
||||
push_u32_be(&mut bytes, 0_u32);
|
||||
push_u32_be(&mut bytes, u32::try_from(piece_length).ok()?);
|
||||
bytes.extend_from_slice(&total_length.to_be_bytes());
|
||||
bytes.extend_from_slice(&0_u64.to_be_bytes());
|
||||
push_u32_be(&mut bytes, u32::try_from(verified_bitfield.len()).ok()?);
|
||||
bytes.extend_from_slice(&verified_bitfield);
|
||||
push_u32_be(&mut bytes, u32::try_from(inflight_pieces.len()).ok()?);
|
||||
for (index, length, bitfield) in inflight_pieces {
|
||||
push_u32_be(&mut bytes, index);
|
||||
push_u32_be(&mut bytes, length);
|
||||
push_u32_be(&mut bytes, u32::try_from(bitfield.len()).ok()?);
|
||||
bytes.extend_from_slice(&bitfield);
|
||||
}
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Encodes a text metadata trailer that can be appended to a binary control file.
|
||||
fn encode_binary_control_trailer(metadata: &ControlMetadata) -> Option<Vec<u8>> {
|
||||
let text = encode_control_metadata(metadata);
|
||||
let text_len = u32::try_from(text.len()).ok()?;
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(BINARY_CONTROL_TRAILER_MAGIC);
|
||||
push_u32_be(&mut bytes, text_len);
|
||||
bytes.extend_from_slice(text.as_bytes());
|
||||
Some(bytes)
|
||||
}
|
||||
|
||||
/// Decodes an upstream-compatible binary control file into normalized metadata.
|
||||
#[expect(
|
||||
clippy::too_many_lines,
|
||||
reason = "binary control metadata decoding keeps the upstream v0/v1 format walk in one auditable parser"
|
||||
)]
|
||||
fn decode_binary_control_metadata(
|
||||
path: &Path,
|
||||
content: &[u8],
|
||||
) -> Result<ControlMetadata, ControlFileError> {
|
||||
if !looks_like_binary_control_file(content) {
|
||||
return Err(ControlFileError::UnsupportedBinaryCompatibility);
|
||||
}
|
||||
|
||||
let mut offset = 0_usize;
|
||||
let version = match read_binary_slice(content, &mut offset, 2)? {
|
||||
[0x00, 0x00] => 0_u16,
|
||||
[0x00, 0x01] => 1_u16,
|
||||
_ => return Err(ControlFileError::UnsupportedBinaryCompatibility),
|
||||
};
|
||||
|
||||
let _extension = read_binary_u32(content, &mut offset, version)?;
|
||||
let info_hash_length = u32_to_usize(read_binary_u32(content, &mut offset, version)?)?;
|
||||
if info_hash_length > 20 {
|
||||
return Err(ControlFileError::Parse(
|
||||
"invalid binary info hash length".to_owned(),
|
||||
));
|
||||
}
|
||||
let _info_hash = read_binary_slice(content, &mut offset, info_hash_length)?;
|
||||
|
||||
let piece_length = u64::from(read_binary_u32(content, &mut offset, version)?);
|
||||
if piece_length == 0 {
|
||||
return Err(ControlFileError::Parse(
|
||||
"binary piece length must not be 0".to_owned(),
|
||||
));
|
||||
}
|
||||
let total_length = read_binary_u64(content, &mut offset, version)?;
|
||||
let _upload_length = read_binary_u64(content, &mut offset, version)?;
|
||||
|
||||
let piece_count = binary_piece_count(total_length, piece_length).ok_or_else(|| {
|
||||
ControlFileError::Parse("binary piece count exceeds supported range".to_owned())
|
||||
})?;
|
||||
let bitfield_length = u32_to_usize(read_binary_u32(content, &mut offset, version)?)?;
|
||||
let expected_bitfield_length = binary_bitfield_length(piece_count).ok_or_else(|| {
|
||||
ControlFileError::Parse("binary bitfield length exceeds supported range".to_owned())
|
||||
})?;
|
||||
if bitfield_length != expected_bitfield_length {
|
||||
return Err(ControlFileError::Parse(format!(
|
||||
"binary bitfield length mismatch: expected {expected_bitfield_length}, got {bitfield_length}"
|
||||
)));
|
||||
}
|
||||
let verified_bitfield = read_binary_slice(content, &mut offset, bitfield_length)?;
|
||||
|
||||
let mut completed_length = 0_u64;
|
||||
let mut piece_states = std::collections::BTreeMap::<u32, PieceState>::new();
|
||||
for index in 0..piece_count {
|
||||
if binary_bit_is_set(verified_bitfield, index) {
|
||||
completed_length = completed_length
|
||||
.checked_add(control_piece_span_bytes(index, piece_length, total_length))
|
||||
.ok_or_else(|| {
|
||||
ControlFileError::Parse("binary completed length overflowed".to_owned())
|
||||
})?;
|
||||
piece_states.insert(index, PieceState::Verified);
|
||||
}
|
||||
}
|
||||
|
||||
let inflight_count = read_binary_u32(content, &mut offset, version)?;
|
||||
for _ in 0..inflight_count {
|
||||
let index = read_binary_u32(content, &mut offset, version)?;
|
||||
if total_length > 0 && index >= piece_count {
|
||||
return Err(ControlFileError::Parse(format!(
|
||||
"binary in-flight piece index out of range: {index}"
|
||||
)));
|
||||
}
|
||||
let piece_span = control_piece_span_bytes(index, piece_length, total_length);
|
||||
let encoded_length = u64::from(read_binary_u32(content, &mut offset, version)?);
|
||||
if encoded_length > piece_span {
|
||||
return Err(ControlFileError::Parse(format!(
|
||||
"binary in-flight piece length exceeds span: {encoded_length}"
|
||||
)));
|
||||
}
|
||||
let encoded_bitfield_length =
|
||||
u32_to_usize(read_binary_u32(content, &mut offset, version)?)?;
|
||||
let expected_piece_bitfield_length =
|
||||
binary_bitfield_length(binary_piece_block_count(encoded_length).ok_or_else(|| {
|
||||
ControlFileError::Parse(
|
||||
"binary in-flight piece block count exceeds supported range".to_owned(),
|
||||
)
|
||||
})?)
|
||||
.ok_or_else(|| {
|
||||
ControlFileError::Parse(
|
||||
"binary in-flight piece bitfield length exceeds supported range".to_owned(),
|
||||
)
|
||||
})?;
|
||||
if encoded_bitfield_length != expected_piece_bitfield_length {
|
||||
return Err(ControlFileError::Parse(format!(
|
||||
"binary in-flight piece bitfield length mismatch: expected {expected_piece_bitfield_length}, got {encoded_bitfield_length}"
|
||||
)));
|
||||
}
|
||||
let piece_bitfield = read_binary_slice(content, &mut offset, encoded_bitfield_length)?;
|
||||
let was_verified = piece_states.get(&index) == Some(&PieceState::Verified);
|
||||
if !was_verified {
|
||||
completed_length = completed_length
|
||||
.checked_add(binary_piece_completed_length(
|
||||
encoded_length,
|
||||
piece_bitfield,
|
||||
))
|
||||
.ok_or_else(|| {
|
||||
ControlFileError::Parse("binary completed length overflowed".to_owned())
|
||||
})?;
|
||||
}
|
||||
piece_states.insert(index, PieceState::InFlight);
|
||||
}
|
||||
|
||||
let trailer_slice = content.get(offset..).ok_or_else(|| {
|
||||
ControlFileError::Parse("binary control trailer offset is out of range".to_owned())
|
||||
})?;
|
||||
if let Some(trailer_metadata) = decode_binary_control_trailer(trailer_slice)? {
|
||||
return Ok(trailer_metadata);
|
||||
}
|
||||
|
||||
Ok(ControlMetadata {
|
||||
version: if version == 0 {
|
||||
ControlFileVersion::CURRENT
|
||||
} else {
|
||||
ControlFileVersion::BINARY_V1
|
||||
},
|
||||
files: vec![DownloadFile {
|
||||
path: infer_binary_control_target_path(path),
|
||||
length: total_length,
|
||||
piece_length,
|
||||
}],
|
||||
checksums: Vec::new(),
|
||||
piece_states: piece_states
|
||||
.into_iter()
|
||||
.map(|(index, state)| (PieceIndex(index), state))
|
||||
.collect(),
|
||||
completed_length,
|
||||
retry_count: 0,
|
||||
last_error: None,
|
||||
last_error_at_unix_ms: None,
|
||||
last_retry_at_unix_ms: None,
|
||||
next_retry_at_unix_ms: None,
|
||||
consecutive_failure_count: None,
|
||||
active_segment_count: None,
|
||||
resume_verified_at_unix_ms: None,
|
||||
resume_generation: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Decodes the optional text trailer appended to a binary control file.
|
||||
fn decode_binary_control_trailer(
|
||||
content: &[u8],
|
||||
) -> Result<Option<ControlMetadata>, ControlFileError> {
|
||||
if content.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
if !content.starts_with(BINARY_CONTROL_TRAILER_MAGIC) {
|
||||
return Ok(None);
|
||||
}
|
||||
let minimum_length = checked_add_usize(
|
||||
BINARY_CONTROL_TRAILER_MAGIC.len(),
|
||||
4,
|
||||
"binary control trailer",
|
||||
)?;
|
||||
if content.len() < minimum_length {
|
||||
return Err(ControlFileError::Parse(
|
||||
"binary control trailer is truncated".to_owned(),
|
||||
));
|
||||
}
|
||||
let mut offset = BINARY_CONTROL_TRAILER_MAGIC.len();
|
||||
let len_end = checked_add_usize(offset, 4, "binary control trailer length field")?;
|
||||
let len_bytes = content
|
||||
.get(offset..len_end)
|
||||
.ok_or_else(|| ControlFileError::Parse("binary control trailer is truncated".to_owned()))?;
|
||||
let text_len = u32_to_usize(u32::from_be_bytes(len_bytes.try_into().map_err(|_| {
|
||||
ControlFileError::Parse("binary control trailer is truncated".to_owned())
|
||||
})?))?;
|
||||
offset = len_end;
|
||||
let text_end = checked_add_usize(offset, text_len, "binary control trailer payload")?;
|
||||
if content.len() != text_end {
|
||||
return Err(ControlFileError::Parse(
|
||||
"binary control trailer length mismatch".to_owned(),
|
||||
));
|
||||
}
|
||||
let text_bytes = content
|
||||
.get(offset..text_end)
|
||||
.ok_or_else(|| ControlFileError::Parse("binary control trailer is truncated".to_owned()))?;
|
||||
let text = std::str::from_utf8(text_bytes).map_err(|_| {
|
||||
ControlFileError::Parse("binary control trailer is not valid UTF-8".to_owned())
|
||||
})?;
|
||||
decode_control_metadata(text).map(Some)
|
||||
}
|
||||
|
||||
/// Infers the payload target path from a binary `.aria2` file path.
|
||||
fn infer_binary_control_target_path(path: &Path) -> PathBuf {
|
||||
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
|
||||
return path.to_path_buf();
|
||||
};
|
||||
let Some(stem) = file_name.strip_suffix(".aria2") else {
|
||||
return path.to_path_buf();
|
||||
};
|
||||
path.with_file_name(stem)
|
||||
}
|
||||
|
||||
/// Computes the number of pieces required by a binary control file.
|
||||
fn binary_piece_count(total_length: u64, piece_length: u64) -> Option<u32> {
|
||||
if piece_length == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
u32::try_from(total_length.div_ceil(piece_length)).ok()
|
||||
}
|
||||
|
||||
/// Computes the number of sub-blocks represented by a piece bitfield.
|
||||
fn binary_piece_block_count(piece_length: u64) -> Option<u32> {
|
||||
if piece_length == 0 {
|
||||
return Some(0);
|
||||
}
|
||||
u32::try_from(piece_length.div_ceil(BINARY_PIECE_BLOCK_LENGTH)).ok()
|
||||
}
|
||||
|
||||
/// Computes the byte length needed to store `bit_count` bits.
|
||||
fn binary_bitfield_length(bit_count: u32) -> Option<usize> {
|
||||
usize::try_from(u64::from(bit_count).div_ceil(8)).ok()
|
||||
}
|
||||
|
||||
/// Appends a big-endian `u16` to a binary control buffer.
|
||||
fn push_u16_be(bytes: &mut Vec<u8>, value: u16) {
|
||||
bytes.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Appends a big-endian `u32` to a binary control buffer.
|
||||
fn push_u32_be(bytes: &mut Vec<u8>, value: u32) {
|
||||
bytes.extend_from_slice(&value.to_be_bytes());
|
||||
}
|
||||
|
||||
/// Marks one bit inside a binary control-file bitfield.
|
||||
fn set_binary_bit(bitfield: &mut [u8], index: u32) {
|
||||
let byte_index = usize::try_from(index >> 3).unwrap_or(usize::MAX);
|
||||
let bit_offset = 7_u32.saturating_sub(index & 7);
|
||||
if let Some(byte) = bitfield.get_mut(byte_index) {
|
||||
*byte |= 1_u8 << bit_offset;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether one bit is set inside a binary control-file bitfield.
|
||||
fn binary_bit_is_set(bitfield: &[u8], index: u32) -> bool {
|
||||
let byte_index = usize::try_from(index >> 3).unwrap_or(usize::MAX);
|
||||
let bit_offset = 7_u32.saturating_sub(index & 7);
|
||||
bitfield
|
||||
.get(byte_index)
|
||||
.is_some_and(|byte| (byte & (1_u8 << bit_offset)) != 0)
|
||||
}
|
||||
|
||||
/// Computes the completed bytes represented by one in-flight piece bitfield.
|
||||
fn binary_piece_completed_length(piece_length: u64, bitfield: &[u8]) -> u64 {
|
||||
let Some(block_count) = binary_piece_block_count(piece_length) else {
|
||||
return 0;
|
||||
};
|
||||
let mut completed = 0_u64;
|
||||
for block_index in 0..block_count {
|
||||
if binary_bit_is_set(bitfield, block_index) {
|
||||
let Some(block_start) = u64::from(block_index).checked_mul(BINARY_PIECE_BLOCK_LENGTH)
|
||||
else {
|
||||
return piece_length;
|
||||
};
|
||||
let Some(remaining) = piece_length.checked_sub(block_start) else {
|
||||
return piece_length;
|
||||
};
|
||||
completed = completed.saturating_add(remaining.min(BINARY_PIECE_BLOCK_LENGTH));
|
||||
}
|
||||
}
|
||||
completed.min(piece_length)
|
||||
}
|
||||
|
||||
/// Computes the span of one piece within the total binary payload length.
|
||||
fn control_piece_span_bytes(index: u32, piece_length: u64, total_length: u64) -> u64 {
|
||||
let Some(start) = u64::from(index).checked_mul(piece_length) else {
|
||||
return 0;
|
||||
};
|
||||
total_length.saturating_sub(start).min(piece_length)
|
||||
}
|
||||
|
||||
/// Reads one raw binary slice and advances the offset.
|
||||
fn read_binary_slice<'a>(
|
||||
content: &'a [u8],
|
||||
offset: &mut usize,
|
||||
len: usize,
|
||||
) -> Result<&'a [u8], ControlFileError> {
|
||||
let end = checked_add_usize(*offset, len, "binary control field")?;
|
||||
let slice = content
|
||||
.get(*offset..end)
|
||||
.ok_or_else(|| ControlFileError::Parse("binary control file is truncated".to_owned()))?;
|
||||
*offset = end;
|
||||
Ok(slice)
|
||||
}
|
||||
|
||||
/// Reads a `u32` field using the endianness defined by the binary version.
|
||||
fn read_binary_u32(
|
||||
content: &[u8],
|
||||
offset: &mut usize,
|
||||
version: u16,
|
||||
) -> Result<u32, ControlFileError> {
|
||||
let raw = read_binary_slice(content, offset, 4)?;
|
||||
let bytes: [u8; 4] = raw
|
||||
.try_into()
|
||||
.map_err(|_| ControlFileError::Parse("binary u32 field is truncated".to_owned()))?;
|
||||
Ok(if version == 0 {
|
||||
u32::from_le_bytes(bytes)
|
||||
} else {
|
||||
u32::from_be_bytes(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
/// Reads a `u64` field using the endianness defined by the binary version.
|
||||
fn read_binary_u64(
|
||||
content: &[u8],
|
||||
offset: &mut usize,
|
||||
version: u16,
|
||||
) -> Result<u64, ControlFileError> {
|
||||
let raw = read_binary_slice(content, offset, 8)?;
|
||||
let bytes: [u8; 8] = raw
|
||||
.try_into()
|
||||
.map_err(|_| ControlFileError::Parse("binary u64 field is truncated".to_owned()))?;
|
||||
Ok(if version == 0 {
|
||||
u64::from_le_bytes(bytes)
|
||||
} else {
|
||||
u64::from_be_bytes(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
/// Converts a `u32` length value into `usize` for buffer indexing.
|
||||
fn u32_to_usize(value: u32) -> Result<usize, ControlFileError> {
|
||||
usize::try_from(value).map_err(|_| {
|
||||
ControlFileError::Parse("binary length exceeds the supported platform size".to_owned())
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds two `usize` values and returns a parse error on overflow.
|
||||
fn checked_add_usize(lhs: usize, rhs: usize, context: &str) -> Result<usize, ControlFileError> {
|
||||
lhs.checked_add(rhs)
|
||||
.ok_or_else(|| ControlFileError::Parse(format!("{context} length overflowed")))
|
||||
}
|
||||
Reference in New Issue
Block a user