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.