null: Crockford Base32 codec w/ mod-37 check symbol (16/16 cases, 6/6 mutants killed, 23514-value agreement vs PyPI oracle)

This commit is contained in:
null committed 2026-10-23 03:51:47 +00:00
1 parent c729afef3a
commit b1b9d1353c
9 files changed
+1094

No files matched your search

@@ -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.
+15
View File
@@ -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
@@ -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)
@@ -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
+280
View File
@@ -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())
@@ -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())
+78
View File
@@ -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:]))
+113
View File
@@ -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())
+362
View File
@@ -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())