Skip to main content

zsync_rs/
http.rs

1use std::io::Read;
2
3use crate::control::ControlFile;
4
5#[derive(Debug, thiserror::Error)]
6pub enum HttpError {
7    #[error("HTTP error: {0}")]
8    Http(String),
9    #[error("IO error: {0}")]
10    Io(#[from] std::io::Error),
11    #[error("Invalid URL: {0}")]
12    InvalidUrl(String),
13    #[error("No URLs available")]
14    NoUrls,
15}
16
17pub struct HttpClient {
18    agent: ureq::Agent,
19}
20
21impl Default for HttpClient {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27impl HttpClient {
28    pub fn new() -> Self {
29        Self {
30            agent: ureq::Agent::config_builder()
31                .https_only(false)
32                .build()
33                .new_agent(),
34        }
35    }
36
37    pub fn fetch_control_file(&self, url: &str) -> Result<ControlFile, HttpError> {
38        let response = self
39            .agent
40            .get(url)
41            .call()
42            .map_err(|e| HttpError::Http(e.to_string()))?;
43
44        let mut reader = response.into_body().into_reader();
45        ControlFile::parse(&mut reader).map_err(|e| HttpError::Http(e.to_string()))
46    }
47
48    pub fn fetch_range(&self, url: &str, start: u64, end: u64) -> Result<Vec<u8>, HttpError> {
49        let range_header = format!("bytes={}-{}", start, end);
50
51        let response = self
52            .agent
53            .get(url)
54            .header("Range", &range_header)
55            .call()
56            .map_err(|e| HttpError::Http(e.to_string()))?;
57
58        let status = response.status();
59        if status != 206 && status != 200 {
60            return Err(HttpError::Http(format!(
61                "Expected 206 Partial Content, got {}",
62                status
63            )));
64        }
65
66        let mut buf = Vec::new();
67        response.into_body().into_reader().read_to_end(&mut buf)?;
68
69        Ok(buf)
70    }
71
72    pub fn fetch_ranges(
73        &self,
74        url: &str,
75        ranges: &[(u64, u64)],
76        blocksize: usize,
77    ) -> Result<Vec<(u64, Vec<u8>)>, HttpError> {
78        let mut results = Vec::new();
79
80        for &(start, end) in ranges {
81            let data = self.fetch_range(url, start, end)?;
82            let aligned_start = (start / blocksize as u64) * blocksize as u64;
83            results.push((aligned_start, data));
84        }
85
86        Ok(results)
87    }
88}
89
90/// Default gap threshold for range merging (256 KiB, same as zsync2).
91pub const DEFAULT_RANGE_GAP_THRESHOLD: u64 = 256 * 1024;
92
93/// Merge byte ranges to minimize HTTP requests.
94/// Gaps smaller than the threshold are merged to save HTTP round-trips.
95pub fn merge_byte_ranges(ranges: &[(u64, u64)], gap_threshold: u64) -> Vec<(u64, u64)> {
96    if ranges.len() <= 1 {
97        return ranges.to_vec();
98    }
99
100    let mut merged = vec![ranges[0]];
101    for &(start, end) in &ranges[1..] {
102        let last = merged.last_mut().unwrap();
103        let gap = start.saturating_sub(last.1 + 1);
104        if gap <= gap_threshold {
105            last.1 = end;
106        } else {
107            merged.push((start, end));
108        }
109    }
110    merged
111}
112
113pub fn byte_ranges_from_block_ranges(
114    block_ranges: &[(usize, usize)],
115    blocksize: usize,
116    file_length: u64,
117) -> Vec<(u64, u64)> {
118    block_ranges
119        .iter()
120        .map(|&(start_block, end_block)| {
121            let start = start_block as u64 * blocksize as u64;
122            let end =
123                ((end_block as u64 * blocksize as u64).saturating_sub(1)).min(file_length - 1);
124            (start, end)
125        })
126        .collect()
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_byte_ranges_from_block_ranges() {
135        let block_ranges = vec![(0, 2), (4, 6)];
136        let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 10000);
137        assert_eq!(byte_ranges, vec![(0, 2047), (4096, 6143)]);
138    }
139
140    #[test]
141    fn test_byte_ranges_clamped_to_file_length() {
142        let block_ranges = vec![(9, 10)];
143        let byte_ranges = byte_ranges_from_block_ranges(&block_ranges, 1024, 9500);
144        assert_eq!(byte_ranges, vec![(9216, 9499)]);
145    }
146}