diff --git a/src/lib.rs b/src/lib.rs index e04b4f6..09f9cea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ use geo_types::{Coord, LineString}; use std::char; +use std::iter::{Enumerate, Peekable}; const MIN_LONGITUDE: f64 = -180.0; const MAX_LONGITUDE: f64 = 180.0; @@ -99,52 +100,61 @@ where /// Decodes a Google Encoded Polyline. /// +/// Returns an error if the polyline is invalid or if the decoded coordinates are out of bounds. +/// /// # Examples /// /// ``` /// use polyline; /// -/// let decodedPolyline = polyline::decode_polyline(&"_p~iF~ps|U_ulLnnqC_mqNvxq`@", 5); +/// let decoded_polyline = polyline::decode_polyline(&"_p~iF~ps|U_ulLnnqC_mqNvxq`@", 5); /// ``` pub fn decode_polyline(polyline: &str, precision: u32) -> Result, String> { - let mut index = 0; - let mut lat: i64 = 0; - let mut lng: i64 = 0; + let mut scaled_lat: i64 = 0; + let mut scaled_lon: i64 = 0; let mut coordinates = vec![]; let base: i32 = 10; let factor = i64::from(base.pow(precision)); - let chars = polyline.as_bytes(); + let mut chars = polyline.as_bytes().iter().copied().enumerate().peekable(); - while index < chars.len() { - let (latitude_change, new_index) = trans(chars, index)?; - if new_index >= chars.len() { - break; + while let Some((lat_start, _)) = chars.peek().copied() { + let latitude_change = decode_next(&mut chars)?; + scaled_lat += latitude_change; + let lat = scaled_lat as f64 / factor as f64; + if !(MIN_LATITUDE..MAX_LATITUDE).contains(&lat) { + return Err(format!("Invalid latitude: {lat} at index: {lat_start}")); } - let (longitude_change, new_index) = trans(chars, new_index)?; - index = new_index; - lat += latitude_change; - lng += longitude_change; + let Some((lon_start, _)) = chars.peek().copied() else { + return Err(format!( + "No longitude to go with latitude at index: {lat_start}" + )); + }; + let longitude_change = decode_next(&mut chars)?; + scaled_lon += longitude_change; + let lon = scaled_lon as f64 / factor as f64; + if !(MIN_LONGITUDE..MAX_LONGITUDE).contains(&lon) { + return Err(format!("Invalid longitude: {lon} at index: {lon_start}")); + } - coordinates.push([lng as f64 / factor as f64, lat as f64 / factor as f64]); + coordinates.push([lon, lat]); } Ok(coordinates.into()) } -fn trans(chars: &[u8], mut index: usize) -> Result<(i64, usize), String> { +fn decode_next( + chars: &mut Peekable>>, +) -> Result { let mut shift = 0; let mut result = 0; - let mut byte; - loop { - byte = chars[index] as u64; + while let Some((idx, mut byte)) = chars.next() { if byte < 63 || (shift > 64 - 5) { - return Err(format!("Cannot decode character at index {}", index)); + return Err(format!("Cannot decode character at index {idx}")); } byte -= 63; - result |= (byte & 0x1f) << shift; - index += 1; + result |= ((byte & 0x1f) as u64) << shift; shift += 5; if byte < 0x20 { break; @@ -156,7 +166,7 @@ fn trans(chars: &[u8], mut index: usize) -> Result<(i64, usize), String> { } else { result >> 1 } as i64; - Ok((coordinate_change, index)) + Ok(coordinate_change) } #[cfg(test)] @@ -232,33 +242,33 @@ mod tests { } #[test] - // emoji is decodable but messes up data - // TODO: handle this case in the future? fn broken_string() { let s = "_p~iF~ps|U_u🗑lLnnqC_mqNvxq`@"; - let res = vec![ - [-120.2, 38.5], - [-120.95, 2306360.53104], - [-126.453, 2306363.08304], - ] - .into(); - assert_eq!(decode_polyline(s, 5).unwrap(), res); + let err = decode_polyline(s, 5).unwrap_err(); + assert_eq!(err, "Invalid latitude: 2306360.53104 at index: 10"); } #[test] - #[should_panic] fn invalid_string() { let s = "invalid_polyline_that_should_be_handled_gracefully"; - decode_polyline(s, 6).unwrap(); + let err = decode_polyline(s, 5).unwrap_err(); + assert_eq!(err, "Cannot decode character at index 12"); + } + + #[test] + fn another_invalid_string() { + let s = "ugh_ugh"; + let err = decode_polyline(s, 5).unwrap_err(); + assert_eq!(err, "Invalid latitude: 49775.95019 at index: 0"); } #[test] - #[should_panic] - // Can't have a latitude > 90.0 fn bad_coords() { + // Can't have a latitude > 90.0 let res: LineString = vec![[-120.2, 38.5], [-120.95, 40.7], [-126.453, 430.252]].into(); - encode_coordinates(res, 5).unwrap(); + let err = encode_coordinates(res, 5).unwrap_err(); + assert_eq!(err, "Invalid latitude: 430.252 at position 2"); } #[test]