7.0 KiB
msg_hudson.pas — Hudson Message Base Handler
Overview
Create msg_hudson.pas in af_2026/allfix/msg/, following the same class-based
pattern as msg_jam.pas and msg_squish.pas. Replaces raw file I/O in msgunit.pas,
doscan.pas, and msgutil.pas with a clean THudsonBase class.
Hudson Format
Five files sharing a common path prefix:
- MSGINFO.BBS — 1 record: low/high msg#, total count, per-board counts (200 boards)
- MSGIDX.BBS — N records: msg# + board# per message
- MSGHDR.BBS — N records: full message headers (fixed size)
- MSGTOIDX.BBS — N records: String[35] recipient name per message
- MSGTXT.BBS — variable String[255] text blocks, referenced by StartBlock/NumBlocks
Record Types (internalized from hudson.inc)
HudsonInfoRec — 406 bytes (6 + 200*2)
HudsonIdxRec — 3 bytes (Integer + Byte)
HudsonToIdxRec — String[35] (36 bytes)
HudsonHdrRec — variable (uses String[35], String[72], String[5], String[8])
HudsonTxtRec — String[255] (256 bytes)
Key field types:
PostTime: String[5]— "HH:MM" formatPostDate: String[8]— "MM-DD-YY" format (2-digit year, 1950 pivot: 00-49→2000s, 50-99→1900s)
THudsonMessage Record
High-level parsed message (AnsiString fields — never FillChar):
THudsonMessage = record
MsgNum: integer;
Board: byte;
WhoFrom: string[35];
WhoTo: string[35];
Subject: string[72];
PostTime: string[5]; { "HH:MM" }
PostDate: string[8]; { "MM-DD-YY" }
MsgAttr: byte; { bit 0=deleted, 4=rcvd, 5=sent, 6=local }
NetAttr: byte;
OrigZone: byte;
OrigNet: word;
OrigNode: word;
DestZone: byte;
DestNet: word;
DestNode: word;
Cost: word;
PrevReply: word;
NextReply: word;
TimesRead: word;
Body: AnsiString; { joined from text blocks }
end;
THudsonBase Class
THudsonBase = class
private
FBasePath: string; { directory path }
FInfoStream: TFileStream; { MSGINFO.BBS }
FIdxStream: TFileStream; { MSGIDX.BBS }
FHdrStream: TFileStream; { MSGHDR.BBS }
FTxtStream: TFileStream; { MSGTXT.BBS }
FToIdxStream: TFileStream; { MSGTOIDX.BBS }
FInfo: HudsonInfoRec;
FInfoRead: boolean;
FReadOnly: boolean;
FIsOpen: boolean;
public
constructor Create(const ABasePath: string);
destructor Destroy; override;
function OpenReadOnly: boolean;
function OpenReadWrite: boolean;
procedure Close;
{ Message count from index file }
function MessageCount: longint;
{ Read message by index position (0-based) }
function ReadMessage(Index: longint; var Msg: THudsonMessage): boolean;
{ Read just the header (no body) }
function ReadHeader(Index: longint; var Hdr: HudsonHdrRec): boolean;
{ Update header in place }
procedure UpdateHeader(Index: longint; const Hdr: HudsonHdrRec);
{ Write new message — appends to all 5 files }
function WriteMessage(var Msg: THudsonMessage): boolean;
{ Read body text from StartBlock/NumBlocks }
function ReadBody(StartBlock, NumBlocks: word): AnsiString;
{ Rebuild MSGIDX + MSGTOIDX + MSGINFO from MSGHDR }
function ReIndex(Renumber: boolean): THudsonReIndexStats;
{ Pack: purge deleted/old messages, compact MSGHDR + MSGTXT }
function Pack(PurgeAgeDays, PurgeMaxCount: word;
Renumber, Backup: boolean): THudsonPackStats;
{ Date helpers }
class function ParseHudsonDate(const D: string[8]): TDateTime;
class function FormatHudsonDate(DT: TDateTime): string;
class function FormatHudsonTime(DT: TDateTime): string;
{ Attribute helpers }
class function AttrDeleted(Attr: byte): boolean; { bit 0 }
class function AttrRcvd(Attr: byte): boolean; { bit 4 }
class function AttrSent(Attr: byte): boolean; { bit 5 }
class function AttrLocal(Attr: byte): boolean; { bit 6 }
property BasePath: string read FBasePath;
property IsOpen: boolean read FIsOpen;
property ReadOnlyMode: boolean read FReadOnly;
property Info: HudsonInfoRec read FInfo;
end;
Date Handling
ParseHudsonDate: "MM-DD-YY" → TDateTime with 1950 pivot:
- YY 00-49 → 2000-2049
- YY 50-99 → 1950-1999
FormatHudsonDate: TDateTime → "MM-DD-YY" (same pivot logic) FormatHudsonTime: TDateTime → "HH:MM"
Text Block I/O
Reading: Read NumBlocks consecutive String[255] records starting at StartBlock.
Each record is 256 bytes (1 length byte + up to 255 data bytes). Concatenate to form body.
Writing: Split body into 255-byte chunks, each stored as String[255]. Record StartBlock = current file position / 256. NumBlocks = number of chunks written.
ReIndex Algorithm
- Read each HudsonHdrRec from MSGHDR.BBS sequentially
- Skip deleted (bit 0 set) unless recovering
- If Renumber: assign sequential message numbers
- Rebuild MSGIDX.BBS, MSGTOIDX.BBS from headers
- Rebuild MSGINFO.BBS: recalculate LowMsg, HighMsg, TotalMsgs, TotalOnBoard[]
Pack Algorithm
- Rename MSGHDR.BBS → .BAK, MSGTXT.BBS → .BAK
- Read old headers sequentially
- Skip deleted/purged messages (age check uses ParseHudsonDate with 1950 pivot)
- For kept messages: copy text blocks to new MSGTXT.BBS, recalculate StartBlock/NumBlocks
- Write updated headers to new MSGHDR.BBS
- Call ReIndex to regenerate index files
- Clean up .BAK unless Backup requested
Test Plan (test_hudson.pas)
Target: 120+ tests
- Record size validation (HudsonInfoRec=406, HudsonIdxRec=3, HudsonToIdxRec=36, HudsonHdrRec size)
- Date parsing: "02-28-26" → 2026, "12-31-99" → 1999, "01-01-50" → 1950, "12-31-49" → 2049
- Date formatting round-trip
- Time formatting: "14:30", "00:00", "23:59"
- Attribute bit helpers
- Create empty base + verify 5 files created
- Write single message + read back
- Write multiple messages across different boards + verify TotalOnBoard[]
- Text block encoding: short body (1 block), long body (multiple blocks), empty body
- ReadHeader without body
- UpdateHeader: toggle sent flag, verify persisted
- ReIndex: delete message, reindex, verify count
- ReIndex with Renumber: verify sequential numbering
- Pack with age purge
- Pack with count purge
- Pack with backup files
- Round-trip: write N messages, pack, verify survivors
- 200-board boundary: messages on board 1 and board 200
- Case-insensitive file finding (MSGINFO.BBS vs msginfo.bbs)
Files
| File | Action |
|---|---|
af_2026/allfix/msg/msg_hudson.pas |
CREATE — THudsonBase class |
af_2026/allfix/test/test_hudson.pas |
CREATE — test suite |
Migration Opportunities (future, not this task)
After msg_hudson.pas is working and tested:
- msgunit.pas WriteMsg1: replace raw file I/O with THudsonBase.WriteMessage
- doscan.pas ScanHudson: replace raw reads with THudsonBase.ReadMessage + UpdateHeader
- msgutil.pas ReIndexHudson/PackHudson: thin wrappers around THudsonBase methods