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
268impl std::fmt::Display for Session {
269    /// Formats the SDP session description, identical to
270    /// [`Session::write`].
271    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        // The output must still parse back into exactly one session with one
475        // media, i.e. no lines were injected.
476        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        // Negating i64::MIN overflows, so it must not be written via `-offset`.
484        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        // The written offset must parse back again.
522        let parsed = Session::parse(&written[..]).unwrap();
523        assert_eq!(parsed.time_zones, sdp.time_zones);
524    }
525}