fix(web): detect duplicate object keys, which JSON.parse drops silently
Copilot's eleventh pass: three comments, one defect in three places.
`JSON.parse` accepts duplicate member names and keeps the last without
complaint, so `{"role":"user","role":"admin"}` becomes `{"role":"admin"}` —
a document that renders, and submits, as *less* than it says. In the read-only
viewer that is the Inspector hiding what the server sent; in a draft it is the
editor showing more than the wire will carry. Nothing downstream could catch
it, because by the time `JSON.parse` returns the evidence is gone.
New `findDuplicateObjectKey` scans the source, scoped per object so
`{"a":{"a":1}}` and `[{"a":1},{"a":2}]` are fine, and compares names by decoded
value so `"a"` and `"a"` collide as they should. It shares its
escape-handling with the number-literal scanner, which now reads strings
through the same helper rather than skipping them inline.
Wired into all four JSON-draft parsers and into the viewer's faithfulness
check, so the same document is refused, or shown unindented, everywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
390e0c45ed
commit
1b28fc8218
2 changed files with 217 additions and 15 deletions
|
|
@ -116,6 +116,15 @@ describe("formatJson", () => {
|
|||
expect(formatJson('{"limit":1e400}')).toBe('{"limit":1e400}');
|
||||
});
|
||||
|
||||
// The starkest instance of the same class: only the *second* `role` survives
|
||||
// `JSON.parse`, so reindenting would show the reader less than the server
|
||||
// sent.
|
||||
it("leaves a payload alone when it names the same member twice", () => {
|
||||
expect(formatJson('{"role":"user","role":"admin"}')).toBe(
|
||||
'{"role":"user","role":"admin"}',
|
||||
);
|
||||
});
|
||||
|
||||
// The spelling of a number changes on a round trip and its *value* does not,
|
||||
// so these still get indented: `0.10000000000000001` and `0.1` are the same
|
||||
// double, and `1e2` and `100` are the same number.
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@
|
|||
* Pure helpers backing the per-MIME dispatch in {@link ContentViewer}. Kept
|
||||
* free of component dependencies so they can be unit-tested in isolation and
|
||||
* reused by the blob renderers (PDF / CSV / HTML) without dragging in React.
|
||||
* The two pure predicates imported below are the exception that proves it:
|
||||
* plain functions over a string, shared so the viewer and the JSON editors
|
||||
* cannot disagree about which payloads survive a round trip.
|
||||
* The predicates imported below are the exception that proves it: plain
|
||||
* functions over a string, shared so the viewer and the JSON editors cannot
|
||||
* disagree about which payloads survive a round trip.
|
||||
*/
|
||||
import { isSerializableJson } from "@inspector/core/json/jsonUtils.js";
|
||||
import { hasImpreciseIntegerLiteral } from "../../../utils/jsonObjectDraft";
|
||||
import {
|
||||
findDuplicateObjectKey,
|
||||
hasImpreciseIntegerLiteral,
|
||||
} from "../../../utils/jsonObjectDraft";
|
||||
|
||||
/**
|
||||
* The renderer family a MIME type maps to. `ContentViewer` switches on this to
|
||||
|
|
@ -160,11 +163,18 @@ export function formatJson(content: string): string {
|
|||
* as it arrived — unindented, but honest. The copy button already copied the
|
||||
* original text either way; this makes what is on screen agree with it.
|
||||
*
|
||||
* The same two checks the draft parsers use, which is why they live in
|
||||
* The same checks the draft parsers use, which is why they live in
|
||||
* `utils/jsonObjectDraft` rather than beside either caller.
|
||||
*/
|
||||
function reformatsFaithfully(text: string, parsed: unknown): boolean {
|
||||
return isSerializableJson(parsed) && !hasImpreciseIntegerLiteral(text);
|
||||
return (
|
||||
isSerializableJson(parsed) &&
|
||||
!hasImpreciseIntegerLiteral(text) &&
|
||||
// A duplicate member name is the same class and the starkest instance:
|
||||
// `{"role":"user","role":"admin"}` would be *shown* as only the second,
|
||||
// which is the viewer hiding what the server sent.
|
||||
findDuplicateObjectKey(text) === null
|
||||
);
|
||||
}
|
||||
|
||||
/** Heuristic: does this plain text (no MIME) look like a JSON document? */
|
||||
|
|
|
|||
Loading…
Reference in a new issue