Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate output of decoding #45

Merged
merged 2 commits into from
May 9, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 46 additions & 36 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LineString<f64>, 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<Enumerate<impl std::iter::Iterator<Item = u8>>>,
) -> Result<i64, String> {
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;
Expand All @@ -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)]
Expand Down Expand Up @@ -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<f64> =
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]
Expand Down