158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
#!/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())
|