Files

92 lines
3.4 KiB
Python

#!/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())