-
Notifications
You must be signed in to change notification settings - Fork 1
NTP Hex Input Preprocessing
This page covers a bonus parser in hex.c that demonstrates additional Hammer techniques: parsing character-level input with h_ch_range, exact repetition with h_repeat_n, and semantic actions with H_ARULE. While the current main.c reads raw binary input directly, this hex parser shows Hammer's versatility beyond binary protocols.
Source file:
hex.cHammer concepts:
h_ch_range·h_repeat_n·H_ARULE· parse tree manipulation
During development, it's common to work with hex strings like:
e30003fa000100000001000000000000...
But Hammer parses raw bytes, not ASCII text. The hex parser bridges this gap: it reads a hex-encoded string and produces a sequence of byte values.
Input: "e3" "00" "03" "fa" ... (ASCII characters)
Output: 0xE3 0x00 0x03 0xFA ... (byte values)
| Combinator | Purpose |
|---|---|
h_ch_range(lo, hi) |
Match a single byte in the range [lo, hi]
|
h_repeat_n(p, n) |
Match p exactly n times |
H_ARULE(name, def) |
Like H_RULE, but automatically attaches a semantic action function |
Matches a single input byte whose value is between lower and upper (inclusive). This works on byte values, so h_ch_range('0', '9') matches ASCII digits and h_ch_range('a', 'f') matches lowercase hex letters.
A single hex digit is either 0-9, a-f, or A-F. We combine those ranges with h_choice:
h_choice(h_ch_range('0', '9'), h_ch_range('a', 'f'), h_ch_range('A', 'F'), NULL)This matches exactly one hex character.
Matches parser exactly count times. The result is a sequence of count parsed values.
Each byte is encoded as two hex characters. We use h_repeat_n with a count of 2:
H_RULE(hex_octet, h_repeat_n(h_choice(h_ch_range('0', '9'), h_ch_range('a', 'f'), h_ch_range('A', 'F'), NULL), 2));hex_octet matches exactly two hex characters, e.g., "e3", "ff", "00".
A complete hex string is zero or more hex octets followed by end of input:
H_RULE(hex_string, h_left(h_many(hex_octet), h_end_p()));This uses:
-
h_many(hex_octet)to match octets as many times as possible (see Extension Fields) -
h_left(..., h_end_p())to ensure we've consumed everything, keeping only the left result (see Assembling the Parser)
At this point, hex_string produces a sequence of sequences of ASCII character values. For input "e3ff":
[ ['e', '3'], ['f', 'f'] ]
But we want actual byte values: [0xE3, 0xFF]. We need a semantic action, a C function that transforms the parse tree.
H_ARULE is like H_RULE, but it automatically looks for a function named act_<name> and attaches it as a semantic action. The act_ prefix naming convention is mandatory since glue.h macros rely on it.
H_ARULE(hex_to_dec, hex_string);This tells Hammer: "When hex_string succeeds, run the function act_hex_to_dec on its result."
The act_hex_to_dec function converts hex character pairs into byte values:
HParsedToken *act_hex_to_dec(const HParseResult *p, void *user_data) {
const HParsedToken *parseToken = p->ast;
size_t numElements = parseToken->seq->used;
HParsedToken *newSeq = H_MAKE_SEQ();
for (int i = 0; i < numElements; i++) {
const HParsedToken *elem = h_seq_index(parseToken, i);
// First hex digit (high nibble)
const HParsedToken *innerElem = h_seq_index(elem, 0);
uint8_t hi = (innerElem->uint > '9') ? innerElem->uint - 'a' + 10 : innerElem->uint - '0';
// Second hex digit (low nibble)
innerElem = h_seq_index(elem, 1);
uint8_t lo = (innerElem->uint > '9') ? innerElem->uint - 'a' + 10 : innerElem->uint - '0';
// Combine into one byte
uint8_t value = (hi << 4) | lo;
HParsedToken *t = H_MAKE_UINT(value);
h_seq_snoc(newSeq, t);
}
return newSeq;
}This function uses several Hammer APIs for working with parse results:
| API | Purpose |
|---|---|
p->ast |
Access the root HParsedToken of a parse result |
parseToken->seq->used |
Get the number of elements in a sequence |
h_seq_index(seq, i) |
Get the i-th element of a sequence (0-indexed) |
innerElem->uint |
Access the unsigned integer value of a token |
H_MAKE_SEQ() |
Create a new empty sequence token |
H_MAKE_UINT(val) |
Create a new unsigned integer token |
h_seq_snoc(seq, elem) |
Append an element to a sequence |
For each hex octet (pair of characters):
-
High nibble: Read the first character's ASCII value. If it's >
'9', it's a letter (a-f), so subtract'a'and add 10. Otherwise subtract'0'. - Low nibble: Same logic for the second character.
- Combine: Shift the high nibble left by 4 bits and OR with the low nibble.
'e' -> 14, '3' -> 3 -> (14 << 4) | 3 = 0xE3 = 227
HParser *topDownParse() {
H_RULE(hex_octet, h_repeat_n(h_choice(h_ch_range('0', '9'), h_ch_range('a', 'f'), h_ch_range('A', 'F'), NULL), 2));
H_RULE(hex_string, h_left(h_many(hex_octet), h_end_p()));
H_ARULE(hex_to_dec, hex_string);
return hex_to_dec;
}Three rules. The semantic action does the heavy lifting of transforming the parsed structure.
| Concept | Hammer Function | When to Use |
|---|---|---|
| Match a character range | h_ch_range(lo, hi) |
Text/ASCII protocols, hex digits |
| Exact repetition | h_repeat_n(p, n) |
Fixed-width fields, character groups |
| Semantic actions (auto-named) | H_ARULE(name, def) |
Transforming parse results with an act_<name> function |
| Build new tokens |
H_MAKE_SEQ(), H_MAKE_UINT()
|
Inside action functions |
| Walk parse trees |
h_seq_index(), ->uint, ->seq->used
|
Inside action functions |
Next: Running and Testing - Build, run, and test with real and synthetic packets.
Previous: Assembling the Parser
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