1use crate::error::{DxError, Result};
7use crate::utf8::validate_string_input;
8use memchr::memchr;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum Token<'a> {
13 Eof,
15 Newline,
17 Colon,
19 Equals,
21 Caret,
23 Pipe,
25 Stream,
27 Ditto,
29 Null,
31 True,
33 False,
35 Bang,
37 Void,
39 Dollar,
41 At,
43 Percent,
45 Dot,
47 Ident(&'a [u8]),
49 Int(i64),
51 Float(f64),
53}
54
55pub struct Tokenizer<'a> {
57 pub(crate) input: &'a [u8],
58 pos: usize,
59}
60
61impl<'a> Tokenizer<'a> {
62 pub fn new(input: &'a [u8]) -> Self {
64 Self { input, pos: 0 }
65 }
66
67 pub fn pos(&self) -> usize {
69 self.pos
70 }
71
72 pub fn reset_to(&mut self, pos: usize) {
74 self.pos = pos;
75 }
76
77 pub fn is_eof(&self) -> bool {
79 self.pos >= self.input.len()
80 }
81
82 pub fn peek(&self) -> Option<u8> {
84 self.input.get(self.pos).copied()
85 }
86
87 pub fn peek_n(&self, n: usize) -> Option<u8> {
89 self.input.get(self.pos + n).copied()
90 }
91
92 pub fn advance(&mut self, n: usize) {
94 self.pos = (self.pos + n).min(self.input.len());
95 }
96
97 pub fn skip_whitespace(&mut self) {
99 while let Some(b) = self.peek() {
100 if b == b' ' || b == b'\t' {
101 self.pos += 1;
102 } else {
103 break;
104 }
105 }
106 }
107
108 pub fn skip_line(&mut self) {
110 while let Some(b) = self.peek() {
111 self.pos += 1;
112 if b == b'\n' {
113 break;
114 }
115 }
116 }
117
118 pub fn read_until(&mut self, delim: u8) -> &'a [u8] {
120 let start = self.pos;
121 let remaining = &self.input[self.pos..];
122
123 match memchr(delim, remaining) {
124 Some(offset) => {
125 self.pos += offset;
126 &self.input[start..self.pos]
127 }
128 None => {
129 self.pos = self.input.len();
130 &self.input[start..]
131 }
132 }
133 }
134
135 pub fn read_until_any(&mut self, delims: &[u8]) -> &'a [u8] {
137 let start = self.pos;
138 while let Some(b) = self.peek() {
139 if delims.contains(&b) {
140 break;
141 }
142 self.pos += 1;
143 }
144 &self.input[start..self.pos]
145 }
146
147 pub fn read_ident(&mut self) -> &'a [u8] {
149 let start = self.pos;
150 while let Some(b) = self.peek() {
151 if b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-' {
152 self.pos += 1;
153 } else {
154 break;
155 }
156 }
157 &self.input[start..self.pos]
158 }
159
160 pub fn read_number(&mut self) -> Result<Token<'a>> {
162 let start = self.pos;
163 let mut has_dot = false;
164 let mut has_exp = false;
165
166 if self.peek() == Some(b'-') {
168 self.pos += 1;
169 }
170
171 while let Some(b) = self.peek() {
173 match b {
174 b'0'..=b'9' => self.pos += 1,
175 b'.' if !has_dot && !has_exp => {
176 has_dot = true;
177 self.pos += 1;
178 }
179 b'e' | b'E' if !has_exp => {
180 has_exp = true;
181 self.pos += 1;
182 if matches!(self.peek(), Some(b'+') | Some(b'-')) {
184 self.pos += 1;
185 }
186 }
187 _ => break,
188 }
189 }
190
191 let num_bytes = &self.input[start..self.pos];
192 let num_str = std::str::from_utf8(num_bytes)
193 .map_err(|_| DxError::InvalidNumber("Invalid UTF-8 in number".to_string()))?;
194
195 if has_dot || has_exp {
196 let val = num_str
197 .parse::<f64>()
198 .map_err(|_| DxError::InvalidNumber(num_str.to_string()))?;
199 Ok(Token::Float(val))
200 } else {
201 let val = num_str
202 .parse::<i64>()
203 .map_err(|_| DxError::InvalidNumber(num_str.to_string()))?;
204 Ok(Token::Int(val))
205 }
206 }
207
208 pub fn read_string_vacuum(&mut self, next_is_number: bool) -> &'a [u8] {
211 let start = self.pos;
212
213 if next_is_number {
214 while let Some(b) = self.peek() {
216 if b.is_ascii_digit() || b == b'-' || b == b'|' || b == b'\n' {
217 break;
218 }
219 self.pos += 1;
220 }
221 } else {
222 while let Some(b) = self.peek() {
225 if b == b'|' || b == b'\n' || b == b'#' {
226 break;
227 }
228 if (b == b'+' || b == b'-') && self.pos > start {
231 let prev = self.input[self.pos - 1];
232 if prev.is_ascii_whitespace() {
233 break;
234 }
235 }
236 self.pos += 1;
237 }
238 }
239
240 let mut end = self.pos;
242 while end > start && self.input[end - 1].is_ascii_whitespace() {
243 end -= 1;
244 }
245
246 &self.input[start..end]
247 }
248
249 pub fn read_string_validated(&mut self, next_is_number: bool) -> Result<&'a str> {
252 let start = self.pos;
253 let bytes = self.read_string_vacuum(next_is_number);
254 validate_string_input(bytes, start)
255 }
256
257 pub fn read_ident_validated(&mut self) -> Result<&'a str> {
259 let start = self.pos;
260 let bytes = self.read_ident();
261 validate_string_input(bytes, start)
262 }
263
264 pub fn next_token(&mut self) -> Result<Token<'a>> {
266 self.skip_whitespace();
267
268 let b = match self.peek() {
269 Some(b) => b,
270 None => return Ok(Token::Eof),
271 };
272
273 let token = match b {
275 b'\n' | b'\r' => {
276 self.pos += 1;
277 if b == b'\r' && self.peek() == Some(b'\n') {
278 self.pos += 1;
279 }
280 return Ok(Token::Newline);
281 }
282 b'#' => {
283 self.skip_line();
284 return self.next_token(); }
286 b':' => {
287 self.pos += 1;
288 Token::Colon
289 }
290 b'=' => {
291 self.pos += 1;
292 Token::Equals
293 }
294 b'^' => {
295 self.pos += 1;
296 Token::Caret
297 }
298 b'|' => {
299 self.pos += 1;
300 Token::Pipe
301 }
302 b'>' => {
303 self.pos += 1;
304 Token::Stream
305 }
306 b'_' => {
307 self.pos += 1;
308 Token::Ditto
309 }
310 b'~' => {
311 self.pos += 1;
312 Token::Null
313 }
314 b'+' => {
315 self.pos += 1;
316 Token::True
317 }
318 b'-' if !matches!(self.peek_n(1), Some(b'0'..=b'9')) => {
319 self.pos += 1;
320 Token::False
321 }
322 b'!' => {
323 self.pos += 1;
324 Token::Bang
325 }
326 b'?' => {
327 self.pos += 1;
328 Token::Void
329 }
330 b'$' => {
331 self.pos += 1;
332 Token::Dollar
333 }
334 b'@' => {
335 self.pos += 1;
336 Token::At
337 }
338 b'%' => {
339 self.pos += 1;
340 Token::Percent
341 }
342 b'.' => {
343 self.pos += 1;
344 Token::Dot
345 }
346 b'0'..=b'9' | b'-' => {
347 return self.read_number();
348 }
349 _ => {
350 let ident = self.read_ident();
351 Token::Ident(ident)
352 }
353 };
354
355 Ok(token)
356 }
357
358 pub fn peek_token(&self) -> Result<Token<'a>> {
360 let mut temp = Self {
361 input: self.input,
362 pos: self.pos,
363 };
364 temp.next_token()
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::*;
371
372 #[test]
373 fn test_tokenizer_basic() {
374 let input = b"id:123 name:Test active:+ score:9.5";
375 let mut tok = Tokenizer::new(input);
376
377 assert_eq!(tok.next_token().unwrap(), Token::Ident(b"id"));
378 assert_eq!(tok.next_token().unwrap(), Token::Colon);
379 assert_eq!(tok.next_token().unwrap(), Token::Int(123));
380 assert_eq!(tok.next_token().unwrap(), Token::Ident(b"name"));
381 assert_eq!(tok.next_token().unwrap(), Token::Colon);
382 }
383
384 #[test]
385 fn test_special_tokens() {
386 let input = b"_ ~ + - ^ | > = $ @ % !";
387 let mut tok = Tokenizer::new(input);
388
389 assert_eq!(tok.next_token().unwrap(), Token::Ditto);
390 assert_eq!(tok.next_token().unwrap(), Token::Null);
391 assert_eq!(tok.next_token().unwrap(), Token::True);
392 assert_eq!(tok.next_token().unwrap(), Token::False);
393 assert_eq!(tok.next_token().unwrap(), Token::Caret);
394 assert_eq!(tok.next_token().unwrap(), Token::Pipe);
395 assert_eq!(tok.next_token().unwrap(), Token::Stream);
396 assert_eq!(tok.next_token().unwrap(), Token::Equals);
397 }
398
399 #[test]
400 #[allow(clippy::approx_constant)] fn test_numbers() {
402 let input = b"123 -456 3.14 -2.5 1e10";
403 let mut tok = Tokenizer::new(input);
404
405 assert_eq!(tok.next_token().unwrap(), Token::Int(123));
406 assert_eq!(tok.next_token().unwrap(), Token::Int(-456));
407 assert_eq!(tok.next_token().unwrap(), Token::Float(3.14));
408 assert_eq!(tok.next_token().unwrap(), Token::Float(-2.5));
409 }
410}