"""EBCDIC ⇄ ASCII translation tables and code-page support.
Host-mode TERSE files always store EBCDIC bytes. Text-mode decompression
maps EBCDIC→ASCII and text-mode compression maps ASCII→EBCDIC using a pair
of 256-byte translation tables.
Two sources of tables are supported:
* ``"internal"`` (the default) — the tables printed in Section 5 of the
format specification. These reproduce, byte for byte, the translation
used by the reference TERSE tooling and are the right choice for
interoperating with AMATERSE.
* Any Python codec name for an EBCDIC code page (``"cp037"``,
``"cp1047"``, ``"cp500"``, ...). Installing the third-party
:mod:`ebcdic` package makes additional code pages such as ``cp1141``
available; it is imported automatically when present.
"""
from __future__ import annotations
import logging
from .exceptions import TerseParameterError
logger = logging.getLogger(__name__)
try: # register the extra EBCDIC code pages when the package is available
import ebcdic as _ebcdic # noqa: F401
HAVE_EBCDIC_PACKAGE = True
except ImportError: # pragma: no cover - depends on environment
HAVE_EBCDIC_PACKAGE = False
#: Name of the built-in spec translation, accepted by :func:`get_tables`.
INTERNAL_CODEPAGE = "internal"
#: Substitute byte used when a character has no mapping in the target
#: encoding: ASCII SUB.
ASCII_SUB = 0x1A
#: Substitute byte used when a character has no EBCDIC mapping: EBCDIC SUB.
EBCDIC_SUB = 0x3F
#: EBCDIC → ASCII table from spec §5.1, indexed by the EBCDIC byte.
EBC_TO_ASC = bytes.fromhex(
"00010203CF09D37FD4D5C30B0C0D0E0F"
"10111213C7B408C91819CCCD831DD21F"
"81821C84860A171B89919295A2050607"
"E0EE16E5D01EEA048AF6C6C21415C11A"
"20A6E180EB909FE2AB8B9B2E3C282B7C"
"26A9AA9CDBA599E3A89E21242A293B5E"
"2D2FDFDC9ADDDE989DACBA2C255F3E3F"
"D78894B0B1B2FCD6FB603A2340273D22"
"F861626364656667686996A4F3AFAEC5"
"8C6A6B6C6D6E6F7071729787CE93F1FE"
"C87E737475767778797AEFC0DA5BF2F9"
"B5B6FDB7B8B9E6BBBCBD8DD9BF5DD8C4"
"7B414243444546474849CBCABEE8ECED"
"7D4A4B4C4D4E4F505152A1ADF5F4A38F"
"5CE7535455565758595AA0858EE9E4D1"
"30313233343536373839B3F7F0FAA7FF"
)
#: ASCII → EBCDIC table from spec §5.2, indexed by the ASCII byte.
ASC_TO_EBC = bytes.fromhex(
"00010203372D2E2F1605250B0C0D0E0F"
"101112133C3D322618193F27221D351F"
"405A7F7B5B6C507D4D5D5C4E6B604B61"
"F0F1F2F3F4F5F6F7F8F97A5E4C7E6E6F"
"7CC1C2C3C4C5C6C7C8C9D1D2D3D4D5D6"
"D7D8D9E2E3E4E5E6E7E8E9ADE0BD5F6D"
"79818283848586878889919293949596"
"979899A2A3A4A5A6A7A8A9C04FD0A107"
"4320211C23EB249B7128384990BAECDF"
"45292A9D722B8A9A6756644A53685946"
"EADA2CDE8B5541FE5851524869DB8E8D"
"737475FA15B0B1B3B4B56AB7B8B9CCBC"
"AB3E3B0ABF8F3A14A017CBCA1A1B9C04"
"34EF1E0608097770BEBBAC5463656662"
"30424757EE33B6E1CDED3644CECF31AA"
"FC9EAE8CDDDC39FB80AFFD7876B29FFF"
)
def _tables_from_codec(codepage: str) -> "tuple[bytes, bytes]":
"""Build a pair of 256-byte translation tables from a Python codec.
:param codepage: Name of a Python codec that maps single bytes to
single characters (any EBCDIC code page qualifies).
:returns: ``(ebc_to_asc, asc_to_ebc)`` tables, each 256 bytes.
:raises TerseParameterError: If the codec name is unknown.
"""
e2a = bytearray(256)
a2e = bytearray(256)
try:
for e in range(256):
ch = bytes((e,)).decode(codepage, errors="replace")
try:
e2a[e] = ord(ch.encode("latin-1"))
except (UnicodeEncodeError, TypeError):
e2a[e] = ASCII_SUB
for a in range(256):
try:
enc = chr(a).encode(codepage)
a2e[a] = enc[0] if len(enc) == 1 else EBCDIC_SUB
except UnicodeEncodeError:
a2e[a] = EBCDIC_SUB
except LookupError as exc:
hint = ""
if not HAVE_EBCDIC_PACKAGE:
hint = " (installing the 'ebcdic' package adds more code pages)"
raise TerseParameterError(
f"unknown code page {codepage!r}{hint}"
) from exc
return bytes(e2a), bytes(a2e)
[docs]
def get_tables(codepage: str = INTERNAL_CODEPAGE) -> "tuple[bytes, bytes]":
"""Return the EBCDIC→ASCII and ASCII→EBCDIC tables for a code page.
:param codepage: ``"internal"`` for the specification's built-in tables
(the AMATERSE-compatible default), or any Python/:mod:`ebcdic`
codec name such as ``"cp037"`` or ``"cp1047"``.
:returns: Tuple ``(ebc_to_asc, asc_to_ebc)`` of two 256-byte tables,
each indexed by the source byte and yielding the target byte.
:raises TerseParameterError: If *codepage* names no known codec.
"""
if codepage.lower() in (INTERNAL_CODEPAGE, "default", "terse"):
return EBC_TO_ASC, ASC_TO_EBC
logger.debug("building translation tables from codec %r", codepage)
return _tables_from_codec(codepage)