Source code for terselib.translate

"""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)