4.5 KiB
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 32j by a different canonical digit changes the value by d*32j, 1 <= |d| <= 31; 37 is prime and 32j is a unit, and |d| < 37, so the change is never 0 mod 37. Adjacent swaps change the value by (a-b)*32j*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 inserting0in 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_lengthpads with0, 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, soa=decodes to a number.base32c.decoderefuses 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 incrosscheck_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.