mesh/delivery merged: source.main moved to novox/mesh-sdk@6097e4f3e040
mesh/delivery-group group fix/1126-seed-same-3 merged: every member is merged
mesh/check pass 8520a4c09960 on 79eb58efe4b0 | carried from mesh-catalog 774fcdfa93ad | unit 2188 run, 1064 kept, 36 skipped | integration 2197 run, 1…
178 lines
4.8 KiB
Go
178 lines
4.8 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):
|
|
// A file's secrets (SecretsField) are named entry by entry, each entry never entered; any other
|
|
// object under a secret key, a vault box included, is one path.
|
|
sx, isSX := x.(map[string]any)
|
|
sy, isSY := y.(map[string]any)
|
|
if k != SecretsField || !isSX || !isSY {
|
|
if !sameAny(x, y) {
|
|
*out = append(*out, join(at, k))
|
|
}
|
|
continue
|
|
}
|
|
for name := range sx {
|
|
if v, in := sy[name]; !in || !sameAny(sx[name], v) {
|
|
*out = append(*out, join(join(at, k), name))
|
|
}
|
|
}
|
|
for name := range sy {
|
|
if _, in := sx[name]; !in {
|
|
*out = append(*out, join(join(at, k), name))
|
|
}
|
|
}
|
|
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
|
|
}
|