#!/usr/bin/env python3 # SPDX-License-Identifier: GPL-3.0-or-later """Host-only tests for the Phase-1.0Y shsrv framing model.""" from __future__ import annotations import argparse import importlib.util from pathlib import Path import sys def load(path: Path): spec = importlib.util.spec_from_file_location("phase10y_framing", path) assert spec and spec.loader module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module def require(value: bool, message: str) -> None: if not value: raise RuntimeError(message) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--root", type=Path, required=True) args = parser.parse_args() model = load(args.root.resolve() / "tools/phase10y_shsrv_framing_model.py") cases = [] def case(name): def register(function): cases.append((name, function)) return function return register def decode(family, chunks): decoder = model.ClientWireDecoder(family) for chunk in chunks: decoder.feed(chunk) return decoder.finalize() @case("01 legacy family passes raw bytes") def _(): require(decode(model.LEGACY_RAW, [b"help\r\n"]).application == b"help\r\n", "legacy transformed") @case("02 legacy family passes Telnet controls to shell") def _(): raw = bytes((model.IAC, model.WILL, 1)) value = decode(model.LEGACY_RAW, [raw]) require(value.application == raw and not value.negotiation_replies, "legacy negotiated") @case("03 current family maps CRLF to LF") def _(): require(decode(model.LIBTELNET_NVT, [b"help\r\n"]).application == b"help\n", "CRLF mismatch") @case("04 current family maps CRNUL to CR") def _(): require(decode(model.LIBTELNET_NVT, [b"x\r\x00"]).application == b"x\r", "CRNUL mismatch") @case("05 current family preserves CR followed by data") def _(): require(decode(model.LIBTELNET_NVT, [b"x\ry"]).application == b"x\ry", "CR data mismatch") @case("06 doubled IAC becomes one application byte") def _(): value = decode(model.LIBTELNET_NVT, [bytes((model.IAC, model.IAC))]) require(value.application == bytes((model.IAC,)), "IAC escape mismatch") @case("07 WILL receives DONT") def _(): value = decode(model.LIBTELNET_NVT, [bytes((model.IAC, model.WILL, 1))]) require(value.negotiation_replies == (bytes((model.IAC, model.DONT, 1)),), "WILL reply mismatch") @case("08 DO receives WONT") def _(): value = decode(model.LIBTELNET_NVT, [bytes((model.IAC, model.DO, 3))]) require(value.negotiation_replies == (bytes((model.IAC, model.WONT, 3)),), "DO reply mismatch") @case("09 initial WONT produces no reply") def _(): require(not decode(model.LIBTELNET_NVT, [bytes((model.IAC, model.WONT, 1))]).negotiation_replies, "WONT replied") @case("10 initial DONT produces no reply") def _(): require(not decode(model.LIBTELNET_NVT, [bytes((model.IAC, model.DONT, 1))]).negotiation_replies, "DONT replied") @case("11 fragmented negotiation is modeled") def _(): value = decode(model.LIBTELNET_NVT, [bytes((model.IAC,)), bytes((model.WILL,)), b"\x01"]) require(value.negotiation_replies == (bytes((model.IAC, model.DONT, 1)),), "fragment reply mismatch") @case("12 subnegotiation is removed") def _(): frame = bytes((model.IAC, model.SB, 31, 0, 80, model.IAC, model.SE)) require(decode(model.LIBTELNET_NVT, [frame[:2], frame[2:]]).application == b"", "subnegotiation leaked") @case("13 incomplete control fails closed") def _(): decoder = model.ClientWireDecoder(model.LIBTELNET_NVT); decoder.feed(bytes((model.IAC,))) try: decoder.finalize() except model.FramingError: return raise RuntimeError("incomplete control accepted") @case("14 outgoing newline becomes CRLF only in current family") def _(): require(model.encode_server_text(model.LIBTELNET_NVT, b"x\n") == b"x\r\n" and model.encode_server_text(model.LEGACY_RAW, b"x\n") == b"x\n", "newline encoding mismatch") @case("15 outgoing CR becomes CRNUL") def _(): require(model.encode_server_text(model.LIBTELNET_NVT, b"x\r") == b"x\r\x00", "CR encoding mismatch") @case("16 outgoing IAC is doubled") def _(): require(model.encode_server_text(model.LIBTELNET_NVT, bytes((model.IAC,))) == bytes((model.IAC, model.IAC)), "outgoing IAC mismatch") @case("17 neither family proactively negotiates") def _(): require(all(model.initial_server_bytes(family) == b"" for family in model.SOURCE_FAMILIES), "proactive bytes invented") @case("18 source-shaped prompt remains candidate only") def _(): value = model.assess_prompt_candidates(b"greeting\r\n/$ ") require(value.classification == "SOURCE_SHAPE_CANDIDATE_ONLY" and value.exact_completion_proven is False, "prompt promoted") @case("19 embedded prompt shape is ambiguous") def _(): value = model.assess_prompt_candidates(b"/tmp/$ embedded$ ") require(value.classification == "AMBIGUOUS_PROMPT_CANDIDATES" and len(value.candidate_offsets) == 2, "ambiguity missed") @case("20 nonterminal prompt shape is incomplete") def _(): value = model.assess_prompt_candidates(b"/$ output") require(value.classification == "NO_TERMINAL_PROMPT_CANDIDATE", "nonterminal candidate accepted") @case("21 model byte bound fails closed") def _(): decoder = model.ClientWireDecoder(model.LEGACY_RAW) try: decoder.feed(b"x" * (model.MAX_MODEL_BYTES + 1)) except model.FramingError: return raise RuntimeError("oversize wire accepted") @case("22 IAC command is recorded, not application data") def _(): value = decode(model.LIBTELNET_NVT, [bytes((model.IAC, 244))]) require(value.application == b"" and value.iac_commands == (244,), "IAC command mismatch") failures = [] for name, function in cases: try: function() print(f"PASS {name}") except Exception as error: # noqa: BLE001 - test harness failures.append(f"{name}: {error}") print(f"FAIL {name}: {error}") if failures: return 1 print(f"Phase-1.0Y framing-model tests passed: {len(cases)}") return 0 if __name__ == "__main__": raise SystemExit(main())