Single Fetcher.Fetch on the Guard that holds DenyPrefixes. skipTlsVerification is ignored. No 502. keys add writes the secret only to --out. Release binary has no -test-allow-prefix.
363 lines
10 KiB
Go
363 lines
10 KiB
Go
package toml
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/pelletier/go-toml/v2/internal/parserbridge"
|
|
"github.com/pelletier/go-toml/v2/unstable"
|
|
)
|
|
|
|
// unmarshalFused decodes a whole document into a native map[string]interface{}
|
|
// tree with no reflection on the document structure, and without building an
|
|
// AST for table headers and scalar key-values. Only container values (arrays
|
|
// and inline tables) are parsed into the parser arena, so that the seen-tracker
|
|
// can validate them and decodeAny can presize the resulting slices and maps —
|
|
// the AST is what makes that cheap O(1) presizing possible.
|
|
//
|
|
// It is used when the target is a fully generic value (interface{} or
|
|
// map[string]interface{}) and the unmarshaler interface is disabled. The
|
|
// seen-tracker validates the document (duplicate keys, type consistency), so
|
|
// the builder creates and merges containers without revalidating. Strict mode
|
|
// never applies to a generic target (a map has no "unknown fields"), and
|
|
// captures never apply (a generic value implements no Unmarshaler).
|
|
func (d *decoder) unmarshalFused(root reflect.Value, data []byte) error {
|
|
var m map[string]interface{}
|
|
if !root.IsNil() {
|
|
// Decode into (merge with) an existing generic map when present.
|
|
if em, ok := root.Interface().(map[string]interface{}); ok {
|
|
m = em
|
|
}
|
|
}
|
|
if m == nil {
|
|
m = map[string]interface{}{}
|
|
}
|
|
|
|
if err := d.fusedDocument(m, data); err != nil {
|
|
return d.wrapFusedError(data, err)
|
|
}
|
|
|
|
if root.CanSet() {
|
|
root.Set(reflect.ValueOf(m))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// fusedDocument runs the top-level expression loop, mirroring
|
|
// Parser.NextExpression but storing values directly into native maps.
|
|
func (d *decoder) fusedDocument(m map[string]interface{}, b []byte) error {
|
|
cur := m
|
|
for {
|
|
b = fusedSkipWS(b)
|
|
if len(b) == 0 {
|
|
return nil
|
|
}
|
|
switch b[0] {
|
|
case '\n':
|
|
b = b[1:]
|
|
case '\r':
|
|
if len(b) > 1 && b[1] == '\n' {
|
|
b = b[2:]
|
|
continue
|
|
}
|
|
return unstable.NewParserError(b[:1], "expected newline but got %#U", b[0])
|
|
case '#':
|
|
_, rest, err := parserbridge.ScanComment(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rest, err = fusedConsumeEOL(rest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = rest
|
|
case '[':
|
|
rest, err := d.fusedTable(b, m, &cur)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = rest
|
|
default:
|
|
rest, err := d.fusedKeyVal(b, cur)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
b = rest
|
|
}
|
|
}
|
|
}
|
|
|
|
// fusedTable handles a [table] or [[array table]] header. b starts at '['. It
|
|
// updates *cur to the table the following key-values belong to.
|
|
func (d *decoder) fusedTable(b []byte, root map[string]interface{}, cur *map[string]interface{}) ([]byte, error) {
|
|
arrayTable := len(b) > 1 && b[1] == '['
|
|
|
|
var start []byte
|
|
if arrayTable {
|
|
start = fusedSkipWS(b[2:])
|
|
} else {
|
|
start = fusedSkipWS(b[1:])
|
|
}
|
|
|
|
var err error
|
|
var rawKey []byte
|
|
d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, start, d.keyParts[:0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if arrayTable {
|
|
if len(b) < 2 || b[0] != ']' || b[1] != ']' {
|
|
return nil, unstable.NewParserError(fusedHL1(b), "expected ']]' to close array table name")
|
|
}
|
|
b = b[2:]
|
|
} else {
|
|
if len(b) == 0 || b[0] != ']' {
|
|
return nil, unstable.NewParserError(fusedHL1(b), "expected ']' to close table name")
|
|
}
|
|
b = b[1:]
|
|
}
|
|
|
|
// The whole expression (including its line termination) is parsed before
|
|
// it is validated, to keep error precedence identical to the AST path.
|
|
b, err = d.fusedFinishLine(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if arrayTable {
|
|
first, err := d.seen.CheckArrayTable(d.keyParts)
|
|
if err != nil {
|
|
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
|
|
}
|
|
*cur = d.anyArrayTableParts(root, d.keyParts, first)
|
|
} else {
|
|
if _, err := d.seen.CheckTable(d.keyParts); err != nil {
|
|
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
|
|
}
|
|
*cur = d.anyTableParts(root, d.keyParts)
|
|
}
|
|
return b, nil
|
|
}
|
|
|
|
// fusedKeyVal handles a `key = value` expression relative to the current table
|
|
// cur. b starts at the first character of the key.
|
|
func (d *decoder) fusedKeyVal(b []byte, cur map[string]interface{}) ([]byte, error) {
|
|
var err error
|
|
var rawKey []byte
|
|
d.keyParts, rawKey, b, err = parserbridge.ScanKey(&d.p, b, d.keyParts[:0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(b) == 0 || b[0] != '=' {
|
|
return nil, unstable.NewParserError(fusedHL1(b), "expected '=' after key")
|
|
}
|
|
b = fusedSkipWS(b[1:])
|
|
if len(b) == 0 {
|
|
return nil, unstable.NewParserError(b, "expected value, not end of input")
|
|
}
|
|
|
|
if c := b[0]; c == '[' || c == '{' {
|
|
// Container value: build its AST so the seen-tracker can validate it
|
|
// and decodeAny can presize the resulting slices and maps.
|
|
nodeAny, rest, err := parserbridge.ParseValue(&d.p, b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
node := nodeAny.(*unstable.Node)
|
|
rest, err = d.fusedFinishLine(rest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
leafID, err := d.seen.CheckKeyValue(d.keyParts)
|
|
if err != nil {
|
|
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
|
|
}
|
|
if err := d.seen.CheckValueUnder(leafID, node); err != nil {
|
|
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
|
|
}
|
|
av, err := d.decodeAny(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.setFusedLeaf(cur, d.keyParts, av)
|
|
return rest, nil
|
|
}
|
|
|
|
// Scalar value: scan it without building a node, then validate and convert
|
|
// it natively.
|
|
k, _, value, rest, err := parserbridge.ScanScalar(&d.p, b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kind := unstable.Kind(k)
|
|
rest, err = d.fusedFinishLine(rest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := d.seen.CheckKeyValue(d.keyParts); err != nil {
|
|
return nil, d.fusedSeenError(rawKey, d.keyParts, err)
|
|
}
|
|
av, err := d.fusedScalar(kind, value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
d.setFusedLeaf(cur, d.keyParts, av)
|
|
return rest, nil
|
|
}
|
|
|
|
// fusedSeenError turns a bare error returned by a SeenTracker parts-method
|
|
// into a ParserError carrying the position (the raw key span) and key path of
|
|
// the offending expression, so that it is reported as a DecodeError with
|
|
// context. It mirrors decoder.wrapSeenError for the fused (AST-less) path.
|
|
func (d *decoder) fusedSeenError(rawKey []byte, parts [][]byte, err error) error {
|
|
key := make(Key, len(parts))
|
|
for i, p := range parts {
|
|
key[i] = string(p)
|
|
}
|
|
return &unstable.ParserError{
|
|
Highlight: rawKey,
|
|
Message: strings.TrimPrefix(err.Error(), "toml: "),
|
|
Key: key,
|
|
}
|
|
}
|
|
|
|
// fusedScalar converts a scanned scalar value into the native Go value used
|
|
// for generic targets. It mirrors the scalar cases of decodeAny.
|
|
func (d *decoder) fusedScalar(kind unstable.Kind, value []byte) (interface{}, error) {
|
|
switch kind {
|
|
case unstable.String:
|
|
return string(value), nil
|
|
case unstable.Integer:
|
|
i, err := parseInteger(value)
|
|
return i, err
|
|
case unstable.Float:
|
|
f, err := parseFloat(value)
|
|
return f, err
|
|
case unstable.Bool:
|
|
return value[0] == 't', nil
|
|
case unstable.DateTime:
|
|
t, err := parseDateTime(value)
|
|
return t, err
|
|
case unstable.LocalDateTime:
|
|
dt, rest, err := parseLocalDateTime(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) > 0 {
|
|
return nil, unstable.NewParserError(rest, "extra characters at the end of a local date time")
|
|
}
|
|
return dt, nil
|
|
case unstable.LocalDate:
|
|
date, err := parseLocalDate(value)
|
|
return date, err
|
|
case unstable.LocalTime:
|
|
t, rest, err := parseLocalTime(value)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(rest) > 0 {
|
|
return nil, unstable.NewParserError(rest, "extra characters at the end of a local time")
|
|
}
|
|
return t, nil
|
|
default:
|
|
return nil, unstable.NewParserError(value, "unsupported value kind %s", kind)
|
|
}
|
|
}
|
|
|
|
// anyTableParts navigates a [table] header (given its key parts) to the map it
|
|
// designates, creating intermediate tables as needed.
|
|
func (d *decoder) anyTableParts(m map[string]interface{}, parts [][]byte) map[string]interface{} {
|
|
cur := m
|
|
for _, p := range parts {
|
|
cur = d.anyChildTable(cur, d.intern(p))
|
|
}
|
|
return cur
|
|
}
|
|
|
|
// anyArrayTableParts navigates a [[array table]] header (given its key parts),
|
|
// appends a fresh element to the designated array, and returns it. first is
|
|
// true the first time this header is seen, in which case any pre-existing array
|
|
// (from a reused target) is reset.
|
|
func (d *decoder) anyArrayTableParts(m map[string]interface{}, parts [][]byte, first bool) map[string]interface{} {
|
|
cur := m
|
|
name := d.intern(parts[0])
|
|
for i := 1; i < len(parts); i++ {
|
|
cur = d.anyChildTable(cur, name)
|
|
name = d.intern(parts[i])
|
|
}
|
|
s, _ := cur[name].([]interface{})
|
|
if first {
|
|
s = s[:0]
|
|
}
|
|
elem := map[string]interface{}{}
|
|
cur[name] = append(s, elem)
|
|
return elem
|
|
}
|
|
|
|
// setFusedLeaf assigns av at the (possibly dotted) key parts within cur,
|
|
// creating intermediate maps as needed.
|
|
func (d *decoder) setFusedLeaf(cur map[string]interface{}, parts [][]byte, av interface{}) {
|
|
for i := 0; i < len(parts)-1; i++ {
|
|
cur = d.anyChildTable(cur, d.intern(parts[i]))
|
|
}
|
|
cur[d.intern(parts[len(parts)-1])] = av
|
|
}
|
|
|
|
// wrapFusedError gives document context to errors produced by the fused
|
|
// decoder.
|
|
func (d *decoder) wrapFusedError(data []byte, err error) error {
|
|
var perr *unstable.ParserError
|
|
if errors.As(err, &perr) && len(perr.Highlight) == 0 {
|
|
// Mirror NextExpression: give end-of-input errors a usable position by
|
|
// extending the empty highlight to the last byte of the document.
|
|
if offset := cap(data) - cap(perr.Highlight); offset > 0 && offset == len(data) {
|
|
perr.Highlight = data[offset-1 : offset]
|
|
}
|
|
}
|
|
return d.wrapError(data, err)
|
|
}
|
|
|
|
func fusedSkipWS(b []byte) []byte {
|
|
for len(b) > 0 && (b[0] == ' ' || b[0] == '\t') {
|
|
b = b[1:]
|
|
}
|
|
return b
|
|
}
|
|
|
|
func fusedConsumeEOL(b []byte) ([]byte, error) {
|
|
if len(b) == 0 {
|
|
return b, nil
|
|
}
|
|
switch b[0] {
|
|
case '\n':
|
|
return b[1:], nil
|
|
case '\r':
|
|
if len(b) > 1 && b[1] == '\n' {
|
|
return b[2:], nil
|
|
}
|
|
}
|
|
return nil, unstable.NewParserError(b[:1], "expected newline but got %#U", b[0])
|
|
}
|
|
|
|
// fusedFinishLine consumes `ws [comment] (newline|eof)` after an expression.
|
|
func (d *decoder) fusedFinishLine(b []byte) ([]byte, error) {
|
|
b = fusedSkipWS(b)
|
|
if len(b) > 0 && b[0] == '#' {
|
|
_, rest, err := parserbridge.ScanComment(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b = rest
|
|
}
|
|
return fusedConsumeEOL(b)
|
|
}
|
|
|
|
func fusedHL1(b []byte) []byte {
|
|
if len(b) > 0 {
|
|
return b[:1]
|
|
}
|
|
return b
|
|
}
|