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