commit 439c6395b12db4f7329a94e472639a5b55169cf0
parent baedec4c102a6e02db8236e10c2733080f2ba3e2
Author: Oliver Lowe <o@olowe.co>
Date: Fri, 5 Jul 2024 16:51:16 +1000
sdp: manage parsing state in a parser struct
The ReadSession and readSession functions were getting pretty long,
with break labels and even a goto. Managing the state in the parser
obviates that stuff. It also let us deduplicate much of the scanning
logic e.g. checking for empty/invalid lines.
Diffstat:
| A | sdp/parser.go | | | 185 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| M | sdp/sdp.go | | | 187 | +++---------------------------------------------------------------------------- |
2 files changed, 190 insertions(+), 182 deletions(-)
diff --git a/sdp/parser.go b/sdp/parser.go
@@ -0,0 +1,184 @@
+package sdp
+
+import (
+ "bufio"
+ "fmt"
+ "net/url"
+ "strconv"
+ "strings"
+)
+
+type parser struct {
+ *bufio.Scanner
+ err error
+ // Field name and value from the current line.
+ // TODO(otl): rename? key, value is very non-specific...
+ key, value string
+ next []string // expected next field names
+
+ session Session
+}
+
+var ftab = [...]string{"i", "u", "e", "p", "c", "b", "t", "r", "z", "m", "a"}
+
+var mtab = [...]string{"i", "c", "b", "a", "m"}
+
+func (p *parser) scan() bool {
+ if !p.Scan() {
+ p.err = p.Err()
+ return false
+ }
+ line := strings.TrimSpace(p.Text())
+ if line == "" {
+ p.err = fmt.Errorf("illegal empty line")
+ return false
+ }
+ k, v, found := strings.Cut(line, "=")
+ if !found {
+ p.err = fmt.Errorf("parse field %q, missing %q", k, "=")
+ return false
+ }
+ p.key = k
+ p.value = v
+
+ if p.next != nil {
+ for i := range p.next {
+ if p.next[i] == p.key {
+ return true
+ }
+ }
+ p.err = fmt.Errorf("unexpected field %q: expected one of %q", k, p.next)
+ return false
+ }
+ return true
+}
+
+func (p *parser) parse() error {
+ next := "v"
+ for p.scan() {
+ if p.key != next {
+ return fmt.Errorf("expected key %q, found %q", next, p.key)
+ }
+ switch p.key {
+ case "v":
+ i, err := strconv.Atoi(p.value)
+ if err != nil {
+ return fmt.Errorf("parse version: %w", err)
+ }
+ if i != 0 {
+ return fmt.Errorf("unsupported version %d", i)
+ }
+ next = "o"
+ case "o":
+ o, err := parseOrigin(p.value)
+ if err != nil {
+ return fmt.Errorf("parse origin: %w", err)
+ }
+ p.session.Origin = o
+ next = "s"
+ case "s":
+ if p.value == "" {
+ return fmt.Errorf("empty name")
+ }
+ p.session.Name = p.value
+ return p.parseOptional()
+ }
+ }
+ return p.err
+}
+
+func (p *parser) parseOptional() error {
+ p.next = ftab[:]
+ for p.scan() {
+ switch p.key {
+ case "i":
+ p.session.Info = p.value
+ p.next = ftab[1:]
+ case "u":
+ u, err := url.Parse(p.value)
+ if err != nil {
+ return fmt.Errorf("parse uri: %w", err)
+ }
+ p.session.URI = u
+ p.next = ftab[2:]
+ case "e":
+ addr, err := parseEmail(p.value)
+ if err != nil {
+ return fmt.Errorf("parse email: %w", err)
+ }
+ p.session.Email = addr
+ p.next = ftab[3:]
+ case "p":
+ p.session.Phone = cleanPhone(p.value)
+ p.next = ftab[4:]
+ case "c":
+ conn, err := parseConnInfo(p.value)
+ if err != nil {
+ return fmt.Errorf("parse connection info: %w", err)
+ }
+ p.session.Connection = &conn
+ p.next = ftab[5:]
+ case "b":
+ bw, err := parseBandwidth(p.value)
+ if err != nil {
+ return fmt.Errorf("parse bandwidth line %q: %w", p.value, err)
+ }
+ p.session.Bandwidth = &bw
+ p.next = ftab[6:]
+ case "a":
+ p.session.Attributes = strings.Fields(p.value)
+ p.next = ftab[7:]
+ case "m":
+ m, err := parseMedia(p.value)
+ if err != nil {
+ return fmt.Errorf("parse media info from %q: %w", p.value, err)
+ }
+ p.session.Media = append(p.session.Media, m)
+ p.next = mtab[:]
+ return p.parseMedia()
+ }
+ }
+ return p.err
+}
+
+func (p *parser) parseMedia() error {
+ var media *Media
+ if len(p.session.Media) > 0 {
+ media = &p.session.Media[len(p.session.Media)-1]
+ }
+ for p.scan() {
+ switch p.key {
+ case "i":
+ media.Title = p.value
+ p.next = mtab[1:]
+ case "c":
+ conn, err := parseConnInfo(p.value)
+ if err != nil {
+ return fmt.Errorf("parse connection info: %w", err)
+ }
+ media.Connection = &conn
+ p.next = mtab[2:]
+ case "b":
+ bw, err := parseBandwidth(p.value)
+ if err != nil {
+ return fmt.Errorf("parse bandwidth: %w", err)
+ }
+ media.Bandwidth = &bw
+ p.next = mtab[3:]
+ case "a":
+ media.Attributes = strings.Fields(p.value)
+ p.next = mtab[4:]
+ case "m":
+ m, err := parseMedia(p.value)
+ if err != nil {
+ return fmt.Errorf("parse media description: %w", err)
+ }
+ p.session.Media = append(p.session.Media, m)
+ media = &p.session.Media[len(p.session.Media)-1]
+ p.next = mtab[:]
+ default:
+ return fmt.Errorf("unsupported field char %s", p.key)
+ }
+ }
+ return p.err
+}
+\ No newline at end of file
diff --git a/sdp/sdp.go b/sdp/sdp.go
@@ -23,8 +23,8 @@ type Session struct {
Phone string
Connection *ConnInfo
Bandwidth *Bandwidth
- Media []Media
Attributes []string
+ Media []Media
// TODO(otl): add rest of fields
}
@@ -36,189 +36,12 @@ type Origin struct {
Address string // IPv4, IPv6 literal or a hostname
}
-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)
- if err != nil {
- return nil, err
- }
-
- // Time for optional fields. We keep a slice...
- // TODO(otl): document in plain language what's going on here.
- next := fchars[:]
- 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":
- session.Info = v
- next = fchars[1:]
- case "u":
- u, err := url.Parse(v)
- if err != nil {
- return nil, fmt.Errorf("parse uri: %w", err)
- }
- session.URI = u
- next = fchars[2:]
- case "e":
- addr, err := parseEmail(v)
- if err != nil {
- return nil, fmt.Errorf("parse email: %w", err)
- }
- session.Email = addr
- next = fchars[3:]
- case "p":
- session.Phone = cleanPhone(v)
- 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
- 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()
-}
-
-func readSession(r io.Reader) (*Session, *bufio.Scanner, error) {
- sc := bufio.NewScanner(r)
- var session Session
- next := "v"
-Loop:
- for sc.Scan() {
- if strings.TrimSpace(sc.Text()) == "" {
- return nil, nil, fmt.Errorf("illegal empty line")
- }
- k, v, found := strings.Cut(sc.Text(), "=")
- if !found {
- return nil, nil, fmt.Errorf("parse field %q: missing %q", next, "=")
- }
- if k != next {
- return nil, nil, fmt.Errorf("expected field %q, found %q", next, k)
- }
- switch k {
- case "v":
- i, err := strconv.Atoi(v)
- if err != nil {
- return nil, nil, fmt.Errorf("parse version: %w", err)
- }
- if i != 0 {
- return nil, nil, fmt.Errorf("unsupported version %d", i)
- }
- next = "o"
- case "o":
- o, err := parseOrigin(v)
- if err != nil {
- return nil, nil, fmt.Errorf("parse origin: %w", err)
- }
- session.Origin = o
- next = "s"
- case "s":
- if v == "" {
- return nil, nil, fmt.Errorf("empty name")
- }
- session.Name = v
- break Loop
- }
+ parser := &parser{Scanner: bufio.NewScanner(rd)}
+ if err := parser.parse(); err != nil {
+ return nil, fmt.Errorf("parse session: %w", err)
}
- return &session, sc, sc.Err()
+ return &parser.session, nil
}
// cleanPhone returns the phone number in s stripped of "-" and space