use crate::oss::Client;
use crate::oss::Error;
use crate::oss::sign_v4::{HTTPVerb, SignV4Param};
use base64::{Engine, engine::general_purpose};
use md5::{Digest, Md5};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::Path;
use tokio::io::AsyncReadExt;
use u_sdk_common::helper::gmt_format;
use url::Url;
pub fn get_content_md5(bytes: &[u8]) -> String {
use md5::{Digest, Md5};
let mut hasher = Md5::new();
hasher.update(bytes);
let res = hasher.finalize();
general_purpose::STANDARD.encode(res)
}
#[test]
fn get_content_md5_test() {
let s = get_content_md5(b"0123456789");
assert_eq!(&s, "eB5eJF1ptWaXm4bijSPyxw==")
}
pub(crate) fn into_request_header(map: HashMap<&str, &str>) -> HeaderMap {
map.into_iter()
.map(|(k, v)| {
let name = HeaderName::from_bytes(k.as_bytes()).unwrap();
let value = HeaderValue::from_bytes(v.as_bytes()).unwrap();
(name, value)
})
.collect()
}
pub(crate) async fn into_request_failed_error(resp: reqwest::Response) -> Error {
let status = resp.status();
let body = resp.text().await;
match body {
Ok(text) => Error::RequestAPIFailed {
status: status.to_string(),
text,
},
Err(e) => Error::Reqwest(e),
}
}
pub(crate) async fn parse_xml_response<T: serde::de::DeserializeOwned>(
resp: reqwest::Response,
) -> Result<T, Error> {
let status = resp.status();
if !status.is_success() {
return Err(into_request_failed_error(resp).await);
}
let text = resp.text().await?;
let data = quick_xml::de::from_str(&text)
.map_err(|e| Error::Common(format!("XML parse error: {}", e)))?;
Ok(data)
}
pub(crate) async fn compute_md5_from_file(path: &Path) -> Result<String, Error> {
let mut file = tokio::fs::File::open(path).await?;
let mut hasher = Md5::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = file.read(&mut buf).await?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
let result = hasher.finalize();
Ok(general_purpose::STANDARD.encode(result))
}
pub(crate) fn validate_object_name(name: &str) -> Result<(), Error> {
let len = name.as_bytes().len();
if len == 0 {
return Err(Error::Common("object_name cannot be empty".to_owned()));
}
if len > 1023 {
return Err(Error::Common(
"object_name is too long, max is 1023 bytes".to_owned(),
));
}
if let Some(first) = name.chars().next() {
if first == '/' || first == '\\' {
return Err(Error::Common(
"object_name cannot start with '/' or '\\'".to_owned(),
));
}
}
if name.bytes().any(|b| b == b'\r' || b == b'\n') {
return Err(Error::Common(
"object_name cannot contain control characters".to_owned(),
));
}
let segments: Vec<&str> = name.split('/').collect();
let to_check = if name.ends_with('/') {
&segments[..segments.len().saturating_sub(1)]
} else {
&segments[..]
};
if to_check.iter().any(|seg| seg.is_empty()) {
return Err(Error::Common(
"object_name cannot contain empty path segments".to_owned(),
));
}
for segment in to_check {
if *segment == "." || *segment == ".." {
return Err(Error::Common(
"object_name cannot contain relative path segments '.' or '..'".to_owned(),
));
}
}
Ok(())
}
#[test]
fn validate_object_name_test() {
assert!(validate_object_name("exampleobject.txt").is_ok());
assert!(validate_object_name("dir/subdir/file_测试-01.log").is_ok());
assert!(validate_object_name("a/b").is_ok());
assert!(validate_object_name("a/b/").is_ok());
assert!(validate_object_name("").is_err());
assert!(validate_object_name(&"a".repeat(1024)).is_err());
assert!(validate_object_name("/badname").is_err());
assert!(validate_object_name("\\badname").is_err());
assert!(validate_object_name("bad\r\nname").is_err());
assert!(validate_object_name("a//abc").is_err());
assert!(validate_object_name("x/y//z").is_err());
assert!(validate_object_name("./abc").is_err());
assert!(validate_object_name("../abc").is_err());
assert!(validate_object_name("abc/./def").is_err());
assert!(validate_object_name("abc/../def").is_err());
}
pub(crate) fn get_request_header_with_bucket_region(
client: &Client,
req_header_map: HashMap<String, String>,
request_url: &Url,
http_verb: HTTPVerb,
signing_region: &str,
bucket: Option<&str>,
) -> HeaderMap {
let (sign_map, remaining_map) = partition_header(req_header_map);
let mut canonical_header = BTreeMap::new();
canonical_header.extend(sign_map.iter().map(|(k, v)| (k.as_str(), v.as_str())));
canonical_header.insert("x-oss-content-sha256", "UNSIGNED-PAYLOAD");
canonical_header.insert("host", request_url.host_str().unwrap());
let mut additional_header = BTreeSet::new();
additional_header.insert("host");
let now = time::OffsetDateTime::now_utc();
let sign_v4_param = SignV4Param {
signing_region,
http_verb,
uri: request_url,
bucket,
header_map: &canonical_header,
additional_header: Some(&additional_header),
date_time: &now,
};
let authorization = client.sign_v4(sign_v4_param);
let mut header = canonical_header.into_iter().collect::<HashMap<_, _>>();
header.insert("Authorization", &authorization);
let gmt = gmt_format(&now);
header.insert("Date", &gmt);
header.extend(remaining_map.iter().map(|(k, v)| (k.as_str(), v.as_str())));
into_request_header(header)
}
pub(crate) fn get_request_header(
client: &Client,
req_header_map: HashMap<String, String>,
request_url: &Url,
http_verb: HTTPVerb,
) -> HeaderMap {
get_request_header_with_bucket_region(
client,
req_header_map,
request_url,
http_verb,
&client.region,
Some(&client.bucket),
)
}
fn partition_header(
header_map: HashMap<String, String>,
) -> (HashMap<String, String>, HashMap<String, String>) {
let mut sign_map = HashMap::new();
let mut remaining_map = HashMap::new();
for (k, v) in header_map {
let k = k.to_lowercase();
if k == "content-type" || k == "content-md5" || k.starts_with("x-oss-") {
sign_map.insert(k, v);
} else {
remaining_map.insert(k, v);
}
}
(sign_map, remaining_map)
}