137 lines
3.5 KiB
Go
137 lines
3.5 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)
|
|
}
|