Add safe configuration report helpers

This commit is contained in:
2026-10-11 21:48:49 +00:00
parent 3542422b2f
commit 30787b2806
3 changed files with 273 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
# Implement ConfigKit adoption and configuration comparison
Status: implementation in progress; Stages 1–5 are complete.
Status: implementation in progress; Stages 1–6 are complete.
## Scope and authority
@@ -308,12 +308,15 @@ at semantic mapping paths. This stage does not classify records or deliver outpu
effects. They do not examine original configs, expand atomic regions, infer
masking from strings, or determine whether a difference exists.
**Completion evidence:** focused helper tests for root and `/`, nested placement
sharing an ancestor, numeric/dot/percent keys, and escaped slash/tilde keys.
Include `~01` decoding to literal `~1`, without decoding twice. Test value tags
for null, booleans, large integers, strings resembling numbers/booleans, and empty
containers. Test malformed snapshot YAML returning an error. Stage 7 owns report
classification and disclosure cases; do not duplicate its complete report suite.
**Completion evidence — complete:** private YAML-node helpers now decode safe
snapshots without converting them through loosely typed values, decode canonical
JSON Pointer tokens once, and insert mapping keys as YAML strings while reusing
ordered ancestors. Focused checks cover root and `/`, nested placement sharing an
ancestor, numeric/dot/percent keys, escaped slash/tilde keys, and `~01` becoming
literal `~1`. Snapshot tags are checked for null, booleans, a large integer,
number/boolean-like strings, and empty mappings/sequences; malformed YAML returns
an error. `go test -count=1 ./...` passes in ckconfig. Classification and report
disclosure remain for the following stage.
## Stage 7: Classify native records and encode the YAML document

View File

@@ -0,0 +1,120 @@
package ckconfig
import (
"fmt"
"strings"
"go.yaml.in/yaml/v4"
)
func decodeSafeSnapshot(snapshot string) (*yaml.Node, error) {
var document yaml.Node
decoder := yaml.NewDecoder(strings.NewReader(snapshot))
if err := decoder.Decode(&document); err != nil {
return nil, fmt.Errorf("decode safe YAML snapshot: %w", err)
}
if document.Kind != yaml.DocumentNode || len(document.Content) == 0 {
return nil, fmt.Errorf("decode safe YAML snapshot: document has no value")
}
return document.Content[0], nil
}
func decodeJSONPointer(pointer string) ([]string, error) {
if pointer == "" {
return nil, nil
}
if pointer[0] != '/' {
return nil, fmt.Errorf("decode JSON pointer %q: path must start with /", pointer)
}
parts := strings.Split(pointer[1:], "/")
for index, part := range parts {
var decoded strings.Builder
decoded.Grow(len(part))
for offset := 0; offset < len(part); offset++ {
if part[offset] != '~' {
decoded.WriteByte(part[offset])
continue
}
if offset+1 >= len(part) {
return nil, fmt.Errorf("decode JSON pointer %q: incomplete escape in token %d", pointer, index)
}
offset++
switch part[offset] {
case '0':
decoded.WriteByte('~')
case '1':
decoded.WriteByte('/')
default:
return nil, fmt.Errorf("decode JSON pointer %q: invalid escape in token %d", pointer, index)
}
}
parts[index] = decoded.String()
}
return parts, nil
}
func placeYAMLValue(category *yaml.Node, path []string, value *yaml.Node) (*yaml.Node, error) {
if value == nil {
return nil, fmt.Errorf("place YAML value: value node is nil")
}
if len(path) == 0 {
return value, nil
}
if category == nil || category.Kind != yaml.MappingNode {
return nil, fmt.Errorf("place YAML value at %q: category ancestor is not a mapping", pointerFromTokens(path[:1]))
}
current := category
for index, token := range path {
keyIndex := mappingKeyIndex(current, token)
if index == len(path)-1 {
if keyIndex >= 0 {
current.Content[keyIndex+1] = value
} else {
current.Content = append(current.Content, stringKey(token), value)
}
return category, nil
}
if keyIndex < 0 {
child := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
current.Content = append(current.Content, stringKey(token), child)
current = child
continue
}
child := current.Content[keyIndex+1]
if child.Kind != yaml.MappingNode {
return nil, fmt.Errorf("place YAML value at %q: ancestor %q is not a mapping", pointerFromTokens(path), pointerFromTokens(path[:index+1]))
}
current = child
}
return category, nil
}
func mappingKeyIndex(mapping *yaml.Node, key string) int {
for index := 0; index+1 < len(mapping.Content); index += 2 {
candidate := mapping.Content[index]
if candidate.Kind == yaml.ScalarNode && candidate.Tag == "!!str" && candidate.Value == key {
return index
}
}
return -1
}
func stringKey(value string) *yaml.Node {
return &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value}
}
func pointerFromTokens(tokens []string) string {
if len(tokens) == 0 {
return ""
}
var pointer strings.Builder
for _, token := range tokens {
pointer.WriteByte('/')
pointer.WriteString(strings.ReplaceAll(strings.ReplaceAll(token, "~", "~0"), "/", "~1"))
}
return pointer.String()
}

View File

@@ -0,0 +1,143 @@
package ckconfig
import (
"reflect"
"strings"
"testing"
"go.yaml.in/yaml/v4"
)
func TestDecodeSafeSnapshotPreservesYAMLTypes(t *testing.T) {
tests := []struct {
name string
text string
kind yaml.Kind
tag string
value string
}{
{name: "null", text: "null\n", kind: yaml.ScalarNode, tag: "!!null", value: "null"},
{name: "boolean", text: "true\n", kind: yaml.ScalarNode, tag: "!!bool", value: "true"},
{name: "large integer", text: "9007199254740993\n", kind: yaml.ScalarNode, tag: "!!int", value: "9007199254740993"},
{name: "numeric string", text: "'123'\n", kind: yaml.ScalarNode, tag: "!!str", value: "123"},
{name: "boolean-like string", text: "'false'\n", kind: yaml.ScalarNode, tag: "!!str", value: "false"},
{name: "empty mapping", text: "{}\n", kind: yaml.MappingNode, tag: "!!map"},
{name: "empty sequence", text: "[]\n", kind: yaml.SequenceNode, tag: "!!seq"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
node, err := decodeSafeSnapshot(test.text)
if err != nil {
t.Fatal(err)
}
if node.Kind != test.kind || node.Tag != test.tag || node.Value != test.value {
t.Fatalf("node kind/tag/value = %v/%q/%q, want %v/%q/%q", node.Kind, node.Tag, node.Value, test.kind, test.tag, test.value)
}
})
}
nested, err := decodeSafeSnapshot("items:\n - name: example\n enabled: false\n")
if err != nil {
t.Fatal(err)
}
if nested.Kind != yaml.MappingNode || nested.Content[1].Kind != yaml.SequenceNode || nested.Content[1].Content[0].Content[3].Tag != "!!bool" {
t.Fatalf("nested collection/scalar tags were not preserved: %#v", nested)
}
}
func TestDecodeJSONPointerPreservesSemanticTokens(t *testing.T) {
tests := []struct {
pointer string
want []string
}{
{pointer: "", want: nil},
{pointer: "/", want: []string{""}},
{pointer: "/parent/child", want: []string{"parent", "child"}},
{pointer: "/0/../a.b/50%", want: []string{"0", "..", "a.b", "50%"}},
{pointer: "/a~1b/c~0d", want: []string{"a/b", "c~d"}},
{pointer: "/~01", want: []string{"~1"}},
}
for _, test := range tests {
got, err := decodeJSONPointer(test.pointer)
if err != nil {
t.Fatalf("decode %q: %v", test.pointer, err)
}
if !reflect.DeepEqual(got, test.want) {
t.Errorf("decode %q = %#v, want %#v", test.pointer, got, test.want)
}
}
for _, pointer := range []string{"missing-leading-slash", "/bad~", "/bad~2escape"} {
if _, err := decodeJSONPointer(pointer); err == nil {
t.Errorf("decode %q unexpectedly succeeded", pointer)
}
}
}
func TestPlaceYAMLValueBuildsOrderedSemanticMappings(t *testing.T) {
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
paths := [][]string{
{"parent", "0", "dot.name", "50%"},
{"parent", "second"},
{"a/b", "~1"},
{""},
}
for index, path := range paths {
value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: string(rune('a' + index))}
updated, err := placeYAMLValue(category, path, value)
if err != nil {
t.Fatalf("place %q: %v", pointerFromTokens(path), err)
}
category = updated
}
if len(category.Content) != 6 || category.Content[0].Value != "parent" || category.Content[2].Value != "a/b" || category.Content[4].Value != "" {
t.Fatalf("mapping insertion order or empty key = %#v", category.Content)
}
parent := category.Content[1]
if parent.Kind != yaml.MappingNode || len(parent.Content) != 4 || parent.Content[0].Value != "0" || parent.Content[2].Value != "second" {
t.Fatalf("nested shared ancestor/order = %#v", parent)
}
numeric := parent.Content[1]
dotted := numeric.Content[1]
if numeric.Kind != yaml.MappingNode || numeric.Content[0].Value != "dot.name" || dotted.Kind != yaml.MappingNode || dotted.Content[0].Value != "50%" {
t.Fatalf("semantic tokens were changed: numeric=%#v dotted=%#v", numeric, dotted)
}
if category.Content[1].Content[0].Tag != "!!str" || category.Content[2].Tag != "!!str" || category.Content[4].Tag != "!!str" {
t.Fatal("mapping keys are not explicitly tagged as YAML strings")
}
}
func TestPlaceYAMLValueHandlesRootAndEmptyKey(t *testing.T) {
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
root := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"}
updated, err := placeYAMLValue(category, nil, root)
if err != nil || updated != root {
t.Fatalf("root placement = %#v, %v", updated, err)
}
category = &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
value := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: "empty-key"}
updated, err = placeYAMLValue(category, []string{""}, value)
if err != nil {
t.Fatal(err)
}
if updated != category || len(category.Content) != 2 || category.Content[0].Value != "" || category.Content[0].Tag != "!!str" || category.Content[1] != value {
t.Fatalf("slash path placement = %#v", category)
}
}
func TestPlaceYAMLValueRejectsIncompatibleAncestors(t *testing.T) {
category := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map", Content: []*yaml.Node{
stringKey("parent"), {Kind: yaml.ScalarNode, Tag: "!!str", Value: "leaf"},
}}
_, err := placeYAMLValue(category, []string{"parent", "child"}, &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!null", Value: "null"})
if err == nil || !strings.Contains(err.Error(), "/parent/child") || !strings.Contains(err.Error(), "/parent") {
t.Fatalf("incompatible ancestor error = %v", err)
}
}
func TestDecodeSafeSnapshotRejectsMalformedYAML(t *testing.T) {
if _, err := decodeSafeSnapshot("value: [unterminated\n"); err == nil {
t.Fatal("malformed snapshot unexpectedly decoded")
}
}