use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use percent_encoding::percent_decode_str;
use regex::Regex;
use reqwest::{
Response, Url,
header::{
ACCEPT_RANGES, CONTENT_DISPOSITION, CONTENT_LENGTH, CONTENT_RANGE, CONTENT_TYPE, ETAG,
HeaderMap, LAST_MODIFIED, WWW_AUTHENTICATE,
},
};
use crate::hash::HashDigest;
#[derive(Debug, Clone)]
pub struct ResponseInfo {
status_code: u16,
request_url: Url,
response_headers: HeaderMap,
}
impl ResponseInfo {
#[cfg(test)]
fn new(status_code: u16, request_url: Url, response_headers: HeaderMap) -> Self {
Self {
status_code,
request_url,
response_headers,
}
}
pub fn from_url(request_url: Url) -> Self {
Self {
status_code: 0,
request_url,
response_headers: HeaderMap::new(),
}
}
pub fn is_successful(&self) -> bool {
(self.status_code >= 200) && (self.status_code <= 299)
}
pub fn status_code(&self) -> u16 {
self.status_code
}
pub fn url(&self) -> &Url {
&self.request_url
}
pub fn response_headers(&self) -> &HeaderMap {
&self.response_headers
}
pub fn mime_type(&self) -> Option<String> {
self.response_headers
.get(CONTENT_TYPE)
.and_then(|val| val.to_str().ok())
.map(|ct| ct.split(';').next().unwrap_or("").trim().to_string())
.filter(|s| !s.is_empty())
}
fn content_length(&self) -> Option<u64> {
self.response_headers
.get(CONTENT_LENGTH)
.and_then(|val| val.to_str().ok())
.and_then(|s| s.parse::<u64>().ok())
}
pub fn total_length(&self) -> Option<u64> {
if let Some(content_range) = self.content_range()
&& content_range.total.is_some()
{
return content_range.total;
}
self.content_length()
}
pub fn etag(&self) -> Option<String> {
self.response_headers
.get(ETAG)
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string())
}
pub fn extract_filename(&self) -> String {
if let Some(header) = self.response_headers.get(CONTENT_DISPOSITION)
&& let Ok(header_str) = header.to_str()
{
for part in header_str.split(';').map(str::trim) {
if let Some(val) = part.strip_prefix("filename*=") {
let value = val.trim_matches('"').trim_start_matches("UTF-8''");
if let Ok(decoded) = percent_decode_str(value).decode_utf8() {
return decoded.into_owned();
}
} else if let Some(val) = part.strip_prefix("filename=") {
return val.trim_matches('"').to_string();
}
}
}
let name_from_query = self.request_url.query_pairs().find_map(|(k, v)| {
let key = k.as_ref();
if (key.eq_ignore_ascii_case("file")
|| key.eq_ignore_ascii_case("filename")
|| key.eq_ignore_ascii_case("name"))
&& !v.is_empty()
{
Some(v.into_owned())
} else {
None
}
});
let name = name_from_query.or_else(|| {
self.request_url
.path_segments()
.and_then(|segments| segments.rev().find(|s| !s.is_empty()))
.map(|s| s.to_string())
});
if let Some(name) = name {
if !name.contains('.')
&& let Some(ext) = self.guess_extension()
{
return format!("{name}.{ext}");
}
return name;
}
if let Some(ext) = self.guess_extension() {
return format!("download.{ext}");
}
"download".to_string()
}
fn guess_extension(&self) -> Option<&'static str> {
let mime = self.mime_type()?;
mime_guess::get_mime_extensions_str(&mime).and_then(|exts| exts.first().copied())
}
pub fn content_range(&self) -> Option<ContentRange> {
self.response_headers
.get(CONTENT_RANGE)
.and_then(|val| val.to_str().ok())
.and_then(|header| {
CONTENT_RANGE_RE.captures(header).and_then(|caps| {
let start = caps.get(1)?.as_str().parse().ok()?;
let end = caps.get(2)?.as_str().parse().ok()?;
let total = match caps.get(3)?.as_str() {
"*" => None,
n => n.parse().ok(),
};
Some(ContentRange { start, end, total })
})
})
}
pub fn accepts_ranges(&self) -> bool {
self.response_headers
.get(ACCEPT_RANGES)
.and_then(|v| v.to_str().ok())
.map(|s| !s.eq_ignore_ascii_case("none"))
.unwrap_or(false)
}
pub fn parse_last_modified(&self) -> Option<i64> {
self.response_headers
.get(LAST_MODIFIED)
.and_then(|header_value| {
header_value.to_str().ok().and_then(|date_str| {
DateTime::parse_from_rfc2822(date_str)
.map(|dt| dt.with_timezone(&Utc))
.map(|x| x.timestamp())
.ok()
})
})
}
pub fn requires_auth(&self) -> bool {
self.status_code == 401
}
pub fn requires_basic_auth(&self) -> bool {
self.requires_auth()
&& self
.response_headers
.get(WWW_AUTHENTICATE)
.and_then(|val| val.to_str().ok())
.map(|s| s.to_ascii_lowercase().contains("basic"))
.unwrap_or(false)
}
pub fn is_partial(&self) -> bool {
self.status_code == 206
}
pub fn is_resumable(&self) -> bool {
self.accepts_ranges()
|| (self.is_partial() && self.content_range().is_some_and(|x| x.total.is_some()))
}
pub fn extract_hashes(&self) -> Vec<HashDigest> {
let mut hashes = Vec::new();
if let Some(val) = self.response_headers.get("Digest")
&& let Ok(s) = val.to_str()
{
for part in s.split(',') {
let part = part.trim();
if let Some((alg, value)) = part.split_once('=') {
let value = value.trim();
match alg.trim().to_ascii_lowercase().as_str() {
"sha-512" => {
hashes.push(HashDigest::SHA512(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-384" => {
hashes.push(HashDigest::SHA384(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-256" => {
hashes.push(HashDigest::SHA256(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha" | "sha1" | "sha-1" => hashes.push(HashDigest::SHA1(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
"md5" => hashes.push(HashDigest::MD5(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
_ => {}
}
}
}
}
for (name, value) in self.response_headers.iter() {
let name_str = name.as_str().to_ascii_lowercase();
if let Some(suffix) = name_str.strip_prefix("x-checksum-")
&& let Ok(val_str) = value.to_str()
{
let val_trimmed = val_str.trim();
fn is_hex(s: &str) -> bool {
s.len().is_multiple_of(2) && s.chars().all(|c| c.is_ascii_hexdigit())
}
let encoding = if is_hex(val_trimmed) {
crate::hash::HashEncoding::Hex
} else {
crate::hash::HashEncoding::Base64
};
match suffix {
"sha512" => {
hashes.push(HashDigest::SHA512(val_trimmed.to_string(), encoding));
return hashes;
}
"sha384" => {
hashes.push(HashDigest::SHA384(val_trimmed.to_string(), encoding));
return hashes;
}
"sha256" => {
hashes.push(HashDigest::SHA256(val_trimmed.to_string(), encoding));
return hashes;
}
"sha" | "sha1" | "sha-1" => {
hashes.push(HashDigest::SHA1(val_trimmed.to_string(), encoding))
}
"md5" => hashes.push(HashDigest::MD5(val_trimmed.to_string(), encoding)),
_ => {}
}
}
}
if let Some(val) = self.response_headers.get("Content-SHA256")
&& let Ok(s) = val.to_str()
{
hashes.push(HashDigest::SHA256(
s.trim().to_string(),
crate::hash::HashEncoding::Hex,
));
return hashes;
}
if let Some(val) = self.response_headers.get("Content-MD5")
&& let Ok(s) = val.to_str()
{
hashes.push(HashDigest::MD5(
s.trim().to_string(),
crate::hash::HashEncoding::Base64,
));
}
if let Some(val) = self.response_headers.get("Content-Digest") {
if let Ok(s) = val.to_str() {
for part in s.split(',') {
let part = part.trim();
if let Some((alg, value)) = part.split_once('=') {
let value = value.trim();
let value = value.trim_matches(':');
match alg.trim().to_ascii_lowercase().as_str() {
"sha-512" => {
hashes.push(HashDigest::SHA512(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-384" => {
hashes.push(HashDigest::SHA384(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-256" => {
hashes.push(HashDigest::SHA256(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha" | "sha1" | "sha-1" => hashes.push(HashDigest::SHA1(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
"md5" => hashes.push(HashDigest::MD5(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
_ => {}
}
}
}
}
} else if let Some(val) = self.response_headers.get("Repr-Digest")
&& let Ok(s) = val.to_str()
{
for part in s.split(',') {
let part = part.trim();
if let Some((alg, value)) = part.split_once('=') {
let value = value.trim();
let value = value.trim_matches(':');
match alg.trim().to_ascii_lowercase().as_str() {
"sha-512" => {
hashes.push(HashDigest::SHA512(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-384" => {
hashes.push(HashDigest::SHA384(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha-256" => {
hashes.push(HashDigest::SHA256(
value.to_string(),
crate::hash::HashEncoding::Base64,
));
return hashes;
}
"sha" | "sha1" | "sha-1" => hashes.push(HashDigest::SHA1(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
"md5" => hashes.push(HashDigest::MD5(
value.to_string(),
crate::hash::HashEncoding::Base64,
)),
_ => {}
}
}
}
}
hashes
}
}
impl From<Response> for ResponseInfo {
fn from(value: Response) -> Self {
Self {
status_code: value.status().as_u16(),
request_url: value.url().to_owned(),
response_headers: value.headers().to_owned(),
}
}
}
static CONTENT_RANGE_RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^bytes (\d+)-(\d+)/(\d+|\*)$").unwrap());
pub struct ContentRange {
pub start: u64,
pub end: u64,
pub total: Option<u64>,
}
#[cfg(test)]
mod tests {
use chrono::Datelike;
use reqwest::header::HeaderValue;
use super::*;
fn make_response_info_with_headers(url: &str, headers: HeaderMap) -> ResponseInfo {
ResponseInfo::new(200, Url::parse(url).unwrap(), headers)
}
#[test]
fn test_accepts_ranges() {
let mut headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert!(!resp.accepts_ranges());
headers.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert!(resp.accepts_ranges());
headers.insert(ACCEPT_RANGES, HeaderValue::from_static("none"));
let resp = make_response_info_with_headers("http://example.com", headers);
assert!(!resp.accepts_ranges());
}
#[test]
fn test_content_length() {
let mut headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.content_length(), None);
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("12345"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.content_length(), Some(12345));
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("notanumber"));
let resp = make_response_info_with_headers("http://example.com", headers);
assert_eq!(resp.content_length(), None);
}
#[test]
fn test_extract_filename_content_disposition() {
let mut headers = HeaderMap::new();
headers.insert(
CONTENT_DISPOSITION,
HeaderValue::from_static("attachment; filename=\"example.txt\""),
);
let resp =
make_response_info_with_headers("http://example.com/path/to/file", headers.clone());
assert_eq!(resp.extract_filename(), "example.txt");
headers.insert(
CONTENT_DISPOSITION,
HeaderValue::from_static("attachment; filename*=UTF-8''%E2%82%AC%20rates"),
);
let resp = make_response_info_with_headers("http://example.com/path/to/file", headers);
assert_eq!(resp.extract_filename(), "€ rates");
}
#[test]
fn test_extract_filename_from_url() {
let headers = HeaderMap::new();
let resp =
make_response_info_with_headers("http://example.com/path/to/file.txt", headers.clone());
assert_eq!(resp.extract_filename(), "file.txt");
let resp = make_response_info_with_headers("http://example.com/", headers.clone());
assert_eq!(resp.extract_filename(), "download");
let resp = make_response_info_with_headers("http://example.com/path/to/file.txt/", headers);
assert_eq!(resp.extract_filename(), "file.txt");
}
#[test]
fn test_extract_filename_appends_extension_from_mime() {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/pdf"));
let resp =
make_response_info_with_headers("http://example.com/path/to/report", headers.clone());
assert_eq!(resp.extract_filename(), "report.pdf");
let resp = make_response_info_with_headers(
"http://example.com/path/to/report.pdf",
headers.clone(),
);
assert_eq!(resp.extract_filename(), "report.pdf");
let mut bad = HeaderMap::new();
bad.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/x-not-a-real-type"),
);
let resp = make_response_info_with_headers("http://example.com/path/to/report", bad);
assert_eq!(resp.extract_filename(), "report");
}
#[test]
fn test_extract_filename_download_with_mime() {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/zip"));
let resp = make_response_info_with_headers("http://example.com/", headers);
assert_eq!(resp.extract_filename(), "download.zip");
}
#[test]
fn test_extract_filename_from_query_params() {
let headers = HeaderMap::new();
let resp = make_response_info_with_headers(
"http://example.com/dl?file=archive.tar.gz",
headers.clone(),
);
assert_eq!(resp.extract_filename(), "archive.tar.gz");
let resp = make_response_info_with_headers(
"http://example.com/dl?filename=image.png",
headers.clone(),
);
assert_eq!(resp.extract_filename(), "image.png");
let resp = make_response_info_with_headers(
"http://example.com/path/segment.bin?name=real.zip",
headers.clone(),
);
assert_eq!(resp.extract_filename(), "real.zip");
let resp = make_response_info_with_headers(
"http://example.com/path/seg.bin?file=",
headers.clone(),
);
assert_eq!(resp.extract_filename(), "seg.bin");
let mut h = HeaderMap::new();
h.insert(CONTENT_TYPE, HeaderValue::from_static("application/pdf"));
let resp = make_response_info_with_headers("http://example.com/dl?name=doc", h);
assert_eq!(resp.extract_filename(), "doc.pdf");
}
#[test]
fn test_parse_last_modified() {
let mut headers = HeaderMap::new();
headers.insert(
LAST_MODIFIED,
HeaderValue::from_static("Wed, 21 Oct 2015 07:28:00 GMT"),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let dt = resp.parse_last_modified();
assert!(dt.is_some());
let dt = dt.unwrap();
let dt = DateTime::from_timestamp(dt, 0).unwrap();
assert_eq!(dt.year(), 2015);
assert_eq!(dt.month(), 10);
assert_eq!(dt.day(), 21);
headers.insert(LAST_MODIFIED, HeaderValue::from_static("invalid-date"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert!(resp.parse_last_modified().is_none());
let headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers);
assert!(resp.parse_last_modified().is_none());
}
#[test]
fn test_content_range() {
let mut headers = HeaderMap::new();
headers.insert(CONTENT_RANGE, HeaderValue::from_static("bytes 0-499/1234"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let cr = resp.content_range();
assert!(cr.is_some());
let cr = cr.unwrap();
assert_eq!(cr.start, 0);
assert_eq!(cr.end, 499);
assert_eq!(cr.total, Some(1234));
headers.insert(CONTENT_RANGE, HeaderValue::from_static("bytes 0-499/*"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let cr = resp.content_range();
assert!(cr.is_some());
let cr = cr.unwrap();
assert_eq!(cr.start, 0);
assert_eq!(cr.end, 499);
assert_eq!(cr.total, None);
headers.insert(CONTENT_RANGE, HeaderValue::from_static("invalid"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert!(resp.content_range().is_none());
let headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers);
assert!(resp.content_range().is_none());
}
#[test]
fn test_etag() {
let mut headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.etag(), None);
headers.insert(ETAG, HeaderValue::from_static("\"abc123\""));
let resp = make_response_info_with_headers("http://example.com", headers);
assert_eq!(resp.etag(), Some("\"abc123\"".to_string()));
}
#[test]
fn test_is_successful() {
let headers = HeaderMap::new();
let resp = ResponseInfo::new(
200,
Url::parse("http://example.com").unwrap(),
headers.clone(),
);
assert!(resp.is_successful());
let resp = ResponseInfo::new(
299,
Url::parse("http://example.com").unwrap(),
headers.clone(),
);
assert!(resp.is_successful());
let resp = ResponseInfo::new(
199,
Url::parse("http://example.com").unwrap(),
headers.clone(),
);
assert!(!resp.is_successful());
let resp = ResponseInfo::new(300, Url::parse("http://example.com").unwrap(), headers);
assert!(!resp.is_successful());
}
#[test]
fn test_total_length() {
let mut headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), None);
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("1234"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), Some(1234));
headers.insert(CONTENT_RANGE, HeaderValue::from_static("bytes 0-499/2000"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), Some(2000));
headers.insert(CONTENT_RANGE, HeaderValue::from_static("bytes 0-499/*"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), Some(1234));
headers.insert(CONTENT_RANGE, HeaderValue::from_static("invalid"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), Some(1234));
headers.insert(CONTENT_LENGTH, HeaderValue::from_static("notanumber"));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
assert_eq!(resp.total_length(), None);
headers.insert(CONTENT_RANGE, HeaderValue::from_static("bytes 0-499/3000"));
let resp = make_response_info_with_headers("http://example.com", headers);
assert_eq!(resp.total_length(), Some(3000));
}
#[test]
fn test_extract_hashes_digest_header() {
let mut headers = HeaderMap::new();
headers.insert("Digest", HeaderValue::from_static("md5=abc123=="));
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::MD5(val, enc) => {
assert_eq!(val, "abc123==");
assert_eq!(*enc, crate::hash::HashEncoding::Base64);
}
_ => panic!("Expected MD5"),
}
headers.insert(
"Digest",
HeaderValue::from_static("sha-256=sha256val, md5=md5val"),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA256(val, enc) => {
assert_eq!(val, "sha256val");
assert_eq!(*enc, crate::hash::HashEncoding::Base64);
}
_ => panic!("Expected SHA256"),
}
headers.insert(
"Digest",
HeaderValue::from_static("sha-512=sha512val, sha-256=sha256val"),
);
let resp = make_response_info_with_headers("http://example.com", headers);
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA512(val, enc) => {
assert_eq!(val, "sha512val");
assert_eq!(*enc, crate::hash::HashEncoding::Base64);
}
_ => panic!("Expected SHA512"),
}
}
#[test]
fn test_extract_hashes_x_checksum_headers() {
let mut headers = HeaderMap::new();
headers.insert(
"x-checksum-sha256",
HeaderValue::from_static("abcdef1234567890"),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA256(val, enc) => {
assert_eq!(val, "abcdef1234567890");
assert_eq!(*enc, crate::hash::HashEncoding::Hex);
}
_ => panic!("Expected SHA256"),
}
headers.clear();
headers.insert(
"x-checksum-sha1",
HeaderValue::from_static("nothexbase64=="),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert!(
hashes
.iter()
.any(|h| matches!(h, HashDigest::SHA1(_, crate::hash::HashEncoding::Base64)))
);
headers.insert(
"x-checksum-md5",
HeaderValue::from_static("abcdefabcdefabcdefabcdefabcdefab"),
);
let resp = make_response_info_with_headers("http://example.com", headers);
let hashes = resp.extract_hashes();
assert!(hashes.iter().any(|h| matches!(h, HashDigest::MD5(_, _))));
}
#[test]
fn test_extract_hashes_content_sha256_and_md5() {
let mut headers = HeaderMap::new();
headers.insert(
"Content-SHA256",
HeaderValue::from_static("abcdefabcdefabcdefabcdefabcdefab"),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA256(val, enc) => {
assert_eq!(val, "abcdefabcdefabcdefabcdefabcdefab");
assert_eq!(*enc, crate::hash::HashEncoding::Hex);
}
_ => panic!("Expected SHA256"),
}
headers.clear();
headers.insert(
"Content-MD5",
HeaderValue::from_static("YWJjZGVmMTIzNDU2Nzg5MA=="),
);
let resp = make_response_info_with_headers("http://example.com", headers);
let hashes = resp.extract_hashes();
assert!(hashes.iter().any(|h| matches!(h, HashDigest::MD5(val, crate::hash::HashEncoding::Base64) if val == "YWJjZGVmMTIzNDU2Nzg5MA==")));
}
#[test]
fn test_extract_hashes_repr_digest() {
let mut headers = HeaderMap::new();
headers.insert(
"Repr-Digest",
HeaderValue::from_static("sha-256=:base64sha256:"),
);
let resp = make_response_info_with_headers("http://example.com", headers.clone());
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA256(val, enc) => {
assert_eq!(val, "base64sha256");
assert_eq!(*enc, crate::hash::HashEncoding::Base64);
}
_ => panic!("Expected SHA256"),
}
headers.insert(
"Repr-Digest",
HeaderValue::from_static("sha-512=:base64sha512:, md5=:md5val:"),
);
let resp = make_response_info_with_headers("http://example.com", headers);
let hashes = resp.extract_hashes();
assert_eq!(hashes.len(), 1);
match &hashes[0] {
HashDigest::SHA512(val, enc) => {
assert_eq!(val, "base64sha512");
assert_eq!(*enc, crate::hash::HashEncoding::Base64);
}
_ => panic!("Expected SHA512"),
}
}
#[test]
fn test_extract_hashes_none() {
let headers = HeaderMap::new();
let resp = make_response_info_with_headers("http://example.com", headers);
let hashes = resp.extract_hashes();
assert!(hashes.is_empty());
}
}