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
268impl std::fmt::Display for Session {
269 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 let mut buf = Vec::new();
273 self.write(&mut buf).map_err(|_| std::fmt::Error)?;
274 f.write_str(&String::from_utf8_lossy(&buf))
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281
282 #[test]
283 fn display_session() {
284 let sdp = "v=0\r
285o=- 0 0 IN IP4 127.0.0.1\r
286s=x\r
287t=0 0\r
288";
289 let session = Session::parse(sdp.as_bytes()).unwrap();
290 let mut buf = Vec::new();
291 session.write(&mut buf).unwrap();
292 assert_eq!(session.to_string(), String::from_utf8(buf).unwrap());
293 }
294
295 #[test]
296 fn write_sdp() {
297 let sdp = Session {
298 origin: Origin {
299 username: Some("jdoe".into()),
300 sess_id: "2890844526".into(),
301 sess_version: 2890842807,
302 nettype: "IN".into(),
303 addrtype: "IP4".into(),
304 unicast_address: "10.47.16.5".into(),
305 },
306 session_name: "SDP Seminar".into(),
307 session_description: Some("A Seminar on the session description protocol".into()),
308 uri: Some("http://www.example.com/seminars/sdp.pdf".into()),
309 emails: vec!["j.doe@example.com (Jane Doe)".into()],
310 phones: vec!["+1 617 555-6011".into()],
311 connection: Some(Connection {
312 nettype: "IN".into(),
313 addrtype: "IP4".into(),
314 connection_address: "224.2.17.12/127".into(),
315 }),
316 bandwidths: vec![Bandwidth {
317 bwtype: "AS".into(),
318 bandwidth: 128,
319 }],
320 times: vec![Time {
321 start_time: 2873397496,
322 stop_time: 2873404696,
323 repeats: vec![Repeat {
324 repeat_interval: 604800,
325 active_duration: 3600,
326 offsets: vec![0, 90000],
327 }],
328 }],
329 time_zones: vec![
330 TimeZone {
331 adjustment_time: 2882844526,
332 offset: -3600,
333 },
334 TimeZone {
335 adjustment_time: 2898848070,
336 offset: 0,
337 },
338 ],
339 key: Some(Key {
340 method: "clear".into(),
341 encryption_key: Some("1234".into()),
342 }),
343 attributes: vec![Attribute {
344 attribute: "recvonly".into(),
345 value: None,
346 }],
347 medias: vec![
348 Media {
349 media: "audio".into(),
350 port: 49170,
351 num_ports: None,
352 proto: "RTP/AVP".into(),
353 fmt: "0".into(),
354 media_title: None,
355 connections: vec![],
356 bandwidths: vec![],
357 key: None,
358 attributes: vec![],
359 },
360 Media {
361 media: "video".into(),
362 port: 51372,
363 num_ports: Some(2),
364 proto: "RTP/AVP".into(),
365 fmt: "99".into(),
366 media_title: None,
367 connections: vec![],
368 bandwidths: vec![],
369 key: None,
370 attributes: vec![Attribute {
371 attribute: "rtpmap".into(),
372 value: Some("99 h263-1998/90000".into()),
373 }],
374 },
375 ],
376 };
377
378 let mut written = vec![];
379 sdp.write(&mut written).unwrap();
380
381 let expected = "v=0\r
382o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5\r
383s=SDP Seminar\r
384i=A Seminar on the session description protocol\r
385u=http://www.example.com/seminars/sdp.pdf\r
386e=j.doe@example.com (Jane Doe)\r
387p=+1 617 555-6011\r
388c=IN IP4 224.2.17.12/127\r
389b=AS:128\r
390t=2873397496 2873404696\r
391r=7d 1h 0 25h\r
392z=2882844526 -1h 2898848070 0\r
393k=clear:1234\r
394a=recvonly\r
395m=audio 49170 RTP/AVP 0\r
396m=video 51372/2 RTP/AVP 99\r
397a=rtpmap:99 h263-1998/90000\r
398";
399
400 assert_eq!(String::from_utf8_lossy(&written), expected);
401 }
402
403 #[test]
404 fn write_sdp_strips_line_breaks() {
405 let sdp = Session {
406 origin: Origin {
407 username: Some("user\r\ns=evil".into()),
408 sess_id: "123\n456".into(),
409 sess_version: 1,
410 nettype: "I\r\nN".into(),
411 addrtype: "IP\r4".into(),
412 unicast_address: "10.0.0.1".into(),
413 },
414 session_name: "test\r\nm=audio 0 RTP/AVP 0".into(),
415 session_description: Some("desc\0x".into()),
416 uri: None,
417 emails: vec![],
418 phones: vec![],
419 connection: Some(Connection {
420 nettype: "I\r\nN".into(),
421 addrtype: "IP\r4".into(),
422 connection_address: "224.0.0.1\r\na=evil".into(),
423 }),
424 bandwidths: vec![Bandwidth {
425 bwtype: "AS".into(),
426 bandwidth: 128,
427 }],
428 times: vec![Time {
429 start_time: 0,
430 stop_time: 0,
431 repeats: vec![],
432 }],
433 time_zones: vec![],
434 key: Some(Key {
435 method: "clear".into(),
436 encryption_key: Some("12\r\n34".into()),
437 }),
438 attributes: vec![Attribute {
439 attribute: "recvonly".into(),
440 value: Some("a\r\nb".into()),
441 }],
442 medias: vec![Media {
443 media: "audio".into(),
444 port: 49170,
445 num_ports: None,
446 proto: "RTP/AVP".into(),
447 fmt: "0".into(),
448 media_title: Some("ti\r\ntle".into()),
449 connections: vec![],
450 bandwidths: vec![],
451 key: None,
452 attributes: vec![],
453 }],
454 };
455
456 let mut written = vec![];
457 sdp.write(&mut written).unwrap();
458
459 let expected = "v=0\r
460o=users=evil 123456 1 IN IP4 10.0.0.1\r
461s=testm=audio 0 RTP/AVP 0\r
462i=descx\r
463c=IN IP4 224.0.0.1a=evil\r
464b=AS:128\r
465t=0 0\r
466k=clear:1234\r
467a=recvonly:ab\r
468m=audio 49170 RTP/AVP 0\r
469i=title\r
470";
471
472 assert_eq!(String::from_utf8_lossy(&written), expected);
473
474 let parsed = Session::parse(&written).unwrap();
477 assert_eq!(parsed.medias.len(), 1);
478 assert_eq!(parsed.session_name, "testm=audio 0 RTP/AVP 0");
479 }
480
481 #[test]
482 fn write_time_zone_min_offset() {
483 let sdp = Session {
485 origin: Origin {
486 username: None,
487 sess_id: "0".into(),
488 sess_version: 0,
489 nettype: "IN".into(),
490 addrtype: "IP4".into(),
491 unicast_address: "0.0.0.0".into(),
492 },
493 session_name: "-".into(),
494 session_description: None,
495 uri: None,
496 emails: vec![],
497 phones: vec![],
498 connection: None,
499 bandwidths: vec![],
500 times: vec![],
501 time_zones: vec![TimeZone {
502 adjustment_time: 2882844526,
503 offset: i64::MIN,
504 }],
505 key: None,
506 attributes: vec![],
507 medias: vec![],
508 };
509
510 let mut written = vec![];
511 sdp.write(&mut written).unwrap();
512
513 let expected = "v=0\r
514o=- 0 0 IN IP4 0.0.0.0\r
515s=-\r
516t=0 0\r
517z=2882844526 -9223372036854775808\r
518";
519 assert_eq!(String::from_utf8_lossy(&written), expected);
520
521 let parsed = Session::parse(&written[..]).unwrap();
523 assert_eq!(parsed.time_zones, sdp.time_zones);
524 }
525}