Files
SeedPass/scripts/canonical_json_cases.py
T
thePR0M3TH3ANandClaude Opus 5 3395aadd1c style: satisfy the black gate that was failing the Tests workflow
The Tests workflow was red on every OS and Python version for one reason:
`black --check` found 28 unformatted files. Part predates this branch (the
tui_v3 screens, core/api.py, core/index0.py); part is mine from this session.

Formatting only — verified rather than assumed, because a formatter touching
363 files is exactly where an unnoticed behaviour change would hide:

  - 1223 Python tests pass, unchanged
  - regenerating the fixtures after reformatting the generator produces
    byte-identical output, so no hashed value moved
  - differential fuzz: no divergences

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-19 12:22:19 -04:00

103 lines
3.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Emit canonical-JSON differential cases from the Python reference.
Entry hashes decide merge winners, so if the two implementations serialize
the same value differently they can pick different winners and never
converge. A security review found several such disagreements (number
formatting, key ordering) that the existing fixtures missed because they
only ever used ASCII keys and integer values.
Writes js/packages/test-vectors/fixtures/canonical_json.json.
"""
from __future__ import annotations
import json
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
OUT = REPO / "js" / "packages" / "test-vectors" / "fixtures" / "canonical_json.json"
# Values a vault can realistically contain, plus the edges that broke.
CASES: list = [
{},
{"a": 1},
{"b": 2, "a": 1},
{"modified_ts": 1700000000, "label": "site"},
{"tags": ["b", "a"], "notes": ""},
# Key ordering: supplementary plane vs BMP above U+E000
{"\U0001f600": 1, "!": 2},
{"z": 1, "é": 2, "中": 3, "\U0001f600": 4},
# Escaping
{'quote"': "back\\slash"},
{"ctrl": "\x00\x01\x1f"},
{"nl": "line\nbreak\ttab"},
{"unicode": "café 中文 \U0001f512"},
# Numbers
{"n": 0},
{"n": -0},
{"n": 1},
{"n": -17},
{"n": 9007199254740991},
{"n": 0.5},
{"n": -0.25},
{"n": 1e-7},
{"n": 1.5e-9},
# Values above 2**53 are deliberately absent: JSON gives no way to tell a
# Python int from a float there, so the two implementations cannot agree
# on a canonical form. canonicalJson refuses them rather than guessing;
# see the "unsupported" list below.
# Nesting and arrays
{"links": [{"target_id": 1, "relation": "totp", "note": ""}]},
[1, "two", None, True, False],
{"deep": {"a": [{"b": {"c": [1, 2, 3]}}]}},
# Entry-shaped, the actual hashed object
{
"type": "password",
"kind": "password",
"label": "example.com",
"length": 16,
"archived": False,
"notes": "",
"tags": ["web"],
"modified_ts": 1700000000,
"links": [],
"custom_fields": [],
},
]
# Values TypeScript must refuse rather than serialize ambiguously.
UNSUPPORTED = [
{"n": 10**20},
{"n": 12345678901234567890},
]
def main() -> None:
cases = []
for value in CASES:
canonical = json.dumps(value, sort_keys=True, separators=(",", ":"))
cases.append({"value": value, "canonical": canonical})
OUT.write_text(
json.dumps(
{
"description": (
"canonicalJson must match json.dumps(sort_keys=True, "
"separators=(',',':')) exactly: entry hashes derived from "
"it decide merge winners across implementations."
),
"cases": cases,
"unsupported": UNSUPPORTED,
},
indent=2,
sort_keys=True,
)
+ "\n"
)
print(f"wrote {OUT.relative_to(REPO)} ({len(cases)} cases)")
if __name__ == "__main__":
main()