cm.session
----------
Building blocks for the Comet wire handshake (FSP-COMET-001
section 7), pure encoding / decoding -- no I/O state machine,
no CRC, no transport. cm.driver (next commit) will compose
these with cm.frame + transport into the actual session
engine.
Records (use mb.address.TFTNAddress, not the daemon's
TCometAddress, so wire layout is consistent across the
ecosystem):
TCometInitInfo -- pre-handshake info we send / they send
in NPKT_INIT. Wire encoding documented inline:
uint32 LE Version
uint32 LE Caps (COPT_* mask)
uint32 LE MaxBlock
uint32 LE WindowSize (low 16 bits)
uint8 NumAddresses
[Zone(2) Net(2) Node(2) Point(2)] * NumAddresses
NUL strings: Password, SysName, SysOp, Mailer,
Location, Time, Phone, NodelistFlags
Last four trailing strings are backward-compatible -- a
reader that runs off the end of the actual payload just
gets empty values for the missing tail fields.
TCometNegotiated -- agreed session params after INIT
exchange (caps = AND of both sides, block / window =
min of both sides, remote AKAs / metadata copied).
Functions:
CMSendBanner / CMRecvBanner -- transport-level banner
exchange via IComTransport. CMRecvBanner does the
BinkP-vs-Comet first-byte detection and returns
cbrBinkP + a 1-byte PeekBuf when the peer's first
byte has the high bit set, so the daemon can hand off
cleanly to its BinkP backend (the same pattern the
Comet daemon uses today between cometses + cometbinkp).
CMBuildInit / CMParseInit -- NPKT_INIT codec.
CMBuildInitAck / CMParseInitAck -- NPKT_INITACK codec.
CMNegotiate -- intersect caps + min block/window,
preserve WeAreOriginator across the call.
cm.session has no logger or transport state of its own --
it's pure helpers that cm.driver will compose.
test_init
---------
39 checks across 5 cases:
- Basic INIT (1 address, no trailing) round-trips.
- Full INIT (2 addresses, all 4 trailing strings)
round-trips byte-exact.
- Truncated trailing tolerated (rev-1 backward compat:
cut the wire after the Mailer NUL, re-parse, get
empty Location/Time/Phone/NodelistFlags).
- CMNegotiate computes AND of caps, min of block /
window, preserves WeAreOriginator.
- INITACK round-trip (12 bytes exact).
run_tests.sh
------------
Mirror of fpc-binkp's test driver -- builds the library
natively, then compiles + runs every test_*.pas under
tests/. Produces the same PASS / FAIL / Results: line
format.
State: 14 cm.* units compile clean on all 7 targets
(x86_64-linux, x86_64-freebsd, i386-go32v2, i386-os2,
i386-win32, i386-linux, i386-freebsd). 60 tests across
2 test programs all green.
Next: cm.driver (the TComSession state machine that
threads cm.frame + cm.session + cm.crypto into the actual
NextStep loop) and cm.transport.tcp (UNIX BSD socket
reference impl), then the cm.xfer file-transfer state
machine.
212 lines
6.1 KiB
ObjectPascal
212 lines
6.1 KiB
ObjectPascal
{ test_init -- INIT codec round-trip + negotiation. }
|
|
|
|
program test_init;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$modeswitch advancedrecords}
|
|
|
|
uses
|
|
Classes, SysUtils,
|
|
mb.address,
|
|
cm.types, cm.session;
|
|
|
|
var
|
|
TestsRun, TestsFailed: Integer;
|
|
|
|
procedure Check(Cond: Boolean; const Msg: string);
|
|
begin
|
|
Inc(TestsRun);
|
|
if not Cond then
|
|
begin
|
|
Inc(TestsFailed);
|
|
Writeln(' FAIL: ', Msg);
|
|
end;
|
|
end;
|
|
|
|
procedure CheckStr(const A, B, Msg: string);
|
|
begin
|
|
Check(A = B, Format('%s (got "%s", want "%s")', [Msg, A, B]));
|
|
end;
|
|
|
|
procedure TestInitRoundTripBasic;
|
|
var
|
|
Built: TBytes;
|
|
I, P: TCometInitInfo;
|
|
begin
|
|
Writeln('TestInitRoundTripBasic');
|
|
|
|
I := Default(TCometInitInfo);
|
|
I.Version := CM_PROTO_REVISION;
|
|
I.Caps := COPT_SHA256 or COPT_FREQ or COPT_NOPWD;
|
|
I.MaxBlock := CM_DEF_BLOCK_SIZE;
|
|
I.WindowSize := CM_DEF_WINDOW;
|
|
SetLength(I.Addresses, 1);
|
|
I.Addresses[0] := MakeFTNAddress(1, 218, 720, 0);
|
|
I.Password := 'secret';
|
|
I.SysName := 'Test System';
|
|
I.SysOp := 'Sysop';
|
|
I.Mailer := 'fpc-comet/0.1.0';
|
|
|
|
Built := CMBuildInit(I);
|
|
Check(Length(Built) > 17, 'built INIT > 17 bytes');
|
|
|
|
Check(CMParseInit(Built, P), 'parse round-trips OK');
|
|
Check(P.Version = I.Version, 'Version');
|
|
Check(P.Caps = I.Caps, 'Caps');
|
|
Check(P.MaxBlock = I.MaxBlock, 'MaxBlock');
|
|
Check(P.WindowSize = I.WindowSize, 'WindowSize');
|
|
Check(Length(P.Addresses) = 1, 'one address parsed');
|
|
Check(FTNAddressEqual(P.Addresses[0], I.Addresses[0]),
|
|
'address preserved');
|
|
CheckStr(P.Password, I.Password, 'Password');
|
|
CheckStr(P.SysName, I.SysName, 'SysName');
|
|
CheckStr(P.SysOp, I.SysOp, 'SysOp');
|
|
CheckStr(P.Mailer, I.Mailer, 'Mailer');
|
|
CheckStr(P.Location, '', 'Location empty');
|
|
CheckStr(P.Phone, '', 'Phone empty');
|
|
end;
|
|
|
|
procedure TestInitWithTrailingStrings;
|
|
var
|
|
Built: TBytes;
|
|
I, P: TCometInitInfo;
|
|
begin
|
|
Writeln('TestInitWithTrailingStrings');
|
|
|
|
I := Default(TCometInitInfo);
|
|
I.Version := CM_PROTO_REVISION;
|
|
I.Caps := COPT_ALL;
|
|
I.MaxBlock := CM_MAX_BLOCK_SIZE;
|
|
I.WindowSize := CM_MAX_WINDOW;
|
|
SetLength(I.Addresses, 2);
|
|
I.Addresses[0] := MakeFTNAddress(1, 218, 720, 0);
|
|
I.Addresses[1] := MakeFTNAddress(1, 218, 720, 5);
|
|
I.Password := 'p';
|
|
I.SysName := 'Multi-AKA Node';
|
|
I.SysOp := 'Operator';
|
|
I.Mailer := 'fpc-comet/test';
|
|
I.Location := 'Reno, NV';
|
|
I.Time := 'Wed, 22 Apr 2026 12:34:56 +0000';
|
|
I.Phone := '775-555-0100';
|
|
I.NodelistFlags := 'CM,V90C,X75,IBN,XX,U,T9';
|
|
|
|
Built := CMBuildInit(I);
|
|
Check(CMParseInit(Built, P), 'parse round-trips');
|
|
Check(Length(P.Addresses) = 2, 'two addresses');
|
|
Check(FTNAddressEqual(P.Addresses[0], I.Addresses[0]), 'AKA 0');
|
|
Check(FTNAddressEqual(P.Addresses[1], I.Addresses[1]), 'AKA 1');
|
|
CheckStr(P.Location, I.Location, 'Location');
|
|
CheckStr(P.Time, I.Time, 'Time');
|
|
CheckStr(P.Phone, I.Phone, 'Phone');
|
|
CheckStr(P.NodelistFlags, I.NodelistFlags, 'NodelistFlags');
|
|
end;
|
|
|
|
procedure TestInitTruncatedTrailingTolerated;
|
|
var
|
|
Full, Truncated: TBytes;
|
|
I, P: TCometInitInfo;
|
|
CutAt, J: Integer;
|
|
begin
|
|
Writeln('TestInitTruncatedTrailingTolerated');
|
|
|
|
I := Default(TCometInitInfo);
|
|
I.Version := CM_PROTO_REVISION;
|
|
I.Caps := COPT_NOPWD;
|
|
I.MaxBlock := CM_DEF_BLOCK_SIZE;
|
|
I.WindowSize := CM_DEF_WINDOW;
|
|
SetLength(I.Addresses, 1);
|
|
I.Addresses[0] := MakeFTNAddress(1, 213, 725, 0);
|
|
I.Password := 'p'; I.SysName := 's'; I.SysOp := 'o'; I.Mailer := 'm';
|
|
I.Location := 'L'; I.Phone := 'PH';
|
|
Full := CMBuildInit(I);
|
|
|
|
{ Truncate after Mailer NUL. Walk the payload counting
|
|
the first 4 NUL terminators (Password, SysName, SysOp,
|
|
Mailer) and cut there. }
|
|
CutAt := 17 + 8; { fixed + 1 address }
|
|
for J := 0 to 3 do
|
|
begin
|
|
while (CutAt < Length(Full)) and (Full[CutAt] <> 0) do
|
|
Inc(CutAt);
|
|
Inc(CutAt); { past the NUL }
|
|
end;
|
|
SetLength(Truncated, CutAt);
|
|
Move(Full[0], Truncated[0], CutAt);
|
|
|
|
Check(CMParseInit(Truncated, P), 'truncated payload parses');
|
|
CheckStr(P.Password, 'p', 'rev-1 fields preserved');
|
|
CheckStr(P.SysName, 's', 'rev-1 SysName');
|
|
CheckStr(P.Mailer, 'm', 'rev-1 Mailer');
|
|
CheckStr(P.Location, '', 'trailing Location empty');
|
|
CheckStr(P.Phone, '', 'trailing Phone empty');
|
|
end;
|
|
|
|
procedure TestNegotiation;
|
|
var
|
|
A, B: TCometInitInfo;
|
|
Sess: TCometNegotiated;
|
|
begin
|
|
Writeln('TestNegotiation');
|
|
|
|
A := Default(TCometInitInfo);
|
|
A.Caps := COPT_SHA256 or COPT_FREQ or COPT_LST or COPT_NOPWD;
|
|
A.MaxBlock := 16384;
|
|
A.WindowSize := 8;
|
|
|
|
B := Default(TCometInitInfo);
|
|
B.Caps := COPT_SHA256 or COPT_LST or COPT_CRYPT;
|
|
B.MaxBlock := 8192;
|
|
B.WindowSize := 4;
|
|
SetLength(B.Addresses, 1);
|
|
B.Addresses[0] := MakeFTNAddress(1, 213, 700, 0);
|
|
B.SysName := 'Other';
|
|
B.Mailer := 'other';
|
|
|
|
Sess := Default(TCometNegotiated);
|
|
Sess.WeAreOriginator := True;
|
|
CMNegotiate(A, B, Sess);
|
|
|
|
Check(Sess.SharedCaps = (COPT_SHA256 or COPT_LST),
|
|
'caps = AND of both sides');
|
|
Check(Sess.MaxBlock = 8192, 'MaxBlock = min(A,B)');
|
|
Check(Sess.WindowSize = 4, 'WindowSize = min(A,B)');
|
|
Check(Length(Sess.RemoteAddrs) = 1, 'remote AKAs preserved');
|
|
Check(Sess.WeAreOriginator, 'WeAreOriginator preserved across negotiate');
|
|
CheckStr(Sess.RemoteSysName, 'Other', 'RemoteSysName preserved');
|
|
end;
|
|
|
|
procedure TestInitAckRoundTrip;
|
|
var
|
|
Sess, P: TCometNegotiated;
|
|
Built: TBytes;
|
|
Caps, Block: LongWord;
|
|
Win: Word;
|
|
begin
|
|
Writeln('TestInitAckRoundTrip');
|
|
|
|
Sess.SharedCaps := COPT_SHA256 or COPT_FREQ;
|
|
Sess.MaxBlock := 32768;
|
|
Sess.WindowSize := 16;
|
|
|
|
Built := CMBuildInitAck(Sess);
|
|
Check(Length(Built) = 12, 'INITACK is 12 bytes');
|
|
|
|
Check(CMParseInitAck(Built, Caps, Block, Win), 'parse INITACK');
|
|
Check(Caps = Sess.SharedCaps, 'SharedCaps');
|
|
Check(Block = Sess.MaxBlock, 'MaxBlock');
|
|
Check(Win = Sess.WindowSize, 'WindowSize');
|
|
end;
|
|
|
|
begin
|
|
TestsRun := 0;
|
|
TestsFailed := 0;
|
|
TestInitRoundTripBasic;
|
|
TestInitWithTrailingStrings;
|
|
TestInitTruncatedTrailingTolerated;
|
|
TestNegotiation;
|
|
TestInitAckRoundTrip;
|
|
Writeln;
|
|
Writeln(Format('%d tests run, %d failed', [TestsRun, TestsFailed]));
|
|
if TestsFailed > 0 then Halt(1);
|
|
end.
|