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 }