mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 23:47:28 +00:00
perf(signer): reduce String allocations in V4 signing path (#3197)
* perf(signer): reduce String allocations in V4 signing path - v4_ignored_headers: HashMap<String, bool> -> HashSet<&'static str> - Header lookups: k.to_string() -> k.as_str() (zero-copy) - Query params: Vec<(String, String)> -> Vec<(&str, &str)> (zero-copy) - Canonical request: single String with push_str instead of 6 intermediate Strings + join - 20 signer tests pass * fix(signer): align ignored header set type --------- Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -16,7 +16,7 @@ use bytes::BytesMut;
|
|||||||
use http::HeaderMap;
|
use http::HeaderMap;
|
||||||
use http::Uri;
|
use http::Uri;
|
||||||
use http::request;
|
use http::request;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use time::{OffsetDateTime, macros::format_description};
|
use time::{OffsetDateTime, macros::format_description};
|
||||||
@@ -63,12 +63,12 @@ struct SignFailure {
|
|||||||
type SignOutcome = std::result::Result<request::Request<Body>, Box<SignFailure>>;
|
type SignOutcome = std::result::Result<request::Request<Body>, Box<SignFailure>>;
|
||||||
|
|
||||||
#[allow(non_upper_case_globals)] // FIXME
|
#[allow(non_upper_case_globals)] // FIXME
|
||||||
static v4_ignored_headers: LazyLock<HashMap<String, bool>> = LazyLock::new(|| {
|
static v4_ignored_headers: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
|
||||||
let mut m = <HashMap<String, bool>>::new();
|
let mut s = HashSet::new();
|
||||||
m.insert("accept-encoding".to_string(), true);
|
s.insert("accept-encoding");
|
||||||
m.insert("authorization".to_string(), true);
|
s.insert("authorization");
|
||||||
m.insert("user-agent".to_string(), true);
|
s.insert("user-agent");
|
||||||
m
|
s
|
||||||
});
|
});
|
||||||
|
|
||||||
fn fail(request: request::Request<Body>, error: SignV4Error) -> SignOutcome {
|
fn fail(request: request::Request<Body>, error: SignV4Error) -> SignOutcome {
|
||||||
@@ -136,11 +136,11 @@ fn try_get_hashed_payload(req: &request::Request<Body>) -> SignResult<String> {
|
|||||||
Ok(hashed_payload.to_string())
|
Ok(hashed_payload.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_get_canonical_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> SignResult<String> {
|
fn try_get_canonical_headers(req: &request::Request<Body>, ignored_headers: &HashSet<&'static str>) -> SignResult<String> {
|
||||||
let mut headers = Vec::<String>::new();
|
let mut headers = Vec::<String>::new();
|
||||||
let mut vals = HashMap::<String, Vec<String>>::new();
|
let mut vals = HashMap::<String, Vec<String>>::new();
|
||||||
for k in req.headers().keys() {
|
for k in req.headers().keys() {
|
||||||
if ignored_headers.get(&k.to_string()).is_some() {
|
if ignored_headers.contains(k.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
headers.push(k.as_str().to_lowercase());
|
headers.push(k.as_str().to_lowercase());
|
||||||
@@ -210,12 +210,12 @@ fn header_exists(key: &str, headers: &[String]) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashMap<String, bool>) -> String {
|
fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashSet<&'static str>) -> String {
|
||||||
let mut headers = Vec::<String>::new();
|
let mut headers = Vec::<String>::new();
|
||||||
let headers_ref = req.headers();
|
let headers_ref = req.headers();
|
||||||
debug!("get_signed_headers headers: {:?}", headers_ref);
|
debug!("get_signed_headers headers: {:?}", headers_ref);
|
||||||
for (k, _) in headers_ref {
|
for (k, _) in headers_ref {
|
||||||
if ignored_headers.get(&k.to_string()).is_some() {
|
if ignored_headers.contains(k.as_str()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
headers.push(k.as_str().to_lowercase());
|
headers.push(k.as_str().to_lowercase());
|
||||||
@@ -229,43 +229,52 @@ fn get_signed_headers(req: &request::Request<Body>, ignored_headers: &HashMap<St
|
|||||||
|
|
||||||
fn try_get_canonical_request(
|
fn try_get_canonical_request(
|
||||||
req: &request::Request<Body>,
|
req: &request::Request<Body>,
|
||||||
ignored_headers: &HashMap<String, bool>,
|
ignored_headers: &HashSet<&'static str>,
|
||||||
hashed_payload: &str,
|
hashed_payload: &str,
|
||||||
) -> SignResult<String> {
|
) -> SignResult<String> {
|
||||||
let mut canonical_query_string = "".to_string();
|
let mut canonical_query_string = String::new();
|
||||||
if let Some(q) = req.uri().query() {
|
if let Some(q) = req.uri().query() {
|
||||||
// Parse query string into key-value pairs
|
// Parse query string into key-value pairs (zero-copy slices)
|
||||||
let mut query_params: Vec<(String, String)> = Vec::new();
|
let mut query_params: Vec<(&str, &str)> = Vec::new();
|
||||||
if !q.is_empty() {
|
if !q.is_empty() {
|
||||||
for param in q.split('&') {
|
for param in q.split('&') {
|
||||||
if let Some((key, value)) = param.split_once('=') {
|
if let Some((key, value)) = param.split_once('=') {
|
||||||
query_params.push((key.to_string(), value.to_string()));
|
query_params.push((key, value));
|
||||||
} else {
|
} else {
|
||||||
query_params.push((param.to_string(), "".to_string()));
|
query_params.push((param, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by key name
|
// Sort by key name
|
||||||
query_params.sort_by(|a, b| a.0.cmp(&b.0));
|
query_params.sort_by(|a, b| a.0.cmp(b.0));
|
||||||
|
|
||||||
// Build canonical query string
|
// Build canonical query string with push_str (no intermediate allocations)
|
||||||
//println!("query_params: {query_params:?}");
|
for (i, (k, v)) in query_params.iter().enumerate() {
|
||||||
let sorted_params: Vec<String> = query_params.iter().map(|(k, v)| format!("{k}={v}")).collect();
|
if i > 0 {
|
||||||
|
canonical_query_string.push('&');
|
||||||
canonical_query_string = sorted_params.join("&");
|
}
|
||||||
|
canonical_query_string.push_str(k);
|
||||||
|
canonical_query_string.push('=');
|
||||||
|
canonical_query_string.push_str(v);
|
||||||
|
}
|
||||||
canonical_query_string = canonical_query_string.replace("+", "%20");
|
canonical_query_string = canonical_query_string.replace("+", "%20");
|
||||||
}
|
}
|
||||||
|
|
||||||
let canonical_request = [
|
// Build canonical request with a single allocation
|
||||||
req.method().to_string(),
|
let mut canonical_request = String::with_capacity(256);
|
||||||
req.uri().path().to_string(),
|
canonical_request.push_str(req.method().as_str());
|
||||||
canonical_query_string,
|
canonical_request.push('\n');
|
||||||
try_get_canonical_headers(req, ignored_headers)?,
|
canonical_request.push_str(req.uri().path());
|
||||||
get_signed_headers(req, ignored_headers),
|
canonical_request.push('\n');
|
||||||
hashed_payload.to_string(),
|
canonical_request.push_str(&canonical_query_string);
|
||||||
];
|
canonical_request.push('\n');
|
||||||
Ok(canonical_request.join("\n"))
|
canonical_request.push_str(&try_get_canonical_headers(req, ignored_headers)?);
|
||||||
|
canonical_request.push('\n');
|
||||||
|
canonical_request.push_str(&get_signed_headers(req, ignored_headers));
|
||||||
|
canonical_request.push('\n');
|
||||||
|
canonical_request.push_str(hashed_payload);
|
||||||
|
Ok(canonical_request)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_get_string_to_sign_v4(
|
fn try_get_string_to_sign_v4(
|
||||||
|
|||||||
Reference in New Issue
Block a user