use std::fmt;
use url::Url;
use crate::Method;
use super::util::percent_encode;
const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub fn canonical_request<'a, Q, H, S>(
method: Method,
url: &Url,
query_string: Q,
headers: H,
signed_headers: S,
) -> String
where
Q: Iterator<Item = (&'a str, &'a str)>,
H: Iterator<Item = (&'a str, &'a str)>,
S: Iterator<Item = &'a str>,
{
let mut string = String::with_capacity(64);
string.push_str(method.to_str());
string.push('\n');
let path = parse_path(url);
string.push_str(&path);
string.push('\n');
canonical_query_string(query_string, &mut string).unwrap();
string.push('\n');
canonical_headers(headers, &mut string).unwrap();
string.push('\n');
canonical_signed_headers(signed_headers, &mut string).unwrap();
string.push('\n');
string.push_str(UNSIGNED_PAYLOAD);
string
}
fn parse_path(url: &Url) -> String {
let host_str = url
.host_str()
.unwrap_or_default();
let first_subdomain = host_str
.split('.')
.next()
.unwrap_or_default();
let standard_path = url.path();
let bucket_object_path = format!("/{}/{}", first_subdomain, standard_path.trim_start_matches('/'));
bucket_object_path
}
fn canonical_query_string<'a, Q>(query_string: Q, mut out: impl fmt::Write) -> fmt::Result
where
Q: Iterator<Item = (&'a str, &'a str)>,
{
let mut first = true;
for (key, val) in query_string {
if !first {
out.write_char('&')?;
}
first = false;
write!(out, "{}={}", percent_encode(key), percent_encode(val))?;
}
Ok(())
}
fn canonical_headers<'a, H>(headers: H, mut out: impl fmt::Write) -> fmt::Result
where
H: Iterator<Item = (&'a str, &'a str)>,
{
for (key, val) in headers {
out.write_str(key)?;
out.write_char(':')?;
out.write_str(val.trim())?;
out.write_char('\n')?;
}
Ok(())
}
fn canonical_signed_headers<'a, H>(signed_headers: H, mut out: impl fmt::Write) -> fmt::Result
where
H: Iterator<Item = &'a str>,
{
let mut first = true;
for key in signed_headers {
if first {
first = false;
} else {
out.write_char(';')?;
}
out.write_str(key)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::Method;
#[test]
fn oss_example() {
let method = Method::Get;
let url = "https://examplebucket.oss-cn-hangzhou.aliyuncs.com"
.parse()
.unwrap();
let expected = concat!(
"GET\n",
"/examplebucket/\n",
"list-type=2&x-oss-additional-headers=host&x-oss-credential=access_key_id%2F20250206%2Fcn-hangzhou%2Foss%2Faliyun_v4_request&x-oss-date=20250206T165151Z&x-oss-expires=86400&x-oss-signature-version=OSS4-HMAC-SHA256\n",
"host:examplebucket.oss-cn-hangzhou.aliyuncs.com\n",
"\n",
"host\n",
"UNSIGNED-PAYLOAD",
);
let got = canonical_request(
method,
&url,
vec![
("list-type", "2"),
("x-oss-additional-headers", "host"),
("x-oss-credential", "access_key_id/20250206/cn-hangzhou/oss/aliyun_v4_request"),
("x-oss-date", "20250206T165151Z"),
("x-oss-expires", "86400"),
("x-oss-signature-version", "OSS4-HMAC-SHA256"),
].into_iter(),
vec![
("host", "examplebucket.oss-cn-hangzhou.aliyuncs.com"),
].into_iter(),
vec!["host"].into_iter(),
);
assert_eq!(got, expected);
}
}