You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

325 lines
7.2 KiB

  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. package ini
  15. import (
  16. "bufio"
  17. "bytes"
  18. "fmt"
  19. "io"
  20. "strconv"
  21. "strings"
  22. "unicode"
  23. )
  24. type tokenType int
  25. const (
  26. _TOKEN_INVALID tokenType = iota
  27. _TOKEN_COMMENT
  28. _TOKEN_SECTION
  29. _TOKEN_KEY
  30. )
  31. type parser struct {
  32. buf *bufio.Reader
  33. isEOF bool
  34. count int
  35. comment *bytes.Buffer
  36. }
  37. func newParser(r io.Reader) *parser {
  38. return &parser{
  39. buf: bufio.NewReader(r),
  40. count: 1,
  41. comment: &bytes.Buffer{},
  42. }
  43. }
  44. // BOM handles header of BOM-UTF8 format.
  45. // http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
  46. func (p *parser) BOM() error {
  47. mask, err := p.buf.Peek(3)
  48. if err != nil && err != io.EOF {
  49. return err
  50. } else if len(mask) < 3 {
  51. return nil
  52. } else if mask[0] == 239 && mask[1] == 187 && mask[2] == 191 {
  53. p.buf.Read(mask)
  54. }
  55. return nil
  56. }
  57. func (p *parser) readUntil(delim byte) ([]byte, error) {
  58. data, err := p.buf.ReadBytes(delim)
  59. if err != nil {
  60. if err == io.EOF {
  61. p.isEOF = true
  62. } else {
  63. return nil, err
  64. }
  65. }
  66. return data, nil
  67. }
  68. func cleanComment(in []byte) ([]byte, bool) {
  69. i := bytes.IndexAny(in, "#;")
  70. if i == -1 {
  71. return nil, false
  72. }
  73. return in[i:], true
  74. }
  75. func readKeyName(in []byte) (string, int, error) {
  76. line := string(in)
  77. // Check if key name surrounded by quotes.
  78. var keyQuote string
  79. if line[0] == '"' {
  80. if len(line) > 6 && string(line[0:3]) == `"""` {
  81. keyQuote = `"""`
  82. } else {
  83. keyQuote = `"`
  84. }
  85. } else if line[0] == '`' {
  86. keyQuote = "`"
  87. }
  88. // Get out key name
  89. endIdx := -1
  90. if len(keyQuote) > 0 {
  91. startIdx := len(keyQuote)
  92. // FIXME: fail case -> """"""name"""=value
  93. pos := strings.Index(line[startIdx:], keyQuote)
  94. if pos == -1 {
  95. return "", -1, fmt.Errorf("missing closing key quote: %s", line)
  96. }
  97. pos += startIdx
  98. // Find key-value delimiter
  99. i := strings.IndexAny(line[pos+startIdx:], "=:")
  100. if i < 0 {
  101. return "", -1, ErrDelimiterNotFound{line}
  102. }
  103. endIdx = pos + i
  104. return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
  105. }
  106. endIdx = strings.IndexAny(line, "=:")
  107. if endIdx < 0 {
  108. return "", -1, ErrDelimiterNotFound{line}
  109. }
  110. return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
  111. }
  112. func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
  113. for {
  114. data, err := p.readUntil('\n')
  115. if err != nil {
  116. return "", err
  117. }
  118. next := string(data)
  119. pos := strings.LastIndex(next, valQuote)
  120. if pos > -1 {
  121. val += next[:pos]
  122. comment, has := cleanComment([]byte(next[pos:]))
  123. if has {
  124. p.comment.Write(bytes.TrimSpace(comment))
  125. }
  126. break
  127. }
  128. val += next
  129. if p.isEOF {
  130. return "", fmt.Errorf("missing closing key quote from '%s' to '%s'", line, next)
  131. }
  132. }
  133. return val, nil
  134. }
  135. func (p *parser) readContinuationLines(val string) (string, error) {
  136. for {
  137. data, err := p.readUntil('\n')
  138. if err != nil {
  139. return "", err
  140. }
  141. next := strings.TrimSpace(string(data))
  142. if len(next) == 0 {
  143. break
  144. }
  145. val += next
  146. if val[len(val)-1] != '\\' {
  147. break
  148. }
  149. val = val[:len(val)-1]
  150. }
  151. return val, nil
  152. }
  153. // hasSurroundedQuote check if and only if the first and last characters
  154. // are quotes \" or \'.
  155. // It returns false if any other parts also contain same kind of quotes.
  156. func hasSurroundedQuote(in string, quote byte) bool {
  157. return len(in) > 2 && in[0] == quote && in[len(in)-1] == quote &&
  158. strings.IndexByte(in[1:], quote) == len(in)-2
  159. }
  160. func (p *parser) readValue(in []byte, ignoreContinuation bool) (string, error) {
  161. line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
  162. if len(line) == 0 {
  163. return "", nil
  164. }
  165. var valQuote string
  166. if len(line) > 3 && string(line[0:3]) == `"""` {
  167. valQuote = `"""`
  168. } else if line[0] == '`' {
  169. valQuote = "`"
  170. }
  171. if len(valQuote) > 0 {
  172. startIdx := len(valQuote)
  173. pos := strings.LastIndex(line[startIdx:], valQuote)
  174. // Check for multi-line value
  175. if pos == -1 {
  176. return p.readMultilines(line, line[startIdx:], valQuote)
  177. }
  178. return line[startIdx : pos+startIdx], nil
  179. }
  180. // Won't be able to reach here if value only contains whitespace.
  181. line = strings.TrimSpace(line)
  182. // Check continuation lines when desired.
  183. if !ignoreContinuation && line[len(line)-1] == '\\' {
  184. return p.readContinuationLines(line[:len(line)-1])
  185. }
  186. i := strings.IndexAny(line, "#;")
  187. if i > -1 {
  188. p.comment.WriteString(line[i:])
  189. line = strings.TrimSpace(line[:i])
  190. }
  191. // Trim single quotes
  192. if hasSurroundedQuote(line, '\'') ||
  193. hasSurroundedQuote(line, '"') {
  194. line = line[1 : len(line)-1]
  195. }
  196. return line, nil
  197. }
  198. // parse parses data through an io.Reader.
  199. func (f *File) parse(reader io.Reader) (err error) {
  200. p := newParser(reader)
  201. if err = p.BOM(); err != nil {
  202. return fmt.Errorf("BOM: %v", err)
  203. }
  204. // Ignore error because default section name is never empty string.
  205. section, _ := f.NewSection(DEFAULT_SECTION)
  206. var line []byte
  207. for !p.isEOF {
  208. line, err = p.readUntil('\n')
  209. if err != nil {
  210. return err
  211. }
  212. line = bytes.TrimLeftFunc(line, unicode.IsSpace)
  213. if len(line) == 0 {
  214. continue
  215. }
  216. // Comments
  217. if line[0] == '#' || line[0] == ';' {
  218. // Note: we do not care ending line break,
  219. // it is needed for adding second line,
  220. // so just clean it once at the end when set to value.
  221. p.comment.Write(line)
  222. continue
  223. }
  224. // Section
  225. if line[0] == '[' {
  226. // Read to the next ']' (TODO: support quoted strings)
  227. // TODO(unknwon): use LastIndexByte when stop supporting Go1.4
  228. closeIdx := bytes.LastIndex(line, []byte("]"))
  229. if closeIdx == -1 {
  230. return fmt.Errorf("unclosed section: %s", line)
  231. }
  232. name := string(line[1:closeIdx])
  233. section, err = f.NewSection(name)
  234. if err != nil {
  235. return err
  236. }
  237. comment, has := cleanComment(line[closeIdx+1:])
  238. if has {
  239. p.comment.Write(comment)
  240. }
  241. section.Comment = strings.TrimSpace(p.comment.String())
  242. // Reset aotu-counter and comments
  243. p.comment.Reset()
  244. p.count = 1
  245. continue
  246. }
  247. kname, offset, err := readKeyName(line)
  248. if err != nil {
  249. // Treat as boolean key when desired, and whole line is key name.
  250. if IsErrDelimiterNotFound(err) && f.options.AllowBooleanKeys {
  251. key, err := section.NewKey(string(line), "true")
  252. if err != nil {
  253. return err
  254. }
  255. key.isBooleanType = true
  256. key.Comment = strings.TrimSpace(p.comment.String())
  257. p.comment.Reset()
  258. continue
  259. }
  260. return err
  261. }
  262. // Auto increment.
  263. isAutoIncr := false
  264. if kname == "-" {
  265. isAutoIncr = true
  266. kname = "#" + strconv.Itoa(p.count)
  267. p.count++
  268. }
  269. key, err := section.NewKey(kname, "")
  270. if err != nil {
  271. return err
  272. }
  273. key.isAutoIncrement = isAutoIncr
  274. value, err := p.readValue(line[offset:], f.options.IgnoreContinuation)
  275. if err != nil {
  276. return err
  277. }
  278. key.SetValue(value)
  279. key.Comment = strings.TrimSpace(p.comment.String())
  280. p.comment.Reset()
  281. }
  282. return nil
  283. }