mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-10-25 14:00:40 +00:00
bdfb8a989c
BUG-3202 (#1545) decodes fields_patch from its raw member and field_overrides through models.FieldValues, to keep number literals. A plain map member MERGED a repeated occurrence; a raw member keeps the LAST, so a request repeating either answered 200 having discarded part of the write. A repeat is now refused with a 400 naming the member, and nothing is written: - models.RefuseRepeatedMember scans an object's top-level members and matches names by encoding/json's own rule (exact, or its case fold), so fields_patch followed by FIELDS_PATCH is a repeat. A test pins the matcher against encoding/json itself. - ItemUpdate.UnmarshalJSON refuses a repeated fields_patch; the move request (now a named type) and the copy/preflight request refuse a repeated field_overrides. The three handlers answer with the message unwrapped; the copy door no longer flattens it to "Invalid JSON body". - FieldValues refuses a second non-null decode itself, as the backstop for a future struct that forgets the scan. Out of scope: repeated keys INSIDE one fields object (BUG-2896), and members that were already raw before #1545 (fields, tags), which kept the last occurrence before and after.
208 lines
7.1 KiB
Go
208 lines
7.1 KiB
Go
package models
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// DecodeFieldsJSON decodes an item's fields blob with every number kept as the
|
|
// literal it was written as (a json.Number), for code that decodes a blob in
|
|
// order to WRITE it back (BUG-3202).
|
|
//
|
|
// A plain json.Unmarshal into map[string]any turns each number into a float64,
|
|
// so the re-encode rounds an integer above 2^53 (9007199254740993 is stored
|
|
// back as 9007199254740992) and rewrites number formatting. Every field write
|
|
// door decoded the stored blob that way, so a write rounded numbers it never
|
|
// named: a fields_patch to `status` rewrote every large integer in the row.
|
|
// json.Number marshals back as its own literal, so a decode-then-encode leaves
|
|
// the numbers it did not change byte-identical.
|
|
//
|
|
// It accepts and refuses exactly what json.Unmarshal into map[string]any
|
|
// does. Trailing data after the value is refused, as Unmarshal refuses it,
|
|
// and so is a number that does not fit a float64 (1e400), which Unmarshal
|
|
// refuses with a range error. Only the REPRESENTATION of an accepted number
|
|
// changes, never the set of blobs a door accepts.
|
|
//
|
|
// A literal `null` decodes to a nil map with no error, as it does through
|
|
// Unmarshal; callers keep their existing handling of that case.
|
|
func DecodeFieldsJSON(raw []byte) (map[string]any, error) {
|
|
var m map[string]any
|
|
if err := DecodeJSONKeepingNumbers(raw, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// DecodeJSONKeepingNumbers is DecodeFieldsJSON for any target: v must be a
|
|
// pointer, as for json.Unmarshal. The range check covers numbers held in
|
|
// interface{} slots (any, map[string]any, []any), which are the only slots
|
|
// UseNumber affects; a typed float64 or int destination is range-checked by
|
|
// the decoder itself, as it always was.
|
|
func DecodeJSONKeepingNumbers(data []byte, v any) error {
|
|
dec := json.NewDecoder(bytes.NewReader(data))
|
|
dec.UseNumber()
|
|
if err := dec.Decode(v); err != nil {
|
|
return err
|
|
}
|
|
if _, err := dec.Token(); err != io.EOF {
|
|
return fmt.Errorf("invalid JSON: trailing data after the top-level value")
|
|
}
|
|
return checkNumbersFitFloat64(v)
|
|
}
|
|
|
|
// checkNumbersFitFloat64 refuses a json.Number that json.Unmarshal would have
|
|
// refused when decoding into an interface: one ParseFloat reports out of
|
|
// range.
|
|
func checkNumbersFitFloat64(v any) error {
|
|
switch x := v.(type) {
|
|
case *map[string]any:
|
|
if x != nil {
|
|
return checkNumbersFitFloat64(*x)
|
|
}
|
|
case *any:
|
|
if x != nil {
|
|
return checkNumbersFitFloat64(*x)
|
|
}
|
|
case *[]any:
|
|
if x != nil {
|
|
return checkNumbersFitFloat64(*x)
|
|
}
|
|
case map[string]any:
|
|
for _, e := range x {
|
|
if err := checkNumbersFitFloat64(e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case []any:
|
|
for _, e := range x {
|
|
if err := checkNumbersFitFloat64(e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case json.Number:
|
|
if _, err := strconv.ParseFloat(string(x), 64); err != nil {
|
|
return fmt.Errorf("json: cannot unmarshal number %s into Go value of type float64", string(x))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// FieldValues is a request member carrying item field values, decoded keeping
|
|
// number literals (BUG-3202). The move and copy `field_overrides` are written
|
|
// into the destination item, so a plain map[string]any member rounded an
|
|
// integer above 2^53 the caller supplied. Absent and null decode to nil, as
|
|
// the plain map did.
|
|
//
|
|
// A repeated member used to MERGE into the plain map; decoded here it would
|
|
// replace, and the last occurrence would win (BUG-3219). encoding/json decodes
|
|
// every occurrence into the same field, so a second call finding the map
|
|
// already set is a repeat, and it is refused. That misses one shape, a null
|
|
// followed by an object, where nothing is dropped; the structs carrying this
|
|
// type also call RefuseRepeatedMember, which sees every occurrence and names
|
|
// the member. This check is the backstop for a struct that forgets to.
|
|
type FieldValues map[string]any
|
|
|
|
func (f *FieldValues) UnmarshalJSON(data []byte) error {
|
|
if *f != nil {
|
|
return &RepeatedMemberError{Member: "a field-values member"}
|
|
}
|
|
m, err := DecodeFieldsJSON(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
*f = m
|
|
return nil
|
|
}
|
|
|
|
// IsJSONNumberLiteral reports whether s is exactly one JSON number token, the
|
|
// form json.Number marshals verbatim. The field-value coercions (the server's
|
|
// items.CoerceFields and the CLI's --field typing) use it to keep a numeric
|
|
// string as its literal (BUG-3202). json.Valid alone would also accept an
|
|
// object or a quoted string, so the first byte is checked too.
|
|
func IsJSONNumberLiteral(s string) bool {
|
|
if s == "" || (s[0] != '-' && (s[0] < '0' || s[0] > '9')) {
|
|
return false
|
|
}
|
|
return json.Valid([]byte(s))
|
|
}
|
|
|
|
// CanonicalJSONNumbers returns v with every number replaced by one canonical
|
|
// spelling of its exact value, for code that COMPARES decoded field values
|
|
// (BUG-3202). Since field writes keep a number's literal, one value can be
|
|
// stored as different text (1e3, 1000, 1000.0); comparing that text reports a
|
|
// change nobody made. Comparing float64s instead has the opposite hole: two
|
|
// different integers above 2^53 round to one float and compare equal. The
|
|
// canonical form is exact and equal for equal values, so both are closed.
|
|
//
|
|
// Numbers may arrive as json.Number (from DecodeJSONKeepingNumbers) or as
|
|
// float64 (from a plain decode); both are canonicalised, so a caller comparing
|
|
// a value from each gets the numeric answer. Maps and slices are copied, never
|
|
// mutated. The result is for comparison and marshals to valid JSON, but it is
|
|
// NOT a storage form: it rewrites the caller's spelling.
|
|
func CanonicalJSONNumbers(v any) any {
|
|
switch x := v.(type) {
|
|
case map[string]any:
|
|
out := make(map[string]any, len(x))
|
|
for k, e := range x {
|
|
out[k] = CanonicalJSONNumbers(e)
|
|
}
|
|
return out
|
|
case []any:
|
|
out := make([]any, len(x))
|
|
for i, e := range x {
|
|
out[i] = CanonicalJSONNumbers(e)
|
|
}
|
|
return out
|
|
case json.Number:
|
|
return json.Number(canonicalNumberLiteral(string(x)))
|
|
case float64:
|
|
return json.Number(canonicalNumberLiteral(strconv.FormatFloat(x, 'g', -1, 64)))
|
|
}
|
|
return v
|
|
}
|
|
|
|
// canonicalNumberLiteral rewrites a JSON number literal as
|
|
// [-]<digits without leading or trailing zeros>e<exponent>, which is exact
|
|
// and the same for every spelling of one value: 1000, 1e3, 1.0e3 and 1000.0
|
|
// all become "1e3", and zero in any spelling becomes "0". A string that is not
|
|
// a number literal is returned unchanged.
|
|
func canonicalNumberLiteral(s string) string {
|
|
if !IsJSONNumberLiteral(s) {
|
|
return s
|
|
}
|
|
neg := strings.HasPrefix(s, "-")
|
|
s = strings.TrimPrefix(s, "-")
|
|
mant, expPart := s, ""
|
|
if i := strings.IndexAny(s, "eE"); i >= 0 {
|
|
mant, expPart = s[:i], s[i+1:]
|
|
}
|
|
exp := 0
|
|
if expPart != "" {
|
|
e, err := strconv.Atoi(expPart)
|
|
if err != nil {
|
|
return s // an exponent too large for int: leave it; no such value fits a float64 anyway
|
|
}
|
|
exp = e
|
|
}
|
|
intPart, frac := mant, ""
|
|
if i := strings.IndexByte(mant, '.'); i >= 0 {
|
|
intPart, frac = mant[:i], mant[i+1:]
|
|
}
|
|
digits := strings.TrimLeft(intPart+frac, "0")
|
|
exp -= len(frac)
|
|
if digits == "" {
|
|
return "0"
|
|
}
|
|
trimmed := strings.TrimRight(digits, "0")
|
|
exp += len(digits) - len(trimmed)
|
|
out := trimmed + "e" + strconv.Itoa(exp)
|
|
if neg {
|
|
out = "-" + out
|
|
}
|
|
return out
|
|
}
|