use crate::token::{BracketKind, Token, TokenKind};
#[must_use]
pub fn text<'a>(input: &'a [u8], t: &Token) -> &'a [u8] {
&input[t.span()]
}
#[must_use]
pub fn lex(input: &[u8]) -> Vec<Token> {
lex_with_capacity(input, input.len() / TOKEN_BYTES_ESTIMATE)
}
pub(crate) const TOKEN_BYTES_ESTIMATE: usize = 2;
#[must_use]
pub fn lex_with_capacity(input: &[u8], cap: usize) -> Vec<Token> {
lex_with_blobs(input, &blob_runs(input), cap)
}
#[must_use]
pub fn blob_runs(input: &[u8]) -> Vec<(usize, usize)> {
crate::spectral::high_entropy_runs(
input,
crate::spectral::BLOB_ENTROPY_PCT,
crate::spectral::BLOB_MIN_LEN,
)
}
#[must_use]
pub fn blob_runs_parallel(input: &[u8]) -> Vec<(usize, usize)> {
crate::spectral::high_entropy_runs_parallel(
input,
crate::spectral::BLOB_ENTROPY_PCT,
crate::spectral::BLOB_MIN_LEN,
)
}
pub(crate) fn blob_runs_in_span(input: &[u8], a: usize, b: usize) -> Vec<(usize, usize)> {
crate::spectral::high_entropy_runs_in_span(
input,
a,
b,
crate::spectral::BLOB_ENTROPY_PCT,
crate::spectral::BLOB_MIN_LEN,
)
}
pub(crate) fn blob_runs_within(input: &[u8], lo: usize, hi: usize) -> Vec<(usize, usize)> {
crate::spectral::high_entropy_runs_within(
input,
lo,
hi,
crate::spectral::BLOB_ENTROPY_PCT,
crate::spectral::BLOB_MIN_LEN,
)
}
#[must_use]
pub fn lex_with_blobs(input: &[u8], blobs: &[(usize, usize)], cap: usize) -> Vec<Token> {
lex_with_shapes(input, blobs, &crate::custom::ShapeSet::new(), cap)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Seams {
pub open: Vec<usize>,
pub close: Vec<usize>,
}
pub(crate) trait TokenSink {
fn emit(&mut self, kind: TokenKind, start: usize, end: usize) -> usize;
fn mate(&mut self, open: usize, close: usize);
fn held(&self) -> usize;
}
impl TokenSink for Vec<Token> {
#[inline]
fn emit(&mut self, kind: TokenKind, start: usize, end: usize) -> usize {
let idx = self.len();
self.push(Token::new(kind, start, end));
idx
}
#[inline]
fn mate(&mut self, open: usize, close: usize) {
self[open].set_mate(Some(close));
self[close].set_mate(Some(open));
}
#[inline]
fn held(&self) -> usize {
self.len()
}
}
pub struct Significant {
base: usize,
pub kinds: Vec<u32>,
pub spans: Vec<(u32, u32)>,
}
impl Significant {
#[must_use]
pub fn with_base(base: usize, cap: usize) -> Self {
Significant { base, kinds: Vec::with_capacity(cap), spans: Vec::with_capacity(cap) }
}
pub fn reset(&mut self, base: usize) {
self.base = base;
self.kinds.clear();
self.spans.clear();
}
pub fn reserve(&mut self, cap: usize) {
self.kinds.reserve(cap);
self.spans.reserve(cap);
}
}
impl TokenSink for Significant {
#[inline]
fn emit(&mut self, kind: TokenKind, start: usize, end: usize) -> usize {
if kind == TokenKind::Whitespace {
return self.kinds.len();
}
let base = self.base;
let at = |offset: usize| {
u32::try_from(offset + base).expect("a byte offset within the stored width")
};
self.kinds.push(kind.code());
self.spans.push((at(start), at(end)));
self.kinds.len() - 1
}
#[inline]
fn mate(&mut self, _open: usize, _close: usize) {}
#[inline]
fn held(&self) -> usize {
self.kinds.len()
}
}
pub const NO_MATE: u32 = u32::MAX;
pub struct PairedSignificant {
pub parts: Significant,
pub mates: Vec<u32>,
}
impl PairedSignificant {
#[must_use]
pub fn with_base(base: usize, cap: usize) -> Self {
PairedSignificant {
parts: Significant::with_base(base, cap),
mates: Vec::with_capacity(cap),
}
}
pub fn reset(&mut self, base: usize) {
self.parts.reset(base);
self.mates.clear();
}
pub fn reserve(&mut self, cap: usize) {
self.parts.reserve(cap);
self.mates.reserve(cap);
}
}
impl TokenSink for PairedSignificant {
#[inline]
fn emit(&mut self, kind: TokenKind, start: usize, end: usize) -> usize {
let before = self.parts.kinds.len();
let idx = self.parts.emit(kind, start, end);
if self.parts.kinds.len() != before {
self.mates.push(NO_MATE);
}
idx
}
#[inline]
fn mate(&mut self, open: usize, close: usize) {
self.mates[open] = u32::try_from(close).expect("a token index within the stored width");
self.mates[close] = u32::try_from(open).expect("a token index within the stored width");
}
#[inline]
fn held(&self) -> usize {
self.parts.held()
}
}
#[must_use]
pub fn lex_chunk(input: &[u8], blobs: &[(usize, usize)], cap: usize) -> (Vec<Token>, Seams) {
let mut toks = Vec::with_capacity(cap);
let mut seams = Seams::default();
lex_inner(input, blobs, &crate::custom::ShapeSet::new(), &mut toks, &mut seams);
(toks, seams)
}
pub fn lex_chunk_into(
input: &[u8],
blobs: &[(usize, usize)],
toks: &mut Vec<Token>,
seams: &mut Seams,
) {
lex_inner(input, blobs, &crate::custom::ShapeSet::new(), toks, seams);
}
#[must_use]
pub fn lex_chunk_significant(
input: &[u8],
blobs: &[(usize, usize)],
base: usize,
cap: usize,
) -> Significant {
let mut out = Significant::with_base(base, cap);
lex_chunk_significant_into(input, blobs, &mut out);
out
}
pub fn lex_chunk_significant_into(input: &[u8], blobs: &[(usize, usize)], out: &mut Significant) {
lex_inner(input, blobs, &crate::custom::ShapeSet::new(), out, &mut Seams::default());
}
pub fn lex_chunk_paired_into(
input: &[u8],
blobs: &[(usize, usize)],
out: &mut PairedSignificant,
seams: &mut Seams,
) {
lex_inner(input, blobs, &crate::custom::ShapeSet::new(), out, seams);
}
#[must_use]
pub fn lex_with_shapes(
input: &[u8],
blobs: &[(usize, usize)],
shapes: &crate::custom::ShapeSet,
cap: usize,
) -> Vec<Token> {
let mut toks = Vec::with_capacity(cap.max(input.len() / TOKEN_BYTES_ESTIMATE));
lex_inner(input, blobs, shapes, &mut toks, &mut Seams::default());
if !shapes.pattern_kinds().is_empty() {
fuse_pattern_kinds(input, &mut toks, shapes);
}
toks
}
fn fuse_pattern_kinds(input: &[u8], toks: &mut Vec<Token>, shapes: &crate::custom::ShapeSet) {
let (context, plain): (Vec<&crate::custom::PatternKind>, Vec<&crate::custom::PatternKind>) =
shapes.pattern_kinds().iter().partition(|k| crate::library::is_context_kind(k.id));
if !context.is_empty() {
let union = crate::library::context_union(&context);
let spans = kind_spans(&union, input, toks)
.into_iter()
.filter_map(|s| {
let text = &input[s.start()..s.end()];
context
.iter()
.find(|k| k.guard.is_some_and(|g| g.accepts(text)))
.map(|k| (s, k.id))
})
.collect::<Vec<_>>();
fuse_spans(toks, &spans);
}
for kind in plain {
let spans = kind_spans(&kind.pattern, input, toks)
.into_iter()
.filter(|s| kind.guard.is_none_or(|g| g.accepts(&input[s.start()..s.end()])))
.map(|s| (s, kind.id))
.collect::<Vec<_>>();
fuse_spans(toks, &spans);
}
}
fn kind_spans(pattern: &crate::ast::Pattern, input: &[u8], toks: &[Token]) -> Vec<crate::engine::Span> {
let spans = match crate::nfa::scan_nfa_over(pattern, input, toks) {
Some(spans) => spans,
None => crate::engine::scan_tokens_from(pattern, input, toks, 0),
};
spans.into_iter().filter(|s| s.end() > s.start()).collect()
}
fn fuse_spans(toks: &mut Vec<Token>, spans: &[(crate::engine::Span, u8)]) {
if spans.is_empty() {
return;
}
let mut fused = Vec::with_capacity(toks.len());
let mut next = spans.iter().peekable();
let mut i = 0;
while i < toks.len() {
let t = toks[i];
match next.peek() {
Some((s, id)) if t.start() >= s.start() && t.end() <= s.end() => {
fused.push(Token::new(TokenKind::Custom(*id), s.start(), s.end()));
while i < toks.len() && toks[i].end() <= s.end() {
i += 1;
}
next.next();
}
_ => {
fused.push(t);
i += 1;
}
}
}
*toks = fused;
pair_brackets(toks);
}
fn pair_brackets(toks: &mut [Token]) {
let mut open: Vec<(BracketKind, usize)> = Vec::new();
for i in 0..toks.len() {
match toks[i].kind {
TokenKind::Open(bk) => {
toks[i].set_mate(None);
open.push((bk, i));
}
TokenKind::Close(bk) => {
toks[i].set_mate(None);
if let Some(&(open_bk, open_at)) = open.last()
&& open_bk == bk
{
open.pop();
toks[open_at].set_mate(Some(i));
toks[i].set_mate(Some(open_at));
}
}
_ => {}
}
}
}
pub fn lex_into(input: &[u8], out: &mut Vec<Token>) {
out.clear();
out.reserve(input.len() / TOKEN_BYTES_ESTIMATE);
lex_inner(input, &blob_runs(input), &crate::custom::ShapeSet::new(), out, &mut Seams::default());
}
pub(crate) fn lex_run_into(input: &[u8], out: &mut Vec<Token>) {
out.clear();
lex_inner(input, &[], &crate::custom::ShapeSet::new(), out, &mut Seams::default());
}
fn lex_inner<S: TokenSink>(
input: &[u8],
blobs: &[(usize, usize)],
shapes: &crate::custom::ShapeSet,
toks: &mut S,
seams: &mut Seams,
) {
if crate::trace::keeping() {
let held = toks.held();
let began = std::time::Instant::now();
lex_counted(input, blobs, shapes, toks, seams);
crate::trace::lexed(toks.held().saturating_sub(held), began.elapsed());
return;
}
lex_counted(input, blobs, shapes, toks, seams);
}
fn lex_counted<S: TokenSink>(
input: &[u8],
blobs: &[(usize, usize)],
shapes: &crate::custom::ShapeSet,
toks: &mut S,
seams: &mut Seams,
) {
let n = input.len();
let Seams { open: open_stack, close: bare_closes } = seams;
let mut open_kinds: Vec<BracketKind> = Vec::new();
let mut i = 0;
let mut bi = 0usize;
while i < n {
while bi < blobs.len() && blobs[bi].1 <= i {
bi += 1;
}
if bi < blobs.len() && i >= blobs[bi].0 {
let end = blobs[bi].1;
let kind = if try_base64(input, i) == Some(end) {
TokenKind::Base64
} else {
TokenKind::Other
};
toks.emit(kind, i, end);
i = end;
bi += 1;
continue;
}
if !shapes.is_empty()
&& let Some((id, end)) =
shapes.longest_at(input, i, crate::custom::Precedence::Before)
{
toks.emit(TokenKind::Custom(id), i, end);
i = end;
continue;
}
let b = input[i];
if b.is_ascii_whitespace() {
let start = i;
i += if input.get(i + 1).is_some_and(u8::is_ascii_whitespace) {
crate::byte_simd::space_run(&input[i..])
} else {
1
};
toks.emit(TokenKind::Whitespace, start, i);
} else if b == b'"'
&& let Some(end) = double_quoted_end(input, i)
{
toks.emit(TokenKind::Quoted, i, end);
i = end;
} else if b == b'\'' && char_literal_end(input, i).is_some() {
let start = i;
i = char_literal_end(input, i).unwrap_or(i + 1);
toks.emit(TokenKind::Quoted, start, i);
} else if b == b'\''
&& let Some(end) = single_quoted_end(input, i)
{
toks.emit(TokenKind::Quoted, i, end);
i = end;
} else {
let (typed, word_end) = if typed_token_possible(b) {
try_typed_token(input, i)
} else {
(None, i)
};
if let Some((kind, end)) = typed {
toks.emit(kind, i, end);
i = end;
} else if !shapes.is_empty()
&& let Some((id, end)) =
shapes.longest_at(input, i, crate::custom::Precedence::After)
{
toks.emit(TokenKind::Custom(id), i, end);
i = end;
} else if b.is_ascii_digit() {
let start = i;
i = scan_number(input, i);
toks.emit(TokenKind::Number, start, i);
} else if b == b'_' || b.is_ascii_alphabetic() || utf8_letter_at(input, i).is_some() {
let start = i;
i = if word_end > i { word_end } else { i + crate::byte_simd::word_run(&input[i..]) };
while let Some(len) = utf8_letter_at(input, i) {
i += len;
i += crate::byte_simd::word_run(&input[i..]);
}
toks.emit(TokenKind::Word, start, i);
} else if let Some(bk) = open_bracket(b) {
let idx = toks.emit(TokenKind::Open(bk), i, i + 1);
open_stack.push(idx);
open_kinds.push(bk);
i += 1;
} else if let Some(bk) = close_bracket(b) {
let idx = toks.emit(TokenKind::Close(bk), i, i + 1);
match open_kinds.last() {
Some(open_bk) if *open_bk == bk => {
open_kinds.pop();
let open_idx = open_stack.pop().expect("one index per open kind");
toks.mate(open_idx, idx);
}
Some(_) => {}
None => bare_closes.push(idx),
}
i += 1;
} else if b == 0xE2
&& input.get(i + 1..i + 3) == Some(&[0x88, 0x92])
&& let Some(end) = crate::quantity::recognize(input, i)
{
toks.emit(TokenKind::Quantity, i, end);
i = end;
} else {
let (c, len) = utf8_char_at(input, i);
let kind = if len > 1 && c.is_whitespace() {
TokenKind::Whitespace
} else {
TokenKind::Punct
};
toks.emit(kind, i, i + len);
i += len;
}
}
}
}
pub(crate) fn utf8_char_at(input: &[u8], i: usize) -> (char, usize) {
let b0 = input[i];
if b0 < 0x80 {
return (char::from(b0), 1);
}
let len = match b0 {
0xC2..=0xDF => 2,
0xE0..=0xEF => 3,
0xF0..=0xF4 => 4,
_ => return ('\u{FFFD}', 1),
};
if i + len <= input.len()
&& let Ok(s) = std::str::from_utf8(&input[i..i + len])
&& let Some(c) = s.chars().next()
{
return (c, len);
}
('\u{FFFD}', 1)
}
pub(crate) fn utf8_letter_at(input: &[u8], i: usize) -> Option<usize> {
if i >= input.len() || input[i] < 0x80 {
return None;
}
let (c, len) = utf8_char_at(input, i);
(len > 1 && c.is_alphabetic()).then_some(len)
}
pub(crate) fn char_literal_end(input: &[u8], i: usize) -> Option<usize> {
if input.get(i) != Some(&b'\'') {
return None;
}
if input.get(i + 1) == Some(&b'\\') {
return (input.get(i + 3) == Some(&b'\'')).then_some(i + 4);
}
if input.get(i + 1).is_some_and(|&c| c != b'\'' && c != b'\\')
&& input.get(i + 2) == Some(&b'\'')
{
return Some(i + 3);
}
None
}
const fn is_word_byte(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_'
}
const fn joins_word(c: u8) -> bool {
is_word_byte(c) || c >= 0x80
}
fn escape_width(input: &[u8], j: usize) -> usize {
if input.get(j + 1) == Some(&b'\r') && input.get(j + 2) == Some(&b'\n') { 3 } else { 2 }
}
pub(crate) fn backslash_before_newline(input: &[u8], nl: usize) -> bool {
let end = if nl > 0 && input[nl - 1] == b'\r' { nl - 1 } else { nl };
end > 0 && input[end - 1] == b'\\'
}
pub(crate) fn double_quoted_end(input: &[u8], i: usize) -> Option<usize> {
if input.get(i) != Some(&b'"') {
return None;
}
let mut j = i + 1;
while j < input.len() {
match input[j] {
b'\\' => j += escape_width(input, j),
b'"' => return Some(j + 1),
b'\n' => return None,
_ => j += 1,
}
}
None
}
pub(crate) fn double_quoted_end_at(
input: &[u8],
i: usize,
closes: &mut crate::byte_simd::CloseOrNewlinePositions<'_>,
) -> Option<usize> {
if input.get(i) != Some(&b'"') {
return None;
}
let mut from = i + 1;
loop {
let q = closes.next_at_or_after(from)?;
let at = if input[q] == b'\n' && q > i + 1 && input[q - 1] == b'\r' { q - 1 } else { q };
let mut run = 0;
while at - run > i + 1 && input[at - run - 1] == b'\\' {
run += 1;
}
if run % 2 == 0 {
return (input[q] == b'"').then_some(q + 1);
}
from = q + 1;
}
}
pub(crate) fn single_quoted_end(input: &[u8], i: usize) -> Option<usize> {
if input.get(i) != Some(&b'\'') || (i > 0 && joins_word(input[i - 1])) {
return None;
}
if (i > 0 && input[i - 1] == b'\'') || input.get(i + 1) == Some(&b'\'') {
return None;
}
if input.get(i + 1).is_some_and(u8::is_ascii_lowercase)
&& input.get(i + 2).is_none_or(|&c| c.is_ascii_whitespace())
{
return None;
}
let mut j = i + 1;
while j < input.len() {
match input[j] {
b'\\' => j += escape_width(input, j),
b'\n' => return None,
b'\'' => return (!input.get(j + 1).is_some_and(|&c| joins_word(c))).then_some(j + 1),
_ => j += 1,
}
}
None
}
fn scan_number(input: &[u8], mut i: usize) -> usize {
let n = input.len();
i += crate::byte_simd::digit_run(&input[i..]);
if i + 1 < n && input[i] == b'.' && input[i + 1].is_ascii_digit() {
i += 1;
i += crate::byte_simd::digit_run(&input[i..]);
}
i
}
fn open_bracket(b: u8) -> Option<BracketKind> {
match b {
b'(' => Some(BracketKind::Paren),
b'[' => Some(BracketKind::Square),
b'{' => Some(BracketKind::Brace),
_ => None,
}
}
fn close_bracket(b: u8) -> Option<BracketKind> {
match b {
b')' => Some(BracketKind::Paren),
b']' => Some(BracketKind::Square),
b'}' => Some(BracketKind::Brace),
_ => None,
}
}
pub(crate) fn punct_kind(b: u8) -> TokenKind {
if let Some(bk) = open_bracket(b) {
TokenKind::Open(bk)
} else if let Some(bk) = close_bracket(b) {
TokenKind::Close(bk)
} else {
TokenKind::Punct
}
}
const fn typed_token_possible(b: u8) -> bool {
b.is_ascii_alphanumeric()
|| matches!(b, b'+' | b'$' | b'#' | b'/' | b'.' | b'-' | b':' | b'_')
}
pub(crate) const TYPED_WORD_RUN: usize = 14;
struct RunFacts {
word_end: usize,
digit_end: usize,
}
impl RunFacts {
fn at(input: &[u8], i: usize) -> Self {
let word_end = i + crate::byte_simd::word_run(&input[i..]);
let mut digit_end = i;
while digit_end < word_end && input[digit_end].is_ascii_digit() {
digit_end += 1;
}
Self { word_end, digit_end }
}
}
#[inline(always)]
fn try_typed_token(input: &[u8], i: usize) -> (Option<(TokenKind, usize)>, usize) {
let facts = RunFacts::at(input, i);
let b = input[i];
let run = facts.word_end - i;
let after = input.get(facts.word_end).copied();
let joined = matches!(after, Some(b':' | b'+' | b'.' | b'-' | b'@' | b'%' | b'/'));
let plain_word = b.is_ascii_alphabetic()
&& !joined
&& run < TYPED_WORD_RUN
&& !(b == b'e' && input[i..].starts_with(b"eyJ"))
&& !(run == 3 && after == Some(b' ') && syslog_head(input, i));
let plain_number = b.is_ascii_digit()
&& !joined
&& facts.digit_end == facts.word_end
&& run < 13
&& !(run == 4 && after == Some(b' '))
&& !after.is_some_and(|c| c >= 0x80)
&& !(after == Some(b' ')
&& crate::quantity::opens_a_symbol(input.get(facts.word_end + 1).copied()));
if plain_word || plain_number {
return (None, facts.word_end);
}
(try_typed_token_chain(input, i, Some(&facts)), facts.word_end)
}
#[inline(never)]
fn try_typed_token_chain(
input: &[u8],
i: usize,
facts: Option<&RunFacts>,
) -> Option<(TokenKind, usize)> {
let b = input[i];
let digit = b.is_ascii_digit();
let alpha = b.is_ascii_alphabetic();
let hex = b.is_ascii_hexdigit();
let gate = |ok: &dyn Fn(&RunFacts) -> bool| facts.is_none_or(ok);
let after_word = |f: &RunFacts| input.get(f.word_end).copied();
let after_digits = |f: &RunFacts| input.get(f.digit_end).copied();
if alpha
&& gate(&|f| matches!(after_word(f), Some(b':' | b'+' | b'.' | b'-')))
&& let Some(end) = try_url(input, i)
{
return Some((TokenKind::Url, end));
}
if b == b'e' && let Some(end) = try_jwt(input, i) {
return Some((TokenKind::Jwt, end));
}
if b == b'+' && let Some(end) = try_phone(input, i) {
return Some((TokenKind::Phone, end));
}
if digit
&& gate(&|f| {
(f.word_end - i == 3 || (f.word_end - i == 1 && b == b'1'))
&& after_word(f) == Some(b'-')
})
&& let Some(end) = try_nanp(input, i)
{
return Some((TokenKind::Phone, end));
}
if hex
&& gate(&|f| f.word_end - i == 8 && after_word(f) == Some(b'-'))
&& let Some(end) = try_uuid(input, i)
{
return Some((TokenKind::Uuid, end));
}
if hex
&& gate(&|f| f.word_end - i == 2 && matches!(after_word(f), Some(b':' | b'-')))
&& let Some(end) = try_mac(input, i)
{
return Some((TokenKind::Mac, end));
}
if hex && gate(&|f| matches!(f.word_end - i, 32 | 40 | 64)) && let Some(end) = try_hash(input, i) {
return Some((TokenKind::HashDigest, end));
}
let octet_then_dot = |f: &RunFacts| f.word_end - i <= 3 && after_word(f) == Some(b'.');
if digit && gate(&octet_then_dot) && let Some(end) = try_cidr(input, i) {
return Some((TokenKind::Cidr, end));
}
if (alpha || digit)
&& gate(&|f| matches!(after_word(f), Some(b'@' | b'.' | b'%' | b'+' | b'-')))
&& let Some(end) = try_email(input, i)
{
return Some((TokenKind::Email, end));
}
if (hex || b == b':')
&& gate(&|f| b == b':' || after_word(f) == Some(b':'))
&& let Some(end) = try_ipv6(input, i)
{
return Some((TokenKind::Ip, end));
}
if digit && gate(&octet_then_dot) && let Some(end) = try_ipv4(input, i) {
return Some((TokenKind::Ip, end));
}
if digit
&& gate(&|f| {
f.word_end - i >= 13
|| (f.word_end - i == 4 && matches!(after_word(f), Some(b' ' | b'-')))
})
&& let Some(end) = try_creditcard(input, i)
{
return Some((TokenKind::CreditCard, end));
}
if (digit || b == b'-')
&& gate(&|f| b == b'-' || after_digits(f) == Some(b'.'))
&& let Some(end) = try_geo(input, i)
{
return Some((TokenKind::Geo, end));
}
if b == b'$' && let Some(end) = try_money(input, i) {
return Some((TokenKind::Money, end));
}
if (digit || b == b'v' || b == b'V')
&& gate(&|f| after_word(f) == Some(b'.'))
&& let Some(end) = try_semver(input, i)
{
return Some((TokenKind::Version, end));
}
let digits_then_unit = |f: &RunFacts| f.digit_end < f.word_end || after_digits(f) == Some(b'.');
if digit && gate(&digits_then_unit) && let Some(end) = try_bytesize(input, i) {
return Some((TokenKind::ByteSize, end));
}
if digit
&& gate(&|f| matches!(after_digits(f), Some(b'.' | b'%')))
&& let Some(end) = try_percent(input, i)
{
return Some((TokenKind::Percent, end));
}
if digit && gate(&digits_then_unit) && let Some(end) = try_duration(input, i) {
return Some((TokenKind::Duration, end));
}
if (digit || b == b'-' || b == b'+')
&& gate(&|f| {
!digit
|| matches!(after_digits(f), Some(c) if c.is_ascii_alphabetic() || c >= 0x80 || matches!(c, b'%' | b'.' | b' '))
})
&& let Some(end) = crate::quantity::recognize(input, i)
{
return Some((TokenKind::Quantity, end));
}
if b == b'#' && let Some(end) = try_hexcolor(input, i) {
return Some((TokenKind::HexColor, end));
}
if (alpha || b == b'/' || b == b'.' || b == b'~')
&& gate(&|f| !alpha || (f.word_end - i == 1 && after_word(f) == Some(b':')))
&& let Some(end) = try_path(input, i)
{
return Some((TokenKind::Path, end));
}
if gate(&|f| f.word_end - i >= TYPED_WORD_RUN || matches!(after_word(f), Some(b'+' | b'/')))
&& let Some(end) = try_base64(input, i)
{
return Some((TokenKind::Base64, end));
}
if gate(&|f| {
(digit && matches!(after_word(f), Some(b'-' | b':' | b'/')))
|| (alpha && f.word_end - i == 3 && after_word(f) == Some(b' '))
}) && let Some(end) = try_timestamp(input, i)
{
return Some((TokenKind::Timestamp, end));
}
None
}
fn try_jwt(input: &[u8], i: usize) -> Option<usize> {
if !input[i..].starts_with(b"eyJ") {
return None;
}
let is_b64url = |b: u8| b.is_ascii_alphanumeric() || b == b'-' || b == b'_';
let seg = |mut j: usize| -> Option<usize> {
let start = j;
while j < input.len() && is_b64url(input[j]) {
j += 1;
}
(j > start).then_some(j)
};
let a = seg(i)?;
if input.get(a) != Some(&b'.') {
return None;
}
let b = seg(a + 1)?;
if input.get(b) != Some(&b'.') {
return None;
}
seg(b + 1)
}
const CARD_LAYOUTS: [&[usize]; 4] = [&[4, 4, 4, 4, 3], &[4, 4, 4, 4], &[4, 6, 5], &[4, 6, 4]];
const DIGITS_MAX: usize = 19;
struct Digits {
buf: [u8; DIGITS_MAX],
len: usize,
}
impl Digits {
fn as_slice(&self) -> &[u8] {
&self.buf[..self.len]
}
}
fn fixed_groups(input: &[u8], i: usize, widths: &[usize], sep: u8) -> Option<(Digits, usize)> {
assert!(
widths.iter().sum::<usize>() <= DIGITS_MAX,
"a fixed layout holds at most {} digits",
DIGITS_MAX
);
let mut digits = Digits { buf: [0; DIGITS_MAX], len: 0 };
let mut j = i;
for (k, &len) in widths.iter().enumerate() {
if k > 0 {
if input.get(j) != Some(&sep) {
return None;
}
j += 1;
}
let start = j;
while j - start < len && input.get(j).is_some_and(u8::is_ascii_digit) {
digits.buf[digits.len] = input[j] - b'0';
digits.len += 1;
j += 1;
}
if j - start != len || input.get(j).is_some_and(u8::is_ascii_digit) {
return None;
}
}
Some((digits, j))
}
struct DigitRun {
digits: Vec<u8>,
groups: Vec<usize>,
seps: Vec<u8>,
end: usize,
}
fn digit_run(input: &[u8], i: usize) -> Option<DigitRun> {
if !input.get(i).is_some_and(u8::is_ascii_digit) {
return None;
}
let mut run = DigitRun { digits: Vec::new(), groups: Vec::new(), seps: Vec::new(), end: i };
loop {
let start = run.end;
while run.end < input.len() && input[run.end].is_ascii_digit() {
run.digits.push(input[run.end] - b'0');
run.end += 1;
}
run.groups.push(run.end - start);
let Some(&c) = input.get(run.end) else { break };
if !matches!(c, b' ' | b'-' | b'.')
|| !input.get(run.end + 1).is_some_and(u8::is_ascii_digit)
{
break;
}
run.seps.push(c);
run.end += 1;
}
Some(run)
}
pub(crate) fn country_code_len(digits: &[u8]) -> Option<usize> {
const TWO_DIGIT: [u8; 44] = [
20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 51, 52, 53, 54,
55, 56, 57, 58, 60, 61, 62, 63, 64, 65, 66, 81, 82, 84, 86, 90, 91, 92, 93, 94, 95, 98,
];
match digits {
[0, ..] => None,
[1, ..] | [7, ..] => Some(1),
[a, b, ..] if TWO_DIGIT.contains(&(*a * 10 + *b)) => Some(2),
[_, _, _, ..] => Some(3),
_ => None,
}
}
fn try_creditcard(input: &[u8], i: usize) -> Option<usize> {
let mut first = i;
while first < input.len() && input[first].is_ascii_digit() {
first += 1;
}
let ends_cleanly = |end: usize| !input.get(end).is_some_and(|b| b.is_ascii_alphabetic());
if (13..=19).contains(&(first - i)) {
let mut digits = Digits { buf: [0; DIGITS_MAX], len: first - i };
for (d, &b) in digits.buf.iter_mut().zip(&input[i..first]) {
*d = b - b'0';
}
return (ends_cleanly(first) && luhn_ok(digits.as_slice())).then_some(first);
}
for layout in CARD_LAYOUTS {
for &sep in b" -" {
if let Some((digits, end)) = fixed_groups(input, i, layout, sep)
&& ends_cleanly(end)
&& luhn_ok(digits.as_slice())
{
return Some(end);
}
}
}
None
}
fn luhn_ok(digits: &[u8]) -> bool {
crate::checksum::luhn_values(digits)
}
fn try_base64(input: &[u8], i: usize) -> Option<usize> {
let mut j = i;
while j < input.len() && (input[j].is_ascii_alphanumeric() || matches!(input[j], b'+' | b'/')) {
j += 1;
}
let body = j;
let mut pad = 0;
while j < input.len() && input[j] == b'=' && pad < 2 {
j += 1;
pad += 1;
}
let len = j - i;
if len < 16 || !len.is_multiple_of(4) {
return None;
}
if input.get(j).is_some_and(|&b| b.is_ascii_alphanumeric() || matches!(b, b'+' | b'/' | b'=')) {
return None;
}
let s = &input[i..body];
let special = pad > 0 || s.iter().any(|&b| matches!(b, b'+' | b'/'));
let diverse = s.iter().any(u8::is_ascii_digit)
&& s.iter().any(u8::is_ascii_uppercase)
&& s.iter().any(u8::is_ascii_lowercase);
(special || diverse).then_some(j)
}
const GEO_MIN_FRACTION: usize = 4;
fn try_geo(input: &[u8], i: usize) -> Option<usize> {
let before = input[..i].strip_suffix(b" ").unwrap_or(&input[..i]);
if before.ends_with(b",") {
return None;
}
let (lat, lat_frac, a) = signed_decimal(input, i)?;
let mut j = a;
if input.get(j) != Some(&b',') {
return None;
}
j += 1;
if input.get(j) == Some(&b' ') {
j += 1;
}
let (long, long_frac, b) = signed_decimal(input, j)?;
if input.get(b).is_some_and(|&c| c.is_ascii_alphanumeric()) {
return None;
}
let mut k = b;
if input.get(k) == Some(&b',') {
k += 1;
if input.get(k) == Some(&b' ') {
k += 1;
}
if matches!(input.get(k), Some(b'-' | b'+')) || input.get(k).is_some_and(u8::is_ascii_digit)
{
return None;
}
}
if lat_frac < GEO_MIN_FRACTION || long_frac < GEO_MIN_FRACTION {
return None;
}
((-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&long)).then_some(b)
}
fn signed_decimal(input: &[u8], i: usize) -> Option<(f64, usize, usize)> {
let mut j = i;
let negative = input.get(j) == Some(&b'-');
if matches!(input.get(j), Some(b'-' | b'+')) {
j += 1;
}
let int_start = j;
let mut value = 0.0f64;
while j < input.len() && input[j].is_ascii_digit() {
value = value * 10.0 + f64::from(input[j] - b'0');
j += 1;
}
if j == int_start || input.get(j) != Some(&b'.') {
return None;
}
j += 1;
let frac_start = j;
let mut place = 1.0f64;
while j < input.len() && input[j].is_ascii_digit() {
place /= 10.0;
value += f64::from(input[j] - b'0') * place;
j += 1;
}
if j == frac_start {
return None;
}
Some((if negative { -value } else { value }, j - frac_start, j))
}
fn try_phone(input: &[u8], i: usize) -> Option<usize> {
if input.get(i) != Some(&b'+') {
return None;
}
let run = digit_run(input, i + 1)?;
if !(7..=15).contains(&run.digits.len()) {
return None;
}
if run.seps.contains(&b'.') && !run.seps.iter().all(|s| *s == b'.') {
return None;
}
let code = country_code_len(&run.digits)?;
if run.groups[0] < code || (code == 1 && run.digits.len() != 11) {
return None;
}
Some(run.end)
}
fn try_nanp(input: &[u8], i: usize) -> Option<usize> {
let (digits, end) = fixed_groups(input, i, &[3, 3, 4], b'-')
.or_else(|| fixed_groups(input, i, &[1, 3, 3, 4], b'-').filter(|(d, _)| d.as_slice()[0] == 1))?;
let plan = &digits.as_slice()[digits.len - 10..];
let assignable = |g: &[u8]| (2..=9).contains(&g[0]) && !(g[1] == 1 && g[2] == 1);
if !assignable(&plan[..3]) || !assignable(&plan[3..6]) {
return None;
}
if input.get(end).is_some_and(|b| b.is_ascii_alphabetic()) {
return None;
}
Some(end)
}
fn try_url(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
if j >= n || !input[j].is_ascii_alphabetic() {
return None;
}
j += 1;
while j < n
&& (input[j].is_ascii_alphanumeric() || matches!(input[j], b'+' | b'.' | b'-'))
{
j += 1;
}
if j + 3 > n || &input[j..j + 3] != b"://" {
return None;
}
j += 3;
let body = j;
while j < n && !input[j].is_ascii_whitespace() && !matches!(input[j], b'"' | b'<' | b'>') {
j += 1;
}
if j == body { None } else { Some(j) }
}
fn try_email(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let local = j;
while j < n
&& (input[j].is_ascii_alphanumeric() || matches!(input[j], b'.' | b'_' | b'%' | b'+' | b'-'))
{
j += 1;
}
if j == local || j >= n || input[j] != b'@' {
return None;
}
j += 1;
let domain = j;
while j < n && (input[j].is_ascii_alphanumeric() || matches!(input[j], b'.' | b'-')) {
j += 1;
}
let dom = &input[domain..j];
let last_dot = dom.iter().rposition(|&c| c == b'.')?;
let tld = &dom[last_dot + 1..];
if tld.len() < 2 || !tld.iter().all(u8::is_ascii_alphabetic) {
return None;
}
Some(j)
}
fn try_ipv4(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
for group in 0..4 {
let start = j;
let mut count = 0;
while j < n && input[j].is_ascii_digit() && count < 3 {
j += 1;
count += 1;
}
if j == start {
return None;
}
let val = input[start..j].iter().fold(0u32, |a, &c| a * 10 + u32::from(c - b'0'));
if val > 255 {
return None;
}
if group < 3 {
if j >= n || input[j] != b'.' {
return None;
}
j += 1;
}
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'.') {
return None;
}
Some(j)
}
fn try_ipv6(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let mut groups = 0usize;
let mut colons = 0usize;
let mut hex_letter = false;
let mut double_colon = false;
loop {
let start = j;
while j < n && input[j].is_ascii_hexdigit() {
hex_letter |= input[j].is_ascii_alphabetic();
j += 1;
}
if j - start > 4 {
return None;
}
if j > start {
groups += 1;
}
if j + 1 < n && input[j] == b':' && input[j + 1] == b':' {
if double_colon {
return None;
}
double_colon = true;
colons += 2;
j += 2;
continue;
}
if j > start && j + 1 < n && input[j] == b':' && input[j + 1].is_ascii_hexdigit() {
colons += 1;
j += 1;
continue;
}
break;
}
let complete = if double_colon { (1..=7).contains(&groups) } else { groups == 8 };
if !complete || colons < 2 || (!hex_letter && !double_colon) {
return None;
}
if input.get(j).is_some_and(|&c| c.is_ascii_alphanumeric() || c == b'_') {
return None;
}
Some(j)
}
fn try_timestamp(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
if let Some(end) = try_syslog(input, i) {
return Some(end);
}
if let Some(end) = try_apache(input, i) {
return Some(end);
}
if let Some(mut j) = try_date(input, i) {
if j < n && matches!(input[j], b'T' | b't' | b' ')
&& let Some(jt) = try_time(input, j + 1, false)
{
j = jt;
}
return Some(j);
}
try_time(input, i, false)
}
fn syslog_head(input: &[u8], i: usize) -> bool {
crate::typed::month_abbrev(&input[i..]).is_some()
&& input.get(i + 3) == Some(&b' ')
&& (input.get(i + 4).is_some_and(u8::is_ascii_digit)
|| (input.get(i + 4) == Some(&b' ') && input.get(i + 5).is_some_and(u8::is_ascii_digit)))
}
fn try_syslog(input: &[u8], i: usize) -> Option<usize> {
if !syslog_head(input, i) {
return None;
}
let mut j = i + 4;
if input.get(j) == Some(&b' ') {
j += 1;
}
let day_start = j;
let mut day = 0u32;
while j < input.len() && input[j].is_ascii_digit() && j - day_start < 2 {
day = day * 10 + u32::from(input[j] - b'0');
j += 1;
}
if j == day_start || day == 0 || day > 31 || input.get(j) != Some(&b' ') {
return None;
}
let end = try_time(input, j + 1, false)?;
(end - (j + 1) >= 8).then_some(end)
}
fn try_apache(input: &[u8], i: usize) -> Option<usize> {
let digit_at = |k: usize| input.get(k).is_some_and(u8::is_ascii_digit);
if !(digit_at(i) && digit_at(i + 1) && input.get(i + 2) == Some(&b'/')) {
return None;
}
crate::typed::month_abbrev(input.get(i + 3..)?)?;
if input.get(i + 6) != Some(&b'/') || !(i + 7..i + 11).all(digit_at) {
return None;
}
if input.get(i + 11) != Some(&b':') {
return None;
}
let end = try_time(input, i + 12, true)?;
(end - (i + 12) >= 8).then_some(end)
}
fn try_date(input: &[u8], i: usize) -> Option<usize> {
try_iso_date(input, i).or_else(|| crate::typed::slash_date(input, i).map(|d| d.end))
}
fn try_iso_date(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let digits = |count: usize, j: &mut usize| -> bool {
for _ in 0..count {
if *j < n && input[*j].is_ascii_digit() {
*j += 1;
} else {
return false;
}
}
true
};
if !digits(4, &mut j) {
return None;
}
if j >= n || input[j] != b'-' {
return None;
}
j += 1;
if !digits(2, &mut j) {
return None;
}
if j >= n || input[j] != b'-' {
return None;
}
j += 1;
if !digits(2, &mut j) {
return None;
}
if j < n && input[j].is_ascii_digit() {
return None;
}
Some(j)
}
fn try_uuid(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
const GROUPS: [usize; 5] = [8, 4, 4, 4, 12];
let mut j = i;
for (g, &len) in GROUPS.iter().enumerate() {
for _ in 0..len {
if j < n && input[j].is_ascii_hexdigit() {
j += 1;
} else {
return None;
}
}
if g < GROUPS.len() - 1 {
if j >= n || input[j] != b'-' {
return None;
}
j += 1;
}
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'-') {
return None;
}
Some(j)
}
fn try_mac(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let sep = *input.get(i + 2)?;
if sep != b':' && sep != b'-' {
return None;
}
let mut j = i;
for group in 0..6 {
if j + 2 > n || !input[j].is_ascii_hexdigit() || !input[j + 1].is_ascii_hexdigit() {
return None;
}
j += 2;
if group < 5 {
if j >= n || input[j] != sep {
return None;
}
j += 1;
}
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b':' || input[j] == b'-') {
return None;
}
Some(j)
}
fn try_cidr(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let after_ip = try_ipv4(input, i)?;
if after_ip >= n || input[after_ip] != b'/' {
return None;
}
let start = after_ip + 1;
let mut j = start;
while j < n && input[j].is_ascii_digit() && j - start < 2 {
j += 1;
}
if j == start {
return None;
}
let prefix = input[start..j].iter().fold(0u32, |a, &c| a * 10 + u32::from(c - b'0'));
if prefix > 32 {
return None;
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'.') {
return None;
}
Some(j)
}
fn try_semver(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
if matches!(input.get(j), Some(b'v' | b'V')) && input.get(j + 1).is_some_and(u8::is_ascii_digit) {
j += 1;
}
for part in 0..3 {
let start = j;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
if j == start {
return None;
}
if part < 2 {
if j >= n || input[j] != b'.' {
return None;
}
j += 1;
}
}
if j < n && input[j] == b'.' && input.get(j + 1).is_some_and(u8::is_ascii_digit) {
return None;
}
let ident = |j: &mut usize| {
while *j < n && (input[*j].is_ascii_alphanumeric() || matches!(input[*j], b'.' | b'-')) {
*j += 1;
}
};
if j + 1 < n && input[j] == b'-' && input[j + 1].is_ascii_alphanumeric() {
j += 1;
ident(&mut j);
}
if j + 1 < n && input[j] == b'+' && input[j + 1].is_ascii_alphanumeric() {
j += 1;
ident(&mut j);
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'_') {
return None;
}
Some(j)
}
fn try_hash(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let mut has_letter = false;
while j < n && input[j].is_ascii_hexdigit() {
has_letter |= input[j].is_ascii_alphabetic();
j += 1;
}
let len = j - i;
if !matches!(len, 32 | 40 | 64) || !has_letter {
return None;
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'_') {
return None;
}
Some(j)
}
fn try_money(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
if input.get(i) != Some(&b'$') || !input.get(i + 1).is_some_and(u8::is_ascii_digit) {
return None;
}
let mut j = i + 1;
while j < n && (input[j].is_ascii_digit() || input[j] == b',') {
j += 1;
}
if input[j - 1] == b',' {
j -= 1;
}
if j + 1 < n && input[j] == b'.' && input[j + 1].is_ascii_digit() {
j += 1;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
}
if j < n && input[j].is_ascii_alphanumeric() {
return None;
}
Some(j)
}
fn try_percent(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let start = j;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
if j == start {
return None;
}
if j + 1 < n && input[j] == b'.' && input[j + 1].is_ascii_digit() {
j += 1;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
}
(j < n && input[j] == b'%').then_some(j + 1)
}
fn try_bytesize(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let start = j;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
if j == start {
return None;
}
if j + 1 < n && input[j] == b'.' && input[j + 1].is_ascii_digit() {
j += 1;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
}
let mag = j;
if j < n && matches!(input[j] | 0x20, b'k' | b'm' | b'g' | b't' | b'p' | b'e') {
j += 1;
}
if j > mag && j < n && input[j] == b'i' {
j += 1;
}
if j >= n || (input[j] | 0x20) != b'b' {
return None;
}
j += 1;
if j < n && input[j].is_ascii_alphanumeric() {
return None;
}
Some(j)
}
fn try_duration(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
let mut j = i;
let mut segments = 0usize;
loop {
let num_start = j;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
if j == num_start {
break;
}
if j + 1 < n && input[j] == b'.' && input[j + 1].is_ascii_digit() {
j += 1;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
}
let two = input.get(j..j + 2).unwrap_or(&[]);
if matches!(two, b"ns" | b"us" | b"ms") {
j += 2;
} else if j < n && matches!(input[j], b's' | b'm' | b'h' | b'd' | b'w' | b'y') {
j += 1;
} else {
j = num_start;
break;
}
segments += 1;
if j < n && input[j].is_ascii_alphabetic() {
return None;
}
if !(j < n && input[j].is_ascii_digit()) {
break;
}
}
if segments == 0 || (j < n && input[j].is_ascii_alphanumeric()) {
return None;
}
Some(j)
}
fn consume_path(input: &[u8], mut j: usize, start: usize) -> Option<usize> {
let n = input.len();
while j < n
&& (input[j].is_ascii_alphanumeric()
|| matches!(input[j], b'/' | b'\\' | b'.' | b'_' | b'-' | b'~'))
{
j += 1;
}
(j - start >= 3).then_some(j)
}
fn try_path(input: &[u8], i: usize) -> Option<usize> {
let b = input[i];
if b.is_ascii_alphabetic()
&& input.get(i + 1) == Some(&b':')
&& matches!(input.get(i + 2), Some(b'\\' | b'/'))
{
return consume_path(input, i + 3, i);
}
if b == b'\\' && input.get(i + 1) == Some(&b'\\') {
return consume_path(input, i + 2, i);
}
let rel = (b == b'.' && input.get(i + 1) == Some(&b'/'))
|| (b == b'.' && input.get(i + 1) == Some(&b'.') && input.get(i + 2) == Some(&b'/'))
|| (b == b'~' && input.get(i + 1) == Some(&b'/'));
if rel {
return consume_path(input, i, i);
}
if b == b'/'
&& (i == 0 || matches!(input[i - 1], b' ' | b'\t' | b'\n' | b'\r' | b'"' | b'\'' | b'(' | b'[' | b'{'))
&& input.get(i + 1).is_some_and(|c| c.is_ascii_alphanumeric() || matches!(c, b'.' | b'_' | b'-'))
{
return consume_path(input, i, i);
}
None
}
fn try_hexcolor(input: &[u8], i: usize) -> Option<usize> {
let n = input.len();
if input.get(i) != Some(&b'#') {
return None;
}
let mut j = i + 1;
while j < n && input[j].is_ascii_hexdigit() {
j += 1;
}
let len = j - (i + 1);
if len != 3 && len != 6 {
return None;
}
if j < n && (input[j].is_ascii_alphanumeric() || input[j] == b'_') {
return None;
}
Some(j)
}
fn try_time(input: &[u8], i: usize, space_zone: bool) -> Option<usize> {
let n = input.len();
let mut j = i;
if j + 2 > n || !input[j].is_ascii_digit() || !input[j + 1].is_ascii_digit() {
return None;
}
j += 2;
if j >= n || input[j] != b':' {
return None;
}
j += 1;
if j + 2 > n || !input[j].is_ascii_digit() || !input[j + 1].is_ascii_digit() {
return None;
}
j += 2;
let mut seconds = false;
if j + 2 < n && input[j] == b':' && input[j + 1].is_ascii_digit() && input[j + 2].is_ascii_digit()
{
j += 3;
seconds = true;
if j + 1 < n && input[j] == b'.' && input[j + 1].is_ascii_digit() {
j += 1;
while j < n && input[j].is_ascii_digit() {
j += 1;
}
}
}
if j < n && input[j].is_ascii_digit() {
return None;
}
if !seconds {
return Some(j);
}
match zone_end(input, j, space_zone) {
Some(end) => Some(end),
None => Some(j),
}
}
fn zone_end(input: &[u8], j: usize, space_zone: bool) -> Option<usize> {
let digit_at = |k: usize| input.get(k).is_some_and(u8::is_ascii_digit);
let ends_cleanly =
|e: usize| !input.get(e).is_some_and(|b| b.is_ascii_alphanumeric() || matches!(b, b':' | b'.'));
match input.get(j) {
Some(b'Z' | b'z') => ends_cleanly(j + 1).then_some(j + 1),
Some(b'+' | b'-') => {
if !(digit_at(j + 1) && digit_at(j + 2)) {
return None;
}
let mut e = j + 3;
if input.get(e) == Some(&b':') && digit_at(e + 1) && digit_at(e + 2) {
e += 3;
} else if digit_at(e) && digit_at(e + 1) {
e += 2;
}
ends_cleanly(e).then_some(e)
}
Some(b' ') if space_zone && matches!(input.get(j + 1), Some(b'+' | b'-')) => {
let e = j + 6;
((j + 2..e).all(digit_at) && ends_cleanly(e)).then_some(e)
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_kind_from_a_pattern_fuses_its_match_into_one_token_and_pairs_the_brackets_again() {
let mut set = crate::custom::ShapeSet::new();
set.declare_text("kind assign = \\W \"=\" \\N").expect("declares");
let input = b"f(x = 1) y";
let toks = lex_with_shapes(input, &[], &set, 0);
let kinds: Vec<TokenKind> =
toks.iter().filter(|t| t.is_significant()).map(|t| t.kind).collect();
assert_eq!(
kinds,
vec![
TokenKind::Word,
TokenKind::Open(BracketKind::Paren),
TokenKind::Custom(0),
TokenKind::Close(BracketKind::Paren),
TokenKind::Word,
]
);
let open = toks
.iter()
.position(|t| t.kind == TokenKind::Open(BracketKind::Paren))
.expect("an open");
let close = toks
.iter()
.position(|t| t.kind == TokenKind::Close(BracketKind::Paren))
.expect("a close");
assert_eq!(toks[open].mate(), Some(close));
assert_eq!(toks[close].mate(), Some(open));
let pat = crate::parser::parse_with_shapes("\\{assign}", &set).expect("parses");
let spans = crate::engine::scan_with_shapes(&pat, input, &set);
assert_eq!(spans.len(), 1);
assert_eq!((spans[0].start(), spans[0].end()), (2, 7));
}
fn kinds(input: &str) -> Vec<TokenKind> {
lex(input.as_bytes()).iter().map(|t| t.kind).collect()
}
#[test]
fn a_path_separator_is_not_an_address() {
let sep = vec![TokenKind::Word, TokenKind::Punct, TokenKind::Punct, TokenKind::Word];
assert_eq!(kinds("std::alpha"), sep);
assert_eq!(kinds("Vec::new"), sep);
assert_eq!(kinds("::"), vec![TokenKind::Punct, TokenKind::Punct]);
assert_eq!(kinds("deadbeef::1"), vec![TokenKind::Word, TokenKind::Ip]);
assert_eq!(
kinds("a:b:c"),
vec![TokenKind::Word, TokenKind::Punct, TokenKind::Word, TokenKind::Punct, TokenKind::Word]
);
for addr in [
"fe80::1",
"::1",
"2001:db8::ff00:42:8329",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334",
"a::b",
"fe80::",
] {
assert_eq!(kinds(addr), vec![TokenKind::Ip], "{addr}");
}
assert_eq!(kinds("12:30:45"), vec![TokenKind::Timestamp]);
}
#[test]
fn recognizer_gates_skip_only_refusals() {
let corpus = concat!(
"https://example.com/a?b=1 svn+ssh://host/x ftp.example.com eyJhbGci.eyJzdWI.SflKx eyJ\n",
"+1 555-123-4567 550e8400-e29b-41d4-a716-446655440000 550e8400-e29b 00:1a:2b:3c:4d:5e 00-1a-2b\n",
"d41d8cd98f00b204e9800998ecf8427e da39a3ee5e6b4b0d3255bfef95601890afd80709 d41d8cd98f00b204e9800998ecf8427eX\n",
"192.168.1.0/24 192.168.1.1 10.0.0.256 1234 fe80::1 2001:db8::ff00:42:8329 12:30 12:30:45 ::1\n",
"user@example.com first.last+tag@mail.co a.b-c%d@x.y user@ user_name@host.org\n",
"4111111111111111 4111 1111 1111 1111 4111-1111-1111-1111 1234567890123 12345678901234567\n",
"698 27 54 164 196 330 3782 822463 10005 4111 1111 1111 1111 003 4111 1111-1111 1111 10000 00000 00009\n",
"212-555-1234 1-800-555-0199 123-456-7890 555-123-4567 2024-01-02 12-34-56 +1 555-123-4567\n",
"+44 20 7123 4567 +33 1 42 68 53 00 +5 10 20 30 40 +3.5 7 8 9 10 11 +1.555.123.4567 +55 10 20 30 40\n",
"37.7749,-122.4194 55.7558, 37.6173 12.5,34.7 1.25,2.50,3.75 40.7,-74.0 x,37.7749,-122.4194\n",
"40.7128,-74.0060 -33.8688, 151.2093 12,34 $1,234.56 $5 $ v1.2.3 1.2.3 1.2 version\n",
"10MB 1.5GiB 512KB 10M 50% 12.5% 1500ms 2.5s 3h20m 5string 5x 90s\n",
"#fff #a1b2c3 #ab C:\\Users\\x D:/data ./rel/path ../up ~/home /usr/bin a/b </div>\n",
"SGVsbG8gV29ybGQhIQ== YWJjZGVmZ2hpamtsbW5vcA abcdefghijklmnop_x abc+def/ghi+jkl/mnop==\n",
"2024-01-02 2024-01-02T12:30:00 2024-1-2 let value_123 = 4551 ; call_7(alpha, beta, 7) ;\n",
"key_9: item_9, item_10 ; if (cond_3) { do_3(x) ; } snake_case_name CamelCase x_ _y 0x1F 007\n",
"e ey eyJ eyJa.b.c beyJa.b.c plain, word; text) tail. 42, 1234 56789 1234-5678 4111 1111 9 0 007. 1e5 12x 3rd\n",
"abcdefghijklm abcdefghijklmn abcdefghijklmn+ word/ word: word- word@ word% 123456789012 1234567890123 12345:\n",
"Sep 14 18:50:53 Sep 5 03:04:05 Dec 31 23:59:59 Sep 14 apples Sep 99 10:00:00 dec 1 10:00\n",
"10/Oct/2000:13:55:36 -0700 10/Oct/2000:13:55:36 10/Xyz/2000:13:55:36 2026-09-01T12:00:00Z\n",
"2026-09-01T12:00:00.123456+02:00 2026-09-01 12:00:00 12:30:45-13:00:00 12:30-13:00 12:30:45 -0700\n",
);
let input = corpus.as_bytes();
for i in 0..input.len() {
if !typed_token_possible(input[i]) {
continue;
}
let facts = RunFacts::at(input, i);
let gated = try_typed_token_chain(input, i, Some(&facts));
let ungated = try_typed_token_chain(input, i, None);
assert_eq!(
gated,
ungated,
"at {i} ({:?})",
String::from_utf8_lossy(&input[i..(i + 24).min(input.len())])
);
}
}
#[test]
fn jwt_and_credit_card() {
assert_eq!(
kinds("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMifQ.SflKxwRJSMeKKF2QT4fwpMeJf"),
vec![TokenKind::Jwt]
);
assert!(!kinds("1.2.3").contains(&TokenKind::Jwt));
assert!(!kinds("a.b.c").contains(&TokenKind::Jwt));
assert_eq!(kinds("4111 1111 1111 1111"), vec![TokenKind::CreditCard]);
assert_eq!(kinds("4111-1111-1111-1111"), vec![TokenKind::CreditCard]);
assert_eq!(kinds("4111111111111111"), vec![TokenKind::CreditCard]);
assert_eq!(kinds("3782 822463 10005"), vec![TokenKind::CreditCard], "fifteen digits, 4-6-5");
assert_eq!(kinds("3056 930902 5904"), vec![TokenKind::CreditCard], "fourteen digits, 4-6-4");
assert_eq!(
kinds("4111 1111 1111 1111 003"),
vec![TokenKind::CreditCard],
"nineteen digits, 4-4-4-4-3"
);
assert!(!kinds("1234567812345678").contains(&TokenKind::CreditCard));
}
#[test]
fn digit_groups_in_no_issuers_layout_are_the_numbers_they_look_like() {
let numbers = |s: &str| kinds(s).into_iter().filter(|k| *k == TokenKind::Number).count();
assert!(!kinds("698 27 54 164 196 330").contains(&TokenKind::CreditCard));
assert_eq!(numbers("698 27 54 164 196 330"), 6);
assert!(!kinds("10000 00000 00009").contains(&TokenKind::CreditCard), "5-5-5, Luhn-valid");
assert!(!kinds("4111 1111-1111 1111").contains(&TokenKind::CreditCard), "two separators");
assert!(!kinds("4111 1111 1111 11111").contains(&TokenKind::CreditCard), "a five-digit group");
let then_number = kinds("4111 1111 1111 1111 42");
assert_eq!(then_number.first(), Some(&TokenKind::CreditCard));
assert_eq!(then_number.last(), Some(&TokenKind::Number));
assert_eq!(numbers("4111 1111 1111 1111 42"), 1);
}
#[test]
fn runs_of_separate_numbers_never_lex_as_a_card() {
let mut state = 0x9e37_79b9u32;
let mut next = move || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
let card_shaped = |widths: &[usize]| {
(0..widths.len())
.any(|i| CARD_LAYOUTS.iter().any(|l| widths[i..].starts_with(l)))
};
let mut lines = 0usize;
let mut fused = 0usize;
let mut shaped = 0usize;
for _ in 0..10_000 {
let n = 1 + next() % 10;
let mut widths: Vec<usize> = Vec::new();
let mut line = String::new();
for _ in 0..n {
let width = (1 + next() % 4) as usize;
widths.push(width);
let value = next() % 10u32.pow(width as u32);
line.push_str(&format!("{value:0width$} "));
}
if card_shaped(&widths) {
shaped += 1;
continue;
}
lines += 1;
if lex(line.as_bytes()).iter().any(|t| t.kind == TokenKind::CreditCard) {
fused += 1;
}
}
assert!(lines > 9_000, "{lines} lines generated, {shaped} card-shaped");
assert_eq!(fused, 0, "{fused} of {lines} lines of separate numbers lexed as a card");
}
#[test]
fn base64_geo_phone() {
assert!(kinds("aGVsbG8gd29ybGQhIQ==").contains(&TokenKind::Base64));
assert!(kinds("YWJjMTIzWFlaZGVmR0hJ").contains(&TokenKind::Base64));
assert!(!kinds("supercalifragilistic").contains(&TokenKind::Base64));
assert!(kinds("37.7749,-122.4194").contains(&TokenKind::Geo));
assert!(!kinds("12,34").contains(&TokenKind::Geo)); assert!(!kinds("91.0000,10.0000").contains(&TokenKind::Geo)); assert_eq!(kinds("+15551234567"), vec![TokenKind::Phone]);
assert!(!kinds("12345").contains(&TokenKind::Phone)); }
#[test]
fn a_coordinate_is_a_pair_at_a_points_precision() {
for coord in [
"37.7749,-122.4194",
"40.7128,-74.0060",
"-33.8688, 151.2093",
"51.5074,-0.1278",
"35.6762,139.6503",
"-22.9068,-43.1729",
"1.3521,103.8198",
"55.7558, 37.6173",
] {
assert_eq!(kinds(coord), vec![TokenKind::Geo], "{coord}");
}
for row in ["1.2500,2.5000,3.7500", "1.2500, 2.5000, 3.7500", "0.1111,0.2222,0.3333,0.4444"] {
assert!(!kinds(row).contains(&TokenKind::Geo), "{row}");
}
for coarse in ["12.5,34.7", "40.7,-74.0", "1.25,2.50"] {
assert!(!kinds(coarse).contains(&TokenKind::Geo), "{coarse}");
}
}
#[test]
fn a_phone_number_opens_with_an_assigned_country_code() {
for phone in [
"+15551234567",
"+1 555 123 4567",
"+1-555-123-4567",
"+1 555-123-4567",
"+1.555.123.4567",
"+44 20 7123 4567",
"+49 30 12345678",
"+81 3 1234 5678",
"+33 1 42 68 53 00",
"+86 10 8888 8888",
"+351 21 123 4567",
] {
assert_eq!(kinds(phone), vec![TokenKind::Phone], "{phone}");
}
for run in ["+5 10 20 30 40", "+1 2 3 4 5 6 7", "+3 51 234 5678"] {
assert!(!kinds(run).contains(&TokenKind::Phone), "{run}");
}
for run in ["+44.5 20 30 40 50", "+3.5 7 8 9 10 11"] {
assert!(!kinds(run).contains(&TokenKind::Phone), "{run}");
}
assert!(!kinds("+1 234 5678").contains(&TokenKind::Phone));
assert!(!kinds("+07 123 456 78").contains(&TokenKind::Phone));
}
#[test]
fn a_north_american_number_is_read_without_its_country_code() {
for phone in ["212-555-1234", "1-800-555-0199", "800-555-0199", "1-212-555-1234"] {
assert_eq!(kinds(phone), vec![TokenKind::Phone], "{phone}");
}
for not_a_number in ["123-456-7890", "555-123-4567", "211-555-1234", "212-411-1234"] {
assert!(!kinds(not_a_number).contains(&TokenKind::Phone), "{not_a_number}");
}
assert!(!kinds("212 555 1234").contains(&TokenKind::Phone));
assert!(!kinds("2125551234").contains(&TokenKind::Phone));
assert_eq!(kinds("2024-01-02"), vec![TokenKind::Timestamp]);
}
#[test]
fn rows_of_decimals_never_lex_as_a_coordinate() {
let mut state = 0x1357_9bdfu32;
let mut next = move || {
state ^= state << 13;
state ^= state >> 17;
state ^= state << 5;
state
};
let mut rows = 0usize;
let mut fused = 0usize;
for _ in 0..10_000 {
let columns = 2 + next() % 7;
let frac = 1 + next() % 6;
if columns == 2 && frac >= GEO_MIN_FRACTION as u32 {
continue;
}
let sep = if next() % 2 == 0 { "," } else { ", " };
let mut row = String::new();
for k in 0..columns {
if k > 0 {
row.push_str(sep);
}
let whole = next() % 100;
let f = next() % 10u32.pow(frac);
row.push_str(&format!("{whole}.{f:0width$}", width = frac as usize));
}
rows += 1;
if lex(row.as_bytes()).iter().any(|t| t.kind == TokenKind::Geo) {
fused += 1;
}
}
assert!(rows > 8_000, "{rows} rows generated");
assert_eq!(fused, 0, "{fused} of {rows} decimal rows lexed as a coordinate");
}
#[test]
fn unicode_letters_join_words() {
assert_eq!(kinds("\u{03B8}"), vec![TokenKind::Word]); assert_eq!(kinds("na\u{EF}ve caf\u{E9}"), vec![
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Word
]);
let toks = lex("Gr\u{FC}\u{DF}e42".as_bytes());
assert_eq!(toks.len(), 1, "one token: {toks:?}");
assert_eq!(toks[0].kind, TokenKind::Word);
assert_eq!(toks[0].len(), "Gr\u{FC}\u{DF}e42".len());
}
#[test]
fn cjk_runs_are_single_word_tokens() {
let toks = lex("\u{4E2D}\u{6587}\u{5206}\u{8BCD}".as_bytes()); assert_eq!(toks.len(), 1, "one Word token: {toks:?}");
assert_eq!(toks[0].kind, TokenKind::Word);
assert_eq!(toks[0].end - toks[0].start, 12, "4 chars x 3 bytes, unsplit");
assert_eq!(kinds("hello \u{4E16}\u{754C} ok"), vec![
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Word
]);
assert_eq!(kinds("\u{65E5}\u{672C}\u{3054}\u{3068}"), vec![TokenKind::Word]);
}
#[test]
fn unicode_non_letters_are_whole_tokens() {
let toks = lex("a\u{2014}b".as_bytes());
let ks: Vec<TokenKind> = toks.iter().map(|t| t.kind).collect();
assert_eq!(ks, vec![TokenKind::Word, TokenKind::Punct, TokenKind::Word]);
assert_eq!(toks[1].end - toks[1].start, 3, "em-dash is one whole token");
assert_eq!(kinds("a\u{A0}b"), vec![
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Word
]);
assert_eq!(lex(&[0x80, b'a']).len(), 2);
}
#[test]
fn classifies_core_kinds() {
let k = kinds("ab 12 3.5 \"x\" ,");
assert_eq!(
k,
vec![
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Number,
TokenKind::Whitespace,
TokenKind::Number,
TokenKind::Whitespace,
TokenKind::Quoted,
TokenKind::Whitespace,
TokenKind::Punct,
]
);
}
#[test]
fn pairs_nested_brackets() {
let toks = lex(b"f(g(x))");
assert_eq!(toks[1].kind, TokenKind::Open(BracketKind::Paren));
assert_eq!(toks[1].mate(), Some(6));
assert_eq!(toks[3].mate(), Some(5));
assert_eq!(toks[5].mate(), Some(3));
assert_eq!(toks[6].mate(), Some(1));
}
#[test]
fn mismatched_close_is_unpaired() {
let toks = lex(b"a)");
assert_eq!(toks[1].kind, TokenKind::Close(BracketKind::Paren));
assert_eq!(toks[1].mate(), None);
}
#[test]
fn char_literal_with_inner_quote_does_not_swallow() {
let toks: Vec<_> = lex(b"'\"' x").into_iter().filter(Token::is_significant).collect();
assert_eq!(toks.len(), 2);
assert_eq!(toks[0].kind, TokenKind::Quoted);
assert_eq!(toks[1].kind, TokenKind::Word);
}
#[test]
fn rust_lifetime_is_not_a_char_literal() {
let toks: Vec<_> = lex(b"&'static T").into_iter().filter(Token::is_significant).collect();
assert!(toks.iter().any(|t| t.kind == TokenKind::Word && text(b"&'static T", t) == b"static"));
assert!(toks.iter().all(|t| t.kind != TokenKind::Quoted));
}
#[test]
fn a_single_quoted_string_is_one_token_where_its_quotes_open_and_close() {
let quoted = |input: &str| {
lex(input.as_bytes())
.iter()
.filter(|t| t.kind == TokenKind::Quoted && input.as_bytes()[t.start()] == b'\'')
.count()
};
assert_eq!(quoted("'hello world'"), 1);
assert_eq!(quoted("x = 'ab \"b\" c' ;"), 1);
assert_eq!(quoted("\"a 'b' c\""), 0);
assert_eq!(quoted("'it\\'s'"), 1);
assert_eq!(quoted("'foo''bar'"), 1);
assert_eq!(quoted("''αρχηγός αποθήκης'', δείχνει ''μέχρι το τέλος''."), 0);
assert_eq!(quoted("x = '' ;"), 0);
assert_eq!(quoted("'\\u{10000}\\u{10001}\\u{10002}'"), 1);
assert_eq!(kinds("'\\u{10000}\\u{10001}'"), vec![TokenKind::Quoted]);
assert_eq!(quoted("rock 'n' roll"), 1);
assert_eq!(quoted("sê hy 'ja' en 'n ander"), 1);
assert_eq!(quoted("'n 'massiewe aanval' teen"), 1);
assert_eq!(lex(b"'n 'massiewe aanval' teen").iter().filter(|t| t.kind == TokenKind::Quoted).map(|t| t.start()).collect::<Vec<_>>(), vec![3]);
for open in [
"&'a str) -> &'a str", "impl<'a, 'b>", "where 'a: 'b", "&'static T", "don't stop, it's late",
"the '90s and '80s", "5'10 tall", "'abc\ndef'", "r'raw'", "'n dag 'n beer met 'n karakter",
"'Hello, world,' he said, 'it's late'", "'n harde \"doo-doo-du-du''.", "'s morgens en 't is 'k",
"Γι' αυτό θα παραμείνει θρύλος στ' όνομά του", "'I was there,' she said",
] {
let strings = usize::from(open.starts_with("'Hello") || open.starts_with("'I "));
assert_eq!(quoted(open), strings, "{open}");
}
let toks: Vec<_> = lex(b"'ab c' d").into_iter().filter(Token::is_significant).collect();
assert_eq!(toks.iter().map(|t| t.kind).collect::<Vec<_>>(), vec![TokenKind::Quoted, TokenKind::Word]);
}
#[test]
fn escaped_char_literal_is_one_token() {
let toks: Vec<_> = lex(b"'\\n'").into_iter().filter(Token::is_significant).collect();
assert_eq!(toks.len(), 1);
assert_eq!(toks[0].kind, TokenKind::Quoted);
}
#[test]
fn quoted_handles_escaped_quote() {
let toks = lex(b"\"a\\\"b\"");
assert_eq!(toks.len(), 1);
assert_eq!(toks[0].kind, TokenKind::Quoted);
assert_eq!(toks[0].end, 6);
}
#[test]
fn a_double_quoted_string_closes_on_its_own_line_or_is_no_string() {
let quoted = |input: &str| -> Vec<String> {
lex(input.as_bytes())
.iter()
.filter(|t| t.kind == TokenKind::Quoted)
.map(|t| input[t.start()..t.end()].to_string())
.collect()
};
assert_eq!(quoted("x = \"a \\\" b\" ;"), vec!["\"a \\\" b\""]);
assert_eq!(quoted("it's 5\" long\nthe \"real\" one\n"), vec!["\"real\""]);
assert_eq!(quoted("say \"no close\nx = \"y\" ;"), vec!["\"y\""]);
assert_eq!(quoted("\"a\\\nb\" c"), vec!["\"a\\\nb\""]);
assert!(quoted("\"a\\\"\nb").is_empty());
assert!(quoted("tail \"never closed").is_empty());
let input = "say \"no close\nnext";
let words: Vec<&str> =
lex(input.as_bytes()).iter().filter(|t| t.kind == TokenKind::Word).map(|t| &input[t.start()..t.end()]).collect();
assert_eq!(words, vec!["say", "no", "close", "next"]);
}
#[test]
fn the_double_quote_rule_read_off_the_positions_is_the_rule_walked() {
let lexer_src: &[u8] = include_bytes!("lexer.rs");
let scan_src: &[u8] = include_bytes!("parallel_lex.rs");
let edges: [&[u8]; 11] = [
b"\"a\\\"b\" x",
b"\"a\\\\\" x",
b"\"a\nb\"",
b"\"a\\\nb\" c",
b"\"a\\\r\nb\"",
b"\"\\\\\\\"\\\\\"",
b"\"\"",
b"\"",
b"\"never closed",
b"\"a\\",
b"x \" y \" z \"\n\"",
];
let mut read = 0usize;
for input in [lexer_src, scan_src].into_iter().chain(edges) {
let quotes: Vec<usize> = (0..input.len()).filter(|&i| input[i] == b'"').collect();
let mut ascending = crate::byte_simd::CloseOrNewlinePositions::new(input);
let mut descending = crate::byte_simd::CloseOrNewlinePositions::new(input);
for (&up, &down) in quotes.iter().zip(quotes.iter().rev()) {
for (i, closes) in [(up, &mut ascending), (down, &mut descending)] {
let context = String::from_utf8_lossy(&input[i.saturating_sub(24)..(i + 40).min(input.len())]);
assert_eq!(double_quoted_end_at(input, i, closes), double_quoted_end(input, i), "at {i}: {context:?}");
}
read += 1;
}
}
assert!(read > 1000, "the two sources hold the quotes this is about: {read}");
}
#[test]
fn a_string_continued_across_a_crlf_reads_as_across_an_lf() {
let lf = "let a = 1;\nlet s = \"see \\\n 10/3 here\";\nlet t = 'ab \\\n 7';\nlet b = 2;\n";
let crlf = lf.replace('\n', "\r\n");
let pattern = crate::parse("\\N").expect("the pattern parses");
for text in [lf, crlf.as_str()] {
let found: Vec<&str> =
crate::scan(&pattern, text.as_bytes()).iter().map(|s| &text[s.start as usize..s.end as usize]).collect();
assert_eq!(found, ["1", "2"], "{text:?}");
}
}
#[test]
fn timestamps_in_every_form_are_one_token() {
for ts in [
"2026-09-01T12:00:00Z",
"2026-09-01T12:00:00.123456+02:00",
"2026-09-01T12:00:00-0700",
"2026-09-01t12:00:00+05",
"2026-09-01 12:00:00",
"2026-09-01 12:00",
"Sep 14 18:50:53",
"Sep 5 03:04:05",
"dec 31 23:59:59.5",
"10/Oct/2000:13:55:36 -0700",
"10/Oct/2000:13:55:36",
"12:30:45.5",
"12:30:45Z",
] {
assert_eq!(kinds(ts), vec![TokenKind::Timestamp], "{ts}");
}
let two_times = vec![TokenKind::Timestamp, TokenKind::Punct, TokenKind::Timestamp];
assert_eq!(kinds("12:30:45-13:00:00"), two_times);
assert_eq!(kinds("12:30-13:00"), two_times);
assert_eq!(kinds("2026-09-01T12:00:00Zulu"), vec![TokenKind::Timestamp, TokenKind::Word]);
assert_eq!(
kinds("12:30:45 -0700"),
vec![TokenKind::Timestamp, TokenKind::Whitespace, TokenKind::Punct, TokenKind::Number]
);
assert_eq!(
kinds("Sep 14 apples"),
vec![
TokenKind::Word,
TokenKind::Whitespace,
TokenKind::Number,
TokenKind::Whitespace,
TokenKind::Word
]
);
assert_eq!(kinds("Sep 99 10:00:00")[0], TokenKind::Word);
assert_eq!(kinds("10/Xyz/2000:13:55:36")[0], TokenKind::Number);
assert_eq!(
kinds("2026-09-01 12345"),
vec![TokenKind::Timestamp, TokenKind::Whitespace, TokenKind::Number]
);
}
#[test]
fn a_slash_date_is_one_timestamp_in_every_order_the_calendar_holds() {
for ts in [
"2026/09/15",
"2026/09/15 10:11:12",
"2026/09/15T10:11:12Z",
"09/15/2026",
"15/09/2026",
"3/4/2026",
"15/09/2026 10:11",
] {
assert_eq!(kinds(ts), vec![TokenKind::Timestamp], "{ts}");
}
let figures = vec![
TokenKind::Number,
TokenKind::Punct,
TokenKind::Number,
TokenKind::Punct,
TokenKind::Number,
];
assert_eq!(kinds("1/2/3"), figures);
assert_eq!(kinds("13/13/2026"), figures);
assert_eq!(kinds("2026/13/01"), figures);
assert_eq!(kinds("03/04/2026"), vec![TokenKind::Timestamp]);
}
#[test]
fn classifies_typed_spans() {
assert_eq!(kinds("1.2.3.4"), vec![TokenKind::Ip]);
assert_eq!(kinds("fe80::1"), vec![TokenKind::Ip]);
assert_eq!(kinds("a@b.com"), vec![TokenKind::Email]);
assert_eq!(kinds("http://x.com/p"), vec![TokenKind::Url]);
assert_eq!(kinds("2026-06-16"), vec![TokenKind::Timestamp]);
assert_eq!(kinds("12:30:45"), vec![TokenKind::Timestamp]);
assert_eq!(kinds("2026-06-16T12:30:45"), vec![TokenKind::Timestamp]);
}
#[test]
fn typed_recognizers_leave_plain_numbers_alone() {
assert_eq!(kinds("12.34"), vec![TokenKind::Number]);
assert_eq!(kinds("2026"), vec![TokenKind::Number]);
assert!(!kinds("999.1.1.1").contains(&TokenKind::Ip));
}
#[test]
fn classifies_new_typed_atoms() {
assert_eq!(kinds("550e8400-e29b-41d4-a716-446655440000"), vec![TokenKind::Uuid]);
assert_eq!(kinds("1.2.3"), vec![TokenKind::Version]);
assert_eq!(kinds("v1.2.3"), vec![TokenKind::Version]);
assert_eq!(kinds("1.0.0-alpha.1"), vec![TokenKind::Version]);
assert_eq!(kinds("01:23:45:67:89:ab"), vec![TokenKind::Mac]);
assert_eq!(kinds("aa-bb-cc-dd-ee-ff"), vec![TokenKind::Mac]);
assert_eq!(kinds("#ff8800"), vec![TokenKind::HexColor]);
assert_eq!(kinds("#f80"), vec![TokenKind::HexColor]);
assert_eq!(kinds("192.168.0.0/24"), vec![TokenKind::Cidr]);
}
#[test]
fn classifies_unit_typed_atoms() {
assert_eq!(kinds("42%"), vec![TokenKind::Percent]);
assert_eq!(kinds("3.14%"), vec![TokenKind::Percent]);
assert_eq!(kinds("512KB"), vec![TokenKind::ByteSize]);
assert_eq!(kinds("1.5GiB"), vec![TokenKind::ByteSize]);
assert_eq!(kinds("2TB"), vec![TokenKind::ByteSize]);
assert_eq!(kinds("10B"), vec![TokenKind::ByteSize]);
assert_eq!(kinds("$5"), vec![TokenKind::Money]);
assert_eq!(kinds("$1,234.56"), vec![TokenKind::Money]);
}
#[test]
fn unit_typed_recognizers_reject_near_misses() {
assert_eq!(kinds("10M"), vec![TokenKind::Number, TokenKind::Word]);
assert_eq!(kinds("50"), vec![TokenKind::Number]);
assert!(!kinds("$word").contains(&TokenKind::Money));
}
#[test]
fn classifies_durations() {
assert_eq!(kinds("1500ms"), vec![TokenKind::Duration]);
assert_eq!(kinds("2.5s"), vec![TokenKind::Duration]);
assert_eq!(kinds("3h20m"), vec![TokenKind::Duration]);
assert_eq!(kinds("90s"), vec![TokenKind::Duration]);
assert_eq!(kinds("5x"), vec![TokenKind::Number, TokenKind::Word]);
assert_eq!(kinds("50"), vec![TokenKind::Number]);
}
#[test]
fn classifies_quantities() {
assert_eq!(kinds("5kg"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5 kg"), vec![TokenKind::Quantity]);
assert_eq!(kinds("20\u{b0}C"), vec![TokenKind::Quantity]);
assert_eq!(kinds("-40\u{b0}C"), vec![TokenKind::Quantity]);
assert_eq!(kinds("3.2 GHz"), vec![TokenKind::Quantity]);
assert_eq!(kinds("40 %"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5\u{a0}kg"), vec![TokenKind::Quantity]);
assert_eq!(kinds("\u{2212}5 dB"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5in"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5A"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5 m/s"), vec![TokenKind::Quantity]);
assert_eq!(kinds("5 m\u{b2}"), vec![TokenKind::Quantity]);
assert_eq!(kinds("10MB"), vec![TokenKind::ByteSize]);
assert_eq!(kinds("5m"), vec![TokenKind::Duration]);
assert_eq!(kinds("40%"), vec![TokenKind::Percent]);
let apart = vec![TokenKind::Number, TokenKind::Whitespace, TokenKind::Word];
assert_eq!(kinds("5 m"), apart);
assert_eq!(kinds("3 in"), apart);
assert_eq!(kinds("5 items"), apart);
assert_eq!(kinds("5 kg"), apart);
assert_eq!(kinds("5K"), vec![TokenKind::Number, TokenKind::Word]);
assert_eq!(kinds("5kgs"), vec![TokenKind::Number, TokenKind::Word]);
assert_eq!(kinds("10-20kg"), vec![TokenKind::Number, TokenKind::Punct, TokenKind::Quantity]);
assert_eq!(kinds("x-5kg"), vec![TokenKind::Word, TokenKind::Punct, TokenKind::Quantity]);
assert_eq!(
kinds("(-5kg)"),
vec![
TokenKind::Open(BracketKind::Paren),
TokenKind::Quantity,
TokenKind::Close(BracketKind::Paren)
]
);
}
#[test]
fn classifies_paths() {
assert_eq!(kinds("/usr/bin/x"), vec![TokenKind::Path]);
assert_eq!(kinds("./rel/f.txt"), vec![TokenKind::Path]);
assert_eq!(kinds("../up/one"), vec![TokenKind::Path]);
assert_eq!(kinds("C:\\dir\\file"), vec![TokenKind::Path]);
assert!(!kinds("a/b").contains(&TokenKind::Path));
}
#[test]
fn classifies_hash_digests() {
assert_eq!(kinds("d41d8cd98f00b204e9800998ecf8427e"), vec![TokenKind::HashDigest]); assert_eq!(
kinds("da39a3ee5e6b4b0d3255bfef95601890afd80709"),
vec![TokenKind::HashDigest]
); assert!(!kinds("deadbeef").contains(&TokenKind::HashDigest));
assert!(!kinds("12345678901234567890123456789012").contains(&TokenKind::HashDigest));
}
#[test]
fn new_typed_recognizers_reject_near_misses() {
assert!(!kinds("deadbeef").contains(&TokenKind::Uuid));
assert_eq!(kinds("1.2.3.4"), vec![TokenKind::Ip]);
assert!(!kinds("1.2.3.4").contains(&TokenKind::Version));
assert!(!kinds("version").contains(&TokenKind::Version));
assert_eq!(kinds("fe80::1"), vec![TokenKind::Ip]);
assert!(!kinds("fe80::1").contains(&TokenKind::Mac));
assert!(!kinds("#12345").contains(&TokenKind::HexColor));
assert!(!kinds("10.0.0.1").contains(&TokenKind::Cidr));
assert!(!kinds("1.2.3.4/99").contains(&TokenKind::Cidr));
assert_eq!(kinds("12:30:45"), vec![TokenKind::Timestamp]);
}
#[test]
fn the_paired_sink_pairs_what_the_token_stream_pairs() {
for src in [
&b""[..],
b"()",
b"(a)",
b"a (b) c",
b"f(x) g(yy) h(zzz)",
b"{ [ ( ) ] }",
b"( ( ) ( ) )",
b"a ( b [ c ] d ) e",
b"(",
b")",
b"( ]",
b"a ( b",
b"a ) b",
b"if (cond) { do(x) ; }",
] {
let toks = lex(src);
let mut paired = PairedSignificant::with_base(0, 0);
lex_chunk_paired_into(src, &blob_runs(src), &mut paired, &mut Seams::default());
let mut sig_of = vec![usize::MAX; toks.len()];
let mut tok_of: Vec<usize> = Vec::new();
for (i, t) in toks.iter().enumerate() {
if t.kind != TokenKind::Whitespace {
sig_of[i] = tok_of.len();
tok_of.push(i);
}
}
assert_eq!(
paired.mates.len(),
tok_of.len(),
"one mate slot per significant token in {:?}",
String::from_utf8_lossy(src)
);
assert_eq!(
paired.parts.kinds.len(),
tok_of.len(),
"the mates keep step with the kinds in {:?}",
String::from_utf8_lossy(src)
);
for (s, &t) in tok_of.iter().enumerate() {
let want = toks[t].mate().map(|m| sig_of[m]);
let got =
(paired.mates[s] != NO_MATE).then(|| paired.mates[s] as usize);
assert_eq!(
got,
want,
"token {t} (significant {s}) in {:?}",
String::from_utf8_lossy(src)
);
}
}
}
}