use crate::escape;
use crate::lexer::{Lexer, Token};
use crate::recovery::{Diagnostic, OnMalformed, ParseOptions, ParseOutcome};
use crate::{
DataRecord, DataSection, Exchange, HeaderRecord, HeaderSection, InstanceId, Parameter, Record,
Span, Spanned, StepError,
};
#[derive(Debug, Clone, PartialEq)]
pub enum Event<S = String> {
StartHeader,
HeaderRecord(HeaderRecord<S>),
EndHeader,
StartData,
DataRecord(DataRecord<S>),
EndData,
}
pub trait EventSink<S = String> {
fn event(&mut self, event: Event<S>);
}
impl<S, F> EventSink<S> for F
where
F: FnMut(Event<S>),
{
fn event(&mut self, event: Event<S>) {
self(event);
}
}
pub fn parse(input: &[u8]) -> Result<Exchange, StepError> {
parse_with(input, ParseOptions::strict()).map(|outcome| outcome.exchange)
}
pub fn parse_with(input: &[u8], options: ParseOptions) -> Result<ParseOutcome, StepError> {
#[derive(Default)]
struct Builder {
header: HeaderSection,
data: DataSection,
}
impl EventSink for Builder {
fn event(&mut self, event: Event) {
match event {
Event::HeaderRecord(record) => self.header.records.push(record),
Event::DataRecord(record) => self.data.records.push(record),
Event::StartHeader | Event::EndHeader | Event::StartData | Event::EndData => {}
}
}
}
let mut builder = Builder::default();
let diagnostics = parse_events_with(input, &mut builder, options)?;
Ok(ParseOutcome {
exchange: Exchange {
header: builder.header,
data: builder.data,
},
diagnostics,
})
}
pub fn parse_events(input: &[u8], sink: &mut impl EventSink) -> Result<(), StepError> {
parse_events_with(input, sink, ParseOptions::strict()).map(|_| ())
}
pub fn parse_events_with(
input: &[u8],
sink: &mut impl EventSink,
options: ParseOptions,
) -> Result<Vec<Diagnostic>, StepError> {
if !crate::is_step_file(input) {
return Err(StepError::not_step("missing ISO-10303-21 marker"));
}
let mut parser = Parser::new(input);
parser.options = options;
parser.parse(sink)?;
Ok(parser.diagnostics)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Phase {
BeforeStart,
BeforeHeader,
Header,
BeforeData,
Data,
BeforeEnd,
Done,
}
struct Parser<'a> {
input: &'a [u8],
lexer: Lexer<'a>,
lookahead: Option<Spanned<Token<'a>>>,
last_end: usize,
phase: Phase,
header_records_seen: usize,
options: ParseOptions,
diagnostics: Vec<Diagnostic>,
}
impl<'a> Parser<'a> {
fn new(input: &'a [u8]) -> Self {
Self {
input,
lexer: Lexer::new(input),
lookahead: None,
last_end: 0,
phase: Phase::BeforeStart,
header_records_seen: 0,
options: ParseOptions::strict(),
diagnostics: Vec::new(),
}
}
#[allow(clippy::too_many_lines)]
fn parse(&mut self, sink: &mut impl EventSink) -> Result<(), StepError> {
loop {
let token = match self.next() {
Ok(Some(token)) => token,
Ok(None) => break,
Err(error) => {
self.recover_or_fail(error, None)?;
continue;
}
};
if self.phase == Phase::Done {
return Err(StepError::syntax(
token.span,
"content after END-ISO-10303-21",
));
}
match token.value {
Token::Name(name) if name.eq_ignore_ascii_case(b"ISO-10303-21") => {
if self.phase != Phase::BeforeStart {
return Err(StepError::syntax(
token.span,
"unexpected ISO-10303-21 marker",
));
}
self.expect_semicolon("after ISO-10303-21")?;
self.phase = Phase::BeforeHeader;
}
Token::Name(name) if name.eq_ignore_ascii_case(b"HEADER") => {
if self.phase != Phase::BeforeHeader {
return Err(StepError::syntax(token.span, "unexpected HEADER section"));
}
self.expect_semicolon("after HEADER")?;
self.phase = Phase::Header;
sink.event(Event::StartHeader);
}
Token::Name(name) if name.eq_ignore_ascii_case(b"DATA") => {
if self.phase != Phase::BeforeData {
return Err(StepError::syntax(token.span, "unexpected DATA section"));
}
self.expect_semicolon("after DATA")?;
self.phase = Phase::Data;
sink.event(Event::StartData);
}
Token::Name(name) if name.eq_ignore_ascii_case(b"ENDSEC") => {
self.expect_semicolon("after ENDSEC")?;
match self.phase {
Phase::Header => {
if self.header_records_seen < 3 {
return Err(StepError::syntax(
token.span,
"missing mandatory STEP header record",
));
}
sink.event(Event::EndHeader);
self.phase = Phase::BeforeData;
}
Phase::Data => {
sink.event(Event::EndData);
self.phase = Phase::BeforeEnd;
}
_ => {
return Err(StepError::syntax(token.span, "ENDSEC outside a section"));
}
}
}
Token::Name(name) if name.eq_ignore_ascii_case(b"END-ISO-10303-21") => {
if self.phase != Phase::BeforeEnd {
return Err(StepError::syntax(
token.span,
"unexpected END-ISO-10303-21 marker",
));
}
self.expect_semicolon("after END-ISO-10303-21")?;
self.phase = Phase::Done;
}
Token::Name(name) if self.phase == Phase::Header => {
self.validate_header_record(&name, token.span)?;
let parameters = self.parse_arguments()?;
self.expect_semicolon("after header record")?;
sink.event(Event::HeaderRecord(HeaderRecord {
name: upper(&name),
parameters,
}));
}
Token::Id(id) if self.phase == Phase::Data => {
let start = token.span.start;
match self.parse_data_record(&id, token.span) {
Ok(record) => sink.event(Event::DataRecord(record)),
Err(error) => self.recover_or_fail(error, Some(start))?,
}
}
_ => {
let error = StepError::syntax(
token.span,
format!("unexpected token {:?} in {:?}", token.value, self.phase),
);
self.recover_or_fail(error, Some(token.span.start))?;
}
}
}
if self.phase != Phase::Done {
let detail = if matches!(self.phase, Phase::Header | Phase::Data) {
"unterminated section (expected ENDSEC)"
} else {
"physical file requires start, HEADER, DATA, and end markers"
};
return Err(StepError::syntax(self.eof_span(), detail));
}
Ok(())
}
fn parse_data_record(
&mut self,
id: &[u8],
id_span: Span,
) -> Result<DataRecord<String>, StepError> {
self.expect_equals()?;
let record_token = self.next()?.ok_or_else(|| {
StepError::syntax(Span::new(id_span.end, id_span.end), "missing record body")
})?;
let records = match record_token.value {
Token::Name(name) => vec![self.parse_named_record(&name)?],
Token::OpenParen => {
let mut records = Vec::new();
loop {
if !self
.peek()?
.is_some_and(|next| next.value != Token::CloseParen)
{
break;
}
let component = self.next()?.ok_or_else(|| {
StepError::syntax(self.eof_span(), "missing complex record")
})?;
let Token::Name(name) = component.value else {
return Err(StepError::syntax(
component.span,
"expected complex record name",
));
};
records.push(self.parse_named_record(&name)?);
}
let close = self.next()?.ok_or_else(|| {
StepError::syntax(self.eof_span(), "unterminated complex instance")
})?;
if close.value != Token::CloseParen {
return Err(StepError::syntax(
close.span,
"expected ')' after complex instance",
));
}
if records.is_empty() {
return Err(StepError::syntax(
record_token.span,
"complex instance must contain a record",
));
}
records
}
_ => {
return Err(StepError::syntax(
record_token.span,
"expected record name or complex instance",
));
}
};
self.expect_semicolon("after data record")?;
Ok(DataRecord {
id: InstanceId::new(std::str::from_utf8(id).expect("instance digits are ASCII"))
.expect("lexer validates instance ids"),
records,
})
}
fn recover_or_fail(
&mut self,
error: StepError,
record_start: Option<usize>,
) -> Result<(), StepError> {
if self.options.on_malformed_record != OnMalformed::Skip || self.phase != Phase::Data {
return Err(error);
}
let start = record_start.unwrap_or_else(|| error.span().start);
let resume = self.resync_from(start.saturating_add(1));
self.lookahead = None;
self.lexer.resume_at(resume);
self.last_end = resume;
self.diagnostics.push(Diagnostic::skipped_record(
Span::new(start, resume),
format!("skipped malformed data record: {}", error.detail()),
));
Ok(())
}
fn resync_from(&self, from: usize) -> usize {
enum Literal {
None,
Text,
Binary,
}
let mut position = from.min(self.input.len());
let mut literal = Literal::None;
while position < self.input.len() {
let byte = self.input[position];
match literal {
Literal::Text => {
if byte == b'\'' {
if self.input.get(position + 1) == Some(&b'\'') {
position += 2;
continue;
}
literal = Literal::None;
}
position += 1;
}
Literal::Binary => {
if byte == b'"' {
literal = Literal::None;
}
position += 1;
}
Literal::None => match byte {
b'\'' => {
literal = Literal::Text;
position += 1;
}
b'"' => {
literal = Literal::Binary;
position += 1;
}
b'/' if self.input.get(position + 1) == Some(&b'*') => {
position = self.input[position + 2..]
.windows(2)
.position(|window| window == b"*/")
.map_or(self.input.len(), |offset| position + 2 + offset + 2);
}
b';' => return position + 1,
_ if self.section_end_at(position) => return position,
_ => position += 1,
},
}
}
self.input.len()
}
fn section_end_at(&self, position: usize) -> bool {
let preceded_by_word = position
.checked_sub(1)
.and_then(|previous| self.input.get(previous))
.is_some_and(|byte| byte.is_ascii_alphanumeric() || *byte == b'_');
!preceded_by_word
&& self
.input
.get(position..position + 6)
.is_some_and(|bytes| bytes.eq_ignore_ascii_case(b"ENDSEC"))
}
fn validate_header_record(&mut self, name: &[u8], span: Span) -> Result<(), StepError> {
const REQUIRED: [&[u8]; 3] = [b"FILE_DESCRIPTION", b"FILE_NAME", b"FILE_SCHEMA"];
if let Some(expected) = REQUIRED.get(self.header_records_seen) {
if !name.eq_ignore_ascii_case(expected) {
return Err(StepError::syntax(
span,
format!(
"expected mandatory {} header record",
String::from_utf8_lossy(expected)
),
));
}
} else if REQUIRED
.iter()
.any(|required| name.eq_ignore_ascii_case(required))
{
return Err(StepError::syntax(span, "duplicate mandatory header record"));
}
self.header_records_seen += 1;
Ok(())
}
fn parse_named_record(&mut self, name: &[u8]) -> Result<Record, StepError> {
Ok(Record {
name: upper(name),
parameters: self.parse_arguments()?,
})
}
fn parse_arguments(&mut self) -> Result<Vec<Parameter>, StepError> {
let token = self
.next()?
.ok_or_else(|| StepError::syntax(self.eof_span(), "expected '(' after record name"))?;
if token.value != Token::OpenParen {
return Err(StepError::syntax(
token.span,
"expected '(' after record name",
));
}
self.parse_parameter_list(0)
}
fn parse_parameter_list(&mut self, depth: usize) -> Result<Vec<Parameter>, StepError> {
if depth > crate::MAX_PARAMETER_NESTING {
let span = match self.peek()? {
Some(token) => token.span,
None => self.eof_span(),
};
return Err(StepError::syntax(span, "parameter nesting limit exceeded"));
}
let mut parameters = Vec::new();
if self
.peek()?
.is_some_and(|token| token.value == Token::CloseParen)
{
let _ = self.next()?;
return Ok(parameters);
}
loop {
parameters.push(self.parse_parameter(depth)?);
let separator = self
.next()?
.ok_or_else(|| StepError::syntax(self.eof_span(), "unterminated parameter list"))?;
match separator.value {
Token::Comma => {}
Token::CloseParen => return Ok(parameters),
_ => {
return Err(StepError::syntax(
separator.span,
"expected ',' or ')' after parameter",
));
}
}
}
}
fn parse_parameter(&mut self, depth: usize) -> Result<Parameter, StepError> {
let token = self
.next()?
.ok_or_else(|| StepError::syntax(self.eof_span(), "expected parameter"))?;
match token.value {
Token::Dollar => Ok(Parameter::Null),
Token::Star => Ok(Parameter::Derived),
Token::Id(id) => Ok(Parameter::Ref(
InstanceId::new(std::str::from_utf8(&id).expect("instance digits are ASCII"))
.expect("lexer validates instance ids"),
)),
Token::Integer(value) => Ok(Parameter::Integer(
String::from_utf8_lossy(&value).into_owned(),
)),
Token::Real(value) => Ok(Parameter::Real(
String::from_utf8_lossy(&value).into_owned(),
)),
Token::Text(raw) => Ok(Parameter::Text(escape::decode(&raw))),
Token::Binary(raw) => Ok(Parameter::Binary(
String::from_utf8_lossy(&raw).into_owned(),
)),
Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"T") => {
Ok(Parameter::Bool(true))
}
Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"F") => {
Ok(Parameter::Bool(false))
}
Token::Keyword(keyword) if keyword.eq_ignore_ascii_case(b"U") => {
Ok(Parameter::LogicalUnknown)
}
Token::Keyword(keyword) => Ok(Parameter::Enum(upper(&keyword))),
Token::OpenParen => Ok(Parameter::List(self.parse_parameter_list(depth + 1)?)),
Token::Name(name) => {
let Some(next) = self.peek()? else {
return Err(StepError::syntax(
self.eof_span(),
"expected '(' after typed parameter name",
));
};
if next.value != Token::OpenParen {
return Err(StepError::syntax(
next.span,
"expected '(' after typed parameter name",
));
}
let _ = self.next()?;
let mut parameters = self.parse_parameter_list(depth + 1)?;
let value = if parameters.len() == 1 {
Box::new(parameters.remove(0))
} else {
Box::new(Parameter::List(parameters))
};
Ok(Parameter::Typed {
type_name: upper(&name),
value,
})
}
value => Err(StepError::syntax(
token.span,
format!("unexpected parameter token {value:?}"),
)),
}
}
fn expect_equals(&mut self) -> Result<(), StepError> {
let token = self
.next()?
.ok_or_else(|| StepError::syntax(self.eof_span(), "expected '=' after instance id"))?;
if token.value == Token::Equals {
Ok(())
} else {
Err(StepError::syntax(
token.span,
"expected '=' after instance id",
))
}
}
fn expect_semicolon(&mut self, context: &str) -> Result<(), StepError> {
let token = self
.next()?
.ok_or_else(|| StepError::syntax(self.eof_span(), format!("expected ';' {context}")))?;
if token.value == Token::Semicolon {
Ok(())
} else {
Err(StepError::syntax(
token.span,
format!("expected ';' {context}"),
))
}
}
fn next(&mut self) -> Result<Option<Spanned<Token<'a>>>, StepError> {
let token = match self.lookahead.take() {
Some(token) => Some(token),
None => self.lexer.next_spanned()?,
};
if let Some(token) = &token {
self.last_end = token.span.end;
}
Ok(token)
}
fn peek(&mut self) -> Result<Option<&Spanned<Token<'a>>>, StepError> {
if self.lookahead.is_none() {
self.lookahead = self.lexer.next_spanned()?;
}
Ok(self.lookahead.as_ref())
}
fn eof_span(&self) -> Span {
let offset = self.last_end.max(self.lexer.offset());
Span::new(offset, offset)
}
}
fn upper(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes).to_ascii_uppercase()
}