-
Notifications
You must be signed in to change notification settings - Fork 0
QWK.NET ‐ QWK Packet Reference
Offset Length Field Format Validation
------ ------ ------------------ -------------- ---------------------------
0 1 Status byte ASCII char 0x20-0x7E (printable)
1 7 Message number ASCII spaces Optional: numeric
8 8 Date MM-DD-YY Hyphens at [2] and [5]
16 5 Time HH:MM Colon at [2]
21 25 To (recipient) ASCII/CP437 Space-padded
46 25 From (sender) ASCII/CP437 Space-padded
71 25 Subject ASCII/CP437 Space-padded
96 12 Password ASCII spaces Rarely used
108 8 Reference number ASCII spaces Numeric (reply-to)
116 6 Block count ASCII spaces Right-aligned numeric
122 1 Alive flag Binary 0xE1 or 0xE2
123 2 Conference number Little-endian Unsigned short
125 2 Logical record Little-endian Unsigned short
127 1 Network tag Binary 0x20, 0x2A, or 0xFF
The following fields are highly reliable for distinguishing headers from body blocks:
Standard Format: MM-DD-YY (8 characters, per QWK specification)
Library-Supported Variants: The library accepts multiple real-world formats:
-
MM-DD-YY(hyphen, 2-digit year) - QWK spec standard -
MM/DD/YY(slash, 2-digit year) - Common BBS variant -
MM-DD-YYYY(hyphen, 4-digit year) - Extended format -
MM/DD/YYYY(slash, 4-digit year) - Extended variant (e.g., mvt2.qwk)
Validation for Header Detection:
- Byte 10 (position 2): Must be '-' (0x2D) OR '/' (0x2F)
- Byte 13 (position 5): Must be '-' (0x2D) OR '/' (0x2F)
- Delimiters must be consistent (both hyphens OR both slashes)
Why reliable for distinguishing headers from body blocks:
- Body text rarely has two delimiters in exactly these positions
- Even if body contains dates, unlikely to be at offset 8-15
- Combined with other checks, creates strong validation
- Accepting both '-' and '/' increases real-world compatibility
Examples:
Valid: "06-15-93" → bytes[10]='-', bytes[13]='-' ✓ (spec standard)
Valid: "12/31/99" → bytes[10]='/', bytes[13]='/' ✓ (slash variant)
Valid: "08/01/1994" → bytes[10]='/', bytes[13]='/' ✓ (4-digit year)
Invalid: "June 15 " → bytes[10]='e', bytes[13]='1' ✗
Invalid: "ππππππππ" → bytes[10]='π', bytes[13]='π' ✗
Invalid: "06.15.93" → bytes[10]='.', bytes[13]='.' ✗ (dots not supported)
Important Note: The header validation is for distinguishing headers from body blocks, not for full date parsing. The actual date parsing (in QwkMessageHeader.TryGetDateTime()) handles these variants properly with Y2K heuristics (00-49→2000s, 50-99→1900s) and 4-digit year validation (1980-2099).
Format: HH:MM (5 characters)
Position: 0 1 2 3 4
Value: H H : M M
Byte: 16 17 18 19 20
Validation:
- Byte 18 (position 2): Must be ':' (0x3A)
Why reliable:
- Colon at exact position 18 is uncommon in body text
- Even if body contains times, unlikely at offset 16-20
- Very strong signal when combined with date hyphens
Examples:
Valid: "22:25" → bytes[18]=':' ✓
Valid: "09:00" → bytes[18]=':' ✓
Invalid: "night" → bytes[18]='g' ✗
Invalid: "ππππ" → bytes[18]='π' ✗
Format: Single ASCII character
Valid values:
' ' (0x20) = Public message, unread
'-' (0x2D) = Public message, read
'*' (0x2A) = Private message, unread
'+' (0x2B) = Private message, read
'~' (0x7E) = Comment to sysop, unread
'`' (0x60) = Comment to sysop, read
'%' (0x25) = Password protected (sender), unread
'^' (0x5E) = Password protected (sender), read
'!' (0x21) = Password protected (group), unread
'#' (0x23) = Password protected (group), read
'$' (0x24) = Password protected (group to all)
Validation:
- Must be in range 0x20-0x7E (printable ASCII)
- Body blocks often start with letters or 0xE3 line terminators
Why reliable:
- Body text starting with control characters (< 0x20) is common
- Body text starting with high ASCII (> 0x7E) is common in BBS messages
- Headers always have printable status bytes
Examples:
Valid: 0x2A ('*') ✓ Private message
Valid: 0x20 (' ') ✓ Public message
Invalid: 0x1B ✗ ESC character (ANSI codes in body)
Invalid: 0xE3 ✗ Line terminator (body text)
Invalid: 0x00 ✗ Null (padding in body)
Format: Single byte
Valid values:
-
0xE1= Message is alive (active) -
0xE2= Message is killed (deleted)
Validation:
- Must be exactly 0xE1 or 0xE2
- No other values are valid per QWK specification
Why reliable:
- Body text at byte 122 is essentially random
- Probability of exactly 0xE1 or 0xE2 is ~0.8% (2/256)
- Very strong discriminator
Examples:
Valid: 0xE1 ✓ Active message
Valid: 0xE2 ✓ Deleted message
Invalid: 0x20 ✗ Space (common in body padding)
Invalid: 0x61 ✗ 'a' (common in body text)
Invalid: 0xE3 ✗ Line terminator (very common in bodies)
Use all four checks in combination for maximum reliability:
private static bool IsPlausibleMessageHeader(ReadOnlySpan<byte> headerBytes)
{
// Check 1: Correct size
if (headerBytes.Length != 128)
return false;
// Check 2: Status byte in printable ASCII range
byte statusByte = headerBytes[0];
if (statusByte < 0x20 || statusByte > 0x7E)
return false;
// Check 3: Date field has delimiters (hyphen OR slash) at positions 2 and 5
// Date is at bytes 8-15, so delimiters are at absolute positions 10 and 13
// Accept both '-' (0x2D) and '/' (0x2F) as valid delimiters
byte delimiter1 = headerBytes[10];
byte delimiter2 = headerBytes[13];
// Must be hyphens OR slashes (but consistent)
bool hasHyphens = (delimiter1 == (byte)'-' && delimiter2 == (byte)'-');
bool hasSlashes = (delimiter1 == (byte)'/' && delimiter2 == (byte)'/');
if (!hasHyphens && !hasSlashes)
return false;
// Check 4: Time field has colon at position 2
// Time is at bytes 16-20, so colon is at absolute position 18
if (headerBytes[18] != (byte)':')
return false;
// Check 5: Alive flag is valid
byte aliveFlag = headerBytes[122];
if (aliveFlag != 0xE1 && aliveFlag != 0xE2)
return false;
// All checks passed
return true;
}Probability of false positive (body block passing validation):
P(status OK) = 95/256 ≈ 37% (95 printable ASCII chars)
P(date delim 1) = 2/256 ≈ 0.8% (hyphen OR slash)
P(date delim 2) = 2/256 ≈ 0.8% (hyphen OR slash)
P(time colon) = 1/256 ≈ 0.4%
P(alive flag) = 2/256 ≈ 0.8%
P(all pass) = 0.37 × 0.008 × 0.008 × 0.004 × 0.008
≈ 0.000000075
≈ 1 in 13 million
Conclusion: False positive rate is negligible. A body block is extremely unlikely to pass all five checks, even when accepting both date delimiter types.
The library supports multiple real-world date format variants:
Supported formats:
MM-DD-YY ✓ Hyphens, 2-digit year (QWK spec standard)
MM/DD/YY ✓ Slashes, 2-digit year (common BBS variant)
MM-DD-YYYY ✓ Hyphens, 4-digit year (extended format)
MM/DD/YYYY ✓ Slashes, 4-digit year (e.g., mvt2.qwk historical packet)
Validation accepts:
- Both hyphen '-' (0x2D) and slash '/' (0x2F) delimiters
- Delimiters must be consistent (both positions must match)
- 4-digit years validated in range 1980-2099
- 2-digit years use Y2K heuristic: 00-49→2000s, 50-99→1900s
Historical variants NOT supported (will be skipped):
06.15.93 ✗ Dots instead of hyphens/slashes (INVALID)
15-06-93 ✗ DD-MM-YY European format (may accidentally pass validation)
Jun-15-93 ✗ Month name abbreviation (INVALID)
Recommendation:
- Accept hyphens OR slashes at validation positions 10 and 13
- Delimiters must be consistent (both same type)
- Full date parsing handles Y2K heuristics and 4-digit year validation
- In lenient mode, log when skipping non-standard formats
Scenario: Body text coincidentally has valid date/time patterns
Example:
"Meeting scheduled for 06-15-93 at 10:30 to discuss..."
Why this still fails validation:
- Status byte would need to be printable (might pass)
- Date hyphens would need to be at exact byte positions 10 and 13 (unlikely)
- Time colon would need to be at exact byte position 18 (unlikely)
- Alive flag at byte 122 would need to be 0xE1 or 0xE2 (very unlikely)
Probability: ~1 in 53 million (see statistical analysis above)
Scenario: Actual message header has corrupted date/time field
Example:
Offset 0: 0x2A (valid status)
Offset 10: 0x00 (corrupted, should be '-')
Offset 13: 0x2D (valid hyphen)
Offset 18: 0x3A (valid colon)
Offset 122: 0xE1 (valid alive flag)
Result: Fails validation (missing first hyphen), message skipped
Handling:
- Log warning: "Skipping potentially corrupted header at offset X"
- Continue to next block
- This is correct behaviour for preservation-grade software
- Better to skip one corrupted message than misparse entire packet
Scenario: Message header with no body blocks
Block count = 1 (header only, no body)
Parsing flow:
1. Read 128 bytes → validate as header ✓
2. Parse header: BlockCount = 1
3. Calculate body blocks: 1 - 1 = 0
4. Read 0 body blocks
5. Create message with empty body
6. Next iteration reads next block → validate as header
Result: Works correctly (no issue)
Scenario: Malicious or corrupted header claims 10,000 blocks
Block count = 10000 (1.25 MB for one message)
Handling:
- Current code will attempt to read 10,000 blocks
- If blocks don't exist (EOF), logs warning and stops
- No infinite loop risk (stream exhaustion stops parsing)
Future enhancement (optional):
const int MaxReasonableBlockCount = 1000; // ~128 KB
if (header.BlockCount > MaxReasonableBlockCount)
{
context.AddWarning(
$"Message {messageNumber} claims {header.BlockCount} blocks " +
$"(>{MaxReasonableBlockCount * 128} bytes). This may indicate corruption.");
// In strict mode, could throw
// In lenient mode, could cap the read or skip the message
}Example 1: Typical personal message
Offset 0: 0x2A ('*' = private, unread)
Offset 1-7: " 1 " (message number 1)
Offset 8-15: "06-15-93" (June 15, 1993)
Offset 16-20: "22:25" (10:25 PM)
Offset 21-45: "JOHN DOE" + spaces
Offset 46-70: "JANE SMITH" + spaces
Offset 71-95: "Test Message" + spaces
Offset 116-121: " 2" (2 blocks total)
Offset 122: 0xE1 (alive)
Offset 123-124: 0x0000 (conference 0, little-endian)
Validation: ✓ All checks pass
Example 2: Public read message
Offset 0: 0x2D ('-' = public, read)
Offset 8-15: "12-31-99"
Offset 16-20: "23:59"
Offset 122: 0xE1
Validation: ✓ All checks pass
Example 1: Body text with 0xE3 terminators
Offset 0: 0xE3 (line terminator, not printable ASCII)
Offset 10: 0x65 ('e' in "message")
Offset 13: 0x20 (space)
Offset 18: 0x74 ('t' in "text")
Offset 122: 0x20 (space padding)
Validation: ✗ Fails status byte check (0xE3 < 0x20)
Example 2: Body text with box-drawing characters
Offset 0: 0xB3 (│ box character, > 0x7E)
Offset 10: 0xC4 (─ box character)
Offset 13: 0xBF (┐ box character)
Offset 18: 0xB3 (│ box character)
Offset 122: 0xC4 (─ box character)
Validation: ✗ Fails status byte check (0xB3 > 0x7E)
Example 3: English text body
Offset 0: 0x54 ('T' in "This is...")
Offset 8-15: "his is a" (no hyphens at 10, 13)
Offset 16-20: " mess" (no colon at 18)
Offset 122: 0x67 ('g' in "something")
Validation: ✗ Fails date hyphen check AND time colon check
Each validation consists of:
- 1× length check (branch)
- 4× byte comparisons (cheap)
- 1× byte range check (two comparisons)
Total: ~7 comparisons per block
Cost: Negligible compared to:
- Stream I/O (reading 128 bytes)
- Memory allocation
- CP437 decoding
- String construction
Recommendation: Always validate, performance impact is unmeasurable.
Use ReadOnlySpan<byte> for zero-allocation validation:
// Good: No allocation
byte[] headerBytes = new byte[128];
stream.Read(headerBytes, 0, 128);
bool isValid = IsPlausibleMessageHeader(headerBytes); // ReadOnlySpan<byte> implicit cast
// Also good: Explicit span
ReadOnlySpan<byte> span = headerBytes.AsSpan();
bool isValid = IsPlausibleMessageHeader(span);Critical validation fields:
- Status byte: 0x20-0x7E
- Date hyphens: bytes[10]='-', bytes[13]='-'
- Time colon: bytes[18]=':'
- Alive flag: 0xE1 or 0xE2
Implementation:
- Simple, fast byte comparisons
- No allocations
- ~1 in 53 million false positive rate
- Handles malformed packets gracefully
Testing:
- Test with real-world malformed packets
- Test with synthetic corrupted data
- Verify round-trip preservation