~nilium/go-ini

dec51faa90abb59144c18a66eb4893641d810f86 — Noel Cower 8 years ago 06da360
Rewrite INI parser

- Now slightly more like Git's config behavior, but not quote.
  All section names are now lowercase except for quoted sections (i.e.,
  those in double quotes). Raw-quoted section names aren't supported
  right now just because that seems like it may not be worth doing. By
  default, readers are still case sensitive, but this can be changed to
  force lower- or upper-casing of unquoted section names.
- Double-quotes are now escape-able with "" (e.g., "foo ""bar"" baz" is
  a valid string).
- Keys may now contain a wider range of characters. This means that
  some tests that were previously supposed to fail are no longer
  allowed to fail.
- Although not yet something you can explicitly use, the INI decoder
  does rely on io.Reader behavior to function. In addition, the decoder
  has its own internal snapshot of the True string it'll use for bare
  keys.
- Syntax errors are hopefully clearer and are explained where possible.
- Recorder is now used in place of map[string][]string when using the
  Reader. Values is intended to be compatible with it.
- Where map[string][]string occurs elsewhere, Values is used, because
  this provides a slightly easier means of interacting with the
  resulting map.
- Hex codes are supported in double-quoted strings (this includes
  section headers, which really need to be handled as a shared read
  function or something).
- Coverage went up a bit, but this isn't really a measure of whether the
  coverage is actually useful. So, grain of salt.
- The old ReadINI function still works more or less as intended. Its
  behavior can be changed by modifying the DefaultReader.

Performance has probably gone down, and allocations probably went up
(still testing that), but overall it seems reasonable to take an
improvement in implementation over performance here, since it's not
like you're going to be mass-reading INI files somewhere.

Change-Id: Ie5ddae9266274f896db0c45ce5b377509b7ba804
5 files changed, 1103 insertions(+), 447 deletions(-)

A errors.go
M ini.go
M ini_test.go
A log_test.go
A values_test.go
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"},
	})
}