]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | // Copyright 2015 Unknwon |
2 | // | |
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 | |
6 | // | |
7 | // http://www.apache.org/licenses/LICENSE-2.0 | |
8 | // | |
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 | |
13 | // under the License. | |
14 | ||
15 | package ini | |
16 | ||
17 | import ( | |
18 | "bufio" | |
19 | "bytes" | |
20 | "fmt" | |
21 | "io" | |
22 | "strconv" | |
23 | "strings" | |
24 | "unicode" | |
25 | ) | |
26 | ||
27 | type tokenType int | |
28 | ||
29 | const ( | |
30 | _TOKEN_INVALID tokenType = iota | |
31 | _TOKEN_COMMENT | |
32 | _TOKEN_SECTION | |
33 | _TOKEN_KEY | |
34 | ) | |
35 | ||
36 | type parser struct { | |
37 | buf *bufio.Reader | |
38 | isEOF bool | |
39 | count int | |
40 | comment *bytes.Buffer | |
41 | } | |
42 | ||
43 | func newParser(r io.Reader) *parser { | |
44 | return &parser{ | |
45 | buf: bufio.NewReader(r), | |
46 | count: 1, | |
47 | comment: &bytes.Buffer{}, | |
48 | } | |
49 | } | |
50 | ||
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 { | |
56 | return err | |
57 | } else if len(mask) < 2 { | |
58 | return nil | |
59 | } | |
60 | ||
61 | switch { | |
62 | case mask[0] == 254 && mask[1] == 255: | |
63 | fallthrough | |
64 | case mask[0] == 255 && mask[1] == 254: | |
65 | p.buf.Read(mask) | |
66 | case mask[0] == 239 && mask[1] == 187: | |
67 | mask, err := p.buf.Peek(3) | |
68 | if err != nil && err != io.EOF { | |
69 | return err | |
70 | } else if len(mask) < 3 { | |
71 | return nil | |
72 | } | |
73 | if mask[2] == 191 { | |
74 | p.buf.Read(mask) | |
75 | } | |
76 | } | |
77 | return nil | |
78 | } | |
79 | ||
80 | func (p *parser) readUntil(delim byte) ([]byte, error) { | |
81 | data, err := p.buf.ReadBytes(delim) | |
82 | if err != nil { | |
83 | if err == io.EOF { | |
84 | p.isEOF = true | |
85 | } else { | |
86 | return nil, err | |
87 | } | |
88 | } | |
89 | return data, nil | |
90 | } | |
91 | ||
92 | func cleanComment(in []byte) ([]byte, bool) { | |
93 | i := bytes.IndexAny(in, "#;") | |
94 | if i == -1 { | |
95 | return nil, false | |
96 | } | |
97 | return in[i:], true | |
98 | } | |
99 | ||
100 | func readKeyName(in []byte) (string, int, error) { | |
101 | line := string(in) | |
102 | ||
103 | // Check if key name surrounded by quotes. | |
104 | var keyQuote string | |
105 | if line[0] == '"' { | |
106 | if len(line) > 6 && string(line[0:3]) == `"""` { | |
107 | keyQuote = `"""` | |
108 | } else { | |
109 | keyQuote = `"` | |
110 | } | |
111 | } else if line[0] == '`' { | |
112 | keyQuote = "`" | |
113 | } | |
114 | ||
115 | // Get out key name | |
116 | endIdx := -1 | |
117 | if len(keyQuote) > 0 { | |
118 | startIdx := len(keyQuote) | |
119 | // FIXME: fail case -> """"""name"""=value | |
120 | pos := strings.Index(line[startIdx:], keyQuote) | |
121 | if pos == -1 { | |
122 | return "", -1, fmt.Errorf("missing closing key quote: %s", line) | |
123 | } | |
124 | pos += startIdx | |
125 | ||
126 | // Find key-value delimiter | |
127 | i := strings.IndexAny(line[pos+startIdx:], "=:") | |
128 | if i < 0 { | |
129 | return "", -1, ErrDelimiterNotFound{line} | |
130 | } | |
131 | endIdx = pos + i | |
132 | return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil | |
133 | } | |
134 | ||
135 | endIdx = strings.IndexAny(line, "=:") | |
136 | if endIdx < 0 { | |
137 | return "", -1, ErrDelimiterNotFound{line} | |
138 | } | |
139 | return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil | |
140 | } | |
141 | ||
142 | func (p *parser) readMultilines(line, val, valQuote string) (string, error) { | |
143 | for { | |
144 | data, err := p.readUntil('\n') | |
145 | if err != nil { | |
146 | return "", err | |
147 | } | |
148 | next := string(data) | |
149 | ||
150 | pos := strings.LastIndex(next, valQuote) | |
151 | if pos > -1 { | |
152 | val += next[:pos] | |
153 | ||
154 | comment, has := cleanComment([]byte(next[pos:])) | |
155 | if has { | |
156 | p.comment.Write(bytes.TrimSpace(comment)) | |
157 | } | |
158 | break | |
159 | } | |
160 | val += next | |
161 | if p.isEOF { | |
162 | return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next) | |
163 | } | |
164 | } | |
165 | return val, nil | |
166 | } | |
167 | ||
168 | func (p *parser) readContinuationLines(val string) (string, error) { | |
169 | for { | |
170 | data, err := p.readUntil('\n') | |
171 | if err != nil { | |
172 | return "", err | |
173 | } | |
174 | next := strings.TrimSpace(string(data)) | |
175 | ||
176 | if len(next) == 0 { | |
177 | break | |
178 | } | |
179 | val += next | |
180 | if val[len(val)-1] != '\\' { | |
181 | break | |
182 | } | |
183 | val = val[:len(val)-1] | |
184 | } | |
185 | return val, nil | |
186 | } | |
187 | ||
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 | |
194 | } | |
195 | ||
196 | func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) { | |
197 | line := strings.TrimLeftFunc(string(in), unicode.IsSpace) | |
198 | if len(line) == 0 { | |
199 | return "", nil | |
200 | } | |
201 | ||
202 | var valQuote string | |
203 | if len(line) > 3 && string(line[0:3]) == `"""` { | |
204 | valQuote = `"""` | |
205 | } else if line[0] == '`' { | |
206 | valQuote = "`" | |
207 | } | |
208 | ||
209 | if len(valQuote) > 0 { | |
210 | startIdx := len(valQuote) | |
211 | pos := strings.LastIndex(line[startIdx:], valQuote) | |
212 | // Check for multi-line value | |
213 | if pos == -1 { | |
214 | return p.readMultilines(line, line[startIdx:], valQuote) | |
215 | } | |
216 | ||
217 | return line[startIdx : pos+startIdx], nil | |
218 | } | |
219 | ||
220 | // Won't be able to reach here if value only contains whitespace. | |
221 | line = strings.TrimSpace(line) | |
222 | ||
223 | // Check continuation lines when desired. | |
224 | if !ignoreContinuation && line[len(line)-1] == '\\' { | |
225 | return p.readContinuationLines(line[:len(line)-1]) | |
226 | } | |
227 | ||
228 | i := strings.IndexAny(line, "#;") | |
229 | if i > -1 { | |
230 | p.comment.WriteString(line[i:]) | |
231 | line = strings.TrimSpace(line[:i]) | |
232 | } | |
233 | ||
234 | // Trim single quotes | |
235 | if hasSurroundedQuote(line, '\'') || | |
236 | hasSurroundedQuote(line, '"') { | |
237 | line = line[1 : len(line)-1] | |
238 | } | |
239 | return line, nil | |
240 | } | |
241 | ||
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) | |
247 | } | |
248 | ||
249 | // Ignore error because default section name is never empty string. | |
250 | section, _ := f.NewSection(DEFAULT_SECTION) | |
251 | ||
252 | var line []byte | |
253 | var inUnparseableSection bool | |
254 | for !p.isEOF { | |
255 | line, err = p.readUntil('\n') | |
256 | if err != nil { | |
257 | return err | |
258 | } | |
259 | ||
260 | line = bytes.TrimLeftFunc(line, unicode.IsSpace) | |
261 | if len(line) == 0 { | |
262 | continue | |
263 | } | |
264 | ||
265 | // Comments | |
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) | |
271 | continue | |
272 | } | |
273 | ||
274 | // Section | |
275 | if line[0] == '[' { | |
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("]")) | |
279 | if closeIdx == -1 { | |
280 | return fmt.Errorf("unclosed section: %s", line) | |
281 | } | |
282 | ||
283 | name := string(line[1:closeIdx]) | |
284 | section, err = f.NewSection(name) | |
285 | if err != nil { | |
286 | return err | |
287 | } | |
288 | ||
289 | comment, has := cleanComment(line[closeIdx+1:]) | |
290 | if has { | |
291 | p.comment.Write(comment) | |
292 | } | |
293 | ||
294 | section.Comment = strings.TrimSpace(p.comment.String()) | |
295 | ||
296 | // Reset aotu-counter and comments | |
297 | p.comment.Reset() | |
298 | p.count = 1 | |
299 | ||
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 | |
305 | continue | |
306 | } | |
307 | } | |
308 | continue | |
309 | } | |
310 | ||
311 | if inUnparseableSection { | |
312 | section.isRawSection = true | |
313 | section.rawBody += string(line) | |
314 | continue | |
315 | } | |
316 | ||
317 | kname, offset, err := readKeyName(line) | |
318 | if err != nil { | |
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") | |
322 | if err != nil { | |
323 | return err | |
324 | } | |
325 | key.isBooleanType = true | |
326 | key.Comment = strings.TrimSpace(p.comment.String()) | |
327 | p.comment.Reset() | |
328 | continue | |
329 | } | |
330 | return err | |
331 | } | |
332 | ||
333 | // Auto increment. | |
334 | isAutoIncr := false | |
335 | if kname == "-" { | |
336 | isAutoIncr = true | |
337 | kname = "#" + strconv.Itoa(p.count) | |
338 | p.count++ | |
339 | } | |
340 | ||
341 | key, err := section.NewKey(kname, "") | |
342 | if err != nil { | |
343 | return err | |
344 | } | |
345 | key.isAutoIncrement = isAutoIncr | |
346 | ||
347 | value, err := p.readValue(line[offset:], f.options.IgnoreContinuation) | |
348 | if err != nil { | |
349 | return err | |
350 | } | |
351 | key.SetValue(value) | |
352 | key.Comment = strings.TrimSpace(p.comment.String()) | |
353 | p.comment.Reset() | |
354 | } | |
355 | return nil | |
356 | } |