opendal/raw/http_util/
header.rs1use std::collections::HashMap;
19
20use base64::engine::general_purpose;
21use base64::Engine;
22use chrono::DateTime;
23use chrono::Utc;
24use http::header::CACHE_CONTROL;
25use http::header::CONTENT_DISPOSITION;
26use http::header::CONTENT_ENCODING;
27use http::header::CONTENT_LENGTH;
28use http::header::CONTENT_RANGE;
29use http::header::CONTENT_TYPE;
30use http::header::ETAG;
31use http::header::LAST_MODIFIED;
32use http::header::LOCATION;
33use http::HeaderMap;
34use http::HeaderName;
35use http::HeaderValue;
36use md5::Digest;
37
38use crate::raw::*;
39use crate::EntryMode;
40use crate::Error;
41use crate::ErrorKind;
42use crate::Metadata;
43use crate::Result;
44
45pub fn parse_location(headers: &HeaderMap) -> Result<Option<&str>> {
50 parse_header_to_str(headers, LOCATION)
51}
52
53pub fn parse_cache_control(headers: &HeaderMap) -> Result<Option<&str>> {
60 parse_header_to_str(headers, CACHE_CONTROL)
61}
62
63pub fn parse_content_length(headers: &HeaderMap) -> Result<Option<u64>> {
65 parse_header_to_str(headers, CONTENT_LENGTH)?
66 .map(|v| {
67 v.parse::<u64>().map_err(|e| {
68 Error::new(ErrorKind::Unexpected, "header value is not valid integer").set_source(e)
69 })
70 })
71 .transpose()
72}
73
74pub fn parse_content_md5(headers: &HeaderMap) -> Result<Option<&str>> {
76 parse_header_to_str(headers, "content-md5")
77}
78
79pub fn parse_content_type(headers: &HeaderMap) -> Result<Option<&str>> {
81 parse_header_to_str(headers, CONTENT_TYPE)
82}
83
84pub fn parse_content_encoding(headers: &HeaderMap) -> Result<Option<&str>> {
86 parse_header_to_str(headers, CONTENT_ENCODING)
87}
88
89pub fn parse_content_range(headers: &HeaderMap) -> Result<Option<BytesContentRange>> {
91 parse_header_to_str(headers, CONTENT_RANGE)?
92 .map(|v| v.parse())
93 .transpose()
94}
95
96pub fn parse_last_modified(headers: &HeaderMap) -> Result<Option<DateTime<Utc>>> {
98 parse_header_to_str(headers, LAST_MODIFIED)?
99 .map(parse_datetime_from_rfc2822)
100 .transpose()
101}
102
103pub fn parse_etag(headers: &HeaderMap) -> Result<Option<&str>> {
105 parse_header_to_str(headers, ETAG)
106}
107
108pub fn parse_content_disposition(headers: &HeaderMap) -> Result<Option<&str>> {
110 parse_header_to_str(headers, CONTENT_DISPOSITION)
111}
112
113pub fn parse_multipart_boundary(headers: &HeaderMap) -> Result<Option<&str>> {
115 parse_header_to_str(headers, CONTENT_TYPE).map(|v| v.and_then(|v| v.split("boundary=").nth(1)))
116}
117
118#[inline]
120pub fn parse_header_to_str<K>(headers: &HeaderMap, name: K) -> Result<Option<&str>>
121where
122 HeaderName: TryFrom<K>,
123{
124 let name = HeaderName::try_from(name).map_err(|_| {
125 Error::new(
126 ErrorKind::Unexpected,
127 "header name must be valid http header name but not",
128 )
129 .with_operation("http_util::parse_header_to_str")
130 })?;
131
132 let value = if let Some(v) = headers.get(&name) {
133 v
134 } else {
135 return Ok(None);
136 };
137
138 Ok(Some(value.to_str().map_err(|e| {
139 Error::new(
140 ErrorKind::Unexpected,
141 "header value must be valid utf-8 string but not",
142 )
143 .with_operation("http_util::parse_header_to_str")
144 .with_context("header_name", name.as_str())
145 .set_source(e)
146 })?))
147}
148
149pub fn parse_into_metadata(path: &str, headers: &HeaderMap) -> Result<Metadata> {
157 let mode = if path.ends_with('/') {
158 EntryMode::DIR
159 } else {
160 EntryMode::FILE
161 };
162 let mut m = Metadata::new(mode);
163
164 if let Some(v) = parse_cache_control(headers)? {
165 m.set_cache_control(v);
166 }
167
168 if let Some(v) = parse_content_length(headers)? {
169 m.set_content_length(v);
170 }
171
172 if let Some(v) = parse_content_type(headers)? {
173 m.set_content_type(v);
174 }
175
176 if let Some(v) = parse_content_encoding(headers)? {
177 m.set_content_encoding(v);
178 }
179
180 if let Some(v) = parse_content_range(headers)? {
181 m.set_content_range(v);
182 }
183
184 if let Some(v) = parse_etag(headers)? {
185 m.set_etag(v);
186 }
187
188 if let Some(v) = parse_content_md5(headers)? {
189 m.set_content_md5(v);
190 }
191
192 if let Some(v) = parse_last_modified(headers)? {
193 m.set_last_modified(v);
194 }
195
196 if let Some(v) = parse_content_disposition(headers)? {
197 m.set_content_disposition(v);
198 }
199
200 Ok(m)
201}
202
203pub fn parse_prefixed_headers(headers: &HeaderMap, prefix: &str) -> HashMap<String, String> {
205 headers
206 .iter()
207 .filter_map(|(name, value)| {
208 name.as_str().strip_prefix(prefix).and_then(|stripped_key| {
209 value
210 .to_str()
211 .ok()
212 .map(|parsed_value| (stripped_key.to_string(), parsed_value.to_string()))
213 })
214 })
215 .collect()
216}
217
218pub fn format_content_md5(bs: &[u8]) -> String {
220 let mut hasher = md5::Md5::new();
221 hasher.update(bs);
222
223 general_purpose::STANDARD.encode(hasher.finalize())
224}
225
226pub fn format_authorization_by_basic(username: &str, password: &str) -> Result<String> {
232 if username.is_empty() {
233 return Err(Error::new(
234 ErrorKind::Unexpected,
235 "can't build authorization header with empty username",
236 ));
237 }
238
239 let value = general_purpose::STANDARD.encode(format!("{username}:{password}"));
240
241 Ok(format!("Basic {value}"))
242}
243
244pub fn format_authorization_by_bearer(token: &str) -> Result<String> {
250 if token.is_empty() {
251 return Err(Error::new(
252 ErrorKind::Unexpected,
253 "can't build authorization header with empty token",
254 ));
255 }
256
257 Ok(format!("Bearer {token}"))
258}
259
260pub fn build_header_value(v: &str) -> Result<HeaderValue> {
262 HeaderValue::from_str(v).map_err(|e| {
263 Error::new(
264 ErrorKind::ConfigInvalid,
265 "header value contains invalid characters",
266 )
267 .with_operation("http_util::build_header_value")
268 .set_source(e)
269 })
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
278 fn test_format_content_md5() {
279 let cases = vec![(
280 r#"<Delete>
281<Object>
282 <Key>sample1.txt</Key>
283 </Object>
284 <Object>
285 <Key>sample2.txt</Key>
286 </Object>
287 </Delete>"#,
288 "WOctCY1SS662e7ziElh4cw==",
289 )];
290
291 for (input, expected) in cases {
292 let actual = format_content_md5(input.as_bytes());
293
294 assert_eq!(actual, expected)
295 }
296 }
297
298 #[test]
303 fn test_format_authorization_by_basic() {
304 let cases = vec![
305 ("aladdin", "opensesame", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"),
306 ("aladdin", "", "Basic YWxhZGRpbjo="),
307 (
308 "Aladdin",
309 "open sesame",
310 "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
311 ),
312 ("Aladdin", "", "Basic QWxhZGRpbjo="),
313 ];
314
315 for (username, password, expected) in cases {
316 let actual =
317 format_authorization_by_basic(username, password).expect("format must success");
318
319 assert_eq!(actual, expected)
320 }
321 }
322
323 #[test]
327 fn test_format_authorization_by_bearer() {
328 let cases = vec![("mF_9.B5f-4.1JqM", "Bearer mF_9.B5f-4.1JqM")];
329
330 for (token, expected) in cases {
331 let actual = format_authorization_by_bearer(token).expect("format must success");
332
333 assert_eq!(actual, expected)
334 }
335 }
336
337 #[test]
338 fn test_parse_multipart_boundary() {
339 let cases = vec![
340 (
341 "multipart/mixed; boundary=gc0p4Jq0M2Yt08jU534c0p",
342 Some("gc0p4Jq0M2Yt08jU534c0p"),
343 ),
344 ("multipart/mixed", None),
345 ];
346
347 for (input, expected) in cases {
348 let mut headers = HeaderMap::new();
349 headers.insert(CONTENT_TYPE, HeaderValue::from_str(input).unwrap());
350
351 let actual = parse_multipart_boundary(&headers).expect("parse must success");
352
353 assert_eq!(actual, expected)
354 }
355 }
356}