mirror of
https://github.com/NotAShelf/batmon.git
synced 2024-11-30 09:06:46 +00:00
357 lines
7.8 KiB
Go
357 lines
7.8 KiB
Go
package tracker
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"github.com/pelletier/go-toml/v2/unstable"
|
|
)
|
|
|
|
type keyKind uint8
|
|
|
|
const (
|
|
invalidKind keyKind = iota
|
|
valueKind
|
|
tableKind
|
|
arrayTableKind
|
|
)
|
|
|
|
func (k keyKind) String() string {
|
|
switch k {
|
|
case invalidKind:
|
|
return "invalid"
|
|
case valueKind:
|
|
return "value"
|
|
case tableKind:
|
|
return "table"
|
|
case arrayTableKind:
|
|
return "array table"
|
|
}
|
|
panic("missing keyKind string mapping")
|
|
}
|
|
|
|
// SeenTracker tracks which keys have been seen with which TOML type to flag
|
|
// duplicates and mismatches according to the spec.
|
|
//
|
|
// Each node in the visited tree is represented by an entry. Each entry has an
|
|
// identifier, which is provided by a counter. Entries are stored in the array
|
|
// entries. As new nodes are discovered (referenced for the first time in the
|
|
// TOML document), entries are created and appended to the array. An entry
|
|
// points to its parent using its id.
|
|
//
|
|
// To find whether a given key (sequence of []byte) has already been visited,
|
|
// the entries are linearly searched, looking for one with the right name and
|
|
// parent id.
|
|
//
|
|
// Given that all keys appear in the document after their parent, it is
|
|
// guaranteed that all descendants of a node are stored after the node, this
|
|
// speeds up the search process.
|
|
//
|
|
// When encountering [[array tables]], the descendants of that node are removed
|
|
// to allow that branch of the tree to be "rediscovered". To maintain the
|
|
// invariant above, the deletion process needs to keep the order of entries.
|
|
// This results in more copies in that case.
|
|
type SeenTracker struct {
|
|
entries []entry
|
|
currentIdx int
|
|
}
|
|
|
|
var pool sync.Pool
|
|
|
|
func (s *SeenTracker) reset() {
|
|
// Always contains a root element at index 0.
|
|
s.currentIdx = 0
|
|
if len(s.entries) == 0 {
|
|
s.entries = make([]entry, 1, 2)
|
|
} else {
|
|
s.entries = s.entries[:1]
|
|
}
|
|
s.entries[0].child = -1
|
|
s.entries[0].next = -1
|
|
}
|
|
|
|
type entry struct {
|
|
// Use -1 to indicate no child or no sibling.
|
|
child int
|
|
next int
|
|
|
|
name []byte
|
|
kind keyKind
|
|
explicit bool
|
|
kv bool
|
|
}
|
|
|
|
// Find the index of the child of parentIdx with key k. Returns -1 if
|
|
// it does not exist.
|
|
func (s *SeenTracker) find(parentIdx int, k []byte) int {
|
|
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
|
|
if bytes.Equal(s.entries[i].name, k) {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
// Remove all descendants of node at position idx.
|
|
func (s *SeenTracker) clear(idx int) {
|
|
if idx >= len(s.entries) {
|
|
return
|
|
}
|
|
|
|
for i := s.entries[idx].child; i >= 0; {
|
|
next := s.entries[i].next
|
|
n := s.entries[0].next
|
|
s.entries[0].next = i
|
|
s.entries[i].next = n
|
|
s.entries[i].name = nil
|
|
s.clear(i)
|
|
i = next
|
|
}
|
|
|
|
s.entries[idx].child = -1
|
|
}
|
|
|
|
func (s *SeenTracker) create(parentIdx int, name []byte, kind keyKind, explicit bool, kv bool) int {
|
|
e := entry{
|
|
child: -1,
|
|
next: s.entries[parentIdx].child,
|
|
|
|
name: name,
|
|
kind: kind,
|
|
explicit: explicit,
|
|
kv: kv,
|
|
}
|
|
var idx int
|
|
if s.entries[0].next >= 0 {
|
|
idx = s.entries[0].next
|
|
s.entries[0].next = s.entries[idx].next
|
|
s.entries[idx] = e
|
|
} else {
|
|
idx = len(s.entries)
|
|
s.entries = append(s.entries, e)
|
|
}
|
|
|
|
s.entries[parentIdx].child = idx
|
|
|
|
return idx
|
|
}
|
|
|
|
func (s *SeenTracker) setExplicitFlag(parentIdx int) {
|
|
for i := s.entries[parentIdx].child; i >= 0; i = s.entries[i].next {
|
|
if s.entries[i].kv {
|
|
s.entries[i].explicit = true
|
|
s.entries[i].kv = false
|
|
}
|
|
s.setExplicitFlag(i)
|
|
}
|
|
}
|
|
|
|
// CheckExpression takes a top-level node and checks that it does not contain
|
|
// keys that have been seen in previous calls, and validates that types are
|
|
// consistent.
|
|
func (s *SeenTracker) CheckExpression(node *unstable.Node) error {
|
|
if s.entries == nil {
|
|
s.reset()
|
|
}
|
|
switch node.Kind {
|
|
case unstable.KeyValue:
|
|
return s.checkKeyValue(node)
|
|
case unstable.Table:
|
|
return s.checkTable(node)
|
|
case unstable.ArrayTable:
|
|
return s.checkArrayTable(node)
|
|
default:
|
|
panic(fmt.Errorf("this should not be a top level node type: %s", node.Kind))
|
|
}
|
|
}
|
|
|
|
func (s *SeenTracker) checkTable(node *unstable.Node) error {
|
|
if s.currentIdx >= 0 {
|
|
s.setExplicitFlag(s.currentIdx)
|
|
}
|
|
|
|
it := node.Key()
|
|
|
|
parentIdx := 0
|
|
|
|
// This code is duplicated in checkArrayTable. This is because factoring
|
|
// it in a function requires to copy the iterator, or allocate it to the
|
|
// heap, which is not cheap.
|
|
for it.Next() {
|
|
if it.IsLast() {
|
|
break
|
|
}
|
|
|
|
k := it.Node().Data
|
|
|
|
idx := s.find(parentIdx, k)
|
|
|
|
if idx < 0 {
|
|
idx = s.create(parentIdx, k, tableKind, false, false)
|
|
} else {
|
|
entry := s.entries[idx]
|
|
if entry.kind == valueKind {
|
|
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
|
}
|
|
}
|
|
parentIdx = idx
|
|
}
|
|
|
|
k := it.Node().Data
|
|
idx := s.find(parentIdx, k)
|
|
|
|
if idx >= 0 {
|
|
kind := s.entries[idx].kind
|
|
if kind != tableKind {
|
|
return fmt.Errorf("toml: key %s should be a table, not a %s", string(k), kind)
|
|
}
|
|
if s.entries[idx].explicit {
|
|
return fmt.Errorf("toml: table %s already exists", string(k))
|
|
}
|
|
s.entries[idx].explicit = true
|
|
} else {
|
|
idx = s.create(parentIdx, k, tableKind, true, false)
|
|
}
|
|
|
|
s.currentIdx = idx
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SeenTracker) checkArrayTable(node *unstable.Node) error {
|
|
if s.currentIdx >= 0 {
|
|
s.setExplicitFlag(s.currentIdx)
|
|
}
|
|
|
|
it := node.Key()
|
|
|
|
parentIdx := 0
|
|
|
|
for it.Next() {
|
|
if it.IsLast() {
|
|
break
|
|
}
|
|
|
|
k := it.Node().Data
|
|
|
|
idx := s.find(parentIdx, k)
|
|
|
|
if idx < 0 {
|
|
idx = s.create(parentIdx, k, tableKind, false, false)
|
|
} else {
|
|
entry := s.entries[idx]
|
|
if entry.kind == valueKind {
|
|
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
|
}
|
|
}
|
|
|
|
parentIdx = idx
|
|
}
|
|
|
|
k := it.Node().Data
|
|
idx := s.find(parentIdx, k)
|
|
|
|
if idx >= 0 {
|
|
kind := s.entries[idx].kind
|
|
if kind != arrayTableKind {
|
|
return fmt.Errorf("toml: key %s already exists as a %s, but should be an array table", kind, string(k))
|
|
}
|
|
s.clear(idx)
|
|
} else {
|
|
idx = s.create(parentIdx, k, arrayTableKind, true, false)
|
|
}
|
|
|
|
s.currentIdx = idx
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SeenTracker) checkKeyValue(node *unstable.Node) error {
|
|
parentIdx := s.currentIdx
|
|
it := node.Key()
|
|
|
|
for it.Next() {
|
|
k := it.Node().Data
|
|
|
|
idx := s.find(parentIdx, k)
|
|
|
|
if idx < 0 {
|
|
idx = s.create(parentIdx, k, tableKind, false, true)
|
|
} else {
|
|
entry := s.entries[idx]
|
|
if it.IsLast() {
|
|
return fmt.Errorf("toml: key %s is already defined", string(k))
|
|
} else if entry.kind != tableKind {
|
|
return fmt.Errorf("toml: expected %s to be a table, not a %s", string(k), entry.kind)
|
|
} else if entry.explicit {
|
|
return fmt.Errorf("toml: cannot redefine table %s that has already been explicitly defined", string(k))
|
|
}
|
|
}
|
|
|
|
parentIdx = idx
|
|
}
|
|
|
|
s.entries[parentIdx].kind = valueKind
|
|
|
|
value := node.Value()
|
|
|
|
switch value.Kind {
|
|
case unstable.InlineTable:
|
|
return s.checkInlineTable(value)
|
|
case unstable.Array:
|
|
return s.checkArray(value)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *SeenTracker) checkArray(node *unstable.Node) error {
|
|
it := node.Children()
|
|
for it.Next() {
|
|
n := it.Node()
|
|
switch n.Kind {
|
|
case unstable.InlineTable:
|
|
err := s.checkInlineTable(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
case unstable.Array:
|
|
err := s.checkArray(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *SeenTracker) checkInlineTable(node *unstable.Node) error {
|
|
if pool.New == nil {
|
|
pool.New = func() interface{} {
|
|
return &SeenTracker{}
|
|
}
|
|
}
|
|
|
|
s = pool.Get().(*SeenTracker)
|
|
s.reset()
|
|
|
|
it := node.Children()
|
|
for it.Next() {
|
|
n := it.Node()
|
|
err := s.checkKeyValue(n)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// As inline tables are self-contained, the tracker does not
|
|
// need to retain the details of what they contain. The
|
|
// keyValue element that creates the inline table is kept to
|
|
// mark the presence of the inline table and prevent
|
|
// redefinition of its keys: check* functions cannot walk into
|
|
// a value.
|
|
pool.Put(s)
|
|
return nil
|
|
}
|