202 lines
7.5 KiB
Rust
202 lines
7.5 KiB
Rust
use std::{
|
|
collections::BTreeMap,
|
|
fs,
|
|
path::PathBuf,
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
use crate::{
|
|
checksum::Checksum,
|
|
model::{DownloadFile, PieceIndex, PieceState},
|
|
};
|
|
|
|
use super::*;
|
|
|
|
fn temp_control_path(name: &str) -> PathBuf {
|
|
let nanos = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("clock should be monotonic enough for test naming")
|
|
.as_nanos();
|
|
std::env::temp_dir().join(format!("aria2-rust-pro-storage-{name}-{nanos}.aria2"))
|
|
}
|
|
|
|
fn upstream_binary_fixture(version: u16) -> Vec<u8> {
|
|
let mut bytes = Vec::new();
|
|
match version {
|
|
0 => {
|
|
bytes.extend_from_slice(&0_u16.to_le_bytes());
|
|
bytes.extend_from_slice(&0_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&0_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&1024_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&81_920_u64.to_le_bytes());
|
|
bytes.extend_from_slice(&0_u64.to_le_bytes());
|
|
bytes.extend_from_slice(&10_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]);
|
|
bytes.extend_from_slice(&2_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&1024_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_le_bytes());
|
|
bytes.push(0x00);
|
|
bytes.extend_from_slice(&2_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&512_u32.to_le_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_le_bytes());
|
|
bytes.push(0x00);
|
|
}
|
|
1 => {
|
|
bytes.extend_from_slice(&1_u16.to_be_bytes());
|
|
bytes.extend_from_slice(&0_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&0_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&1024_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&81_920_u64.to_be_bytes());
|
|
bytes.extend_from_slice(&0_u64.to_be_bytes());
|
|
bytes.extend_from_slice(&10_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&[0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe]);
|
|
bytes.extend_from_slice(&2_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&1024_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_be_bytes());
|
|
bytes.push(0x00);
|
|
bytes.extend_from_slice(&2_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&512_u32.to_be_bytes());
|
|
bytes.extend_from_slice(&1_u32.to_be_bytes());
|
|
bytes.push(0x00);
|
|
}
|
|
other => panic!("unexpected test fixture version: {other}"),
|
|
}
|
|
bytes
|
|
}
|
|
|
|
fn piece_states_by_index(metadata: &ControlMetadata) -> BTreeMap<u32, PieceState> {
|
|
metadata
|
|
.piece_states
|
|
.iter()
|
|
.map(|(index, state)| (index.0, *state))
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn control_metadata_roundtrip_preserves_runtime_fields() {
|
|
let metadata = ControlMetadata {
|
|
version: ControlFileVersion::CURRENT,
|
|
files: vec![DownloadFile {
|
|
path: PathBuf::from("D:/downloads/a.bin"),
|
|
length: 1024,
|
|
piece_length: 256,
|
|
}],
|
|
checksums: vec![Checksum::Sha256("abc123".to_owned())],
|
|
piece_states: vec![
|
|
(PieceIndex(0), PieceState::Verified),
|
|
(PieceIndex(1), PieceState::InFlight),
|
|
],
|
|
completed_length: 512,
|
|
retry_count: 3,
|
|
last_error: Some("timeout".to_owned()),
|
|
last_error_at_unix_ms: Some(1_700_000_000_001),
|
|
last_retry_at_unix_ms: Some(1_700_000_000_010),
|
|
next_retry_at_unix_ms: Some(1_700_000_000_020),
|
|
consecutive_failure_count: Some(2),
|
|
active_segment_count: Some(4),
|
|
resume_verified_at_unix_ms: Some(1_700_000_000_100),
|
|
resume_generation: Some(7),
|
|
};
|
|
let encoded = encode_control_metadata(&metadata);
|
|
let decoded = decode_control_metadata(&encoded).unwrap();
|
|
assert_eq!(decoded, metadata);
|
|
}
|
|
|
|
#[test]
|
|
fn control_metadata_decode_keeps_backward_compat_defaults() {
|
|
let raw = "version=1.0\nfiles=0\nchecksums=0\ncompleted_length=12\nretry_count=1\npieces=0";
|
|
let decoded = decode_control_metadata(raw).unwrap();
|
|
assert_eq!(decoded.completed_length, 12);
|
|
assert_eq!(decoded.retry_count, 1);
|
|
assert_eq!(decoded.last_error, None);
|
|
assert_eq!(decoded.last_error_at_unix_ms, None);
|
|
assert_eq!(decoded.active_segment_count, None);
|
|
assert_eq!(decoded.resume_generation, None);
|
|
}
|
|
|
|
#[test]
|
|
fn binary_control_reader_loads_upstream_v1_fixture() {
|
|
let path = temp_control_path("binary-v1-fixture.bin");
|
|
fs::write(&path, upstream_binary_fixture(1)).unwrap();
|
|
|
|
let loaded = read_aria2_binary_control_file(&path).unwrap();
|
|
let states = piece_states_by_index(&loaded);
|
|
let file = loaded
|
|
.files
|
|
.first()
|
|
.expect("binary control fixture should contain one file");
|
|
|
|
assert_eq!(loaded.files.len(), 1);
|
|
assert_eq!(file.path, path.with_extension(""));
|
|
assert_eq!(file.length, 81_920);
|
|
assert_eq!(file.piece_length, 1_024);
|
|
assert_eq!(loaded.completed_length, 80_896);
|
|
assert_eq!(states.get(&0), Some(&PieceState::Verified));
|
|
assert_eq!(states.get(&1), Some(&PieceState::InFlight));
|
|
assert_eq!(states.get(&2), Some(&PieceState::InFlight));
|
|
assert_eq!(states.get(&78), Some(&PieceState::Verified));
|
|
assert_eq!(states.get(&79), None);
|
|
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
|
|
#[test]
|
|
fn control_file_reader_auto_detects_upstream_v0_binary_fixture() {
|
|
let path = temp_control_path("binary-v0-fixture.bin");
|
|
fs::write(&path, upstream_binary_fixture(0)).unwrap();
|
|
|
|
let loaded = read_aria2_control_file(&path).unwrap();
|
|
let states = piece_states_by_index(&loaded);
|
|
let file = loaded
|
|
.files
|
|
.first()
|
|
.expect("binary control fixture should contain one file");
|
|
|
|
assert_eq!(loaded.files.len(), 1);
|
|
assert_eq!(file.path, path.with_extension(""));
|
|
assert_eq!(file.length, 81_920);
|
|
assert_eq!(file.piece_length, 1_024);
|
|
assert_eq!(loaded.completed_length, 80_896);
|
|
assert_eq!(states.get(&0), Some(&PieceState::Verified));
|
|
assert_eq!(states.get(&1), Some(&PieceState::InFlight));
|
|
assert_eq!(states.get(&2), Some(&PieceState::InFlight));
|
|
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
|
|
#[test]
|
|
fn control_file_roundtrip_preserves_multiline_runtime_error() {
|
|
let metadata = ControlMetadata {
|
|
version: ControlFileVersion::CURRENT,
|
|
files: vec![DownloadFile {
|
|
path: PathBuf::from("D:/downloads/a.bin"),
|
|
length: 1024,
|
|
piece_length: 256,
|
|
}],
|
|
checksums: Vec::new(),
|
|
piece_states: vec![(PieceIndex(0), PieceState::Verified)],
|
|
completed_length: 128,
|
|
retry_count: 2,
|
|
last_error: Some("timeout\nmirror=2;retry".to_owned()),
|
|
last_error_at_unix_ms: Some(1_700_000_001_111),
|
|
last_retry_at_unix_ms: Some(1_700_000_001_222),
|
|
next_retry_at_unix_ms: Some(1_700_000_001_333),
|
|
consecutive_failure_count: Some(4),
|
|
active_segment_count: Some(6),
|
|
resume_verified_at_unix_ms: Some(1_700_000_001_444),
|
|
resume_generation: Some(12),
|
|
};
|
|
let path = temp_control_path("roundtrip");
|
|
write_aria2_control_file(&path, &metadata).unwrap();
|
|
let raw = fs::read(&path).unwrap();
|
|
assert!(
|
|
raw.starts_with(&[0x00, 0x01]),
|
|
"control file should start with the upstream binary v0001 header"
|
|
);
|
|
let loaded = read_aria2_control_file(&path).unwrap();
|
|
assert_eq!(loaded, metadata);
|
|
let _ = fs::remove_file(path);
|
|
}
|