From baedec4c102a6e02db8236e10c2733080f2ba3e2 Mon Sep 17 00:00:00 2001 From: Oliver Lowe Date: Fri, 5 Jul 2024 15:41:47 +1000 Subject: [PATCH] sdp: Parse option media description fields The parsing logic has become pretty tricky! Tests pass, so refactoring can be done pretty safely as long as we still get the Session struct out that we expect. --- sdp/sdp.go | 111 +++++++++++++++++++++++++++++++++++++++++------- sdp/sdp_test.go | 28 ++++++++++-- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/sdp/sdp.go b/sdp/sdp.go index 937b86e..58f39ff 100644 --- a/sdp/sdp.go +++ b/sdp/sdp.go @@ -17,12 +17,14 @@ type Session struct { Origin Origin Name string - Info string - URI *url.URL - Email *mail.Address - Phone string - Bandwidth *Bandwidth - Media []Media + Info string + URI *url.URL + Email *mail.Address + Phone string + Connection *ConnInfo + Bandwidth *Bandwidth + Media []Media + Attributes []string // TODO(otl): add rest of fields } @@ -34,7 +36,9 @@ type Origin struct { Address string // IPv4, IPv6 literal or a hostname } -var fchars = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m"} +var fchars = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m", "a"} + +var mchars = [...]string{"i", "c", "b", "a", "m"} func ReadSession(rd io.Reader) (*Session, error) { session, sc, err := readSession(rd) @@ -44,7 +48,7 @@ func ReadSession(rd io.Reader) (*Session, error) { // Time for optional fields. We keep a slice... // TODO(otl): document in plain language what's going on here. - onext := fchars[:] + next := fchars[:] for sc.Scan() { if sc.Text() == "" { return nil, fmt.Errorf("illegal empty line") @@ -55,52 +59,121 @@ func ReadSession(rd io.Reader) (*Session, error) { } var allowed bool - for i := range onext { - if onext[i] == k { + for i := range next { + if next[i] == k { allowed = true } } if !allowed { - return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, onext) + return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, next) } switch k { case "i": session.Info = v - onext = fchars[1:] + next = fchars[1:] case "u": u, err := url.Parse(v) if err != nil { return nil, fmt.Errorf("parse uri: %w", err) } session.URI = u - onext = fchars[2:] + next = fchars[2:] case "e": addr, err := parseEmail(v) if err != nil { return nil, fmt.Errorf("parse email: %w", err) } session.Email = addr - onext = fchars[3:] + next = fchars[3:] case "p": session.Phone = cleanPhone(v) - onext = fchars[4:] + next = fchars[4:] + case "c": + conn, err := parseConnInfo(v) + if err != nil { + return nil, fmt.Errorf("parse connection info: %w", err) + } + session.Connection = &conn + next = fchars[5:] case "b": bw, err := parseBandwidth(v) if err != nil { return nil, fmt.Errorf("parse bandwidth line %q: %w", v, err) } session.Bandwidth = &bw - onext = fchars[5:] + next = fchars[6:] + case "a": + session.Attributes = strings.Fields(v) + next = fchars[7:] case "m": m, err := parseMedia(v) if err != nil { return nil, fmt.Errorf("parse media info from %q: %w", v, err) } session.Media = append(session.Media, m) + next = mchars[:] + goto Media } } +Media: + var media *Media + if len(session.Media) > 0 { + media = &session.Media[len(session.Media)-1] + } + for sc.Scan() { + if sc.Text() == "" { + return nil, fmt.Errorf("illegal empty line") + } + k, v, found := strings.Cut(sc.Text(), "=") + if !found { + return nil, fmt.Errorf("parse field %q: missing %q", k, "=") + } + + var allowed bool + for i := range next { + if next[i] == k { + allowed = true + } + } + if !allowed { + return nil, fmt.Errorf("unexpected field %q: expected one of %q", k, next) + } + + switch k { + case "i": + media.Title = v + next = mchars[1:] + case "c": + conn, err := parseConnInfo(v) + if err != nil { + return nil, fmt.Errorf("parse connection info: %w", err) + } + media.Connection = &conn + next = mchars[2:] + case "b": + bw, err := parseBandwidth(v) + if err != nil { + return nil, fmt.Errorf("parse bandwidth: %w", err) + } + media.Bandwidth = &bw + next = mchars[3:] + case "a": + media.Attributes = strings.Fields(v) + next = mchars[4:] + case "m": + m, err := parseMedia(v) + if err != nil { + return nil, fmt.Errorf("parse media description: %w", err) + } + session.Media = append(session.Media, m) + media = &session.Media[len(session.Media)-1] + next = mchars[:] + default: + return nil, fmt.Errorf("unsupported field char %s", k) + } + } return session, sc.Err() } @@ -227,6 +300,12 @@ type Media struct { PortCount int Protocol uint8 Format []string + // Optional fields + Title string + Connection *ConnInfo + Bandwidth *Bandwidth + // TODO(otl): store as k, v pairs + Attributes []string } const ( diff --git a/sdp/sdp_test.go b/sdp/sdp_test.go index 0d86785..ebb1ae8 100644 --- a/sdp/sdp_test.go +++ b/sdp/sdp_test.go @@ -27,10 +27,31 @@ func TestReadSession(t *testing.T) { }, Email: &mail.Address{"Jane Doe", "jane@jdoe.example.com"}, Phone: "+16175556011", + Connection: &ConnInfo{ + Type: "IP4", + Address: "198.51.100.1", + }, Media: []Media{ - {"audio", 49170, 0, ProtoRTP, []string{"0"}}, - {"audio", 49180, 0, ProtoRTP, []string{"0"}}, - {"video", 51372, 0, ProtoRTP, []string{"99"}}, + Media{ + Type: "audio", + Port: 49170, + Protocol: ProtoRTP, + Format: []string{"0"}, + }, + Media{ + Type: "audio", + Port: 49180, + Protocol: ProtoRTP, + Format: []string{"0"}, + }, + Media{ + Type: "video", + Port: 51372, + Protocol: ProtoRTP, + Format: []string{"99"}, + Connection: &ConnInfo{"IP6", "2001:db8::2", 0, 0}, + Attributes: []string{"rtpmap:99", "h263-1998/90000"}, + }, }, }, }, @@ -71,7 +92,6 @@ func TestReadSession(t *testing.T) { if !reflect.DeepEqual(*session, tt.want) { t.Errorf("got %+v\nwant %+v\n", *session, tt.want) } - t.Errorf("TODO still not parsing all fields") }) } }