Skip to main content

sdp_types/
writer.rs

1// Copyright (C) 2019 Sebastian Dröge <sebastian@centricular.com>
2//
3// Licensed under the MIT license, see the LICENSE file or <http://opensource.org/licenses/MIT>
4
5use 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
23// NUL, CR and LF characters are not allowed inside SDP lines (RFC 8866,
24// section 5). The string fields of the data model are arbitrary strings, so
25// strip those characters when writing to prevent injecting additional lines
26// into the generated session description.
27struct 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                // unsigned_abs to avoid overflow for i64::MIN
123                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    /// Write the SDP session description to a `std::io::Write`.
203    ///
204    /// The values are written as-is without further validation, see the
205    /// crate-level documentation for details. If the session description has
206    /// no time description, a `t=0 0` line is written instead.
207    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        // The output must still parse back into exactly one session with one
452        // media, i.e. no lines were injected.
453        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        // Negating i64::MIN overflows, so it must not be written via `-offset`.
461        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        // The written offset must parse back again.
499        let parsed = Session::parse(&written[..]).unwrap();
500        assert_eq!(parsed.time_zones, sdp.time_zones);
501    }
502}