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 { 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 { 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); }