r""" Big5-BBS hybrid codec for Taiwanese BBS systems. Traditional Taiwanese BBS systems (PttBBS, DreamBBS, etc.) send a byte stream that mixes Big5-encoded Chinese text with single-byte half-width (半形) art characters whose byte values (0xA1-0xFE) overlap with Big5 lead bytes. These lone high bytes appear immediately before ANSI escape sequences (``\x1b[...``) and cannot form valid Big5 pairs since ESC (0x1B) is not a valid Big5 second byte (which must be 0x40-0x7E or 0xA1-0xFE). Search keywords / 搜尋關鍵字: PCMan, PCManX, PttBBS, DreamBBS, 批踢踢實業坊, 半形字, 半形藝術字, 連線字元, Big5 BBS art, Taiwan BBS encoding Decoding algorithm: - When a Big5 lead byte (0xA1-0xFE) is followed by a valid Big5 second byte (0x40-0x7E or 0xA1-0xFE) AND the pair maps to a defined Big5 character, the pair is decoded as Big5. - When a lead byte is followed by a structurally valid second byte but the pair is undefined in Big5 (e.g. 0xF9 0xF9), the lone lead byte is decoded via CP437 and the second byte is re-processed. This handles BBS art that uses repeated high bytes as decorative fills (e.g. ∙∙∙∙ from 0xF9 runs). - When a lead byte is followed by any other byte (e.g. ESC), the lone lead byte is decoded via CP437 and the following byte is re-processed. - Bytes below 0xA1 are decoded via latin-1 (identical to ASCII for 0x00-0x7F). """ # std imports import codecs from typing import Tuple, Union class Codec(codecs.Codec): """Big5-BBS stateless codec (decodes entire buffer at once with final=True).""" def encode(self, input: str, errors: str = "strict") -> Tuple[bytes, int]: """Encode string to bytes, preferring Big5 with CP437 fallback per character.""" result = [] for char in input: try: result.append(char.encode("big5")) except UnicodeEncodeError: encoded, _ = codecs.charmap_encode(char, errors, _CP437_ENCODING_TABLE) result.append(encoded) return b"".join(result), len(input) def decode(self, input: bytes, errors: str = "strict") -> Tuple[str, int]: """Decode bytes using Big5/CP437 hybrid algorithm.""" dec = IncrementalDecoder(errors) return dec.decode(input, final=True), len(input) class IncrementalEncoder(codecs.IncrementalEncoder): """Big5-BBS incremental encoder; Big5 primary, CP437 fallback.""" def encode(self, input: str, final: bool = False) -> bytes: """Encode input string incrementally.""" result = [] for char in input: try: result.append(char.encode("big5")) except UnicodeEncodeError: encoded, _ = codecs.charmap_encode(char, self.errors, _CP437_ENCODING_TABLE) result.append(encoded) return b"".join(result) def reset(self) -> None: """Reset encoder state (stateless; no-op).""" def getstate(self) -> int: """Return encoder state.""" return 0 def setstate(self, state: Union[int, str]) -> None: """Restore encoder state.""" class IncrementalDecoder(codecs.IncrementalDecoder): """ Big5-BBS incremental decoder with one-byte lookahead. Holds at most one pending Big5 lead byte between calls. """ def __init__(self, errors: str = "strict") -> None: """Initialize decoder with empty pending buffer.""" super().__init__(errors) self._buf: bytes = b"" def decode(self, input: bytes, final: bool = False) -> str: # type: ignore[override] """Decode input bytes using Big5/CP437 hybrid algorithm.""" data = self._buf + input self._buf = b"" result = [] i = 0 while i < len(data): b = data[i] if 0xA1 <= b <= 0xFE: if i + 1 < len(data): b2 = data[i + 1] if (0x40 <= b2 <= 0x7E) or (0xA1 <= b2 <= 0xFE): try: result.append(bytes([b, b2]).decode("big5", errors="strict")) i += 2 except UnicodeDecodeError: # Structurally valid but undefined in Big5, treat # the lone lead byte as a CP437 half-width character. result.append(bytes([b]).decode("cp437")) i += 1 else: result.append(bytes([b]).decode("cp437")) i += 1 elif not final: self._buf = bytes([b]) break else: result.append(bytes([b]).decode("cp437")) i += 1 else: result.append(bytes([b]).decode("latin-1")) i += 1 return "".join(result) def reset(self) -> None: """Reset decoder state.""" self._buf = b"" def getstate(self) -> Tuple[bytes, int]: """Return decoder state as (buffer, flags) tuple.""" return (self._buf, 0) def setstate(self, state: Tuple[bytes, int]) -> None: """Restore decoder state from (buffer, flags) tuple.""" self._buf = state[0] class StreamWriter(Codec, codecs.StreamWriter): """Big5-BBS stream writer.""" class StreamReader(Codec, codecs.StreamReader): """Big5-BBS stream reader.""" def getregentry() -> codecs.CodecInfo: """Return the codec registry entry.""" return codecs.CodecInfo( name="big5bbs", encode=Codec().encode, decode=Codec().decode, # type: ignore[arg-type] incrementalencoder=IncrementalEncoder, incrementaldecoder=IncrementalDecoder, streamreader=StreamReader, streamwriter=StreamWriter, ) def getaliases() -> Tuple[str, ...]: """Return codec aliases (normalized: hyphens replaced with underscores).""" return ("big5_bbs", "big5_pcman", "big5_pcmanx", "big5_ptt") def _build_cp437_encoding_table() -> dict[int, int]: """Build a Unicode ordinal → byte value map for CP437.""" table: dict[int, int] = {} for byte_val in range(256): char = bytes([byte_val]).decode("cp437") table[ord(char)] = byte_val return table _CP437_ENCODING_TABLE = _build_cp437_encoding_table()