use rvoip_sip_core::types::headers::{HeaderName, HeaderValue, TypedHeader};
fn other(name: &str, value: impl Into<String>) -> TypedHeader {
TypedHeader::Other(
HeaderName::Other(name.to_string()),
HeaderValue::Raw(value.into().into_bytes()),
)
}
pub fn diversion(value: impl Into<String>) -> TypedHeader {
other("Diversion", value)
}
pub fn history_info(value: impl Into<String>) -> TypedHeader {
other("History-Info", value)
}
pub fn privacy(value: impl Into<String>) -> TypedHeader {
other("Privacy", value)
}
pub fn replaces(value: impl Into<String>) -> TypedHeader {
other("Replaces", value)
}
pub fn target_dialog(value: impl Into<String>) -> TypedHeader {
other("Target-Dialog", value)
}
pub fn session_expires(value: impl Into<String>) -> TypedHeader {
other("Session-Expires", value)
}
pub fn min_se(seconds: u32) -> TypedHeader {
other("Min-SE", seconds.to_string())
}
pub fn p_charging_vector(value: impl Into<String>) -> TypedHeader {
other("P-Charging-Vector", value)
}
pub fn p_called_party_id(value: impl Into<String>) -> TypedHeader {
other("P-Called-Party-ID", value)
}
use bytes::Bytes;
#[derive(Debug, Clone)]
pub struct MultipartPart {
pub headers: Vec<TypedHeader>,
pub body: Bytes,
}
impl MultipartPart {
pub fn new(
content_type: impl Into<String>,
disposition: Option<&str>,
body: impl Into<Bytes>,
) -> Self {
let mut headers = Vec::with_capacity(2);
headers.push(other("Content-Type", content_type));
if let Some(disp) = disposition {
headers.push(other("Content-Disposition", disp));
}
Self {
headers,
body: body.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MultipartParseError {
MissingBoundary,
EmptyBoundary,
UnterminatedBody,
MalformedPartHeaders,
}
impl std::fmt::Display for MultipartParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MissingBoundary => write!(f, "multipart Content-Type missing boundary parameter"),
Self::EmptyBoundary => write!(f, "multipart boundary parameter is empty"),
Self::UnterminatedBody => {
write!(f, "multipart body missing closing --<boundary>-- marker")
}
Self::MalformedPartHeaders => {
write!(f, "multipart part headers missing CRLF CRLF terminator")
}
}
}
}
impl std::error::Error for MultipartParseError {}
pub fn multipart_mixed(parts: Vec<MultipartPart>) -> (TypedHeader, Bytes) {
let boundary = format!("rvoip-{}", uuid::Uuid::new_v4().simple());
let mut body: Vec<u8> = Vec::new();
for part in &parts {
body.extend_from_slice(b"--");
body.extend_from_slice(boundary.as_bytes());
body.extend_from_slice(b"\r\n");
for h in &part.headers {
body.extend_from_slice(h.to_string().as_bytes());
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(b"\r\n");
body.extend_from_slice(&part.body);
body.extend_from_slice(b"\r\n");
}
body.extend_from_slice(b"--");
body.extend_from_slice(boundary.as_bytes());
body.extend_from_slice(b"--\r\n");
let content_type = other(
"Content-Type",
format!("multipart/mixed; boundary={}", boundary),
);
(content_type, Bytes::from(body))
}
pub fn multipart_parse(
content_type: &str,
body: &[u8],
) -> Result<Vec<MultipartPart>, MultipartParseError> {
let boundary_param = content_type
.split(';')
.map(str::trim)
.find_map(|p| p.strip_prefix("boundary="))
.ok_or(MultipartParseError::MissingBoundary)?
.trim_matches('"');
if boundary_param.is_empty() {
return Err(MultipartParseError::EmptyBoundary);
}
let marker = format!("--{}", boundary_param);
let close = format!("--{}--", boundary_param);
let body_str = String::from_utf8_lossy(body);
if !body_str.contains(&close) {
return Err(MultipartParseError::UnterminatedBody);
}
let mut parts = Vec::new();
let trimmed = body_str.split(&close).next().unwrap_or("");
let segments: Vec<&str> = trimmed.split(&marker).collect();
for seg in segments.iter().skip(1) {
let seg = seg.trim_start_matches("\r\n").trim_end_matches("\r\n");
if seg.is_empty() {
continue;
}
let (headers_blob, body_blob) = seg
.split_once("\r\n\r\n")
.ok_or(MultipartParseError::MalformedPartHeaders)?;
let mut headers = Vec::new();
for line in headers_blob.split("\r\n") {
if let Some((name, value)) = line.split_once(':') {
headers.push(other(name.trim(), value.trim()));
}
}
parts.push(MultipartPart {
headers,
body: Bytes::copy_from_slice(body_blob.as_bytes()),
});
}
Ok(parts)
}
#[cfg(test)]
mod multipart_tests {
use super::*;
#[test]
fn round_trip_two_parts() {
let parts = vec![
MultipartPart::new("application/sdp", None, "v=0\r\no=- 1 1 IN IP4 0.0.0.0\r\n"),
MultipartPart::new(
"application/isup;version=ansi92",
Some("signal"),
vec![0x01, 0x02, 0x03],
),
];
let (ct, body) = multipart_mixed(parts);
let ct_str = ct.to_string();
let value = ct_str.split_once(':').map(|(_, v)| v.trim()).unwrap();
let round = multipart_parse(value, &body).expect("parse round-trip");
assert_eq!(round.len(), 2);
assert!(round[0]
.headers
.iter()
.any(|h| h.to_string().contains("application/sdp")));
assert_eq!(&round[1].body[..], &[0x01, 0x02, 0x03]);
}
#[test]
fn missing_boundary_returns_err() {
let err = multipart_parse("multipart/mixed", b"--x--\r\n").unwrap_err();
assert_eq!(err, MultipartParseError::MissingBoundary);
}
#[test]
fn unterminated_body_returns_err() {
let err = multipart_parse("multipart/mixed; boundary=x", b"--x\r\nfoo\r\n").unwrap_err();
assert_eq!(err, MultipartParseError::UnterminatedBody);
}
}