Skip to main content

aria2_core/request/
request_group.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
5use tokio::sync::RwLock;
6use tracing::{debug, info};
7
8use crate::error::Result;
9use crate::rate_limiter::RateLimiter;
10use crate::segment::Segment;
11
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub enum DownloadStatus {
14    #[default]
15    Waiting,
16    Active,
17    Paused,
18    Error(String),
19    Complete,
20    Removed,
21}
22
23// Custom serde impl so every variant serializes as a plain lowercase string
24// (e.g. "active", "waiting", "error"), matching C++ aria2's wire format that
25// browser plugins expect. The `Error(String)` payload (error message) is not
26// emitted here; callers surface it via `StatusInfo.error_message` instead.
27impl Serialize for DownloadStatus {
28    fn serialize<S: serde::Serializer>(
29        &self,
30        serializer: S,
31    ) -> std::result::Result<S::Ok, S::Error> {
32        serializer.serialize_str(self.as_str())
33    }
34}
35
36impl<'de> Deserialize<'de> for DownloadStatus {
37    fn deserialize<D: serde::Deserializer<'de>>(
38        deserializer: D,
39    ) -> std::result::Result<Self, D::Error> {
40        let s = String::deserialize(deserializer)?;
41        match s.as_str() {
42            "waiting" => Ok(DownloadStatus::Waiting),
43            "active" => Ok(DownloadStatus::Active),
44            "paused" => Ok(DownloadStatus::Paused),
45            "error" => Ok(DownloadStatus::Error(String::new())),
46            "complete" => Ok(DownloadStatus::Complete),
47            "removed" => Ok(DownloadStatus::Removed),
48            _ => Err(serde::de::Error::unknown_variant(
49                "DownloadStatus",
50                &[
51                    "waiting", "active", "paused", "error", "complete", "removed",
52                ],
53            )),
54        }
55    }
56}
57
58impl fmt::Display for DownloadStatus {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        match self {
61            DownloadStatus::Waiting => write!(f, "waiting"),
62            DownloadStatus::Active => write!(f, "active"),
63            DownloadStatus::Paused => write!(f, "paused"),
64            DownloadStatus::Error(_) => write!(f, "error"),
65            DownloadStatus::Complete => write!(f, "complete"),
66            DownloadStatus::Removed => write!(f, "removed"),
67        }
68    }
69}
70
71impl DownloadStatus {
72    pub fn is_active(&self) -> bool {
73        matches!(self, DownloadStatus::Active | DownloadStatus::Waiting)
74    }
75
76    pub fn is_completed(&self) -> bool {
77        matches!(self, DownloadStatus::Complete)
78    }
79
80    pub fn is_paused(&self) -> bool {
81        matches!(self, DownloadStatus::Paused)
82    }
83
84    pub fn is_stopped(&self) -> bool {
85        !self.is_active() && !matches!(self, DownloadStatus::Removed)
86    }
87
88    pub fn as_str(&self) -> &'static str {
89        match self {
90            DownloadStatus::Active => "active",
91            DownloadStatus::Waiting => "waiting",
92            DownloadStatus::Paused => "paused",
93            DownloadStatus::Error(_) => "error",
94            DownloadStatus::Complete => "complete",
95            DownloadStatus::Removed => "removed",
96        }
97    }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct GroupId(pub u64);
102
103impl GroupId {
104    pub fn new(id: u64) -> Self {
105        GroupId(id)
106    }
107
108    pub fn value(&self) -> u64 {
109        self.0
110    }
111
112    /// Create GroupId from hex string (e.g., "deadbeef")
113    ///
114    /// Returns None if the string is not valid hex or too large for u64.
115    pub fn from_hex_string(hex_str: &str) -> Option<Self> {
116        let trimmed = hex_str.trim_start_matches("0x");
117        if trimmed.is_empty() {
118            return None;
119        }
120        let val = u64::from_str_radix(trimmed, 16).ok()?;
121        Some(GroupId(val))
122    }
123
124    /// Generate a random GroupId using current timestamp + random
125    pub fn new_random() -> Self {
126        use std::collections::hash_map::DefaultHasher;
127        use std::hash::{Hash, Hasher};
128        use std::time::{SystemTime, UNIX_EPOCH};
129
130        let nanos = SystemTime::now()
131            .duration_since(UNIX_EPOCH)
132            .unwrap_or_default()
133            .as_nanos();
134        let mut hasher = DefaultHasher::new();
135        nanos.hash(&mut hasher);
136        rand::random::<u64>().hash(&mut hasher);
137        GroupId(hasher.finish())
138    }
139
140    /// Format GID as hex string (lowercase, no prefix)
141    pub fn to_hex_string(&self) -> String {
142        format!("{:016x}", self.0)
143    }
144}
145
146#[derive(Debug, Clone)]
147pub struct DownloadOptions {
148    pub split: Option<u16>,
149    pub max_connection_per_server: Option<u16>,
150    pub max_download_limit: Option<u64>,
151    pub max_upload_limit: Option<u64>,
152    pub dir: Option<String>,
153    pub out: Option<String>,
154    /// File allocation strategy: "none", "prealloc", "falloc", "trunc", or "mmap".
155    /// When "mmap", `MmapDiskWriter` is used for files above `mmap_threshold`.
156    pub file_allocation: Option<String>,
157    /// File size threshold (bytes) above which mmap writes are used when
158    /// `file_allocation = "mmap"`. Default: 256 MiB.
159    pub mmap_threshold: Option<u64>,
160    /// Zero-fill allocated blocks after fallocate on platforms that don't
161    /// zero-fill (macOS `F_PREALLOCATE`, Windows `SetFileValidData`).
162    /// Prevents exposure of residual disk data at a performance cost.
163    /// Has no effect on Linux. Defaults to `false` (matches
164    /// `constants::DEFAULT_SECURE_FALLOC`).
165    pub secure_falloc: bool,
166    pub seed_time: Option<u64>,
167    pub seed_ratio: Option<f64>,
168    pub checksum: Option<(String, String)>,
169    pub cookie_file: Option<String>,
170    pub cookies: Option<String>,
171    pub bt_force_encrypt: bool,
172    pub bt_require_crypto: bool,
173    pub enable_dht: bool,
174    pub dht_listen_port: Option<u16>,
175    pub dht_entry_point: Option<Vec<String>>,
176    pub enable_public_trackers: bool,
177    pub bt_piece_selection_strategy: String,
178    pub bt_endgame_threshold: u32,
179    pub max_retries: u32,
180    pub retry_wait: u64,
181    pub http_proxy: Option<String>,
182    pub all_proxy: Option<String>,
183    pub https_proxy: Option<String>,
184    pub ftp_proxy: Option<String>,
185    pub no_proxy: Option<String>,
186    pub dht_file_path: Option<String>,
187
188    // ------------------------------------------------------------------
189    // Choking algorithm configuration (BT tit-for-tat)
190    // ------------------------------------------------------------------
191    /// Maximum number of peers to unchoke simultaneously during seeding.
192    /// Default: 4. Set to enable the choking algorithm.
193    pub bt_max_upload_slots: Option<u32>,
194
195    /// Interval in seconds between optimistic unchokes.
196    /// Default: 30.
197    pub bt_optimistic_unchoke_interval: Option<u64>,
198
199    /// Timeout in seconds after which a peer is considered snubbed (not sending data).
200    /// Default: 60.
201    pub bt_snubbed_timeout: Option<u64>,
202
203    // ------------------------------------------------------------------
204    // Piece selection priority mode (G2)
205    // ------------------------------------------------------------------
206    /// Piece selection priority: "rarest" (default), "head" (sequential from start),
207    /// "tail" (sequential from end).
208    pub bt_prioritize_piece: String,
209
210    // ------------------------------------------------------------------
211    // uTP (UDP Transport Protocol - BEP 29)
212    // ------------------------------------------------------------------
213    /// Enable uTP (UDP Transport Protocol) for BitTorrent connections.
214    /// This implements BEP 29 and is an experimental feature not in original aria2.
215    /// Default: false.
216    pub enable_utp: bool,
217
218    /// UDP port for uTP connections. 0 = auto-assign.
219    /// Experimental feature not in original aria2.
220    pub utp_listen_port: Option<u16>,
221
222    // ------------------------------------------------------------------
223    // HTTP headers (C++ aria2 `--header` / RPC `header` option)
224    // ------------------------------------------------------------------
225    /// Custom HTTP request headers as `"Name: Value"` strings.
226    /// Applied to both HEAD probes and range GETs.
227    pub header: Vec<String>,
228    /// Override `User-Agent` header. Also injected into the `header` list by
229    /// [`DownloadOptions::parsed_headers`] when set.
230    pub user_agent: Option<String>,
231    /// Override `Referer` header. Also injected into the `header` list by
232    /// [`DownloadOptions::parsed_headers`] when set.
233    pub referer: Option<String>,
234}
235
236// Manual Default impl: `enable_dht` and `enable_public_trackers` default to
237// `true` (matching the load path in `option_handler.rs` and `task.rs` which
238// use `unwrap_or(true)`). All other fields use their type-level defaults.
239impl Default for DownloadOptions {
240    fn default() -> Self {
241        Self {
242            split: None,
243            max_connection_per_server: None,
244            max_download_limit: None,
245            max_upload_limit: None,
246            dir: None,
247            out: None,
248            file_allocation: None,
249            mmap_threshold: None,
250            secure_falloc: false,
251            seed_time: None,
252            seed_ratio: None,
253            checksum: None,
254            cookie_file: None,
255            cookies: None,
256            bt_force_encrypt: false,
257            bt_require_crypto: false,
258            enable_dht: true,
259            dht_listen_port: None,
260            dht_entry_point: None,
261            enable_public_trackers: true,
262            bt_piece_selection_strategy: String::new(),
263            bt_endgame_threshold: 0,
264            max_retries: 0,
265            retry_wait: 0,
266            http_proxy: None,
267            all_proxy: None,
268            https_proxy: None,
269            ftp_proxy: None,
270            no_proxy: None,
271            dht_file_path: None,
272            bt_max_upload_slots: None,
273            bt_optimistic_unchoke_interval: None,
274            bt_snubbed_timeout: None,
275            bt_prioritize_piece: String::new(),
276            enable_utp: false,
277            utp_listen_port: None,
278            header: Vec::new(),
279            user_agent: None,
280            referer: None,
281        }
282    }
283}
284
285impl DownloadOptions {
286    /// Parse the raw `header` list into `(name, value)` pairs, splitting each
287    /// `"Name: Value"` entry on the first `:`. When `user_agent` or `referer`
288    /// are set, they are appended as `User-Agent` / `Referer` pairs (unless an
289    /// entry with the same name already exists), so callers only need to handle
290    /// a single header list.
291    pub fn parsed_headers(&self) -> Vec<(String, String)> {
292        let mut result: Vec<(String, String)> = Vec::new();
293        for raw in &self.header {
294            if let Some((name, value)) = raw.split_once(':') {
295                let name = name.trim().to_string();
296                let value = value.trim().to_string();
297                if !name.is_empty() {
298                    result.push((name, value));
299                }
300            }
301        }
302        // Overlay user_agent / referer if not already present (case-insensitive).
303        if let Some(ref ua) = self.user_agent
304            && !has_header(&result, "User-Agent")
305        {
306            result.push(("User-Agent".to_string(), ua.clone()));
307        }
308        if let Some(ref ref_) = self.referer
309            && !has_header(&result, "Referer")
310        {
311            result.push(("Referer".to_string(), ref_.clone()));
312        }
313        result
314    }
315}
316
317/// Case-insensitive check whether a `(name, value)` header list already contains
318/// an entry with the given name.
319fn has_header(headers: &[(String, String)], name: &str) -> bool {
320    headers.iter().any(|(n, _)| n.eq_ignore_ascii_case(name))
321}
322
323/// Options that can be changed at runtime via `aria2.changeOption`.
324/// All other options are startup-only and cannot be modified after the
325/// download begins. Keep in sync with [`RequestGroup::update_option`].
326pub const RUNTIME_CHANGEABLE_OPTIONS: &[&str] = &[
327    "split",
328    "max-connection-per-server",
329    "max-download-limit",
330    "max-upload-limit",
331    "max-retries",
332    "retry-wait",
333    "header",
334    "user-agent",
335    "referer",
336    "bt-max-upload-slots",
337    "bt-snubbed-timeout",
338    "bt-optimistic-unchoke-interval",
339    "bt-endgame-threshold",
340    "seed-time",
341    "seed-ratio",
342];
343
344pub struct RequestGroup {
345    gid: GroupId,
346    uris: Vec<String>,
347    options: DownloadOptions,
348    status: Arc<RwLock<DownloadStatus>>,
349    segments: Arc<RwLock<Vec<Segment>>>,
350    total_length: u64,
351    completed_length: Arc<RwLock<u64>>,
352    download_speed: Arc<RwLock<u64>>,
353    upload_speed: Arc<RwLock<u64>>,
354    start_time: Arc<RwLock<Option<std::time::Instant>>>,
355    end_time: Arc<RwLock<Option<std::time::Instant>>>,
356    // New progress tracking fields (for session persistence)
357    pub completed_length_atomic: AtomicU64,
358    pub total_length_atomic: AtomicU64,
359    pub uploaded_length: AtomicU64,
360    pub download_speed_cached: AtomicU64,
361    pub bt_bitfield: RwLock<Option<Vec<u8>>>,
362
363    // BT metadata fields (for session persistence enhancement)
364    /// Number of pieces in the torrent (0 for non-BT downloads)
365    pub bt_num_pieces: AtomicU32,
366    /// Size of each piece in bytes (0 for non-BT downloads)
367    pub bt_piece_length: AtomicU32,
368    /// Info hash hex string for torrent identification (None for non-BT downloads)
369    /// Uses std::sync::RwLock instead of tokio::sync::RwLock for non-async access
370    pub bt_info_hash_hex: std::sync::RwLock<Option<String>>,
371
372    /// Handle to the download's `RateLimiter` so that runtime option updates
373    /// (e.g. via `aria2.changeOption`) can dynamically adjust the rate.
374    /// `None` until the download engine wires up a `ThrottledWriter`.
375    /// Uses its own `RwLock` (independent of the `RequestGroupMan` write lock)
376    /// so that `set_rate_limiter` can take `&self`.
377    pub rate_limiter: RwLock<Option<RateLimiter>>,
378}
379
380impl RequestGroup {
381    pub fn new(gid: GroupId, uris: Vec<String>, options: DownloadOptions) -> Self {
382        info!("Creating request group #{}", gid.value());
383
384        RequestGroup {
385            gid,
386            uris,
387            options,
388            status: Arc::new(RwLock::new(DownloadStatus::Waiting)),
389            segments: Arc::new(RwLock::new(Vec::new())),
390            total_length: 0,
391            completed_length: Arc::new(RwLock::new(0)),
392            download_speed: Arc::new(RwLock::new(0)),
393            upload_speed: Arc::new(RwLock::new(0)),
394            start_time: Arc::new(RwLock::new(None)),
395            end_time: Arc::new(RwLock::new(None)),
396            // Initialize new progress tracking fields
397            completed_length_atomic: AtomicU64::new(0),
398            total_length_atomic: AtomicU64::new(0),
399            uploaded_length: AtomicU64::new(0),
400            download_speed_cached: AtomicU64::new(0),
401            bt_bitfield: RwLock::new(None),
402
403            // Initialize BT metadata fields (default to 0/None for non-BT downloads)
404            bt_num_pieces: AtomicU32::new(0),
405            bt_piece_length: AtomicU32::new(0),
406            bt_info_hash_hex: std::sync::RwLock::new(None),
407
408            // Rate limiter is wired up later by the download engine via
409            // `set_rate_limiter` once a `ThrottledWriter` is constructed.
410            rate_limiter: RwLock::new(None),
411        }
412    }
413
414    pub async fn start(&mut self) -> Result<()> {
415        let mut status = self.status.write().await;
416        let mut start_time = self.start_time.write().await;
417
418        *status = DownloadStatus::Active;
419        *start_time = Some(std::time::Instant::now());
420
421        info!("Starting download task #{}", self.gid.value());
422        Ok(())
423    }
424
425    pub async fn pause(&mut self) -> Result<()> {
426        let mut status = self.status.write().await;
427
428        if matches!(*status, DownloadStatus::Active) {
429            *status = DownloadStatus::Paused;
430            info!("Pausing download task #{}", self.gid.value());
431        }
432
433        Ok(())
434    }
435
436    pub async fn remove(&mut self) -> Result<()> {
437        let mut status = self.status.write().await;
438        let mut end_time = self.end_time.write().await;
439
440        *status = DownloadStatus::Removed;
441        *end_time = Some(std::time::Instant::now());
442
443        info!("Removing download task #{}", self.gid.value());
444        Ok(())
445    }
446
447    pub async fn complete(&mut self) -> Result<()> {
448        let mut status = self.status.write().await;
449        let mut end_time = self.end_time.write().await;
450        let mut completed_length = self.completed_length.write().await;
451
452        *status = DownloadStatus::Complete;
453        *end_time = Some(std::time::Instant::now());
454        *completed_length = self.total_length;
455
456        info!("Completing download task #{}", self.gid.value());
457        Ok(())
458    }
459
460    pub async fn error(&mut self, err: impl Into<String>) -> Result<()> {
461        let mut status = self.status.write().await;
462        let mut end_time = self.end_time.write().await;
463
464        *status = DownloadStatus::Error(err.into());
465        *end_time = Some(std::time::Instant::now());
466
467        debug!("Download task #{} encountered error", self.gid.value());
468        Ok(())
469    }
470
471    pub async fn status(&self) -> DownloadStatus {
472        self.status.read().await.clone()
473    }
474
475    pub fn gid(&self) -> GroupId {
476        self.gid
477    }
478
479    pub fn uris(&self) -> &[String] {
480        &self.uris
481    }
482
483    pub fn options(&self) -> &DownloadOptions {
484        &self.options
485    }
486
487    /// Store a handle to the download's `RateLimiter` so that runtime option
488    /// updates (e.g. via `aria2.changeOption`) can dynamically adjust the rate.
489    /// The `RateLimiter` is `Clone` and shares `Arc<RateLimiterInner>`, so both
490    /// the `ThrottledWriter` and `RequestGroup` see the same token buckets.
491    ///
492    /// Takes `&self` because `rate_limiter` has its own `RwLock`, independent
493    /// of the `RequestGroupMan` outer write lock.
494    pub async fn set_rate_limiter(&self, limiter: RateLimiter) {
495        *self.rate_limiter.write().await = Some(limiter);
496    }
497
498    /// Update a single runtime-changeable option by key (using aria2's
499    /// kebab-case option names, e.g. `"max-download-limit"`).
500    ///
501    /// Returns `true` if the option was recognized and updated, `false` if the
502    /// key is not a runtime-changeable option (see [`RUNTIME_CHANGEABLE_OPTIONS`]).
503    ///
504    /// Takes `&mut self` because `options` is a plain field — the caller
505    /// (`RequestGroupMan`) holds the outer `Arc<RwLock<RequestGroup>>` write
506    /// lock. For `max-download-limit` / `max-upload-limit`, the stored
507    /// `RateLimiter` (if any) is also updated so the change takes effect
508    /// immediately on the live download.
509    pub async fn update_option(&mut self, key: &str, value: serde_json::Value) -> bool {
510        match key {
511            "split" => {
512                if let Some(v) = value.as_u64() {
513                    self.options.split = Some(v as u16);
514                    tracing::warn!(
515                        new_split = v,
516                        "split changed but will take effect on download restart/retry, \
517                         not mid-download (current segments unchanged)"
518                    );
519                }
520                true
521            }
522            "max-download-limit" => {
523                let rate = value.as_u64();
524                self.options.max_download_limit = rate;
525                if let Some(ref limiter) = *self.rate_limiter.read().await {
526                    limiter.set_download_rate(rate);
527                }
528                true
529            }
530            "max-upload-limit" => {
531                let rate = value.as_u64();
532                self.options.max_upload_limit = rate;
533                if let Some(ref limiter) = *self.rate_limiter.read().await {
534                    limiter.set_upload_rate(rate);
535                }
536                true
537            }
538            "max-retries" => {
539                if let Some(v) = value.as_u64() {
540                    self.options.max_retries = v as u32;
541                }
542                true
543            }
544            "retry-wait" => {
545                if let Some(v) = value.as_u64() {
546                    self.options.retry_wait = v;
547                }
548                true
549            }
550            "header" => {
551                // Accept both a JSON array of strings and a newline-separated
552                // string (matching aria2's wire format).
553                match &value {
554                    serde_json::Value::Array(arr) => {
555                        self.options.header = arr
556                            .iter()
557                            .filter_map(|v| v.as_str().map(|s| s.to_string()))
558                            .collect();
559                    }
560                    serde_json::Value::String(s) => {
561                        self.options.header = s
562                            .split('\n')
563                            .map(|l| l.trim().to_string())
564                            .filter(|l| !l.is_empty())
565                            .collect();
566                    }
567                    _ => {}
568                }
569                true
570            }
571            "user-agent" => {
572                self.options.user_agent = value.as_str().map(|s| s.to_string());
573                true
574            }
575            "referer" => {
576                self.options.referer = value.as_str().map(|s| s.to_string());
577                true
578            }
579            "max-connection-per-server" => {
580                if let Some(v) = value.as_u64() {
581                    self.options.max_connection_per_server = Some(v as u16);
582                }
583                true
584            }
585            "bt-max-upload-slots" => {
586                if let Some(v) = value.as_u64() {
587                    self.options.bt_max_upload_slots = Some(v as u32);
588                }
589                true
590            }
591            "bt-snubbed-timeout" => {
592                if let Some(v) = value.as_u64() {
593                    self.options.bt_snubbed_timeout = Some(v);
594                }
595                true
596            }
597            "bt-optimistic-unchoke-interval" => {
598                if let Some(v) = value.as_u64() {
599                    self.options.bt_optimistic_unchoke_interval = Some(v);
600                }
601                true
602            }
603            "bt-endgame-threshold" => {
604                if let Some(v) = value.as_u64() {
605                    self.options.bt_endgame_threshold = v as u32;
606                }
607                true
608            }
609            "seed-time" => {
610                if let Some(v) = value.as_u64() {
611                    self.options.seed_time = Some(v);
612                }
613                true
614            }
615            "seed-ratio" => {
616                if let Some(v) = value.as_f64() {
617                    self.options.seed_ratio = Some(v);
618                }
619                true
620            }
621            _ => false,
622        }
623    }
624
625    pub fn total_length(&self) -> u64 {
626        self.total_length
627    }
628
629    pub async fn set_total_length(&mut self, length: u64) {
630        self.total_length = length;
631        debug!("Setting total length: {} bytes", length);
632    }
633
634    pub async fn completed_length(&self) -> u64 {
635        *self.completed_length.read().await
636    }
637
638    pub async fn update_completed_length(&self, length: u64) {
639        let mut completed_length = self.completed_length.write().await;
640        *completed_length = length;
641    }
642
643    pub async fn update_progress(&self, completed_length: u64) {
644        let mut cl = self.completed_length.write().await;
645        *cl = completed_length;
646    }
647
648    pub async fn progress(&self) -> f64 {
649        let total = self.total_length;
650        let completed = *self.completed_length.read().await;
651
652        if total == 0 {
653            0.0
654        } else {
655            (completed as f64 / total as f64) * 100.0
656        }
657    }
658
659    pub async fn download_speed(&self) -> u64 {
660        *self.download_speed.read().await
661    }
662
663    pub async fn upload_speed(&self) -> u64 {
664        *self.upload_speed.read().await
665    }
666
667    pub async fn update_speed(&self, dl_speed: u64, ul_speed: u64) {
668        let mut ds = self.download_speed.write().await;
669        let mut us = self.upload_speed.write().await;
670        *ds = dl_speed;
671        *us = ul_speed;
672    }
673
674    pub async fn add_segment(&mut self, segment: Segment) {
675        let mut segments = self.segments.write().await;
676        segments.push(segment);
677        debug!("Adding segment, current segments: {}", segments.len());
678    }
679
680    pub async fn segments(&self) -> Vec<Segment> {
681        self.segments.read().await.clone()
682    }
683
684    pub async fn elapsed_time(&self) -> Option<std::time::Duration> {
685        let start = *self.start_time.read().await;
686        start.map(|t| t.elapsed())
687    }
688
689    pub async fn eta(&self) -> Option<std::time::Duration> {
690        let speed = *self.download_speed.read().await;
691        let remaining = self
692            .total_length
693            .saturating_sub(*self.completed_length.read().await);
694
695        if speed == 0 || remaining == 0 {
696            None
697        } else {
698            Some(std::time::Duration::from_secs(remaining / speed))
699        }
700    }
701
702    // New progress tracking methods (for session persistence)
703    // These use AtomicU64 for lock-free reads, suitable for frequent polling
704
705    /// Set completed length using atomic store (lock-free)
706    pub fn set_completed_length(&self, val: u64) {
707        self.completed_length_atomic.store(val, Ordering::Relaxed);
708    }
709
710    /// Get completed length using atomic load (lock-free)
711    pub fn get_completed_length(&self) -> u64 {
712        self.completed_length_atomic.load(Ordering::Relaxed)
713    }
714
715    /// Set total length using atomic store (lock-free)
716    pub fn set_total_length_atomic(&self, val: u64) {
717        self.total_length_atomic.store(val, Ordering::Relaxed);
718    }
719
720    /// Get total length using atomic load (lock-free)
721    pub fn get_total_length_atomic(&self) -> u64 {
722        self.total_length_atomic.load(Ordering::Relaxed)
723    }
724
725    /// Set uploaded length using atomic store (lock-free)
726    pub fn set_uploaded_length(&self, val: u64) {
727        self.uploaded_length.store(val, Ordering::Relaxed);
728    }
729
730    /// Get uploaded length using atomic load (lock-free)
731    pub fn get_uploaded_length(&self) -> u64 {
732        self.uploaded_length.load(Ordering::Relaxed)
733    }
734
735    /// Set download speed cache using atomic store (lock-free)
736    pub fn set_download_speed_cached(&self, val: u64) {
737        self.download_speed_cached.store(val, Ordering::Relaxed);
738    }
739
740    /// Get download speed from cache using atomic load (lock-free)
741    pub fn get_download_speed_cached(&self) -> u64 {
742        self.download_speed_cached.load(Ordering::Relaxed)
743    }
744
745    /// Set BT bitfield (async, uses RwLock)
746    pub async fn set_bt_bitfield(&self, bf: Option<Vec<u8>>) {
747        *self.bt_bitfield.write().await = bf;
748    }
749
750    /// Get BT bitfield (async, uses RwLock)
751    pub async fn get_bt_bitfield(&self) -> Option<Vec<u8>> {
752        self.bt_bitfield.read().await.clone()
753    }
754
755    /// Set resume offset for HTTP/FTP range request resumption
756    pub fn set_resume_offset(&self, offset: u64) {
757        // Store resume offset in completed_length so the download engine
758        // knows where to resume from
759        self.completed_length_atomic
760            .store(offset, Ordering::Relaxed);
761    }
762
763    // BT metadata methods (for session persistence enhancement)
764
765    /// Set BT metadata fields (num_pieces, piece_length, info_hash_hex)
766    /// Called by BtDownloadCommand after parsing TorrentMeta
767    pub fn set_bt_metadata(&self, num_pieces: u32, piece_length: u32, info_hash_hex: String) {
768        self.bt_num_pieces.store(num_pieces, Ordering::Relaxed);
769        self.bt_piece_length.store(piece_length, Ordering::Relaxed);
770        // Use std::sync::RwLock for non-async access
771        *self.bt_info_hash_hex.write().unwrap() = Some(info_hash_hex);
772    }
773
774    /// Get number of pieces (lock-free atomic read)
775    pub fn get_bt_num_pieces(&self) -> u32 {
776        self.bt_num_pieces.load(Ordering::Relaxed)
777    }
778
779    /// Get piece length (lock-free atomic read)
780    pub fn get_bt_piece_length(&self) -> u32 {
781        self.bt_piece_length.load(Ordering::Relaxed)
782    }
783
784    /// Get info hash hex string (async-compatible wrapper)
785    pub async fn get_bt_info_hash_hex_async(&self) -> Option<String> {
786        // std::sync::RwLock can be used in async context for short reads
787        self.bt_info_hash_hex.read().unwrap().clone()
788    }
789
790    /// Get info hash hex string (blocking read for non-async contexts)
791    pub fn get_bt_info_hash_hex(&self) -> Option<String> {
792        self.bt_info_hash_hex.read().unwrap().clone()
793    }
794}
795
796#[cfg(test)]
797mod tests {
798    use super::*;
799
800    #[test]
801    fn test_request_group_progress_fields_default() {
802        // New RequestGroup should have all zeros/None defaults for progress fields
803        let group = RequestGroup::new(
804            GroupId::new(1),
805            vec!["http://example.com/file.zip".to_string()],
806            DownloadOptions::default(),
807        );
808
809        // Verify all atomic fields default to 0
810        assert_eq!(
811            group.get_completed_length(),
812            0,
813            "completed_length_atomic should default to 0"
814        );
815        assert_eq!(
816            group.get_total_length_atomic(),
817            0,
818            "total_length_atomic should default to 0"
819        );
820        assert_eq!(
821            group.get_uploaded_length(),
822            0,
823            "uploaded_length should default to 0"
824        );
825        assert_eq!(
826            group.get_download_speed_cached(),
827            0,
828            "download_speed_cached should default to 0"
829        );
830    }
831
832    #[test]
833    fn test_set_get_completed_length() {
834        let group = RequestGroup::new(
835            GroupId::new(2),
836            vec!["http://test.com/file.bin".to_string()],
837            DownloadOptions::default(),
838        );
839
840        // Test set/get roundtrip
841        group.set_completed_length(1024);
842        assert_eq!(
843            group.get_completed_length(),
844            1024,
845            "Should return 1024 after setting"
846        );
847
848        // Test update to different value
849        group.set_completed_length(2048);
850        assert_eq!(
851            group.get_completed_length(),
852            2048,
853            "Should return 2048 after update"
854        );
855
856        // Test large value
857        group.set_completed_length(u64::MAX);
858        assert_eq!(
859            group.get_completed_length(),
860            u64::MAX,
861            "Should handle u64::MAX"
862        );
863
864        // Test zero
865        group.set_completed_length(0);
866        assert_eq!(group.get_completed_length(), 0, "Should handle 0");
867    }
868
869    #[test]
870    fn test_set_get_total_length() {
871        let group = RequestGroup::new(
872            GroupId::new(3),
873            vec!["http://example.com/large.iso".to_string()],
874            DownloadOptions::default(),
875        );
876
877        // Test set/get roundtrip
878        group.set_total_length_atomic(1048576); // 1MB
879        assert_eq!(
880            group.get_total_length_atomic(),
881            1048576,
882            "Should return 1MB after setting"
883        );
884
885        // Test update
886        group.set_total_length_atomic(1073741824); // 1GB
887        assert_eq!(
888            group.get_total_length_atomic(),
889            1073741824,
890            "Should return 1GB after update"
891        );
892    }
893
894    #[tokio::test]
895    async fn test_set_get_bt_bitfield() {
896        let group = RequestGroup::new(
897            GroupId::new(4),
898            vec!["magnet:?xt=urn:btih:abc123".to_string()],
899            DownloadOptions::default(),
900        );
901
902        // Default should be None
903        let bf = group.get_bt_bitfield().await;
904        assert!(bf.is_none(), "bt_bitfield should default to None");
905
906        // Set and retrieve bitfield
907        let test_bitfield = vec![0xFF, 0xF0, 0x0F];
908        group.set_bt_bitfield(Some(test_bitfield.clone())).await;
909        let retrieved = group.get_bt_bitfield().await;
910        assert!(
911            retrieved.is_some(),
912            "bt_bitfield should be Some after setting"
913        );
914        assert_eq!(
915            retrieved.unwrap(),
916            test_bitfield,
917            "bitfield should match what was set"
918        );
919
920        // Set back to None
921        group.set_bt_bitfield(None).await;
922        let bf_none = group.get_bt_bitfield().await;
923        assert!(
924            bf_none.is_none(),
925            "bt_bitfield should be None after clearing"
926        );
927
928        // Test with empty bitfield
929        group.set_bt_bitfield(Some(vec![])).await;
930        let empty_bf = group.get_bt_bitfield().await;
931        assert!(empty_bf.is_some(), "empty bitfield should still be Some");
932        assert!(empty_bf.unwrap().is_empty(), "bitfield should be empty vec");
933    }
934
935    #[tokio::test]
936    async fn test_concurrent_access() {
937        use std::sync::Arc;
938
939        let group = Arc::new(RequestGroup::new(
940            GroupId::new(5),
941            vec!["http://load.test/file.dat".to_string()],
942            DownloadOptions::default(),
943        ));
944
945        // Spawn multiple tasks that read/write progress concurrently
946        let mut handles = Vec::new();
947
948        for i in 0..10 {
949            let g = Arc::clone(&group);
950            handles.push(tokio::spawn(async move {
951                // Write progress
952                g.set_completed_length(i * 100);
953                g.set_total_length_atomic(10000);
954                g.set_uploaded_length(i * 10);
955                g.set_download_speed_cached(i * 1000);
956
957                // Read progress (should not deadlock)
958                let _cl = g.get_completed_length();
959                let _tl = g.get_total_length_atomic();
960                let _ul = g.get_uploaded_length();
961                let _ds = g.get_download_speed_cached();
962
963                // Occasionally write bitfield (async)
964                if i % 3 == 0 {
965                    let bf = vec![i as u8; 8];
966                    g.set_bt_bitfield(Some(bf)).await;
967                    let _retrieved = g.get_bt_bitfield().await;
968                }
969
970                // Small delay to increase chance of race conditions
971                tokio::time::sleep(std::time::Duration::from_millis(1)).await;
972            }));
973        }
974
975        // Wait for all tasks to complete without deadlock
976        for handle in handles {
977            handle.await.expect("Task should complete without panic");
978        }
979
980        // Verify final state is consistent
981        let final_cl = group.get_completed_length();
982        let final_tl = group.get_total_length_atomic();
983        let final_ul = group.get_uploaded_length();
984        let final_ds = group.get_download_speed_cached();
985
986        // Values should be from one of the concurrent writers (we don't know which)
987        assert!(final_cl <= 900, "completed_length should be <= 900");
988        assert_eq!(final_tl, 10000, "total_length should be 10000");
989        assert!(final_ul <= 90, "uploaded_length should be <= 90");
990        assert!(final_ds <= 9000, "download_speed should be <= 9000");
991    }
992
993    #[test]
994    fn test_set_get_uploaded_length() {
995        let group = RequestGroup::new(
996            GroupId::new(6),
997            vec!["http://seed.test/file.torrent".to_string()],
998            DownloadOptions::default(),
999        );
1000
1001        // Test default
1002        assert_eq!(group.get_uploaded_length(), 0);
1003
1004        // Test set/get
1005        group.set_uploaded_length(512);
1006        assert_eq!(group.get_uploaded_length(), 512);
1007
1008        // Test large value
1009        group.set_uploaded_length(u64::MAX / 2);
1010        assert_eq!(group.get_uploaded_length(), u64::MAX / 2);
1011    }
1012
1013    #[test]
1014    fn test_set_get_download_speed_cached() {
1015        let group = RequestGroup::new(
1016            GroupId::new(7),
1017            vec!["http://speed.test/large.file".to_string()],
1018            DownloadOptions::default(),
1019        );
1020
1021        // Test default
1022        assert_eq!(group.get_download_speed_cached(), 0);
1023
1024        // Test realistic download speed (e.g., 5 MB/s = 5242880 bytes/s)
1025        group.set_download_speed_cached(5242880);
1026        assert_eq!(group.get_download_speed_cached(), 5242880);
1027
1028        // Test speed update (simulating periodic updates)
1029        group.set_download_speed_cached(10485760); // 10 MB/s
1030        assert_eq!(group.get_download_speed_cached(), 10485760);
1031    }
1032
1033    #[test]
1034    fn test_download_options_choking_config_defaults() {
1035        // New DownloadOptions should have None for choking algorithm fields (opt-in)
1036        let opts = DownloadOptions::default();
1037
1038        assert!(
1039            opts.bt_max_upload_slots.is_none(),
1040            "bt_max_upload_slots should default to None"
1041        );
1042        assert!(
1043            opts.bt_optimistic_unchoke_interval.is_none(),
1044            "bt_optimistic_unchoke_interval should default to None"
1045        );
1046        assert!(
1047            opts.bt_snubbed_timeout.is_none(),
1048            "bt_snubbed_timeout should default to None"
1049        );
1050    }
1051
1052    #[test]
1053    fn test_download_options_choking_config_custom() {
1054        // Verify that custom choking config values can be set
1055        let opts = DownloadOptions {
1056            bt_max_upload_slots: Some(8),
1057            bt_optimistic_unchoke_interval: Some(15),
1058            bt_snubbed_timeout: Some(45),
1059            ..DownloadOptions::default()
1060        };
1061
1062        assert_eq!(opts.bt_max_upload_slots, Some(8));
1063        assert_eq!(opts.bt_optimistic_unchoke_interval, Some(15));
1064        assert_eq!(opts.bt_snubbed_timeout, Some(45));
1065    }
1066
1067    #[test]
1068    fn test_download_options_choking_config_clone() {
1069        // Verify choking config fields are preserved through Clone
1070        let opts = DownloadOptions {
1071            bt_max_upload_slots: Some(6),
1072            bt_optimistic_unchoke_interval: Some(20),
1073            bt_snubbed_timeout: Some(90),
1074            ..DownloadOptions::default()
1075        };
1076
1077        let cloned = opts.clone();
1078
1079        assert_eq!(cloned.bt_max_upload_slots, Some(6));
1080        assert_eq!(cloned.bt_optimistic_unchoke_interval, Some(20));
1081        assert_eq!(cloned.bt_snubbed_timeout, Some(90));
1082    }
1083
1084    // ==================== BT Metadata Tests ====================
1085
1086    #[test]
1087    fn test_bt_metadata_defaults() {
1088        let group = RequestGroup::new(
1089            GroupId::new(8),
1090            vec!["http://example.com/file.zip".to_string()],
1091            DownloadOptions::default(),
1092        );
1093
1094        // Non-BT downloads should have 0/None defaults
1095        assert_eq!(
1096            group.get_bt_num_pieces(),
1097            0,
1098            "bt_num_pieces should default to 0"
1099        );
1100        assert_eq!(
1101            group.get_bt_piece_length(),
1102            0,
1103            "bt_piece_length should default to 0"
1104        );
1105        assert_eq!(
1106            group.get_bt_info_hash_hex(),
1107            None,
1108            "bt_info_hash_hex should default to None"
1109        );
1110    }
1111
1112    #[test]
1113    fn test_set_bt_metadata() {
1114        let group = RequestGroup::new(
1115            GroupId::new(9),
1116            vec!["magnet:?xt=urn:btih:abc123def456".to_string()],
1117            DownloadOptions::default(),
1118        );
1119
1120        // Set BT metadata
1121        group.set_bt_metadata(
1122            100,
1123            262144,
1124            "abc123def456789012345678901234567890abcd".to_string(),
1125        );
1126
1127        // Verify values
1128        assert_eq!(group.get_bt_num_pieces(), 100);
1129        assert_eq!(group.get_bt_piece_length(), 262144); // 256KB
1130        assert_eq!(
1131            group.get_bt_info_hash_hex(),
1132            Some("abc123def456789012345678901234567890abcd".to_string())
1133        );
1134    }
1135
1136    #[test]
1137    fn test_bt_metadata_update() {
1138        let group = RequestGroup::new(
1139            GroupId::new(10),
1140            vec!["bt://test.torrent".to_string()],
1141            DownloadOptions::default(),
1142        );
1143
1144        // Initial set
1145        group.set_bt_metadata(50, 16384, "first_hash".to_string());
1146        assert_eq!(group.get_bt_num_pieces(), 50);
1147
1148        // Update with new values
1149        group.set_bt_metadata(200, 524288, "updated_hash".to_string());
1150        assert_eq!(group.get_bt_num_pieces(), 200);
1151        assert_eq!(group.get_bt_piece_length(), 524288);
1152        assert_eq!(
1153            group.get_bt_info_hash_hex(),
1154            Some("updated_hash".to_string())
1155        );
1156    }
1157
1158    #[tokio::test]
1159    async fn test_bt_info_hash_hex_async() {
1160        let group = RequestGroup::new(
1161            GroupId::new(11),
1162            vec!["magnet:?xt=urn:btih:test".to_string()],
1163            DownloadOptions::default(),
1164        );
1165
1166        // Set via blocking method
1167        group.set_bt_metadata(10, 1024, "async_test_hash".to_string());
1168
1169        // Read via async method
1170        let hash = group.get_bt_info_hash_hex_async().await;
1171        assert_eq!(hash, Some("async_test_hash".to_string()));
1172    }
1173
1174    #[tokio::test]
1175    async fn test_update_option_new_runtime_changeable() {
1176        let gid = GroupId::new(1);
1177        let uris = vec!["http://example.com/file".to_string()];
1178        let mut group = RequestGroup::new(gid, uris, DownloadOptions::default());
1179
1180        // max-connection-per-server
1181        assert!(
1182            group
1183                .update_option("max-connection-per-server", serde_json::json!(4))
1184                .await
1185        );
1186        assert_eq!(group.options().max_connection_per_server, Some(4));
1187
1188        // bt-max-upload-slots
1189        assert!(
1190            group
1191                .update_option("bt-max-upload-slots", serde_json::json!(8))
1192                .await
1193        );
1194        assert_eq!(group.options().bt_max_upload_slots, Some(8));
1195
1196        // bt-snubbed-timeout
1197        assert!(
1198            group
1199                .update_option("bt-snubbed-timeout", serde_json::json!(120))
1200                .await
1201        );
1202        assert_eq!(group.options().bt_snubbed_timeout, Some(120));
1203
1204        // bt-optimistic-unchoke-interval
1205        assert!(
1206            group
1207                .update_option("bt-optimistic-unchoke-interval", serde_json::json!(45))
1208                .await
1209        );
1210        assert_eq!(group.options().bt_optimistic_unchoke_interval, Some(45));
1211
1212        // bt-endgame-threshold
1213        assert!(
1214            group
1215                .update_option("bt-endgame-threshold", serde_json::json!(50))
1216                .await
1217        );
1218        assert_eq!(group.options().bt_endgame_threshold, 50);
1219
1220        // seed-time
1221        assert!(
1222            group
1223                .update_option("seed-time", serde_json::json!(3600))
1224                .await
1225        );
1226        assert_eq!(group.options().seed_time, Some(3600));
1227
1228        // seed-ratio
1229        assert!(
1230            group
1231                .update_option("seed-ratio", serde_json::json!(2.0))
1232                .await
1233        );
1234        assert_eq!(group.options().seed_ratio, Some(2.0));
1235
1236        // Unknown option returns false
1237        assert!(
1238            !group
1239                .update_option("unknown-option", serde_json::json!(1))
1240                .await
1241        );
1242    }
1243}