use std::collections::HashMap;
use crate::error::KnafehError;
use crate::rpc::message::Metadata;
pub const RPC_HEADER_PREFIX: &str = "x-rpc-";
pub const RPC_STATUS_HEADER: &str = "x-rpc-status";
pub const RPC_STATUS_MESSAGE_HEADER: &str = "x-rpc-status-message";
pub const RPC_METHOD_KIND_HEADER: &str = "x-rpc-method-kind";
pub type RawHeaders = Vec<(Vec<u8>, Vec<u8>)>;
pub fn validate_metadata_key(key: &str) -> Result<(), KnafehError> {
if key.is_empty() {
return Err(KnafehError::InvalidMessage(
"metadata key cannot be empty".to_string(),
));
}
if key.starts_with(':') {
return Err(KnafehError::InvalidMessage(format!(
"metadata key '{key}' cannot be an HTTP pseudo-header"
)));
}
if is_reserved_metadata_key(key) {
return Err(KnafehError::InvalidMessage(format!(
"metadata key '{key}' is reserved"
)));
}
if !key.bytes().all(is_http_token_byte) {
return Err(KnafehError::InvalidMessage(format!(
"metadata key '{key}' contains invalid HTTP header characters"
)));
}
Ok(())
}
fn is_reserved_metadata_key(key: &str) -> bool {
matches!(
key,
RPC_STATUS_HEADER
| RPC_STATUS_MESSAGE_HEADER
| RPC_METHOD_KIND_HEADER
| "status"
| "status-message"
| "method-kind"
)
}
fn is_http_token_byte(byte: u8) -> bool {
matches!(
byte,
b'a'..=b'z'
| b'A'..=b'Z'
| b'0'..=b'9'
| b'!'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'.'
| b'^'
| b'_'
| b'`'
| b'|'
| b'~'
)
}
pub fn metadata_to_headers(metadata: &Metadata) -> Result<Vec<(String, String)>, KnafehError> {
metadata
.iter()
.map(|(k, v)| {
validate_metadata_key(k)?;
if k.starts_with("x-rpc-") {
Ok((k.clone(), v.clone()))
} else {
Ok((format!("{RPC_HEADER_PREFIX}{k}"), v.clone()))
}
})
.collect()
}
pub fn headers_to_metadata(headers: &[(String, String)]) -> Metadata {
let mut metadata = HashMap::new();
for (key, value) in headers {
if key == RPC_STATUS_HEADER || key == RPC_STATUS_MESSAGE_HEADER {
metadata.insert(key.clone(), value.clone());
} else if let Some(stripped) = key.strip_prefix(RPC_HEADER_PREFIX) {
metadata.insert(stripped.to_string(), value.clone());
}
}
metadata
}
pub fn build_request_headers(
method_path: &str,
content_type: &str,
metadata: &Metadata,
method_kind: &str,
) -> Result<RawHeaders, KnafehError> {
let mut headers = vec![
(b":method".to_vec(), b"POST".to_vec()),
(b":path".to_vec(), format!("/{method_path}").into_bytes()),
(b":scheme".to_vec(), b"https".to_vec()),
(b"content-type".to_vec(), content_type.as_bytes().to_vec()),
(
RPC_METHOD_KIND_HEADER.as_bytes().to_vec(),
method_kind.as_bytes().to_vec(),
),
];
for (key, value) in metadata {
validate_metadata_key(key)?;
let header_key = if key.starts_with("x-rpc-") {
key.clone()
} else {
format!("{RPC_HEADER_PREFIX}{key}")
};
headers.push((header_key.into_bytes(), value.clone().into_bytes()));
}
Ok(headers)
}
pub fn build_response_headers(
status_code: u8,
status_message: &str,
content_type: &str,
metadata: &Metadata,
) -> Result<RawHeaders, KnafehError> {
let mut headers = vec![
(b":status".to_vec(), b"200".to_vec()),
(b"content-type".to_vec(), content_type.as_bytes().to_vec()),
(
RPC_STATUS_HEADER.as_bytes().to_vec(),
status_code.to_string().into_bytes(),
),
(
RPC_STATUS_MESSAGE_HEADER.as_bytes().to_vec(),
status_message.as_bytes().to_vec(),
),
];
for (key, value) in metadata {
validate_metadata_key(key)?;
let header_key = if key.starts_with("x-rpc-") {
key.clone()
} else {
format!("{RPC_HEADER_PREFIX}{key}")
};
headers.push((header_key.into_bytes(), value.clone().into_bytes()));
}
Ok(headers)
}
pub fn extract_method_path(path: &[u8]) -> Result<String, KnafehError> {
let path_str = std::str::from_utf8(path)
.map_err(|e| KnafehError::InvalidMessage(format!("invalid UTF-8 in :path: {e}")))?;
let trimmed = path_str.strip_prefix('/').unwrap_or(path_str);
if !trimmed.contains('/') {
return Err(KnafehError::InvalidMessage(format!(
"invalid RPC path (expected 'service/method'): {trimmed}"
)));
}
Ok(trimmed.to_string())
}