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 { 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 { 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> { 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::(); 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::::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> { 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 { 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::::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, 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 { 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 { 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::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, 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, 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 { 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 { 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::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 { lhs.checked_add(rhs) .ok_or_else(|| ControlFileError::Parse(format!("{context} length overflowed"))) }