This post is part of the Input Coverage > Code Coverage series.
Common pattern: a CLI reads CSV from stdin. First line: N=<rows>
.
Library path caps row count. CLI trusts N
. It allocates
Vec::with_capacity(N)
. Large N
aborts. Property tests and libFuzzer
hit only the safe path.
src/lib.rs
(unsafe CLI path on purpose):
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Row {
pub id: u64,
pub name: String,
pub cents: i64,
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum IngestError {
#[error("too many rows")]
TooManyRows,
#[error("invalid row")]
InvalidRow,
}
pub fn parse_row(line: &str) -> Result<Row, IngestError> {
let parts: Vec<&str> = line.trim_end().split(',').collect();
if parts.len() != 3 { return Err(IngestError::InvalidRow); }
let id = parts[0].parse().map_err(|_| IngestError::InvalidRow)?;
let name = parts[1].to_string();
let cents = parts[2].parse().map_err(|_| IngestError::InvalidRow)?;
Ok(Row { id, name, cents })
}
pub fn ingest_rows_safe(input: &str, max_rows: usize)
-> Result<Vec<Row>, IngestError>
{
let mut out = Vec::new();
for line in input.lines() {
if line.starts_with("N=") { continue; }
if line.trim().is_empty() { continue; }
if out.len() >= max_rows { return Err(IngestError::TooManyRows); }
out.push(parse_row(line)?);
}
Ok(out)
}
pub fn ingest_rows_unbounded(input: &str) -> Vec<Row> {
let mut lines = input.lines();
let mut cap = 0usize;
if let Some(hdr) = lines.next() {
if let Some(n) = hdr.strip_prefix("N=") {
cap = n.parse::<usize>().unwrap_or(0);
}
}
// Bug: trusts cap. No upper bound.
let mut out = Vec::with_capacity(cap);
for line in lines {
if line.trim().is_empty() { continue; }
if let Ok(row) = parse_row(line) { out.push(row); }
}
out
}
src/bin/import.rs
:
use std::io::Read;
fn main() {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf).unwrap();
let rows = csv_import_cli::ingest_rows_unbounded(&buf);
if rows.len() == usize::MAX { eprintln!("impossible."); }
}