use nom::{
branch::alt,
bytes::complete::{take_while, 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(
char('('),
separated_list0(delimited(ws, char(','), ws), move |i| {
token_at_depth(i, depth)
}),
char(')'),
),
),
|(type_name, args)| Token::TypedValue(type_name, args),
)(input)
}
fn ws(input: &[u8]) -> IResult<&[u8], ()> {
map(take_while(|c: u8| c.is_ascii_whitespace()), |_| ())(input)
}
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(
char('('),
separated_list0(delimited(ws, char(','), ws), move |i| {
token_at_depth(i, depth)
}),
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(
char('('),
separated_list0(delimited(ws, char(','), ws), token),
tuple((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)]
mod tests {
use super::*;
#[test]
#[allow(clippy::approx_constant)]
fn test_basic_tokens() {
type Parser = for<'a> fn(&'a [u8]) -> IResult<&'a [u8], Token<'a>>;
let cases: &[(Parser, &[u8], Token)] = &[
(entity_ref, b"#123", Token::EntityRef(123)),
(entity_ref, b"#0", Token::EntityRef(0)),
(string_literal, b"'hello'", Token::String(b"hello")),
(
string_literal,
b"'with spaces'",
Token::String(b"with spaces"),
),
(integer, b"42", Token::Integer(42)),
(integer, b"-42", Token::Integer(-42)),
(integer, b"0", Token::Integer(0)),
(float, b"3.14", Token::Float(3.14)),
(float, b"-3.14", Token::Float(-3.14)),
(float, b"1.5E-10", Token::Float(1.5e-10)),
(enum_value, b".TRUE.", Token::Enum(b"TRUE")),
(enum_value, b".FALSE.", Token::Enum(b"FALSE")),
(enum_value, b".ELEMENT.", Token::Enum(b"ELEMENT")),
];
for (parse, input, expected) in cases {
assert_eq!(
parse(input),
Ok((&b""[..], expected.clone())),
"tokenizing {input:?}"
);
}
}
#[test]
fn test_list() {
let result = list(b"(1,2,3)");
assert!(result.is_ok());
let (_, token) = result.unwrap();
match token {
Token::List(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], Token::Integer(1));
assert_eq!(items[1], Token::Integer(2));
assert_eq!(items[2], Token::Integer(3));
}
_ => panic!("Expected List token"),
}
}
#[test]
fn test_nested_list() {
let result = list(b"(1,(2,3),4)");
assert!(result.is_ok());
let (_, token) = result.unwrap();
match token {
Token::List(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], Token::Integer(1));
match &items[1] {
Token::List(inner) => {
assert_eq!(inner.len(), 2);
assert_eq!(inner[0], Token::Integer(2));
assert_eq!(inner[1], Token::Integer(3));
}
_ => panic!("Expected nested List"),
}
assert_eq!(items[2], Token::Integer(4));
}
_ => panic!("Expected List token"),
}
}
#[test]
fn test_parse_entity() {
let input = "#123=IFCWALL('guid','owner',$,$,'name',$,$,$);";
let result = parse_entity(input);
assert!(result.is_ok());
let (id, ifc_type, args) = result.unwrap();
assert_eq!(id, 123);
assert_eq!(ifc_type, IfcType::IfcWall);
assert_eq!(args.len(), 8);
}
#[test]
fn test_parse_entity_with_nested_list() {
let simple = "(0.,0.,1.)";
println!("Testing simple list: {}", simple);
let simple_result = list(simple.as_bytes());
println!("Simple list result: {:?}", simple_result);
let input = "#9=IFCDIRECTION((0.,0.,1.));";
println!("\nTesting full entity: {}", input);
let result = parse_entity(input);
if let Err(ref e) = result {
println!("Parse error: {:?}", e);
println!("\nTrying to parse just arguments: ((0.,0.,1.))");
let args_input = "((0.,0.,1.))";
let args_result = list(args_input.as_bytes());
println!("Args list result: {:?}", args_result);
}
assert!(result.is_ok(), "Failed to parse: {:?}", result);
let (id, _ifc_type, args) = result.unwrap();
assert_eq!(id, 9);
assert_eq!(args.len(), 1);
if let Token::List(inner) = &args[0] {
assert_eq!(inner.len(), 3);
} else {
panic!("Expected Token::List, got {:?}", args[0]);
}
}
#[test]
fn test_parse_entity_rejects_excessive_nesting() {
let n = (MAX_NESTING_DEPTH as usize) + 64;
let mut s = String::from("#1=IFCWALL(");
for _ in 0..n {
s.push('(');
}
s.push('1');
for _ in 0..n {
s.push(')');
}
s.push_str(");");
assert!(parse_entity(&s).is_err());
}
#[test]
fn test_parse_entity_accepts_moderate_nesting() {
let n = 32;
let mut s = String::from("#1=IFCWALL(");
for _ in 0..n {
s.push('(');
}
s.push('1');
for _ in 0..n {
s.push(')');
}
s.push_str(");");
assert!(parse_entity(&s).is_ok());
}
fn nested(n: usize) -> String {
let mut s = String::from("#1=IFCWALL(");
for _ in 0..n {
s.push('(');
}
s.push('1');
for _ in 0..n {
s.push(')');
}
s.push_str(");");
s
}
#[test]
fn test_parse_entity_accepts_exactly_max_nesting() {
assert!(parse_entity(&nested(MAX_NESTING_DEPTH as usize)).is_ok());
}
#[test]
fn test_parse_entity_rejects_one_over_max_nesting() {
assert!(parse_entity(&nested(MAX_NESTING_DEPTH as usize + 1)).is_err());
}
}