Mehdi Akiki
Rust Failure Atlas / Runtime, memory, and library APIs

RFA-651 · Case file with fixtures · Case 623 of 694 · Runtime evidence

Vec split_off Requires an Index at or Before len

split_off is an infallible API for a prevalidated boundary. Validate untrusted indexes, remember that len is allowed, and keep byte, character, and element units distinct.

Reviewed
Rust
Rust 1.98.1, edition 2024
Targets
all Rust targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
An external or stale element boundary was used without checking the inclusive upper split bound against current initialized length.
First discriminating check
Validate at <= len on the same snapshot and choose error, clamp, or panic according to whether the boundary is trusted.

Vec::split_off(at) keeps elements before at in the original vector and returns elements from at onward in a new vector. It assumes the boundary is valid. The failing fixture supplies four for a length-three vector and reproduces the documented panic.

The valid interval includes len

Indexes naming an element use 0..len. A split boundary uses 0..=len: zero moves the entire vector into the result, while len returns an empty tail. Any value above len is invalid.

The split_off documentation states the panic condition and describes capacity behaviour. The vector’s len counts initialized elements, not capacity.

I write the condition as at <= values.len() rather than trying to adapt an element-access check using <. Boundary and element positions are related but not identical domains.

Validate indexes from outside the algorithm

The repaired fixture wraps the precondition and returns None for an invalid split. A production API may return a domain error containing the requested boundary and permitted length.

If the index is derived internally from a prior search on the unchanged vector, direct split_off can be appropriate and a panic may signal an invariant bug. If it comes from a client, file, database cursor, or stale concurrent snapshot, validation belongs at the boundary.

Clamping with min(len) avoids the panic but can hide corrupt offsets. I clamp only when “everything remaining” is genuinely the documented meaning of an oversized request.

Length and capacity answer different questions

A vector with length three and capacity sixteen still cannot split at four. Capacity describes allocated room for future elements, not existing initialized items.

This confusion appears in unsafe code and buffer protocols. Writing through spare capacity requires initialization and a correct later length update; safe collection methods operate on the current length.

split_off allocates a new vector for the returned elements under its documented behaviour. If the operation only needs two borrowed views, slice splitting avoids moving ownership and allocation. The slice API offers split_at_checked for a non-panicking boundary.

Keep index units explicit

For Vec<T>, the index counts elements. For String and str, byte boundaries also need UTF-8 character-boundary validity. Protocol offsets may count bytes, code points, records, or pages.

I use names such as record_index or byte_offset and convert once with validation. A bare usize does not encode units. Newtypes can prevent mixing important offsets across layers.

When the source can change between computing and applying an index, I hold the appropriate borrow or lock, use versioning, or revalidate against the same snapshot. A valid old index can exceed a newly shortened vector.

Ownership and failure order matter

After a successful split, elements from the boundary move to the new vector and the original length changes. References and indexes into the old logical sequence need reassessment. Safe Rust prevents live borrows across the mutable call, but stored numeric indexes remain and can become semantically stale.

If allocation fails, Rust’s normal allocation policy may abort or use fallible alternatives depending on APIs and environment. For extremely large processing, drain, chunked iteration, or a queue design may avoid duplicating vector allocations.

Tests cover empty vectors, zero, exactly len, one above len, and a middle split. Property tests can concatenate the two outputs and prove they reproduce the input ordering.

My split_off checklist

  • Is at an element boundary and is its valid range 0..=len?
  • Does the index come from trusted invariant logic or untrusted/stale input?
  • Should invalid input return an error, clamp, or reveal an internal bug?
  • Is capacity being confused with initialized length?
  • Would borrowed slice splitting avoid allocation and ownership transfer?
  • Are byte, character, element, and record offsets kept distinct?
  • Can mutation occur between computing and applying the boundary?
  • Do edge and property tests preserve order and concatenation?

The core principle is that split positions lie between elements, including both ends, but never beyond the collection. split_off assumes that boundary was established. I validate external offsets and choose borrowed or owned splitting from the operation’s real lifecycle.