1use alloc::string::{String, ToString};
15#[cfg(test)]
16use alloc::vec::Vec;
17
18#[derive(Debug, Clone, PartialEq)]
26pub enum Token {
27 Ident(String),
30 Number(f64),
33 LeftParen,
35 RightParen,
37 Comma,
39 Empty,
41 Eof,
43}
44
45#[derive(Debug, Clone, PartialEq)]
53pub enum WktError {
54 UnexpectedChar {
56 pos: usize,
58 ch: char,
60 },
61 UnexpectedToken {
63 expected: &'static str,
65 found: String,
67 },
68 UnexpectedEof,
70 InvalidNumber(String),
72 UnknownGeometryType(String),
74 TypeMismatch {
77 expected: &'static str,
79 found: &'static str,
81 },
82 NestingTooDeep,
86}
87
88impl core::fmt::Display for WktError {
89 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
90 match self {
91 WktError::UnexpectedChar { pos, ch } => {
92 write!(f, "unexpected character {ch:?} at byte {pos}")
93 }
94 WktError::UnexpectedToken { expected, found } => {
95 write!(f, "expected {expected}, found {found}")
96 }
97 WktError::UnexpectedEof => f.write_str("unexpected end of input"),
98 WktError::InvalidNumber(s) => write!(f, "invalid number {s:?}"),
99 WktError::UnknownGeometryType(s) => write!(f, "unknown geometry type {s:?}"),
100 WktError::TypeMismatch { expected, found } => {
101 write!(f, "type mismatch: expected {expected}, found {found}")
102 }
103 WktError::NestingTooDeep => {
104 f.write_str("WKT nesting too deep; exceeded the reader's recursion limit")
105 }
106 }
107 }
108}
109
110#[cfg(feature = "std")]
111impl std::error::Error for WktError {}
112
113pub(crate) struct Lexer<'a> {
129 input: &'a str,
130 pos: usize,
131}
132
133impl<'a> Lexer<'a> {
134 pub(crate) fn new(input: &'a str) -> Self {
135 Self { input, pos: 0 }
136 }
137
138 pub(crate) fn next_token(&mut self) -> Result<Token, WktError> {
142 let bytes = self.input.as_bytes();
143 while let Some(&byte) = bytes.get(self.pos) {
144 if byte.is_ascii_whitespace() {
145 self.pos += 1;
146 } else if !byte.is_ascii() {
147 let ch = self.input[self.pos..]
148 .chars()
149 .next()
150 .expect("position is inside the input");
151 if ch.is_whitespace() {
152 self.pos += ch.len_utf8();
153 } else {
154 break;
155 }
156 } else {
157 break;
158 }
159 }
160
161 let Some(&byte) = bytes.get(self.pos) else {
162 return Ok(Token::Eof);
163 };
164 let start = self.pos;
165 if byte == b'(' {
166 self.pos += 1;
167 Ok(Token::LeftParen)
168 } else if byte == b')' {
169 self.pos += 1;
170 Ok(Token::RightParen)
171 } else if byte == b',' {
172 self.pos += 1;
173 Ok(Token::Comma)
174 } else if byte.is_ascii_alphabetic() {
175 self.pos += 1;
176 while bytes.get(self.pos).is_some_and(u8::is_ascii_alphabetic) {
177 self.pos += 1;
178 }
179 let word = self.input[start..self.pos].to_ascii_uppercase();
180 if word == "EMPTY" {
181 Ok(Token::Empty)
182 } else {
183 Ok(Token::Ident(word))
184 }
185 } else if byte == b'+' || byte == b'-' || byte == b'.' || byte.is_ascii_digit() {
186 let negative = byte == b'-';
187 let mut all_digits = byte == b'+' || byte == b'-' || byte.is_ascii_digit();
188 let mut saw_digit = byte.is_ascii_digit();
189 let mut integer = if saw_digit { u64::from(byte - b'0') } else { 0 };
190 self.pos += 1;
191 while let Some(&byte) = bytes.get(self.pos) {
192 if byte.is_ascii_digit()
193 || byte == b'.'
194 || byte == b'+'
195 || byte == b'-'
196 || byte == b'e'
197 || byte == b'E'
198 {
199 if all_digits {
200 if byte.is_ascii_digit() {
201 saw_digit = true;
202 match integer
203 .checked_mul(10)
204 .and_then(|value| value.checked_add(u64::from(byte - b'0')))
205 {
206 Some(next) => integer = next,
207 None => all_digits = false,
208 }
209 } else {
210 all_digits = false;
211 }
212 }
213 self.pos += 1;
214 } else {
215 break;
216 }
217 }
218 let slice = &self.input[start..self.pos];
219 let value = if all_digits && saw_digit {
220 #[allow(
221 clippy::cast_precision_loss,
222 reason = "integer-to-f64 uses the same IEEE-754 rounding as parsing its decimal spelling"
223 )]
224 let value = integer as f64;
225 if negative { -value } else { value }
226 } else {
227 slice
228 .parse()
229 .map_err(|_| WktError::InvalidNumber(slice.to_string()))?
230 };
231 Ok(Token::Number(value))
232 } else {
233 let ch = self.input[self.pos..]
234 .chars()
235 .next()
236 .expect("position is inside the input");
237 Err(WktError::UnexpectedChar { pos: self.pos, ch })
238 }
239 }
240}
241
242#[cfg(test)]
243pub(crate) fn tokenize(input: &str) -> Result<Vec<Token>, WktError> {
244 let mut lexer = Lexer::new(input);
245 let mut tokens = Vec::new();
246 loop {
247 let token = lexer.next_token()?;
248 let done = token == Token::Eof;
249 tokens.push(token);
250 if done {
251 return Ok(tokens);
252 }
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 #![allow(
262 clippy::float_cmp,
263 reason = "number tokens come from exact integer / short-decimal WKT literals"
264 )]
265
266 use super::{Token, WktError, tokenize};
267 use alloc::vec;
268
269 #[test]
270 fn each_token_kind() {
271 let toks = tokenize("POINT ( 1 , 2 )").unwrap();
272 assert_eq!(
273 toks,
274 vec![
275 Token::Ident("POINT".into()),
276 Token::LeftParen,
277 Token::Number(1.0),
278 Token::Comma,
279 Token::Number(2.0),
280 Token::RightParen,
281 Token::Eof,
282 ]
283 );
284 }
285
286 #[test]
287 fn identifiers_are_uppercased() {
288 let toks = tokenize("LineString").unwrap();
289 assert_eq!(toks, vec![Token::Ident("LINESTRING".into()), Token::Eof]);
290 }
291
292 #[test]
293 fn empty_is_its_own_token() {
294 let toks = tokenize("POINT EMPTY").unwrap();
295 assert_eq!(
296 toks,
297 vec![Token::Ident("POINT".into()), Token::Empty, Token::Eof]
298 );
299 }
300
301 #[test]
302 fn dimension_suffix_lexes_as_ident() {
303 let toks = tokenize("POINT ZM").unwrap();
304 assert_eq!(
305 toks,
306 vec![
307 Token::Ident("POINT".into()),
308 Token::Ident("ZM".into()),
309 Token::Eof,
310 ]
311 );
312 }
313
314 #[test]
315 fn number_e_notation() {
316 let toks = tokenize("1.5e-3").unwrap();
317 assert_eq!(toks, vec![Token::Number(0.0015), Token::Eof]);
318 }
319
320 #[test]
321 fn number_signed_and_decimal() {
322 let toks = tokenize("-10 20.5 +3").unwrap();
323 assert_eq!(
324 toks,
325 vec![
326 Token::Number(-10.0),
327 Token::Number(20.5),
328 Token::Number(3.0),
329 Token::Eof,
330 ]
331 );
332 }
333
334 #[test]
335 fn integral_fast_path_matches_standard_float_rounding() {
336 for literal in [
337 "0",
338 "-0",
339 "9007199254740993",
340 "18446744073709551615",
341 "-18446744073709551615",
342 ] {
343 let expected = literal.parse::<f64>().unwrap();
344 let tokens = tokenize(literal).unwrap();
345 assert!(
346 matches!(&tokens[0], Token::Number(actual) if actual.to_bits() == expected.to_bits()),
347 "literal {literal}: expected number token {expected:?}"
348 );
349 }
350 }
351
352 #[test]
353 fn malformed_char_reports_position() {
354 let err = tokenize("POINT (1 @)").unwrap_err();
355 assert_eq!(err, WktError::UnexpectedChar { pos: 9, ch: '@' });
356 }
357
358 #[test]
359 fn malformed_number_reports_slice() {
360 let err = tokenize("1.2.3").unwrap_err();
361 assert_eq!(err, WktError::InvalidNumber("1.2.3".into()));
362 }
363
364 #[test]
367 fn every_error_variant_displays_descriptively() {
368 use alloc::format;
369
370 assert_eq!(
371 format!("{}", WktError::UnexpectedChar { pos: 9, ch: '@' }),
372 "unexpected character '@' at byte 9"
373 );
374 assert_eq!(
375 format!(
376 "{}",
377 WktError::UnexpectedToken {
378 expected: "'('",
379 found: "Comma".into()
380 }
381 ),
382 "expected '(', found Comma"
383 );
384 assert_eq!(
385 format!("{}", WktError::UnexpectedEof),
386 "unexpected end of input"
387 );
388 assert_eq!(
389 format!("{}", WktError::InvalidNumber("1.2.3".into())),
390 "invalid number \"1.2.3\""
391 );
392 assert_eq!(
393 format!("{}", WktError::UnknownGeometryType("TRIANGLE".into())),
394 "unknown geometry type \"TRIANGLE\""
395 );
396 assert_eq!(
397 format!(
398 "{}",
399 WktError::TypeMismatch {
400 expected: "POINT",
401 found: "LINESTRING"
402 }
403 ),
404 "type mismatch: expected POINT, found LINESTRING"
405 );
406 assert!(
407 format!("{}", WktError::NestingTooDeep).contains("nesting too deep"),
408 "nesting message"
409 );
410 }
411}