forked from gwatts/iniconf
/
parser.go
139 lines (128 loc) · 3.18 KB
/
parser.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// Copyright Splunk, Inc. 2014
//
// Author: Gareth Watts <gareth@splunk.com>
package iniconf
// handle low level lexing/parsing of .ini files
// this still needs to handle inline comments
import (
"bufio"
"bytes"
"io"
"strings"
"unicode"
"unicode/utf8"
)
const (
entryTypeNone = iota
entryTypeBlank // blank line
entryTypeComment
entryTypeSection
entryTypeKV
)
type parseValue struct {
entryType int
commentLine []byte
section string
key string
value []byte
rawLines [][]byte // raw lines making up the value, including any trailing comment
lineNum int
}
func (pv *parseValue) addRawLine(line []byte) {
rl := make([]byte, len(line))
copy(rl, line)
pv.rawLines = append(pv.rawLines, rl)
}
type parser struct {
br *bufio.Reader
bufLine []byte
lineNum int
}
func newParser(br *bufio.Reader) *parser {
return &parser{br: br}
}
func (p *parser) nextLine() (line []byte, err error) {
var isPrefix bool
if p.bufLine != nil {
return p.bufLine, nil
}
p.bufLine, isPrefix, err = p.br.ReadLine()
if isPrefix {
// line longer than 4KB
return nil, newParseError(ErrLineTooLong, p.lineNum)
}
p.lineNum++
return p.bufLine, err
}
func (p *parser) NextValue() (*parseValue, error) {
result := new(parseValue)
inValue := false
for {
rawLine, err := p.nextLine()
// make copy
rawLine = append([]byte{}, rawLine...)
if err == io.EOF && inValue {
// will return EOF on next read
return result, nil
} else if err != nil {
return result, err
}
// strip trailing whitespace
line := bytes.TrimRightFunc(rawLine, unicode.IsSpace)
if inValue {
if len(line) == 0 || !startsWithSpace(line) {
// leave p.bufLine intact for next call
return result, nil
}
result.value = append(result.value, '\n')
result.value = append(result.value, bytes.TrimLeftFunc(line, unicode.IsSpace)...)
result.addRawLine(rawLine)
// we crossed the line, so mark it zero
p.bufLine = nil
} else if len(line) > 0 {
result.lineNum = p.lineNum
switch line[0] {
case ';':
result.entryType = entryTypeComment
result.addRawLine(rawLine)
p.bufLine = nil
return result, nil
case '[':
if line[len(line)-1] != ']' {
return nil, newParseError(ErrInvalidSection, p.lineNum)
}
result.entryType = entryTypeSection
result.section = strings.TrimFunc(string(line[1:len(line)-1]), unicode.IsSpace)
result.addRawLine(rawLine)
p.bufLine = nil
return result, nil
default:
kv := bytes.SplitN(line, []byte("="), 2)
if len(kv) != 2 {
return nil, newParseError(ErrInvalidEntry, p.lineNum)
}
result.entryType = entryTypeKV
result.key = string(bytes.TrimRightFunc(kv[0], unicode.IsSpace))
value := bytes.TrimLeftFunc(kv[1], unicode.IsSpace)
result.value = append([]byte{}, value...)
result.addRawLine(rawLine)
// the next line may be a continuation of this value
inValue = true
p.bufLine = nil
}
} else {
// empty line
result.entryType = entryTypeBlank
result.addRawLine([]byte(""))
p.bufLine = nil
return result, nil
}
}
}
func startsWithSpace(s []byte) bool {
r, _ := utf8.DecodeRune(s)
if r == utf8.RuneError {
return false
}
return unicode.IsSpace(r)
}