Two functional units land plus a unit test. No tests/
infrastructure (run_tests.sh) or session/transfer logic
yet -- those follow in subsequent commits.
cm.types
--------
Pure constants + records, no I/O dependencies. Lifts the
on-the-wire constants and types out of cometdef.pas (the
Comet daemon's omnibus def file) into a focused unit:
- All NPKT_* packet-type byte codes from FSP-COMET-001
section 6, including the LST trio (Q/R/S) and the
KEYEX/KEYEXACK pair.
- Internal pseudo-types (N_NOPKT / N_BADPKT / N_CARRIER
/ N_TIMEOUT) that the receive layer returns to signal
control conditions.
- All COPT_* capability flags from section 7.3, with
COPT_ALL convenience mask.
- Frame sizing (CM_FRAME_*), block sizing
(CM_MIN/MAX/DEF_BLOCK_SIZE), window sizing,
timeouts, adaptive-block parameters.
- High-level enums for embedders: TCometDirection,
TCometPhase (cmpInit through cmpDone), TCometAuthMethod
(numerics matching the daemon's existing AUTH_* codes),
TCometErrorCode, TCometSendFailReason,
TCometReceiveDecision.
- TCometRemoteInfo / TCometSessionStats /
TCometSessionResult records that mirror the bp.types
shape so consumers writing both BinkP and Comet code
have the same mental model.
Imports TFTNAddress from fpc-msgbase (mb.address) -- single
source of truth for the FTN address layout across the
ecosystem, same approach fpc-binkp settled on.
cm.frame
--------
TStream-based frame I/O. Implements FSP-COMET-001 section
5 wire format:
LEN(4 LE) | TYPE(1) | SEQ(1) | PAYLOAD(N) | CRC32(4 LE)
CRC-32 covers TYPE + SEQ + PAYLOAD; LEN counts everything
after itself. Inlined the CRC table (Ethernet / zip
polynomial 0xEDB88320) so the unit has zero external
dependencies on FPC's `crc` unit -- portable across all 7
target platforms with a tiny code-size cost.
Reader semantics match bp.frame: clean EOF at LEN
boundary -> False (peer closed cleanly), partial body ->
False with stream rewound so caller can retry on more
bytes, oversize header / CRC mismatch -> raise
ECometFrameError.
Frames carry their payload as TBytes (refcounted), so
unlike the cometfrm.pas source the parsed frame doesn't
need to be consumed before the next ReadFrame call.
test_frame
----------
21 checks across 6 cases:
- empty payload (NPKT_IDLE) round-trip + 10-byte wire size
- 16-byte payload round-trip + byte-exact match
- 16 KB payload round-trip + size + bytes
- half-frame returns False (not raise), stream rewound
- bit-flip in payload raises ECometFrameError on CRC
- CMPacketName mapping for known + unknown codes
All 21 pass. Cross-platform clean on all 7 targets:
x86_64-linux, x86_64-freebsd, i386-go32v2, i386-os2,
i386-win32, i386-linux, i386-freebsd.
211 lines
4.9 KiB
ObjectPascal
211 lines
4.9 KiB
ObjectPascal
{ test_frame -- pure TMemoryStream-driven exercise of cm.frame.
|
|
|
|
No sockets, no auth, no protocol state -- builds canned byte
|
|
sequences and asserts the parser decodes them correctly,
|
|
plus round-trips frames through the writer. }
|
|
|
|
program test_frame;
|
|
|
|
{$mode objfpc}{$H+}
|
|
{$modeswitch advancedrecords}
|
|
|
|
uses
|
|
Classes, SysUtils, cm.types, cm.frame;
|
|
|
|
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 TestEmptyPayload;
|
|
var
|
|
MS: TMemoryStream;
|
|
F1, F2: TCometFrame;
|
|
begin
|
|
Writeln('TestEmptyPayload');
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
{ INIT with no payload (won't happen in real protocol but
|
|
exercises the zero-length payload path). }
|
|
F1.InitPayload(NPKT_IDLE, 0, F2 { dummy }, 0);
|
|
CMWriteFrame(MS, F1);
|
|
|
|
Check(MS.Size = 4 + 2 + 4,
|
|
'empty-payload frame is 10 bytes (LEN+TYPE+SEQ+CRC)');
|
|
|
|
MS.Position := 0;
|
|
Check(CMReadFrame(MS, F2), 'round-trip empty payload');
|
|
Check(F2.PktType = NPKT_IDLE, 'PktType preserved');
|
|
Check(F2.Seq = 0, 'Seq preserved');
|
|
Check(Length(F2.Payload) = 0, 'payload still empty');
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TestSmallPayload;
|
|
var
|
|
MS: TMemoryStream;
|
|
F1, F2: TCometFrame;
|
|
Body: array[0..15] of Byte;
|
|
I: Integer;
|
|
begin
|
|
Writeln('TestSmallPayload');
|
|
for I := 0 to 15 do Body[I] := Byte(I * 17 + 1);
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
F1.InitPayload(NPKT_FINFO, 7, Body, 16);
|
|
CMWriteFrame(MS, F1);
|
|
|
|
MS.Position := 0;
|
|
Check(CMReadFrame(MS, F2), 'round-trip 16-byte payload');
|
|
Check(F2.PktType = NPKT_FINFO, 'PktType preserved');
|
|
Check(F2.Seq = 7, 'Seq preserved');
|
|
Check(Length(F2.Payload) = 16, 'payload length preserved');
|
|
Check(CompareByte(F2.Payload[0], Body[0], 16) = 0,
|
|
'payload bytes preserved');
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TestLargePayload;
|
|
var
|
|
MS: TMemoryStream;
|
|
F1, F2: TCometFrame;
|
|
Body: array[0..16383] of Byte;
|
|
I: Integer;
|
|
begin
|
|
Writeln('TestLargePayload');
|
|
for I := 0 to 16383 do Body[I] := Byte(I and $FF);
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
F1.InitPayload(NPKT_DATA, 42, Body, 16384);
|
|
CMWriteFrame(MS, F1);
|
|
|
|
Check(MS.Size = 4 + 2 + 16384 + 4,
|
|
'16K-payload frame size = LEN + TYPE+SEQ + 16K + CRC');
|
|
|
|
MS.Position := 0;
|
|
Check(CMReadFrame(MS, F2), 'round-trip 16K payload');
|
|
Check(Length(F2.Payload) = 16384, 'large payload length');
|
|
Check(CompareByte(F2.Payload[0], Body[0], 16384) = 0,
|
|
'large payload bytes preserved');
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TestPartialReadReturnsFalse;
|
|
var
|
|
MS: TMemoryStream;
|
|
F1, F2: TCometFrame;
|
|
Body: array[0..7] of Byte;
|
|
I: Integer;
|
|
Wire: TBytes;
|
|
begin
|
|
Writeln('TestPartialReadReturnsFalse');
|
|
for I := 0 to 7 do Body[I] := Byte(I);
|
|
|
|
{ Build a complete frame in memory, then strip trailing
|
|
bytes so the reader sees a partial. }
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
F1.InitPayload(NPKT_DATA, 1, Body, 8);
|
|
CMWriteFrame(MS, F1);
|
|
SetLength(Wire, MS.Size);
|
|
MS.Position := 0;
|
|
MS.Read(Wire[0], MS.Size);
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
|
|
{ Truncate to half its bytes. }
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
MS.WriteBuffer(Wire[0], Length(Wire) div 2);
|
|
MS.Position := 0;
|
|
Check(not CMReadFrame(MS, F2),
|
|
'half-frame returns False (not raise)');
|
|
Check(MS.Position = 0,
|
|
'partial-read rewinds for caller to retry');
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TestBadCRCRaises;
|
|
var
|
|
MS: TMemoryStream;
|
|
F1, F2: TCometFrame;
|
|
Body: array[0..7] of Byte;
|
|
I: Integer;
|
|
Raised: Boolean;
|
|
Wire: TBytes;
|
|
begin
|
|
Writeln('TestBadCRCRaises');
|
|
for I := 0 to 7 do Body[I] := Byte(I);
|
|
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
F1.InitPayload(NPKT_DATA, 1, Body, 8);
|
|
CMWriteFrame(MS, F1);
|
|
SetLength(Wire, MS.Size);
|
|
MS.Position := 0;
|
|
MS.Read(Wire[0], MS.Size);
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
|
|
{ Flip a bit in the payload -- CRC should fail. }
|
|
Wire[6] := Wire[6] xor $01;
|
|
|
|
MS := TMemoryStream.Create;
|
|
try
|
|
MS.WriteBuffer(Wire[0], Length(Wire));
|
|
MS.Position := 0;
|
|
Raised := False;
|
|
try
|
|
CMReadFrame(MS, F2);
|
|
except
|
|
on ECometFrameError do Raised := True;
|
|
end;
|
|
Check(Raised, 'corrupted payload raises ECometFrameError');
|
|
finally
|
|
MS.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TestPacketName;
|
|
begin
|
|
Writeln('TestPacketName');
|
|
Check(CMPacketName(NPKT_INIT) = 'NPKT_INIT', 'INIT name');
|
|
Check(CMPacketName(NPKT_DATA) = 'NPKT_DATA', 'DATA name');
|
|
Check(CMPacketName(NPKT_LSTREQ) = 'NPKT_LSTREQ', 'LSTREQ name');
|
|
Check(CMPacketName($AB) = 'NPKT_AB', 'unknown code fallback');
|
|
end;
|
|
|
|
begin
|
|
TestsRun := 0;
|
|
TestsFailed := 0;
|
|
|
|
TestEmptyPayload;
|
|
TestSmallPayload;
|
|
TestLargePayload;
|
|
TestPartialReadReturnsFalse;
|
|
TestBadCRCRaises;
|
|
TestPacketName;
|
|
|
|
Writeln;
|
|
Writeln(Format('%d tests run, %d failed', [TestsRun, TestsFailed]));
|
|
if TestsFailed > 0 then Halt(1);
|
|
end.
|