func (p *textPlistParser) parseDictionary(ignoreEof bool) cfValue {
	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()
		}
		p.skipWhitespaceAndComments()
		var val cfValue
		n := p.next()
		if n == ';' {
			val = keypv
		} else if n == '=' {
			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)
	}
	dict := &cfDictionary{keys: keys, values: values}
	return dict.maybeUID(p.format == OpenStepFormat)
}
the { has already been consumed
package plist
import (
	"encoding/base64"
	"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 {
		return zeroCopy8BitString(buffer, 3, len(buffer)-3), nil
	} else if len(buffer) >= 2 {
		switch {
		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)
		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)
		}
	}
	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)
			}
			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  
				continue        
			} 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
}
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': // This is our extension.
		s = string(rune(p.parseHexDigits(2)))
	case 'u', 'U': // 'u' is a GNUstep extension.
		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()  
	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)
			} else {
				s += section
				return cfString(s)
			}
		case '\\':
			slowPath = true
			s += p.emit()
			p.next()  
			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)
}