|
| 1 | +package sdk |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "unicode/utf8" |
| 8 | + |
| 9 | + "github.com/gowebpki/jcs" |
| 10 | +) |
| 11 | + |
| 12 | +// ErrInvalidJSON reports bytes that are not one JSON document, or a document |
| 13 | +// with a repeated object member or an escaped lone surrogate. |
| 14 | +var ErrInvalidJSON = errors.New("twilightai: not a canonical-encodable JSON document") |
| 15 | + |
| 16 | +// CanonicalJSON re-encodes one JSON document in its RFC 8785 (JCS) form, so |
| 17 | +// that equal documents are equal bytes in every language that implements the |
| 18 | +// RFC: object members sorted by UTF-16 code units, no insignificant |
| 19 | +// whitespace, minimal string escaping, and numbers in their IEEE-754 |
| 20 | +// binary64 form (2.0 becomes 2, 1e2 becomes 100). Numbers therefore have |
| 21 | +// binary64 semantics: an identifier or quantity that must stay exact belongs |
| 22 | +// in a JSON string, not a number. |
| 23 | +// |
| 24 | +// Input that is not valid UTF-8, not exactly one JSON value, an object with |
| 25 | +// a repeated member, or a string with an escaped lone surrogate is |
| 26 | +// ErrInvalidJSON. The last two have no single meaning, and the JCS parser |
| 27 | +// would otherwise resolve them (the last member wins, the surrogate becomes |
| 28 | +// U+FFFD) and let two different inputs canonicalize to the same bytes. |
| 29 | +func CanonicalJSON(raw []byte) (json.RawMessage, error) { |
| 30 | + if !utf8.Valid(raw) { |
| 31 | + return nil, fmt.Errorf("%w: invalid UTF-8", ErrInvalidJSON) |
| 32 | + } |
| 33 | + if err := rejectEscapedLoneSurrogates(raw); err != nil { |
| 34 | + return nil, fmt.Errorf("%w: %w", ErrInvalidJSON, err) |
| 35 | + } |
| 36 | + canonical, err := jcs.Transform(raw) |
| 37 | + if err != nil { |
| 38 | + return nil, fmt.Errorf("%w: %w", ErrInvalidJSON, err) |
| 39 | + } |
| 40 | + return json.RawMessage(canonical), nil |
| 41 | +} |
| 42 | + |
| 43 | +// canonicalMarshal encodes v with encoding/json and canonicalizes the result, |
| 44 | +// so a value that embeds raw JSON (a json.RawMessage field, a map of them) |
| 45 | +// encodes canonically too. |
| 46 | +func canonicalMarshal(v any) (json.RawMessage, error) { |
| 47 | + raw, err := json.Marshal(v) |
| 48 | + if err != nil { |
| 49 | + return nil, err |
| 50 | + } |
| 51 | + return CanonicalJSON(raw) |
| 52 | +} |
| 53 | + |
| 54 | +// CanonicalProviderOptions returns options with every value re-encoded by |
| 55 | +// CanonicalJSON, so a Request that carries them marshals to the same bytes |
| 56 | +// for the same options however they were written. Neither the map nor its |
| 57 | +// values are modified; a nil or empty map is returned as is. |
| 58 | +func CanonicalProviderOptions(options map[string]json.RawMessage) (map[string]json.RawMessage, error) { |
| 59 | + if len(options) == 0 { |
| 60 | + return options, nil |
| 61 | + } |
| 62 | + out := make(map[string]json.RawMessage, len(options)) |
| 63 | + for namespace, raw := range options { |
| 64 | + canonical, err := CanonicalJSON(raw) |
| 65 | + if err != nil { |
| 66 | + return nil, fmt.Errorf("provider options for %q: %w", namespace, err) |
| 67 | + } |
| 68 | + out[namespace] = canonical |
| 69 | + } |
| 70 | + return out, nil |
| 71 | +} |
| 72 | + |
| 73 | +var errLoneSurrogate = errors.New("escaped lone surrogate") |
| 74 | + |
| 75 | +// rejectEscapedLoneSurrogates refuses strings whose \u escapes do not form |
| 76 | +// valid UTF-16, before the JCS parser replaces them with U+FFFD. It checks |
| 77 | +// only escaped UTF-16 structure; JSON syntax stays the parser's job, so a |
| 78 | +// truncated escape is left for it to report. |
| 79 | +func rejectEscapedLoneSurrogates(raw []byte) error { |
| 80 | + for i := 0; i < len(raw); i++ { |
| 81 | + if raw[i] != '"' { |
| 82 | + continue |
| 83 | + } |
| 84 | + i++ |
| 85 | + for i < len(raw) { |
| 86 | + switch raw[i] { |
| 87 | + case '"': |
| 88 | + goto nextToken |
| 89 | + case '\\': |
| 90 | + if i+1 >= len(raw) { |
| 91 | + return nil |
| 92 | + } |
| 93 | + if raw[i+1] != 'u' && raw[i+1] != 'U' { |
| 94 | + i += 2 |
| 95 | + continue |
| 96 | + } |
| 97 | + if i+6 > len(raw) { |
| 98 | + return nil |
| 99 | + } |
| 100 | + code, ok := parseHex4(raw[i+2 : i+6]) |
| 101 | + if !ok { |
| 102 | + return nil |
| 103 | + } |
| 104 | + switch { |
| 105 | + case 0xd800 <= code && code <= 0xdbff: |
| 106 | + if i+12 > len(raw) || raw[i+6] != '\\' || (raw[i+7] != 'u' && raw[i+7] != 'U') { |
| 107 | + return errLoneSurrogate |
| 108 | + } |
| 109 | + low, ok := parseHex4(raw[i+8 : i+12]) |
| 110 | + if !ok || low < 0xdc00 || low > 0xdfff { |
| 111 | + return errLoneSurrogate |
| 112 | + } |
| 113 | + i += 12 |
| 114 | + case 0xdc00 <= code && code <= 0xdfff: |
| 115 | + return errLoneSurrogate |
| 116 | + default: |
| 117 | + i += 6 |
| 118 | + } |
| 119 | + default: |
| 120 | + i++ |
| 121 | + } |
| 122 | + } |
| 123 | + nextToken: |
| 124 | + } |
| 125 | + return nil |
| 126 | +} |
| 127 | + |
| 128 | +func parseHex4(raw []byte) (rune, bool) { |
| 129 | + if len(raw) != 4 { |
| 130 | + return 0, false |
| 131 | + } |
| 132 | + var n rune |
| 133 | + for _, b := range raw { |
| 134 | + n <<= 4 |
| 135 | + switch { |
| 136 | + case '0' <= b && b <= '9': |
| 137 | + n += rune(b - '0') |
| 138 | + case 'a' <= b && b <= 'f': |
| 139 | + n += rune(b-'a') + 10 |
| 140 | + case 'A' <= b && b <= 'F': |
| 141 | + n += rune(b-'A') + 10 |
| 142 | + default: |
| 143 | + return 0, false |
| 144 | + } |
| 145 | + } |
| 146 | + return n, true |
| 147 | +} |
0 commit comments