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