use std::fmt;
use rvoip_sip_core::types::headers::{HeaderName, HeaderValue, TypedHeader};
use rvoip_sip_core::types::Method;
use super::policy::{self, HeaderRole};
use super::view::SipHeaderView;
#[derive(Default, Debug, Clone)]
pub struct BuilderHeaderState {
pub headers: Vec<TypedHeader>,
pub strictness: BuilderStrictness,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Default)]
pub enum BuilderStrictness {
#[default]
Strict,
Lenient,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ViolationReason {
StackManaged,
WrongMethod,
UseDedicatedSetter(&'static str),
}
impl fmt::Display for ViolationReason {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ViolationReason::StackManaged => f.write_str("owned by the dialog/transaction layer"),
ViolationReason::WrongMethod => f.write_str("not allowed on this SIP method"),
ViolationReason::UseDedicatedSetter(s) => {
write!(f, "use the dedicated `{s}` setter instead")
}
}
}
}
#[derive(Debug, Clone)]
pub struct HeaderPolicyViolation {
pub method: Method,
pub header: HeaderName,
pub reason: ViolationReason,
}
impl fmt::Display for HeaderPolicyViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"header policy violation on {}: {:?} — {}",
self.method, self.header, self.reason
)
}
}
impl std::error::Error for HeaderPolicyViolation {}
#[derive(Default, Debug, Clone)]
pub struct HeaderCarryThroughReport {
pub copied: Vec<HeaderName>,
pub skipped: Vec<(HeaderName, ViolationReason)>,
}
pub trait SipRequestOptions: Sized + Send + Sync {
fn method(&self) -> Method;
fn header_state_mut(&mut self) -> &mut BuilderHeaderState;
fn header_state(&self) -> &BuilderHeaderState;
fn with_header(mut self, header: TypedHeader) -> Result<Self, HeaderPolicyViolation> {
let method = self.method();
let name = header.name();
let role = policy::classify(method.clone(), &name);
match (&role, self.header_state().strictness) {
(HeaderRole::StackManaged, _) => Err(HeaderPolicyViolation {
method,
header: name,
reason: ViolationReason::StackManaged,
}),
(HeaderRole::MethodShaped { setter }, BuilderStrictness::Strict) => {
Err(HeaderPolicyViolation {
method,
header: name,
reason: ViolationReason::UseDedicatedSetter(setter),
})
}
(HeaderRole::MethodShaped { setter }, BuilderStrictness::Lenient) => {
tracing::warn!(
method = %method,
header = ?name,
setter = setter,
"Builder Lenient mode: dropping method-shaped header; \
use the dedicated setter instead",
);
Ok(self)
}
(HeaderRole::ApplicationControlled, _) => {
self.header_state_mut().headers.push(header);
Ok(self)
}
}
}
fn with_headers(self, headers: Vec<TypedHeader>) -> Result<Self, HeaderPolicyViolation> {
let mut me = self;
for h in headers {
me = me.with_header(h)?;
}
Ok(me)
}
fn with_raw_header(
self,
name: impl Into<HeaderName>,
value: impl Into<String>,
) -> Result<Self, HeaderPolicyViolation> {
let name = name.into();
let canonical = canonicalize_header_name(name);
let hv = HeaderValue::Raw(value.into().into_bytes());
self.with_header(TypedHeader::Other(canonical, hv))
}
fn strip_header(mut self, name: &HeaderName) -> Self {
let state = self.header_state_mut();
state
.headers
.retain(|h| !super::view::header_name_eq(&h.name(), name));
self
}
fn with_headers_from<S: SipHeaderView>(
mut self,
source: &S,
names: &[HeaderName],
) -> Result<(Self, HeaderCarryThroughReport), HeaderPolicyViolation> {
let method = self.method();
let mut report = HeaderCarryThroughReport::default();
for name in names {
if policy::forbidden_for_carry_through(name) {
report
.skipped
.push((name.clone(), ViolationReason::StackManaged));
continue;
}
for hdr in source.headers_named(name) {
let role = policy::classify(method.clone(), &hdr.name());
match role {
HeaderRole::StackManaged => {
report
.skipped
.push((name.clone(), ViolationReason::StackManaged));
}
HeaderRole::MethodShaped { setter } => {
report
.skipped
.push((name.clone(), ViolationReason::UseDedicatedSetter(setter)));
}
HeaderRole::ApplicationControlled => {
self.header_state_mut().headers.push(hdr.clone());
report.copied.push(name.clone());
}
}
}
}
Ok((self, report))
}
fn staged_headers(&self) -> &[TypedHeader] {
&self.header_state().headers
}
fn with_strictness(mut self, mode: BuilderStrictness) -> Self {
self.header_state_mut().strictness = mode;
self
}
}
pub fn take_staged(state: &mut BuilderHeaderState) -> Vec<TypedHeader> {
std::mem::take(&mut state.headers)
}
fn canonicalize_header_name(name: HeaderName) -> HeaderName {
match name {
HeaderName::Other(s) => HeaderName::Other(canonicalize_other(&s)),
other => other,
}
}
fn canonicalize_other(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut start_of_word = true;
for ch in s.chars() {
if ch == '-' {
out.push('-');
start_of_word = true;
} else if start_of_word {
for upper in ch.to_uppercase() {
out.push(upper);
}
start_of_word = false;
} else {
for lower in ch.to_lowercase() {
out.push(lower);
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonicalize_simple() {
assert_eq!(canonicalize_other("x-customer-id"), "X-Customer-Id");
assert_eq!(canonicalize_other("X-CUSTOMER-ID"), "X-Customer-Id");
assert_eq!(canonicalize_other("history-info"), "History-Info");
}
}