"""Strict, reversible ASCII identifier rendering for pre-certified glyph audio. This module deliberately contains no runtime generation, audio, asset loading, or release-evidence integration. It defines only the fail-closed ``glyph_ascii_v1`` grammar and the proof object that a later integration layer can bind to a frozen asset manifest. The accepted language is intentionally narrower than RFC email/URL syntax: * every input byte must already be lowercase ASCII (there is no case folding); * email local parts, DNS labels, and URL path segments contain letters plus a small set of non-consecutive separators; * URLs are exactly ``http`` or ``https``, have no authority decorations, and have no query or fragment. Every accepted byte maps bijectively to one of 32 expanded, unambiguous production spoken tokens. Tokens in a complete rendering are separated by exactly one ASCII space. """ from __future__ import annotations from collections.abc import Mapping from dataclasses import dataclass import hashlib import json import re GLYPH_ASCII_V1_GRAMMAR_ID = "glyph_ascii_v1" GLYPH_ASCII_V1_MAX_IDENTIFIER_BYTES = 512 GLYPH_ASCII_V1_MAX_ATOMS = 512 GLYPH_ASCII_V1_MAX_CANONICAL_CHARS = 8_192 _SHA256_RE = re.compile(r"[0-9a-f]{64}\Z", flags=re.ASCII) _ALPHA = r"[a-z]+" _LABEL = rf"{_ALPHA}(?:-{_ALPHA})*" _DOMAIN = rf"{_LABEL}(?:\.{_LABEL})+" _LOCAL = rf"{_ALPHA}(?:[._-]{_ALPHA})*" _PATH_SEGMENT = rf"{_ALPHA}(?:[-_.]{_ALPHA})*" _EMAIL_RE = re.compile(rf"{_LOCAL}@{_DOMAIN}\Z", flags=re.ASCII) _URL_RE = re.compile( rf"(?:http|https)://{_DOMAIN}(?:/{_PATH_SEGMENT}(?:/{_PATH_SEGMENT})*)?\Z", flags=re.ASCII, ) _ZH_ORDINALS = ( "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", "二十一", "二十二", "二十三", "二十四", "二十五", "二十六", ) class GlyphAsciiV1Error(ValueError): """Raised when an identifier, manifest binding, or proof is invalid.""" @dataclass(frozen=True) class GlyphAsciiV1Symbol: """One member of the frozen 32-byte canonical alphabet.""" raw_byte: int canonical_token: str asset_id: str _LETTER_SYMBOLS = tuple( GlyphAsciiV1Symbol( raw_byte=ord(character), canonical_token=f"第{ordinal}個英文字母", asset_id=f"letter-{character}", ) for character, ordinal in zip("abcdefghijklmnopqrstuvwxyz", _ZH_ORDINALS) ) _PUNCTUATION_SYMBOLS = ( GlyphAsciiV1Symbol(ord("@"), "網路符號小老鼠", "symbol-at"), GlyphAsciiV1Symbol(ord("."), "網路符號點", "symbol-dot"), GlyphAsciiV1Symbol(ord("-"), "這個符號叫做橫線", "symbol-hyphen"), GlyphAsciiV1Symbol(ord("_"), "這個符號叫做底線", "symbol-underscore"), GlyphAsciiV1Symbol(ord(":"), "這個符號叫做冒號", "symbol-colon"), GlyphAsciiV1Symbol(ord("/"), "網路符號斜線", "symbol-slash"), ) GLYPH_ASCII_V1_SYMBOLS = _LETTER_SYMBOLS + _PUNCTUATION_SYMBOLS _SYMBOL_BY_RAW_BYTE = {symbol.raw_byte: symbol for symbol in GLYPH_ASCII_V1_SYMBOLS} _SYMBOL_BY_CANONICAL_TOKEN = { symbol.canonical_token: symbol for symbol in GLYPH_ASCII_V1_SYMBOLS } _EXPECTED_ASSET_IDS = tuple(symbol.asset_id for symbol in GLYPH_ASCII_V1_SYMBOLS) if ( len(GLYPH_ASCII_V1_SYMBOLS) != 32 or len(_SYMBOL_BY_RAW_BYTE) != 32 or len(_SYMBOL_BY_CANONICAL_TOKEN) != 32 or len(set(_EXPECTED_ASSET_IDS)) != 32 ): raise RuntimeError("glyph_ascii_v1 alphabet is not bijective") @dataclass(frozen=True) class GlyphAsciiV1AtomProof: """Range- and manifest-bound proof for one exact source byte.""" ordinal: int absolute_raw_start: int absolute_raw_end: int identifier_raw_start: int identifier_raw_end: int canonical_start: int canonical_end: int raw_byte: int canonical_token: str asset_id: str manifest_entry_sha256: str @dataclass(frozen=True) class GlyphAsciiV1Proof: """Complete canonical rendering proof for one accepted identifier.""" grammar_id: str identifier_kind: str absolute_raw_start: int absolute_raw_end: int canonical_start: int canonical_end: int raw_sha256: str canonical_sha256: str asset_manifest_sha256: str atoms: tuple[GlyphAsciiV1AtomProof, ...] def _require_plain_nonnegative_int(value: object, *, field: str) -> int: if type(value) is not int or value < 0: raise GlyphAsciiV1Error(f"{field} must be a non-negative integer") return value def _require_sha256(value: object, *, field: str) -> str: if type(value) is not str or _SHA256_RE.fullmatch(value) is None: raise GlyphAsciiV1Error(f"{field} must be a lowercase SHA-256 digest") return value def _validated_manifest( asset_entry_sha256_by_asset_id: Mapping[str, str], ) -> tuple[tuple[str, str], ...]: if not isinstance(asset_entry_sha256_by_asset_id, Mapping): raise GlyphAsciiV1Error("asset manifest entries must be a mapping") if set(asset_entry_sha256_by_asset_id) != set(_EXPECTED_ASSET_IDS): raise GlyphAsciiV1Error( "asset manifest must contain exactly the 32 glyph_ascii_v1 asset ids" ) entries: list[tuple[str, str]] = [] for asset_id in _EXPECTED_ASSET_IDS: if type(asset_id) is not str: raise GlyphAsciiV1Error("asset id has an invalid type") digest = _require_sha256( asset_entry_sha256_by_asset_id[asset_id], field=f"asset manifest entry {asset_id!r}", ) entries.append((asset_id, digest)) return tuple(entries) def _asset_manifest_sha256_from_entries( entries: tuple[tuple[str, str], ...], ) -> str: payload = json.dumps( { "grammar_id": GLYPH_ASCII_V1_GRAMMAR_ID, "entries": [ {"asset_id": asset_id, "sha256": digest} for asset_id, digest in entries ], }, ensure_ascii=True, separators=(",", ":"), sort_keys=True, ).encode("ascii") return hashlib.sha256(payload).hexdigest() def glyph_ascii_v1_asset_manifest_sha256( asset_entry_sha256_by_asset_id: Mapping[str, str], ) -> str: """Return the deterministic logical-manifest digest for all 32 assets.""" return _asset_manifest_sha256_from_entries( _validated_manifest(asset_entry_sha256_by_asset_id) ) def _strict_raw_bytes(raw_identifier: str) -> bytes: if type(raw_identifier) is not str: raise GlyphAsciiV1Error("raw identifier must be a string") # This is intentionally lower(), never casefold(). Eligibility requires # the source to arrive in its canonical lowercase form. if raw_identifier != raw_identifier.lower(): raise GlyphAsciiV1Error("raw identifier is not native lowercase") try: raw_bytes = raw_identifier.encode("ascii", errors="strict") except UnicodeEncodeError as error: raise GlyphAsciiV1Error("raw identifier is not strict ASCII") from error if not raw_bytes: raise GlyphAsciiV1Error("raw identifier is empty") if len(raw_bytes) > GLYPH_ASCII_V1_MAX_IDENTIFIER_BYTES: raise GlyphAsciiV1Error("raw identifier exceeds the byte cap") if len(raw_bytes) > GLYPH_ASCII_V1_MAX_ATOMS: raise GlyphAsciiV1Error("raw identifier exceeds the atom cap") return raw_bytes def glyph_ascii_v1_identifier_kind(raw_identifier: str) -> str: """Parse the exact v1 byte grammar and return ``email`` or ``url``.""" raw_bytes = _strict_raw_bytes(raw_identifier) # Match the decoded ASCII only after the byte-level eligibility checks. raw = raw_bytes.decode("ascii") matches = tuple( kind for kind, pattern in (("email", _EMAIL_RE), ("url", _URL_RE)) if pattern.fullmatch(raw) is not None ) if len(matches) != 1: raise GlyphAsciiV1Error("raw identifier is outside the unambiguous v1 grammar") return matches[0] def render_glyph_ascii_v1( raw_identifier: str, *, asset_entry_sha256_by_asset_id: Mapping[str, str], absolute_raw_start: int = 0, canonical_start: int = 0, ) -> tuple[str, GlyphAsciiV1Proof]: """Render one identifier and bind every byte to its frozen asset entry.""" absolute_raw_start = _require_plain_nonnegative_int( absolute_raw_start, field="absolute_raw_start", ) canonical_start = _require_plain_nonnegative_int( canonical_start, field="canonical_start", ) identifier_kind = glyph_ascii_v1_identifier_kind(raw_identifier) raw_bytes = raw_identifier.encode("ascii") manifest_entries = _validated_manifest(asset_entry_sha256_by_asset_id) entry_sha_by_asset_id = dict(manifest_entries) manifest_sha256 = _asset_manifest_sha256_from_entries(manifest_entries) tokens: list[str] = [] atoms: list[GlyphAsciiV1AtomProof] = [] canonical_cursor = canonical_start for ordinal, raw_byte in enumerate(raw_bytes): symbol = _SYMBOL_BY_RAW_BYTE.get(raw_byte) if symbol is None: # The grammar and alphabet are intentionally independent checks. raise GlyphAsciiV1Error("accepted grammar byte lacks a canonical atom") if ordinal: canonical_cursor += 1 # exactly one ASCII space between atoms token_start = canonical_cursor token_end = token_start + len(symbol.canonical_token) tokens.append(symbol.canonical_token) atoms.append( GlyphAsciiV1AtomProof( ordinal=ordinal, absolute_raw_start=absolute_raw_start + ordinal, absolute_raw_end=absolute_raw_start + ordinal + 1, identifier_raw_start=ordinal, identifier_raw_end=ordinal + 1, canonical_start=token_start, canonical_end=token_end, raw_byte=raw_byte, canonical_token=symbol.canonical_token, asset_id=symbol.asset_id, manifest_entry_sha256=entry_sha_by_asset_id[symbol.asset_id], ) ) canonical_cursor = token_end canonical = " ".join(tokens) if len(canonical) > GLYPH_ASCII_V1_MAX_CANONICAL_CHARS: raise GlyphAsciiV1Error("canonical rendering exceeds the character cap") proof = GlyphAsciiV1Proof( grammar_id=GLYPH_ASCII_V1_GRAMMAR_ID, identifier_kind=identifier_kind, absolute_raw_start=absolute_raw_start, absolute_raw_end=absolute_raw_start + len(raw_bytes), canonical_start=canonical_start, canonical_end=canonical_start + len(canonical), raw_sha256=hashlib.sha256(raw_bytes).hexdigest(), canonical_sha256=hashlib.sha256(canonical.encode("utf-8")).hexdigest(), asset_manifest_sha256=manifest_sha256, atoms=tuple(atoms), ) return canonical, proof def inverse_glyph_ascii_v1( proof: GlyphAsciiV1Proof, *, asset_entry_sha256_by_asset_id: Mapping[str, str], ) -> str: """Validate a proof from first principles and reconstruct its exact bytes.""" if type(proof) is not GlyphAsciiV1Proof: raise GlyphAsciiV1Error("glyph proof has an invalid type") manifest_entries = _validated_manifest(asset_entry_sha256_by_asset_id) entry_sha_by_asset_id = dict(manifest_entries) manifest_sha256 = _asset_manifest_sha256_from_entries(manifest_entries) if ( type(proof.grammar_id) is not str or proof.grammar_id != GLYPH_ASCII_V1_GRAMMAR_ID or type(proof.identifier_kind) is not str or proof.identifier_kind not in {"email", "url"} or _require_plain_nonnegative_int( proof.absolute_raw_start, field="proof.absolute_raw_start", ) != proof.absolute_raw_start or _require_plain_nonnegative_int( proof.absolute_raw_end, field="proof.absolute_raw_end", ) != proof.absolute_raw_end or _require_plain_nonnegative_int( proof.canonical_start, field="proof.canonical_start", ) != proof.canonical_start or _require_plain_nonnegative_int( proof.canonical_end, field="proof.canonical_end", ) != proof.canonical_end or proof.absolute_raw_end <= proof.absolute_raw_start or proof.canonical_end <= proof.canonical_start or _require_sha256(proof.raw_sha256, field="proof.raw_sha256") != proof.raw_sha256 or _require_sha256(proof.canonical_sha256, field="proof.canonical_sha256") != proof.canonical_sha256 or _require_sha256( proof.asset_manifest_sha256, field="proof.asset_manifest_sha256", ) != proof.asset_manifest_sha256 or proof.asset_manifest_sha256 != manifest_sha256 or type(proof.atoms) is not tuple or not proof.atoms or len(proof.atoms) > GLYPH_ASCII_V1_MAX_ATOMS ): raise GlyphAsciiV1Error("glyph proof header is inconsistent") raw_bytes = bytearray() canonical_tokens: list[str] = [] raw_absolute_cursor = proof.absolute_raw_start raw_identifier_cursor = 0 canonical_cursor = proof.canonical_start for ordinal, atom in enumerate(proof.atoms): if type(atom) is not GlyphAsciiV1AtomProof: raise GlyphAsciiV1Error("glyph atom proof has an invalid type") if ( type(atom.canonical_token) is not str or type(atom.asset_id) is not str ): raise GlyphAsciiV1Error("glyph atom text field has an invalid type") integer_fields = ( atom.ordinal, atom.absolute_raw_start, atom.absolute_raw_end, atom.identifier_raw_start, atom.identifier_raw_end, atom.canonical_start, atom.canonical_end, atom.raw_byte, ) if any(type(value) is not int for value in integer_fields): raise GlyphAsciiV1Error("glyph atom integer field has an invalid type") entry_sha256 = _require_sha256( atom.manifest_entry_sha256, field="atom.manifest_entry_sha256", ) symbol = _SYMBOL_BY_CANONICAL_TOKEN.get(atom.canonical_token) expected_canonical_start = canonical_cursor + (1 if ordinal else 0) if ( atom.ordinal != ordinal or atom.absolute_raw_start != raw_absolute_cursor or atom.absolute_raw_end != raw_absolute_cursor + 1 or atom.identifier_raw_start != raw_identifier_cursor or atom.identifier_raw_end != raw_identifier_cursor + 1 or atom.canonical_start != expected_canonical_start or atom.canonical_end != expected_canonical_start + len(atom.canonical_token) or symbol is None or symbol.raw_byte != atom.raw_byte or symbol.asset_id != atom.asset_id or entry_sha256 != atom.manifest_entry_sha256 or entry_sha_by_asset_id.get(atom.asset_id) != atom.manifest_entry_sha256 ): raise GlyphAsciiV1Error("glyph atom proof is inconsistent") raw_bytes.append(atom.raw_byte) canonical_tokens.append(atom.canonical_token) raw_absolute_cursor = atom.absolute_raw_end raw_identifier_cursor = atom.identifier_raw_end canonical_cursor = atom.canonical_end canonical = " ".join(canonical_tokens) try: raw = bytes(raw_bytes).decode("ascii", errors="strict") except UnicodeDecodeError as error: raise GlyphAsciiV1Error("glyph proof does not reconstruct ASCII") from error if ( raw_absolute_cursor != proof.absolute_raw_end or raw_identifier_cursor != len(raw_bytes) or canonical_cursor != proof.canonical_end or proof.absolute_raw_end != proof.absolute_raw_start + len(raw_bytes) or proof.canonical_end != proof.canonical_start + len(canonical) or hashlib.sha256(bytes(raw_bytes)).hexdigest() != proof.raw_sha256 or hashlib.sha256(canonical.encode("utf-8")).hexdigest() != proof.canonical_sha256 ): raise GlyphAsciiV1Error("glyph proof does not cover its declared ranges") # Re-parse the reconstructed bytes, then rebuild the only valid proof. kind = glyph_ascii_v1_identifier_kind(raw) expected_canonical, expected_proof = render_glyph_ascii_v1( raw, asset_entry_sha256_by_asset_id=entry_sha_by_asset_id, absolute_raw_start=proof.absolute_raw_start, canonical_start=proof.canonical_start, ) if ( kind != proof.identifier_kind or expected_canonical != canonical or type(expected_proof) is not type(proof) or expected_proof != proof ): raise GlyphAsciiV1Error("glyph proof is not the canonical expected proof") return raw