use crate::std::borrow::ToOwned;
use crate::std::vec::Vec;
use super::ExtensionValue;
use super::{ForwardedElement, NodeId};
use crate::forwarded::ForwardedProtocol;
use rama_core::error::BoxErrorExt as _;
use rama_core::error::{BoxError, ErrorContext, ErrorExt};
use rama_utils::macros::match_ignore_ascii_case_str;
pub(crate) fn parse_single_forwarded_element(bytes: &[u8]) -> Result<ForwardedElement, BoxError> {
if bytes.len() > 512 {
return Err(
BoxError::from_static_str("forwarded element bytes length is too big")
.context_field("length", bytes.len()),
);
}
let (element, bytes) = parse_next_forwarded_element(bytes)?;
if bytes.is_empty() {
Ok(element)
} else {
Err(
BoxError::from_static_str("trailing characters found following the forwarded element")
.context_field("count", bytes.len()),
)
}
}
pub(crate) fn parse_one_plus_forwarded_elements(
bytes: &[u8],
) -> Result<(ForwardedElement, Vec<ForwardedElement>), BoxError> {
if bytes.len() > 8096 {
return Err(
BoxError::from_static_str("forwarded elements list bytes length is too big")
.context_field("length", bytes.len()),
);
}
let mut others = Vec::new();
let (first, mut bytes) = parse_next_forwarded_element(bytes)?;
if bytes.is_empty() {
return Ok((first, others));
}
loop {
if bytes[0] != b',' {
return Err(BoxError::from_static_str(
"unexpected char in forward element list: expected comma",
)
.context_field("char", bytes[0]));
}
bytes = &bytes[1..];
let (next, b) = parse_next_forwarded_element(bytes)?;
others.push(next);
bytes = b;
if bytes.is_empty() {
return Ok((first, others));
}
}
}
fn parse_next_forwarded_element(mut bytes: &[u8]) -> Result<(ForwardedElement, &[u8]), BoxError> {
if bytes.is_empty() {
return Err(BoxError::from_static_str(
"empty str is not a valid Forwarded Element",
));
}
let mut el = ForwardedElement {
by_node: None,
for_node: None,
authority: None,
proto: None,
proto_version: None,
extensions: None,
};
loop {
let Some(separator_index) = bytes.iter().position(|b| *b == b'=') else {
bytes = trim_left(bytes);
break;
};
let key = parse_value(trim(&bytes[..separator_index]))?;
bytes = trim_left(&bytes[separator_index + 1..]);
let mut unescaped: Option<Vec<u8>> = None;
let value_slice: &[u8];
let quoted: bool;
if let Some(index) = bytes
.iter()
.position(|b| *b == b'"' || *b == b';' || *b == b',')
{
match bytes[index] {
b'"' => {
if index != 0 {
return Err(BoxError::from_static_str("dangling quote string quote")
.context_field("quote_pos", index));
}
bytes = &bytes[1..];
let scan = scan_quoted_string(bytes)?;
value_slice = &bytes[..scan.raw_len];
bytes = &bytes[scan.consumed..];
unescaped = scan.decoded;
quoted = true;
}
b';' | b',' => {
value_slice = &bytes[..index];
bytes = &bytes[index..];
quoted = false;
}
_ => {
#[expect(
clippy::unreachable,
reason = "the iter::position above only returns indices for `\"`, `;`, or `,`"
)]
{
unreachable!("we should only ever find a quote or semicolon at this point")
}
}
}
} else {
value_slice = bytes;
bytes = &bytes[bytes.len()..];
quoted = false;
}
let value_bytes: &[u8] = unescaped.as_deref().unwrap_or(value_slice);
let value = if quoted {
parse_quoted_value(value_bytes)?
} else {
parse_value(value_bytes)?
};
if !quoted && value.contains(['[', ']', ':']) {
return Err(BoxError::from_static_str(
"Forwarded Element pair's value was expected to be a quoted string due to the chars it contains"
).context_str_field("value", value));
}
match_ignore_ascii_case_str! {
match(key) {
"for" => if el.for_node.is_some() {
return Err(BoxError::from_static_str("Forwarded Element can only contain one 'for' property"));
} else {
el.for_node = Some(NodeId::try_from(value).context("parse Forwarded Element 'for' node")?);
},
"host" => if el.authority.is_some() {
return Err(BoxError::from_static_str("Forwarded Element can only contain one 'host' property"));
} else {
el.authority = Some(value.parse().context("parse Forwarded Element 'host' authority")?);
},
"by" => if el.by_node.is_some() {
return Err(BoxError::from_static_str("Forwarded Element can only contain one 'by' property"));
} else {
el.by_node = Some(NodeId::try_from(value).context("parse Forwarded Element 'by' node")?);
},
"proto" => if el.proto.is_some() {
return Err(BoxError::from_static_str("Forwarded Element can only contain one 'proto' property"));
} else {
el.proto = Some(ForwardedProtocol::try_from(value).context("parse Forwarded Element 'proto' protocol")?);
},
_ => {
el.extensions.get_or_insert_with(Default::default)
.insert(key.to_owned(), ExtensionValue{
value: value.to_owned(),
quoted,
});
}
}
}
bytes = trim_left(bytes);
if bytes.is_empty() || bytes[0] != b';' {
break;
}
bytes = &bytes[1..];
}
if el.for_node.is_none() && el.by_node.is_none() && el.authority.is_none() && el.proto.is_none()
{
return Err(BoxError::from_static_str(
"invalid forwarded element: none of required properties are set",
));
}
bytes = trim_left(bytes);
Ok((el, bytes))
}
struct QuotedScan {
consumed: usize,
raw_len: usize,
decoded: Option<Vec<u8>>,
}
fn scan_quoted_string(bytes: &[u8]) -> Result<QuotedScan, BoxError> {
let mut i = 0;
let mut decoded: Option<Vec<u8>> = None;
while i < bytes.len() {
match bytes[i] {
b'"' => {
return Ok(QuotedScan {
consumed: i + 1,
raw_len: i,
decoded,
});
}
b'\\' => {
if i + 1 >= bytes.len() {
break;
}
let buf = decoded.get_or_insert_with(|| bytes[..i].to_vec());
buf.push(bytes[i + 1]);
i += 2;
}
other => {
if let Some(buf) = decoded.as_mut() {
buf.push(other);
}
i += 1;
}
}
}
Err(BoxError::from_static_str(
"quote string missing trailer quote",
))
}
fn trim_left(b: &[u8]) -> &[u8] {
let mut offset = 0;
while offset < b.len() && matches!(b[offset], b' ' | b'\t') {
offset += 1;
}
&b[offset..]
}
fn trim_right(b: &[u8]) -> &[u8] {
if b.is_empty() {
return b;
}
let mut offset = b.len();
while offset > 0 && matches!(b[offset - 1], b' ' | b'\t') {
offset -= 1;
}
&b[..offset]
}
fn trim(b: &[u8]) -> &[u8] {
trim_right(trim_left(b))
}
fn parse_value(slice: &[u8]) -> Result<&str, BoxError> {
if slice.iter().any(|b| !(32..127).contains(b) || *b == b'"') {
return Err(BoxError::from_static_str(
"value contains invalid characters",
));
}
core::str::from_utf8(slice).context("parse value as utf-8")
}
fn parse_quoted_value(slice: &[u8]) -> Result<&str, BoxError> {
if let Some(idx) = slice
.iter()
.position(|b| matches!(*b, 0..=0x08 | 0x0a..=0x1f | 0x7f))
{
return Err(
BoxError::from_static_str("quoted value contains invalid control byte")
.context_field("byte_index", idx),
);
}
core::str::from_utf8(slice).context("parse quoted value as utf-8")
}