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 ADDED
@@ -0,0 +1,6 @@
1
+ # .gitignore
2
+ __pycache__/
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .hypothesis/
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