1use std::fmt::{Display, Formatter};
7use std::fs;
8use std::io::Write;
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12use crate::error::{Aria2Error, FatalError, Result};
13use tracing::{debug, info, warn};
14
15#[derive(Clone, Debug)]
17pub struct PeerAddr {
18 pub ip: String,
20 pub port: u16,
22}
23
24impl Display for PeerAddr {
25 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}:{}", self.ip, self.port)
27 }
28}
29
30#[derive(Clone, Debug, Default)]
32pub struct DownloadStats {
33 pub uploaded_bytes: u64,
35 pub downloaded_bytes: u64,
37 pub upload_speed: f64,
39 pub download_speed: f64,
41 pub elapsed_seconds: u64,
43}
44
45#[derive(Clone, Debug)]
49pub struct BtProgress {
50 pub info_hash: [u8; 20],
52 pub bitfield: Vec<u8>,
54 pub peers: Vec<PeerAddr>,
56 pub stats: DownloadStats,
58 pub piece_length: u32,
60 pub total_size: u64,
62 pub num_pieces: u32,
64 pub save_time: SystemTime,
66 pub version: u32,
68}
69
70impl Default for BtProgress {
71 fn default() -> Self {
72 BtProgress {
73 info_hash: [0u8; 20],
74 bitfield: Vec::new(),
75 peers: Vec::new(),
76 stats: DownloadStats::default(),
77 piece_length: 0,
78 total_size: 0,
79 num_pieces: 0,
80 save_time: SystemTime::UNIX_EPOCH,
81 version: 1,
82 }
83 }
84}
85
86impl BtProgress {
87 pub fn to_hex_hash(&self) -> String {
93 self.info_hash
94 .iter()
95 .map(|byte| format!("{:02x}", byte))
96 .collect()
97 }
98
99 pub fn completion_ratio(&self) -> f64 {
107 if self.num_pieces == 0 || self.bitfield.is_empty() {
108 return 0.0;
109 }
110
111 let mut set_bits = 0u32;
112 for &byte in &self.bitfield {
113 set_bits += byte.count_ones();
114 }
115
116 set_bits as f64 / self.num_pieces as f64
117 }
118}
119
120pub struct BtProgressManager {
125 progress_dir: PathBuf,
127}
128
129impl BtProgressManager {
130 pub fn new(progress_dir: &Path) -> Result<Self> {
142 fs::create_dir_all(progress_dir).map_err(|e| {
143 Aria2Error::Fatal(FatalError::Config(format!(
144 "Failed to create progress directory {}: {}",
145 progress_dir.display(),
146 e
147 )))
148 })?;
149
150 info!(path = %progress_dir.display(), "BT progress manager initialized");
151
152 Ok(BtProgressManager {
153 progress_dir: progress_dir.to_path_buf(),
154 })
155 }
156
157 pub fn get_progress_file_path(&self, info_hash: &[u8; 20]) -> PathBuf {
159 let hex_hash: String = info_hash.iter().map(|b| format!("{:02x}", b)).collect();
160 self.progress_dir.join(format!("{}.aria2", hex_hash))
161 }
162
163 pub fn save_progress(&self, info_hash: &[u8; 20], progress: &BtProgress) -> Result<()> {
178 let file_path = self.get_progress_file_path(info_hash);
179
180 debug!(
181 path = %file_path.display(),
182 hash = %progress.to_hex_hash(),
183 "Saving BT progress"
184 );
185
186 let tmp_path = {
188 let timestamp = std::time::SystemTime::now()
189 .duration_since(std::time::UNIX_EPOCH)
190 .unwrap_or_default()
191 .as_nanos();
192 let thread_id = std::thread::current().id();
193 let unique_suffix = format!(
195 "{:x}.tmp.{}",
196 timestamp,
197 format!("{:?}", thread_id)
199 .chars()
200 .filter(|c| c.is_ascii_hexdigit())
201 .collect::<String>()
202 );
203 file_path.with_extension(unique_suffix)
204 };
205
206 let content = self.serialize_progress(progress);
207 {
208 let mut file = fs::File::create(&tmp_path).map_err(|e| {
209 Aria2Error::Io(format!("Failed to create temporary progress file: {}", e))
210 })?;
211
212 file.write_all(content.as_bytes())
213 .map_err(|e| Aria2Error::Io(format!("Failed to write progress data: {}", e)))?;
214
215 file.flush().map_err(|e| {
216 Aria2Error::Io(format!("Failed to flush progress file buffer: {}", e))
217 })?;
218 }
219
220 fs::rename(&tmp_path, &file_path).map_err(|e| {
222 let _ = fs::remove_file(&tmp_path);
224 Aria2Error::Io(format!("Failed to rename progress file: {}", e))
225 })?;
226
227 info!(
228 path = %file_path.display(),
229 pieces = progress.num_pieces,
230 ratio = progress.completion_ratio(),
231 "BT progress saved successfully"
232 );
233
234 Ok(())
235 }
236
237 fn serialize_progress(&self, progress: &BtProgress) -> String {
242 let estimated_size = 512 + progress.peers.len() * 24 + progress.bitfield.len() * 3;
245 let mut output = String::with_capacity(estimated_size);
246
247 use std::fmt::Write;
249 output.push_str("[Download]\n");
250 let _ = writeln!(output, "info_hash={}", progress.to_hex_hash());
251 let _ = writeln!(output, "version={}", progress.version);
252 let _ = writeln!(output, "num_pieces={}", progress.num_pieces);
253 let _ = writeln!(output, "piece_length={}", progress.piece_length);
254 let _ = writeln!(output, "total_size={}", progress.total_size);
255 let _ = writeln!(output, "downloaded={}", progress.stats.downloaded_bytes);
256 let _ = writeln!(output, "uploaded={}", progress.stats.uploaded_bytes);
257 let _ = writeln!(output, "elapsed={}", progress.stats.elapsed_seconds);
258
259 let _ = write!(output, "bitfield=");
261 for &byte in &progress.bitfield {
262 let _ = write!(output, "{:02x}", byte);
263 }
264 let _ = writeln!(output);
265
266 output.push_str("[Peers]\n");
268 for peer in &progress.peers {
269 let _ = writeln!(output, "{}", peer);
270 }
271
272 output
273 }
274
275 pub fn load_progress(&self, info_hash: &[u8; 20]) -> Result<BtProgress> {
294 let file_path = self.get_progress_file_path(info_hash);
295
296 debug!(
297 path = %file_path.display(),
298 hash = %info_hash.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
299 "Loading BT progress"
300 );
301
302 let content = match fs::read_to_string(&file_path) {
303 Ok(content) => content,
304 Err(e) => {
305 if e.kind() == std::io::ErrorKind::InvalidData {
307 return self.load_binary_format(info_hash, &file_path);
308 }
309 return Err(Aria2Error::Io(format!(
310 "Failed to read progress file: {}",
311 e
312 )));
313 }
314 };
315
316 self.parse_text_format(info_hash, &content, &file_path)
317 }
318
319 fn parse_text_format(
321 &self,
322 expected_hash: &[u8; 20],
323 content: &str,
324 file_path: &Path,
325 ) -> Result<BtProgress> {
326 let mut progress = BtProgress {
327 info_hash: *expected_hash,
328 bitfield: Vec::new(),
329 peers: Vec::new(),
330 stats: DownloadStats::default(),
331 piece_length: 0,
332 total_size: 0,
333 num_pieces: 0,
334 save_time: SystemTime::now(),
335 version: 1,
336 };
337
338 let mut current_section = String::new();
339
340 for line in content.lines() {
341 let line = line.trim();
342
343 if line.is_empty() {
345 continue;
346 }
347
348 if line.starts_with('[') && line.ends_with(']') {
350 current_section = line[1..line.len() - 1].to_string();
351 continue;
352 }
353
354 match current_section.as_str() {
355 "Download" => {
356 if let Some((key, value)) = line.split_once('=') {
357 match key.trim() {
358 "info_hash" => {
359 let file_hash = value.trim().to_lowercase();
361 let expected_hex: String =
362 expected_hash.iter().map(|b| format!("{:02x}", b)).collect();
363 if file_hash != expected_hex {
364 return Err(Aria2Error::Io(format!(
365 "Progress file info_hash mismatch: file={}, expected={}",
366 file_hash, expected_hex
367 )));
368 }
369 }
370 "version" => {
371 progress.version = value.trim().parse::<u32>().unwrap_or(1);
372 }
373 "num_pieces" => {
374 progress.num_pieces = value.trim().parse::<u32>().unwrap_or(0);
375 }
376 "piece_length" => {
377 progress.piece_length = value.trim().parse::<u32>().unwrap_or(0);
378 }
379 "total_size" => {
380 progress.total_size = value.trim().parse::<u64>().unwrap_or(0);
381 }
382 "downloaded" => {
383 progress.stats.downloaded_bytes =
384 value.trim().parse::<u64>().unwrap_or(0);
385 }
386 "uploaded" => {
387 progress.stats.uploaded_bytes =
388 value.trim().parse::<u64>().unwrap_or(0);
389 }
390 "elapsed" => {
391 progress.stats.elapsed_seconds =
392 value.trim().parse::<u64>().unwrap_or(0);
393 }
394 "bitfield" => {
395 progress.bitfield = self.parse_bitfield_hex(value.trim());
396 }
397 _ => {}
398 }
399 }
400 }
401 "Peers" => {
402 let peer = self.parse_peer_addr(line);
403 if let Some(p) = peer {
404 progress.peers.push(p);
405 }
406 }
407 _ => {}
408 }
409 }
410
411 info!(
412 path = %file_path.display(),
413 pieces = progress.num_pieces,
414 ratio = progress.completion_ratio(),
415 "BT progress loaded successfully"
416 );
417
418 Ok(progress)
419 }
420
421 fn load_binary_format(&self, _info_hash: &[u8; 20], _file_path: &Path) -> Result<BtProgress> {
423 Err(Aria2Error::Io(
425 "Old C++ binary format not supported, please use text format".to_string(),
426 ))
427 }
428
429 fn parse_bitfield_hex(&self, hex_str: &str) -> Vec<u8> {
431 let hex_str = hex_str.trim();
432 if hex_str.is_empty() {
433 return Vec::new();
434 }
435
436 (0..hex_str.len())
437 .step_by(2)
438 .filter_map(|i| {
439 if i + 1 < hex_str.len() {
440 u8::from_str_radix(&hex_str[i..i + 2], 16).ok()
441 } else {
442 None
443 }
444 })
445 .collect()
446 }
447
448 fn parse_peer_addr(&self, addr_str: &str) -> Option<PeerAddr> {
450 let addr_str = addr_str.trim();
451 if addr_str.is_empty() {
452 return None;
453 }
454
455 if let Some(colon_pos) = addr_str.rfind(':') {
457 let ip = addr_str[..colon_pos].trim().to_string();
458 let port: u16 = addr_str[colon_pos + 1..].trim().parse().ok()?;
459
460 Some(PeerAddr { ip, port })
461 } else {
462 None
463 }
464 }
465
466 pub fn remove_progress(&self, info_hash: &[u8; 20]) -> Result<()> {
476 let file_path = self.get_progress_file_path(info_hash);
477
478 debug!(
479 hash = %info_hash.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
480 "Deleting BT progress"
481 );
482
483 if file_path.exists() {
484 fs::remove_file(&file_path)
485 .map_err(|e| Aria2Error::Io(format!("Failed to delete progress file: {}", e)))?;
486
487 info!(path = %file_path.display(), "BT progress file deleted");
488 } else {
489 warn!(path = %file_path.display(), "Progress file does not exist");
490 }
491
492 Ok(())
493 }
494
495 pub fn list_saved_progresses(&self) -> Vec<[u8; 20]> {
503 let mut hashes = Vec::new();
504
505 if let Ok(entries) = fs::read_dir(&self.progress_dir) {
506 for entry in entries.flatten() {
507 let path = entry.path();
508 if let Some(name) = path.file_name() {
509 let name_str = name.to_string_lossy();
510 if let Some(hex_hash) = name_str.strip_suffix(".aria2") {
511 if let Ok(hash) = Self::hex_to_info_hash(hex_hash) {
514 hashes.push(hash);
515 } else {
516 warn!(
517 filename = %name_str,
518 "Cannot parse info_hash from progress file name"
519 );
520 }
521 }
522 }
523 }
524 }
525
526 debug!(count = hashes.len(), "Listed all saved BT progress");
527
528 hashes
529 }
530
531 fn hex_to_info_hash(hex_str: &str) -> std::result::Result<[u8; 20], ()> {
533 if hex_str.len() != 40 {
534 return Err(());
535 }
536
537 let mut hash = [0u8; 20];
538 for (i, byte) in hash.iter_mut().enumerate() {
539 *byte = u8::from_str_radix(&hex_str[i * 2..i * 2 + 2], 16).map_err(|_| ())?;
540 }
541
542 Ok(hash)
543 }
544}
545
546#[cfg(test)]
547mod tests {
548 use super::*;
549
550 #[test]
551 fn test_peer_addr_display() {
552 let peer = PeerAddr {
553 ip: "192.168.1.1".to_string(),
554 port: 6881,
555 };
556 assert_eq!(format!("{}", peer), "192.168.1.1:6881");
557 }
558
559 #[test]
560 fn test_bt_progress_to_hex_hash() {
561 let progress = BtProgress {
562 info_hash: [
563 0xAB, 0xCD, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
564 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
565 ],
566 ..Default::default()
567 };
568 assert_eq!(
569 progress.to_hex_hash(),
570 "abcd123400000000000000000000000000000000"
571 );
572 }
573
574 #[test]
575 fn test_completion_ratio_zero_pieces() {
576 let progress = BtProgress {
577 num_pieces: 0,
578 bitfield: vec![],
579 ..Default::default()
580 };
581 assert_eq!(progress.completion_ratio(), 0.0);
582 }
583
584 #[test]
585 fn test_completion_ratio_full() {
586 let progress = BtProgress {
588 num_pieces: 4,
589 bitfield: vec![0xFF], ..Default::default()
591 };
592 assert!(progress.completion_ratio() > 0.0);
594 }
595
596 #[test]
597 fn test_parse_bitfield_hex() {
598 let manager = create_test_manager();
599 let result = manager.parse_bitfield_hex("ff00ff");
600 assert_eq!(result, vec![0xFF, 0x00, 0xFF]);
601 }
602
603 #[test]
604 fn test_parse_bitfield_empty() {
605 let manager = create_test_manager();
606 let result = manager.parse_bitfield_hex("");
607 assert!(result.is_empty());
608 }
609
610 #[test]
611 fn test_parse_peer_addr_ipv4() {
612 let manager = create_test_manager();
613 let peer = manager.parse_peer_addr("192.168.1.100:6881").unwrap();
614 assert_eq!(peer.ip, "192.168.1.100");
615 assert_eq!(peer.port, 6881);
616 }
617
618 #[test]
619 fn test_parse_peer_addr_invalid() {
620 let manager = create_test_manager();
621 assert!(manager.parse_peer_addr("invalid").is_none());
622 assert!(manager.parse_peer_addr("").is_none());
623 }
624
625 #[test]
626 fn test_hex_to_info_hash_valid() {
627 let hex = "abcdef1234567890abcdef1234567890abcdef12";
628 let hash = BtProgressManager::hex_to_info_hash(hex).unwrap();
629 assert_eq!(
630 hash,
631 [
632 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF, 0x12, 0x34, 0x56,
633 0x78, 0x90, 0xAB, 0xCD, 0xEF, 0x12
634 ]
635 );
636 }
637
638 #[test]
639 fn test_hex_to_info_hash_invalid_length() {
640 assert!(BtProgressManager::hex_to_info_hash("abc123").is_err());
641 }
642
643 fn create_test_manager() -> BtProgressManager {
644 let dir = std::env::temp_dir().join("bt_progress_test_12345");
645 let _ = fs::create_dir_all(&dir);
646 BtProgressManager::new(&dir).unwrap()
647 }
648}