92 lines
3.4 KiB
Python
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())
|