S24: Indexing (v[i], s[a..b]) on untrusted input
What it looks like
Section titled “What it looks like”A line from a partner’s CSV upload: id,sku,quantity. The first three characters of the SKU are the category.
pub fn parse_line(line: &str) -> Line<'_> { let fields: Vec<&str> = line.split(',').collect(); Line { id: fields[0], category: &fields[1][..3], quantity: fields[2], }}Why AI agents produce it
Section titled “Why AI agents produce it”- It is the Java code, translated.
fields[1].substring(0, 3)becomes&fields[1][..3]. The sample data in the prompt is always well-formed, so the code passes the tests. - Indexing looks safe because Rust is “memory safe”. It is: an out-of-range index never reads wrong memory. But it panics, and a panic is a crash of the thread.
Why it is a problem
Section titled “Why it is a problem”Each index is a panic that one bad line can trigger:
| Input line | What happens |
|---|---|
7,BK-1234,2 |
Works. |
7,BK-1234 |
fields[2] panics: index out of bounds. |
7,BK,2 |
[..3] panics: byte index 3 is out of bounds. |
7,BKü-1,2 |
[..3] panics: byte index 3 is not a char boundary. “ü” is two bytes. |
All three panics are tests in examples/smells/src/s24_indexing_untrusted_input.rs. The last one is the one that nobody tests: a partner in Germany sends a SKU with an umlaut. In a server, one request kills its worker thread. In a batch job, one line stops the whole import, and the error says nothing about which line.
The better version
Section titled “The better version”WorseThree indexes, each a panic on bad input.
pub fn parse_line(line: &str) -> Line<'_> {let fields: Vec<&str> = line.split(',').collect();Line {id: fields[0],category: &fields[1][..3],quantity: fields[2],}}BetterA slice pattern checks the field count, and
get(..3)returnsNoneinstead of a panic.#[derive(Debug, PartialEq, Eq)]pub enum LineError {FieldCount(usize),ShortSku,}pub fn parse_line(line: &str) -> Result<Line<'_>, LineError> {let fields: Vec<&str> = line.split(',').collect();let [id, sku, quantity] = fields[..] else {return Err(LineError::FieldCount(fields.len()));};let category = sku.get(..3).ok_or(LineError::ShortSku)?; // None if too short or not on a char boundaryOk(Line {id,category,quantity,})}
The tools that replace indexing:
- Slice patterns:
let [id, sku, quantity] = fields[..] else { ... }checks the length and names the fields in one step. get:v.get(i)ands.get(a..b)returnOption.Nonefor out of range, and for a string range that cuts a character.- Methods that cannot fail:
split_once,strip_prefix,first(),last(),chars().take(n), and iterators instead of index loops.
When it is acceptable
Section titled “When it is acceptable”- When the index is correct by construction, and a wrong index would be a bug in the program, not bad input:
const MONTHS: [&str; 12] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",];
/// `month` comes from `chrono` or another type that guarantees 1 to 12./// A wrong value is a bug in this program, not bad input, so a panic is correct.pub fn month_name(month: u32) -> &'static str { MONTHS[month as usize - 1]}- Right after a check:
if v.len() >= 3 { &v[..3] }. A slice pattern orgetis still clearer. - Indexes from the same collection:
v.iter().position(...)returns a valid index forv, as long asvdoes not change in between. - Byte slices on ASCII-only data that you have validated, such as a fixed-format code that you checked with
is_ascii().
How to detect it
Section titled “How to detect it”- Clippy
indexing_slicing(restriction group, off by default) reports every index and slice that can panic. It is noisy in normal code, so many teams enable it only for parsing modules:#![warn(clippy::indexing_slicing)]at the top of the module. - Clippy
string_slice(restriction group, off by default) reports slicing of strings. - In a review, trace the data: if an index or a range comes from input (a request, a file, a split of a line), ask what happens when it is out of range.
- Test with short lines, empty fields, and non-ASCII text, not only with the sample from the prompt.
What to tell your agent
Section titled “What to tell your agent”parse_line must not panic on any input. Replace fields[0], fields[1] andfields[2] with a slice pattern (let [id, sku, quantity] = fields[..] else),and replace &sku[..3] with sku.get(..3). Return a LineError for a wrong fieldcount and for a short SKU. Add tests for "7,BK-1234", "7,BK,2" and "7,BKü-1,2".Related: S02: unwrap() / expect() in non-test code and S36: panic! used for normal errors.