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 }