Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
161 lines
4.2 KiB
Go
161 lines
4.2 KiB
Go
package delivery
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DifferingPaths is every JSON path at which two bodies of one resource differ, sorted (issue 1126, T1: the shadow
|
|
// shows its work): object keys and list places joined by dots (`content.url`, `links.0`), a string that holds a JSON
|
|
// object or list read as that JSON and walked on (a runtime's config carried as text), a key or place one side alone
|
|
// holds named as it is. A key a secret could be under (secretWords) is named and not walked into. A body that is not
|
|
// JSON differs at "" (the whole), and a resource one side alone holds has none. At most MaxPaths: the last then says how many more there are. A path names keys
|
|
// alone, never a value, so it holds no secret.
|
|
func DifferingPaths(composed, running json.RawMessage) []string {
|
|
if composed == nil || running == nil {
|
|
return nil // one side alone holds it: its why says so
|
|
}
|
|
var a, b any
|
|
okA := unmarshalNumbers(composed, &a) == nil
|
|
okB := unmarshalNumbers(running, &b) == nil
|
|
if !okA || !okB {
|
|
if bytes.Equal(composed, running) {
|
|
return nil
|
|
}
|
|
return []string{""}
|
|
}
|
|
var out []string
|
|
walkDiffering(a, b, "", &out)
|
|
sort.Strings(out)
|
|
if len(out) > MaxPaths {
|
|
more := len(out) - (MaxPaths - 1)
|
|
out = append(out[:MaxPaths-1], "… "+strconv.Itoa(more)+" more")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func unmarshalNumbers(raw []byte, into *any) error {
|
|
d := json.NewDecoder(bytes.NewReader(raw))
|
|
d.UseNumber()
|
|
return d.Decode(into)
|
|
}
|
|
|
|
// asJSON is a string holding a JSON object or list read as that value; v itself otherwise.
|
|
func asJSON(v any) any {
|
|
s, ok := v.(string)
|
|
if !ok {
|
|
return v
|
|
}
|
|
t := strings.TrimSpace(s)
|
|
if t == "" || (t[0] != '{' && t[0] != '[') {
|
|
return v
|
|
}
|
|
var parsed any
|
|
if unmarshalNumbers([]byte(t), &parsed) != nil {
|
|
return v
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func join(at, key string) string {
|
|
if at == "" {
|
|
return key
|
|
}
|
|
return at + "." + key
|
|
}
|
|
|
|
func walkDiffering(a, b any, at string, out *[]string) {
|
|
a, b = asJSON(a), asJSON(b)
|
|
ma, isMA := a.(map[string]any)
|
|
mb, isMB := b.(map[string]any)
|
|
if isMA && isMB {
|
|
keys := map[string]bool{}
|
|
for k := range ma {
|
|
keys[k] = true
|
|
}
|
|
for k := range mb {
|
|
keys[k] = true
|
|
}
|
|
for k := range keys {
|
|
x, inA := ma[k]
|
|
y, inB := mb[k]
|
|
switch {
|
|
case !inA || !inB:
|
|
*out = append(*out, join(at, k))
|
|
case secretKey(k):
|
|
if !sameAny(x, y) {
|
|
*out = append(*out, join(at, k))
|
|
}
|
|
default:
|
|
walkDiffering(x, y, join(at, k), out)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
la, isLA := a.([]any)
|
|
lb, isLB := b.([]any)
|
|
if isLA && isLB {
|
|
for i := range max(len(la), len(lb)) {
|
|
if i >= len(la) || i >= len(lb) {
|
|
*out = append(*out, join(at, strconv.Itoa(i)))
|
|
continue
|
|
}
|
|
walkDiffering(la[i], lb[i], join(at, strconv.Itoa(i)), out)
|
|
}
|
|
return
|
|
}
|
|
if !sameAny(a, b) {
|
|
*out = append(*out, at)
|
|
}
|
|
}
|
|
|
|
func sameAny(a, b any) bool {
|
|
x, errA := json.Marshal(a)
|
|
y, errB := json.Marshal(b)
|
|
return errA == nil && errB == nil && bytes.Equal(x, y)
|
|
}
|
|
|
|
// FitAnswer is a with its shown bodies cut until its JSON is within limit bytes (MaxAnswerBytes for the node's key):
|
|
// first the bodies' texts, from the last differing module back, keeping their paths; then the paths too. What is cut
|
|
// is said in the answer's note. The verdict and every list of modules are never cut.
|
|
func FitAnswer(a CandidateAnswer, limit int) CandidateAnswer {
|
|
size := func() int {
|
|
raw, err := json.Marshal(a)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return len(raw)
|
|
}
|
|
if size() <= limit {
|
|
return a
|
|
}
|
|
a.Differs = append([]ModuleDiffers(nil), a.Differs...)
|
|
for i := range a.Differs {
|
|
a.Differs[i].Bodies = append([]ResourceBodies(nil), a.Differs[i].Bodies...)
|
|
}
|
|
cutTexts, cutPaths := false, false
|
|
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
|
|
for j := range a.Differs[i].Bodies {
|
|
a.Differs[i].Bodies[j].Composed, a.Differs[i].Bodies[j].Running = "", ""
|
|
}
|
|
cutTexts = true
|
|
}
|
|
for i := len(a.Differs) - 1; i >= 0 && size() > limit; i-- {
|
|
for j := range a.Differs[i].Bodies {
|
|
a.Differs[i].Bodies[j].Paths = nil
|
|
}
|
|
cutPaths = true
|
|
}
|
|
why := "the bodies shown were cut to keep the answer within " + strconv.Itoa(limit>>10) + " KiB"
|
|
if cutPaths {
|
|
why += ", their paths too"
|
|
}
|
|
if cutTexts {
|
|
a.Note = strings.TrimPrefix(a.Note+"; ", "; ") + why
|
|
}
|
|
return a
|
|
}
|