123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- package plist
- import (
- "encoding/binary"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "runtime"
- "strings"
- "time"
- "unicode/utf16"
- "unicode/utf8"
- )
- type textPlistParser struct {
- reader io.Reader
- format int
- input string
- start int
- pos int
- width int
- }
- func convertU16(buffer []byte, bo binary.ByteOrder) (string, error) {
- if len(buffer)%2 != 0 {
- return "", errors.New("truncated utf16")
- }
- tmp := make([]uint16, len(buffer)/2)
- for i := 0; i < len(buffer); i += 2 {
- tmp[i/2] = bo.Uint16(buffer[i : i+2])
- }
- return string(utf16.Decode(tmp)), nil
- }
- func guessEncodingAndConvert(buffer []byte) (string, error) {
- if len(buffer) >= 3 && buffer[0] == 0xEF && buffer[1] == 0xBB && buffer[2] == 0xBF {
- // UTF-8 BOM
- return zeroCopy8BitString(buffer, 3, len(buffer)-3), nil
- } else if len(buffer) >= 2 {
- // UTF-16 guesses
- switch {
- // stream is big-endian (BOM is FE FF or head is 00 XX)
- case (buffer[0] == 0xFE && buffer[1] == 0xFF):
- return convertU16(buffer[2:], binary.BigEndian)
- case (buffer[0] == 0 && buffer[1] != 0):
- return convertU16(buffer, binary.BigEndian)
- // stream is little-endian (BOM is FE FF or head is XX 00)
- case (buffer[0] == 0xFF && buffer[1] == 0xFE):
- return convertU16(buffer[2:], binary.LittleEndian)
- case (buffer[0] != 0 && buffer[1] == 0):
- return convertU16(buffer, binary.LittleEndian)
- }
- }
- // fallback: assume ASCII (not great!)
- return zeroCopy8BitString(buffer, 0, len(buffer)), nil
- }
- func (p *textPlistParser) parseDocument() (pval cfValue, parseError error) {
- defer func() {
- if r := recover(); r != nil {
- if _, ok := r.(runtime.Error); ok {
- panic(r)
- }
- // Wrap all non-invalid-plist errors.
- parseError = plistParseError{"text", r.(error)}
- }
- }()
- buffer, err := ioutil.ReadAll(p.reader)
- if err != nil {
- panic(err)
- }
- p.input, err = guessEncodingAndConvert(buffer)
- if err != nil {
- panic(err)
- }
- val := p.parsePlistValue()
- p.skipWhitespaceAndComments()
- if p.peek() != eof {
- if _, ok := val.(cfString); !ok {
- p.error("garbage after end of document")
- }
- p.start = 0
- p.pos = 0
- val = p.parseDictionary(true)
- }
- pval = val
- return
- }
- const eof rune = -1
- func (p *textPlistParser) error(e string, args ...interface{}) {
- line := strings.Count(p.input[:p.pos], "\n")
- char := p.pos - strings.LastIndex(p.input[:p.pos], "\n") - 1
- panic(fmt.Errorf("%s at line %d character %d", fmt.Sprintf(e, args...), line, char))
- }
- func (p *textPlistParser) next() rune {
- if int(p.pos) >= len(p.input) {
- p.width = 0
- return eof
- }
- r, w := utf8.DecodeRuneInString(p.input[p.pos:])
- p.width = w
- p.pos += p.width
- return r
- }
- func (p *textPlistParser) backup() {
- p.pos -= p.width
- }
- func (p *textPlistParser) peek() rune {
- r := p.next()
- p.backup()
- return r
- }
- func (p *textPlistParser) emit() string {
- s := p.input[p.start:p.pos]
- p.start = p.pos
- return s
- }
- func (p *textPlistParser) ignore() {
- p.start = p.pos
- }
- func (p *textPlistParser) empty() bool {
- return p.start == p.pos
- }
- func (p *textPlistParser) scanUntil(ch rune) {
- if x := strings.IndexRune(p.input[p.pos:], ch); x >= 0 {
- p.pos += x
- return
- }
- p.pos = len(p.input)
- }
- func (p *textPlistParser) scanUntilAny(chs string) {
- if x := strings.IndexAny(p.input[p.pos:], chs); x >= 0 {
- p.pos += x
- return
- }
- p.pos = len(p.input)
- }
- func (p *textPlistParser) scanCharactersInSet(ch *characterSet) {
- for ch.Contains(p.next()) {
- }
- p.backup()
- }
- func (p *textPlistParser) scanCharactersNotInSet(ch *characterSet) {
- var r rune
- for {
- r = p.next()
- if r == eof || ch.Contains(r) {
- break
- }
- }
- p.backup()
- }
- func (p *textPlistParser) skipWhitespaceAndComments() {
- for {
- p.scanCharactersInSet(&whitespace)
- if strings.HasPrefix(p.input[p.pos:], "//") {
- p.scanCharactersNotInSet(&newlineCharacterSet)
- } else if strings.HasPrefix(p.input[p.pos:], "/*") {
- if x := strings.Index(p.input[p.pos:], "*/"); x >= 0 {
- p.pos += x + 2 // skip the */ as well
- continue // consume more whitespace
- } else {
- p.error("unexpected eof in block comment")
- }
- } else {
- break
- }
- }
- p.ignore()
- }
- func (p *textPlistParser) parseOctalDigits(max int) uint64 {
- var val uint64
- for i := 0; i < max; i++ {
- r := p.next()
- if r >= '0' && r <= '7' {
- val <<= 3
- val |= uint64((r - '0'))
- } else {
- p.backup()
- break
- }
- }
- return val
- }
- func (p *textPlistParser) parseHexDigits(max int) uint64 {
- var val uint64
- for i := 0; i < max; i++ {
- r := p.next()
- if r >= 'a' && r <= 'f' {
- val <<= 4
- val |= 10 + uint64((r - 'a'))
- } else if r >= 'A' && r <= 'F' {
- val <<= 4
- val |= 10 + uint64((r - 'A'))
- } else if r >= '0' && r <= '9' {
- val <<= 4
- val |= uint64((r - '0'))
- } else {
- p.backup()
- break
- }
- }
- return val
- }
- // the \ has already been consumed
- func (p *textPlistParser) parseEscape() string {
- var s string
- switch p.next() {
- case 'a':
- s = "\a"
- case 'b':
- s = "\b"
- case 'v':
- s = "\v"
- case 'f':
- s = "\f"
- case 't':
- s = "\t"
- case 'r':
- s = "\r"
- case 'n':
- s = "\n"
- case '\\':
- s = `\`
- case '"':
- s = `"`
- case 'x':
- s = string(rune(p.parseHexDigits(2)))
- case 'u', 'U':
- s = string(rune(p.parseHexDigits(4)))
- case '0', '1', '2', '3', '4', '5', '6', '7':
- p.backup() // we've already consumed one of the digits
- s = string(rune(p.parseOctalDigits(3)))
- default:
- p.backup() // everything else should be accepted
- }
- p.ignore() // skip the entire escape sequence
- return s
- }
- // the " has already been consumed
- func (p *textPlistParser) parseQuotedString() cfString {
- p.ignore() // ignore the "
- slowPath := false
- s := ""
- for {
- p.scanUntilAny(`"\`)
- switch p.peek() {
- case eof:
- p.error("unexpected eof in quoted string")
- case '"':
- section := p.emit()
- p.pos++ // skip "
- if !slowPath {
- return cfString(section)
- }
- s += section
- return cfString(s)
- case '\\':
- slowPath = true
- s += p.emit()
- p.next() // consume \
- s += p.parseEscape()
- }
- }
- }
- func (p *textPlistParser) parseUnquotedString() cfString {
- p.scanCharactersNotInSet(&gsQuotable)
- s := p.emit()
- if s == "" {
- p.error("invalid unquoted string (found an unquoted character that should be quoted?)")
- }
- return cfString(s)
- }
- // the { has already been consumed
- func (p *textPlistParser) parseDictionary(ignoreEOF bool) *cfDictionary {
- //p.ignore() // ignore the {
- var keypv cfValue
- keys := make([]string, 0, 32)
- values := make([]cfValue, 0, 32)
- outer:
- for {
- p.skipWhitespaceAndComments()
- switch p.next() {
- case eof:
- if !ignoreEOF {
- p.error("unexpected eof in dictionary")
- }
- fallthrough
- case '}':
- break outer
- case '"':
- keypv = p.parseQuotedString()
- default:
- p.backup()
- keypv = p.parseUnquotedString()
- }
- // INVARIANT: key can't be nil; parseQuoted and parseUnquoted
- // will panic out before they return nil.
- p.skipWhitespaceAndComments()
- var val cfValue
- n := p.next()
- if n == ';' {
- val = keypv
- } else if n == '=' {
- // whitespace is consumed within
- val = p.parsePlistValue()
- p.skipWhitespaceAndComments()
- if p.next() != ';' {
- p.error("missing ; in dictionary")
- }
- } else {
- p.error("missing = in dictionary")
- }
- keys = append(keys, string(keypv.(cfString)))
- values = append(values, val)
- }
- return &cfDictionary{keys: keys, values: values}
- }
- // the ( has already been consumed
- func (p *textPlistParser) parseArray() *cfArray {
- //p.ignore() // ignore the (
- values := make([]cfValue, 0, 32)
- outer:
- for {
- p.skipWhitespaceAndComments()
- switch p.next() {
- case eof:
- p.error("unexpected eof in array")
- case ')':
- break outer // done here
- case ',':
- continue // restart; ,) is valid and we don't want to blow it
- default:
- p.backup()
- }
- pval := p.parsePlistValue() // whitespace is consumed within
- if str, ok := pval.(cfString); ok && string(str) == "" {
- // Empty strings in arrays are apparently skipped?
- // TODO: Figure out why this was implemented.
- continue
- }
- values = append(values, pval)
- }
- return &cfArray{values}
- }
- // the <* have already been consumed
- func (p *textPlistParser) parseGNUStepValue() cfValue {
- typ := p.next()
- p.ignore()
- p.scanUntil('>')
- if typ == eof || typ == '>' || p.empty() || p.peek() == eof {
- p.error("invalid GNUStep extended value")
- }
- v := p.emit()
- p.next() // consume the >
- switch typ {
- case 'I':
- if v[0] == '-' {
- n := mustParseInt(v, 10, 64)
- return &cfNumber{signed: true, value: uint64(n)}
- }
- n := mustParseUint(v, 10, 64)
- return &cfNumber{signed: false, value: n}
- case 'R':
- n := mustParseFloat(v, 64)
- return &cfReal{wide: true, value: n} // TODO(DH) 32/64
- case 'B':
- b := v[0] == 'Y'
- return cfBoolean(b)
- case 'D':
- t, err := time.Parse(textPlistTimeLayout, v)
- if err != nil {
- p.error(err.Error())
- }
- return cfDate(t.In(time.UTC))
- }
- p.error("invalid GNUStep type " + string(typ))
- return nil
- }
- // The < has already been consumed
- func (p *textPlistParser) parseHexData() cfData {
- buf := make([]byte, 256)
- i := 0
- c := 0
- for {
- r := p.next()
- switch r {
- case eof:
- p.error("unexpected eof in data")
- case '>':
- if c&1 == 1 {
- p.error("uneven number of hex digits in data")
- }
- p.ignore()
- return cfData(buf[:i])
- case ' ', '\t', '\n', '\r', '\u2028', '\u2029': // more lax than apple here: skip spaces
- continue
- }
- buf[i] <<= 4
- if r >= 'a' && r <= 'f' {
- buf[i] |= 10 + byte((r - 'a'))
- } else if r >= 'A' && r <= 'F' {
- buf[i] |= 10 + byte((r - 'A'))
- } else if r >= '0' && r <= '9' {
- buf[i] |= byte((r - '0'))
- } else {
- p.error("unexpected hex digit `%c'", r)
- }
- c++
- if c&1 == 0 {
- i++
- if i >= len(buf) {
- realloc := make([]byte, len(buf)*2)
- copy(realloc, buf)
- buf = realloc
- }
- }
- }
- }
- func (p *textPlistParser) parsePlistValue() cfValue {
- for {
- p.skipWhitespaceAndComments()
- switch p.next() {
- case eof:
- return &cfDictionary{}
- case '<':
- if p.next() == '*' {
- p.format = GNUStepFormat
- return p.parseGNUStepValue()
- }
- p.backup()
- return p.parseHexData()
- case '"':
- return p.parseQuotedString()
- case '{':
- return p.parseDictionary(false)
- case '(':
- return p.parseArray()
- default:
- p.backup()
- return p.parseUnquotedString()
- }
- }
- }
- func newTextPlistParser(r io.Reader) *textPlistParser {
- return &textPlistParser{
- reader: r,
- format: OpenStepFormat,
- }
- }
|