b3c32 0.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- b3c32-0.0.2/.gitignore +6 -0
- b3c32-0.0.2/PKG-INFO +73 -0
- b3c32-0.0.2/README.md +59 -0
- b3c32-0.0.2/pyproject.toml +32 -0
- b3c32-0.0.2/src/b3c32/__init__.py +30 -0
- b3c32-0.0.2/src/b3c32/core.py +132 -0
- b3c32-0.0.2/src/b3c32/errors.py +35 -0
- b3c32-0.0.2/src/b3c32/smoke.py +65 -0
- b3c32-0.0.2/tests/test_alphabet.py +67 -0
- b3c32-0.0.2/tests/test_b3c32.py +560 -0
- b3c32-0.0.2/tests/test_errors.py +45 -0
- b3c32-0.0.2/tests/test_package.py +38 -0
- b3c32-0.0.2/tests/test_smoke.py +90 -0
- b3c32-0.0.2/uv.lock +267 -0
b3c32-0.0.2/.gitignore
ADDED
b3c32-0.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: b3c32
|
|
3
|
+
Version: 0.0.2
|
|
4
|
+
Summary: Compact, hand-writable, prefix-matchable BLAKE3 XOF hashes in Crockford Base32
|
|
5
|
+
Project-URL: Homepage, https://github.com/marcus-grant/b3c32
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Topic :: Security :: Cryptography
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Requires-Dist: blake3
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# b3c32 (Python)
|
|
16
|
+
|
|
17
|
+
Compact, hand-writable, prefix-matchable XOF hashes and identities for
|
|
18
|
+
anything.
|
|
19
|
+
|
|
20
|
+
This is the Python implementation of b3c32, and until otherwise stated
|
|
21
|
+
it is the reference implementation: the contract artifacts at the
|
|
22
|
+
project root are certified against it, and other language
|
|
23
|
+
implementations are certified against the same frozen vectors. That
|
|
24
|
+
status is expected to change once a native Rust implementation exists.
|
|
25
|
+
|
|
26
|
+
Codes are unkeyed BLAKE3 sliced on 40-bit boundaries, encoded as
|
|
27
|
+
low-pad bitstream Crockford Base32. The result is short enough to write
|
|
28
|
+
by hand, unambiguous enough to read aloud, and prefix-matchable so a
|
|
29
|
+
truncated code still resolves.
|
|
30
|
+
|
|
31
|
+
## Use
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from b3c32 import hash_b32
|
|
35
|
+
|
|
36
|
+
code = hash_b32(b"hello", 120)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Width is in bits. 120 is the current certified width. This is not a
|
|
40
|
+
ceiling on usefulness: slice the returned code at any multiple of 40
|
|
41
|
+
bits, which is 5 bytes or 8 characters, and the short form is a true
|
|
42
|
+
string prefix of the long one. Store the full code, index a prefix.
|
|
43
|
+
|
|
44
|
+
The package also exports `hash_digest` for raw digest bytes, the codec
|
|
45
|
+
functions `encode_crockford_b32`, `decode_crockford_b32`, and
|
|
46
|
+
`coerce_crockford_b32`, the alphabet as `CROCKFORD32_ALPHABET`, and the
|
|
47
|
+
error types `UncertifiedWidthError` and `CoercionError`.
|
|
48
|
+
|
|
49
|
+
## Consuming
|
|
50
|
+
|
|
51
|
+
Pin an exact version. The project is pre-1.0 and no compatibility
|
|
52
|
+
policy is in force, so any release may change the contract. Call
|
|
53
|
+
`verify_conformance` in your own suite to learn when it does:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from b3c32 import verify_conformance
|
|
57
|
+
|
|
58
|
+
def test_b3c32_contract():
|
|
59
|
+
verify_conformance()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
It raises AssertionError naming the failed claim. Its scope is drift
|
|
63
|
+
detection through the public API at the certified width; deep
|
|
64
|
+
certification is this project's own suite against the pinned BLAKE3
|
|
65
|
+
reference vectors.
|
|
66
|
+
|
|
67
|
+
## Status
|
|
68
|
+
|
|
69
|
+
Pre-release. Certification hardening is in progress. The full scheme
|
|
70
|
+
definition, the normative conformance contract, and the frozen vectors
|
|
71
|
+
live at the project root:
|
|
72
|
+
<https://github.com/marcus-grant/b3c32>
|
|
73
|
+
|
b3c32-0.0.2/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# b3c32 (Python)
|
|
2
|
+
|
|
3
|
+
Compact, hand-writable, prefix-matchable XOF hashes and identities for
|
|
4
|
+
anything.
|
|
5
|
+
|
|
6
|
+
This is the Python implementation of b3c32, and until otherwise stated
|
|
7
|
+
it is the reference implementation: the contract artifacts at the
|
|
8
|
+
project root are certified against it, and other language
|
|
9
|
+
implementations are certified against the same frozen vectors. That
|
|
10
|
+
status is expected to change once a native Rust implementation exists.
|
|
11
|
+
|
|
12
|
+
Codes are unkeyed BLAKE3 sliced on 40-bit boundaries, encoded as
|
|
13
|
+
low-pad bitstream Crockford Base32. The result is short enough to write
|
|
14
|
+
by hand, unambiguous enough to read aloud, and prefix-matchable so a
|
|
15
|
+
truncated code still resolves.
|
|
16
|
+
|
|
17
|
+
## Use
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from b3c32 import hash_b32
|
|
21
|
+
|
|
22
|
+
code = hash_b32(b"hello", 120)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Width is in bits. 120 is the current certified width. This is not a
|
|
26
|
+
ceiling on usefulness: slice the returned code at any multiple of 40
|
|
27
|
+
bits, which is 5 bytes or 8 characters, and the short form is a true
|
|
28
|
+
string prefix of the long one. Store the full code, index a prefix.
|
|
29
|
+
|
|
30
|
+
The package also exports `hash_digest` for raw digest bytes, the codec
|
|
31
|
+
functions `encode_crockford_b32`, `decode_crockford_b32`, and
|
|
32
|
+
`coerce_crockford_b32`, the alphabet as `CROCKFORD32_ALPHABET`, and the
|
|
33
|
+
error types `UncertifiedWidthError` and `CoercionError`.
|
|
34
|
+
|
|
35
|
+
## Consuming
|
|
36
|
+
|
|
37
|
+
Pin an exact version. The project is pre-1.0 and no compatibility
|
|
38
|
+
policy is in force, so any release may change the contract. Call
|
|
39
|
+
`verify_conformance` in your own suite to learn when it does:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from b3c32 import verify_conformance
|
|
43
|
+
|
|
44
|
+
def test_b3c32_contract():
|
|
45
|
+
verify_conformance()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
It raises AssertionError naming the failed claim. Its scope is drift
|
|
49
|
+
detection through the public API at the certified width; deep
|
|
50
|
+
certification is this project's own suite against the pinned BLAKE3
|
|
51
|
+
reference vectors.
|
|
52
|
+
|
|
53
|
+
## Status
|
|
54
|
+
|
|
55
|
+
Pre-release. Certification hardening is in progress. The full scheme
|
|
56
|
+
definition, the normative conformance contract, and the frozen vectors
|
|
57
|
+
live at the project root:
|
|
58
|
+
<https://github.com/marcus-grant/b3c32>
|
|
59
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# python/pyproject.toml
|
|
2
|
+
[project]
|
|
3
|
+
name = "b3c32"
|
|
4
|
+
version = "0.0.2"
|
|
5
|
+
description = "Compact, hand-writable, prefix-matchable BLAKE3 XOF hashes in Crockford Base32"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
license = "Apache-2.0"
|
|
9
|
+
dependencies = ["blake3"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 3 - Alpha",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Topic :: Security :: Cryptography",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/marcus-grant/b3c32"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = ["pytest", "hypothesis", "ruff", "pyright"]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["hatchling"]
|
|
25
|
+
build-backend = "hatchling.build"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["src/b3c32"]
|
|
29
|
+
|
|
30
|
+
[tool.pyright]
|
|
31
|
+
venvPath = "."
|
|
32
|
+
venv = ".venv"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# python/src/b3c32/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
b3c32: compact, hand-writable, prefix-matchable XOF hashes.
|
|
4
|
+
Author: Marcus Grant
|
|
5
|
+
Date: 2026-07-24
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from b3c32.core import (
|
|
10
|
+
CROCKFORD32_ALPHABET,
|
|
11
|
+
coerce_crockford_b32,
|
|
12
|
+
decode_crockford_b32,
|
|
13
|
+
encode_crockford_b32,
|
|
14
|
+
hash_b32,
|
|
15
|
+
hash_digest,
|
|
16
|
+
)
|
|
17
|
+
from b3c32.errors import CoercionError, UncertifiedWidthError
|
|
18
|
+
from b3c32.smoke import verify_conformance
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"CROCKFORD32_ALPHABET",
|
|
22
|
+
"CoercionError",
|
|
23
|
+
"UncertifiedWidthError",
|
|
24
|
+
"coerce_crockford_b32",
|
|
25
|
+
"decode_crockford_b32",
|
|
26
|
+
"encode_crockford_b32",
|
|
27
|
+
"hash_b32",
|
|
28
|
+
"hash_digest",
|
|
29
|
+
"verify_conformance",
|
|
30
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# python/src/b3c32/core.py
|
|
2
|
+
"""
|
|
3
|
+
BLAKE3 hashing and Crockford Base32 codec for b3c32 codes.
|
|
4
|
+
Author: Marcus Grant
|
|
5
|
+
Date: 2026-01-26
|
|
6
|
+
Revisions: [2026-07-24]
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from blake3 import blake3
|
|
11
|
+
|
|
12
|
+
from b3c32.errors import CoercionError, UncertifiedWidthError
|
|
13
|
+
|
|
14
|
+
_CERTIFIED_BITS = frozenset({120})
|
|
15
|
+
_TRANS_CROCKFORD_AMBIG = str.maketrans({"O": "0", "I": "1", "L": "1"})
|
|
16
|
+
|
|
17
|
+
CROCKFORD32_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def hash_digest(data: bytes, bits: int) -> bytes:
|
|
21
|
+
"""Compute the content digest at a certified width.
|
|
22
|
+
|
|
23
|
+
Unkeyed BLAKE3 XOF sliced to bits, gated on the certified set.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
data: The bytes to hash.
|
|
27
|
+
bits: Digest width; must be in the certified set.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The digest of bits // 8 bytes.
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
UncertifiedWidthError: bits is not a certified width.
|
|
34
|
+
"""
|
|
35
|
+
if bits not in _CERTIFIED_BITS:
|
|
36
|
+
raise UncertifiedWidthError(bits)
|
|
37
|
+
return blake3(data).digest(length=bits // 8)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def encode_crockford_b32(data: bytes) -> str:
|
|
41
|
+
"""Encode bytes as Crockford Base32, low-pad bitstream.
|
|
42
|
+
|
|
43
|
+
Bits are taken MSB-first as a single stream, grouped into 5-bit
|
|
44
|
+
units from the left; a final partial group is zero-extended in the
|
|
45
|
+
least-significant positions (low-pad) per the Base32-for-Humans
|
|
46
|
+
draft, Section 3.1.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
data: The bytes to encode.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Crockford Base32 string, length ceil(len(data) * 8 / 5).
|
|
53
|
+
"""
|
|
54
|
+
num, bit_count = int.from_bytes(data, byteorder="big"), len(data) * 8
|
|
55
|
+
symbol_count = (bit_count + 4) // 5 # ceil(bits / 5)
|
|
56
|
+
num <<= (5 - bit_count % 5) % 5 # low-pad to next 5-bit boundary
|
|
57
|
+
symbols = []
|
|
58
|
+
for i in range(symbol_count):
|
|
59
|
+
symbol_num = (num >> (5 * (symbol_count - 1 - i))) & 0b11111
|
|
60
|
+
symbols.append(CROCKFORD32_ALPHABET[symbol_num])
|
|
61
|
+
return "".join(symbols)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def hash_b32(data: bytes, bits: int) -> str:
|
|
65
|
+
"""Compute the canonical code at a certified width.
|
|
66
|
+
|
|
67
|
+
Composes hash_digest and the Crockford encoder.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data: The bytes to hash.
|
|
71
|
+
bits: Digest width; must be in the certified set.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Crockford Base32 code of bits // 5 characters.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
UncertifiedWidthError: bits is not a certified width.
|
|
78
|
+
"""
|
|
79
|
+
return encode_crockford_b32(hash_digest(data, bits))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def decode_crockford_b32(code: str) -> bytes:
|
|
83
|
+
"""Decode canonical Crockford Base32 to bytes.
|
|
84
|
+
|
|
85
|
+
Symbols are taken MSB-first in 5-bit groups. Trailing bits that do
|
|
86
|
+
not complete a byte are discarded: they are pad the encoder added to
|
|
87
|
+
fill a whole symbol, never part of the input. Reference
|
|
88
|
+
implementation of the encoding's inverse.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
code: Canonical (uppercase, alphabet-only) Crockford string.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The decoded bytes.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: A character is outside the Crockford alphabet.
|
|
98
|
+
"""
|
|
99
|
+
accumulated_int = 0
|
|
100
|
+
for symbol in code:
|
|
101
|
+
symbol_int_value = CROCKFORD32_ALPHABET.index(symbol)
|
|
102
|
+
# Shift left 5 to make room, OR to append this symbol's bits
|
|
103
|
+
accumulated_int = (accumulated_int << 5) | symbol_int_value
|
|
104
|
+
bit_count = 5 * len(code)
|
|
105
|
+
# Trailing bits past the last whole byte are encoder pad, so drop them
|
|
106
|
+
accumulated_int >>= bit_count % 8
|
|
107
|
+
return accumulated_int.to_bytes(bit_count // 8, "big")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def coerce_crockford_b32(code: str) -> str:
|
|
111
|
+
"""Coerce user-supplied code for non-strict decodes/lookups.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
code: User-supplied code string.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Canonical uppercase string with ambiguous chars normalized.
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
CoercionError: If code is empty or contains invalid characters.
|
|
121
|
+
"""
|
|
122
|
+
s = code.strip().upper()
|
|
123
|
+
s = s.replace("-", "").replace(" ", "")
|
|
124
|
+
s = s.translate(_TRANS_CROCKFORD_AMBIG)
|
|
125
|
+
|
|
126
|
+
if not s:
|
|
127
|
+
raise CoercionError()
|
|
128
|
+
|
|
129
|
+
for ch in s:
|
|
130
|
+
if ch not in CROCKFORD32_ALPHABET:
|
|
131
|
+
raise CoercionError(ch)
|
|
132
|
+
return s
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# python/src/b3c32/errors.py
|
|
2
|
+
"""
|
|
3
|
+
Exception types for b3c32.
|
|
4
|
+
Author: Marcus Grant
|
|
5
|
+
Date: 2026-07-24
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class UncertifiedWidthError(ValueError):
|
|
11
|
+
"""Raised when a requested digest width is not in the certified set.
|
|
12
|
+
|
|
13
|
+
Subclasses ValueError so naive callers still catch it; distinct so
|
|
14
|
+
consumer smoke tests can assert the gate survived an upgrade.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, bits: int) -> None:
|
|
18
|
+
self.bits = bits
|
|
19
|
+
super().__init__(f"digest width {bits} bits is not certified by contract")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CoercionError(ValueError):
|
|
23
|
+
"""Raised when user-supplied code cannot be coerced to canonical form.
|
|
24
|
+
|
|
25
|
+
Carries the offending character, or None when the input was empty
|
|
26
|
+
after normalization.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, char: str | None = None) -> None:
|
|
30
|
+
self.char = char
|
|
31
|
+
if char is None:
|
|
32
|
+
msg = "cannot coerce input, empty after normalization"
|
|
33
|
+
else:
|
|
34
|
+
msg = f"cannot coerce input, offending char: {char}"
|
|
35
|
+
super().__init__(msg)
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# python/src/b3c32/smoke.py
|
|
2
|
+
"""
|
|
3
|
+
Consumer smoke checks for contract drift detection.
|
|
4
|
+
Author: Marcus Grant
|
|
5
|
+
Date: 2026-07-27
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from b3c32 import (
|
|
10
|
+
CoercionError,
|
|
11
|
+
UncertifiedWidthError,
|
|
12
|
+
coerce_crockford_b32,
|
|
13
|
+
decode_crockford_b32,
|
|
14
|
+
encode_crockford_b32,
|
|
15
|
+
hash_b32,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def verify_conformance() -> None:
|
|
20
|
+
"""Assert the installed b3c32 still honors the contract surface.
|
|
21
|
+
|
|
22
|
+
One curated assertion per contract claim a consumer depends on.
|
|
23
|
+
Raises AssertionError naming the failed claim, or the library's
|
|
24
|
+
own error types if raise behavior itself has drifted.
|
|
25
|
+
|
|
26
|
+
Scope: drift detection through the public API only. Deep
|
|
27
|
+
certification, including XOF output past the 64-byte block and
|
|
28
|
+
exhaustive codec domains, is the library's own suite against the
|
|
29
|
+
pinned reference file. A consumer build passing here is the
|
|
30
|
+
certified surface at 120 bits; it is not itself a certification.
|
|
31
|
+
"""
|
|
32
|
+
code = hash_b32(bytes(i % 251 for i in range(0)), 120)
|
|
33
|
+
msg = f"smoke: pipeline vector mismatch at 120 bits, got {code}"
|
|
34
|
+
assert code == "NW9MKEFNZ6GTD8209QN3DQ69", msg
|
|
35
|
+
|
|
36
|
+
digest_wide = bytes(range(20))
|
|
37
|
+
narrow = encode_crockford_b32(digest_wide[:15])
|
|
38
|
+
wide = encode_crockford_b32(digest_wide)
|
|
39
|
+
msg = "smoke: 40-bit prefix relation broken on aligned widths"
|
|
40
|
+
assert wide.startswith(narrow), msg
|
|
41
|
+
|
|
42
|
+
encoded = encode_crockford_b32(b"foobar")
|
|
43
|
+
msg = f"smoke: non-aligned encode mismatch, got {encoded}"
|
|
44
|
+
assert encoded == "CSQPYRK1E8", msg
|
|
45
|
+
|
|
46
|
+
decoded = decode_crockford_b32("CSQPYRK1E8")
|
|
47
|
+
assert decoded == b"foobar", f"smoke: decode inversion mismatch, got {decoded!r}"
|
|
48
|
+
|
|
49
|
+
coerced = coerce_crockford_b32("oil-1")
|
|
50
|
+
msg = f"smoke: coercion mismatch, got {coerced}"
|
|
51
|
+
assert coerced == "0111", msg
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
coerce_crockford_b32("!")
|
|
55
|
+
except CoercionError:
|
|
56
|
+
pass
|
|
57
|
+
else:
|
|
58
|
+
raise AssertionError("smoke: CoercionError not raised on invalid char")
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
hash_b32(b"", 160)
|
|
62
|
+
except UncertifiedWidthError:
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
raise AssertionError("smoke: UncertifiedWidthError not raised at 160")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# python/tests/test_alphabet.py
|
|
2
|
+
"""
|
|
3
|
+
Tests for the Crockford alphabet contract.
|
|
4
|
+
Author: Marcus Grant
|
|
5
|
+
Date: 2026-08-11
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import string
|
|
10
|
+
|
|
11
|
+
from b3c32.core import CROCKFORD32_ALPHABET
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TestCrockfordAlphabet:
|
|
15
|
+
"""Contract 4.4. The alphabet is pinned two independent ways, neither
|
|
16
|
+
restating the module's literal.
|
|
17
|
+
|
|
18
|
+
The first route rebuilds the alphabet from stdlib constants and the
|
|
19
|
+
I, L, O, U exclusion rule, asserting the literal matches. The second
|
|
20
|
+
pins it from spec facts alone: the digits block, the A and Z
|
|
21
|
+
endpoints, the four skip transitions, and strict ascent. The routes
|
|
22
|
+
share no source, so a typo in the literal fails the first and a
|
|
23
|
+
wrong exclusion rule fails the second.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def test_matches_rule_construction(self):
|
|
27
|
+
"""The literal equals digits followed by uppercase letters with
|
|
28
|
+
I, L, O, U removed, built from stdlib constants."""
|
|
29
|
+
alphanum = string.digits + string.ascii_uppercase
|
|
30
|
+
assert CROCKFORD32_ALPHABET == "".join(c for c in alphanum if c not in "ILOU")
|
|
31
|
+
|
|
32
|
+
def test_length_is_32(self):
|
|
33
|
+
"""The alphabet is exactly 32 symbols, one per 5-bit value."""
|
|
34
|
+
assert len(CROCKFORD32_ALPHABET) == 32
|
|
35
|
+
|
|
36
|
+
def test_symbols_unique(self):
|
|
37
|
+
"""No symbol repeats, every 5bit value maps to a distinct character."""
|
|
38
|
+
assert len(set(CROCKFORD32_ALPHABET)) == 32
|
|
39
|
+
|
|
40
|
+
def test_excludes_ambiguous_letters(self):
|
|
41
|
+
"""No I,L,O,U appear: 1st 3 ambiguous visually to 1,0; U reserved to checksum"""
|
|
42
|
+
assert not any(c in CROCKFORD32_ALPHABET for c in "ILOU")
|
|
43
|
+
|
|
44
|
+
def test_digits_block(self):
|
|
45
|
+
"""First 10 symbols are digits 0 to 9 without stdlib string helpers."""
|
|
46
|
+
assert CROCKFORD32_ALPHABET[:10] == "".join(str(n) for n in range(10))
|
|
47
|
+
|
|
48
|
+
def test_letters_start_after_digits(self):
|
|
49
|
+
"""A appears at 10, after digits block, and Z at end index of 31."""
|
|
50
|
+
assert CROCKFORD32_ALPHABET[10] == "A"
|
|
51
|
+
assert CROCKFORD32_ALPHABET[31] == "Z"
|
|
52
|
+
|
|
53
|
+
def test_skip_transitions_positional(self):
|
|
54
|
+
"""Skips at HJ, KM, NP, TV, pinning which are omitted in alphanum sequence."""
|
|
55
|
+
assert CROCKFORD32_ALPHABET.index("H") + 1 == CROCKFORD32_ALPHABET.index("J")
|
|
56
|
+
assert CROCKFORD32_ALPHABET.index("K") + 1 == CROCKFORD32_ALPHABET.index("M")
|
|
57
|
+
assert CROCKFORD32_ALPHABET.index("N") + 1 == CROCKFORD32_ALPHABET.index("P")
|
|
58
|
+
assert CROCKFORD32_ALPHABET.index("T") + 1 == CROCKFORD32_ALPHABET.index("V")
|
|
59
|
+
|
|
60
|
+
def test_ordering_monotonic(self):
|
|
61
|
+
"""Digits 0-9 occupy indices 0-9 and letters ascend thereafter,
|
|
62
|
+
so lexical order of encoded strings matches bit order."""
|
|
63
|
+
previous_symbol = CROCKFORD32_ALPHABET[10]
|
|
64
|
+
for current_symbol in CROCKFORD32_ALPHABET[11:]:
|
|
65
|
+
msg = f"Alphabet order not monotonic: {previous_symbol} >= {current_symbol}"
|
|
66
|
+
assert current_symbol > previous_symbol, msg
|
|
67
|
+
previous_symbol = current_symbol
|