-
Notifications
You must be signed in to change notification settings - Fork 1
NTP Assembling the Parser
We've built parsers for every component of an NTP packet: the header, the data fields, the extension fields, and the MAC. Now we wire them together into a complete parser and connect it to main().
An NTP packet always starts with the essential fields. After that, it may or may not have extension fields and/or a MAC. Two possible shapes:
| Type | Structure |
|---|---|
| Type 1 |
essential_fields + ext_fields (zero or more) |
| Type 2 |
essential_fields + ext_fields (zero or more) + mac
|
Because
ext_fieldsusesh_many(which matches zero or more), Type 1 covers packets with no extensions as well as packets with extensions but no MAC. Type 2 covers every case that includes a MAC.
Each type is a sequence that ends with h_end_p():
A parser that succeeds only if there is no more input to consume. It doesn't read any bytes; it's a zero-width assertion. By placing it at the end of a sequence, we ensure the parser accounts for every byte in the input.
Without h_end_p(), a parser could match a prefix of the input and silently ignore trailing garbage.
// Type 1: essential fields + optional extension fields + end of input
H_RULE(type1, h_sequence(essential_fields, ext_fields, h_end_p(), NULL));
// Type 2: essential fields + optional extension fields + MAC + end of input
H_RULE(type2, h_sequence(essential_fields, ext_fields, mac, h_end_p(), NULL));We need the parser to try Type 1 and Type 2 and succeed on whichever matches.
Tries each parser in order from left to right. Returns the result of the first parser that succeeds. If none succeed, the entire choice fails.
Order matters. h_choice uses backtracking. If p1 partially matches but ultimately fails, the input position resets and p2 gets a fresh start.
Why is
type1listed beforetype2? For a packet with extensions but no MAC,type1matches becauseext_fieldsconsumes the extensions andh_end_p()confirms there's nothing left. For a packet with a MAC,type1fails becauseh_end_p()sees the remaining MAC bytes and rejects. Backtracking kicks in,type2tries, and it succeeds because it parses the MAC before asserting end-of-input.
Parses left, then right, but only returns left's result. right's result is discarded.
Useful when you want to assert something (like end-of-input) without including it in the result tree:
HParser *ntp = h_left(h_choice(type1, type2, NULL), h_end_p());There's already an h_end_p() inside each type. The outer one is a defensive pattern: if someone later adds a new type and forgets the inner h_end_p(), the outer one catches it.
The entire parser definition from ntp.c:
HParsedToken *opt_ext_len(const HParseResult *p, void *user_data) {
unsigned int temp = p->ast->uint;
return H_MAKE_UINT(temp - 4);
}
HParser *ntpParser() {
// Header
H_RULE(leap, h_int_range(h_bits(2, false), 0, 3));
H_RULE(version, h_int_range(h_bits(3, false), 1, 4));
H_RULE(mode, h_int_range(h_bits(3, false), 0, 7));
H_RULE(stratum, h_int_range(h_uint8(), 0, 16));
H_RULE(poll, h_int8());
H_RULE(precision, h_int8());
H_RULE(header, h_sequence(leap, version, mode, stratum, poll, precision, NULL));
// Essential fields
H_RULE(root_delay, h_sequence(h_int16(), h_int16(), NULL));
H_RULE(root_disp, h_sequence(h_int16(), h_int16(), NULL));
H_RULE(ref_id, h_uint32());
H_RULE(ref_ts, h_sequence(h_uint32(), h_uint32(), NULL));
H_RULE(org_ts, h_sequence(h_uint32(), h_uint32(), NULL));
H_RULE(rec_ts, h_sequence(h_uint32(), h_uint32(), NULL));
H_RULE(xmt_ts, h_sequence(h_uint32(), h_uint32(), NULL));
H_RULE(essential_fields,
h_sequence(header, root_delay, root_disp, ref_id,
ref_ts, org_ts, rec_ts, xmt_ts, NULL));
// Optional extension fields
H_RULE(field_type, h_int16());
H_RULE(ext_field,
h_sequence(
field_type, h_put_value(h_uint16(), "opt_len_val"),
h_length_value(
h_action(h_free_value("opt_len_val"), opt_ext_len, NULL),
h_uint8()),
NULL));
H_RULE(ext_fields, h_many(ext_field));
// Optional MAC
H_RULE(key_id, h_uint32());
H_RULE(dgst, h_sequence(h_int64(), h_int64(), NULL));
H_RULE(mac, h_sequence(key_id, dgst, NULL));
// Packet types
H_RULE(type1, h_sequence(essential_fields, ext_fields, h_end_p(), NULL));
H_RULE(type2, h_sequence(essential_fields, ext_fields, mac, h_end_p(), NULL));
// Choose the matching type
HParser *ntp = h_left(h_choice(type1, type2, NULL), h_end_p());
return ntp;
}main.c is intentionally simple. It reads raw bytes, runs the parser, and prints the result:
#include "hex.c"
#include "ntp.c"
int main(int argc, char *argv[]) {
uint8_t input[1024] = {0};
FILE *f_input = stdin;
if (argc > 1) {
f_input = fopen(argv[1], "rb");
if (!f_input) {
puts("ERR: cannot open file");
return 1;
}
} else {
puts("Reading from stdin");
}
size_t input_size = fread(input, 1, sizeof(input), f_input);
HParseResult *result = h_parse(ntpParser(), input, input_size);
if (result) {
printf("Packet accepted\n");
return 0;
} else {
printf("Packet rejected\n");
return -1;
}
}Key points:
-
ntpParser()returns anHParser *, the top-level parser we just built. -
h_parse()takes the parser, a byte array, and its length. ReturnsNULLon failure. (See Hammer Fundamentals.) - The input is raw bytes, not hex strings. See Running and Testing for how to provide input in different formats.
Top-to-bottom view of how the parser is organized:
h_left
├── h_choice
│ ├── type1: h_sequence
│ │ ├── essential_fields: h_sequence
│ │ │ ├── header: h_sequence
│ │ │ │ ├── leap (h_int_range + h_bits)
│ │ │ │ ├── version (h_int_range + h_bits)
│ │ │ │ ├── mode (h_int_range + h_bits)
│ │ │ │ ├── stratum (h_int_range + h_uint8)
│ │ │ │ ├── poll (h_int8)
│ │ │ │ └── precision (h_int8)
│ │ │ ├── root_delay (h_int16 + h_int16)
│ │ │ ├── root_disp (h_int16 + h_int16)
│ │ │ ├── ref_id (h_uint32)
│ │ │ ├── ref_ts (h_uint32 + h_uint32)
│ │ │ ├── org_ts (h_uint32 + h_uint32)
│ │ │ ├── rec_ts (h_uint32 + h_uint32)
│ │ │ └── xmt_ts (h_uint32 + h_uint32)
│ │ ├── ext_fields: h_many
│ │ │ └── ext_field: h_sequence
│ │ │ ├── field_type (h_int16)
│ │ │ ├── length (h_put_value + h_uint16)
│ │ │ └── value (h_length_value + h_action)
│ │ └── h_end_p
│ │
│ └── type2: h_sequence
│ ├── essential_fields (same as above)
│ ├── ext_fields (same as above)
│ ├── mac: h_sequence
│ │ ├── key_id (h_uint32)
│ │ └── dgst (h_int64 + h_int64)
│ └── h_end_p
│
└── h_end_p (safety net)
| Combinator | Purpose in This Parser |
|---|---|
h_end_p() |
Ensures every byte is accounted for |
h_choice(a, b, ..., NULL) |
Try packet types in order until one matches |
h_left(a, b) |
Parse a and b, return only a's result |
| Design Pattern | Why |
|---|---|
h_end_p() inside each type |
Each type must consume all remaining input |
h_end_p() outside h_choice
|
Safety net for future additions |
Order of types in h_choice
|
More restrictive types can go first; backtracking handles failures |
Next: Hex Input Preprocessing - A bonus parser that converts hex strings to bytes.
Previous: Extension Fields and MAC
Learn Hammer
Protocol Examples
NTP
- NTP Overview
- Parsing the Header
- Parsing Data Fields
- Extension Fields and MAC
- Assembling the Parser
- Hex Input Preprocessing
- Running and Testing
DNS
TFTP
- TFTP Overview
- RRQ/WRQ Packets
- DATA Packets
- ACK Packets
- ERROR Packets
- Assembling the Parser
- Running and Testing
References
- Hammer Quick Reference
- Parsing Backends
- Unit Testing
- Using RTEMS
- Extending Hammer
- Adding a New Example
- Adding a New Binding
Further Reading