From b41edb8bfe7c68b9aeab2a99fabfd8ecd7ecd1a9 Mon Sep 17 00:00:00 2001 From: Noel Cower Date: Tue, 16 Feb 2021 17:25:20 -0800 Subject: [PATCH] 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. --- errors.go | 20 ++++++++++++++++++ ini.go | 63 ++++++++++++++++++++++++++++++++++++------------------- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/errors.go b/errors.go index 67efa64..d2c68d0 100644 --- a/errors.go +++ b/errors.go @@ -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 +} diff --git a/ini.go b/ini.go index a9c2ce2..cd84914 100644 --- a/ini.go +++ b/ini.go @@ -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) -- 2.45.2