#![allow(missing_docs)]
use rvoip_sip_core::types::headers::{HeaderName, TypedHeader};
use rvoip_sip_core::types::Method;
use super::ViolationReason;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HeaderRole {
StackManaged,
MethodShaped { setter: &'static str },
ApplicationControlled,
}
#[derive(Debug, Clone)]
pub struct MissingRequiredHeader {
pub method: Method,
pub name: HeaderName,
pub reason: &'static str,
}
pub fn classify(method: Method, name: &HeaderName) -> HeaderRole {
if is_always_stack_managed(name) {
return HeaderRole::StackManaged;
}
if matches!(name, HeaderName::Route) {
return HeaderRole::StackManaged;
}
if let Some(setter) = method_shaped_setter(method, name) {
return HeaderRole::MethodShaped { setter };
}
HeaderRole::ApplicationControlled
}
pub fn forbidden_for_carry_through(name: &HeaderName) -> bool {
is_always_stack_managed(name) || matches!(name, HeaderName::Route)
}
pub fn validate_outbound(
method: Method,
headers: &[TypedHeader],
) -> Result<(), Vec<MissingRequiredHeader>> {
let mut violations = Vec::new();
for h in headers {
let name = h.name();
if matches!(classify(method.clone(), &name), HeaderRole::StackManaged) {
violations.push(MissingRequiredHeader {
method: method.clone(),
name,
reason: "stack-managed header staged in application extras — \
this name is owned by the dialog/transaction layer",
});
}
}
if violations.is_empty() {
Ok(())
} else {
Err(violations)
}
}
fn is_always_stack_managed(name: &HeaderName) -> bool {
matches!(
name,
HeaderName::CallId
| HeaderName::CSeq
| HeaderName::Via
| HeaderName::MaxForwards
| HeaderName::ContentLength
| HeaderName::RecordRoute
)
}
fn method_shaped_setter(method: Method, name: &HeaderName) -> Option<&'static str> {
use HeaderName as H;
use Method as M;
match (method, name) {
(M::Invite | M::Register | M::Subscribe, H::Contact) => Some("with_contact_uri"),
(
M::Invite | M::Register | M::Subscribe | M::Message | M::Options | M::Refer,
H::Authorization,
) => Some("with_credentials"),
(M::Register | M::Subscribe, H::Expires) => Some("with_expires"),
(M::Refer, H::ReferTo) => Some("refer(.., refer_to)"),
(M::Notify | M::Subscribe, H::Event) => Some("notify(.., event_package)"),
(M::Notify, H::SubscriptionState) => Some("with_subscription_state"),
_ => None,
}
}
pub fn role_to_violation(role: &HeaderRole) -> Option<ViolationReason> {
match role {
HeaderRole::StackManaged => Some(ViolationReason::StackManaged),
HeaderRole::MethodShaped { setter } => Some(ViolationReason::UseDedicatedSetter(setter)),
HeaderRole::ApplicationControlled => None,
}
}