///|
/// Tokens produced by the STIX patterning lexer.
pub(all) enum Tok {
Eof
Ident(String)
String(String)
Number(String)
Timestamp(String)
LBrack
RBrack
LParen
RParen
Colon
Dot
Star
Comma
Eq
Ne
Gt
Lt
Ge
Le
} derive(Eq, Debug)
///|
fn tok_label(tok : Tok) -> String {
match tok {
Eof => "end of pattern"
Ident(text) => "identifier \{text}"
String(_) => "string"
Number(text) => "number \{text}"
Timestamp(_) => "timestamp literal"
LBrack => "["
RBrack => "]"
LParen => "("
RParen => ")"
Colon => ":"
Dot => "."
Star => "*"
Comma => ","
Eq => "="
Ne => "!="
Gt => ">"
Lt => "<"
Ge => ">="
Le => "<="
}
}
///|
fn tokenize_pattern(text : String) -> Result[Array[Tok], String] {
let tokens : Array[Tok] = []
let mut i = 0
while i < text.length() {
match char_at(text, i) {
None => break
Some(ch) =>
if ch.is_ascii_whitespace() {
i += 1
} else if ch == '[' {
tokens.push(LBrack)
i += 1
} else if ch == ']' {
tokens.push(RBrack)
i += 1
} else if ch == '(' {
tokens.push(LParen)
i += 1
} else if ch == ')' {
tokens.push(RParen)
i += 1
} else if ch == ':' {
tokens.push(Colon)
i += 1
} else if ch == '.' {
tokens.push(Dot)
i += 1
} else if ch == '*' {
tokens.push(Star)
i += 1
} else if ch == ',' {
tokens.push(Comma)
i += 1
} else if ch == '=' {
tokens.push(Eq)
i += 1
} else if ch == '!' {
if char_eq(text, i + 1, '=') {
tokens.push(Ne)
i += 2
} else {
return Err("unexpected '!' in pattern")
}
} else if ch == '>' {
if char_eq(text, i + 1, '=') {
tokens.push(Ge)
i += 2
} else {
tokens.push(Gt)
i += 1
}
} else if ch == '<' {
if char_eq(text, i + 1, '=') {
tokens.push(Le)
i += 2
} else {
tokens.push(Lt)
i += 1
}
} else if ch == '\'' {
match read_quoted(text, i) {
Err(err) => return Err(err)
Ok(pair) => {
tokens.push(String(pair.0))
i = pair.1
}
}
} else if ch == 't' && char_eq(text, i + 1, '\'') {
match read_quoted(text, i + 1) {
Err(err) => return Err(err)
Ok(pair) => {
tokens.push(Timestamp(pair.0))
i = pair.1
}
}
} else if ch == '-' || ch.is_ascii_digit() {
match read_number(text, i) {
Err(err) => return Err(err)
Ok(pair) => {
tokens.push(Number(pair.0))
i = pair.1
}
}
} else if is_ascii_ident_start(ch) {
let start = i
i += 1
while i < text.length() {
match char_at(text, i) {
Some(next) =>
if is_ascii_ident_continue(next) {
i += 1
} else {
break
}
None => break
}
}
tokens.push(Ident(slice_to(text, start, i)))
} else {
return Err("unexpected character in STIX pattern")
}
}
}
tokens.push(Eof)
Ok(tokens)
}
///|
fn read_quoted(text : String, start : Int) -> Result[(String, Int), String] {
if !char_eq(text, start, '\'') {
return Err("expected string literal")
}
let mut i = start + 1
let mut out = ""
while i < text.length() {
match char_at(text, i) {
None => return Err("unterminated string literal")
Some('\\') =>
match char_at(text, i + 1) {
None => return Err("unterminated escape in string")
Some('\'') => {
out = "\{out}'"
i += 2
}
Some('\\') => {
out = "\{out}\\"
i += 2
}
Some('n') => {
out = "\{out}\n"
i += 2
}
Some('t') => {
out = "\{out}\t"
i += 2
}
Some('r') => {
out = "\{out}\r"
i += 2
}
Some(_) => return Err("unsupported string escape")
}
Some('\'') => return Ok((out, i + 1))
Some(ch) => {
out = "\{out}\{ch}"
i += 1
}
}
}
Err("unterminated string literal")
}
///|
fn read_number(text : String, start : Int) -> Result[(String, Int), String] {
let mut i = start
if char_eq(text, i, '-') {
i += 1
}
let digits_start = i
while i < text.length() {
match char_at(text, i) {
Some(ch) => if ch.is_ascii_digit() { i += 1 } else { break }
None => break
}
}
if i == digits_start {
return Err("invalid number in pattern")
}
if char_eq(text, i, '.') {
i += 1
let frac = i
while i < text.length() {
match char_at(text, i) {
Some(ch) => if ch.is_ascii_digit() { i += 1 } else { break }
None => break
}
}
if i == frac {
return Err("invalid fractional number in pattern")
}
}
Ok((slice_to(text, start, i), i))
}
///|
fn ident_is(tok : Tok, name : String) -> Bool {
match tok {
Ident(text) => text == name
_ => false
}
}