1use super::*;
6
7fn format_time<W: std::io::Write + ?Sized>(w: &mut W, t: u64) -> Result<(), std::io::Error> {
8 if t == 0 {
9 write!(w, "{t}")?;
10 } else if t % 86_400 == 0 {
11 write!(w, "{}d", t / 86_400)?;
12 } else if t % 3_600 == 0 {
13 write!(w, "{}h", t / 3_600)?;
14 } else if t % 60 == 0 {
15 write!(w, "{}m", t / 60)?;
16 } else {
17 write!(w, "{t}")?;
18 }
19
20 Ok(())
21}
22
23struct FreeText<'a>(&'a str);
28
29impl std::fmt::Display for FreeText<'_> {
30 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
31 for c in self.0.chars().filter(|c| !matches!(c, '\0' | '\r' | '\n')) {
32 std::fmt::Write::write_char(f, c)?;
33 }
34 Ok(())
35 }
36}
37
38impl Origin {
39 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
40 writeln!(
41 w,
42 "o={username} {sess_id} {sess_version} {nettype} {addrtype} {unicast_address}\r",
43 username = match self.username {
44 Some(ref username) if !username.is_empty() => FreeText(username),
45 _ => FreeText("-"),
46 },
47 sess_id = FreeText(&self.sess_id),
48 sess_version = self.sess_version,
49 nettype = FreeText(self.nettype.as_str()),
50 addrtype = FreeText(self.addrtype.as_str()),
51 unicast_address = FreeText(&self.unicast_address),
52 )
53 }
54}
55
56impl Connection {
57 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
58 writeln!(
59 w,
60 "c={nettype} {addrtype} {connection_address}\r",
61 nettype = FreeText(self.nettype.as_str()),
62 addrtype = FreeText(self.addrtype.as_str()),
63 connection_address = FreeText(&self.connection_address)
64 )
65 }
66}
67
68impl Bandwidth {
69 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
70 writeln!(
71 w,
72 "b={bwtype}:{bw}\r",
73 bwtype = FreeText(&self.bwtype),
74 bw = self.bandwidth
75 )
76 }
77}
78
79impl Time {
80 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
81 writeln!(
82 w,
83 "t={start_time} {stop_time}\r",
84 start_time = self.start_time,
85 stop_time = self.stop_time,
86 )
87 }
88}
89
90impl Repeat {
91 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
92 write!(w, "r=")?;
93 format_time(w, self.repeat_interval)?;
94 write!(w, " ")?;
95 format_time(w, self.active_duration)?;
96
97 for offset in &self.offsets {
98 write!(w, " ")?;
99 format_time(w, *offset)?;
100 }
101
102 writeln!(w, "\r")
103 }
104}
105
106impl TimeZone {
107 fn write<W: std::io::Write + ?Sized>(
108 w: &mut W,
109 zones: &[TimeZone],
110 ) -> Result<(), std::io::Error> {
111 write!(w, "z=")?;
112
113 let mut first = true;
114 for zone in zones {
115 if !first {
116 write!(w, " ")?;
117 }
118
119 write!(w, "{} ", zone.adjustment_time)?;
120 if zone.offset < 0 {
121 write!(w, "-")?;
122 format_time(w, zone.offset.unsigned_abs())?;
124 } else {
125 format_time(w, zone.offset as u64)?;
126 }
127 first = false;
128 }
129 writeln!(w, "\r")
130 }
131}
132
133impl Key {
134 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
135 if let Some(ref encryption_key) = self.encryption_key {
136 writeln!(
137 w,
138 "k={method}:{encryption_key}\r",
139 method = FreeText(&self.method),
140 encryption_key = FreeText(encryption_key)
141 )
142 } else {
143 writeln!(w, "k={method}\r", method = FreeText(&self.method))
144 }
145 }
146}
147
148impl Attribute {
149 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
150 if let Some(ref value) = self.value {
151 writeln!(
152 w,
153 "a={name}:{value}\r",
154 name = FreeText(&self.attribute),
155 value = FreeText(value)
156 )
157 } else {
158 writeln!(w, "a={name}\r", name = FreeText(&self.attribute))
159 }
160 }
161}
162
163impl Media {
164 fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
165 write!(
166 w,
167 "m={media} {port}",
168 media = FreeText(&self.media),
169 port = self.port
170 )?;
171
172 if let Some(ref num_ports) = self.num_ports {
173 write!(w, "/{num_ports}")?;
174 }
175 writeln!(w, " {} {}\r", FreeText(&self.proto), FreeText(&self.fmt))?;
176
177 if let Some(ref media_title) = self.media_title {
178 writeln!(w, "i={}\r", FreeText(media_title))?;
179 }
180
181 for connection in &self.connections {
182 connection.write(w)?;
183 }
184
185 for bandwidth in &self.bandwidths {
186 bandwidth.write(w)?;
187 }
188
189 if let Some(ref key) = self.key {
190 key.write(w)?;
191 }
192
193 for attribute in &self.attributes {
194 attribute.write(w)?;
195 }
196
197 Ok(())
198 }
199}
200
201impl Session {
202 pub fn write<W: std::io::Write + ?Sized>(&self, w: &mut W) -> Result<(), std::io::Error> {
208 writeln!(w, "v=0\r")?;
209 self.origin.write(w)?;
210 writeln!(w, "s={}\r", FreeText(&self.session_name))?;
211
212 if let Some(ref session_description) = self.session_description {
213 writeln!(w, "i={}\r", FreeText(session_description))?;
214 }
215
216 if let Some(ref uri) = self.uri {
217 writeln!(w, "u={}\r", FreeText(uri))?;
218 }
219
220 for email in &self.emails {
221 writeln!(w, "e={}\r", FreeText(email))?;
222 }
223
224 for phone in &self.phones {
225 writeln!(w, "p={}\r", FreeText(phone))?;
226 }
227
228 if let Some(ref connection) = self.connection {
229 connection.write(w)?;
230 }
231
232 for bandwidth in &self.bandwidths {
233 bandwidth.write(w)?;
234 }
235
236 if !self.times.is_empty() {
237 for time in &self.times {
238 time.write(w)?;
239
240 for repeat in &time.repeats {
241 repeat.write(w)?;
242 }
243 }
244 } else {
245 writeln!(w, "t=0 0\r")?;
246 }
247
248 if !self.time_zones.is_empty() {
249 TimeZone::write(w, &self.time_zones)?;
250 }
251
252 if let Some(ref key) = self.key {
253 key.write(w)?;
254 }
255
256 for attribute in &self.attributes {
257 attribute.write(w)?;
258 }
259
260 for media in &self.medias {
261 media.write(w)?;
262 }
263
264 Ok(())
265 }
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271
272 #[test]
273 fn write_sdp() {
274 let sdp = Session {
275 origin: Origin {
276 username: Some("jdoe".into()),
277 sess_id: "2890844526".into(),
278 sess_version: 2890842807,
279 nettype: "IN".into(),
280 addrtype: "IP4".into(),
281 unicast_address: "10.47.16.5".into(),
282 },
283 session_name: "SDP Seminar".into(),
284 session_description: Some("A Seminar on the session description protocol".into()),
285 uri: Some("http://www.example.com/seminars/sdp.pdf".into()),
286 emails: vec!["j.doe@example.com (Jane Doe)".into()],
287 phones: vec!["+1 617 555-6011".into()],
288 connection: Some(Connection {
289 nettype: "IN".into(),
290 addrtype: "IP4".into(),
291 connection_address: "224.2.17.12/127".into(),
292 }),
293 bandwidths: vec![Bandwidth {
294 bwtype: "AS".into(),
295 bandwidth: 128,
296 }],
297 times: vec![Time {
298 start_time: 2873397496,
299 stop_time: 2873404696,
300 repeats: vec![Repeat {
301 repeat_interval: 604800,
302 active_duration: 3600,
303 offsets: vec![0, 90000],
304 }],
305 }],
306 time_zones: vec![
307 TimeZone {
308 adjustment_time: 2882844526,
309 offset: -3600,
310 },
311 TimeZone {
312 adjustment_time: 2898848070,
313 offset: 0,
314 },
315 ],
316 key: Some(Key {
317 method: "clear".into(),
318 encryption_key: Some("1234".into()),
319 }),
320 attributes: vec![Attribute {
321 attribute: "recvonly".into(),
322 value: None,
323 }],
324 medias: vec![
325 Media {
326 media: "audio".into(),
327 port: 49170,
328 num_ports: None,
329 proto: "RTP/AVP".into(),
330 fmt: "0".into(),
331 media_title: None,
332 connections: vec![],
333 bandwidths: vec![],
334 key: None,
335 attributes: vec![],
336 },
337 Media {
338 media: "video".into(),
339 port: 51372,
340 num_ports: Some(2),
341 proto: "RTP/AVP".into(),
342 fmt: "99".into(),
343 media_title: None,
344 connections: vec![],
345 bandwidths: vec![],
346 key: None,
347 attributes: vec![Attribute {
348 attribute: "rtpmap".into(),
349 value: Some("99 h263-1998/90000".into()),
350 }],
351 },
352 ],
353 };
354
355 let mut written = vec![];
356 sdp.write(&mut written).unwrap();
357
358 let expected = "v=0\r
359o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r
360s=SDP Seminar\r
361i=A Seminar on the session description protocol\r
362u=http://www.example.com/seminars/sdp.pdf\r
363e=j.doe@example.com (Jane Doe)\r
364p=+1 617 555-6011\r
365c=IN IP4 224.2.17.12/127\r
366b=AS:128\r
367t=2873397496 2873404696\r
368r=7d 1h 0 25h\r
369z=2882844526 -1h 2898848070 0\r
370k=clear:1234\r
371a=recvonly\r
372m=audio 49170 RTP/AVP 0\r
373m=video 51372/2 RTP/AVP 99\r
374a=rtpmap:99 h263-1998/90000\r
375";
376
377 assert_eq!(String::from_utf8_lossy(&written), expected);
378 }
379
380 #[test]
381 fn write_sdp_strips_line_breaks() {
382 let sdp = Session {
383 origin: Origin {
384 username: Some("user\r\ns=evil".into()),
385 sess_id: "123\n456".into(),
386 sess_version: 1,
387 nettype: "I\r\nN".into(),
388 addrtype: "IP\r4".into(),
389 unicast_address: "10.0.0.1".into(),
390 },
391 session_name: "test\r\nm=audio 0 RTP/AVP 0".into(),
392 session_description: Some("desc\0x".into()),
393 uri: None,
394 emails: vec![],
395 phones: vec![],
396 connection: Some(Connection {
397 nettype: "I\r\nN".into(),
398 addrtype: "IP\r4".into(),
399 connection_address: "224.0.0.1\r\na=evil".into(),
400 }),
401 bandwidths: vec![Bandwidth {
402 bwtype: "AS".into(),
403 bandwidth: 128,
404 }],
405 times: vec![Time {
406 start_time: 0,
407 stop_time: 0,
408 repeats: vec![],
409 }],
410 time_zones: vec![],
411 key: Some(Key {
412 method: "clear".into(),
413 encryption_key: Some("12\r\n34".into()),
414 }),
415 attributes: vec![Attribute {
416 attribute: "recvonly".into(),
417 value: Some("a\r\nb".into()),
418 }],
419 medias: vec![Media {
420 media: "audio".into(),
421 port: 49170,
422 num_ports: None,
423 proto: "RTP/AVP".into(),
424 fmt: "0".into(),
425 media_title: Some("ti\r\ntle".into()),
426 connections: vec![],
427 bandwidths: vec![],
428 key: None,
429 attributes: vec![],
430 }],
431 };
432
433 let mut written = vec![];
434 sdp.write(&mut written).unwrap();
435
436 let expected = "v=0\r
437o=users=evil 123456 1 IN IP4 10.0.0.1\r
438s=testm=audio 0 RTP/AVP 0\r
439i=descx\r
440c=IN IP4 224.0.0.1a=evil\r
441b=AS:128\r
442t=0 0\r
443k=clear:1234\r
444a=recvonly:ab\r
445m=audio 49170 RTP/AVP 0\r
446i=title\r
447";
448
449 assert_eq!(String::from_utf8_lossy(&written), expected);
450
451 let parsed = Session::parse(&written).unwrap();
454 assert_eq!(parsed.medias.len(), 1);
455 assert_eq!(parsed.session_name, "testm=audio 0 RTP/AVP 0");
456 }
457
458 #[test]
459 fn write_time_zone_min_offset() {
460 let sdp = Session {
462 origin: Origin {
463 username: None,
464 sess_id: "0".into(),
465 sess_version: 0,
466 nettype: "IN".into(),
467 addrtype: "IP4".into(),
468 unicast_address: "0.0.0.0".into(),
469 },
470 session_name: "-".into(),
471 session_description: None,
472 uri: None,
473 emails: vec![],
474 phones: vec![],
475 connection: None,
476 bandwidths: vec![],
477 times: vec![],
478 time_zones: vec![TimeZone {
479 adjustment_time: 2882844526,
480 offset: i64::MIN,
481 }],
482 key: None,
483 attributes: vec![],
484 medias: vec![],
485 };
486
487 let mut written = vec![];
488 sdp.write(&mut written).unwrap();
489
490 let expected = "v=0\r
491o=- 0 0 IN IP4 0.0.0.0\r
492s=-\r
493t=0 0\r
494z=2882844526 -9223372036854775808\r
495";
496 assert_eq!(String::from_utf8_lossy(&written), expected);
497
498 let parsed = Session::parse(&written[..]).unwrap();
500 assert_eq!(parsed.time_zones, sdp.time_zones);
501 }
502}