A errors.go => errors.go +54 -0
@@ 0,0 1,54 @@
+package ini
+
+import (
+ "errors"
+ "fmt"
+)
+
+type SyntaxError struct {
+ Line, Col int
+ Err error
+ Desc string
+}
+
+func (s *SyntaxError) Error() string {
+ if s.Desc == "" {
+ return fmt.Sprintf("ini: syntax error at %d:%d: %v", s.Line, s.Col, s.Err)
+ }
+ return fmt.Sprintf("ini: syntax error at %d:%d: %v -- %s", s.Line, s.Col, s.Err, s.Desc)
+}
+
+type UnclosedError rune
+
+func (u UnclosedError) Expecting() rune {
+ switch u := rune(u); u {
+ case '{':
+ return '}'
+ case '(':
+ return ')'
+ case '[':
+ return ']'
+ case '<':
+ return '>'
+ default:
+ return u
+ }
+}
+
+func (u UnclosedError) Error() string {
+ return fmt.Sprintf("ini: unclosed %c, expecting %c", rune(u), u.Expecting())
+}
+
+type BadCharError rune
+
+func (r BadCharError) Error() string {
+ return fmt.Sprintf("ini: encountered invalid character %q", rune(r))
+}
+
+var (
+ ErrSectionRawStr = errors.New("ini: raw string not accepted in section")
+ ErrUnclosedSection = errors.New("ini: section missing closing ]")
+ ErrEmptyKey = errors.New("ini: key is empty")
+
+ ErrBadNewline = BadCharError('\n')
+)
M ini.go => ini.go +627 -335
@@ 1,8 1,8 @@
+// Package ini is an INI parsing library.
package ini
import (
"bytes"
- "errors"
"fmt"
"io"
"strings"
@@ 11,445 11,737 @@ import (
)
const (
- chPrefixBegin = byte('[')
- chPrefixEnd = byte(']')
- chSpace = byte(' ')
- chQuote = byte('"')
- chRawQuote = byte('`')
- chTab = byte('\t')
- chLine = byte('\n')
- chFeed = byte('\r') // ignored outside of strings.
- chComment = byte(';')
- chEquals = byte('=')
- chEscape = byte('\\')
-)
+ // Sections
-const (
- valueMarkers = "\n" + string(chComment) + string(chEquals)
- horizontalWhitespace = " \t"
- whitespaceSansLine = " \t\r"
- anyWhitespace = " \t\r\n"
- commentNewline = "\n" + string(chComment)
-)
+ rSectionOpen = '['
+ rSectionClose = ']'
-// True is the value set for value-less keys in an INI.
-const True string = "1"
+ // Quoted values
+
+ rQuote = '"'
+ rRawQuote = '`'
+
+ // Whitespace
+
+ rSpace = ' '
+ rTab = '\t'
+ rNewline = '\n'
+ rCR = '\r' // Ignored outside of strings.
-// defaultINICapacity is the default capacity used when allocating a new INI
-// map[string]string. Overriding this is as simple as allocating your own map
-// and passing it to ReadINI.
-const defaultINICapacity int = 32
+ // Comment
-// PrefixSeparator is the default separator character used for key-value pairs
-// beneath section headings (e.g., "[buzzsaw]").
-var PrefixSeparator string = "."
+ rSemicolon = ';'
+ rHash = '#'
-type iniParser struct {
- rb []byte // slice of rbfix for reading
- quoted [][]byte // slice of quoted string parts
- result map[string][]string // Resulting string map
- prefix string
+ // Values
+
+ rEquals = '='
+ rEscape = '\\'
+)
+
+// Values is any set of INI values. This may be used as a Recorder for a Reader.
+type Values map[string][]string
+
+func (v Values) Set(key, value string) {
+ v[key] = []string{value}
}
-func (p *iniParser) put(k, v string) {
- if p.result == nil {
- return
+func (v Values) Add(key, value string) {
+ v[key] = append(v[key], value)
+}
+
+func (v Values) Get(key string) string {
+ if d := v[key]; len(d) > 0 {
+ return d[0]
}
+ return ""
+}
+
+func (v Values) Del(key string) {
+ delete(v, key)
+}
+
+func (v Values) Contains(key string) bool {
+ _, ok := v[key]
+ return ok
+}
- if len(p.prefix) > 0 {
- k = p.prefix + k
+func (v Values) Copy(dst Values) Values {
+ if dst == nil {
+ dst = make(Values, len(v))
}
- p.result[k] = append(p.result[k], v)
+ for k, vs := range v {
+ dst[k] = append(dst[k], vs...)
+ }
+ return dst
}
-func advance(b []byte, from int, skip string) []byte {
- skipIdx := 0
- skipLen := len(skip)
- bLen := len(b)
-trySkipAgain:
- for skipIdx = 0; skipIdx < skipLen && from < bLen; skipIdx++ {
- if b[from] == skip[skipIdx] {
- from++
- goto trySkipAgain
+func (v Values) Matching(dst Values, fn func(string, []string) bool) Values {
+ if dst == nil {
+ dst = make(Values)
+ }
+ for k, vs := range v {
+ if fn(k, vs) {
+ dst[k] = append(dst[k], vs...)
}
}
+ return dst
+}
- if from == 0 {
- return b
- }
+// nextfunc is a parsing function that modifies the decoder's state and returns another parsing
+// function. If nextfunc returns io.EOF, parsing is complete. Any other error halts parsing.
+type nextfunc func() (nextfunc, error)
- return b[from:]
-}
+type decoder struct {
+ true string
-func sanitizePrefix(prefix []byte) []byte {
- var out bytes.Buffer
- out.Grow(len(prefix))
+ rd io.Reader
+ readrune func() (rune, int, error)
- var (
- quoted = false
- escaped = false
- last rune = -1
- chomp = 0
- dropTail = func() {
- if chomp > 0 {
- out.Truncate(out.Len() - chomp)
- }
- chomp = 0
- }
- )
+ err error
+ sep []byte
+ sep2 [4]byte
+ dst Recorder
+ casefn func(rune) rune
- for _, r := range string(prefix) {
- if escaped {
- out.Write(escape(r))
- escaped = false
- goto write
- }
+ current rune
+ line, col int
- if !quoted && last == r && r == ' ' {
- goto next
- } else if !quoted && r == ' ' {
- dropTail()
- chomp, _ = out.WriteString(PrefixSeparator)
- goto next
- } else if r == '"' {
- if quoted = !quoted; quoted {
- dropTail()
- chomp, _ = out.WriteString(PrefixSeparator)
- goto next
- } else {
- dropTail()
- chomp, _ = out.WriteString(PrefixSeparator)
- goto next
- }
- }
+ // Storage
+ buffer bytes.Buffer
+ key string
+ prefix []byte // prefix is prepended to all buffered keys
+ prefix2 [32]byte
- if quoted && r == '\\' {
- escaped = true
- goto next
- }
+ // peek / next state
+ havenext bool
+ next rune
+ nexterr error
+}
+
+const True string = "1"
+
+func ReadINI(b []byte, out Values) (Values, error) {
+ if out == nil {
+ out = make(Values)
+ }
+ err := DefaultDecoder.Read(bytes.NewBuffer(b), out)
+ if err != nil {
+ return nil, err
+ }
+ return out, err
+}
- write:
- chomp = 0
- out.WriteRune(r)
- next:
- last = r
+func (d *decoder) add(key, value string) {
+ if d.dst != nil {
+ d.dst.Add(key, value)
}
+}
- dropTail()
- return out.Bytes()
+func (d *decoder) syntaxerr(err error, msg ...interface{}) *SyntaxError {
+ if se, ok := err.(*SyntaxError); ok {
+ return se
+ }
+ se := &SyntaxError{Line: d.line, Col: d.col, Err: err, Desc: fmt.Sprint(msg...)}
+ return se
}
-func (p *iniParser) readPrefix() error {
- p.rb = advance(p.rb, 0, anyWhitespace)
+func (d *decoder) nextRune() (r rune, size int, err error) {
+ if d.err != nil {
+ return d.current, utf8.RuneLen(d.current), d.err
+ }
- if len(p.rb) == 0 {
- return nil
+ if d.havenext {
+ r, size, err = d.peekRune()
+ d.havenext = false
+ } else if d.readrune != nil {
+ r, size, err = d.readrune()
+ } else {
+ r, size, err = readrune(d.rd)
}
- if p.rb[0] != chPrefixBegin {
- return p.readComment()
+ d.current = r
+
+ if err != nil {
+ d.err = err
+ d.rd = nil
}
- p.rb = advance(p.rb, 1, horizontalWhitespace)
- end := bytes.IndexByte(p.rb, chPrefixEnd)
- if end == -1 {
- return fmt.Errorf("No closing ']' found for prefix")
+ if d.current == '\n' {
+ d.line++
+ d.col = 1
}
- prefix := bytes.Trim(p.rb[:end], whitespaceSansLine)
- prefix = sanitizePrefix(prefix)
+ return r, size, err
+}
- p.rb = p.rb[end+1:]
- prefixStr := string(prefix)
+func (d *decoder) skip() error {
+ _, _, err := d.nextRune()
+ return err
+}
- if strings.ContainsAny(prefixStr, "\n") {
- return fmt.Errorf("Prefixes may not contain newlines (%q)", prefixStr)
+func (d *decoder) peekRune() (r rune, size int, err error) {
+ if d.havenext {
+ r = d.next
+ size = utf8.RuneLen(r)
+ return r, size, d.nexterr
}
- if len(prefixStr) > 0 {
- p.prefix = prefixStr + PrefixSeparator
+
+ // Even if there's an error.
+ d.havenext = true
+ if d.readrune != nil {
+ r, size, err = d.readrune()
} else {
- p.prefix = ""
+ r, size, err = readrune(d.rd)
}
+ d.next, d.nexterr = r, err
+ return r, size, err
+}
- return nil
+func (d *decoder) readUntil(oneof runeset, buffer bool, runemap func(rune) rune) (err error) {
+ for out := &d.buffer; ; {
+ var r rune
+ r, _, err = d.nextRune()
+ if err != nil {
+ return err
+ } else if oneof.Contains(r) {
+ return nil
+ } else if buffer {
+ if runemap != nil {
+ r = runemap(r)
+ }
+ if r >= 0 {
+ out.WriteRune(r)
+ }
+ }
+ }
}
-func (p *iniParser) readComment() error {
- if p.rb[0] != chComment {
- return p.readKey()
+func escaped(r rune) rune {
+ switch r {
+ case '0':
+ return 0
+ case 'a':
+ return '\a'
+ case 'b':
+ return '\b'
+ case 'f':
+ return '\f'
+ case 'n':
+ return '\n'
+ case 'r':
+ return '\r'
+ case 't':
+ return '\t'
+ case 'v':
+ return '\v'
+ default:
+ return r
}
+}
- if eol := bytes.IndexByte(p.rb, chLine); eol == -1 {
- p.rb = nil
- } else {
- p.rb = p.rb[eol+1:]
+func (d *decoder) readComment() (next nextfunc, err error) {
+ defer stopOnEOF(&next, &err)
+ next, err = d.readElem, d.readUntil(oneRune(rNewline), true, nil)
+ return
+}
+
+func isHorizSpace(r rune) bool { return r == ' ' || r == '\t' || r == '\r' }
+
+func (d *decoder) skipSpace(newlines bool) error {
+ fn := unicode.IsSpace
+ if !newlines {
+ fn = isHorizSpace
}
+ if fn(d.current) {
+ return d.readUntil(notRune(runeFunc(fn)), false, nil)
+ }
return nil
}
-func (p *iniParser) readKey() error {
- var keyBytes []byte
- var ch byte
+func isKeyEnd(r rune) bool {
+ return r == rEquals || r == rHash || r == rSemicolon || unicode.IsSpace(r)
+}
- eqIdx := bytes.IndexAny(p.rb, valueMarkers)
- if eqIdx == -1 {
- keyBytes = p.rb
- p.rb = nil
- } else {
- keyBytes = p.rb[:eqIdx]
- ch = p.rb[eqIdx]
+func casenop(r rune) rune { return r }
- if ch == chEquals {
- // swallow the '='
- p.rb = p.rb[eqIdx+1:]
- } else {
- p.rb = p.rb[eqIdx:]
+func (d *decoder) readKey() (nextfunc, error) {
+ casefn := d.casefn
+ d.buffer.Write(d.prefix)
+ switch d.current {
+ case rEquals:
+ return nil, d.syntaxerr(ErrEmptyKey, "keys may not be blank")
+ case rQuote, rRawQuote:
+ return nil, d.syntaxerr(BadCharError(d.current), "keys may not be quoted strings")
+ default:
+ r := d.current
+ if casefn != nil {
+ r = casefn(r)
}
+ d.buffer.WriteRune(r)
}
- keyBytes = bytes.TrimRight(keyBytes, whitespaceSansLine)
- key := string(keyBytes)
+ err := d.readUntil(runeFunc(isKeyEnd), true, casefn)
+ if err != nil && err != io.EOF {
+ return nil, err
+ }
+
+ if err == io.EOF {
+ d.add(d.buffer.String(), d.true)
+ return nil, nil
+ }
+
+ d.key = d.buffer.String()
+ d.buffer.Reset()
+
+ return d.readValueSep, nil
+}
- for _, r := range key {
- if r != '-' && r != '_' && r != '.' && !unicode.IsSymbol(r) && !unicode.IsLetter(r) && !unicode.IsNumber(r) && !unicode.IsMark(r) {
- return fmt.Errorf("Keys may only contain letters, numbers, marks, symbols, hyphens, and underscores; %q is not a valid character.", r)
+func (d *decoder) readValueSep() (next nextfunc, err error) {
+ if err = must(d.skipSpace(false), io.EOF, nil); err == io.EOF {
+ d.add(d.key, d.true)
+ return nil, nil
+ }
+
+ defer stopOnEOF(&next, &err)
+ // Aside from whitespace, the only thing that can follow a key is a newline or =.
+ switch d.current {
+ case rNewline:
+ d.add(d.key, d.true)
+ return d.readElem, d.skip()
+ case rEquals:
+ if err = d.skip(); err == io.EOF {
+ d.add(d.key, "")
+ return nil, nil
}
+ return d.readValue, nil
+ case rHash, rSemicolon:
+ d.add(d.key, d.true)
+ return d.readComment, nil
+ default:
+ return nil, d.syntaxerr(BadCharError(d.current), "expected either =, newline, or a comment")
}
+}
+
+func (d *decoder) readHexCode(size int) (result rune, err error) {
+ for i := 0; i < size; i++ {
+ r, sz, err := d.nextRune()
+ if err != nil {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ return -1, d.syntaxerr(err, "expected hex code")
+ } else if sz != 1 {
+ // Quick size check
+ return -1, d.syntaxerr(BadCharError(r), "expected hex code")
+ }
- if eqIdx == -1 {
- p.put(key, True)
- return nil
+ if r >= 'A' && r <= 'F' {
+ r = 10 + (r - 'A')
+ } else if r >= 'a' && r <= 'f' {
+ r = 10 + (r - 'a')
+ } else if r >= '0' && r <= '9' {
+ r -= '0'
+ } else {
+ return -1, d.syntaxerr(BadCharError(r), "expected hex code")
+ }
+ result = result<<4 | r
}
+ return result, nil
+}
- var err error
- var value string
- switch ch {
- case chEquals:
- value, err = p.readValue()
- case chComment:
- fallthrough
- case chLine:
- value = True
+func (d *decoder) readStringValue() (next nextfunc, err error) {
+ err = d.readUntil(runestr(`"\`), true, nil)
+ if err == io.EOF {
+ return nil, d.syntaxerr(UnclosedError('"'), "encountered EOF inside string")
+ } else if err != nil {
+ return nil, err
}
- if err != nil {
- return err
+ switch d.current {
+ case '"':
+ if r, _, perr := d.peekRune(); perr == nil && r == rQuote {
+ d.buffer.WriteRune(r)
+ return d.readStringValue, d.skip()
+ }
+ case '\\':
+ r, _, err := d.nextRune()
+ must(err)
+ switch r {
+ case 'x': // 1 octet
+ r, err = d.readHexCode(2)
+ d.buffer.WriteByte(byte(r & 0xFF))
+ case 'u': // 2 octets
+ r, err = d.readHexCode(4)
+ d.buffer.WriteRune(r)
+ case 'U': // 4 octets
+ r, err = d.readHexCode(8)
+ d.buffer.WriteRune(r)
+ default:
+ r = escaped(r)
+ d.buffer.WriteRune(escaped(r))
+ }
+ return d.readStringValue, err
}
- p.put(key, string(value))
- return nil
+ defer stopOnEOF(&next, &err)
+ d.add(d.key, d.buffer.String())
+ return d.readElem, d.skip()
}
-// isValueBegin is a function intended to be used as a callback for
-// bytes.IndexFunc when searching for the start of a value in a byte sequence.
-// Effectively, this just means searching for the first non-whitespace rune.
-// If a newline or comment is encountered, these are treated as empty values.
-func isValueBegin(r rune) bool {
- return !(r == '\t' || r == ' ' || r == '\r')
+func (d *decoder) readRawValue() (next nextfunc, err error) {
+ err = d.readUntil(oneRune(rRawQuote), true, nil)
+ if err == io.EOF {
+ return nil, d.syntaxerr(UnclosedError('`'), "encountered EOF inside raw string")
+ } else if err != nil {
+ return nil, err
+ }
+
+ if r, _, perr := d.peekRune(); perr == nil && r == rRawQuote {
+ d.buffer.WriteRune(r)
+ return d.readRawValue, d.skip()
+ }
+
+ defer stopOnEOF(&next, &err)
+ d.add(d.key, d.buffer.String())
+ return d.readElem, d.skip()
}
-// readValue reads the value side of a key-value pair. For quotes, this
-// descends into readQuote.
-func (p *iniParser) readValue() (string, error) {
- if len(p.rb) == 0 {
- return "", nil
+func (d *decoder) readValue() (next nextfunc, err error) {
+ if err = must(d.skipSpace(false), io.EOF); err == io.EOF {
+ d.add(d.key, "")
+ return nil, nil
}
- idx := bytes.IndexFunc(p.rb, isValueBegin)
+ switch d.current {
+ case rNewline:
+ // Terminated by newline
+ defer stopOnEOF(&next, &err)
+ d.add(d.key, "")
+ return d.readElem, d.skip()
+ case rQuote:
+ return d.readStringValue, nil
+ case rRawQuote:
+ return d.readRawValue, nil
+ case rHash, rSemicolon:
+ // Terminated by comment
+ d.add(d.key, "")
+ return d.readComment, nil
+ }
+
+ defer stopOnEOF(&next, &err)
+ d.buffer.WriteRune(d.current)
+ must(d.readUntil(runestr("\n;#"), true, nil), io.EOF)
- if idx == -1 {
- return string(bytes.Trim(p.rb, anyWhitespace)), nil
+ value := string(bytes.TrimRightFunc(d.buffer.Bytes(), unicode.IsSpace))
+ d.add(d.key, value)
+ return d.readElem, err
+}
+
+func (d *decoder) readQuotedSubsection() (next nextfunc, err error) {
+ if must(d.readUntil(runestr(`"\`), true, nil), io.EOF) == io.EOF {
+ return nil, d.syntaxerr(UnclosedError('"'), "encountered EOF inside quoted section name")
}
- switch p.rb[idx] {
- case chQuote:
- p.rb = p.rb[idx+1:]
- return p.readQuote()
- case chRawQuote:
- p.rb = p.rb[idx+1:]
- return p.readRawQuote()
- case chComment:
- fallthrough
- case chLine:
- return ``, nil
- // value := bytes.Trim(p.rb[:idx], horizontalWhitespace)
- // p.rb = p.rb[idx:]
- // return string(value), nil
- default:
- end := bytes.IndexAny(p.rb, commentNewline)
- if end == -1 {
- value := string(bytes.Trim(p.rb[idx:], anyWhitespace))
- p.rb = nil
- return value, nil
- } else {
- value := bytes.Trim(p.rb[idx:end], horizontalWhitespace)
- p.rb = p.rb[end:]
- return string(value), nil
+ switch d.current {
+ case rQuote:
+ var r rune
+ if r, _, err = d.peekRune(); err == nil && r == rQuote {
+ d.buffer.WriteRune(r)
+ return d.readQuotedSubsection, d.skip()
}
+
+ return d.readSubsection, d.skip()
+ case rEscape:
+ r, _, err := d.nextRune()
+ must(err)
+ switch r {
+ case 'x': // 1 octet
+ r, err = d.readHexCode(2)
+ d.buffer.WriteByte(byte(r & 0xFF))
+ case 'u': // 2 octets
+ r, err = d.readHexCode(4)
+ d.buffer.WriteRune(r)
+ case 'U': // 4 octets
+ r, err = d.readHexCode(8)
+ d.buffer.WriteRune(r)
+ default:
+ r = escaped(r)
+ d.buffer.WriteRune(escaped(r))
+ }
+ return d.readQuotedSubsection, nil
}
+ return nil, d.syntaxerr(BadCharError(d.current), "expected a closing quote or escape character")
}
-// Escape codes.
-// Any not listed here escape to the literal character escaped.
-var (
- escNUL = []byte{0} // NUL
- escBell = []byte{byte('\a')} // Bell
- escBackspace = []byte{byte('\b')} // Backspace
- escFeed = []byte{byte('\f')} // Form feed
- escNewline = []byte{byte('\n')} // Newline
- escCR = []byte{byte('\r')} // Carriage return
- escHTab = []byte{byte('\t')} // Horizontal tab
- escVTab = []byte{byte('\v')} // Vertical tab
- escSlash = []byte{byte('\\')} // Backslash
- escDQuote = []byte{byte('"')} // Double quote
-)
+func (d *decoder) readHeaderOpen() (nextfunc, error) {
+ if d.current != rSectionOpen {
+ // This should be more or less impossible, based on how it's called.
+ return nil, d.syntaxerr(BadCharError(d.current), "expected an opening bracket ('[')")
+ }
+ return d.readSubsection, d.skip()
+}
-func escape(b rune) []byte {
- var storage [4]byte
- var seq []byte
- switch b {
- case '0':
- seq = escNUL
- case 'a':
- seq = escBell
- case 'b':
- seq = escBackspace
- case 'f':
- seq = escFeed
- case 'n':
- seq = escNewline
- case 'r':
- seq = escCR
- case 't':
- seq = escHTab
- case 'v':
- seq = escVTab
- case '\\':
- seq = escSlash
- case '"':
- seq = escDQuote
- default:
- n := utf8.EncodeRune(storage[:], b)
- seq = storage[:n]
+func (d *decoder) addPrefixSep() {
+ sep := d.sep
+ if d.buffer.Len() < len(sep) || bytes.HasSuffix(d.buffer.Bytes(), sep) {
+ return
}
- return seq
+ d.buffer.Write(sep)
}
-func (p *iniParser) readRawQuote() (string, error) {
- var (
- parts = p.quoted[:0]
- idx int
- ch byte
- )
+func (d *decoder) readSubsection() (next nextfunc, err error) {
+ d.addPrefixSep()
- for ch != chRawQuote {
- idx = bytes.IndexByte(p.rb, chRawQuote)
- if idx == -1 {
- return ``, io.ErrUnexpectedEOF
+ switch d.current {
+ case rSectionClose:
+ if d.buffer.Len() == 0 {
+ d.prefix = d.prefix[:0]
+ } else {
+ d.prefix = append(d.prefix[:0], d.buffer.Bytes()...)
}
- ch = p.rb[idx]
-
- if ch == chRawQuote && len(p.rb) > idx+1 && p.rb[idx+1] == chRawQuote {
- parts = append(parts, p.rb[:idx+1])
- idx += 1
- ch = 0 // Reset ch since it was escaped.
- } else if ch == chRawQuote && idx != 0 {
- parts = append(parts, p.rb[:idx])
+ defer stopOnEOF(&next, &err)
+ return d.readElem, d.skip()
+ case rRawQuote:
+ return nil, d.syntaxerr(ErrSectionRawStr, "raw strings are not allowed in section names")
+ case rQuote:
+ return d.readQuotedSubsection, nil
+ case rSpace, rTab:
+ return d.readSubsection, d.skipSpace(false)
+ case rNewline:
+ return nil, d.syntaxerr(ErrBadNewline, "section headings may not contain unquoted newlines")
+ default:
+ if unicode.IsSpace(d.current) {
+ return nil, d.syntaxerr(BadCharError(d.current), "expected section name")
}
- p.rb = p.rb[idx+1:]
}
- p.quoted = parts[:0]
- switch len(parts) {
- case 0:
- return ``, nil
- case 1:
- return string(parts[0]), nil
- default:
- return string(bytes.Join(parts, nil)), nil
+ // Buffer initial
+ r := d.current
+ casefn := d.casefn
+ if casefn != nil {
+ r = casefn(r)
}
+ d.buffer.WriteRune(r)
+
+ return d.readSubsection, d.readUntil(runestr(" \t\n\"]"), true, casefn)
}
-func (p *iniParser) readQuote() (string, error) {
+func (d *decoder) start() (next nextfunc, err error) {
+ _, _, err = d.nextRune()
+ return d.readElem, err
+}
- var (
- parts = p.quoted[:0]
- idx int
- ch byte
- )
- for ch != chQuote {
- idx = bytes.IndexAny(p.rb, `\"`)
- if idx == -1 {
- return ``, io.ErrUnexpectedEOF
- }
- ch = p.rb[idx]
-
- if ch == chEscape && len(p.rb) > idx+1 {
- r, _ := utf8.DecodeRune(p.rb[idx+1:])
- seq := escape(r)
- parts = append(parts, p.rb[:idx], seq)
- idx += 1
- } else if ch == chEscape {
- return ``, io.ErrUnexpectedEOF
- } else if ch == chQuote && idx != 0 {
- parts = append(parts, p.rb[:idx])
+func (d *decoder) readElem() (next nextfunc, err error) {
+ d.buffer.Reset()
+
+ if d.err == io.EOF {
+ return nil, nil
+ } else if d.err != nil {
+ return nil, err
+ }
+
+ switch d.current {
+ case rSectionOpen:
+ return d.readHeaderOpen()
+ case rHash, rSemicolon:
+ return d.readComment()
+ case ' ', '\t', '\n', '\f', '\r', 0x85, 0xA0:
+ if err = d.skipSpace(true); err == io.EOF {
+ return nil, nil
}
- p.rb = p.rb[idx+1:]
+ return d.readElem, err
+ default:
+ return d.readKey()
+ }
+}
+
+var defaultSeparator = []byte{'.'}
+
+const None = "\x00\x00\x13\x15\xff\x00\x12\x00\x13"
+
+func (d *decoder) reset(cfg *Reader, dst Recorder, rd io.Reader) {
+ const defaultBufferCap = 64
+
+ if cfg == nil {
+ cfg = &DefaultDecoder
}
- p.quoted = parts[:0]
- switch len(parts) {
- case 0:
- return ``, nil
- case 1:
- return string(parts[0]), nil
+ if rx, ok := rd.(runeReader); ok {
+ d.readrune = rx.ReadRune
+ } else {
+ d.readrune = nil
+ }
+
+ switch cfg.Casing {
+ case UpperCase:
+ d.casefn = unicode.ToUpper
+ case LowerCase:
+ d.casefn = unicode.ToLower
default:
- return string(bytes.Join(parts, nil)), nil
+ d.casefn = nil
+ }
+
+ d.rd = rd
+ d.err = nil
+ d.dst = dst
+
+ d.current = 0
+ d.line = 1
+ d.col = 0
+
+ if cfg.True == None {
+ d.true = ""
+ } else if cfg.True != "" {
+ d.true = cfg.True
+ } else {
+ d.true = True
}
+
+ if cfg.Separator == None {
+ d.sep = nil
+ } else if cfg.Separator != "" {
+ d.sep = append(d.sep2[:0], cfg.Separator...)
+ } else {
+ d.sep = d.sep2[:1]
+ d.sep[0] = '.'
+ }
+
+ d.buffer.Reset()
+ d.buffer.Grow(defaultBufferCap)
+
+ d.key = ""
+ if d.prefix == nil {
+ d.prefix = d.prefix2[:0]
+ }
+
+ d.havenext = false
+ d.nexterr = nil
}
-// ReadINI accepts a slice of bytes containing a string of bytes ostensibly
-// parse-able as an INI and returns a map of strings to strings.
-//
-// Section names, such as "[buzzsaw]", are treated as prefixes to keys that
-// follow them. So, for example, a "key = value" pair following a buzzsaw
-// section would be recorded in the resulting map as
-// map[string]string{"buzzsaw.key":"value"}.
-//
-// Value-less keys are treated as boolean flags and will be set to "1".
-//
-// Values enclosed in double quotes can contain newlines and escape characters
-// supported by Go (\a, \b, \f, \n, \r, \t, \v, as well as escaped quotes and
-// backslashes and \0 for the NUL character).
-func ReadINI(b []byte, out map[string][]string) (map[string][]string, error) {
- var l int = len(b)
- if l == 0 {
- return out, nil
+func (d *decoder) read() (err error) {
+ defer panictoerr(&err)
+ var next nextfunc = d.start
+ for next != nil && err == nil {
+ next, err = next()
}
+ return err
+}
- if out == nil {
- out = make(map[string][]string, defaultINICapacity)
+type KeyCase int
+
+const (
+ // LowerCase indicates that you want all unquoted subsections lower-cased. This is the
+ // default key casing.
+ LowerCase KeyCase = iota
+ // UpperCase indicates that you want all unquoted subsections upper-cased.
+ UpperCase
+ // CaseSensitive indicates that you want all unquoted subsections left as-is.
+ CaseSensitive
+)
+
+var DefaultDecoder = Reader{
+ Separator: ".",
+ Casing: CaseSensitive,
+ True: True,
+}
+
+// Recorder is any type that can accept INI values. Multiple calls to Add may occur with the same
+// key. It is up to the Recorder to decide if it discards or appends to prior versions of a key. If
+// Add panics, the value that it panics with is returned as an error.
+type Recorder interface {
+ Add(key, value string)
+}
+
+type Reader struct {
+ Separator string
+ Casing KeyCase
+ True string
+}
+
+func (d *Reader) Read(r io.Reader, dst Recorder) error {
+ var dec decoder
+ dec.reset(d, dst, r)
+ return dec.read()
+}
+
+// Utility functions
+
+func panictoerr(err *error) {
+ rc := recover()
+ if perr, ok := rc.(error); ok {
+ *err = perr
+ } else if rc != nil {
+ *err = fmt.Errorf("ini: panic: %v", rc)
}
- var p iniParser = iniParser{
- rb: b,
- quoted: make([][]byte, 0, 4),
- result: out,
+ if *err == io.EOF {
+ *err = io.ErrUnexpectedEOF
}
- var err error
- for len(p.rb) > 0 {
- err = p.readPrefix()
+}
- if err != nil {
- return nil, err
+func must(err error, allowed ...error) error {
+ if err == nil {
+ return err
+ }
+
+ for _, e := range allowed {
+ if e == err {
+ return err
}
+ }
+
+ panic(err)
+}
+
+func stopOnEOF(next *nextfunc, err *error) {
+ if *err == io.EOF {
+ *next = nil
+ *err = nil
+ }
+}
+
+// Rune handling
+
+type runeReader interface {
+ ReadRune() (rune, int, error)
+}
- if len(p.rb) == l {
- return nil, errors.New("Read could not advance")
+func readrune(rd io.Reader) (r rune, size int, err error) {
+ if rd, ok := rd.(runeReader); ok {
+ return rd.ReadRune()
+ }
+ var b [4]byte
+ for i, t := 0, 1; i < len(b); i, t = i+1, t+1 {
+ _, err = rd.Read(b[i:t])
+ if err != nil {
+ return r, size, err
+ } else if c := b[:t]; utf8.FullRune(c) {
+ r, size = utf8.DecodeRune(c)
+ return r, size, err
}
- l = len(p.rb)
}
- return out, nil
+ return unicode.ReplacementChar, 1, nil
+}
+
+type (
+ runeset interface {
+ Contains(rune) bool
+ }
+
+ oneRune rune
+ runeFunc func(rune) bool
+ runestr string
+)
+
+func notRune(runes runeset) runeset {
+ return runeFunc(func(r rune) bool { return !runes.Contains(r) })
}
+
+func (s runestr) Contains(r rune) bool { return strings.ContainsRune(string(s), r) }
+
+func (fn runeFunc) Contains(r rune) bool { return fn(r) }
+
+func (lhs oneRune) Contains(rhs rune) bool { return rune(lhs) == rhs }
M ini_test.go => ini_test.go +262 -112
@@ 1,155 1,281 @@
package ini
import (
+ "bytes"
+ "io"
"reflect"
+ "strings"
"testing"
+ "testing/iotest"
)
+var succReaders = map[string]func(string) io.Reader{
+ "bytes.Buffer": func(s string) io.Reader { return bytes.NewBufferString(s) },
+ "strings.Reader": func(s string) io.Reader { return strings.NewReader(s) },
+ "iotest.OneByteReader": func(s string) io.Reader { return iotest.OneByteReader(strings.NewReader(s)) },
+}
+
+var failReaders = map[string]func(string) io.Reader{
+ "bytes.Buffer": func(s string) io.Reader { return bytes.NewBufferString(s) },
+ "strings.Reader": func(s string) io.Reader { return strings.NewReader(s) },
+ "iotest.OneByteReader": func(s string) io.Reader { return iotest.OneByteReader(strings.NewReader(s)) },
+ "iotest.DataErrReader": func(s string) io.Reader { return iotest.DataErrReader(strings.NewReader(s)) },
+ "iotest.HalfReader": func(s string) io.Reader { return iotest.HalfReader(strings.NewReader(s)) },
+ "iotest.TimeoutReader": func(s string) io.Reader { return iotest.TimeoutReader(strings.NewReader(s)) },
+}
+
+func TestPanicToErr_nonerr(t *testing.T) {
+ var err error
+ func() {
+ defer panictoerr(&err)
+ panic("foobar!")
+ }()
+
+ if want := "ini: panic: foobar!"; err == nil || err.Error() != want {
+ t.Errorf("err(%v) = %v; want %q", err, want)
+ }
+}
+
+func TestReadINI_altsep(t *testing.T) {
+ dec := Reader{
+ Separator: "_-_",
+ Casing: UpperCase,
+ }
+ testReadINIMatching(t, &dec, "[section name] abc = 1234", Values{"SECTION_-_NAME_-_ABC": []string{"1234"}})
+}
+
+func TestReadINI_keyless(t *testing.T) {
+ testReadINIError(t, "[section name] \n= ; \n")
+ testReadINIMatching(t, nil, "[section name] abc", Values{"section.name.abc": []string{True}})
+}
+
+func TestReadINI_section_badspace(t *testing.T) {
+ testReadINIError(t, "[\f\r\n] \n= ; \n") // expected section name
+}
+
func TestReadINIEmpty(t *testing.T) {
- testReadINIMatching(t, "\n\t\n;empty\n\t\n\t", map[string][]string{})
+ testReadINIMatching(t, nil, "\n\t\n;empty\n\t\n\t", Values{})
+}
+
+func TestReadINI_rawval(t *testing.T) {
+ // at start
+ testReadINIMatching(t, nil, "k = ```raw`` value`", Values{"k": []string{"`raw` value"}})
+ // single quote
+ testReadINIMatching(t, nil, "k = ````", Values{"k": []string{"`"}})
+ // middle
+ testReadINIMatching(t, nil, "k = `raw ``value`` surrounded`", Values{"k": []string{"raw `value` surrounded"}})
+ // at end
+ testReadINIMatching(t, nil, "k = `raw ``value```", Values{"k": []string{"raw `value`"}})
+
+ // Unclosed raw
+ testReadINIError(t, "k = `with raw quote")
+ // Unclosed regular
+ testReadINIError(t, `k = "with regular quote`)
}
func TestReadINISectionSpaces(t *testing.T) {
- // Errors
- testReadINIError(t, "\n[newline section\n]\nk = v\n")
- testReadINIError(t, "\n[\nnewline section]\nk = v\n")
- testReadINIError(t, "\n[\nnewline\nsection]\nk = v\n")
+
+ // Empty section
+ testReadINIMatching(t, nil, "\n[ ]\nk = v\n", Values{`k`: []string{`v`}})
// Good
- expected := map[string][]string{`section.ok.k`: []string{`v`}}
- testReadINIMatching(t, `
- [section ok]
- k = v
+ expected := Values{`section.OK.k`: []string{True}}
+ testReadINIMatching(t, &Reader{Casing: LowerCase}, `
+ [section "OK"]
+ K
`,
expected)
+
+ expected = Values{`section.ok.k`: []string{`v`}}
+ testReadINIMatching(t, &Reader{Casing: LowerCase}, `
+ [Section OK]
+ K = v
+ `,
+ expected)
+
+ // Errors
+ testReadINIError(t, "\n[newline section\n]\nk = v\n")
+ testReadINIError(t, "\n[\nnewline section]\nk = v\n")
+ testReadINIError(t, "\n[\nnewline\nsection]\nk = v\n")
}
func TestReadQuotedMulti(t *testing.T) {
src := `
- [foo "http://git.spiff.io"]
- insteadOf = left
- insteadOf = right
+ [foo HTTP://GIT.SPIFF.IO.FOO ]
+ insteadOf = ` + "`left`" + ` ; comment
+ insteadOf = center;
+ insteadOf = "right"; comment
+ [foo "HTTP:\\GIT.SPIFF.IO\x00\u00AB\U00007fff"]
+ insteadOf = ` + "`left`" + ` ; comment
+ insteadOf = center;
+ insteadOf = "right"; comment
+ [foo """HTTP://GIT.SPIFF.IO"""]
+ insteadOf = ` + "`left`" + ` # comment
+ insteadOf = center#
+ insteadOf = "right"# comment
`
- expected := map[string][]string{
- `foo.http://git.spiff.io.insteadOf`: []string{"left", "right"},
+ expected := Values{
+ `foo.HTTP://GIT.SPIFF.IO.FOO.insteadOf`: []string{"left", "center", "right"},
+ "foo.HTTP:\\GIT.SPIFF.IO\x00\u00AB\U00007FFF.insteadOf": []string{"left", "center", "right"},
+ `foo."HTTP://GIT.SPIFF.IO".insteadOf`: []string{"left", "center", "right"},
}
- testReadINIMatching(t, src, expected)
+ testReadINIMatching(t, nil, src, expected)
+ testReadINIError(t, "[section `with raw quote`]")
+ testReadINIError(t, `[section "with unclosed quote`)
+ testReadINIError(t, `[section "with escape at eof \`)
}
func TestReadINISectionValueComment(t *testing.T) {
- testReadINIMatching(t,
- ` key = ; `,
- map[string][]string{
- `key`: []string{``},
+ testReadINIMatching(t, nil,
+ `[section "Quoted Subsection"] key = ; Comment`,
+ Values{
+ `section.Quoted Subsection.key`: []string{``},
},
)
}
func TestReadINIValueNewline(t *testing.T) {
- expected := map[string][]string{`key`: []string{``}}
- testReadINIMatching(t, " key = \n ", expected)
- testReadINIMatching(t, " key =\n ", expected)
- testReadINIMatching(t, " key=\n ", expected)
- testReadINIMatching(t, "\nkey=\n ", expected)
- testReadINIMatching(t, "key=\n ", expected)
- testReadINIMatching(t, "key\t=\n ", expected)
- testReadINIMatching(t, "key\t=\t\n ", expected)
- testReadINIMatching(t, "key=\t\n ", expected)
+ expected := Values{`key`: []string{``}}
+ testReadINIMatching(t, nil, " key = \n ", expected)
+ testReadINIMatching(t, nil, " key =\n ", expected)
+ testReadINIMatching(t, nil, " key=\n ", expected)
+ testReadINIMatching(t, nil, "\nkey=\n ", expected)
+ testReadINIMatching(t, nil, "key=\n ", expected)
+ testReadINIMatching(t, nil, "key\t=\n ", expected)
+ testReadINIMatching(t, nil, "key\t=\t\n ", expected)
+ testReadINIMatching(t, nil, "key=\t\n ", expected)
}
func TestReadINIValueSimple(t *testing.T) {
- expected := map[string][]string{`key`: []string{`value`}}
+ expected := Values{`key`: []string{`value`}}
// In the interest of being possibly unusually thorough.
- testReadINIMatching(t, " key = value ", expected)
- testReadINIMatching(t, " key=value ", expected)
- testReadINIMatching(t, " key= value ", expected)
- testReadINIMatching(t, " key =value ", expected)
- testReadINIMatching(t, " key\t=\tvalue ", expected)
- testReadINIMatching(t, "\tkey\t=\tvalue\t", expected)
- testReadINIMatching(t, "\tkey\t=value\t", expected)
- testReadINIMatching(t, "\tkey=\tvalue\t", expected)
- testReadINIMatching(t, "\tkey=value\t", expected)
+ testReadINIMatching(t, nil, " key = value ", expected)
+ testReadINIMatching(t, nil, " key=value ", expected)
+ testReadINIMatching(t, nil, " key= value ", expected)
+ testReadINIMatching(t, nil, " key =value ", expected)
+ testReadINIMatching(t, nil, " key\t=\tvalue ", expected)
+ testReadINIMatching(t, nil, "\tkey\t=\tvalue\t", expected)
+ testReadINIMatching(t, nil, "\tkey\t=value\t", expected)
+ testReadINIMatching(t, nil, "\tkey=\tvalue\t", expected)
+ testReadINIMatching(t, nil, "\tkey=value\t", expected)
}
func TestReadINIFlagSimple(t *testing.T) {
- expected := map[string][]string{
- `key`: []string{True},
- }
+ var (
+ expected = Values{`key`: []string{True}}
+ empty = Values{`key`: []string{""}}
+ )
- testReadINIMatching(t, "key", expected)
- testReadINIMatching(t, " key ", expected)
- testReadINIMatching(t, " key", expected)
- testReadINIMatching(t, " key;comment", expected)
- testReadINIMatching(t, " key ; comment", expected)
- testReadINIMatching(t, " \nkey ", expected)
- testReadINIMatching(t, " \nkey", expected)
- testReadINIMatching(t, " \nkey\n", expected)
- testReadINIMatching(t, " key \n", expected)
- testReadINIMatching(t, " key\n ", expected)
- testReadINIMatching(t, "\tkey\t\n", expected)
- testReadINIMatching(t, "\tkey\t", expected)
- testReadINIMatching(t, "\tkey", expected)
- testReadINIMatching(t, "key\t", expected)
+ // empty
+ testReadINIMatching(t, nil, "key=", empty)
+ testReadINIMatching(t, nil, "key=", empty)
+ testReadINIMatching(t, nil, "key=``", empty)
+ testReadINIMatching(t, nil, `key=""`, empty)
+ testReadINIMatching(t, nil, "key= ", empty)
+ testReadINIMatching(t, nil, "key= ``", empty)
+ testReadINIMatching(t, nil, `key= ""`, empty)
+ testReadINIMatching(t, nil, "key = ", empty)
+ testReadINIMatching(t, nil, "key = ``", empty)
+ testReadINIMatching(t, nil, `key = ""`, empty)
+ testReadINIMatching(t, nil, "[] key = ", empty)
+ testReadINIMatching(t, nil, "[] key = ``", empty)
+ testReadINIMatching(t, nil, `[] key = ""`, empty)
+
+ // true
+ testReadINIMatching(t, nil, "key", expected)
+ testReadINIMatching(t, nil, " key ", expected)
+ testReadINIMatching(t, nil, " key", expected)
+ testReadINIMatching(t, nil, " key;comment", expected)
+ testReadINIMatching(t, nil, " key ; comment", expected)
+ testReadINIMatching(t, nil, " \nkey ", expected)
+ testReadINIMatching(t, nil, " \nkey", expected)
+ testReadINIMatching(t, nil, " \nkey\n", expected)
+ testReadINIMatching(t, nil, " key \n", expected)
+ testReadINIMatching(t, nil, " key\n ", expected)
+ testReadINIMatching(t, nil, "\tkey\t\n", expected)
+ testReadINIMatching(t, nil, "\tkey\t", expected)
+ testReadINIMatching(t, nil, "\tkey", expected)
+ testReadINIMatching(t, nil, "key\t", expected)
testReadINIError(t, "key spaced")
}
+func TestReadINI_hexstring(t *testing.T) {
+ expected := Values{
+ "hex": []string{"\x00\u00ab\u00AB\U0000ABAB"},
+ "raw": []string{`\x00\u00ab\u00AB\U0000ABAB`},
+ }
+
+ testReadINIMatching(t, nil, (`
+hex = "\x00\u00ab\u00AB\U0000ABAB"
+raw = ` + "`" + `\x00\u00ab\u00AB\U0000ABAB` + "`" + ``)[1:],
+ expected)
+
+ testReadINIError(t, `hex = "\xg0\u00ab\u00AB\U0000ABAB"`) // Fail on bad character
+ testReadINIError(t, `hex = "\x😀0\u00ab\u00AB\U0000ABAB"`) // Fail on size > 1
+ testReadINIError(t, `hex = "\x`) // Fail on EOF
+ testReadINIError(t, `hex = "\x1`) // Fail on EOF
+ testReadINIError(t, `hex = "\x12`) // Fail on EOF, but not in readHexCode
+}
+
func TestReadINIUnicode(t *testing.T) {
- expected := map[string][]string{
+ expected := Values{
"-_kŭjəl_-": []string{"käkə-pō"},
}
- testReadINIMatching(t, "-_kŭjəl_- = käkə-pō", expected)
- testReadINIMatching(t, "-_kŭjəl_-=käkə-pō", expected)
- testReadINIMatching(t, "\t-_kŭjəl_-\t=\tkäkə-pō\t", expected)
-
- testReadINIError(t, "-_kŭj′əl_-")
- testReadINIError(t, " -_kŭj′əl_-")
- testReadINIError(t, "-_kŭj′əl_- ")
- testReadINIError(t, " -_kŭj′əl_- ")
- testReadINIError(t, "-_kŭj′əl_-\t")
- testReadINIError(t, "\t-_kŭj′əl_-")
- testReadINIError(t, "\t-_kŭj′əl_-\t")
+ testReadINIMatching(t, nil, "-_kŭjəl_- = käkə-pō", expected)
+ testReadINIMatching(t, nil, "-_kŭjəl_-=käkə-pō", expected)
+ testReadINIMatching(t, nil, "\t-_kŭjəl_-\t=\tkäkə-pō\t", expected)
+ testReadINIMatching(t, nil, "-_kŭj′əl_-", Values{"-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, "[WUBWUB]-_kŭj′əl_-", Values{"WUBWUB.-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, &Reader{Casing: LowerCase}, "[WUBWUB]-_kŭj′əl_-", Values{"wubwub.-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, "-_kŭj′əl_- ", Values{"-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, " -_kŭj′əl_- ", Values{"-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, "-_kŭj′əl_-\t", Values{"-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, "\t-_kŭj′əl_-", Values{"-_kŭj′əl_-": []string{True}})
+ testReadINIMatching(t, nil, "\t-_kŭj′əl_-\t", Values{"-_kŭj′əl_-": []string{True}})
}
func TestReadMultiline(t *testing.T) {
- expected := map[string][]string{
+ expected := Values{
`foo`: []string{True},
`bar`: []string{``},
`baz`: []string{`value`},
}
- testReadINIMatching(t, "foo\nbar=;\nbaz=value", expected)
- testReadINIMatching(t, "foo;\nbar=\nbaz=value", expected)
- testReadINIMatching(t, "foo\nbar=\nbaz = value", expected)
- testReadINIMatching(t, "foo\t\n\tbar =\nbaz = value", expected)
- testReadINIMatching(t, "foo\t\n\tbar =\nbaz = \"value\"", expected)
+ testReadINIMatching(t, nil, "foo\nbar=;\nbaz=value", expected)
+ testReadINIMatching(t, nil, "foo;\nbar=\nbaz=value", expected)
+ testReadINIMatching(t, nil, "foo\nbar=\nbaz = value", expected)
+ testReadINIMatching(t, nil, "foo\t\n\tbar =\nbaz = value", expected)
+ testReadINIMatching(t, nil, "foo\t\n\tbar =\nbaz = \"value\"", expected)
}
func TestReadQuoted(t *testing.T) {
- expected := map[string][]string{
+ expected := Values{
`normal`: []string{` a thing `},
`escaped`: []string{string([]byte{0}) + "\a\b\f\n\r\t\v\\\"jkl;"},
`raw`: []string{"a\n`b`\nc\n"},
- `quote`: []string{"`", `"`},
+ `quote`: []string{"`", `"""`, `""`},
}
// In the interest of being possibly unusually thorough.
- testReadINIMatching(t, `
+ testReadINIMatching(t, nil, `
; Test a fairly normal string
normal = " a thing "
escaped = "\0\a\b\f\n\r\t\v\\\"\j\k\l\;"
raw = `+"`a\n``b``\nc\n`"+`
quote = `+"````"+`
- quote = `+"`\"`",
+ quote = `+"`\"\"\"`"+`
+ quote = """"""`,
expected)
- testReadINIMatching(t, `
+ testReadINIMatching(t, nil, `
; Test one with inline characters that could be escaped.
normal = " a thing "
escaped = "\0\a\b\f
\r \v\\\"\j\k\l\;" ; Tests escaping non-escape characters as themselves
raw = `+"`a\n``b``\nc\n`"+`
quote = `+"````"+`
- quote = `+"`\"`",
+ quote = `+"`\"\"\"`"+`
+ quote = """"""`,
expected)
testReadINIError(t, `unterminated = "`)
@@ 180,70 306,94 @@ lmn
[]
no_prefix = this has no prefix
+zero = "\x00\xaA\x0F\u00AB\U0000ABAB"
`
- keys := map[string][]string{
+ keys := Values{
`a`: []string{"5\n\n"},
`prefix.foo.a`: []string{`value of "a"`},
`prefix.foo.b`: []string{`unhandled`},
- `prefix.foo.c`: []string{`1`},
+ `prefix.foo.c`: []string{True},
`prefix.bar.d`: []string{``},
`prefix.bar.efg`: []string{``},
- `prefix.bar.hij`: []string{`1`},
- `prefix.bar.k`: []string{`1`},
- `prefix.bar.lmn`: []string{`1`},
+ `prefix.bar.hij`: []string{True},
+ `prefix.bar.k`: []string{True},
+ `prefix.bar.lmn`: []string{True},
`no_prefix`: []string{`this has no prefix`},
+ `zero`: []string{"\x00\xaA\x0F\u00AB\U0000ABAB"},
}
- testReadINIMatching(t, s, keys)
+ testReadINIMatching(t, nil, s, keys)
}
-func testReadINIMatching(t *testing.T, b string, expected map[string][]string) {
- actual, err := ReadINI([]byte(b), nil)
-
- t.Logf("Parsing:\n%s", b)
+func testReadINIMatching(t *testing.T, dec *Reader, b string, expected map[string][]string) {
+ defer pushlog(t)()
+ check := func(actual Values, err error) {
+ dlog(1, actual)
+ defer func() {
+ if t.Failed() {
+ dlogf(2, "Failed to parse:\n%s", b)
+ t.FailNow()
+ } else {
+ dlog(2, "Succeeded")
+ }
+ }()
+
+ if err != nil {
+ elog(2, "Error reading INI:", err)
+ return
+ }
- if err != nil {
- t.Error("Error reading INI:", err)
- }
+ if actual == nil {
+ elog(2, "Returned map is nil")
+ return
+ } else if len(actual) != len(expected) {
+ elogf(2, "Returned map has %d values, expected %d", len(actual), len(expected))
+ }
- if actual == nil {
- t.Fatalf("Returned map is nil")
- } else if len(actual) != len(expected) {
- t.Errorf("Returned map has %d values, expected %d", len(actual), len(expected))
- }
+ for k, v := range expected {
+ mv, ok := actual[k]
+ if !ok {
+ elogf(2, "Result map does not contain key %q", k)
+ }
- for k, v := range expected {
- mv, ok := actual[k]
- if !ok {
- t.Errorf("Result map does not contain key %q", k)
+ if !reflect.DeepEqual(v, mv) {
+ elogf(2, "Value of %q in result map %q != (expected) %q", k, mv, v)
+ }
}
- if !reflect.DeepEqual(v, mv) {
- t.Errorf("Value of %q in result map %q != (expected) %q", k, mv, v)
+ for k := range actual {
+ _, ok := expected[k]
+ if ok {
+ continue
+ }
+ elogf(2, "Key %q in result is not in expected results", k)
}
}
- for k := range actual {
- _, ok := expected[k]
- if ok {
- continue
- }
- t.Errorf("Key %q in result is not in expected results", k)
+ if dec == nil {
+ check(ReadINI([]byte(b), nil))
+ }
+ for desc, fn := range succReaders {
+ r := fn(b)
+ dlog(1, "Testing with reader: ", desc)
+ dst := Values{}
+ check(dst, dec.Read(r, dst))
}
}
func testReadINIError(t *testing.T, b string) error {
+ defer pushlog(t)()
actual, err := ReadINI([]byte(b), nil)
if err == nil {
- t.Errorf("Expected error, got nil")
+ elog(1, "Expected error, got nil")
} else {
- t.Log("Error returned:", err)
+ dlog(1, "Error returned: ", err)
}
if actual != nil {
- t.Errorf("Returned map isn't nil")
+ elog(1, "Returned map isn't nil")
}
return err
A log_test.go => log_test.go +68 -0
@@ 0,0 1,68 @@
+//build +testing
+
+package ini
+
+import (
+ "fmt"
+ "path"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+var (
+ debugLogFn func(v ...interface{})
+ errorLogFn func(v ...interface{})
+)
+
+func pushlog(t *testing.T) func() {
+ d, e := debugLogFn, errorLogFn
+ debugLogFn = t.Log
+ errorLogFn = t.Error
+ return func() {
+ debugLogFn, errorLogFn = d, e
+ }
+}
+
+// Info
+
+func debugPrefix(depth, minlen int) string {
+ pc, file, line, _ := runtime.Caller(depth + 2)
+ fname := "<unknown>"
+ if fn := runtime.FuncForPC(pc); fn != nil {
+ fname = path.Base(fn.Name())
+ }
+
+ prefix := fmt.Sprint(filepath.Base(file), ":", line, ":", fname, ": ")
+ if len(prefix) < minlen {
+ prefix = prefix + strings.Repeat(" ", minlen-len(prefix))
+ }
+ return prefix
+}
+
+func dlog(depth int, v ...interface{}) {
+ if debugLogFn != nil {
+ debugLogFn("D", debugPrefix(depth, 64)+fmt.Sprint(v...))
+ }
+}
+
+func dlogf(depth int, format string, v ...interface{}) {
+ if debugLogFn != nil {
+ debugLogFn("D", debugPrefix(depth, 64)+fmt.Sprintf(format, v...))
+ }
+}
+
+// Errors
+
+func elog(depth int, v ...interface{}) {
+ if errorLogFn != nil {
+ errorLogFn("E", debugPrefix(depth, 52)+fmt.Sprint(v...))
+ }
+}
+
+func elogf(depth int, format string, v ...interface{}) {
+ if errorLogFn != nil {
+ errorLogFn("E", debugPrefix(depth, 52)+fmt.Sprintf(format, v...))
+ }
+}
A values_test.go => values_test.go +92 -0
@@ 0,0 1,92 @@
+package ini
+
+import (
+ "reflect"
+ "strings"
+ "testing"
+)
+
+func TestValues_copy(t *testing.T) {
+ expectedCopied := Values{
+ "foo": []string{"baz", "quux"},
+ "wub": []string{"456"},
+ }
+ expectedCopiedTo := Values{
+ "foo": []string{"bar", "baz", "quux"},
+ "wub": []string{"456"},
+ }
+
+ v := Values{}
+
+ v.Add("foo", "baz")
+ v.Add("foo", "quux")
+ v.Set("wub", "123")
+ v.Set("wub", "456")
+ v.Set("bob", "someone")
+ v.Del("bob")
+
+ dup := v.Copy(nil)
+ duppedTo := v.Copy(Values{"foo": []string{"bar"}})
+
+ if !reflect.DeepEqual(v, expectedCopied) {
+ t.Errorf("v = %#v; want %#v", v, expectedCopied)
+ }
+ if !reflect.DeepEqual(dup, expectedCopied) {
+ t.Errorf("dup = %#v; want %#v", dup, expectedCopied)
+ }
+ if !reflect.DeepEqual(duppedTo, expectedCopiedTo) {
+ t.Errorf("duppedTo = %#v; want %#v", duppedTo, expectedCopiedTo)
+ }
+
+ check := func(k, want string) {
+ if got := expectedCopiedTo.Get(k); got != want {
+ t.Errorf("duppedTo.Get(%q) = %q; want %q", k, got, want)
+ }
+ }
+
+ check("foo", "bar")
+ check("nothing", "")
+}
+
+func TestValues_contains(t *testing.T) {
+ v := Values{"foo": nil}
+ check := func(k string, want bool) {
+ if got := v.Contains(k); want != got {
+ t.Errorf("v.Contains(%q) = %t; want %t", k, got, want)
+ }
+ }
+
+ check("foo", true)
+ check("not.present", false)
+}
+
+func TestValues_matching(t *testing.T) {
+ v := Values{
+ "foo.bar": nil,
+ "foo.baz": []string{"x"},
+ "quux.bar": []string{"wop"},
+ "foo": []string{"a thing"},
+ }
+
+ check := func(got, expected Values) {
+ if !reflect.DeepEqual(expected, got) {
+ t.Errorf("v.Matching(...) = %#v; want %#v", got, expected)
+ } else {
+ t.Log("v.Matching(...) = %#v", got)
+ }
+ }
+
+ check(v.Matching(nil, func(s string, _ []string) bool {
+ return strings.HasPrefix(s, "foo.")
+ }), Values{
+ "foo.bar": nil,
+ "foo.baz": []string{"x"},
+ })
+
+ check(v.Matching(Values{"foo.baz": []string{"y"}}, func(s string, _ []string) bool {
+ return strings.HasPrefix(s, "foo.")
+ }), Values{
+ "foo.bar": nil,
+ "foo.baz": []string{"y", "x"},
+ })
+}