This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user