FastCGI implementation in OCaml

Add comprehensive FastCGI test case corpus for parser validation

- Generated 24 binary test files covering all FastCGI record types
- Includes management records (GET_VALUES, UNKNOWN_TYPE)
- Covers all application record types for Responder/Authorizer/Filter roles
- Tests stream patterns, multiplexing, padding, and edge cases
- Added Python generators and validators for test case creation
- Comprehensive documentation of test scenarios and protocol flows

๐Ÿค– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

+123
test_cases/README.md
···
+
# FastCGI Test Cases
+
+
This directory contains binary test case files representing various FastCGI records as defined in the FastCGI specification. These files can be used to test a FastCGI parser implementation.
+
+
## Test Case Files
+
+
### Management Records (requestId = 0)
+
+
- **`get_values.bin`** - FCGI_GET_VALUES record requesting capability information
+
- **`get_values_result.bin`** - FCGI_GET_VALUES_RESULT record with capability responses
+
- **`unknown_type.bin`** - FCGI_UNKNOWN_TYPE record for unrecognized record types
+
+
### Application Records (requestId > 0)
+
+
#### BEGIN_REQUEST Records
+
- **`begin_request_responder.bin`** - Begin request for Responder role with KEEP_CONN flag
+
- **`begin_request_no_keep.bin`** - Begin request for Responder role without KEEP_CONN flag
+
- **`begin_request_authorizer.bin`** - Begin request for Authorizer role
+
- **`begin_request_filter.bin`** - Begin request for Filter role
+
+
#### Stream Records
+
- **`params_get.bin`** - FCGI_PARAMS record with GET request environment variables
+
- **`params_post.bin`** - FCGI_PARAMS record with POST request environment variables
+
- **`params_empty.bin`** - Empty FCGI_PARAMS record (end of params stream)
+
- **`stdin_form_data.bin`** - FCGI_STDIN record with form data
+
- **`stdin_empty.bin`** - Empty FCGI_STDIN record (end of stdin stream)
+
- **`stdout_response.bin`** - FCGI_STDOUT record with HTTP response
+
- **`stdout_empty.bin`** - Empty FCGI_STDOUT record (end of stdout stream)
+
- **`stderr_message.bin`** - FCGI_STDERR record with error message
+
- **`stderr_empty.bin`** - Empty FCGI_STDERR record (end of stderr stream)
+
- **`data_filter.bin`** - FCGI_DATA record with file content (for Filter role)
+
- **`data_empty.bin`** - Empty FCGI_DATA record (end of data stream)
+
+
#### Control Records
+
- **`end_request_success.bin`** - FCGI_END_REQUEST record with successful completion
+
- **`end_request_error.bin`** - FCGI_END_REQUEST record with error status
+
- **`abort_request.bin`** - FCGI_ABORT_REQUEST record
+
+
### Complex Scenarios
+
- **`multiplexed_requests.bin`** - Multiple concurrent requests on same connection
+
- **`large_record.bin`** - Record with maximum content size (65KB)
+
- **`padded_record.bin`** - Record with padding for 8-byte alignment
+
+
## Record Format
+
+
All records follow the FastCGI record format:
+
+
```
+
typedef struct {
+
unsigned char version; // FCGI_VERSION_1 (1)
+
unsigned char type; // Record type (1-11)
+
unsigned char requestIdB1; // Request ID high byte
+
unsigned char requestIdB0; // Request ID low byte
+
unsigned char contentLengthB1; // Content length high byte
+
unsigned char contentLengthB0; // Content length low byte
+
unsigned char paddingLength; // Padding length
+
unsigned char reserved; // Reserved (always 0)
+
// contentData[contentLength]
+
// paddingData[paddingLength]
+
} FCGI_Record;
+
```
+
+
## Usage for Parser Testing
+
+
These test cases can be used to verify:
+
+
1. **Header parsing** - Correct extraction of version, type, requestId, lengths
+
2. **Content parsing** - Proper handling of record body data
+
3. **Stream handling** - Recognition of stream start/end patterns
+
4. **Name-value pair parsing** - Decoding of FCGI_PARAMS format
+
5. **Role-specific parsing** - Different record sequences for each role
+
6. **Error handling** - Response to unknown types, malformed data
+
7. **Multiplexing** - Handling multiple concurrent request IDs
+
8. **Padding** - Correct skipping of padding bytes
+
+
## Typical Protocol Flows
+
+
### Simple Responder Request
+
1. `begin_request_responder.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdin_empty.bin`
+
5. `stdout_response.bin`
+
6. `stdout_empty.bin`
+
7. `end_request_success.bin`
+
+
### POST Request with Form Data
+
1. `begin_request_responder.bin`
+
2. `params_post.bin`
+
3. `params_empty.bin`
+
4. `stdin_form_data.bin`
+
5. `stdin_empty.bin`
+
6. `stdout_response.bin`
+
7. `stdout_empty.bin`
+
8. `end_request_success.bin`
+
+
### Authorizer Request
+
1. `begin_request_authorizer.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdout_response.bin`
+
5. `stdout_empty.bin`
+
6. `end_request_success.bin`
+
+
### Filter Request
+
1. `begin_request_filter.bin`
+
2. `params_get.bin`
+
3. `params_empty.bin`
+
4. `stdin_empty.bin`
+
5. `data_filter.bin`
+
6. `data_empty.bin`
+
7. `stdout_response.bin`
+
8. `stdout_empty.bin`
+
9. `end_request_success.bin`
+
+
## Binary Format Verification
+
+
You can inspect the binary content using hexdump:
+
```bash
+
hexdump -C begin_request_responder.bin
+
```
+
+
The first 8 bytes should always be the FastCGI header, followed by the record content and any padding.
test_cases/abort_request.bin

This is a binary file and will not be displayed.

test_cases/begin_request_authorizer.bin

This is a binary file and will not be displayed.

test_cases/begin_request_filter.bin

This is a binary file and will not be displayed.

test_cases/begin_request_no_keep.bin

This is a binary file and will not be displayed.

test_cases/begin_request_responder.bin

This is a binary file and will not be displayed.

test_cases/data_empty.bin

This is a binary file and will not be displayed.

test_cases/data_filter.bin

This is a binary file and will not be displayed.

test_cases/end_request_error.bin

This is a binary file and will not be displayed.

test_cases/end_request_success.bin

This is a binary file and will not be displayed.

+349
test_cases/generate_test_cases.py
···
+
#!/usr/bin/env python3
+
"""
+
Generate FastCGI test case files based on the specification.
+
"""
+
import struct
+
import os
+
+
# FastCGI constants from the specification
+
FCGI_VERSION_1 = 1
+
+
# Record types
+
FCGI_BEGIN_REQUEST = 1
+
FCGI_ABORT_REQUEST = 2
+
FCGI_END_REQUEST = 3
+
FCGI_PARAMS = 4
+
FCGI_STDIN = 5
+
FCGI_STDOUT = 6
+
FCGI_STDERR = 7
+
FCGI_DATA = 8
+
FCGI_GET_VALUES = 9
+
FCGI_GET_VALUES_RESULT = 10
+
FCGI_UNKNOWN_TYPE = 11
+
+
# Roles
+
FCGI_RESPONDER = 1
+
FCGI_AUTHORIZER = 2
+
FCGI_FILTER = 3
+
+
# Flags
+
FCGI_KEEP_CONN = 1
+
+
# Protocol status
+
FCGI_REQUEST_COMPLETE = 0
+
FCGI_CANT_MPX_CONN = 1
+
FCGI_OVERLOADED = 2
+
FCGI_UNKNOWN_ROLE = 3
+
+
def create_record_header(version, record_type, request_id, content_length, padding_length=0):
+
"""Create a FastCGI record header."""
+
return struct.pack('>BBHHBB',
+
version,
+
record_type,
+
request_id,
+
content_length,
+
padding_length,
+
0) # reserved
+
+
def encode_name_value_pair(name, value):
+
"""Encode a name-value pair according to FastCGI spec."""
+
name_bytes = name.encode('utf-8')
+
value_bytes = value.encode('utf-8')
+
+
name_len = len(name_bytes)
+
value_len = len(value_bytes)
+
+
# Encode lengths
+
if name_len < 128:
+
name_len_encoded = struct.pack('B', name_len)
+
else:
+
name_len_encoded = struct.pack('>I', name_len | 0x80000000)
+
+
if value_len < 128:
+
value_len_encoded = struct.pack('B', value_len)
+
else:
+
value_len_encoded = struct.pack('>I', value_len | 0x80000000)
+
+
return name_len_encoded + value_len_encoded + name_bytes + value_bytes
+
+
def create_begin_request():
+
"""Create FCGI_BEGIN_REQUEST record."""
+
# FCGI_BeginRequestBody
+
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
+
return header + body
+
+
def create_begin_request_no_keep_conn():
+
"""Create FCGI_BEGIN_REQUEST record without keep connection."""
+
body = struct.pack('>HB5x', FCGI_RESPONDER, 0)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 1, len(body))
+
return header + body
+
+
def create_begin_request_authorizer():
+
"""Create FCGI_BEGIN_REQUEST record for authorizer role."""
+
body = struct.pack('>HB5x', FCGI_AUTHORIZER, 0)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
+
return header + body
+
+
def create_begin_request_filter():
+
"""Create FCGI_BEGIN_REQUEST record for filter role."""
+
body = struct.pack('>HB5x', FCGI_FILTER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 3, len(body))
+
return header + body
+
+
def create_end_request():
+
"""Create FCGI_END_REQUEST record."""
+
# FCGI_EndRequestBody
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE) # app_status=0, protocol_status=COMPLETE
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
return header + body
+
+
def create_end_request_error():
+
"""Create FCGI_END_REQUEST record with error status."""
+
body = struct.pack('>IB3x', 1, FCGI_REQUEST_COMPLETE) # app_status=1 (error)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
return header + body
+
+
def create_params_record():
+
"""Create FCGI_PARAMS record with CGI environment variables."""
+
# Typical CGI parameters
+
params = [
+
("REQUEST_METHOD", "GET"),
+
("SCRIPT_NAME", "/test.cgi"),
+
("REQUEST_URI", "/test.cgi?foo=bar"),
+
("QUERY_STRING", "foo=bar"),
+
("SERVER_NAME", "localhost"),
+
("SERVER_PORT", "80"),
+
("HTTP_HOST", "localhost"),
+
("HTTP_USER_AGENT", "Mozilla/5.0"),
+
("CONTENT_TYPE", ""),
+
("CONTENT_LENGTH", "0")
+
]
+
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
+
return header + body
+
+
def create_params_record_post():
+
"""Create FCGI_PARAMS record for POST request."""
+
params = [
+
("REQUEST_METHOD", "POST"),
+
("SCRIPT_NAME", "/form.cgi"),
+
("REQUEST_URI", "/form.cgi"),
+
("QUERY_STRING", ""),
+
("SERVER_NAME", "localhost"),
+
("SERVER_PORT", "443"),
+
("HTTPS", "on"),
+
("HTTP_HOST", "localhost"),
+
("HTTP_USER_AGENT", "curl/7.68.0"),
+
("CONTENT_TYPE", "application/x-www-form-urlencoded"),
+
("CONTENT_LENGTH", "23")
+
]
+
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, len(body))
+
return header + body
+
+
def create_empty_params():
+
"""Create empty FCGI_PARAMS record (end of params stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 1, 0)
+
return header
+
+
def create_stdin_record():
+
"""Create FCGI_STDIN record with form data."""
+
data = b"name=John&email=john@example.com"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, len(data))
+
return header + data
+
+
def create_empty_stdin():
+
"""Create empty FCGI_STDIN record (end of stdin stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDIN, 1, 0)
+
return header
+
+
def create_stdout_record():
+
"""Create FCGI_STDOUT record with HTTP response."""
+
response = b"Content-Type: text/html\r\n\r\n<html><body>Hello World</body></html>"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
+
return header + response
+
+
def create_empty_stdout():
+
"""Create empty FCGI_STDOUT record (end of stdout stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
+
return header
+
+
def create_stderr_record():
+
"""Create FCGI_STDERR record with error message."""
+
error_msg = b"Warning: Configuration file not found\n"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, len(error_msg))
+
return header + error_msg
+
+
def create_empty_stderr():
+
"""Create empty FCGI_STDERR record (end of stderr stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDERR, 1, 0)
+
return header
+
+
def create_get_values():
+
"""Create FCGI_GET_VALUES record."""
+
# Request standard capability variables
+
body = (encode_name_value_pair("FCGI_MAX_CONNS", "") +
+
encode_name_value_pair("FCGI_MAX_REQS", "") +
+
encode_name_value_pair("FCGI_MPXS_CONNS", ""))
+
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES, 0, len(body))
+
return header + body
+
+
def create_get_values_result():
+
"""Create FCGI_GET_VALUES_RESULT record."""
+
body = (encode_name_value_pair("FCGI_MAX_CONNS", "1") +
+
encode_name_value_pair("FCGI_MAX_REQS", "1") +
+
encode_name_value_pair("FCGI_MPXS_CONNS", "0"))
+
header = create_record_header(FCGI_VERSION_1, FCGI_GET_VALUES_RESULT, 0, len(body))
+
return header + body
+
+
def create_unknown_type():
+
"""Create FCGI_UNKNOWN_TYPE record."""
+
# FCGI_UnknownTypeBody
+
body = struct.pack('B7x', 99) # unknown type 99
+
header = create_record_header(FCGI_VERSION_1, FCGI_UNKNOWN_TYPE, 0, len(body))
+
return header + body
+
+
def create_abort_request():
+
"""Create FCGI_ABORT_REQUEST record."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_ABORT_REQUEST, 1, 0)
+
return header
+
+
def create_data_record():
+
"""Create FCGI_DATA record (for Filter role)."""
+
file_data = b"This is file content that needs to be filtered\nLine 2\nLine 3\n"
+
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, len(file_data))
+
return header + file_data
+
+
def create_empty_data():
+
"""Create empty FCGI_DATA record (end of data stream)."""
+
header = create_record_header(FCGI_VERSION_1, FCGI_DATA, 3, 0)
+
return header
+
+
def create_multiplexed_records():
+
"""Create a sequence showing multiplexed requests."""
+
records = []
+
+
# Request 1 begins
+
records.append(create_begin_request())
+
records.append(create_params_record())
+
records.append(create_empty_params())
+
+
# Request 2 begins (different request ID)
+
body = struct.pack('>HB5x', FCGI_RESPONDER, FCGI_KEEP_CONN)
+
header = create_record_header(FCGI_VERSION_1, FCGI_BEGIN_REQUEST, 2, len(body))
+
records.append(header + body)
+
+
# Request 1 continues
+
records.append(create_empty_stdin())
+
+
# Request 2 params
+
params = [("REQUEST_METHOD", "POST"), ("SCRIPT_NAME", "/other.cgi")]
+
body = b''
+
for name, value in params:
+
body += encode_name_value_pair(name, value)
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, len(body))
+
records.append(header + body)
+
+
# Request 2 empty params
+
header = create_record_header(FCGI_VERSION_1, FCGI_PARAMS, 2, 0)
+
records.append(header)
+
+
# Request 2 completes first
+
response = b"Content-Type: text/plain\r\n\r\nRequest 2 done"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, len(response))
+
records.append(header + response)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 2, 0)
+
records.append(header)
+
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 2, len(body))
+
records.append(header + body)
+
+
# Request 1 completes
+
response = b"Content-Type: text/html\r\n\r\n<html><body>Request 1 done</body></html>"
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(response))
+
records.append(header + response)
+
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, 0)
+
records.append(header)
+
+
body = struct.pack('>IB3x', 0, FCGI_REQUEST_COMPLETE)
+
header = create_record_header(FCGI_VERSION_1, FCGI_END_REQUEST, 1, len(body))
+
records.append(header + body)
+
+
return b''.join(records)
+
+
def create_large_record():
+
"""Create a record with maximum content size."""
+
# Create a large response (just under 64KB)
+
large_content = b"x" * 65000
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(large_content))
+
return header + large_content
+
+
def create_padded_record():
+
"""Create a record with padding for alignment."""
+
data = b"Hello" # 5 bytes
+
padding_length = 3 # to align to 8-byte boundary (5 + 3 = 8)
+
header = create_record_header(FCGI_VERSION_1, FCGI_STDOUT, 1, len(data), padding_length)
+
padding = b'\x00' * padding_length
+
return header + data + padding
+
+
# Test case definitions
+
test_cases = {
+
"begin_request_responder.bin": create_begin_request,
+
"begin_request_no_keep.bin": create_begin_request_no_keep_conn,
+
"begin_request_authorizer.bin": create_begin_request_authorizer,
+
"begin_request_filter.bin": create_begin_request_filter,
+
"end_request_success.bin": create_end_request,
+
"end_request_error.bin": create_end_request_error,
+
"params_get.bin": create_params_record,
+
"params_post.bin": create_params_record_post,
+
"params_empty.bin": create_empty_params,
+
"stdin_form_data.bin": create_stdin_record,
+
"stdin_empty.bin": create_empty_stdin,
+
"stdout_response.bin": create_stdout_record,
+
"stdout_empty.bin": create_empty_stdout,
+
"stderr_message.bin": create_stderr_record,
+
"stderr_empty.bin": create_empty_stderr,
+
"get_values.bin": create_get_values,
+
"get_values_result.bin": create_get_values_result,
+
"unknown_type.bin": create_unknown_type,
+
"abort_request.bin": create_abort_request,
+
"data_filter.bin": create_data_record,
+
"data_empty.bin": create_empty_data,
+
"multiplexed_requests.bin": create_multiplexed_records,
+
"large_record.bin": create_large_record,
+
"padded_record.bin": create_padded_record,
+
}
+
+
if __name__ == "__main__":
+
for filename, creator in test_cases.items():
+
with open(filename, 'wb') as f:
+
f.write(creator())
+
print(f"Created {filename}")
+
+
print(f"\nGenerated {len(test_cases)} test case files")
+
print("\nTest case descriptions:")
+
print("- begin_request_*.bin: Various BEGIN_REQUEST records for different roles")
+
print("- end_request_*.bin: END_REQUEST records with different status codes")
+
print("- params_*.bin: PARAMS records with CGI environment variables")
+
print("- stdin_*.bin: STDIN records with request body data")
+
print("- stdout_*.bin: STDOUT records with response data")
+
print("- stderr_*.bin: STDERR records with error messages")
+
print("- get_values*.bin: Management records for capability negotiation")
+
print("- unknown_type.bin: Unknown record type handling")
+
print("- abort_request.bin: Request abortion")
+
print("- data_*.bin: DATA records for Filter role")
+
print("- multiplexed_requests.bin: Multiple concurrent requests")
+
print("- large_record.bin: Maximum size record")
+
print("- padded_record.bin: Record with padding for alignment")
test_cases/get_values.bin

This is a binary file and will not be displayed.

test_cases/get_values_result.bin

This is a binary file and will not be displayed.

test_cases/large_record.bin

This is a binary file and will not be displayed.

test_cases/multiplexed_requests.bin

This is a binary file and will not be displayed.

test_cases/padded_record.bin

This is a binary file and will not be displayed.

test_cases/params_empty.bin

This is a binary file and will not be displayed.

test_cases/params_get.bin

This is a binary file and will not be displayed.

test_cases/params_post.bin

This is a binary file and will not be displayed.

test_cases/stderr_empty.bin

This is a binary file and will not be displayed.

test_cases/stderr_message.bin

This is a binary file and will not be displayed.

test_cases/stdin_empty.bin

This is a binary file and will not be displayed.

test_cases/stdin_form_data.bin

This is a binary file and will not be displayed.

test_cases/stdout_empty.bin

This is a binary file and will not be displayed.

test_cases/stdout_response.bin

This is a binary file and will not be displayed.

+24
test_cases/test_case_sizes.txt
···
+
abort_request.bin 8 bytes
+
begin_request_authorizer.bin 16 bytes
+
begin_request_filter.bin 16 bytes
+
begin_request_no_keep.bin 16 bytes
+
begin_request_responder.bin 16 bytes
+
data_empty.bin 8 bytes
+
data_filter.bin 69 bytes
+
end_request_error.bin 16 bytes
+
end_request_success.bin 16 bytes
+
get_values_result.bin 59 bytes
+
get_values.bin 56 bytes
+
large_record.bin 65008 bytes
+
multiplexed_requests.bin 496 bytes
+
padded_record.bin 16 bytes
+
params_empty.bin 8 bytes
+
params_get.bin 216 bytes
+
params_post.bin 246 bytes
+
stderr_empty.bin 8 bytes
+
stderr_message.bin 46 bytes
+
stdin_empty.bin 8 bytes
+
stdin_form_data.bin 40 bytes
+
stdout_empty.bin 8 bytes
+
stdout_response.bin 72 bytes
+
unknown_type.bin 16 bytes
test_cases/unknown_type.bin

This is a binary file and will not be displayed.

+130
test_cases/validate_test_cases.py
···
+
#!/usr/bin/env python3
+
"""
+
Validate that the generated FastCGI test cases are properly formatted.
+
"""
+
import struct
+
import os
+
import glob
+
+
def parse_record_header(data):
+
"""Parse a FastCGI record header."""
+
if len(data) < 8:
+
return None
+
+
version, record_type, request_id, content_length, padding_length, reserved = struct.unpack('>BBHHBB', data[:8])
+
return {
+
'version': version,
+
'type': record_type,
+
'request_id': request_id,
+
'content_length': content_length,
+
'padding_length': padding_length,
+
'reserved': reserved,
+
'total_length': 8 + content_length + padding_length
+
}
+
+
def validate_file(filename):
+
"""Validate a single test case file."""
+
print(f"\nValidating {filename}:")
+
+
with open(filename, 'rb') as f:
+
data = f.read()
+
+
if len(data) < 8:
+
print(f" โŒ File too short: {len(data)} bytes")
+
return False
+
+
# Parse all records in the file
+
offset = 0
+
record_count = 0
+
+
while offset < len(data):
+
if offset + 8 > len(data):
+
print(f" โŒ Incomplete header at offset {offset}")
+
return False
+
+
header = parse_record_header(data[offset:])
+
if not header:
+
print(f" โŒ Failed to parse header at offset {offset}")
+
return False
+
+
record_count += 1
+
print(f" Record {record_count}:")
+
print(f" Version: {header['version']}")
+
print(f" Type: {header['type']}")
+
print(f" Request ID: {header['request_id']}")
+
print(f" Content Length: {header['content_length']}")
+
print(f" Padding Length: {header['padding_length']}")
+
print(f" Reserved: {header['reserved']}")
+
+
# Validate header fields
+
if header['version'] != 1:
+
print(f" โŒ Invalid version: {header['version']}")
+
return False
+
+
if header['type'] < 1 or header['type'] > 11:
+
print(f" โŒ Invalid record type: {header['type']}")
+
return False
+
+
if header['reserved'] != 0:
+
print(f" โŒ Reserved field not zero: {header['reserved']}")
+
return False
+
+
# Check if we have enough data for content and padding
+
expected_end = offset + header['total_length']
+
if expected_end > len(data):
+
print(f" โŒ Not enough data: need {header['total_length']}, have {len(data) - offset}")
+
return False
+
+
# Extract content
+
content_start = offset + 8
+
content_end = content_start + header['content_length']
+
content = data[content_start:content_end]
+
+
# Extract padding
+
padding_start = content_end
+
padding_end = padding_start + header['padding_length']
+
padding = data[padding_start:padding_end]
+
+
print(f" Content: {len(content)} bytes")
+
if header['padding_length'] > 0:
+
print(f" Padding: {len(padding)} bytes")
+
+
# Show content preview for small records
+
if len(content) <= 32:
+
print(f" Content hex: {content.hex()}")
+
else:
+
print(f" Content preview: {content[:16].hex()}...")
+
+
print(f" โœ… Record valid")
+
+
offset = expected_end
+
+
print(f" โœ… File valid: {record_count} record(s), {len(data)} total bytes")
+
return True
+
+
def main():
+
"""Validate all test case files."""
+
test_files = glob.glob("*.bin")
+
test_files.sort()
+
+
print(f"Found {len(test_files)} test case files")
+
+
valid_count = 0
+
for filename in test_files:
+
if validate_file(filename):
+
valid_count += 1
+
+
print(f"\n{'='*50}")
+
print(f"Validation complete: {valid_count}/{len(test_files)} files valid")
+
+
if valid_count == len(test_files):
+
print("โœ… All test cases are valid!")
+
else:
+
print("โŒ Some test cases failed validation")
+
return 1
+
+
return 0
+
+
if __name__ == "__main__":
+
import sys
+
sys.exit(main())
+438
test_cases/validation_results.txt
···
+
Found 24 test case files
+
+
Validating abort_request.bin:
+
Record 1:
+
Version: 1
+
Type: 2
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating begin_request_authorizer.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0002000000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_filter.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 3
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0003010000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_no_keep.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001000000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating begin_request_responder.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating data_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 8
+
Request ID: 3
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating data_filter.bin:
+
Record 1:
+
Version: 1
+
Type: 8
+
Request ID: 3
+
Content Length: 61
+
Padding Length: 0
+
Reserved: 0
+
Content: 61 bytes
+
Content preview: 546869732069732066696c6520636f6e...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 69 total bytes
+
+
Validating end_request_error.bin:
+
Record 1:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000100000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating end_request_success.bin:
+
Record 1:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating get_values.bin:
+
Record 1:
+
Version: 1
+
Type: 9
+
Request ID: 0
+
Content Length: 48
+
Padding Length: 0
+
Reserved: 0
+
Content: 48 bytes
+
Content preview: 0e00464347495f4d41585f434f4e4e53...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 56 total bytes
+
+
Validating get_values_result.bin:
+
Record 1:
+
Version: 1
+
Type: 10
+
Request ID: 0
+
Content Length: 51
+
Padding Length: 0
+
Reserved: 0
+
Content: 51 bytes
+
Content preview: 0e01464347495f4d41585f434f4e4e53...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 59 total bytes
+
+
Validating large_record.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 65000
+
Padding Length: 0
+
Reserved: 0
+
Content: 65000 bytes
+
Content preview: 78787878787878787878787878787878...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 65008 total bytes
+
+
Validating multiplexed_requests.bin:
+
Record 1:
+
Version: 1
+
Type: 1
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
โœ… Record valid
+
Record 2:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 208
+
Padding Length: 0
+
Reserved: 0
+
Content: 208 bytes
+
Content preview: 0e03524551554553545f4d4554484f44...
+
โœ… Record valid
+
Record 3:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
Record 4:
+
Version: 1
+
Type: 1
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0001010000000000
+
โœ… Record valid
+
Record 5:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
Record 6:
+
Version: 1
+
Type: 4
+
Request ID: 2
+
Content Length: 43
+
Padding Length: 0
+
Reserved: 0
+
Content: 43 bytes
+
Content preview: 0e04524551554553545f4d4554484f44...
+
โœ… Record valid
+
Record 7:
+
Version: 1
+
Type: 4
+
Request ID: 2
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
Record 8:
+
Version: 1
+
Type: 6
+
Request ID: 2
+
Content Length: 42
+
Padding Length: 0
+
Reserved: 0
+
Content: 42 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
โœ… Record valid
+
Record 9:
+
Version: 1
+
Type: 6
+
Request ID: 2
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
Record 10:
+
Version: 1
+
Type: 3
+
Request ID: 2
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
โœ… Record valid
+
Record 11:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 67
+
Padding Length: 0
+
Reserved: 0
+
Content: 67 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
โœ… Record valid
+
Record 12:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
Record 13:
+
Version: 1
+
Type: 3
+
Request ID: 1
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 0000000000000000
+
โœ… Record valid
+
โœ… File valid: 13 record(s), 496 total bytes
+
+
Validating padded_record.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 5
+
Padding Length: 3
+
Reserved: 0
+
Content: 5 bytes
+
Padding: 3 bytes
+
Content hex: 48656c6c6f
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
Validating params_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating params_get.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 208
+
Padding Length: 0
+
Reserved: 0
+
Content: 208 bytes
+
Content preview: 0e03524551554553545f4d4554484f44...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 216 total bytes
+
+
Validating params_post.bin:
+
Record 1:
+
Version: 1
+
Type: 4
+
Request ID: 1
+
Content Length: 238
+
Padding Length: 0
+
Reserved: 0
+
Content: 238 bytes
+
Content preview: 0e04524551554553545f4d4554484f44...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 246 total bytes
+
+
Validating stderr_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 7
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating stderr_message.bin:
+
Record 1:
+
Version: 1
+
Type: 7
+
Request ID: 1
+
Content Length: 38
+
Padding Length: 0
+
Reserved: 0
+
Content: 38 bytes
+
Content preview: 5761726e696e673a20436f6e66696775...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 46 total bytes
+
+
Validating stdin_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating stdin_form_data.bin:
+
Record 1:
+
Version: 1
+
Type: 5
+
Request ID: 1
+
Content Length: 32
+
Padding Length: 0
+
Reserved: 0
+
Content: 32 bytes
+
Content hex: 6e616d653d4a6f686e26656d61696c3d6a6f686e406578616d706c652e636f6d
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 40 total bytes
+
+
Validating stdout_empty.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 0
+
Padding Length: 0
+
Reserved: 0
+
Content: 0 bytes
+
Content hex:
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 8 total bytes
+
+
Validating stdout_response.bin:
+
Record 1:
+
Version: 1
+
Type: 6
+
Request ID: 1
+
Content Length: 64
+
Padding Length: 0
+
Reserved: 0
+
Content: 64 bytes
+
Content preview: 436f6e74656e742d547970653a207465...
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 72 total bytes
+
+
Validating unknown_type.bin:
+
Record 1:
+
Version: 1
+
Type: 11
+
Request ID: 0
+
Content Length: 8
+
Padding Length: 0
+
Reserved: 0
+
Content: 8 bytes
+
Content hex: 6300000000000000
+
โœ… Record valid
+
โœ… File valid: 1 record(s), 16 total bytes
+
+
==================================================
+
Validation complete: 24/24 files valid
+
โœ… All test cases are valid!