Skip to main content

aria2_core/engine/
sequential_download.rs

1use std::sync::Arc;
2
3use futures::StreamExt;
4use reqwest;
5
6use crate::constants;
7use crate::engine::download_cookie::CookieHelper;
8use crate::engine::download_progress::ProgressUpdater;
9use crate::engine::retry_policy::RetryPolicy;
10use crate::error::{Aria2Error, RecoverableError, Result};
11use crate::filesystem::disk_writer::{
12    CachedDiskWriter, DefaultDiskWriter, DiskWriter, SeekableDiskWriter,
13};
14use crate::filesystem::resume_helper::{ResumeHelper, ResumeState};
15use crate::rate_limiter::{RateLimiter, RateLimiterConfig, ThrottledWriter};
16use crate::request::request_group::RequestGroup;
17
18pub struct GapDownloadResult {
19    pub completed_gaps: Vec<(u64, u64)>,
20    pub error: Option<Aria2Error>,
21}
22
23pub struct SequentialDownloader {
24    client: Arc<reqwest::Client>,
25    output_path: std::path::PathBuf,
26    headers: Vec<(String, String)>,
27    cookie_helper: CookieHelper,
28    progress_updater: ProgressUpdater,
29    group: Arc<tokio::sync::RwLock<RequestGroup>>,
30}
31
32impl SequentialDownloader {
33    pub fn new(
34        client: Arc<reqwest::Client>,
35        output_path: std::path::PathBuf,
36        headers: Vec<(String, String)>,
37        cookie_helper: CookieHelper,
38        progress_updater: ProgressUpdater,
39        group: Arc<tokio::sync::RwLock<RequestGroup>>,
40    ) -> Self {
41        Self {
42            client,
43            output_path,
44            headers,
45            cookie_helper,
46            progress_updater,
47            group,
48        }
49    }
50
51    pub async fn execute(
52        &mut self,
53        uri: &str,
54        resume_state: &ResumeState,
55        total_length: u64,
56    ) -> Result<()> {
57        #[cfg(not(target_os = "linux"))]
58        let _ = total_length;
59
60        #[cfg(target_os = "linux")]
61        {
62            let no_proxy = {
63                let guard = self.group.read().await;
64                let opts = guard.options();
65                opts.http_proxy.is_none() && opts.all_proxy.is_none()
66            };
67            if !resume_state.should_resume
68                && total_length > 0
69                && no_proxy
70                && !uri.starts_with("https://")
71                && self.headers.is_empty()
72                && self.cookie_helper.build_cookie_header(uri).is_none()
73            {
74                match self.try_splice_sequential(uri, total_length).await {
75                    Ok(()) => return Ok(()),
76                    Err(e) => {
77                        tracing::debug!(
78                            "Splice download failed for {}, falling back to streaming: {}",
79                            uri,
80                            e
81                        );
82                    }
83                }
84            }
85        }
86
87        let url_parsed = reqwest::Url::parse(uri).ok();
88        let mut request = if let Some(range_header) = ResumeHelper::build_range_header(resume_state)
89        {
90            tracing::debug!("Resume download: {}", range_header);
91            self.client.get(uri).header("Range", range_header)
92        } else {
93            self.client.get(uri)
94        };
95
96        if let Some(ref url) = url_parsed {
97            let cookie_hdr = self.cookie_helper.build_cookie_header_from_url(url);
98            if !cookie_hdr.is_empty() {
99                request = request.header("Cookie", &cookie_hdr);
100            }
101        }
102
103        for (name, value) in &self.headers {
104            request = request.header(name, value);
105        }
106
107        let response = request.send().await.map_err(|e| {
108            Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
109                message: format!("HTTP request failed: {}", e),
110            })
111        })?;
112
113        self.cookie_helper.extract_and_store_cookies(uri, &response);
114
115        let status = response.status();
116        if !status.is_success() && status.as_u16() != 206 {
117            if status.as_u16() >= 500 {
118                return Err(Aria2Error::Recoverable(RecoverableError::ServerError {
119                    code: status.as_u16(),
120                }));
121            }
122            return Err(Aria2Error::Fatal(crate::error::FatalError::Config(
123                format!("HTTP error: {}", status),
124            )));
125        }
126
127        let resp_length = response.content_length().unwrap_or(0) as u64;
128        let actual_total = if resume_state.should_resume {
129            resume_state.start_offset + resp_length
130        } else {
131            resp_length
132        };
133        {
134            let mut g = self.group.write().await;
135            g.set_total_length(actual_total).await;
136            g.set_total_length_atomic(actual_total);
137        }
138
139        let start_offset = if resume_state.should_resume {
140            resume_state.start_offset
141        } else {
142            0
143        };
144
145        self.progress_updater.reset(start_offset);
146
147        let rate_limit = { self.group.read().await.options().max_download_limit };
148
149        let raw_writer = DefaultDiskWriter::new(&self.output_path);
150        let mut writer: Box<dyn DiskWriter> = match rate_limit {
151            Some(rate) if rate > 0 => {
152                let cfg = RateLimiterConfig::new(Some(rate), None);
153                let limiter = RateLimiter::new(&cfg);
154                tracing::debug!("Download speed limit enabled: {} bytes/s", rate);
155                {
156                    let g = self.group.read().await;
157                    g.set_rate_limiter(limiter.clone()).await;
158                }
159                Box::new(ThrottledWriter::new(raw_writer, limiter))
160            }
161            _ => Box::new(raw_writer),
162        };
163
164        let mut stream = response.bytes_stream();
165        let mut completed_bytes = start_offset;
166        let write_piece = constants::RATE_LIMITER_CHUNK_SIZE;
167
168        while let Some(chunk) = stream.next().await {
169            let data: bytes::Bytes = chunk.map_err(|e| {
170                Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
171                    message: e.to_string(),
172                })
173            })?;
174
175            let mut offset = 0usize;
176            while offset < data.len() {
177                let end = (offset + write_piece).min(data.len());
178                let piece = &data[offset..end];
179                writer.write(piece).await?;
180                completed_bytes += piece.len() as u64;
181                offset = end;
182
183                self.progress_updater
184                    .update_progress(
185                        completed_bytes,
186                        constants::PROGRESS_UPDATE_BYTES as u64,
187                        constants::HTTP_SPEED_UPDATE_INTERVAL_MS,
188                    )
189                    .await;
190            }
191        }
192
193        writer.finalize().await.ok();
194
195        let final_speed = {
196            let g = self.group.read().await;
197            let elapsed = g.elapsed_time().await;
198            match elapsed {
199                Some(d) if d.as_secs_f64() > 0.0 => {
200                    (completed_bytes as f64 / d.as_secs_f64()) as u64
201                }
202                _ => 0,
203            }
204        };
205        {
206            let mut g = self.group.write().await;
207            g.update_progress(completed_bytes).await;
208            g.update_speed(final_speed, 0).await;
209            g.set_completed_length(completed_bytes);
210            g.set_download_speed_cached(final_speed);
211            g.complete().await?;
212        }
213
214        tracing::info!(
215            "Sequential download complete: {} ({} bytes)",
216            self.output_path.display(),
217            completed_bytes
218        );
219        self.cookie_helper.save_cookies_if_configured();
220        Ok(())
221    }
222
223    pub async fn execute_with_gaps(
224        &mut self,
225        uri: &str,
226        total_length: u64,
227        completed_ranges: &[(u64, u64)],
228    ) -> GapDownloadResult {
229        let gaps = Self::find_all_gaps(completed_ranges, total_length);
230        tracing::info!(
231            "Starting sequential download with gaps: uri={}, total={}, gaps={:?}",
232            uri,
233            total_length,
234            gaps
235        );
236
237        if gaps.is_empty() {
238            tracing::info!("No gaps to download, download complete");
239            return GapDownloadResult {
240                completed_gaps: Vec::new(),
241                error: None,
242            };
243        }
244
245        let url_parsed = reqwest::Url::parse(uri).ok();
246        let cookie_hdr = if let Some(ref url) = url_parsed {
247            let hdr = self.cookie_helper.build_cookie_header_from_url(url);
248            if hdr.is_empty() { None } else { Some(hdr) }
249        } else {
250            None
251        };
252
253        let mut completed_bytes = completed_ranges.iter().map(|(_, len)| len).sum::<u64>();
254        self.progress_updater.reset(completed_bytes);
255
256        let mut writer = CachedDiskWriter::new(&self.output_path, Some(total_length), None);
257
258        let rate_limit = { self.group.read().await.options().max_download_limit };
259        let limiter = rate_limit
260            .filter(|&r| r > 0)
261            .map(|r| RateLimiter::new(&RateLimiterConfig::new(Some(r), None)));
262        if let Some(ref lim) = limiter {
263            let g = self.group.read().await;
264            g.set_rate_limiter(lim.clone()).await;
265        }
266
267        let mut last_progress_update = completed_bytes;
268        let mut completed_gaps: Vec<(u64, u64)> = Vec::new();
269
270        for (gap_start, gap_length) in gaps {
271            let gap_end = gap_start + gap_length - 1;
272            let range_header = format!("bytes={}-{}", gap_start, gap_end);
273            tracing::debug!("Sequential Range request for gap: {}", range_header);
274
275            let mut request = self.client.get(uri).header("Range", &range_header);
276            if let Some(ref hdr) = cookie_hdr {
277                request = request.header("Cookie", hdr);
278            }
279            for (name, value) in &self.headers {
280                request = request.header(name, value);
281            }
282
283            let response = match request.send().await {
284                Ok(r) => r,
285                Err(e) => {
286                    tracing::warn!(
287                        "Gap download failed ({}), cleaning up partial data",
288                        range_header
289                    );
290                    Self::cleanup_partial_gap(&mut writer, gap_start, 0).await;
291                    return GapDownloadResult {
292                        completed_gaps,
293                        error: Some(Aria2Error::Recoverable(
294                            RecoverableError::TemporaryNetworkFailure {
295                                message: format!("HTTP request failed: {}", e),
296                            },
297                        )),
298                    };
299                }
300            };
301
302            self.cookie_helper.extract_and_store_cookies(uri, &response);
303
304            let status = response.status();
305            if !status.is_success() && status.as_u16() != 206 {
306                tracing::warn!(
307                    "Gap download failed with HTTP status {} ({}), cleaning up partial data",
308                    status,
309                    range_header
310                );
311                Self::cleanup_partial_gap(&mut writer, gap_start, 0).await;
312                let error = if status.as_u16() >= 500 {
313                    Aria2Error::Recoverable(RecoverableError::ServerError {
314                        code: status.as_u16(),
315                    })
316                } else {
317                    Aria2Error::Fatal(crate::error::FatalError::Config(format!(
318                        "HTTP error: {}",
319                        status
320                    )))
321                };
322                return GapDownloadResult {
323                    completed_gaps,
324                    error: Some(error),
325                };
326            }
327
328            let mut stream = response.bytes_stream();
329            let mut stream_offset = gap_start;
330            let mut bytes_downloaded = 0u64;
331
332            while let Some(chunk_result) = stream.next().await {
333                let data: bytes::Bytes = match chunk_result {
334                    Ok(d) => d,
335                    Err(e) => {
336                        tracing::warn!(
337                            "Gap download failed during streaming ({}), cleaning up partial data",
338                            range_header
339                        );
340                        Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
341                        return GapDownloadResult {
342                            completed_gaps,
343                            error: Some(Aria2Error::Recoverable(
344                                RecoverableError::TemporaryNetworkFailure {
345                                    message: e.to_string(),
346                                },
347                            )),
348                        };
349                    }
350                };
351
352                if let Some(ref lim) = limiter {
353                    lim.acquire_download(data.len() as u64).await;
354                }
355
356                match writer.write_bytes_at(stream_offset, data.clone()).await {
357                    Ok(_) => {}
358                    Err(e) => {
359                        tracing::warn!(
360                            "Write failed during gap download ({}), cleaning up partial data",
361                            range_header
362                        );
363                        Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
364                        return GapDownloadResult {
365                            completed_gaps,
366                            error: Some(Aria2Error::Fatal(crate::error::FatalError::Config(
367                                format!("Write failed: {}", e),
368                            ))),
369                        };
370                    }
371                }
372
373                let data_len = data.len() as u64;
374                completed_bytes += data_len;
375                stream_offset += data_len;
376                bytes_downloaded += data_len;
377
378                if completed_bytes - last_progress_update >= constants::PROGRESS_UPDATE_BYTES as u64
379                {
380                    self.progress_updater
381                        .update_progress(
382                            completed_bytes,
383                            constants::PROGRESS_UPDATE_BYTES as u64,
384                            constants::HTTP_SPEED_UPDATE_INTERVAL_MS,
385                        )
386                        .await;
387                    last_progress_update = completed_bytes;
388                }
389            }
390
391            if bytes_downloaded == gap_length {
392                completed_gaps.push((gap_start, gap_length));
393                tracing::debug!(
394                    "Gap download complete: {} ({} bytes)",
395                    range_header,
396                    bytes_downloaded
397                );
398            } else {
399                tracing::warn!(
400                    "Gap download incomplete: expected {} bytes, got {} bytes",
401                    gap_length,
402                    bytes_downloaded
403                );
404                Self::cleanup_partial_gap(&mut writer, gap_start, bytes_downloaded).await;
405            }
406        }
407
408        if let Err(e) = writer.flush().await {
409            tracing::warn!("Flush failed during gap download: {}", e);
410            return GapDownloadResult {
411                completed_gaps,
412                error: Some(Aria2Error::Fatal(crate::error::FatalError::Config(
413                    format!("Flush failed: {}", e),
414                ))),
415            };
416        }
417
418        let final_speed = {
419            let g = self.group.read().await;
420            let elapsed = g.elapsed_time().await;
421            match elapsed {
422                Some(d) if d.as_secs_f64() > 0.0 => {
423                    (completed_bytes as f64 / d.as_secs_f64()) as u64
424                }
425                _ => 0,
426            }
427        };
428        {
429            let mut g = self.group.write().await;
430            g.set_total_length(completed_bytes).await;
431            g.set_total_length_atomic(completed_bytes);
432            g.update_progress(completed_bytes).await;
433            g.update_speed(final_speed, 0).await;
434            g.set_completed_length(completed_bytes);
435            g.set_download_speed_cached(final_speed);
436            if let Err(e) = g.complete().await {
437                tracing::warn!("Failed to complete request group: {}", e);
438                return GapDownloadResult {
439                    completed_gaps,
440                    error: Some(e),
441                };
442            }
443        }
444
445        tracing::info!(
446            "Sequential download with gaps complete: {} ({} bytes, {} gaps completed)",
447            self.output_path.display(),
448            completed_bytes,
449            completed_gaps.len()
450        );
451        self.cookie_helper.save_cookies_if_configured();
452        GapDownloadResult {
453            completed_gaps,
454            error: None,
455        }
456    }
457
458    async fn cleanup_partial_gap(
459        writer: &mut CachedDiskWriter,
460        gap_start: u64,
461        bytes_written: u64,
462    ) {
463        if bytes_written == 0 {
464            return;
465        }
466        let zero_data = vec![0u8; bytes_written as usize];
467        if let Err(e) = writer
468            .write_bytes_at(gap_start, bytes::Bytes::from(zero_data))
469            .await
470        {
471            tracing::warn!(
472                "Failed to cleanup partial gap at {} ({} bytes): {}",
473                gap_start,
474                bytes_written,
475                e
476            );
477        }
478    }
479
480    pub fn merge_ranges(ranges: &[(u64, u64)]) -> Vec<(u64, u64)> {
481        if ranges.is_empty() {
482            return Vec::new();
483        }
484
485        let mut sorted = ranges.to_vec();
486        sorted.sort_by_key(|r| r.0);
487
488        let mut merged = Vec::new();
489        let mut current = sorted[0];
490
491        for &(offset, length) in sorted.iter().skip(1) {
492            let current_end = current.0 + current.1;
493            let next_end = offset + length;
494
495            if offset <= current_end {
496                current = (current.0, std::cmp::max(current_end, next_end) - current.0);
497            } else {
498                merged.push(current);
499                current = (offset, length);
500            }
501        }
502        merged.push(current);
503        merged
504    }
505
506    pub fn find_all_gaps(completed_ranges: &[(u64, u64)], total_length: u64) -> Vec<(u64, u64)> {
507        let merged_ranges = Self::merge_ranges(completed_ranges);
508        let mut gaps = Vec::new();
509        if merged_ranges.is_empty() {
510            if total_length > 0 {
511                gaps.push((0, total_length));
512            }
513            return gaps;
514        }
515
516        let mut current = 0;
517        for &(offset, length) in &merged_ranges {
518            if offset > current {
519                gaps.push((current, offset - current));
520            }
521            current = std::cmp::max(current, offset + length);
522        }
523        if current < total_length {
524            gaps.push((current, total_length - current));
525        }
526        gaps
527    }
528
529    pub async fn execute_with_gaps_with_retry(
530        &mut self,
531        uri: &str,
532        total_length: u64,
533        completed_ranges: &[(u64, u64)],
534        retry_policy: &RetryPolicy,
535    ) -> Result<()> {
536        let mut last_err = None;
537        let mut accumulated_completed: Vec<(u64, u64)> = completed_ranges.to_vec();
538
539        for attempt in 0..=retry_policy.max_retries {
540            if attempt > 0
541                && let Some(wait) = retry_policy.compute_wait(attempt - 1)
542            {
543                tracing::info!(
544                    "Sequential download with gaps retry #{} (waiting {:?}), {} ranges already completed...",
545                    attempt,
546                    wait,
547                    accumulated_completed.len()
548                );
549                tokio::time::sleep(wait).await;
550            }
551
552            let result = self
553                .execute_with_gaps(uri, total_length, &accumulated_completed)
554                .await;
555
556            if !result.completed_gaps.is_empty() {
557                tracing::info!(
558                    "Attempt #{} completed {} gaps",
559                    attempt + 1,
560                    result.completed_gaps.len()
561                );
562                accumulated_completed.extend(result.completed_gaps);
563                accumulated_completed = Self::merge_ranges(&accumulated_completed);
564            }
565
566            if result.error.is_none() {
567                return Ok(());
568            }
569
570            tracing::warn!(
571                "Sequential download with gaps attempt #{} failed: {}",
572                attempt + 1,
573                result.error.as_ref().unwrap()
574            );
575            last_err = result.error;
576
577            if retry_policy.is_exhausted(attempt)
578                || !retry_policy.should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
579            {
580                break;
581            }
582        }
583
584        Err(last_err.unwrap_or_else(|| {
585            Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
586                message: "All retries failed".into(),
587            })
588        }))
589    }
590
591    pub async fn execute_with_retry(
592        &mut self,
593        uri: &str,
594        resume_state: &ResumeState,
595        total_length: u64,
596        retry_policy: &RetryPolicy,
597    ) -> Result<()> {
598        let mut last_err = None;
599
600        for attempt in 0..=retry_policy.max_retries {
601            if attempt > 0
602                && let Some(wait) = retry_policy.compute_wait(attempt - 1)
603            {
604                tracing::info!(
605                    "Sequential download retry #{} (waiting {:?})...",
606                    attempt,
607                    wait
608                );
609                tokio::time::sleep(wait).await;
610            }
611
612            match self.execute(uri, resume_state, total_length).await {
613                Ok(()) => return Ok(()),
614                Err(e) => {
615                    tracing::warn!("Sequential download attempt #{} failed: {}", attempt + 1, e);
616                    last_err = Some(e);
617                    if retry_policy.is_exhausted(attempt)
618                        || !retry_policy
619                            .should_retry_error(&format!("{:?}", last_err.as_ref().unwrap()))
620                    {
621                        break;
622                    }
623                }
624            }
625        }
626
627        Err(last_err.unwrap_or_else(|| {
628            Aria2Error::Recoverable(RecoverableError::TemporaryNetworkFailure {
629                message: "All retries failed".into(),
630            })
631        }))
632    }
633}
634
635impl SequentialDownloader {
636    #[cfg(target_os = "linux")]
637    async fn try_splice_sequential(&mut self, uri: &str, total_length: u64) -> Result<()> {
638        let file = std::fs::OpenOptions::new()
639            .write(true)
640            .create(true)
641            .truncate(true)
642            .open(&self.output_path)?;
643
644        let bytes = crate::http::splice_http::try_splice_download(uri, 0, total_length, &file, 0)
645            .await
646            .map_err(|e| Aria2Error::Io(format!("splice download failed: {e}")))?;
647
648        let final_speed = {
649            let g = self.group.read().await;
650            let elapsed = g.elapsed_time().await;
651            match elapsed {
652                Some(d) if d.as_secs_f64() > 0.0 => (bytes as f64 / d.as_secs_f64()) as u64,
653                _ => 0,
654            }
655        };
656
657        {
658            let mut g = self.group.write().await;
659            g.set_total_length(bytes).await;
660            g.set_total_length_atomic(bytes);
661            g.update_progress(bytes).await;
662            g.set_completed_length(bytes);
663            g.update_speed(final_speed, 0).await;
664            g.set_download_speed_cached(final_speed);
665            g.complete().await?;
666        }
667
668        tracing::info!(
669            "Sequential download (splice) complete: {} ({} bytes)",
670            self.output_path.display(),
671            bytes
672        );
673        self.cookie_helper.save_cookies_if_configured();
674        Ok(())
675    }
676}
677
678#[cfg(test)]
679mod tests {
680    use super::SequentialDownloader;
681
682    #[test]
683    fn test_merge_ranges_empty() {
684        let ranges = &[];
685        let result = SequentialDownloader::merge_ranges(ranges);
686        assert!(result.is_empty());
687    }
688
689    #[test]
690    fn test_merge_ranges_single() {
691        let ranges = &[(0, 100)];
692        let result = SequentialDownloader::merge_ranges(ranges);
693        assert_eq!(result, vec![(0, 100)]);
694    }
695
696    #[test]
697    fn test_merge_ranges_non_overlapping_sorted() {
698        let ranges = &[(0, 100), (200, 100), (400, 100)];
699        let result = SequentialDownloader::merge_ranges(ranges);
700        assert_eq!(result, vec![(0, 100), (200, 100), (400, 100)]);
701    }
702
703    #[test]
704    fn test_merge_ranges_non_overlapping_unsorted() {
705        let ranges = &[(200, 100), (0, 100), (400, 100)];
706        let result = SequentialDownloader::merge_ranges(ranges);
707        assert_eq!(result, vec![(0, 100), (200, 100), (400, 100)]);
708    }
709
710    #[test]
711    fn test_merge_ranges_overlapping_inner() {
712        let ranges = &[(0, 200), (50, 50)];
713        let result = SequentialDownloader::merge_ranges(ranges);
714        assert_eq!(result, vec![(0, 200)]);
715    }
716
717    #[test]
718    fn test_merge_ranges_overlapping_inner_unsorted() {
719        let ranges = &[(50, 50), (0, 200)];
720        let result = SequentialDownloader::merge_ranges(ranges);
721        assert_eq!(result, vec![(0, 200)]);
722    }
723
724    #[test]
725    fn test_merge_ranges_overlapping_partial() {
726        let ranges = &[(0, 100), (50, 150)];
727        let result = SequentialDownloader::merge_ranges(ranges);
728        assert_eq!(result, vec![(0, 200)]);
729    }
730
731    #[test]
732    fn test_merge_ranges_overlapping_partial_unsorted() {
733        let ranges = &[(50, 150), (0, 100)];
734        let result = SequentialDownloader::merge_ranges(ranges);
735        assert_eq!(result, vec![(0, 200)]);
736    }
737
738    #[test]
739    fn test_merge_ranges_adjacent() {
740        let ranges = &[(0, 100), (100, 100)];
741        let result = SequentialDownloader::merge_ranges(ranges);
742        assert_eq!(result, vec![(0, 200)]);
743    }
744
745    #[test]
746    fn test_merge_ranges_adjacent_unsorted() {
747        let ranges = &[(100, 100), (0, 100)];
748        let result = SequentialDownloader::merge_ranges(ranges);
749        assert_eq!(result, vec![(0, 200)]);
750    }
751
752    #[test]
753    fn test_merge_ranges_duplicate() {
754        let ranges = &[(0, 100), (0, 100)];
755        let result = SequentialDownloader::merge_ranges(ranges);
756        assert_eq!(result, vec![(0, 100)]);
757    }
758
759    #[test]
760    fn test_merge_ranges_multiple_overlapping() {
761        let ranges = &[(0, 100), (50, 150), (200, 100), (180, 150)];
762        let result = SequentialDownloader::merge_ranges(ranges);
763        assert_eq!(result, vec![(0, 330)]);
764    }
765
766    #[test]
767    fn test_merge_ranges_zero_length() {
768        let ranges = &[(0, 0), (100, 0), (200, 100)];
769        let result = SequentialDownloader::merge_ranges(ranges);
770        assert_eq!(result, vec![(0, 0), (100, 0), (200, 100)]);
771    }
772
773    #[test]
774    fn test_merge_ranges_complex() {
775        let ranges = &[(10, 5), (0, 20), (15, 25), (50, 10), (45, 20), (100, 50)];
776        let result = SequentialDownloader::merge_ranges(ranges);
777        assert_eq!(result, vec![(0, 40), (45, 20), (100, 50)]);
778    }
779
780    #[test]
781    fn test_find_all_gaps_empty_ranges() {
782        let ranges = &[];
783        let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
784        assert_eq!(gaps, vec![(0, 1000)]);
785    }
786
787    #[test]
788    fn test_find_all_gaps_no_gaps() {
789        let ranges = &[(0, 1000)];
790        let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
791        assert!(gaps.is_empty());
792    }
793
794    #[test]
795    fn test_find_all_gaps_single_gap() {
796        let ranges = &[(0, 500), (600, 400)];
797        let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
798        assert_eq!(gaps, vec![(500, 100)]);
799    }
800
801    #[test]
802    fn test_find_all_gaps_multiple_gaps() {
803        let ranges = &[(0, 100), (200, 100), (400, 200)];
804        let gaps = SequentialDownloader::find_all_gaps(ranges, 1000);
805        assert_eq!(gaps, vec![(100, 100), (300, 100), (600, 400)]);
806    }
807
808    #[test]
809    fn test_find_all_gaps_overlapping_ranges() {
810        let ranges = &[(0, 200), (100, 150), (300, 50)];
811        let gaps = SequentialDownloader::find_all_gaps(ranges, 500);
812        assert_eq!(gaps, vec![(250, 50), (350, 150)]);
813    }
814}