Parsing

76. slice::as_chunks — Split Slices into Fixed-Size Arrays

You’re calling .chunks(4) and immediately doing chunk.try_into().unwrap() to get an array. as_chunks gives you &[[T; N]] directly — a slice of properly typed arrays, plus the remainder.

The Problem

When you use .chunks(N), each chunk is a &[T] — a dynamically sized slice. The compiler doesn’t know its length, so you’re stuck converting manually:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn sum_pairs(data: &[i32]) -> Vec<i32> {
    data.chunks(2)
        .filter(|c| c.len() == 2) // skip incomplete last chunk
        .map(|c| c[0] + c[1])     // runtime indexing, no guarantees
        .collect()
}

fn main() {
    let values = [1, 2, 3, 4, 5];
    assert_eq!(sum_pairs(&values), vec![3, 7]);
}

That works, but the compiler can’t verify your index access at compile time. You’re also throwing away the last chunk if it’s incomplete, with no easy way to inspect it.

After: as_chunks

Stabilized in Rust 1.88, as_chunks splits a slice into a &[[T; N]] and a remainder &[T] in one call:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn sum_pairs(data: &[i32]) -> Vec<i32> {
    let (chunks, _remainder) = data.as_chunks::<2>();
    chunks.iter().map(|[a, b]| a + b).collect()
}

fn main() {
    let values = [1, 2, 3, 4, 5];
    assert_eq!(sum_pairs(&values), vec![3, 7]);

    // The remainder is available too
    let (chunks, remainder) = values.as_chunks::<2>();
    assert_eq!(chunks, &[[1, 2], [3, 4]]);
    assert_eq!(remainder, &[5]);
}

Each chunk is &[i32; 2], so you can pattern-match [a, b] directly. The compiler knows the size — no bounds checks, no panics.

Processing Fixed-Width Records

Parsing binary data with fixed-width fields is where as_chunks shines. Imagine RGB pixel data packed as bytes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn brighten(pixels: &mut [u8], amount: u8) {
    let (chunks, _) = pixels.as_chunks_mut::<3>();
    for [r, g, b] in chunks {
        *r = r.saturating_add(amount);
        *g = g.saturating_add(amount);
        *b = b.saturating_add(amount);
    }
}

fn main() {
    let mut pixels = [100, 150, 200, 50, 60, 70, 255, 128, 0];
    brighten(&mut pixels, 30);
    assert_eq!(pixels, [130, 180, 230, 80, 90, 100, 255, 158, 30]);
}

No manual stride arithmetic. Each iteration gives you exactly 3 bytes, pattern-matched into r, g, b.

Don’t Lose the Remainder

Unlike chunks_exact() where you call .remainder() on the iterator after consuming it, as_chunks returns the remainder upfront:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let data = [10, 20, 30, 40, 50, 60, 70];

    let (fours, rest) = data.as_chunks::<4>();
    assert_eq!(fours.len(), 1);       // one complete chunk: [10, 20, 30, 40]
    assert_eq!(fours[0], [10, 20, 30, 40]);
    assert_eq!(rest, &[50, 60, 70]);  // leftover elements

    // as_rchunks starts from the right instead
    let (rest, fours) = data.as_rchunks::<4>();
    assert_eq!(rest, &[10, 20, 30]);
    assert_eq!(fours[0], [40, 50, 60, 70]);
}

as_rchunks is the mirror — it aligns chunks to the end, putting the remainder at the front. Useful when your trailing data is the structured part (e.g., a checksum or footer).

The Full Family

All stabilized in Rust 1.88, these come in immutable and mutable variants:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let data: &[u8] = &[1, 2, 3, 4, 5, 6, 7];

    // as_chunks — align from left, remainder on right
    let (chunks, rem) = data.as_chunks::<3>();
    assert_eq!(chunks, &[[1, 2, 3], [4, 5, 6]]);
    assert_eq!(rem, &[7]);

    // as_rchunks — align from right, remainder on left
    let (rem, chunks) = data.as_rchunks::<3>();
    assert_eq!(rem, &[1]);
    assert_eq!(chunks, &[[2, 3, 4], [5, 6, 7]]);

    // Mutable versions: as_chunks_mut, as_rchunks_mut
    let mut buf = [0u8; 6];
    let (chunks, _) = buf.as_chunks_mut::<2>();
    chunks[0] = [0xCA, 0xFE];
    chunks[1] = [0xBA, 0xBE];
    chunks[2] = [0xDE, 0xAD];
    assert_eq!(buf, [0xCA, 0xFE, 0xBA, 0xBE, 0xDE, 0xAD]);
}

Whenever you’re reaching for .chunks(N) with a compile-time constant, as_chunks::<N>() gives you stronger types, better ergonomics, and the remainder without gymnastics.

#059 Apr 2026

59. split_first_chunk — Destructure Slices into Arrays

Parsing a header from a byte buffer? Extracting the first N elements of a slice? split_first_chunk hands you a fixed-size array and the remainder in one call — no manual indexing, no panics.

The Problem

You have a byte slice and need to pull out a fixed-size prefix — say a 4-byte magic number or a 2-byte length field. The manual approach is fragile:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
fn parse_header(data: &[u8]) -> Option<([u8; 4], &[u8])> {
    if data.len() < 4 {
        return None;
    }
    let header: [u8; 4] = data[..4].try_into().unwrap();
    let rest = &data[4..];
    Some((header, rest))
}

fn main() {
    let packet = b"RUST is awesome";
    let (header, rest) = parse_header(packet).unwrap();
    assert_eq!(&header, b"RUST");
    assert_eq!(rest, b" is awesome");
}

That try_into().unwrap() is ugly, and if you get the index arithmetic wrong, you get a panic at runtime.

After: split_first_chunk

Stabilized in Rust 1.77, split_first_chunk splits a slice into a &[T; N] array reference and the remaining slice — returning None if the slice is too short:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_header(data: &[u8]) -> Option<(&[u8; 4], &[u8])> {
    data.split_first_chunk::<4>()
}

fn main() {
    let packet = b"RUST is awesome";
    let (magic, rest) = parse_header(packet).unwrap();
    assert_eq!(magic, b"RUST");
    assert_eq!(rest, b" is awesome");

    // Too short — returns None instead of panicking
    let tiny = b"RS";
    assert!(tiny.split_first_chunk::<4>().is_none());
}

One method call. No manual slicing, no try_into, and the const generic N ensures the compiler knows the exact array size.

Chaining Chunks for Protocol Parsing

Real protocols have multiple fields. Chain split_first_chunk calls to peel them off one at a time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
fn parse_packet(data: &[u8]) -> Option<([u8; 2], [u8; 4], &[u8])> {
    let (version, rest) = data.split_first_chunk::<2>()?;
    let (length, payload) = rest.split_first_chunk::<4>()?;
    Some((*version, *length, payload))
}

fn main() {
    let raw = b"\x01\x02\x00\x00\x00\x05hello";
    let (version, length, payload) = parse_packet(raw).unwrap();

    assert_eq!(version, [0x01, 0x02]);
    assert_eq!(length, [0x00, 0x00, 0x00, 0x05]);
    assert_eq!(payload, b"hello");
}

Each ? short-circuits if the remaining data is too short. No bounds checks scattered across your code.

From the Other End: split_last_chunk

Need to grab a suffix instead — like a trailing checksum? split_last_chunk mirrors the API from the back:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn strip_checksum(data: &[u8]) -> Option<(&[u8], &[u8; 2])> {
    data.split_last_chunk::<2>()
}

fn main() {
    let msg = b"payload\xAB\xCD";
    let (body, checksum) = strip_checksum(msg).unwrap();
    assert_eq!(body, b"payload");
    assert_eq!(checksum, &[0xAB, 0xCD]);

    let short = b"\x01";
    assert!(strip_checksum(short).is_none());
}

Same safety, same ergonomics — just peeling from the tail.

The Full Family

These methods come in mutable variants too, all stabilized in 1.77:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
fn main() {
    // Immutable — borrow array refs from a slice
    let data: &[u8] = &[1, 2, 3, 4, 5];
    let (head, tail) = data.split_first_chunk::<2>().unwrap();
    assert_eq!(head, &[1, 2]);
    assert_eq!(tail, &[3, 4, 5]);

    // split_last_chunk — from the back
    let (init, last) = data.split_last_chunk::<2>().unwrap();
    assert_eq!(init, &[1, 2, 3]);
    assert_eq!(last, &[4, 5]);

    // first_chunk / last_chunk — just the array, no remainder
    let first: &[u8; 3] = data.first_chunk::<3>().unwrap();
    assert_eq!(first, &[1, 2, 3]);

    let last: &[u8; 3] = data.last_chunk::<3>().unwrap();
    assert_eq!(last, &[3, 4, 5]);
}

Wherever you reach for &data[..N] and a try_into(), there’s probably a chunk method that does it better. Type-safe, bounds-checked, and zero-cost.

52. Peekable::next_if_map — Peek, Match, and Transform in One Step

Tired of peeking at the next element, checking if it matches, and then consuming and transforming it? next_if_map collapses that entire dance into a single call.

The problem

When writing parsers or processing token streams, you often need to conditionally consume the next element and extract something from it. With next_if and peek, you end up doing the work twice — once to check, once to transform:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clunky: peek, check, consume, transform — separately
let val = if iter.peek().is_some_and(|n| *n > 0) {
    iter.next().map(|n| n * 10)
} else {
    None
};

assert_eq!(val, Some(10));

It works, but you’re expressing the same logic in two places and the code doesn’t clearly convey its intent.

Enter next_if_map

Stabilized in Rust 1.94, Peekable::next_if_map takes a closure that returns Result<R, I::Item>. Return Ok(transformed) to consume the element, or Err(original) to put it back:

1
2
3
4
5
6
7
8
9
let mut iter = vec![1_i64, 2, -3, 4].into_iter().peekable();

// Clean: one closure handles the check AND the transformation
let val = iter.next_if_map(|n| {
    if n > 0 { Ok(n * 10) } else { Err(n) }
});

assert_eq!(val, Some(10));
assert_eq!(iter.next(), Some(2)); // iterator continues normally

The element is only consumed when you return Ok. If you return Err, the original value goes back and the iterator is unchanged.

Parsing example: extract leading digits

This is where next_if_map really shines — pulling typed tokens out of a character stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let mut chars = "42abc".chars().peekable();

let mut number = 0u32;
while let Some(digit) = chars.next_if_map(|c| {
    c.to_digit(10).ok_or(c)
}) {
    number = number * 10 + digit;
}

assert_eq!(number, 42);
assert_eq!(chars.next(), Some('a')); // non-digit stays unconsumed

Each character is inspected once: digits are consumed and converted, and the first non-digit stops the loop without being eaten.

Key details

  • Atomic peek + consume + transform: no redundant checks, no repeated logic
  • Non-destructive on rejection: returning Err(item) puts the element back
  • Also available: next_if_map_mut takes FnOnce(&mut I::Item) -> Option<R> for when you don’t need ownership
  • Stable since Rust 1.94

Next time you’re writing a peek-then-consume pattern, reach for next_if_map — your parser will thank you.

#044 Mar 2026

44. split_once — Split a String Exactly Once

When you need to split a string on the first occurrence of a delimiter, split_once is cleaner than anything you’d write by hand. Stable since Rust 1.52.

Parsing key=value pairs, HTTP headers, file paths — almost everywhere you split a string, you only care about the first separator. Before split_once, you’d reach for .find() plus index arithmetic:

The old way

1
2
3
4
5
6
7
8
let s = "Content-Type: application/json; charset=utf-8";

let colon = s.find(':').unwrap();
let header = &s[..colon];
let value = s[colon + 1..].trim();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

Works, but it’s four lines of noise. The index arithmetic is easy to get wrong, and .trim() is a separate step.

With split_once

1
2
3
4
5
6
let s = "Content-Type: application/json; charset=utf-8";

let (header, value) = s.split_once(": ").unwrap();

assert_eq!(header, "Content-Type");
assert_eq!(value, "application/json; charset=utf-8");

One line. The delimiter is consumed, both sides are returned, and you pattern-match directly into named bindings.

Handling missing delimiters

split_once returns Option<(&str, &str)>None if the delimiter isn’t found. This makes it composable with ? or if let:

1
2
3
4
5
6
7
fn parse_env_var(s: &str) -> Option<(&str, &str)> {
    s.split_once('=')
}

assert_eq!(parse_env_var("HOME=/root"), Some(("HOME", "/root")));
assert_eq!(parse_env_var("NOVALUE"), None);
assert_eq!(parse_env_var("KEY=a=b=c"), Some(("KEY", "a=b=c")));

Note the last case: split_once stops at the first =. The rest of the string — a=b=c — is kept intact in the second half. That’s usually exactly what you want.

rsplit_once — split from the right

When you need the last delimiter instead of the first, rsplit_once has you covered:

1
2
3
4
5
6
let path = "/home/martin/projects/rustbites/content/posts/bite-044.md";

let (dir, filename) = path.rsplit_once('/').unwrap();

assert_eq!(dir, "/home/martin/projects/rustbites/content/posts");
assert_eq!(filename, "bite-044.md");

Multi-char delimiters work too

The delimiter can be any pattern — a char, a &str, or even a closure:

1
2
3
4
5
6
7
8
let record = "alice::42::engineer";

let (name, rest) = record.split_once("::").unwrap();
let (age_str, role) = rest.split_once("::").unwrap();

assert_eq!(name, "alice");
assert_eq!(age_str, "42");
assert_eq!(role, "engineer");

Whenever you reach for .splitn(2, ...) just to grab two halves, replace it with split_once — the intent is clearer and the return type is more ergonomic.