Skip to main content

aria2_protocol/http/
response.rs

1use tracing::debug;
2
3#[derive(Debug, Clone)]
4pub struct HttpResponse {
5    pub status_code: u16,
6    pub status_text: String,
7    pub headers: Vec<(String, String)>,
8    pub body: Vec<u8>,
9}
10
11impl HttpResponse {
12    pub fn new(status_code: u16, status_text: String) -> Self {
13        Self {
14            status_code,
15            status_text,
16            headers: Vec::new(),
17            body: Vec::new(),
18        }
19    }
20
21    pub fn is_success(&self) -> bool {
22        (200..300).contains(&self.status_code)
23    }
24
25    pub fn is_redirect(&self) -> bool {
26        [301, 302, 303, 307, 308].contains(&self.status_code)
27    }
28
29    pub fn is_partial_content(&self) -> bool {
30        self.status_code == 206
31    }
32
33    pub fn is_client_error(&self) -> bool {
34        (400..500).contains(&self.status_code)
35    }
36
37    pub fn is_server_error(&self) -> bool {
38        (500..600).contains(&self.status_code)
39    }
40
41    pub fn header(&self, name: &str) -> Option<&String> {
42        self.headers
43            .iter()
44            .find(|(k, _)| k.eq_ignore_ascii_case(name))
45            .map(|(_, v)| v)
46    }
47
48    pub fn header_all(&self, name: &str) -> Vec<&String> {
49        self.headers
50            .iter()
51            .filter(|(k, _)| k.eq_ignore_ascii_case(name))
52            .map(|(_, v)| v)
53            .collect()
54    }
55
56    pub fn content_length(&self) -> Option<u64> {
57        self.header("content-length")
58            .and_then(|v| v.parse::<u64>().ok())
59    }
60
61    pub fn content_type(&self) -> Option<&str> {
62        self.header("content-type").map(|s| s.as_str())
63    }
64
65    pub fn content_encoding(&self) -> Option<&str> {
66        self.header("content-encoding").map(|s| s.as_str())
67    }
68
69    pub fn content_disposition(&self) -> Option<&str> {
70        self.header("content-disposition").map(|s| s.as_str())
71    }
72
73    pub fn accept_ranges(&self) -> bool {
74        self.header("accept-ranges")
75            .map(|v| v.eq_ignore_ascii_case("bytes"))
76            .unwrap_or(false)
77    }
78
79    pub fn location(&self) -> Option<&str> {
80        self.header("location").map(|s| s.as_str())
81    }
82
83    pub fn etag(&self) -> Option<&str> {
84        self.header("etag").map(|s| s.as_str())
85    }
86
87    pub fn last_modified(&self) -> Option<&str> {
88        self.header("last-modified").map(|s| s.as_str())
89    }
90
91    pub fn server(&self) -> Option<&str> {
92        self.header("server").map(|s| s.as_str())
93    }
94
95    pub fn connection(&self) -> Option<&str> {
96        self.header("connection").map(|s| s.as_str())
97    }
98
99    pub fn transfer_encoding(&self) -> Option<&str> {
100        self.header("transfer-encoding").map(|s| s.as_str())
101    }
102
103    pub fn body_as_utf8(&self) -> Result<&str, std::str::Utf8Error> {
104        std::str::from_utf8(&self.body)
105    }
106
107    pub fn body_len(&self) -> usize {
108        self.body.len()
109    }
110
111    pub fn parse_content_range(&self) -> Option<ContentRange> {
112        let raw = self.header("content-range")?;
113        ContentRange::parse(raw)
114    }
115}
116
117#[derive(Debug, Clone, PartialEq)]
118pub struct ContentRange {
119    pub unit: String,
120    pub start: u64,
121    pub end: u64,
122    pub total_size: Option<u64>,
123}
124
125impl ContentRange {
126    pub fn parse(value: &str) -> Option<Self> {
127        let value = value.trim();
128        if !value.starts_with("bytes ") {
129            debug!("Content-Range格式异常: 非bytes单位");
130            return None;
131        }
132
133        let range_part = &value[6..];
134        let parts: Vec<&str> = range_part.split('/').collect();
135        if parts.len() != 2 {
136            debug!("Content-Range格式异常: 分割失败");
137            return None;
138        }
139
140        let range_values: Vec<&str> = parts[0].split('-').collect();
141        if range_values.len() != 2 {
142            debug!("Content-Range范围解析失败");
143            return None;
144        }
145
146        let start: u64 = range_values[0].trim().parse().ok()?;
147        let end: u64 = range_values[1].trim().parse().ok()?;
148        let total_size = match parts[1].trim() {
149            "*" => None,
150            s => s.parse().ok(),
151        };
152
153        Some(Self {
154            unit: "bytes".to_string(),
155            start,
156            end,
157            total_size,
158        })
159    }
160
161    pub fn size(&self) -> u64 {
162        self.end - self.start + 1
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::http::request::HttpRequest;
170
171    #[test]
172    fn test_response_status_checks() {
173        let resp_ok = HttpResponse::new(200, "OK".into());
174        assert!(resp_ok.is_success());
175        assert!(!resp_ok.is_redirect());
176        assert!(!resp_ok.is_partial_content());
177
178        let resp_206 = HttpResponse::new(206, "Partial Content".into());
179        assert!(resp_206.is_success());
180        assert!(resp_206.is_partial_content());
181
182        let resp_301 = HttpResponse::new(301, "Moved".into());
183        assert!(resp_301.is_redirect());
184
185        let resp_404 = HttpResponse::new(404, "Not Found".into());
186        assert!(resp_404.is_client_error());
187
188        let resp_500 = HttpResponse::new(500, "Server Error".into());
189        assert!(resp_500.is_server_error());
190    }
191
192    #[test]
193    fn test_headers_lookup() {
194        let mut resp = HttpResponse::new(200, "OK".into());
195        resp.headers
196            .push(("Content-Length".to_string(), "1024".to_string()));
197        resp.headers.push((
198            "Content-Type".to_string(),
199            "application/octet-stream".to_string(),
200        ));
201
202        assert_eq!(resp.content_length(), Some(1024));
203        assert_eq!(resp.content_type(), Some("application/octet-stream"));
204        assert_eq!(resp.header("content-length"), Some(&"1024".to_string()));
205    }
206
207    #[test]
208    fn test_content_range_parse() {
209        let cr = ContentRange::parse("bytes 0-499/1000").unwrap();
210        assert_eq!(cr.start, 0);
211        assert_eq!(cr.end, 499);
212        assert_eq!(cr.total_size, Some(1000));
213        assert_eq!(cr.size(), 500);
214
215        let cr_unknown_total = ContentRange::parse("bytes 0-499/*").unwrap();
216        assert_eq!(cr_unknown_total.total_size, None);
217
218        assert!(ContentRange::parse("invalid").is_none());
219        assert!(ContentRange::parse("bits 0-499/1000").is_none());
220    }
221
222    #[test]
223    fn test_request_builder() {
224        let req = HttpRequest::get("https://example.com/file.bin")
225            .with_range(500, Some(999))
226            .with_user_agent("aria2/1.37.0-Rust");
227
228        assert_eq!(req.method, "GET");
229        assert!(req.has_range());
230        assert_eq!(req.get_header("Range"), Some(&"bytes=500-999".to_string()));
231        assert_eq!(
232            req.get_header("User-Agent"),
233            Some(&"aria2/1.37.0-Rust".to_string())
234        );
235    }
236}