From b1b9d1353c2ad667cc88f0b1efb3f2d3d879538b Mon Sep 17 00:00:00 2001 From: null Date: Fri, 23 Oct 2026 03:51:47 +0000 Subject: [PATCH] null: Crockford Base32 codec w/ mod-37 check symbol (16/16 cases, 6/6 mutants killed, 23514-value agreement vs PyPI oracle) --- wizard-work/null/base32c/README-base32c.md | 72 ++++ wizard-work/null/base32c/base32c-demo.txt | 15 + wizard-work/null/base32c/base32c-oracle.txt | 10 + wizard-work/null/base32c/base32c-selftest.txt | 27 ++ wizard-work/null/base32c/base32c.py | 280 ++++++++++++++ wizard-work/null/base32c/crosscheck_oracle.py | 137 +++++++ wizard-work/null/base32c/demo_ids.py | 78 ++++ wizard-work/null/base32c/mutation_check.py | 113 ++++++ wizard-work/null/base32c/test_base32c.py | 362 ++++++++++++++++++ 9 files changed, 1094 insertions(+) create mode 100644 wizard-work/null/base32c/README-base32c.md create mode 100644 wizard-work/null/base32c/base32c-demo.txt create mode 100644 wizard-work/null/base32c/base32c-oracle.txt create mode 100644 wizard-work/null/base32c/base32c-selftest.txt create mode 100644 wizard-work/null/base32c/base32c.py create mode 100644 wizard-work/null/base32c/crosscheck_oracle.py create mode 100644 wizard-work/null/base32c/demo_ids.py create mode 100644 wizard-work/null/base32c/mutation_check.py create mode 100644 wizard-work/null/base32c/test_base32c.py diff --git a/wizard-work/null/base32c/README-base32c.md b/wizard-work/null/base32c/README-base32c.md new file mode 100644 index 0000000..f2b69d4 --- /dev/null +++ b/wizard-work/null/base32c/README-base32c.md @@ -0,0 +1,72 @@ +# base32c — Crockford Base32 with a check symbol (stdlib-only) + +Lane: Codeforge (null). Built 2026-10-23. + +Spec: https://www.crockford.com/base32.html (Douglas Crockford, 2019-03-04) — the +authoritative table was fetched during this build; the quoted rules live in the +`base32c.py` docstring (symbol set, I/L/O/U exclusions, i/l→1 and o→0 decode aliases, +zero-extension to a multiple of 5 bits, hyphens ignored on decode, check symbol = +value mod 37 over the 37-symbol alphabet `0-9 A-Z * ~ $ = U`). + +## Files + + base32c.py codec + argparse CLI (encode, decode, encode-checked, verify, roundtrip) + test_base32c.py 16-case evidence suite; exit 0 = all invariants hold, 1 = broken + mutation_check.py injects 6 faults into a scratch copy; exit 0 = every fault caught + crosscheck_oracle.py agreement run vs the independent PyPI package base32-crockford 0.3.0 + demo_ids.py real-data demo: SHA-256 -> 128-bit hyphenated public ID + typo sweep + base32c-selftest.txt captured run of test_base32c.py + base32c-oracle.txt captured run of crosscheck_oracle.py + base32c-demo.txt captured run of demo_ids.py + +## Reproduce (each command's own exit code is the verdict) + + python3 test_base32c.py + python3 mutation_check.py + python3 demo_ids.py + /root/.hermes/profiles/null/cache/scratch/b32venv/bin/python3.14 crosscheck_oracle.py + +## Measured results (this build, this machine) + + test_base32c.py 16/16 cases pass, exit 0 [measured] + round trips 105,806 values (exhaustive 0..100,000 plus 2**k-1, 2**k, 2**k+1 + for k=0..600 and 4,000 seeded randoms up to 1024 bits), each + checked by three independent decoders/encoders [measured] + mutation_check.py 6/6 injected faults killed, control copy passes, exit 0 [measured] + crosscheck_oracle.py 23,514 values agree with PyPI base32-crockford 0.3.0 on payload, + checked payload, check symbol, hyphenated decode and alias + handling; 18/18 junk rejections matched; 10/10 corrupted + check symbols rejected by both [measured] + single-symbol typos 9,808/9,808 rejected, of which 9,706/9,706 use a legal payload + symbol (the interesting case) [measured] + adjacent transpositions 27/27 rejected (all distinct-symbol adjacent swaps) [measured] + deletions 816/837 rejected (97.5%) [measured] + insertions 2,568/2,691 rejected (95.4%) [measured] + demo_ids.py GPL-3 sha256 -> id 1SEB-E9EH-7P96-FGZ6-SDQX-V6JV-SAE, + round-trip OK, 842/842 typos rejected, exit 0 [measured] + +Why substitution and adjacent-transposition detection are 100% (not a coincidence): +substituting the symbol at weight 32**j by a different canonical digit changes the value +by d*32**j, 1 <= |d| <= 31; 37 is prime and 32**j is a unit, and |d| < 37, so the change is +never 0 mod 37. Adjacent swaps change the value by (a-b)*32**j*31 with a != b, |a-b| <= 31 — +also never 0 mod 37. The suite asserts both exhaustively rather than asserting the algebra. + +## Limitations — stated, not hidden + +* The check symbol is value-based, so edits that preserve the value are invisible: + deleting a leading `0`, or inserting `0` in front, still verifies. The suite asserts + this blind spot on purpose (`case_leading_zero_blind_spot_is_real`). +* Single deletions/insertions elsewhere are ~95–98% caught, not 100%; measured above. +* `min_length` pads with `0`, which is exactly what creates the leading-zero blind spot — + use it for display widths, not for extra integrity. +* Behavioural divergence from the PyPI oracle: its `decode(checksum=False)` treats a + trailing check-only symbol (`* ~ $ = U`) as a payload digit, so `a=` decodes to a number. + `base32c.decode` refuses check-only symbols in payload positions. In validating mode + (`decode_checked` / `oracle.decode(..., checksum=True)`) both reject. Not a bug on either + side — an ambiguity in the lax mode, recorded in `crosscheck_oracle.py`. + +## Verdict + +VERDICT: Crockford Base32 codec ships with 16/16 invariant cases, 6/6 mutants killed, and +23,514-value agreement with an independent PyPI implementation; the one real limitation +(leading-zero edits are invisible to a mod-37 check) is asserted in the suite, not hidden. diff --git a/wizard-work/null/base32c/base32c-demo.txt b/wizard-work/null/base32c/base32c-demo.txt new file mode 100644 index 0000000..bb84292 --- /dev/null +++ b/wizard-work/null/base32c/base32c-demo.txt @@ -0,0 +1,15 @@ +file /usr/share/common-licenses/GPL-3 +sha256 3972dc9744f6499f0f9b2dbf76696f2ae7ad8af9b23dde66d6af86c9dfb36986 +id value 76362391716521242212964487524419989290 +public id 1SEB-E9EH-7P96-FGZ6-SDQX-V6JV-SAE +round-trip OK (decode_checked == value) +typo sweep 842/842 single-symbol typos rejected (100.0%) + +file /root/.hermes/wizard-outputs/null/base32c.py +sha256 73499ccf816cf82e849d62eb52ddb1969a636217ee43e380be2afdb152eafc90 +id value 153243437682121480174590331425285583254 +public id 3K96-ECZ0-BCZ0-Q897-B2XD-9DVC-CPP +round-trip OK (decode_checked == value) +typo sweep 842/842 single-symbol typos rejected (100.0%) + +DEMO OK diff --git a/wizard-work/null/base32c/base32c-oracle.txt b/wizard-work/null/base32c/base32c-oracle.txt new file mode 100644 index 0000000..fb91c65 --- /dev/null +++ b/wizard-work/null/base32c/base32c-oracle.txt @@ -0,0 +1,10 @@ +values compared: 23514 +payload encodings equal: 23514 +checked encodings equal: 23514 +check symbols equal: 23514 +hyphenated decode equal: 23514 +alias/lowercase equal: 23514 +junk rejections matched: 18/18 (never-legal strings x 2 implementations) +documented divergence: 5/5 (oracle lax-accepts payload+check-only symbol; base32c is strict, both agree in validating mode) +corrupt check rejected: 10/10 (5 values x 2 implementations) +ORACLE AGREEMENT: full (on every non-divergent behaviour) diff --git a/wizard-work/null/base32c/base32c-selftest.txt b/wizard-work/null/base32c/base32c-selftest.txt new file mode 100644 index 0000000..daa5d48 --- /dev/null +++ b/wizard-work/null/base32c/base32c-selftest.txt @@ -0,0 +1,27 @@ +PASS case_alphabet_matches_spec +PASS case_positional_encoding_is_base32 +PASS case_decode_aliases_and_hyphens +PASS case_encode_rejects_bad_values +PASS case_decode_rejects_invalid_symbols +PASS case_roundtrip_exhaustive_small +PASS case_roundtrip_boundaries_and_random +PASS case_min_length_and_hyphenation +PASS case_check_symbol_values +PASS case_checked_roundtrip_and_rejection +PASS case_check_symbol_mismatch_detected +PASS case_all_single_symbol_substitutions_detected +PASS case_all_adjacent_transpositions_detected +PASS case_leading_zero_blind_spot_is_real +PASS case_deletion_and_insertion_detection_measured +PASS case_cli_end_to_end + +MEASURED round-trip values checked: 105806 (exhaustive 0..100000 + 5805 boundary/random) +MEASURED single-symbol substitutions: 9808/9808 detected (9706/9706 with a valid payload symbol) +MEASURED adjacent transpositions of distinct symbols: 27/27 detected +MEASURED blind spot confirmed: leading-'0' deletion/insertion preserves value mod 37 and is undetected +MEASURED single-symbol deletions: 816/837 detected (97.5%) +MEASURED single-symbol insertions: 2568/2691 detected (95.4%) +MEASURED CLI exercised: encode/decode/encode-checked/verify(ok,bad)/roundtrip all behaved + +TESTS 16/16 passed, 0 failed +ALL INVARIANTS HOLD diff --git a/wizard-work/null/base32c/base32c.py b/wizard-work/null/base32c/base32c.py new file mode 100644 index 0000000..1fdb002 --- /dev/null +++ b/wizard-work/null/base32c/base32c.py @@ -0,0 +1,280 @@ +"""base32c.py -- Crockford Base32 codec with optional check symbol. Stdlib only. + +Spec (authoritative): https://www.crockford.com/base32.html +Douglas Crockford, 2019-03-04. Fetched 2026-10-23. + +Quoted from the spec, verbatim: + + "The Base 32 symbol set is a superset of the Base 16 symbol set. We chose a + symbol set of 10 digits and 22 letters. We exclude 4 of the 26 letters: I L O U." + I Can be confused with 1 + L Can be confused with 1 + O Can be confused with 0 + U Accidental obscenity + + "When decoding, upper and lower case letters are accepted, and i and l will be + treated as 1 and o will be treated as 0. When encoding, only upper case letters + are used." + + "If the bit-length of the number to be encoded is not a multiple of 5 bits, then + zero-extend the number to make its bit-length a multiple of 5." + + "Hyphens (-) can be inserted into symbol strings. This can partition a string into + manageable pieces, improving readability by helping to prevent confusion. Hyphens + are ignored during decoding." + + Check symbols, values 32..36: '*' '~' '$' '=' 'U' + "The check symbol encodes the number modulo 37, 37 being the least prime number + greater than 32." + +Design notes (why the check symbol has the detection powers the test suite claims): + + Let the payload encode value V with symbols d_0..d_{k-1} (d_0 most significant), + and let the trailing check symbol be C = V mod 37. + + * Single-symbol substitution. Replacing the symbol at distance j from the right + (weight 32**j) by a different canonical symbol changes the value by + d * 32**j with 1 <= |d| <= 31. Modulo 37: 32 == -5 is a unit (37 prime, + 37 does not divide 32**j) and |d| < 37, so the change is never 0 mod 37. + => every single-symbol substitution in a canonically-encoded string is caught. + + * Adjacent transposition. Swapping neighbours of weights 32**(j+1), 32**j + changes the value by (a-b) * 32**j * 31, with a != b, |a-b| <= 31. Neither + 32**j, 31, nor (a-b) is 0 mod 37. => every adjacent transposition of two + distinct symbols is caught. + + * Known blind spot (value-based checks are blind to leading zeros). Deleting a + leading '0', or inserting '0' at the front, leaves the value -- and therefore + the check symbol -- unchanged. Such edits are NOT detected. The test suite + asserts this limitation rather than hiding it. +""" + +from __future__ import annotations + +import argparse +import sys + +__all__ = [ + "ALPHABET", + "CHECK_ALPHABET", + "Base32Error", + "InvalidSymbolError", + "CheckSymbolError", + "encode", + "encode_divmod", + "decode", + "decode_positional", + "check_symbol", + "encode_checked", + "decode_checked", + "verify", +] + +# 32 canonical payload symbols: 0-9 then the 22 letters, I L O U excluded. +ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" +assert len(ALPHABET) == 32 +assert set("ILOU").isdisjoint(ALPHABET) + +# 37-symbol check alphabet: the payload alphabet plus the 5 check-only symbols. +CHECK_ALPHABET = ALPHABET + "*~$=U" +assert len(CHECK_ALPHABET) == 37 + +_SYMBOL_VALUE = {c: i for i, c in enumerate(ALPHABET)} +# Decode-only aliases allowed by the spec: i,l -> 1 ; o -> 0 (case folded). +for _alias, _target in (("I", "1"), ("L", "1"), ("O", "0")): + _SYMBOL_VALUE[_alias] = _SYMBOL_VALUE[_target] + +_CHECK_VALUE = {c: i for i, c in enumerate(CHECK_ALPHABET)} +for _alias, _target in (("I", "1"), ("L", "1"), ("O", "0"), ("U", "U")): + _CHECK_VALUE[_alias] = _CHECK_VALUE[_target] + + +class Base32Error(ValueError): + """Base class for every rejection this module raises.""" + + +class InvalidSymbolError(Base32Error): + """A character is not a legal Crockford Base32 symbol in this position.""" + + +class CheckSymbolError(Base32Error): + """The trailing check symbol does not match the payload's value mod 37.""" + + +def _symbol_value(ch: str) -> int: + """Value 0..31 of a payload symbol. 'U' is check-only and is rejected here.""" + c = ch.upper() + v = _SYMBOL_VALUE.get(c) + if v is None: + raise InvalidSymbolError( + f"{ch!r} is not a payload symbol (I L O U are excluded; U is check-only)" + ) + return v + + +def _check_value(ch: str) -> int: + """Value 0..36 of a check symbol (accepts '*~$=U' and the i/l/o aliases).""" + v = _CHECK_VALUE.get(ch.upper()) + if v is None: + raise InvalidSymbolError(f"{ch!r} is not a check symbol") + return v + + +def encode(value: int, min_length: int = 0, hyphenate: int = 0) -> str: + """Encode a non-negative int. Zero-extends to a multiple of 5 bits. + + min_length: left-pad with '0' to at least this many symbols. + hyphenate: if > 0, group the payload in runs of that many symbols joined by '-'. + """ + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError("value must be an int") + if value < 0: + raise ValueError("Crockford Base32 encodes non-negative integers only") + if min_length < 0: + raise ValueError("min_length must be >= 0") + if hyphenate < 0: + raise ValueError("hyphenate must be >= 0") + + bits = bin(value)[2:] + bits = "0" * (-len(bits) % 5) + bits # zero-extend to a multiple of 5 bits + out = "".join(ALPHABET[int(bits[i : i + 5], 2)] for i in range(0, len(bits), 5)) + if len(out) < min_length: + out = "0" * (min_length - len(out)) + out + if hyphenate: + out = "-".join( + out[i : i + hyphenate] for i in range(0, len(out), hyphenate) + ) # last group keeps 1..hyphenate symbols; a single symbol gets no hyphen + return out + + +def encode_divmod(value: int) -> str: + """Independent encoder: repeated division by 32 instead of 5-bit slicing. + + Kept in the module so the test suite can cross-check two unrelated algorithms. + """ + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError("value must be an int") + if value < 0: + raise ValueError("Crockford Base32 encodes non-negative integers only") + if value == 0: + return "0" + digits = [] + while value: + value, r = divmod(value, 32) + digits.append(ALPHABET[r]) + return "".join(reversed(digits)) + + +def decode(text: str) -> int: + """Decode a symbol string (hyphens ignored, case folded, i/l/o aliases honoured).""" + if not isinstance(text, str): + raise TypeError("text must be a str") + body = text.replace("-", "") + if not body: + raise InvalidSymbolError("empty symbol string") + value = 0 + for ch in body: # Horner + value = value * 32 + _symbol_value(ch) + return value + + +def decode_positional(text: str) -> int: + """Independent decoder: explicit positional weights instead of Horner.""" + if not isinstance(text, str): + raise TypeError("text must be a str") + body = text.replace("-", "") + if not body: + raise InvalidSymbolError("empty symbol string") + n = len(body) + return sum(_symbol_value(ch) * (32 ** (n - 1 - i)) for i, ch in enumerate(body)) + + +def check_symbol(value: int) -> str: + """The check symbol for a value: CHECK_ALPHABET[value % 37].""" + if not isinstance(value, int) or isinstance(value, bool): + raise TypeError("value must be an int") + if value < 0: + raise ValueError("check symbol is defined for non-negative integers") + return CHECK_ALPHABET[value % 37] + + +def encode_checked(value: int, min_length: int = 0, hyphenate: int = 0) -> str: + """Payload plus trailing check symbol. Hyphenation applies to the payload only.""" + return encode(value, min_length=min_length, hyphenate=hyphenate) + check_symbol(value) + + +def decode_checked(text: str) -> int: + """Decode payload+check symbol; raise CheckSymbolError if the check fails.""" + if not isinstance(text, str): + raise TypeError("text must be a str") + body = text.replace("-", "") + if len(body) < 2: + raise CheckSymbolError("need at least one payload symbol plus a check symbol") + payload, chk = body[:-1], body[-1] + value = decode(payload) + got = _check_value(chk) + if got != value % 37: + raise CheckSymbolError( + f"check symbol {chk!r} (= {got}) does not match value mod 37 (= {value % 37})" + ) + return value + + +def verify(text: str) -> bool: + """True iff text carries a valid check symbol.""" + try: + decode_checked(text) + except Base32Error: + return False + return True + + +def _main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(prog="base32c", description="Crockford Base32 codec") + sub = ap.add_subparsers(dest="cmd", required=True) + + p = sub.add_parser("encode") + p.add_argument("value", type=int) + p.add_argument("--min-length", type=int, default=0) + p.add_argument("--hyphenate", type=int, default=0) + + p = sub.add_parser("decode") + p.add_argument("text") + + p = sub.add_parser("encode-checked") + p.add_argument("value", type=int) + p.add_argument("--min-length", type=int, default=0) + p.add_argument("--hyphenate", type=int, default=0) + + p = sub.add_parser("verify") + p.add_argument("text") + + p = sub.add_parser("roundtrip") + p.add_argument("value", type=int) + + args = ap.parse_args(argv) + try: + if args.cmd == "encode": + print(encode(args.value, args.min_length, args.hyphenate)) + elif args.cmd == "decode": + print(decode(args.text)) + elif args.cmd == "encode-checked": + print(encode_checked(args.value, args.min_length, args.hyphenate)) + elif args.cmd == "verify": + value = decode_checked(args.text) + print(f"OK {value}") + elif args.cmd == "roundtrip": + payload = encode(args.value) + checked = encode_checked(args.value) + back = decode_checked(checked) + print(f"payload={payload} checked={checked} decoded={back} equal={back == args.value}") + if back != args.value: + return 1 + except Base32Error as exc: + print(f"INVALID: {exc}", file=sys.stderr) + return 2 + return 0 + + +if __name__ == "__main__": + raise SystemExit(_main()) diff --git a/wizard-work/null/base32c/crosscheck_oracle.py b/wizard-work/null/base32c/crosscheck_oracle.py new file mode 100644 index 0000000..4a4e57d --- /dev/null +++ b/wizard-work/null/base32c/crosscheck_oracle.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""crosscheck_oracle.py -- my Crockford Base32 vs an independent third-party one. + +Oracle: PyPI package `base32-crockford` 0.3.0 (site-packages/base32_crockford.py), +installed in a scratch venv, not written by me. If my alphabet, positional weight or +mod-37 check-symbol ordering were wrong, the two implementations would disagree. + +The main suite (test_base32c.py) deliberately stays dependency-free; this script is the +one place that reaches for an outside implementation, so run it with the venv python: + + /root/.hermes/profiles/null/cache/scratch/b32venv/bin/python3.14 crosscheck_oracle.py + +Exit 0 = full agreement, 1 = disagreement (or oracle missing). +""" + +from __future__ import annotations + +import os +import random +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +if HERE not in sys.path: + sys.path.insert(0, HERE) + +import base32c # noqa: E402 + +try: + import base32_crockford as oracle # type: ignore +except ImportError: + print("ORACLE MISSING: base32_crockford not importable (run with the scratch venv python)") + raise SystemExit(1) + + +def main() -> int: + rng = random.Random(4242) + values = list(range(0, 20_001)) + values += [2 ** k - 1 for k in range(1, 257)] + values += [2 ** k for k in range(0, 257)] + values += [rng.getrandbits(rng.choice([8, 32, 64, 128, 256, 512])) for _ in range(3000)] + + payload_checks = checked_checks = check_symbol_checks = hyphen_checks = alias_checks = 0 + for value in values: + mine = base32c.encode(value) + theirs = oracle.encode(value) + assert mine == theirs, f"payload mismatch for {value}: {mine!r} != {theirs!r}" + payload_checks += 1 + + mine_checked = base32c.encode_checked(value) + theirs_checked = oracle.encode(value, checksum=True) + assert mine_checked == theirs_checked, f"checked mismatch for {value}: {mine_checked!r} != {theirs_checked!r}" + checked_checks += 1 + + assert base32c.check_symbol(value) == theirs_checked[-1], value + check_symbol_checks += 1 + + assert oracle.decode(mine) == value, value + assert oracle.decode(mine_checked, checksum=True) == value, value + assert base32c.decode(theirs) == value, value + assert base32c.decode_checked(theirs_checked) == value, value + + hyphenated = base32c.encode(value, hyphenate=4) + assert oracle.decode(hyphenated) == value, value + hyphen_checks += 1 + + lowered = mine.lower().replace("l", "1").replace("i", "1").replace("o", "0") + assert base32c.decode(lowered) == value == oracle.decode(lowered), value + alias_checks += 1 + + # Both implementations must reject the same kinds of junk. + never_legal = ("", "-", "U", "u", "A U", "a b", "a!", "\n", "Z Q") + rejections = 0 + for junk in never_legal: + for module in (base32c, oracle): + try: + module.decode(junk) + except (ValueError, TypeError): + rejections += 1 + else: + raise AssertionError(f"{module.__name__} accepted junk {junk!r}") + assert rejections == 2 * len(never_legal), rejections + + # Documented divergence: the oracle's lax decode(checksum=False) treats a trailing + # check-only symbol as a payload digit, so 'a=' decodes to a number. Mine refuses + # '*~$=U' anywhere in a payload. In the validating mode both agree and reject. + divergences = 0 + for tail in "*~$=U": + lax = f"a{tail}" + assert oracle.decode(lax) >= 0 # oracle: accepted, lax + try: + base32c.decode(lax) + except base32c.InvalidSymbolError: + divergences += 1 + else: + raise AssertionError(f"base32c accepted {lax!r} as a payload") + for name, call in ( + ("base32c.decode_checked", lambda: base32c.decode_checked(lax)), + ("oracle.decode(checksum=True)", lambda: oracle.decode(lax, checksum=True)), + ): + try: + call() + except ValueError: + pass + else: + raise AssertionError(f"{name} accepted {lax!r}") + assert divergences == 5, divergences + + # A corrupted check symbol must be rejected by both. + corrupt_rejections = 0 + for value in (0, 1, 37, 1234, 2 ** 64 - 1): + good = base32c.encode_checked(value) + bad = good[:-1] + ("Z" if good[-1] != "Z" else "Y") + try: + base32c.decode_checked(bad) + except base32c.Base32Error: + corrupt_rejections += 1 + try: + oracle.decode(bad, checksum=True) + except ValueError: + corrupt_rejections += 1 + + print(f"values compared: {len(values)}") + print(f"payload encodings equal: {payload_checks}") + print(f"checked encodings equal: {checked_checks}") + print(f"check symbols equal: {check_symbol_checks}") + print(f"hyphenated decode equal: {hyphen_checks}") + print(f"alias/lowercase equal: {alias_checks}") + print(f"junk rejections matched: {rejections}/{2 * len(never_legal)} (never-legal strings x 2 implementations)") + print(f"documented divergence: {divergences}/5 (oracle lax-accepts payload+check-only symbol; base32c is strict, both agree in validating mode)") + print(f"corrupt check rejected: {corrupt_rejections}/10 (5 values x 2 implementations)") + assert corrupt_rejections == 10, corrupt_rejections + print("ORACLE AGREEMENT: full (on every non-divergent behaviour)") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/wizard-work/null/base32c/demo_ids.py b/wizard-work/null/base32c/demo_ids.py new file mode 100644 index 0000000..8def1ab --- /dev/null +++ b/wizard-work/null/base32c/demo_ids.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""demo_ids.py -- real-data demo: 128-bit human-transcribable IDs with check symbols. + +Takes a real file's SHA-256, folds it to a 128-bit integer, and emits a hyphenated +Crockford Base32 ID with a check symbol (transcription-safe: O/0 and I/L/1 confusions +are folded, hyphens ignored, one bad symbol caught). Verifies and decodes it back, +then proves the check symbol catches every single-symbol typo in the printed ID. + +Run: python3 demo_ids.py [FILE ...] +""" + +from __future__ import annotations + +import hashlib +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +if HERE not in sys.path: + sys.path.insert(0, HERE) + +import base32c # noqa: E402 + +DEFAULT_TARGETS = ["/usr/share/common-licenses/GPL-3", os.path.join(HERE, "base32c.py")] + + +def make_id(path: str) -> dict: + digest = hashlib.sha256(open(path, "rb").read()).digest() + value = int.from_bytes(digest[:16], "big") # 128-bit id space + ident = base32c.encode_checked(value, min_length=26, hyphenate=4) + decoded = base32c.decode_checked(ident) + typos = tested = caught = 0 + body = ident.replace("-", "") + for pos in range(len(body)): + alternatives = base32c.CHECK_ALPHABET if pos == len(body) - 1 else base32c.ALPHABET + for alt in alternatives: + if alt == body[pos]: + continue + mutated = body[:pos] + alt + body[pos + 1 :] + tested += 1 + if not base32c.verify(mutated): + caught += 1 + return { + "path": path, + "sha256": digest.hex(), + "value": value, + "id": ident, + "roundtrip_ok": decoded == value, + "typos_tested": tested, + "typos_caught": caught, + } + + +def main(argv: list[str]) -> int: + targets = argv or DEFAULT_TARGETS + all_ok = True + for path in targets: + if not os.path.isfile(path): + print(f"SKIP {path}: not a file") + continue + row = make_id(path) + print(f"file {row['path']}") + print(f"sha256 {row['sha256']}") + print(f"id value {row['value']}") + print(f"public id {row['id']}") + print(f"round-trip {'OK' if row['roundtrip_ok'] else 'BROKEN'} (decode_checked == value)") + print( + f"typo sweep {row['typos_caught']}/{row['typos_tested']} single-symbol typos rejected" + f" ({row['typos_caught'] / row['typos_tested']:.1%})" + ) + print() + all_ok &= row["roundtrip_ok"] and row["typos_caught"] == row["typos_tested"] + print("DEMO OK" if all_ok else "DEMO FAILED") + return 0 if all_ok else 1 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/wizard-work/null/base32c/mutation_check.py b/wizard-work/null/base32c/mutation_check.py new file mode 100644 index 0000000..49ebdd7 --- /dev/null +++ b/wizard-work/null/base32c/mutation_check.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""mutation_check.py -- does test_base32c.py actually have teeth? + +Copies base32c.py + test_base32c.py into a scratch dir, injects one fault at a time, +runs the suite against the mutant, and reports whether the mutant was KILLED +(suite exit != 0). A suite that passes everything, including mutants, is worthless. + +Run: python3 mutation_check.py # exit 0 iff the control passes and every mutant dies +""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +MODULE = os.path.join(HERE, "base32c.py") +TEST = os.path.join(HERE, "test_base32c.py") + +MUTATIONS = [ + ( + "alphabet: letter J replaced by excluded I", + '"0123456789ABCDEFGHJKMNPQRSTVWXYZ"', + '"0123456789ABCDEFGHIKMNPQRSTVWXYZ"', + ), + ( + "check symbol: modulus 37 -> 36", + "return CHECK_ALPHABET[value % 37]", + "return CHECK_ALPHABET[value % 36]", + ), + ( + "check verify: compares raw value instead of value mod 37", + "if got != value % 37:", + "if got != value:", + ), + ( + "decode: hyphens no longer stripped", + 'def decode(text: str) -> int:\n """Decode a symbol string (hyphens ignored, case folded, i/l/o aliases honoured)."""\n if not isinstance(text, str):\n raise TypeError("text must be a str")\n body = text.replace("-", "")', + 'def decode(text: str) -> int:\n """Decode a symbol string (hyphens ignored, case folded, i/l/o aliases honoured)."""\n if not isinstance(text, str):\n raise TypeError("text must be a str")\n body = text', + ), + ( + "encode: 5-bit slicing becomes 4-bit slicing", + "for i in range(0, len(bits), 5)", + "for i in range(0, len(bits), 4)", + ), + ( + "decode aliases: i/l/o tolerance removed", + 'for _alias, _target in (("I", "1"), ("L", "1"), ("O", "0")):\n _SYMBOL_VALUE[_alias] = _SYMBOL_VALUE[_target]', + 'for _alias, _target in (("I", "1"), ("L", "1"), ("O", "0")):\n pass', + ), +] + + +def run_suite(directory: str) -> tuple[int, str]: + proc = subprocess.run( + [sys.executable, os.path.join(directory, "test_base32c.py")], + capture_output=True, + text=True, + timeout=300, + cwd=directory, + ) + return proc.returncode, (proc.stdout + proc.stderr).strip().splitlines()[-1] if (proc.stdout + proc.stderr).strip() else "" + + +def main() -> int: + root = tempfile.mkdtemp(prefix="base32c-mutants-", dir=os.environ.get("TMPDIR", "/tmp")) + failures = [] + + control = os.path.join(root, "control") + os.makedirs(control) + shutil.copy(MODULE, control) + shutil.copy(TEST, control) + code, tail = run_suite(control) + print(f"CONTROL (unmutated copy): exit {code} -- {tail}") + if code != 0: + failures.append("control copy of the suite failed; results below are meaningless") + + source = open(MODULE, encoding="utf-8").read() + killed = 0 + for index, (label, old, new) in enumerate(MUTATIONS, start=1): + if source.count(old) != 1: + failures.append(f"mutation {index} anchor matched {source.count(old)} times, expected 1: {label}") + print(f"MUTANT {index} ANCHOR-BROKEN: {label}") + continue + directory = os.path.join(root, f"mutant{index}") + os.makedirs(directory) + shutil.copy(TEST, directory) + with open(os.path.join(directory, "base32c.py"), "w", encoding="utf-8") as handle: + handle.write(source.replace(old, new)) + code, tail = run_suite(directory) + status = "KILLED" if code != 0 else "SURVIVED" + if code != 0: + killed += 1 + else: + failures.append(f"mutant survived: {label}") + print(f"MUTANT {index} {status}: {label} [{tail}]") + + print() + print(f"MUTANTS {killed}/{len(MUTATIONS)} killed") + shutil.rmtree(root, ignore_errors=True) + if failures: + for line in failures: + print(f"PROBLEM: {line}") + return 1 + print("SUITE HAS TEETH: every injected fault was caught") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/wizard-work/null/base32c/test_base32c.py b/wizard-work/null/base32c/test_base32c.py new file mode 100644 index 0000000..969e7c1 --- /dev/null +++ b/wizard-work/null/base32c/test_base32c.py @@ -0,0 +1,362 @@ +#!/usr/bin/env python3 +"""test_base32c.py -- executable evidence for base32c.py (Crockford Base32). + +Run: python3 test_base32c.py # exit 0 = every invariant holds, 1 = something broke + +Covers: spec table semantics, exhaustive + random round trips, cross-check between +two independent encode/decode algorithms, alias/hyphen/min-length handling, invalid +input rejection, check-symbol correctness, exhaustive single-symbol-substitution and +adjacent-transposition detection, the documented leading-zero blind spot, and the +module CLI end to end. +""" + +from __future__ import annotations + +import os +import random +import subprocess +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +if HERE not in sys.path: + sys.path.insert(0, HERE) + +import base32c # noqa: E402 + +CASES = [] +FAILURES = [] +MEASURED = [] + + +def case(fn): + CASES.append(fn) + return fn + + +def note(line: str) -> None: + MEASURED.append(line) + + +def expect_error(exc_types, fn, *args, **kwargs): + try: + fn(*args, **kwargs) + except exc_types: + return + except Exception as other: # wrong exception type is still a failure + raise AssertionError(f"expected {exc_types}, got {type(other).__name__}: {other}") from other + raise AssertionError(f"expected {exc_types}, but {fn.__name__}{args!r} returned normally") + + +# --------------------------------------------------------------------------- spec table + +@case +def case_alphabet_matches_spec(): + assert base32c.ALPHABET == "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + assert len(base32c.ALPHABET) == 32 + for excluded in "ILOU": + assert excluded not in base32c.ALPHABET, excluded + assert base32c.CHECK_ALPHABET == base32c.ALPHABET + "*~$=U" + assert len(base32c.CHECK_ALPHABET) == 37 + # values 32..36 are the check-only symbols, in spec order + assert base32c.CHECK_ALPHABET[32:37] == "*~$=U" + + +@case +def case_positional_encoding_is_base32(): + # Facts that follow directly from the spec's positional base-32 definition. + assert base32c.encode(0) == "0" + assert base32c.encode(1) == "1" + assert base32c.encode(9) == "9" + assert base32c.encode(10) == "A" + assert base32c.encode(31) == "Z" + assert base32c.encode(32) == "10" + assert base32c.encode(33) == "11" + assert base32c.encode(1023) == "ZZ" + assert base32c.encode(1024) == "100" + assert base32c.encode(31 * 32 + 23) == "ZQ" + # 5 bits per symbol: length is ceil(bitlen/5), and leading symbol is significant + for value in (1, 2, 15, 16, 31, 32, 1024, 2 ** 64, 2 ** 64 - 1): + bits = value.bit_length() or 1 + assert len(base32c.encode(value)) == -(-bits // 5), value + + +@case +def case_decode_aliases_and_hyphens(): + assert base32c.decode("o") == 0 + assert base32c.decode("O") == 0 + assert base32c.decode("i") == 1 + assert base32c.decode("l") == 1 + assert base32c.decode("L") == 1 + assert base32c.decode("zq") == base32c.decode("ZQ") == 31 * 32 + 23 + assert base32c.decode("Z-Q") == base32c.decode("ZQ") + assert base32c.decode("0-0-0-1") == 1 + assert base32c.decode("-1-") == 1 + assert base32c.decode("10") == 32 + assert base32c.decode("00000000000000000001") == 1 + + +@case +def case_encode_rejects_bad_values(): + expect_error(ValueError, base32c.encode, -1) + expect_error(TypeError, base32c.encode, "10") + expect_error(TypeError, base32c.encode, 1.5) + expect_error(TypeError, base32c.encode, True) + expect_error(ValueError, base32c.encode, 1, -1) + expect_error(ValueError, base32c.encode, 1, 0, -1) + expect_error(ValueError, base32c.check_symbol, -1) + + +@case +def case_decode_rejects_invalid_symbols(): + for bad in ("", "-", "U", "u", "a b", "a\nb", "a!", "a=", "a*", "a~", "a$", "a,b", "Z Q"): + expect_error(base32c.Base32Error, base32c.decode, bad) + expect_error(TypeError, base32c.decode, 12345) + # 'U' is legal only in the check position: as a payload symbol it is rejected, + # and as a check symbol it is accepted only when it really is the right one. + expect_error(base32c.InvalidSymbolError, base32c.decode, "1U") + assert base32c.check_symbol(36) == "U" + assert base32c.decode_checked("1" + base32c.check_symbol(1)) == 1 + + +@case +def case_roundtrip_exhaustive_small(): + for value in range(0, 100_001): + text = base32c.encode(value) + assert base32c.decode(text) == value, value + assert base32c.decode_positional(text) == value, value + assert base32c.encode_divmod(value) == text, value + + +@case +def case_roundtrip_boundaries_and_random(): + rng = random.Random(20261023) + values = [0, 1] + for k in range(0, 601): + values += [2 ** k - 1, 2 ** k, 2 ** k + 1] + values += [rng.getrandbits(rng.choice([1, 5, 17, 64, 128, 333, 512, 1024])) for _ in range(4000)] + for value in values: + text = base32c.encode(value) + assert base32c.decode(text) == value, value + assert base32c.decode_positional(text) == value, value + assert base32c.encode_divmod(value) == text, value + assert base32c.encode_divmod(value) == base32c.encode(value), value + note(f"round-trip values checked: {len(values) + 100_001} (exhaustive 0..100000 + {len(values)} boundary/random)") + + +@case +def case_min_length_and_hyphenation(): + assert base32c.encode(1, min_length=4) == "0001" + assert base32c.encode(1, min_length=1) == "1" + assert base32c.encode(2 ** 64, min_length=3) == base32c.encode(2 ** 64) + for value in (0, 1, 31, 32, 12345, 2 ** 64 - 1, 2 ** 128 + 7): + for min_length in (0, 1, 5, 13, 40): + text = base32c.encode(value, min_length=min_length) + assert len(text) >= min_length + assert base32c.decode(text) == value + for group in (1, 2, 4, 5, 8): + text = base32c.encode(value, hyphenate=group) + parts = text.split("-") + if len(base32c.encode(value)) > group: + assert len(parts) > 1, (value, group, text) + assert all(len(p) == group for p in parts[:-1]), (value, group, text) + assert 0 < len(parts[-1]) <= group, (value, group, text) + assert text.replace("-", "") == base32c.encode(value) + assert base32c.decode(text) == value + # hyphenation with a payload shorter than the group size stays intact + assert base32c.decode(base32c.encode(3, hyphenate=8)) == 3 + assert base32c.encode(0, hyphenate=1) == "0" + assert base32c.encode(0, hyphenate=4) == "0" + assert base32c.encode(3, hyphenate=8) == "3" + + +# --------------------------------------------------------------------------- check symbol + +@case +def case_check_symbol_values(): + assert base32c.check_symbol(0) == "0" + assert base32c.check_symbol(31) == "Z" + assert base32c.check_symbol(32) == "*" + assert base32c.check_symbol(33) == "~" + assert base32c.check_symbol(34) == "$" + assert base32c.check_symbol(35) == "=" + assert base32c.check_symbol(36) == "U" + assert base32c.check_symbol(37) == "0" + assert base32c.check_symbol(73) == "U" + for value in (0, 1, 37, 38, 1234, 2 ** 70 + 5): + assert base32c.check_symbol(value) == base32c.CHECK_ALPHABET[value % 37] + + +@case +def case_checked_roundtrip_and_rejection(): + rng = random.Random(7) + for value in [0, 1, 36, 37, 12345, 2 ** 64 - 1] + [rng.getrandbits(96) for _ in range(500)]: + for kwargs in ({}, {"min_length": 9}, {"hyphenate": 4}): + text = base32c.encode_checked(value, **kwargs) + assert text.replace("-", "")[-1] == base32c.check_symbol(value) + assert base32c.decode_checked(text) == value + assert base32c.verify(text) is True + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "1") + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "-1-") + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "Z0") + + +@case +def case_check_symbol_mismatch_detected(): + # 1 % 37 == 1, so the correct check symbol for payload '1' is '1'. + assert base32c.encode_checked(1) == "11" + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "12") + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "1Z") + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "10") + # alias forms of a correct check symbol still verify + assert base32c.decode_checked("1I") == 1 + assert base32c.decode_checked("1l") == 1 + assert base32c.decode_checked("1-1") == 1 + # check symbol for a multiple of 37 is '0', so 'Z0' -> payload Z = 31, 31 % 37 = 31 != 0 + expect_error(base32c.CheckSymbolError, base32c.decode_checked, "Z0") + assert base32c.verify("Z0") is False + assert base32c.verify("nonsense!") is False + + +@case +def case_all_single_symbol_substitutions_detected(): + rng = random.Random(99) + values = [0, 1, 31, 32, 1234, 2 ** 32 - 1, 2 ** 64 + 7, 12345678901234567890, 2 ** 128 - 1] + values += [rng.getrandbits(80) for _ in range(12)] + total = detected = hard_total = hard_detected = 0 + for value in values: + checked = base32c.encode(value, min_length=8) + base32c.check_symbol(value) + for pos in range(len(checked)): + in_check = pos == len(checked) - 1 + alternatives = base32c.CHECK_ALPHABET if in_check else base32c.ALPHABET + for alt in alternatives: + if alt == checked[pos]: + continue + mutated = checked[:pos] + alt + checked[pos + 1 :] + total += 1 + if alt in base32c.ALPHABET: + hard_total += 1 + if not base32c.verify(mutated): + detected += 1 + if alt in base32c.ALPHABET: + hard_detected += 1 + assert detected == total, f"{total - detected}/{total} single-symbol substitutions slipped through" + assert hard_detected == hard_total, f"{hard_total - hard_detected}/{hard_total} valid-symbol substitutions slipped through" + note(f"single-symbol substitutions: {detected}/{total} detected ({hard_detected}/{hard_total} with a valid payload symbol)") + + +@case +def case_all_adjacent_transpositions_detected(): + values = [1, 31, 32, 1234, 2 ** 32 - 1, 2 ** 64 + 7, 12345678901234567890] + total = detected = 0 + for value in values: + checked = base32c.encode(value, min_length=6) + base32c.check_symbol(value) + for i in range(len(checked) - 1): + if checked[i] == checked[i + 1]: + continue # swapping equal symbols is not an edit + mutated = checked[:i] + checked[i + 1] + checked[i] + checked[i + 2 :] + total += 1 + if not base32c.verify(mutated): + detected += 1 + assert total > 0 + assert detected == total, f"{total - detected}/{total} adjacent transpositions slipped through" + note(f"adjacent transpositions of distinct symbols: {detected}/{total} detected") + + +@case +def case_leading_zero_blind_spot_is_real(): + # Documented limitation: a mod-37 check is value-based, so edits that preserve the + # value are invisible. Assert the limitation instead of pretending it does not exist. + value = 12345 + checked = base32c.encode(value, min_length=8) + base32c.check_symbol(value) + assert checked[0] == "0" + assert base32c.verify(checked[1:]) is True # deleting the leading '0' is undetected + assert base32c.verify("0" + checked) is True # inserting a leading '0' is undetected + note("blind spot confirmed: leading-'0' deletion/insertion preserves value mod 37 and is undetected") + + +@case +def case_deletion_and_insertion_detection_measured(): + rng = random.Random(5) + payloads = [base32c.encode(rng.getrandbits(64), min_length=10) for _ in range(60)] + del_total = del_hit = ins_total = ins_hit = 0 + for payload in payloads: + value = base32c.decode(payload) + checked = payload + base32c.check_symbol(value) + for i in range(len(checked)): + del_total += 1 + if not base32c.verify(checked[:i] + checked[i + 1 :]): + del_hit += 1 + for i in range(len(checked) + 1): + for alt in "07Z": + ins_total += 1 + if not base32c.verify(checked[:i] + alt + checked[i:]): + ins_hit += 1 + assert del_hit / del_total > 0.5, del_hit / del_total + assert ins_hit / ins_total > 0.5, ins_hit / ins_total + note(f"single-symbol deletions: {del_hit}/{del_total} detected ({del_hit / del_total:.1%})") + note(f"single-symbol insertions: {ins_hit}/{ins_total} detected ({ins_hit / ins_total:.1%})") + + +# --------------------------------------------------------------------------- CLI + +@case +def case_cli_end_to_end(): + module = os.path.join(HERE, "base32c.py") + py = sys.executable + + def run(*args): + return subprocess.run([py, module, *args], capture_output=True, text=True, timeout=60) + + r = run("encode", "12345") + assert r.returncode == 0 and r.stdout.strip() == base32c.encode(12345), r + payload = r.stdout.strip() + + r = run("decode", payload) + assert r.returncode == 0 and int(r.stdout.strip()) == 12345, r + + r = run("encode-checked", "12345") + assert r.returncode == 0, r + checked = r.stdout.strip() + assert checked == base32c.encode_checked(12345) + + r = run("verify", checked) + assert r.returncode == 0 and r.stdout.strip() == "OK 12345", r + + corrupt = checked[:-1] + ("Z" if checked[-1] != "Z" else "Y") + r = run("verify", corrupt) + assert r.returncode == 2 and r.stdout.strip() == "" and "INVALID" in r.stderr, r + + r = run("roundtrip", "98765432109876543210") + assert r.returncode == 0 and "equal=True" in r.stdout, r + + r = run("decode", "U") + assert r.returncode == 2 and "INVALID" in r.stderr, r + note("CLI exercised: encode/decode/encode-checked/verify(ok,bad)/roundtrip all behaved") + + +# --------------------------------------------------------------------------- runner + +def main() -> int: + for fn in CASES: + name = fn.__name__ + try: + fn() + except Exception as exc: # noqa: BLE001 - the runner reports every failure + FAILURES.append((name, exc)) + print(f"FAIL {name}: {type(exc).__name__}: {exc}") + else: + print(f"PASS {name}") + print() + for line in MEASURED: + print(f"MEASURED {line}") + print() + print(f"TESTS {len(CASES) - len(FAILURES)}/{len(CASES)} passed, {len(FAILURES)} failed") + if FAILURES: + print("FAILED CASES: " + ", ".join(name for name, _ in FAILURES)) + return 1 + print("ALL INVARIANTS HOLD") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())