Cloud Parts

nom

A Rust parser combinator library, built for binary formats first, whose parsers return slices of their input instead of copying it and can ask for more data when input arrives in pieces.

What it is
Binary parsing and encoding (parser combinators)
Type
Open source
Made by
Open-source project created by Geoffroy Couprie, now in the rust-bakery organisation
License
MIT
Facts checked
9 October 2026

What it is

In nom a parser is a function that takes input and returns IResult: either the remaining input plus the value it parsed, or an error. Small parsers come built in, such as tag("SN") to match literal bytes, take(4), be_u16 and le_u32 for integers in either byte order, and a bits module that treats bytes as a bit stream. Combinators build bigger parsers from them: pair and tuples run parsers in sequence, alt tries alternatives, many0 repeats, length_count reads a count and then that many items, and map_res converts the result. Since nom 8 (January 2025), combinators are applied through the Parser trait, written combinator(arg).parse(input).

Input can be &[u8], &str or bits, and when a parser returns part of its input it returns a slice of it rather than a copy. Each basic parser comes in a complete version, for input that is all in memory, and a streaming version that returns Incomplete with how many more bytes it needs, so a network service can wait for the next packet instead of failing. nom works without the standard library, with or without an allocator. Its README lists parsers built on it for TLS, X.509, DER, Kerberos, SNMP, pcap, Matroska, tar, gzip, SQL and more.

Why teams pick it

  • Zero-copy: parsers hand back slices of the input, so a header parser can return the payload without copying it
  • Streaming built in: Incomplete tells the caller how much more data is needed, and the same parser works on whole or chunked input
  • One set of combinators for bytes, bits and text, so it fits binary protocols and text formats such as configuration files alike
  • Parsers are small functions that can be unit-tested alone; the README says fuzzing has so far found flaws only in code written outside nom

Watch out for

  • Reads only: there is no matching serializer, so writing the format back out is separate code
  • Three major rewrites of the API: functions replaced macros in 5.0 (2019), macros were removed in 7.0 (2021) and 8.0 (2025) moved to the Parser trait, so older tutorials don’t compile
  • You choose streaming or complete for each basic parser; a streaming parser at the end of a whole file reports Incomplete rather than an error
  • Generic signatures can need hand-written type annotations, for example the error type when switching into bit-level parsing

How it fits with other technologies

A sensor packet with 4-bit fields and a counted array, parsed from complete and partial input · rust

use nom::{
    bits::{bits, streaming::take},
    bytes::streaming::tag,
    error::Error,
    multi::length_count,
    number::streaming::{be_i16, be_u16, u8},
    IResult, Parser,
};

#[derive(Debug)]
struct Reading {
    version: u8,
    flags: u8,
    sensor_id: u16,
    temps: Vec<i16>, // hundredths of a degree
}

// one byte split into two 4-bit fields
fn nibbles(input: &[u8]) -> IResult<&[u8], (u8, u8)> {
    bits::<_, _, Error<(&[u8], usize)>, _, _>((take(4usize), take(4usize))).parse(input)
}

// "SN" | version: 4 bits | flags: 4 bits | sensor id: u16 | count: u8 | count × i16
fn reading(input: &[u8]) -> IResult<&[u8], Reading> {
    let (input, _) = tag(&b"SN"[..]).parse(input)?;
    let (input, (version, flags)) = nibbles(input)?;
    let (input, sensor_id) = be_u16(input)?;
    let (input, temps) = length_count(u8, be_i16).parse(input)?;
    Ok((input, Reading { version, flags, sensor_id, temps }))
}

fn main() {
    let bytes = [b'S', b'N', 0x12, 0x00, 0x07, 0x02, 0x08, 0x34, 0xFF, 0x9C, 0xAA];
    println!("{:?}", reading(&bytes));
    // Ok(([170], Reading { version: 1, flags: 2, sensor_id: 7, temps: [2100, -100] }))

    // the second reading hasn't arrived yet: nom asks for more instead of failing
    println!("{:?}", reading(&bytes[..8]));
    // Err(Incomplete(Size(2)))

    println!("{:?}", reading(b"XX\x12"));
    // Err(Error(Error { input: [88, 88, 18], code: Tag }))
}

See it in a real stack

No architecture uses nom yet. Drop it into the playground and wire it up yourself.Open playground →
Where it came from2015–2025

Geoffroy Couprie wrote nom to parse binary formats in Rust as fast as the usual hand-written C parsers but without their buffer overflows. Couprie published it in early 2015, months before Rust itself reached 1.0, and nom’s own stable 1.0 followed that November. Its first parsers were written with Rust macros such as named! and do_parse!, which generated the code that consumed input safely.

In 2017 Couprie and Pierre Chifflier of France’s national cybersecurity agency, ANSSI, presented “Writing parsers like it is 2017” at the IEEE Security & Privacy workshops. They argued for fixing whole classes of parsing bugs by writing parsers in a memory-safe language with combinators, and showed nom parsers running inside C programs: an FLV demuxer for VLC and a TLS parser for the Suricata intrusion detection system.

Later versions changed how parsers are written while keeping the model. Version 5 (2019) rebuilt nom on plain functions, version 7 (2021) removed the original macros, and version 8 (2025) moved combinators onto a trait to shrink the generated code and the API, moving tools for programming-language parsing into a separate nom-language crate.

Why it’s built this way

Return slices, never copies
Rust’s compiler tracks which code owns which memory, and a slice carries a pointer and a length, so a parser can return parts of its input safely without copying them.
Don’t own the input
Because nom parsers only borrow the data, a host program in C can keep its own buffers and call into Rust, as the VLC and Suricata integrations did.
Say “need more data” instead of guessing
Network formats and huge files arrive in parts; nom reports Incomplete rather than returning a wrong result, so the outcome is the same whether data comes whole or in chunks.
From macros to functions to traits
Macros came first; functions in 5.0 made parsers ordinary Rust, and traits in 8.0 cut the amount of code each parser generates.

Timeline

  1. 2015

    First releases in early 2015; 1.0 in November

  2. 2017

    “Writing parsers like it is 2017” puts nom parsers into VLC and Suricata

  3. 2019

    5.0 rebuilds nom on functions instead of macros

  4. 2021

    7.0 removes the original macro combinators

  5. 2025

    8.0 moves to the Parser trait and splits out nom-language

Sources4 · facts checked 9 October 2026

Improve this page on GitHub