Source code for terselib.header
"""Layer 0 — parsing and building TERSE file headers (spec §2).
Two header layouts exist:
* **Native** (version ``0x01`` / ``0x07``): 4 bytes — the version byte plus
three fixed validation bytes. Always SPACK, binary, fixed-record.
* **Host** (version ``0x02`` = PACK, ``0x05`` = SPACK): 12 bytes carrying
the record format (F/V), the record length, and informational MVS fields.
"""
from __future__ import annotations
import dataclasses
import logging
import struct
from . import constants as C
from .exceptions import TerseFormatError, TerseParameterError
logger = logging.getLogger(__name__)
#: BlockSize used by the MVS-annotated header form: the usable half-track
#: capacity of a 3390 DASD volume, as written by AMATERSE.
MVS_BLOCK_CAPACITY = 27998
[docs]
@dataclasses.dataclass
class TerseHeader:
"""Parsed representation of a TERSE file header.
:ivar version: The version byte (``0x01``, ``0x02``, ``0x05`` or
``0x07``).
:ivar method: Compression method, ``"PACK"`` or ``"SPACK"``.
:ivar host: True for host files (12-byte header), False for native.
:ivar variable: True when the record format is variable (V/VB).
:ivar record_length: Effective record length. For fixed files this is
the LRECL; for variable files it is the maximum record *data*
length (LRECL − 4). ``0`` for native files, which carry none.
:ivar flags: Raw flags byte (host only).
:ivar ratio: Compression-ratio hint (informational, host only).
:ivar block_size: Block size hint (informational, host only).
:ivar size: Header size in bytes (4 or 12).
"""
version: int
method: str
host: bool
variable: bool
record_length: int
flags: int = 0
ratio: int = 0
block_size: int = 0
size: int = 12
[docs]
def describe(self) -> str:
"""Return a human-readable multi-line summary of the header."""
lines = [
f"version : 0x{self.version:02X} "
f"({'host' if self.host else 'native'} {self.method})",
f"method : {self.method} "
f"({'LZW' if self.method == C.METHOD_PACK else 'LZMW'})",
f"record format: {'V (variable)' if self.variable else 'F (fixed)'}"
if self.host else "record format: n/a (native raw stream)",
]
if self.host:
lrecl = (self.record_length + 4) if self.variable \
else self.record_length
lines.append(f"record length: {self.record_length} "
f"(LRECL {lrecl})")
lines.append(f"flags : 0x{self.flags:02X}"
+ (" [MVS]" if self.flags & C.FLAGMVS else ""))
lines.append(f"ratio hint : {self.ratio}")
lines.append(f"block size : {self.block_size}")
return "\n".join(lines)
[docs]
def parse_header(data: bytes) -> TerseHeader:
"""Parse and validate the header at the start of *data* (spec §2, §12).
:param data: The file image (only the first 12 bytes are examined).
:returns: A validated :class:`TerseHeader`; its :attr:`~TerseHeader.size`
gives the offset at which the 12-bit code stream begins.
:raises TerseFormatError: If the header is not a valid TERSE header.
"""
if len(data) < 4:
raise TerseFormatError("file too short to be a TERSE file")
version = data[0]
if version in (C.VERSION_NATIVE_SPACK, C.VERSION_NATIVE_SPACK_ALT):
if data[1:4] != C.NATIVE_VALIDATION:
raise TerseFormatError(
"bad native-header validation bytes "
f"{data[1:4].hex()} (expected {C.NATIVE_VALIDATION.hex()})")
hdr = TerseHeader(version=version, method=C.METHOD_SPACK,
host=False, variable=False, record_length=0,
size=4)
logger.debug("parsed native header: %s", hdr)
return hdr
if version not in (C.VERSION_HOST_PACK, C.VERSION_HOST_SPACK):
raise TerseFormatError(
f"unknown TERSE version byte 0x{version:02X}")
if len(data) < 12:
raise TerseFormatError("file too short for a host TERSE header")
variable_flag = data[1]
if variable_flag not in (0x00, 0x01):
raise TerseFormatError(
f"invalid VariableFlag 0x{variable_flag:02X}")
reclen1, flags, ratio, blocksize, reclen2 = struct.unpack(
">HBBHI", data[2:12])
if reclen1 == 0 and reclen2 == 0:
raise TerseFormatError("both record-length fields are zero")
if reclen1 != 0 and reclen2 != 0 and reclen1 != reclen2:
raise TerseFormatError(
f"inconsistent record lengths {reclen1} != {reclen2}")
record_length = reclen1 if reclen1 != 0 else reclen2
if record_length >= 2 ** 31:
raise TerseFormatError(f"record length {record_length} out of range")
if not flags & C.FLAGMVS:
if flags or ratio or blocksize:
raise TerseFormatError(
"FLAGMVS clear but Flags/Ratio/BlockSize are non-zero")
method = C.METHOD_PACK if version == C.VERSION_HOST_PACK \
else C.METHOD_SPACK
hdr = TerseHeader(version=version, method=method, host=True,
variable=(variable_flag == 0x01),
record_length=record_length, flags=flags,
ratio=ratio, block_size=blocksize, size=12)
logger.debug("parsed host header: %s", hdr)
return hdr
[docs]
def build_header(method: str, host: bool, variable: bool,
record_length: int, mvs_style: bool = True) -> bytes:
"""Build a header for a file being compressed (spec §2, §8.3).
:param method: ``"PACK"`` or ``"SPACK"``.
:param host: True for a host file (12-byte header), False for native
(4-byte header; forces SPACK).
:param variable: True for variable record format (host only).
:param record_length: Record length to record in the header — the
LRECL for fixed files, LRECL − 4 (the maximum record data length)
for variable files. Ignored for native files.
:param mvs_style: When true (the default), write the MVS-annotated
header form AMATERSE itself produces (``FLAGMVS | FLAGVS`` and a
3390 half-track block size); when false, write the minimal form
with ``Flags = Ratio = BlockSize = 0``.
:returns: The encoded header bytes.
:raises TerseParameterError: On an invalid method / mode combination.
"""
if method not in (C.METHOD_PACK, C.METHOD_SPACK):
raise TerseParameterError(f"unknown method {method!r}")
if not host:
if method != C.METHOD_SPACK:
raise TerseParameterError("native files always use SPACK")
return bytes((C.VERSION_NATIVE_SPACK,)) + C.NATIVE_VALIDATION
if record_length <= 0:
raise TerseParameterError(
f"record length must be positive, got {record_length}")
if record_length >= 2 ** 31:
raise TerseParameterError(
f"record length {record_length} out of range")
version = C.VERSION_HOST_PACK if method == C.METHOD_PACK \
else C.VERSION_HOST_SPACK
if record_length > 0xFFFF:
reclen1, reclen2 = 0, record_length
else:
reclen1, reclen2 = record_length, 0
flags = ratio = blocksize = 0
if mvs_style:
flags = C.FLAGMVS | C.FLAGVS
if variable:
blocksize = MVS_BLOCK_CAPACITY
else:
blocksize = max(
record_length,
(MVS_BLOCK_CAPACITY // record_length) * record_length
if record_length <= MVS_BLOCK_CAPACITY else record_length)
blocksize = min(blocksize, 0xFFFF)
return struct.pack(">BBHBBHI", version, 0x01 if variable else 0x00,
reclen1, flags, ratio, blocksize, reclen2)