aria2_protocol/http/
response.rs1use 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}