481 lines
9.8 KiB
Go
481 lines
9.8 KiB
Go
package hjson
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
)
|
|
|
|
type hjsonParser struct {
|
|
data []byte
|
|
at int // The index of the current character
|
|
ch byte // The current character
|
|
}
|
|
|
|
func (p *hjsonParser) resetAt() {
|
|
p.at = 0
|
|
p.ch = ' '
|
|
}
|
|
|
|
func isPunctuatorChar(c byte) bool {
|
|
return c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':'
|
|
}
|
|
|
|
func (p *hjsonParser) errAt(message string) error {
|
|
var i int
|
|
col := 0
|
|
line := 1
|
|
for i = p.at - 1; i > 0 && p.data[i] != '\n'; i-- {
|
|
col++
|
|
}
|
|
for ; i > 0; i-- {
|
|
if p.data[i] == '\n' {
|
|
line++
|
|
}
|
|
}
|
|
samEnd := p.at - col + 20
|
|
if samEnd > len(p.data) {
|
|
samEnd = len(p.data)
|
|
}
|
|
return fmt.Errorf("%s at line %d,%d >>> %s", message, line, col, string(p.data[p.at-col:samEnd]))
|
|
}
|
|
|
|
func (p *hjsonParser) next() bool {
|
|
// get the next character.
|
|
if p.at < len(p.data) {
|
|
p.ch = p.data[p.at]
|
|
p.at++
|
|
return true
|
|
}
|
|
p.ch = 0
|
|
return false
|
|
}
|
|
|
|
func (p *hjsonParser) peek(offs int) byte {
|
|
pos := p.at + offs
|
|
if pos >= 0 && pos < len(p.data) {
|
|
return p.data[p.at+offs]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
var escapee = map[byte]byte{
|
|
'"': '"',
|
|
'\'': '\'',
|
|
'\\': '\\',
|
|
'/': '/',
|
|
'b': '\b',
|
|
'f': '\f',
|
|
'n': '\n',
|
|
'r': '\r',
|
|
't': '\t',
|
|
}
|
|
|
|
func (p *hjsonParser) readString(allowML bool) (string, error) {
|
|
|
|
// Parse a string value.
|
|
res := new(bytes.Buffer)
|
|
|
|
// callers make sure that (ch === '"' || ch === "'")
|
|
// When parsing for string values, we must look for " and \ characters.
|
|
exitCh := p.ch
|
|
for p.next() {
|
|
if p.ch == exitCh {
|
|
p.next()
|
|
if allowML && exitCh == '\'' && p.ch == '\'' && res.Len() == 0 {
|
|
// ''' indicates a multiline string
|
|
p.next()
|
|
return p.readMLString()
|
|
} else {
|
|
return res.String(), nil
|
|
}
|
|
}
|
|
if p.ch == '\\' {
|
|
p.next()
|
|
if p.ch == 'u' {
|
|
uffff := 0
|
|
for i := 0; i < 4; i++ {
|
|
p.next()
|
|
var hex int
|
|
if p.ch >= '0' && p.ch <= '9' {
|
|
hex = int(p.ch - '0')
|
|
} else if p.ch >= 'a' && p.ch <= 'f' {
|
|
hex = int(p.ch - 'a' + 0xa)
|
|
} else if p.ch >= 'A' && p.ch <= 'F' {
|
|
hex = int(p.ch - 'A' + 0xa)
|
|
} else {
|
|
return "", p.errAt("Bad \\u char " + string(p.ch))
|
|
}
|
|
uffff = uffff*16 + hex
|
|
}
|
|
res.WriteRune(rune(uffff))
|
|
} else if ech, ok := escapee[p.ch]; ok {
|
|
res.WriteByte(ech)
|
|
} else {
|
|
return "", p.errAt("Bad escape \\" + string(p.ch))
|
|
}
|
|
} else if p.ch == '\n' || p.ch == '\r' {
|
|
return "", p.errAt("Bad string containing newline")
|
|
} else {
|
|
res.WriteByte(p.ch)
|
|
}
|
|
}
|
|
return "", p.errAt("Bad string")
|
|
}
|
|
|
|
func (p *hjsonParser) readMLString() (value string, err error) {
|
|
|
|
// Parse a multiline string value.
|
|
res := new(bytes.Buffer)
|
|
triple := 0
|
|
|
|
// we are at ''' +1 - get indent
|
|
indent := 0
|
|
for {
|
|
c := p.peek(-indent - 5)
|
|
if c == 0 || c == '\n' {
|
|
break
|
|
}
|
|
indent++
|
|
}
|
|
|
|
skipIndent := func() {
|
|
skip := indent
|
|
for p.ch > 0 && p.ch <= ' ' && p.ch != '\n' && skip > 0 {
|
|
skip--
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
// skip white/to (newline)
|
|
for p.ch > 0 && p.ch <= ' ' && p.ch != '\n' {
|
|
p.next()
|
|
}
|
|
if p.ch == '\n' {
|
|
p.next()
|
|
skipIndent()
|
|
}
|
|
|
|
// When parsing multiline string values, we must look for ' characters.
|
|
lastLf := false
|
|
for {
|
|
if p.ch == 0 {
|
|
return "", p.errAt("Bad multiline string")
|
|
} else if p.ch == '\'' {
|
|
triple++
|
|
p.next()
|
|
if triple == 3 {
|
|
sres := res.Bytes()
|
|
if lastLf {
|
|
return string(sres[0 : len(sres)-1]), nil // remove last EOL
|
|
}
|
|
return string(sres), nil
|
|
}
|
|
continue
|
|
} else {
|
|
for triple > 0 {
|
|
res.WriteByte('\'')
|
|
triple--
|
|
lastLf = false
|
|
}
|
|
}
|
|
if p.ch == '\n' {
|
|
res.WriteByte('\n')
|
|
lastLf = true
|
|
p.next()
|
|
skipIndent()
|
|
} else {
|
|
if p.ch != '\r' {
|
|
res.WriteByte(p.ch)
|
|
lastLf = false
|
|
}
|
|
p.next()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *hjsonParser) readKeyname() (string, error) {
|
|
|
|
// quotes for keys are optional in Hjson
|
|
// unless they include {}[],: or whitespace.
|
|
|
|
if p.ch == '"' || p.ch == '\'' {
|
|
return p.readString(false)
|
|
}
|
|
|
|
name := new(bytes.Buffer)
|
|
start := p.at
|
|
space := -1
|
|
for {
|
|
if p.ch == ':' {
|
|
if name.Len() == 0 {
|
|
return "", p.errAt("Found ':' but no key name (for an empty key name use quotes)")
|
|
} else if space >= 0 && space != name.Len() {
|
|
p.at = start + space
|
|
return "", p.errAt("Found whitespace in your key name (use quotes to include)")
|
|
}
|
|
return name.String(), nil
|
|
} else if p.ch <= ' ' {
|
|
if p.ch == 0 {
|
|
return "", p.errAt("Found EOF while looking for a key name (check your syntax)")
|
|
}
|
|
if space < 0 {
|
|
space = name.Len()
|
|
}
|
|
} else {
|
|
if isPunctuatorChar(p.ch) {
|
|
return "", p.errAt("Found '" + string(p.ch) + "' where a key name was expected (check your syntax or use quotes if the key name includes {}[],: or whitespace)")
|
|
}
|
|
name.WriteByte(p.ch)
|
|
}
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
func (p *hjsonParser) white() {
|
|
for p.ch > 0 {
|
|
// Skip whitespace.
|
|
for p.ch > 0 && p.ch <= ' ' {
|
|
p.next()
|
|
}
|
|
// Hjson allows comments
|
|
if p.ch == '#' || p.ch == '/' && p.peek(0) == '/' {
|
|
for p.ch > 0 && p.ch != '\n' {
|
|
p.next()
|
|
}
|
|
} else if p.ch == '/' && p.peek(0) == '*' {
|
|
p.next()
|
|
p.next()
|
|
for p.ch > 0 && !(p.ch == '*' && p.peek(0) == '/') {
|
|
p.next()
|
|
}
|
|
if p.ch > 0 {
|
|
p.next()
|
|
p.next()
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *hjsonParser) readTfnns() (interface{}, error) {
|
|
|
|
// Hjson strings can be quoteless
|
|
// returns string, true, false, or null.
|
|
|
|
if isPunctuatorChar(p.ch) {
|
|
return nil, p.errAt("Found a punctuator character '" + string(p.ch) + "' when expecting a quoteless string (check your syntax)")
|
|
}
|
|
chf := p.ch
|
|
value := new(bytes.Buffer)
|
|
value.WriteByte(p.ch)
|
|
|
|
for {
|
|
p.next()
|
|
isEol := p.ch == '\r' || p.ch == '\n' || p.ch == 0
|
|
if isEol ||
|
|
p.ch == ',' || p.ch == '}' || p.ch == ']' ||
|
|
p.ch == '#' ||
|
|
p.ch == '/' && (p.peek(0) == '/' || p.peek(0) == '*') {
|
|
switch chf {
|
|
case 'f':
|
|
if strings.TrimSpace(value.String()) == "false" {
|
|
return false, nil
|
|
}
|
|
case 'n':
|
|
if strings.TrimSpace(value.String()) == "null" {
|
|
return nil, nil
|
|
}
|
|
case 't':
|
|
if strings.TrimSpace(value.String()) == "true" {
|
|
return true, nil
|
|
}
|
|
default:
|
|
if chf == '-' || chf >= '0' && chf <= '9' {
|
|
if n, err := tryParseNumber(value.Bytes(), false); err == nil {
|
|
return n, nil
|
|
}
|
|
}
|
|
}
|
|
if isEol {
|
|
// remove any whitespace at the end (ignored in quoteless strings)
|
|
return strings.TrimSpace(value.String()), nil
|
|
}
|
|
}
|
|
value.WriteByte(p.ch)
|
|
}
|
|
}
|
|
|
|
func (p *hjsonParser) readArray() (value interface{}, err error) {
|
|
|
|
// Parse an array value.
|
|
// assuming ch == '['
|
|
|
|
array := make([]interface{}, 0, 1)
|
|
|
|
p.next()
|
|
p.white()
|
|
|
|
if p.ch == ']' {
|
|
p.next()
|
|
return array, nil // empty array
|
|
}
|
|
|
|
for p.ch > 0 {
|
|
var val interface{}
|
|
if val, err = p.readValue(); err != nil {
|
|
return nil, err
|
|
}
|
|
array = append(array, val)
|
|
p.white()
|
|
// in Hjson the comma is optional and trailing commas are allowed
|
|
if p.ch == ',' {
|
|
p.next()
|
|
p.white()
|
|
}
|
|
if p.ch == ']' {
|
|
p.next()
|
|
return array, nil
|
|
}
|
|
p.white()
|
|
}
|
|
|
|
return nil, p.errAt("End of input while parsing an array (did you forget a closing ']'?)")
|
|
}
|
|
|
|
func (p *hjsonParser) readObject(withoutBraces bool) (value interface{}, err error) {
|
|
// Parse an object value.
|
|
|
|
object := make(map[string]interface{})
|
|
|
|
if !withoutBraces {
|
|
// assuming ch == '{'
|
|
p.next()
|
|
}
|
|
|
|
p.white()
|
|
if p.ch == '}' && !withoutBraces {
|
|
p.next()
|
|
return object, nil // empty object
|
|
}
|
|
for p.ch > 0 {
|
|
var key string
|
|
if key, err = p.readKeyname(); err != nil {
|
|
return nil, err
|
|
}
|
|
p.white()
|
|
if p.ch != ':' {
|
|
return nil, p.errAt("Expected ':' instead of '" + string(p.ch) + "'")
|
|
}
|
|
p.next()
|
|
// duplicate keys overwrite the previous value
|
|
var val interface{}
|
|
if val, err = p.readValue(); err != nil {
|
|
return nil, err
|
|
}
|
|
object[key] = val
|
|
p.white()
|
|
// in Hjson the comma is optional and trailing commas are allowed
|
|
if p.ch == ',' {
|
|
p.next()
|
|
p.white()
|
|
}
|
|
if p.ch == '}' && !withoutBraces {
|
|
p.next()
|
|
return object, nil
|
|
}
|
|
p.white()
|
|
}
|
|
|
|
if withoutBraces {
|
|
return object, nil
|
|
}
|
|
return nil, p.errAt("End of input while parsing an object (did you forget a closing '}'?)")
|
|
}
|
|
|
|
func (p *hjsonParser) readValue() (interface{}, error) {
|
|
|
|
// Parse a Hjson value. It could be an object, an array, a string, a number or a word.
|
|
|
|
p.white()
|
|
switch p.ch {
|
|
case '{':
|
|
return p.readObject(false)
|
|
case '[':
|
|
return p.readArray()
|
|
case '"', '\'':
|
|
return p.readString(true)
|
|
default:
|
|
return p.readTfnns()
|
|
}
|
|
}
|
|
|
|
func (p *hjsonParser) rootValue() (interface{}, error) {
|
|
// Braces for the root object are optional
|
|
|
|
p.white()
|
|
switch p.ch {
|
|
case '{':
|
|
return p.checkTrailing(p.readObject(false))
|
|
case '[':
|
|
return p.checkTrailing(p.readArray())
|
|
}
|
|
|
|
// assume we have a root object without braces
|
|
res, err := p.checkTrailing(p.readObject(true))
|
|
if err == nil {
|
|
return res, nil
|
|
}
|
|
|
|
// test if we are dealing with a single JSON value instead (true/false/null/num/"")
|
|
p.resetAt()
|
|
if res2, err2 := p.checkTrailing(p.readValue()); err2 == nil {
|
|
return res2, nil
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
func (p *hjsonParser) checkTrailing(v interface{}, err error) (interface{}, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.white()
|
|
if p.ch > 0 {
|
|
return nil, p.errAt("Syntax error, found trailing characters")
|
|
}
|
|
return v, nil
|
|
}
|
|
|
|
// Unmarshal parses the Hjson-encoded data and stores the result
|
|
// in the value pointed to by v.
|
|
//
|
|
// Unmarshal uses the inverse of the encodings that
|
|
// Marshal uses, allocating maps, slices, and pointers as necessary.
|
|
//
|
|
func Unmarshal(data []byte, v interface{}) (err error) {
|
|
var value interface{}
|
|
parser := &hjsonParser{data, 0, ' '}
|
|
parser.resetAt()
|
|
value, err = parser.rootValue()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rv := reflect.ValueOf(v)
|
|
if rv.Kind() != reflect.Ptr || rv.IsNil() {
|
|
return fmt.Errorf("non-pointer %v", reflect.TypeOf(v))
|
|
}
|
|
for rv.Kind() == reflect.Ptr {
|
|
rv = rv.Elem()
|
|
}
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = fmt.Errorf("%v", e)
|
|
}
|
|
}()
|
|
rv.Set(reflect.ValueOf(value))
|
|
return err
|
|
}
|