use crate::error::{AstroError, Result};
use lazy_static::lazy_static;
use regex::{Regex, RegexBuilder};
use std::str::FromStr;
lazy_static! {
static ref HMS_REGEX: Regex = RegexBuilder::new(
r"(\d{1,3}(?:\.\d{1,10})?)\s*h\s*(\d{1,2}(?:\.\d{1,10})?)\s*m?\s*(\d{1,2}(?:\.\d{1,10})?)\s*s?"
)
.size_limit(1024 * 100) .dfa_size_limit(1024 * 1024) .build()
.expect("HMS regex compilation failed");
static ref DMS_REGEX: Regex = RegexBuilder::new(
r"([+-]?\d{1,3}(?:\.\d{1,10})?)\s*[°d]?\s*(\d{1,2}(?:\.\d{1,10})?)\s*['′m]?\s*(\d{1,2}(?:\.\d{1,10})?)\s*[\"″s]?"
)
.size_limit(1024 * 100)
.dfa_size_limit(1024 * 1024)
.build()
.expect("DMS regex compilation failed");
/// Decimal degrees pattern
static ref DECIMAL_REGEX: Regex = Regex::new(
r"^[+-]?\d{1,3}(?:\.\d{1,15})?$"
).expect("Decimal regex compilation failed");
/// Compact format pattern (DDMM.mmm or DDMMSS)
static ref COMPACT_REGEX: Regex = Regex::new(
r"^(\d{4,7})(?:\.(\d{1,6}))?$"
).expect("Compact regex compilation failed");
}
/// Optimized coordinate parsing with minimal allocations
pub fn parse_coordinate_optimized(input: &str, is_latitude: bool) -> Result<f64> {
// Input validation with early returns to prevent DoS
if input.len() > 100 {
return Err(AstroError::InvalidDmsFormat {
input: format!("{}...", &input[..20]),
expected: "Input too long (>100 chars)"
});
}
let trimmed = input.trim();
if trimmed.is_empty() {
return Err(AstroError::InvalidDmsFormat {
input: input.to_string(),
expected: "Non-empty coordinate string"
});
}
// Extract compass direction without allocations
let (value_slice, compass_dir) = extract_compass_direction_optimized(trimmed);
// Try parsing in order of likelihood for better average performance
// 1. Decimal degrees (most common case)
if let Ok(value) = try_parse_decimal_optimized(value_slice) {
return apply_compass_direction_optimized(value, compass_dir, is_latitude);
}
// 2. DMS format
if let Ok(value) = try_parse_dms_optimized(value_slice) {
return apply_compass_direction_optimized(value, compass_dir, is_latitude);
}
// 3. HMS format (longitude only)
if !is_latitude {
if let Ok(value) = try_parse_hms_optimized(value_slice) {
return apply_compass_direction_optimized(value, compass_dir, is_latitude);
}
}
// 4. Compact formats
if let Ok(value) = try_parse_compact_optimized(value_slice) {
return apply_compass_direction_optimized(value, compass_dir, is_latitude);
}
// All parsing failed
Err(AstroError::InvalidDmsFormat {
input: input.to_string(),
expected: if is_latitude {
"Examples: 40.7128, 40.7128N, 40°42'46\""
} else {
"Examples: -74.0060, 74.0060W, 4h56m27s"
}
})
}
fn extract_compass_direction_optimized(input: &str) -> (&str, Option<char>) {
let bytes = input.as_bytes();
if bytes.is_empty() {
return (input, None);
}
let first = bytes[0].to_ascii_uppercase();
if matches!(first, b'N' | b'S' | b'E' | b'W') {
let remainder = &input[1..].trim_start();
return (remainder, Some(first as char));
}
if input.len() <= 50 { let last = bytes[bytes.len() - 1].to_ascii_uppercase();
if matches!(last, b'N' | b'S' | b'E' | b'W') {
if bytes.len() == 1 ||
(bytes.len() > 1 && !bytes[bytes.len() - 2].is_ascii_alphabetic()) {
let value_part = &input[..input.len() - 1].trim_end();
return (value_part, Some(last as char));
}
}
}
if input.len() <= 30 {
let upper = input.to_uppercase();
for &(word, dir) in &[("NORTH", 'N'), ("SOUTH", 'S'), ("EAST", 'E'), ("WEST", 'W')] {
if let Some(pos) = upper.find(word) {
let mut result = String::with_capacity(input.len());
result.push_str(&input[..pos]);
result.push_str(&input[pos + word.len()..]);
return (Box::leak(result.into_boxed_str()), Some(dir));
}
}
}
(input, None)
}
fn try_parse_decimal_optimized(s: &str) -> Result<f64> {
if s.chars().any(|c| c.is_alphabetic() && !matches!(c, 'e' | 'E')) {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "decimal degrees"
});
}
if !DECIMAL_REGEX.is_match(s) {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "valid decimal format"
});
}
f64::from_str(s.trim_start_matches('+')).map_err(|_| AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "decimal degrees"
})
}
fn try_parse_dms_optimized(s: &str) -> Result<f64> {
if !s.chars().any(|c| c.is_ascii_digit()) {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "DMS format"
});
}
if let Some(caps) = DMS_REGEX.captures(s) {
let d_str = &caps[1];
let is_negative = s.starts_with('-') || d_str.starts_with('-');
let d = f64::from_str(d_str.trim_start_matches('-')).unwrap_or(0.0);
let m = caps.get(2).and_then(|c| f64::from_str(c.as_str()).ok()).unwrap_or(0.0);
let s = caps.get(3).and_then(|c| f64::from_str(c.as_str()).ok()).unwrap_or(0.0);
if m >= 60.0 || s >= 60.0 {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "valid minutes/seconds (< 60)"
});
}
let abs_value = d + m/60.0 + s/3600.0;
return Ok(if is_negative { -abs_value } else { abs_value });
}
Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "DMS format"
})
}
fn try_parse_hms_optimized(s: &str) -> Result<f64> {
if !s.contains('h') && !s.contains('H') {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "HMS format with 'h' indicator"
});
}
if let Some(caps) = HMS_REGEX.captures(s) {
let h = f64::from_str(&caps[1]).unwrap_or(0.0);
let m = caps.get(2).and_then(|c| f64::from_str(c.as_str()).ok()).unwrap_or(0.0);
let s = caps.get(3).and_then(|c| f64::from_str(c.as_str()).ok()).unwrap_or(0.0);
if h >= 24.0 || m >= 60.0 || s >= 60.0 {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "valid HMS (h<24, m<60, s<60)"
});
}
return Ok((h + m/60.0 + s/3600.0) * 15.0);
}
Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "HMS format"
})
}
fn try_parse_compact_optimized(s: &str) -> Result<f64> {
if !s.chars().all(|c| c.is_ascii_digit() || c == '.') {
return Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "compact numeric format"
});
}
if let Some(caps) = COMPACT_REGEX.captures(s) {
let main_part = &caps[1];
let decimal_part = caps.get(2).map(|m| m.as_str()).unwrap_or("");
if main_part.len() == 4 || main_part.len() == 5 {
if let Ok(ddmm) = i32::from_str(main_part) {
let dd = ddmm / 100;
let mm = ddmm % 100;
if mm < 60 { let mut result = dd as f64 + mm as f64 / 60.0;
if !decimal_part.is_empty() {
if let Ok(decimal_mins) = f64::from_str(&format!("0.{}", decimal_part)) {
result += decimal_mins / 60.0;
}
}
return Ok(result);
}
}
}
if main_part.len() == 6 || main_part.len() == 7 {
let (dd_len, _) = if main_part.len() == 7 { (3, true) } else { (2, false) };
if let Ok(dd) = i32::from_str(&main_part[..dd_len]) {
if let Ok(mm) = i32::from_str(&main_part[dd_len..dd_len+2]) {
if let Ok(ss) = i32::from_str(&main_part[dd_len+2..]) {
if mm < 60 && ss < 60 {
return Ok(dd as f64 + mm as f64 / 60.0 + ss as f64 / 3600.0);
}
}
}
}
}
}
Err(AstroError::InvalidDmsFormat {
input: s.to_string(),
expected: "compact DDMM.mmm or DDMMSS format"
})
}
fn apply_compass_direction_optimized(mut value: f64, direction: Option<char>, is_latitude: bool) -> Result<f64> {
if let Some(dir) = direction {
match dir {
'S' if is_latitude => value = -value.abs(),
'W' if !is_latitude => value = -value.abs(),
'N' if is_latitude => value = value.abs(),
'E' if !is_latitude => value = value.abs(),
'N' | 'S' if !is_latitude => return Err(AstroError::InvalidDmsFormat {
input: format!("{}{}", value, dir),
expected: "E/W for longitude, not N/S"
}),
'E' | 'W' if is_latitude => return Err(AstroError::InvalidDmsFormat {
input: format!("{}{}", value, dir),
expected: "N/S for latitude, not E/W"
}),
_ => {}
}
}
if is_latitude {
if !(-90.0..=90.0).contains(&value) {
return Err(AstroError::InvalidDmsFormat {
input: format!("{}", value),
expected: "latitude in range [-90, 90] degrees"
});
}
} else {
if value.abs() > 360.0 {
return Err(AstroError::InvalidDmsFormat {
input: format!("{}", value),
expected: "reasonable longitude value"
});
}
while value > 180.0 {
value -= 360.0;
}
while value <= -180.0 {
value += 360.0;
}
}
if !value.is_finite() {
return Err(AstroError::InvalidDmsFormat {
input: "NaN/Infinity".to_string(),
expected: "finite coordinate value"
});
}
Ok(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_optimized_parsing_performance() {
let test_cases = vec![
("40.7128", true, 40.7128),
("40°42'46\"N", true, 40.712777777777777),
("4h56m27s", false, 74.1125),
("4042.767N", true, 40.71278333333333),
("-74.0060", false, -74.006),
];
for (input, is_lat, expected) in test_cases {
let result = parse_coordinate_optimized(input, is_lat).unwrap();
assert!((result - expected).abs() < 1e-10,
"Failed for {}: got {}, expected {}", input, result, expected);
}
}
#[test]
fn test_dos_protection() {
let malicious_inputs = vec![
"a".repeat(1000), "d".repeat(100) + "m" + &"s".repeat(100), "123".repeat(50) + "°", ];
for input in malicious_inputs {
let start = std::time::Instant::now();
let _result = parse_coordinate_optimized(&input, true);
let elapsed = start.elapsed();
assert!(elapsed.as_millis() < 1,
"DoS protection failed: {} took {:?}", &input[..20.min(input.len())], elapsed);
}
}
#[test]
fn test_security_bounds() {
let extreme_values = vec![
("999.999", true, false), ("400.0", false, false), ("NaN", true, false), ("Infinity", false, false), ];
for (input, is_lat, should_pass) in extreme_values {
let result = parse_coordinate_optimized(input, is_lat);
if should_pass {
assert!(result.is_ok(), "Should accept {}", input);
} else {
assert!(result.is_err(), "Should reject {}", input);
}
}
}
}
pub fn benchmark_comparison() {
use std::time::Instant;
let test_inputs = vec![
"40.7128N", "74.0060W", "40°42'46\"", "4h56m27s",
"40d42m46s", "4042.767N", "-92.3009", "51.5074",
];
let start = Instant::now();
for _ in 0..10000 {
for input in &test_inputs {
let _ = parse_coordinate_optimized(input, true);
}
}
let optimized_time = start.elapsed();
println!("Optimized parsing: {:?} for {} iterations",
optimized_time, 10000 * test_inputs.len());
println!("Rate: {:.0} parses/sec",
(10000 * test_inputs.len()) as f64 / optimized_time.as_secs_f64());
}