From 1c85d339c3e2b0f953ce0f32c4ef8688bc39cd72 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Sun, 25 Oct 2026 17:15:42 +0100 Subject: [PATCH] Reject negative or out-of-range minutes/seconds in D'" angles (#2116) Found while testing crates/engine/src/units.rs:333-340 (parse_dms) Co-authored-by: Claude Haiku 5.5 Co-authored-by: Brandon Thomas --- crates/engine/src/units.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/units.rs b/crates/engine/src/units.rs index a3018d8f3..0a18a0039 100644 --- a/crates/engine/src/units.rs +++ b/crates/engine/src/units.rs @@ -327,6 +327,12 @@ fn bearing(t: &str) -> Option { Some(deg.to_radians()) } +/// Minutes or seconds of a D'" angle: a number in `0..=60`. +fn dms_part(t: &str) -> Option { + let v: f64 = t.trim().parse().ok()?; + (0.0..=60.0).contains(&v).then_some(v) +} + /// Degrees, `45d`, `45d30'` or `45d30'15"` (a leading `-` negates the whole angle). fn parse_dms(t: &str) -> Option { let (neg, t) = match t.trim().strip_prefix('-') { @@ -339,11 +345,11 @@ fn parse_dms(t: &str) -> Option { if !rest.is_empty() { let (m, rest2) = rest.split_once('\'').unwrap_or((rest, "")); if !m.trim().is_empty() { - deg += m.trim().parse::().ok()? / 60.0; + deg += dms_part(m)? / 60.0; } let s = rest2.trim().trim_end_matches('"'); if !s.is_empty() { - deg += s.parse::().ok()? / 3600.0; + deg += dms_part(s)? / 3600.0; } } let deg = if neg { -deg } else { deg }; @@ -363,6 +369,14 @@ pub const DEG: f64 = PI / 180.0; mod tests { use super::*; + #[test] + fn dms_minutes_and_seconds_must_be_within_0_to_60() { + for t in ["45d-30'", "45d30'-15\"", "45d75'", "45d30'75\"", "n45d-30'e"] { + assert_eq!(parse_angle(t), None, "{t}"); + } + assert!(parse_angle("45d30'15\"").is_some()); + } + #[test] fn engineering_huge_distance_falls_back_to_decimal() { assert_eq!(format_distance(1e30, 3, 2), format!("{:.2}", 1e30));