#!/usr/bin/env python3 """probe_unicode_red.py -- RED probe for the Unicode-digit array-index bug. Reproduces three distinct symptoms of the same root cause (is_array_index uses str.isdigit(), which is True for Unicode digit glyphs whose int() raises a plain ValueError instead of JsonPointerError): 1. the library leaks a plain ValueError past its error contract 2. the CLI prints a traceback instead of a one-line error 3. two operations disagree on whether the same pointer is a valid index Run: python3 probe_unicode_red.py (expect exit 1 = the bug is present) """ import json import os import subprocess import sys HERE = os.path.dirname(os.path.abspath(__file__)) ENGINE = os.path.join(HERE, "jsonptr_patch.py") sys.path.insert(0, HERE) import jsonptr_patch as E # noqa: E402 # Non-ASCII glyphs for which str.isdigit() is True. SUPERSCRIPT_TWO = "\u00b2" DEVANAGARI_ONE = "\u0661" MATHEMATICAL_ONE = "\U0001d7d9" def symptom_library_leak(): """resolve() must raise JsonPointerError; it raises a bare ValueError.""" try: E.resolve([1, 2, 3], (SUPERSCRIPT_TWO,)) except E.JsonPointerError: return False, "JsonPointerError (contract held)" except ValueError as exc: return True, f"plain ValueError leaked: {exc}" return True, "no error at all" def symptom_op_divergence(): """is_array_index and the op handlers must agree on the same token.""" try: E.apply_patch([1, 2, 3], [{"op": "remove", "path": "/" + SUPERSCRIPT_TWO}]) except E.JsonPatchError as exc: return False, f"JsonPatchError (contract held): {exc}" except ValueError as exc: return True, f"plain ValueError leaked from op handler: {exc}" return True, "op handler accepted an index is_array_index accepts" def symptom_cli_traceback(): """The CLI must print one line and exit 1, never a traceback.""" tmp = os.path.join(HERE, "_probe_cli_tmp") os.makedirs(tmp, exist_ok=True) doc = os.path.join(tmp, "doc.json") patch = os.path.join(tmp, "patch.json") with open(doc, "w", encoding="utf-8") as fh: fh.write("[1, 2, 3]") with open(patch, "w", encoding="utf-8") as fh: json.dump([{"op": "remove", "path": "/" + SUPERSCRIPT_TWO}], fh, ensure_ascii=False) proc = subprocess.run([sys.executable, ENGINE, "--doc", doc, "--patch", patch], capture_output=True, text=True) err = proc.stderr bad = "Traceback" in err or proc.returncode not in (0, 1) detail = (f"rc={proc.returncode}, stderr={' '.join(err.split())[:80]!r}" f", traceback={'Traceback' in err}") return bad, detail def main(): symptoms = [("library leak", symptom_library_leak()), ("op divergence", symptom_op_divergence()), ("cli traceback", symptom_cli_traceback())] print("Unicode-digit array-index red probe") for name, (buggy, detail) in symptoms: print(f" {'BUG ' if buggy else 'ok '} {name:16} {detail}") any_bug = any(b for _, (b, _) in symptoms) print(f"tokens: is_array_index({SUPERSCRIPT_TWO!r})=" f"{E.is_array_index(SUPERSCRIPT_TWO)}, " f"is_array_index({DEVANAGARI_ONE!r})=" f"{E.is_array_index(DEVANAGARI_ONE)}, " f"is_array_index({MATHEMATICAL_ONE!r})=" f"{E.is_array_index(MATHEMATICAL_ONE)}") print("RED" if any_bug else "GREEN") return 1 if any_bug else 0 if __name__ == "__main__": sys.exit(main())