use nom::{
branch::alt,
bytes::complete::take_while1,
character::complete::{char, digit1, one_of},
combinator::{map, map_res, opt, recognize},
multi::separated_list0,
sequence::{delimited, pair, preceded, tuple},
IResult,
};
use crate::error::{Error, Result};
use crate::generated::IfcType;
#[derive(Debug, Clone, PartialEq)]
pub enum Token<'a> {
EntityRef(u32),
String(&'a [u8]),
Integer(i64),
Float(f64),
Enum(&'a [u8]),
List(Vec<Token<'a>>),
TypedValue(&'a [u8], Vec<Token<'a>>),
Null,
Derived,
}
fn entity_ref(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map(
preceded(char('#'), map_res(digit1, lexical_core::parse::<u32>)),
Token::EntityRef,
)(input)
}
fn string_literal(input: &[u8]) -> IResult<&[u8], Token<'_>> {
#[inline]
fn parse_string_content(input: &[u8], quote_byte: u8) -> IResult<&[u8], &[u8]> {
let bytes = input;
let mut pos = 0;
while let Some(found) = memchr::memchr(quote_byte, &bytes[pos..]) {
let idx = pos + found;
if idx + 1 < bytes.len() && bytes[idx + 1] == quote_byte {
pos = idx + 2; continue;
}
return Ok((&input[idx..], &input[..idx]));
}
Err(nom::Err::Error(nom::error::Error::new(
input,
nom::error::ErrorKind::Char,
)))
}
alt((
map(
delimited(char('\''), |i| parse_string_content(i, b'\''), char('\'')),
Token::String,
),
map(
delimited(char('"'), |i| parse_string_content(i, b'"'), char('"')),
Token::String,
),
))(input)
}
#[inline]
fn integer(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map_res(recognize(tuple((opt(char('-')), digit1))), |s: &[u8]| {
lexical_core::parse::<i64>(s)
.map(Token::Integer)
.map_err(|_| "parse error")
})(input)
}
#[inline]
fn float(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map_res(
recognize(tuple((
opt(char('-')),
digit1,
char('.'),
opt(digit1), opt(tuple((one_of("eE"), opt(one_of("+-")), digit1))),
))),
|s: &[u8]| {
lexical_core::parse::<f64>(s)
.map(Token::Float)
.map_err(|_| "parse error")
},
)(input)
}
fn enum_value(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map(
delimited(
char('.'),
take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_'),
char('.'),
),
Token::Enum,
)(input)
}
fn null(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map(char('$'), |_| Token::Null)(input)
}
fn derived(input: &[u8]) -> IResult<&[u8], Token<'_>> {
map(char('*'), |_| Token::Derived)(input)
}
const MAX_NESTING_DEPTH: u32 = 256;
fn typed_value_at_depth(input: &[u8], depth: u32) -> IResult<&[u8], Token<'_>> {
map(
pair(
take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_'),
delimited(
pair(ws, char('(')),
preceded(
ws,
separated_list0(delimited(ws, char(','), ws), move |i| {
token_at_depth(i, depth)
}),
),
pair(ws, char(')')),
),
),
|(type_name, args)| Token::TypedValue(type_name, args),
)(input)
}
fn ws(input: &[u8]) -> IResult<&[u8], ()> {
let end = super::lexical::skip_step_trivia(input, 0).unwrap_or(input.len());
Ok((&input[end..], ()))
}
fn token(input: &[u8]) -> IResult<&[u8], Token<'_>> {
token_at_depth(input, 0)
}
fn token_at_depth(input: &[u8], depth: u32) -> IResult<&[u8], Token<'_>> {
if depth > MAX_NESTING_DEPTH {
return Err(nom::Err::Failure(nom::error::Error::new(
input,
nom::error::ErrorKind::TooLarge,
)));
}
delimited(
ws,
alt((
null, derived, entity_ref, enum_value, string_literal, move |i| list_at_depth(i, depth + 1), float,
integer,
move |i| typed_value_at_depth(i, depth + 1),
)),
ws,
)(input)
}
#[cfg(test)]
fn list(input: &[u8]) -> IResult<&[u8], Token<'_>> {
list_at_depth(input, 0)
}
fn list_at_depth(input: &[u8], depth: u32) -> IResult<&[u8], Token<'_>> {
map(
delimited(
pair(char('('), ws),
separated_list0(delimited(ws, char(','), ws), move |i| {
token_at_depth(i, depth)
}),
pair(ws, char(')')),
),
Token::List,
)(input)
}
#[allow(clippy::type_complexity)]
pub fn parse_entity<'a, T>(input: &'a T) -> Result<(u32, IfcType, Vec<Token<'a>>)>
where
T: AsRef<[u8]> + ?Sized,
{
let input = input.as_ref();
let result: IResult<&[u8], (u32, &[u8], Vec<Token>)> = tuple((
delimited(
ws,
preceded(char('#'), map_res(digit1, lexical_core::parse::<u32>)),
ws,
),
preceded(
char('='),
delimited(
ws,
take_while1(|c: u8| c.is_ascii_alphanumeric() || c == b'_'),
ws,
),
),
delimited(
pair(char('('), ws),
separated_list0(delimited(ws, char(','), ws), token),
tuple((ws, char(')'), ws, char(';'))),
),
))(input);
match result {
Ok((_, (id, type_str, args))) => {
let type_str = std::str::from_utf8(type_str)
.map_err(|_| Error::parse(0, "Entity type is not ASCII/UTF-8"))?;
let ifc_type = IfcType::from_str(type_str);
Ok((id, ifc_type, args))
}
Err(e) => Err(Error::parse(0, format!("Failed to parse entity: {}", e))),
}
}
#[cfg(test)]
#[path = "tokenizer_tests.rs"]
mod tokenizer_tests;