1use std::net::IpAddr;
8
9use crate::session::{
10 Address, Attribute, Connection, MediaDescription, Origin, SessionDescription, Timing,
11};
12use crate::{Result, SdpError};
13
14pub fn parse(input: &str) -> Result<SessionDescription> {
16 let mut origin = None;
17 let mut session_name = None;
18 let mut connection = None;
19 let mut timing = Vec::new();
20 let mut attributes = Vec::new();
21 let mut media: Vec<MediaDescription> = Vec::new();
22 let mut other = Vec::new();
23
24 for line in input.lines() {
25 let line = line.trim_end_matches('\r');
26 if line.is_empty() {
27 continue;
28 }
29
30 let mut chars = line.chars();
31 let kind = chars
32 .next()
33 .ok_or_else(|| SdpError::MalformedLine(line.to_owned()))?;
34 if chars.next() != Some('=') {
35 return Err(SdpError::MalformedLine(line.to_owned()));
36 }
37 let value = line.get(2..).unwrap_or("");
38
39 match kind {
40 'v' => {
41 }
44 'o' => origin = Some(parse_origin(value)?),
45 's' => {
46 if !value.is_empty() {
49 session_name = Some(value.to_owned());
50 }
51 }
52 'c' => {
53 let parsed = parse_connection(value)?;
54 match media.last_mut() {
56 Some(stream) => stream.connection = Some(parsed),
57 None => connection = Some(parsed),
58 }
59 }
60 't' => timing.push(parse_timing(value)),
61 'm' => media.push(parse_media(value)?),
62 'a' => {
63 let attribute = parse_attribute(value);
64 match media.last_mut() {
65 Some(stream) => stream.attributes.push(attribute),
66 None => attributes.push(attribute),
67 }
68 }
69 _ => match media.last_mut() {
74 Some(stream) => stream.other.push((kind, value.to_owned())),
75 None => other.push((kind, value.to_owned())),
76 },
77 }
78 }
79
80 Ok(SessionDescription {
81 origin: origin.ok_or(SdpError::Missing("o="))?,
82 session_name: session_name.unwrap_or_else(|| "-".to_owned()),
83 connection,
84 timing,
85 attributes,
86 media,
87 other,
88 })
89}
90
91fn parse_origin(value: &str) -> Result<Origin> {
92 let mut parts = value.split_whitespace();
93 let username = parts.next().unwrap_or("-").to_owned();
94 let session_id = parts
95 .next()
96 .and_then(|v| v.parse().ok())
97 .ok_or_else(|| invalid("origin session id", value))?;
98 let session_version = parts
99 .next()
100 .and_then(|v| v.parse().ok())
101 .ok_or_else(|| invalid("origin session version", value))?;
102 let _net_type = parts.next();
103 let _addr_type = parts.next();
104 let address = parts
105 .next()
106 .map(parse_address)
107 .ok_or_else(|| invalid("origin address", value))?;
108
109 Ok(Origin {
110 username,
111 session_id,
112 session_version,
113 address,
114 })
115}
116
117fn parse_connection(value: &str) -> Result<Connection> {
118 let address = value
119 .split_whitespace()
120 .nth(2)
121 .and_then(|raw| {
122 raw.split('/').next().filter(|v| !v.is_empty())
125 })
126 .map(parse_address)
127 .ok_or_else(|| invalid("connection address", value))?;
128 Ok(Connection { address })
129}
130
131fn parse_address(raw: &str) -> Address {
132 raw.parse::<IpAddr>()
136 .map_or_else(|_| Address::Host(raw.to_owned()), Address::Ip)
137}
138
139fn parse_timing(value: &str) -> Timing {
140 let mut parts = value.split_whitespace();
141 Timing {
144 start: parts.next().and_then(|v| v.parse().ok()).unwrap_or(0),
145 stop: parts.next().and_then(|v| v.parse().ok()).unwrap_or(0),
146 }
147}
148
149fn parse_media(value: &str) -> Result<MediaDescription> {
150 let mut parts = value.split_whitespace();
151 let media = parts
152 .next()
153 .ok_or_else(|| invalid("media type", value))?
154 .to_owned();
155 let port = parts
156 .next()
157 .and_then(|raw| {
158 raw.split('/').next().and_then(|v| v.parse::<u16>().ok())
161 })
162 .ok_or_else(|| invalid("media port", value))?;
163 let protocol = parts
164 .next()
165 .ok_or_else(|| invalid("media protocol", value))?
166 .to_owned();
167 let formats: Vec<String> = parts.map(str::to_owned).collect();
168 if formats.is_empty() {
171 return Err(invalid("media formats", value));
172 }
173
174 Ok(MediaDescription {
175 media,
176 port,
177 protocol,
178 formats,
179 connection: None,
180 attributes: Vec::new(),
181 other: Vec::new(),
182 })
183}
184
185fn parse_attribute(value: &str) -> Attribute {
186 match value.split_once(':') {
187 Some((name, rest)) => Attribute::valued(name, rest),
188 None => Attribute::flag(value),
189 }
190}
191
192fn invalid(field: &'static str, value: &str) -> SdpError {
193 SdpError::Invalid {
194 field,
195 value: value.to_owned(),
196 }
197}
198
199#[cfg(test)]
200#[allow(
201 clippy::unwrap_used,
202 clippy::expect_used,
203 clippy::panic,
204 clippy::indexing_slicing
205)]
206mod tests {
207 use super::*;
208 use crate::session::Direction;
209
210 const OFFER: &str = "v=0\r\n\
211 o=alice 2890844526 2890844526 IN IP4 192.0.2.10\r\n\
212 s=A call\r\n\
213 c=IN IP4 192.0.2.10\r\n\
214 t=0 0\r\n\
215 m=audio 49170 RTP/AVP 0 8 101\r\n\
216 a=rtpmap:0 PCMU/8000\r\n\
217 a=rtpmap:8 PCMA/8000\r\n\
218 a=rtpmap:101 telephone-event/8000\r\n\
219 a=fmtp:101 0-15\r\n\
220 a=sendrecv\r\n\
221 a=ptime:20\r\n";
222
223 #[test]
224 fn a_session_description_parses_into_its_parts() {
225 let sdp = parse(OFFER).expect("parses");
226 assert_eq!(sdp.origin.username, "alice");
227 assert_eq!(sdp.origin.session_id, 2_890_844_526);
228 assert_eq!(sdp.session_name, "A call");
229 assert_eq!(
230 sdp.connection.expect("a connection").address.to_string(),
231 "192.0.2.10"
232 );
233 assert_eq!(sdp.media.len(), 1);
234
235 let audio = &sdp.media[0];
236 assert_eq!(audio.media, "audio");
237 assert_eq!(audio.port, 49170);
238 assert_eq!(audio.protocol, "RTP/AVP");
239 assert_eq!(audio.formats, vec!["0", "8", "101"]);
240 assert_eq!(audio.rtpmap("0"), Some("PCMU/8000"));
241 assert_eq!(audio.rtpmap("8"), Some("PCMA/8000"));
242 assert_eq!(audio.direction(), Direction::SendRecv);
243 }
244
245 #[test]
248 fn attributes_attach_to_the_media_line_they_follow() {
249 let sdp = parse(
250 "v=0\r\n\
251 o=- 1 1 IN IP4 192.0.2.1\r\n\
252 s=-\r\n\
253 c=IN IP4 192.0.2.1\r\n\
254 t=0 0\r\n\
255 a=session-level\r\n\
256 m=audio 5000 RTP/AVP 0\r\n\
257 a=sendonly\r\n\
258 m=video 5002 RTP/AVP 96\r\n\
259 a=recvonly\r\n",
260 )
261 .expect("parses");
262
263 assert_eq!(sdp.attributes.len(), 1);
264 assert_eq!(sdp.attributes[0].name, "session-level");
265 assert_eq!(sdp.media[0].direction(), Direction::SendOnly);
266 assert_eq!(sdp.media[1].direction(), Direction::RecvOnly);
267 }
268
269 #[test]
271 fn a_media_connection_overrides_the_session_one() {
272 let sdp = parse(
273 "v=0\r\n\
274 o=- 1 1 IN IP4 192.0.2.1\r\n\
275 s=-\r\n\
276 c=IN IP4 192.0.2.1\r\n\
277 t=0 0\r\n\
278 m=audio 5000 RTP/AVP 0\r\n\
279 c=IN IP4 198.51.100.7\r\n\
280 m=video 5002 RTP/AVP 96\r\n",
281 )
282 .expect("parses");
283
284 assert_eq!(
285 sdp.address_for(&sdp.media[0])
286 .expect("an address")
287 .to_string(),
288 "198.51.100.7"
289 );
290 assert_eq!(
291 sdp.address_for(&sdp.media[1])
292 .expect("an address")
293 .to_string(),
294 "192.0.2.1",
295 "a stream with no c= falls back to the session's"
296 );
297 }
298
299 #[test]
302 fn an_absent_direction_means_sendrecv() {
303 let sdp = parse(
304 "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\n\
305 m=audio 5000 RTP/AVP 0\r\n",
306 )
307 .expect("parses");
308 assert_eq!(sdp.media[0].direction(), Direction::SendRecv);
309 }
310
311 #[test]
313 fn bare_line_feeds_are_accepted() {
314 let sdp = parse(
315 "v=0\no=- 1 1 IN IP4 192.0.2.1\ns=-\nc=IN IP4 192.0.2.1\nt=0 0\nm=audio 5000 RTP/AVP 0\n",
316 )
317 .expect("parses");
318 assert_eq!(sdp.media[0].port, 5000);
319 }
320
321 #[test]
326 fn unknown_lines_survive_a_round_trip() {
327 let input = "v=0\r\n\
328 o=- 1 1 IN IP4 192.0.2.1\r\n\
329 s=-\r\n\
330 c=IN IP4 192.0.2.1\r\n\
331 t=0 0\r\n\
332 b=AS:64\r\n\
333 i=session info\r\n\
334 z=0 0\r\n\
335 m=audio 5000 RTP/AVP 0\r\n";
336 let sdp = parse(input).expect("parses");
337 let out = sdp.to_string_sdp();
338 let position = |needle: &str| {
339 out.find(needle)
340 .unwrap_or_else(|| panic!("{needle} missing from {out}"))
341 };
342 assert!(position("i=session info") < position("c=IN"), "{out}");
343 assert!(position("c=IN") < position("b=AS:64"), "{out}");
344 assert!(position("b=AS:64") < position("t=0 0"), "{out}");
345 assert!(position("t=0 0") < position("z=0 0"), "{out}");
346 assert!(position("z=0 0") < position("m=audio"), "{out}");
347 }
348
349 #[test]
353 fn media_level_lines_stay_with_their_stream() {
354 let sdp = parse(
355 "v=0\r\n\
356 o=- 1 1 IN IP4 192.0.2.1\r\n\
357 s=-\r\n\
358 c=IN IP4 192.0.2.1\r\n\
359 t=0 0\r\n\
360 m=audio 5000 RTP/AVP 0\r\n\
361 b=TIAS:64000\r\n\
362 m=video 5002 RTP/AVP 96\r\n\
363 b=TIAS:256000\r\n",
364 )
365 .expect("parses");
366
367 assert!(sdp.other.is_empty(), "nothing here is session-level");
368
369 let out = sdp.to_string_sdp();
370 let position = |needle: &str| {
371 out.find(needle)
372 .unwrap_or_else(|| panic!("{needle} missing from {out}"))
373 };
374 assert!(position("m=audio") < position("b=TIAS:64000"), "{out}");
375 assert!(position("b=TIAS:64000") < position("m=video"), "{out}");
376 assert!(position("m=video") < position("b=TIAS:256000"), "{out}");
377 }
378
379 #[test]
380 fn a_parsed_description_reserializes_to_the_same_meaning() {
381 let sdp = parse(OFFER).expect("parses");
382 let again = parse(&sdp.to_string_sdp()).expect("reparses");
383 assert_eq!(sdp, again);
384 }
385
386 #[test]
389 fn port_zero_is_a_rejected_stream_not_a_parse_error() {
390 let sdp = parse(
391 "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\n\
392 m=audio 0 RTP/AVP 0\r\n",
393 )
394 .expect("parses");
395 assert!(sdp.media[0].is_rejected());
396 }
397
398 #[test]
402 fn a_media_line_without_formats_is_rejected() {
403 assert!(matches!(
404 parse(
405 "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\n\
406 m=application 49174 udp\r\n",
407 ),
408 Err(SdpError::Invalid {
409 field: "media formats",
410 ..
411 })
412 ));
413 }
414
415 #[test]
416 fn a_line_without_an_equals_sign_is_rejected() {
417 assert!(matches!(
418 parse("v=0\r\nthis is not sdp\r\n"),
419 Err(SdpError::MalformedLine(_))
420 ));
421 }
422
423 #[test]
427 fn an_empty_session_name_becomes_a_dash() {
428 let sdp = parse(
429 "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=\r\nc=IN IP4 192.0.2.1\r\nt=0 0\r\n\
430 m=audio 5000 RTP/AVP 0\r\n",
431 )
432 .expect("parses");
433 assert_eq!(sdp.session_name, "-");
434 assert!(sdp.to_string_sdp().contains("s=-\r\n"));
435 }
436
437 #[test]
438 fn a_description_without_an_origin_is_rejected() {
439 assert!(matches!(
440 parse("v=0\r\ns=-\r\nt=0 0\r\n"),
441 Err(SdpError::Missing("o="))
442 ));
443 }
444
445 #[test]
446 fn ipv6_addresses_round_trip() {
447 let sdp = parse(
448 "v=0\r\no=- 1 1 IN IP6 2001:db8::1\r\ns=-\r\nc=IN IP6 2001:db8::1\r\nt=0 0\r\n\
449 m=audio 5000 RTP/AVP 0\r\n",
450 )
451 .expect("parses");
452 let out = sdp.to_string_sdp();
453 assert!(out.contains("c=IN IP6 2001:db8::1"), "{out}");
454 assert!(out.contains("o=- 1 1 IN IP6 2001:db8::1"), "{out}");
455 }
456
457 #[test]
461 fn a_domain_name_address_is_kept_not_rejected() {
462 let sdp = parse(
463 "v=0\r\n\
464 o=alice 2890844526 2890844526 IN IP4 host.anywhere.com\r\n\
465 s=-\r\n\
466 c=IN IP4 host.anywhere.com\r\n\
467 t=0 0\r\n\
468 m=audio 49170 RTP/AVP 0\r\n\
469 a=rtpmap:0 PCMU/8000\r\n",
470 )
471 .expect("parses");
472
473 assert_eq!(sdp.origin.address.to_string(), "host.anywhere.com");
474 assert_eq!(
475 sdp.address_for(&sdp.media[0]),
476 None,
477 "a name is not an address until someone resolves it, and this crate does no I/O"
478 );
479
480 let out = sdp.to_string_sdp();
481 assert!(
482 out.contains("o=alice 2890844526 2890844526 IN IP4 host.anywhere.com"),
483 "{out}"
484 );
485 assert!(out.contains("c=IN IP4 host.anywhere.com"), "{out}");
486 }
487
488 #[test]
490 fn a_multicast_connection_drops_its_ttl_suffix() {
491 let sdp = parse(
492 "v=0\r\no=- 1 1 IN IP4 192.0.2.1\r\ns=-\r\nc=IN IP4 224.0.1.1/127\r\nt=0 0\r\n\
493 m=audio 5000 RTP/AVP 0\r\n",
494 )
495 .expect("parses");
496 assert_eq!(
497 sdp.connection.expect("a connection").address.to_string(),
498 "224.0.1.1"
499 );
500 }
501}