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!(SessionHeader::parse("").is_err());
199 assert!(SessionHeader::parse(" ;timeout=5").is_err());
200 assert_eq!(p("12345678").timeout, None);
201 }
202
203 #[test]
204 fn an_unbalanced_quote_falls_back_to_the_first_semicolon() {
205 let (h, w) = SessionHeader::parse_with_warnings("ab\"c;timeout=30").unwrap();
207 assert_eq!(h.id, "ab\"c");
208 assert_eq!(h.timeout, Some(Duration::from_secs(30)));
209 assert!(w.is_empty(), "{w:?}");
210 let (h, w) = SessionHeader::parse_with_warnings("\"a;b\";timeout=30").unwrap();
212 assert_eq!(h.id, "\"a;b\"");
213 assert_eq!(h.timeout, Some(Duration::from_secs(30)));
214 assert!(w.is_empty(), "{w:?}");
215 assert_eq!(p("ab\"c").id, "ab\"c");
217 }
218
219 #[test]
220 fn canonical_output_round_trips_and_is_idempotent() {
221 for v in [
222 "abc",
223 "abc ; Timeout = 30 ; x=\"a b\"",
224 "abc;timeout=x;flag",
225 ] {
226 let a = p(v);
227 let c = a.to_header_value().unwrap();
228 let b = p(&c);
229 assert_eq!(a.id, b.id);
230 assert_eq!(b.to_header_value().unwrap(), c, "idempotent {v}");
231 assert_eq!(p(&c), b);
232 }
233 assert_eq!(
234 p("abc ; Timeout = 30").to_header_value().unwrap(),
235 "abc;timeout=30"
236 );
237 }
238
239 #[test]
240 fn the_serializer_never_emits_control_characters() {
241 let mut h = SessionHeader::new("abc");
242 h.id = "ab\r\nSet-Cookie: x".into();
243 assert!(matches!(
244 h.to_header_value(),
245 Err(Error::HeaderSerialize(_))
246 ));
247 let mut h = SessionHeader::new("abc");
248 h.extensions.push(("x\r\ny".into(), None));
249 assert!(h.to_header_value().is_err());
250 let mut h = SessionHeader::new("abc");
251 h.extensions.push(("x".into(), Some("a\r\nb".into())));
252 assert!(h.to_header_value().is_err());
253 let mut h = SessionHeader::new("abc");
254 h.id = String::new();
255 assert!(h.to_header_value().is_err());
256 let mut h = SessionHeader::new("ab\"c");
258 h.timeout = Some(Duration::from_secs(5));
259 assert!(h.to_header_value().is_err());
260 }
261 #[test]
264 fn a_received_lone_quote_id_parses_but_is_not_emittable() {
265 let (h, _) = SessionHeader::parse_with_warnings("\";;!").unwrap();
266 assert_eq!(h.id, "\"");
267 assert!(matches!(
268 h.to_header_value(),
269 Err(crate::Error::HeaderSerialize(_))
270 ));
271 }
272}