Skip to content

NTP Hex Input Preprocessing

Elbasiouny, Mahmoud edited this page May 29, 2026 · 5 revisions

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.c

Hammer concepts: h_ch_range · h_repeat_n · H_ARULE · parse tree manipulation


The Problem

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)

New Combinators

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

Step 1: Matching Hex Characters with h_ch_range

h_ch_range(lower, upper)

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.


Step 2: Grouping Hex Pairs with h_repeat_n

h_repeat_n(parser, count)

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".


Step 3: Many Octets Then End

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)

Step 4: Semantic Actions with H_ARULE

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(name, definition)

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."


Step 5: The Action Function

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;
}

Walking the Parse Tree

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

The Conversion Logic

For each hex octet (pair of characters):

  1. 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'.
  2. Low nibble: Same logic for the second character.
  3. Combine: Shift the high nibble left by 4 bits and OR with the low nibble.
'e' -> 14,  '3' -> 3  ->  (14 << 4) | 3  =  0xE3 = 227

The Complete topDownParse() Function

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.


Summary

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

Clone this wiki locally