streaming

Media streaming and broadcast systems in Go
Log | Files | Refs | README | LICENSE

lex.go (6247B)


      1 package m3u8
      2 
      3 import (
      4 	"bufio"
      5 	"fmt"
      6 	"io"
      7 	"os"
      8 	"strings"
      9 	"unicode"
     10 	"unicode/utf8"
     11 )
     12 
     13 type item struct {
     14 	typ itemType
     15 	val string
     16 }
     17 
     18 func (it item) String() string {
     19 	if it.typ == itemNewline {
     20 		return "newline"
     21 	}
     22 	return fmt.Sprintf("%s: %s", it.typ, it.val)
     23 }
     24 
     25 type itemType int
     26 
     27 const (
     28 	itemError itemType = iota
     29 	itemTag
     30 	itemAttrName
     31 	itemEquals
     32 	itemNumber
     33 	itemString
     34 	itemComma
     35 	itemURL
     36 	itemNewline
     37 	itemEOF
     38 )
     39 
     40 func (t itemType) String() string {
     41 	switch t {
     42 	case itemError:
     43 		return "error"
     44 	case itemTag:
     45 		return "tag"
     46 	case itemAttrName:
     47 		return "attribute name"
     48 	case itemEquals:
     49 		return "equals"
     50 	case itemNumber:
     51 		return "number"
     52 	case itemString:
     53 		return "string"
     54 	case itemComma:
     55 		return "comma"
     56 	case itemURL:
     57 		return "url"
     58 	case itemNewline:
     59 		return "newline"
     60 	case itemEOF:
     61 		return "EOF"
     62 	}
     63 	return "unknown item type"
     64 }
     65 
     66 const tagStart = "#EXT"
     67 
     68 // A lexer... TODO
     69 // The design is described in "Lexical Scanning in Go" by Rob Pike:
     70 // https://www.youtube.com/watch?v=HxaD_trXwRE
     71 type lexer struct {
     72 	sc    *bufio.Scanner
     73 	input string
     74 	start int
     75 	pos   int
     76 	width int
     77 	items chan item
     78 
     79 	// if enabled, emitted items are printed to standard error.
     80 	debug bool
     81 }
     82 
     83 type stateFn func(*lexer) stateFn
     84 
     85 func (l *lexer) next() (r rune) {
     86 	if l.pos >= len(l.input) {
     87 		return -1
     88 	}
     89 	r, l.width = utf8.DecodeRuneInString(l.input[l.pos:])
     90 	l.pos += l.width
     91 	return r
     92 }
     93 
     94 // ignore skips the current rune.
     95 func (l *lexer) ignore() { l.start = l.pos }
     96 
     97 // backup steps the lexer back one rune.
     98 func (l *lexer) backup() { l.pos -= l.width }
     99 
    100 func (l *lexer) peek() rune {
    101 	r := l.next()
    102 	l.backup()
    103 	return r
    104 }
    105 
    106 func (l *lexer) errorf(format string, a ...any) stateFn {
    107 	err := fmt.Sprintf(format, a...)
    108 	l.items <- item{itemError, err}
    109 	return nil
    110 }
    111 
    112 func (l *lexer) run() {
    113 	for state := lexStart; state != nil; {
    114 		state = state(l)
    115 	}
    116 	close(l.items)
    117 }
    118 
    119 func (l *lexer) emit(t itemType) {
    120 	l.items <- item{t, l.input[l.start:l.pos]}
    121 	if l.debug {
    122 		fmt.Fprintln(os.Stderr, item{t, l.input[l.start:l.pos]})
    123 	}
    124 	l.start = l.pos
    125 }
    126 
    127 func newLexer(r io.Reader) *lexer {
    128 	return &lexer{
    129 		sc:    bufio.NewScanner(r),
    130 		items: make(chan item),
    131 		debug: false,
    132 	}
    133 }
    134 
    135 func lexStart(l *lexer) stateFn {
    136 	for l.sc.Scan() {
    137 		if l.sc.Text() == "" {
    138 			continue // ignore blank lines
    139 		}
    140 		text := l.sc.Text()
    141 		// ignore valid but meaningless trailing commas.
    142 		text = strings.TrimSuffix(text, ",")
    143 		l.input = text + "\n"
    144 		l.pos = 0
    145 		l.start = 0
    146 		if strings.HasPrefix(l.input, tagStart) {
    147 			return lexTag(l)
    148 		} else if strings.HasPrefix(l.input, "#") {
    149 			continue // ignore comments
    150 		}
    151 		// not a tag, so must be a URL.
    152 		// emit the URL, then the newline we appended ourselves.
    153 		l.pos = len(text)
    154 		l.emit(itemURL)
    155 		l.emit(itemNewline)
    156 	}
    157 	if err := l.sc.Err(); err != nil {
    158 		panic(err)
    159 	}
    160 	return nil
    161 }
    162 
    163 func lexTag(l *lexer) stateFn {
    164 	r := l.next()
    165 	if r != '#' {
    166 		return l.errorf("missing starting #")
    167 	}
    168 	return lexTagName(l)
    169 }
    170 
    171 func lexTagName(l *lexer) stateFn {
    172 	for {
    173 		r := l.peek()
    174 		if isTagNameChar(r) {
    175 			l.next()
    176 			continue
    177 		}
    178 		switch r {
    179 		case '\n':
    180 			l.emit(itemTag)
    181 			l.next()
    182 			l.emit(itemNewline)
    183 			return lexStart(l)
    184 		case ':':
    185 			l.emit(itemTag)
    186 			l.next()
    187 			l.ignore() // ignore ':' after tag name
    188 			return lexAttrs(l)
    189 		}
    190 		return l.errorf("illegal tag character %q", r)
    191 	}
    192 }
    193 
    194 func isTagNameChar(r rune) bool {
    195 	if r >= 'A' && r <= 'Z' {
    196 		return true
    197 	}
    198 	switch r {
    199 	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
    200 		return true
    201 	case '-':
    202 		return true
    203 	}
    204 	return false
    205 }
    206 
    207 func lexAttrs(l *lexer) stateFn {
    208 	for {
    209 		switch r := l.peek(); {
    210 		case isTagNameChar(r):
    211 			l.next()
    212 			continue
    213 		case r == '\n':
    214 			if len(l.input[l.start:l.pos]) != 0 {
    215 				l.emit(itemAttrName)
    216 			}
    217 			l.next()
    218 			l.emit(itemNewline)
    219 			return lexStart(l)
    220 		case r == '=':
    221 			l.emit(itemAttrName)
    222 			l.next()
    223 			l.emit(itemEquals)
    224 			return lexAttrValue(l)
    225 		case r == ',':
    226 			l.next()
    227 			l.emit(itemComma)
    228 			return lexAttrs(l)
    229 		case r == '.':
    230 			return lexAttrValue(l)
    231 		case r == '@':
    232 			return lexAttrValue(l)
    233 		case r == ':':
    234 			return lexAttrValue(l)
    235 		case r == '"':
    236 			l.next()
    237 			return lexQString(l)
    238 		default:
    239 			return l.errorf("illegal character %q in attribute name", r)
    240 		}
    241 	}
    242 }
    243 
    244 func lexAttrValue(l *lexer) stateFn {
    245 	r := l.next()
    246 	switch r {
    247 	case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', ':':
    248 		return lexNumber(l)
    249 	case '"':
    250 		return lexQString(l)
    251 	}
    252 	if isTagNameChar(r) {
    253 		return lexRawString(l)
    254 	}
    255 	return l.errorf("unquoted string starting with illegal character %q", r)
    256 }
    257 
    258 func lexNumber(l *lexer) stateFn {
    259 Loop:
    260 	for {
    261 		switch r := l.peek(); r {
    262 		case 'x', '@':
    263 			// are we lexing a resolution? e.g. 640x480
    264 			// or a byte range? e.g. 69@3000
    265 			return lexRawString(l)
    266 		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.':
    267 			l.next()
    268 			continue
    269 		case ':', 'T', 'Z':
    270 			// could be a RFC 3339 timestamp
    271 			l.next()
    272 			if !unicode.IsDigit(l.peek()) {
    273 				return l.errorf("expected digit after timestamp character %c, got %c", r, l.peek())
    274 			}
    275 			return lexRawString(l)
    276 		default:
    277 			l.emit(itemNumber)
    278 			break Loop
    279 		}
    280 	}
    281 
    282 	// Hack to lex segment titles. A title can be any UTF-8 text until a newline!
    283 	// Titles are an exception since they don't follow any of the
    284 	// other rules for text in HLS tags. Since titles can only be
    285 	// placed after the segment duration we jam our lexing workaround here.
    286 	// The lexer is too clever/complicated and I'm not smart enough to handle anything smarter.
    287 	// TODO(otl): good case for using a more basic line-by-line parser
    288 	if strings.HasPrefix(l.input, tagSegmentDuration) {
    289 		if l.peek() == ',' {
    290 			l.next()
    291 			l.emit(itemComma)
    292 			return lexSegmentTitle(l)
    293 		}
    294 	}
    295 	return lexAttrs(l)
    296 }
    297 
    298 
    299 func lexQString(l *lexer) stateFn {
    300 	for {
    301 		r := l.next()
    302 		if r == '"' {
    303 			l.emit(itemString)
    304 			return lexAttrs(l)
    305 		} else if r == '\n' {
    306 			return l.errorf("unterminated quoted string")
    307 		}
    308 	}
    309 }
    310 
    311 func lexRawString(l *lexer) stateFn {
    312 	for {
    313 		if l.peek() == ',' || l.peek() == '\n' {
    314 			break
    315 		}
    316 		l.next()
    317 	}
    318 	l.emit(itemString)
    319 	return lexAttrs(l)
    320 }
    321 
    322 func lexSegmentTitle(l *lexer) stateFn {
    323 	for {
    324 		if l.peek() == '\n' {
    325 			break
    326 		}
    327 		l.next()
    328 	}
    329 	l.emit(itemString)
    330 	return lexStart(l)
    331 }