~nilium/go-ini

b41edb8bfe7c68b9aeab2a99fabfd8ecd7ecd1a9 — Noel Cower 3 years ago 4af0597 main v0.2.0
breaking: Allow a Recorder to return an error

When a Recorder's Add method is called, allow it to return an error.
This is a breaking change to go-ini to support cases where a recorder
fails (e.g., when streaming key-value pairs). This can be used to break
out of a recording loop, though go-ini won't do anything to detect
specific cases of this.

When a recorder returns an error, the reader will always wrap that
error in a RecordingError.

Also add an Unwrap method to SyntaxError to support error unwrapping.
2 files changed, 61 insertions(+), 22 deletions(-)

M errors.go
M ini.go
M errors.go => errors.go +20 -0
@@ 21,6 21,10 @@ func (s *SyntaxError) Error() string {
	return fmt.Sprintf("ini: syntax error at %d:%d: %v -- %s", s.Line, s.Col, s.Err, s.Desc)
}

func (s *SyntaxError) Unwrap() error {
	return s.Err
}

// UnclosedError is an error describing an unclosed bracket from {, (, [, and <. It is typically set
// as the Err field of a SyntaxError.
//


@@ 67,3 71,19 @@ var (
	// ErrBadNewline is a BadCharError for unexpected newlines.
	ErrBadNewline = BadCharError('\n')
)

// RecordingError wraps any error returned by a Recorder, and includes both the key and value that
// caused the error. The error message for a RecordingError does not include the value, as that may
// contain sensitive data. The key is assumed to be harmless.
type RecordingError struct {
	Key, Value string
	Err        error
}

func (r *RecordingError) Error() string {
	return fmt.Sprintf("error recording key=%q: %v", r.Key, r.Err)
}

func (r *RecordingError) Unwrap() error {
	return r.Err
}

M ini.go => ini.go +41 -22
@@ 3,6 3,7 @@ package ini // import "go.spiff.io/go-ini"

import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"strings"


@@ 48,8 49,9 @@ func (v Values) Set(key, value string) {
}

// Add adds a value to key's value slice (allocating one if none is present).
func (v Values) Add(key, value string) {
func (v Values) Add(key, value string) error {
	v[key] = append(v[key], value)
	return nil
}

// Get returns the first value for key. If key does not exist or has an empty value slice, Get


@@ 160,10 162,20 @@ func ReadINI(b []byte, out Values) (Values, error) {
	return out, err
}

func (d *decoder) add(key, value string) {
	if d.dst != nil {
		d.dst.Add(key, value)
func (d *decoder) add(key, value string) error {
	if d.dst == nil {
		return nil

	}
	err := d.dst.Add(key, value)
	if err != nil {
		err = &RecordingError{
			Key:   key,
			Value: value,
			Err:   err,
		}
	}
	return err
}

func (d *decoder) syntaxerr(err error, msg ...interface{}) *SyntaxError {


@@ 316,8 328,7 @@ func (d *decoder) readKey() (nextfunc, error) {
	}

	if errors.Is(err, io.EOF) {
		d.add(d.buffer.String(), d.true)
		return nil, nil
		return nil, d.add(d.buffer.String(), d.true)
	}

	d.key = d.buffer.String()


@@ 328,25 339,24 @@ func (d *decoder) readKey() (nextfunc, error) {

func (d *decoder) readValueSep() (next nextfunc, err error) {
	if err = must(d.skipSpace(false), io.EOF, nil); errors.Is(err, io.EOF) {
		d.add(d.key, d.true)
		return nil, nil
		return nil, d.add(d.key, d.true)
	}

	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)
		if err := d.add(d.key, d.true); err != nil {
			return nil, err
		}
		return d.readElem, d.skip()
	case rEquals:
		if err = d.skip(); errors.Is(err, io.EOF) {
			d.add(d.key, "")
			return nil, nil
			return nil, d.add(d.key, "")
		}
		return d.readValue, nil
	case rHash, rSemicolon:
		d.add(d.key, d.true)
		return d.readComment, nil
		return d.readComment, d.add(d.key, d.true)
	default:
		return nil, d.syntaxerr(BadCharError(d.current), "expected either =, newline, or a comment")
	}


@@ 414,7 424,9 @@ func (d *decoder) readStringValue() (next nextfunc, err error) {
	}

	defer stopOnEOF(&next, &err)
	d.add(d.key, d.buffer.String())
	if err := d.add(d.key, d.buffer.String()); err != nil {
		return nil, err
	}
	return d.readElem, d.skip()
}



@@ 432,21 444,24 @@ func (d *decoder) readRawValue() (next nextfunc, err error) {
	}

	defer stopOnEOF(&next, &err)
	d.add(d.key, d.buffer.String())
	if err := d.add(d.key, d.buffer.String()); err != nil {
		return nil, err
	}
	return d.readElem, d.skip()
}

func (d *decoder) readValue() (next nextfunc, err error) {
	if err = must(d.skipSpace(false), io.EOF); errors.Is(err, io.EOF) {
		d.add(d.key, "")
		return nil, nil
		return nil, d.add(d.key, "")
	}

	switch d.current {
	case rNewline:
		// Terminated by newline
		defer stopOnEOF(&next, &err)
		d.add(d.key, "")
		if err := d.add(d.key, ""); err != nil {
			return nil, err
		}
		return d.readElem, d.skip()
	case rQuote:
		return d.readStringValue, nil


@@ 454,8 469,7 @@ func (d *decoder) readValue() (next nextfunc, err error) {
		return d.readRawValue, nil
	case rHash, rSemicolon:
		// Terminated by comment
		d.add(d.key, "")
		return d.readComment, nil
		return d.readComment, d.add(d.key, "")
	}

	defer stopOnEOF(&next, &err)


@@ 463,7 477,9 @@ func (d *decoder) readValue() (next nextfunc, err error) {
	must(d.readUntil(runestr("\n;#"), true, nil), io.EOF)

	value := string(bytes.TrimRightFunc(d.buffer.Bytes(), unicode.IsSpace))
	d.add(d.key, value)
	if err := d.add(d.key, value); err != nil {
		return nil, err
	}
	return d.readElem, err
}



@@ 694,7 710,7 @@ var DefaultDecoder = Reader{
// 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)
	Add(key, value string) error
}

// Reader is an INI reader configuration. It does not hold state and may be copied as needed.


@@ 717,6 733,9 @@ type Reader struct {

// Read decodes INI file input from r and conveys it to dst. If an error occurs, it is returned. If
// the error is an EOF before parsing is finished, io.ErrUnexpectedEOF is returned.
//
// If the Recorder returns an error when adding a key, an error of type *RecordingError is returned,
// wrapping the error that the Recorder returned.
func (d *Reader) Read(r io.Reader, dst Recorder) error {
	var dec decoder
	dec.reset(d, dst, r)