1 // Copyright 2015 Unknwon
3 // Licensed under the Apache License, Version 2.0 (the "License"): you may
4 // not use this file except in compliance with the License. You may obtain
5 // a copy of the License at
7 // http://www.apache.org/licenses/LICENSE-2.0
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12 // License for the specific language governing permissions and limitations
30 _TOKEN_INVALID tokenType = iota
43 func newParser(r io.Reader) *parser {
45 buf: bufio.NewReader(r),
47 comment: &bytes.Buffer{},
51 // BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format.
52 // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
53 func (p *parser) BOM() error {
54 mask, err := p.buf.Peek(2)
55 if err != nil && err != io.EOF {
57 } else if len(mask) < 2 {
62 case mask[0] == 254 && mask[1] == 255:
64 case mask[0] == 255 && mask[1] == 254:
66 case mask[0] == 239 && mask[1] == 187:
67 mask, err := p.buf.Peek(3)
68 if err != nil && err != io.EOF {
70 } else if len(mask) < 3 {
80 func (p *parser) readUntil(delim byte) ([]byte, error) {
81 data, err := p.buf.ReadBytes(delim)
92 func cleanComment(in []byte) ([]byte, bool) {
93 i := bytes.IndexAny(in, "#;")
100 func readKeyName(in []byte) (string, int, error) {
103 // Check if key name surrounded by quotes.
106 if len(line) > 6 && string(line[0:3]) == `"""` {
111 } else if line[0] == '`' {
117 if len(keyQuote) > 0 {
118 startIdx := len(keyQuote)
119 // FIXME: fail case -> """"""name"""=value
120 pos := strings.Index(line[startIdx:], keyQuote)
122 return "", -1, fmt.Errorf("missing closing key quote: %s", line)
126 // Find key-value delimiter
127 i := strings.IndexAny(line[pos+startIdx:], "=:")
129 return "", -1, ErrDelimiterNotFound{line}
132 return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
135 endIdx = strings.IndexAny(line, "=:")
137 return "", -1, ErrDelimiterNotFound{line}
139 return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
142 func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
144 data, err := p.readUntil('\n')
150 pos := strings.LastIndex(next, valQuote)
154 comment, has := cleanComment([]byte(next[pos:]))
156 p.comment.Write(bytes.TrimSpace(comment))
162 return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next)
168 func (p *parser) readContinuationLines(val string) (string, error) {
170 data, err := p.readUntil('\n')
174 next := strings.TrimSpace(string(data))
180 if val[len(val)-1] != '\\' {
183 val = val[:len(val)-1]
188 // hasSurroundedQuote check if and only if the first and last characters
189 // are quotes \" or \'.
190 // It returns false if any other parts also contain same kind of quotes.
191 func hasSurroundedQuote(in string, quote byte) bool {
192 return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote &&
193 strings.IndexByte(in[1:], quote) == len(in)-2
196 func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) {
197 line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
203 if len(line) > 3 && string(line[0:3]) == `"""` {
205 } else if line[0] == '`' {
209 if len(valQuote) > 0 {
210 startIdx := len(valQuote)
211 pos := strings.LastIndex(line[startIdx:], valQuote)
212 // Check for multi-line value
214 return p.readMultilines(line, line[startIdx:], valQuote)
217 return line[startIdx : pos+startIdx], nil
220 // Won't be able to reach here if value only contains whitespace.
221 line = strings.TrimSpace(line)
223 // Check continuation lines when desired.
224 if !ignoreContinuation && line[len(line)-1] == '\\' {
225 return p.readContinuationLines(line[:len(line)-1])
228 i := strings.IndexAny(line, "#;")
230 p.comment.WriteString(line[i:])
231 line = strings.TrimSpace(line[:i])
234 // Trim single quotes
235 if hasSurroundedQuote(line, '\'') ||
236 hasSurroundedQuote(line, '"') {
237 line = line[1 : len(line)-1]
242 // parse parses data through an io.Reader.
243 func (f *File) parse(reader io.Reader) (err error) {
244 p := newParser(reader)
245 if err = p.BOM(); err != nil {
246 return fmt.Errorf("BOM: %v", err)
249 // Ignore error because default section name is never empty string.
250 section, _ := f.NewSection(DEFAULT_SECTION)
253 var inUnparseableSection bool
255 line, err = p.readUntil('\n')
260 line = bytes.TrimLeftFunc(line, unicode.IsSpace)
266 if line[0] == '#' || line[0] == ';' {
267 // Note: we do not care ending line break,
268 // it is needed for adding second line,
269 // so just clean it once at the end when set to value.
270 p.comment.Write(line)
276 // Read to the next ']' (TODO: support quoted strings)
277 // TODO(unknwon): use LastIndexByte when stop supporting Go1.4
278 closeIdx := bytes.LastIndex(line, []byte("]"))
280 return fmt.Errorf("unclosed section: %s", line)
283 name := string(line[1:closeIdx])
284 section, err = f.NewSection(name)
289 comment, has := cleanComment(line[closeIdx+1:])
291 p.comment.Write(comment)
294 section.Comment = strings.TrimSpace(p.comment.String())
296 // Reset aotu-counter and comments
300 inUnparseableSection = false
301 for i := range f.options.UnparseableSections {
302 if f.options.UnparseableSections[i] == name ||
303 (f.options.Insensitive && strings.ToLower(f.options.UnparseableSections[i]) == strings.ToLower(name)) {
304 inUnparseableSection = true
311 if inUnparseableSection {
312 section.isRawSection = true
313 section.rawBody += string(line)
317 kname, offset, err := readKeyName(line)
319 // Treat as boolean key when desired, and whole line is key name.
320 if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
321 key, err := section.NewKey(string(line), "true")
325 key.isBooleanType = true
326 key.Comment = strings.TrimSpace(p.comment.String())
337 kname = "#" + strconv.Itoa(p.count)
341 key, err := section.NewKey(kname, "")
345 key.isAutoIncrement = isAutoIncr
347 value, err := p.readValue(line[offset:], f.options.IgnoreContinuation)
352 key.Comment = strings.TrimSpace(p.comment.String())