Files
fpc-msgbase/docs/format-notes/hudson.md

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" format
  • PostDate: 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

  1. Read each HudsonHdrRec from MSGHDR.BBS sequentially
  2. Skip deleted (bit 0 set) unless recovering
  3. If Renumber: assign sequential message numbers
  4. Rebuild MSGIDX.BBS, MSGTOIDX.BBS from headers
  5. Rebuild MSGINFO.BBS: recalculate LowMsg, HighMsg, TotalMsgs, TotalOnBoard[]

Pack Algorithm

  1. Rename MSGHDR.BBS → .BAK, MSGTXT.BBS → .BAK
  2. Read old headers sequentially
  3. Skip deleted/purged messages (age check uses ParseHudsonDate with 1950 pivot)
  4. For kept messages: copy text blocks to new MSGTXT.BBS, recalculate StartBlock/NumBlocks
  5. Write updated headers to new MSGHDR.BBS
  6. Call ReIndex to regenerate index files
  7. 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