rtsp_runtime/
session_header.rs1use std::time::Duration;
27
28use crate::error::{Error, Result};
29use crate::rfc2326_lex as lex;
30
31pub const DEFAULT_SESSION_TIMEOUT: Duration = Duration::from_secs(60);
33
34const PARAM_TIMEOUT: &str = "timeout";
36const DELTA_SECONDS_DIGITS: usize = 19;
38
39#[non_exhaustive]
41#[derive(Debug, Clone, PartialEq, Eq)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
43pub struct SessionHeader {
44 pub id: String,
46 pub timeout: Option<Duration>,
48 pub extensions: Vec<(String, Option<String>)>,
50}
51
52impl SessionHeader {
53 pub fn new(id: impl Into<String>) -> Self {
55 SessionHeader {
56 id: id.into(),
57 timeout: None,
58 extensions: Vec::new(),
59 }
60 }
61
62 pub fn parse(value: &str) -> Result<Self> {
64 Self::parse_with_warnings(value).map(|(h, _)| h)
65 }
66
67 pub fn parse_with_warnings(value: &str) -> Result<(Self, Vec<String>)> {
70 let (id, segs) = lex::split_session_value(value);
74 if id.is_empty() {
75 return Err(Error::SessionParse("empty session id".into()));
76 }
77 if lex::has_ctl(id) {
80 return Err(Error::SessionParse(format!(
81 "control character in session id {id:?}"
82 )));
83 }
84 let mut out = SessionHeader::new(id);
85 let mut warnings = Vec::new();
86 for seg in segs {
87 if lex::trim_lws(seg).is_empty() {
88 continue;
89 }
90 let (name, raw) = lex::split_param(seg).map_err(|e| Error::SessionParse(e.0))?;
91 let value = match raw {
92 Some(v) => Some(lex::unquote(v).map_err(|e| Error::SessionParse(e.0))?.0),
93 None => None,
94 };
95 if name.eq_ignore_ascii_case(PARAM_TIMEOUT) {
96 let parsed = value
97 .as_deref()
98 .and_then(|v| lex::digits(v, DELTA_SECONDS_DIGITS, PARAM_TIMEOUT).ok());
99 match parsed {
100 Some(n) if n > 0 => out.timeout = Some(Duration::from_secs(n.into())),
102 _ => {
103 warnings.push(format!(
104 "malformed or zero timeout {value:?}; using the 60 s default"
105 ));
106 out.timeout = Some(DEFAULT_SESSION_TIMEOUT);
107 }
108 }
109 } else {
110 out.extensions.push((name.to_string(), value));
111 }
112 }
113 Ok((out, warnings))
114 }
115
116 pub fn to_header_value(&self) -> Result<String> {
124 if self.id.is_empty() || lex::has_ctl(&self.id) || lex::trim_lws(&self.id) != self.id {
126 return Err(Error::HeaderSerialize(format!(
127 "invalid session id {:?}",
128 self.id
129 )));
130 }
131 let mut s = self.id.clone();
132 if let Some(t) = self.timeout {
133 s.push(lex::PARAM_SEP);
134 s.push_str(&format!("{PARAM_TIMEOUT}{}{}", lex::VALUE_SEP, t.as_secs()));
135 }
136 for (name, value) in &self.extensions {
137 if !lex::is_token(name) {
138 return Err(Error::HeaderSerialize(format!(
139 "extension name {name:?} is not a token"
140 )));
141 }
142 s.push(lex::PARAM_SEP);
143 s.push_str(name);
144 if let Some(v) = value {
145 s.push(lex::VALUE_SEP);
146 s.push_str(&lex::emit_value(v).map_err(|e| Error::HeaderSerialize(e.0))?);
147 }
148 }
149 let first = lex::split_outside_quotes(&s, lex::PARAM_SEP)[0];
152 if lex::trim_lws(first) != self.id {
153 return Err(Error::HeaderSerialize(format!(
154 "session id {:?} cannot be emitted unambiguously",
155 self.id
156 )));
157 }
158 Ok(s)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn p(v: &str) -> SessionHeader {
167 SessionHeader::parse(v).unwrap()
168 }
169
170 #[test]
171 fn the_interop_table() {
172 for v in [
173 "abc;timeout=30",
174 "abc; timeout=30",
175 "abc ; timeout=30",
176 "abc;TIMEOUT=30",
177 "abc;timeout = 30",
178 ] {
179 let h = p(v);
180 assert_eq!(
181 (h.id.as_str(), h.timeout),
182 ("abc", Some(Duration::from_secs(30))),
183 "{v}"
184 );
185 }
186 let (h, w) = SessionHeader::parse_with_warnings("abc;timeout=0").unwrap();
187 assert_eq!(
188 h.timeout,
189 Some(DEFAULT_SESSION_TIMEOUT),
190 "timeout=0 is invalid"
191 );
192 assert_eq!(w.len(), 1);
193 let (h, w) = SessionHeader::parse_with_warnings("abc;timeout=x").unwrap();
194 assert_eq!(h.timeout, Some(DEFAULT_SESSION_TIMEOUT));
195 assert_eq!(w.len(), 1);
196 let h = p("abc;foo=bar");
197 assert_eq!(h.extensions, vec![("foo".into(), Some("bar".into()))]);
198 assert!(matches!(
199 SessionHeader::parse(""),
200 Err(Error::SessionParse(_))
201 ));
202 assert!(SessionHeader::parse(" ;timeout=5").is_err());
203 assert_eq!(p("12345678").timeout, None);
204 }
205
206 #[test]
207 fn an_unbalanced_quote_falls_back_to_the_first_semicolon() {
208 let (h, w) = SessionHeader::parse_with_warnings("ab\"c;timeout=30").unwrap();
210 assert_eq!(h.id, "ab\"c");
211 assert_eq!(h.timeout, Some(Duration::from_secs(30)));
212 assert!(w.is_empty(), "{w:?}");
213 let (h, w) = SessionHeader::parse_with_warnings("\"a;b\";timeout=30").unwrap();
215 assert_eq!(h.id, "\"a;b\"");
216 assert_eq!(h.timeout, Some(Duration::from_secs(30)));
217 assert!(w.is_empty(), "{w:?}");
218 assert_eq!(p("ab\"c").id, "ab\"c");
220 }
221
222 #[test]
223 fn canonical_output_round_trips_and_is_idempotent() {
224 for v in [
225 "abc",
226 "abc ; Timeout = 30 ; x=\"a b\"",
227 "abc;timeout=x;flag",
228 ] {
229 let a = p(v);
230 let c = a.to_header_value().unwrap();
231 let b = p(&c);
232 assert_eq!(a.id, b.id);
233 assert_eq!(b.to_header_value().unwrap(), c, "idempotent {v}");
234 assert_eq!(p(&c), b);
235 }
236 assert_eq!(
237 p("abc ; Timeout = 30").to_header_value().unwrap(),
238 "abc;timeout=30"
239 );
240 }
241
242 #[test]
243 fn the_serializer_never_emits_control_characters() {
244 let mut h = SessionHeader::new("abc");
245 h.id = "ab\r\nSet-Cookie: x".into();
246 assert!(matches!(
247 h.to_header_value(),
248 Err(Error::HeaderSerialize(_))
249 ));
250 let mut h = SessionHeader::new("abc");
251 h.extensions.push(("x\r\ny".into(), None));
252 assert!(matches!(
253 h.to_header_value(),
254 Err(Error::HeaderSerialize(_))
255 ));
256 let mut h = SessionHeader::new("abc");
257 h.extensions.push(("x".into(), Some("a\r\nb".into())));
258 assert!(matches!(
259 h.to_header_value(),
260 Err(Error::HeaderSerialize(_))
261 ));
262 let mut h = SessionHeader::new("abc");
263 h.id = String::new();
264 assert!(matches!(
265 h.to_header_value(),
266 Err(Error::HeaderSerialize(_))
267 ));
268 let mut h = SessionHeader::new("ab\"c");
270 h.timeout = Some(Duration::from_secs(5));
271 assert!(matches!(
272 h.to_header_value(),
273 Err(Error::HeaderSerialize(_))
274 ));
275 }
276 #[test]
279 fn a_received_lone_quote_id_parses_but_is_not_emittable() {
280 let (h, _) = SessionHeader::parse_with_warnings("\";;!").unwrap();
281 assert_eq!(h.id, "\"");
282 assert!(matches!(
283 h.to_header_value(),
284 Err(crate::Error::HeaderSerialize(_))
285 ));
286 }
287}