diff --git a/crates/engine/src/cmd/datamerge/parse.rs b/crates/engine/src/cmd/datamerge/parse.rs index 6cc838c3c..dfc49e842 100644 --- a/crates/engine/src/cmd/datamerge/parse.rs +++ b/crates/engine/src/cmd/datamerge/parse.rs @@ -210,6 +210,10 @@ fn select_range(range: &str, count: usize, idx: &mut Vec, seen: &mut std: } fn parse_record_no(s: &str) -> Result { + // `parse` alone would accept a leading '+'. + if !s.bytes().all(|b| b.is_ascii_digit()) { + return Err(format!("bad record \"{s}\"")); + } s.parse::().map_err(|_| format!("bad record \"{s}\"")) } diff --git a/crates/engine/src/cmd/datamerge/tests.rs b/crates/engine/src/cmd/datamerge/tests.rs index 9af882653..212175d78 100644 --- a/crates/engine/src/cmd/datamerge/tests.rs +++ b/crates/engine/src/cmd/datamerge/tests.rs @@ -1179,6 +1179,14 @@ fn select_records_dedupes_large_ranges_in_first_seen_order() { assert_eq!(&idx[..3], &[4, 0, 1]); } +#[test] +fn select_records_rejects_signed_record_numbers() { + for (tok, bad) in [("+2", "+2"), ("1-+3", "+3"), ("+1,+4", "+1")] { + let err = parse::select_records(5, "range", 1, tok, None).unwrap_err(); + assert!(err.contains(&format!("bad record \"{bad}\"")), "{tok}: {err}"); + } +} + #[test] fn oversized_data_file_is_refused_before_reading() { let dir = scratch("oversized");