Skip to main content

openbim_step/
writer.rs

1//! Semantic exchange writer.
2//!
3//! Output is structurally equivalent rather than byte-identical: whitespace,
4//! comments, and keyword case are normalized while numeric lexemes are preserved.
5
6use crate::escape;
7use crate::lexer::{Lexer, Token};
8use crate::{Exchange, HeaderRecord, Parameter, Record};
9use std::io::{self, Write};
10
11/// Writes a complete physical file.
12///
13/// # Errors
14///
15/// Returns an I/O error from `output`, or [`std::io::ErrorKind::InvalidInput`]
16/// when an exchange contains an invalid identifier, number, binary, or empty instance.
17pub fn write<S: AsRef<str>, W: Write + ?Sized>(
18    exchange: &Exchange<S>,
19    output: &mut W,
20) -> io::Result<()> {
21    validate_header(&exchange.header.records)?;
22    writeln!(output, "ISO-10303-21;")?;
23    writeln!(output, "HEADER;")?;
24    for record in &exchange.header.records {
25        write_identifier(record.name.as_ref(), output)?;
26        write!(output, "(")?;
27        write_parameters(&record.parameters, output, 0)?;
28        writeln!(output, ");")?;
29    }
30    writeln!(output, "ENDSEC;")?;
31    writeln!(output, "DATA;")?;
32    for instance in &exchange.data.records {
33        write!(output, "#{}=", instance.id.as_str())?;
34        match instance.records.as_slice() {
35            [] => return Err(invalid("data instance must contain a record")),
36            [record] => write_record(record, output)?,
37            records => {
38                write!(output, "(")?;
39                for record in records {
40                    write_record(record, output)?;
41                }
42                write!(output, ")")?;
43            }
44        }
45        writeln!(output, ";")?;
46    }
47    writeln!(output, "ENDSEC;")?;
48    writeln!(output, "END-ISO-10303-21;")
49}
50
51/// Writes an exchange to an owned UTF-8 string.
52///
53/// The physical syntax emitted by this crate is ASCII; text content is escaped.
54///
55/// # Errors
56///
57/// Returns [`std::io::ErrorKind::InvalidInput`] for invalid STEP syntax values.
58pub fn write_to_string<S: AsRef<str>>(exchange: &Exchange<S>) -> io::Result<String> {
59    let mut bytes = Vec::new();
60    write(exchange, &mut bytes)?;
61    String::from_utf8(bytes).map_err(|error| io::Error::new(io::ErrorKind::InvalidData, error))
62}
63
64fn invalid(detail: &'static str) -> io::Error {
65    io::Error::new(io::ErrorKind::InvalidInput, detail)
66}
67
68fn validate_header<S: AsRef<str>>(records: &[HeaderRecord<S>]) -> io::Result<()> {
69    const REQUIRED: [&str; 3] = ["FILE_DESCRIPTION", "FILE_NAME", "FILE_SCHEMA"];
70    if records.len() < REQUIRED.len() {
71        return Err(invalid("missing mandatory STEP header record"));
72    }
73    for (record, required) in records.iter().zip(REQUIRED) {
74        if !record.name.as_ref().eq_ignore_ascii_case(required) {
75            return Err(invalid("mandatory STEP header records are out of order"));
76        }
77    }
78    if records[REQUIRED.len()..].iter().any(|record| {
79        REQUIRED
80            .iter()
81            .any(|required| record.name.as_ref().eq_ignore_ascii_case(required))
82    }) {
83        return Err(invalid("duplicate mandatory STEP header record"));
84    }
85    Ok(())
86}
87
88fn require_identifier(value: &str) -> io::Result<()> {
89    let keyword = value.strip_prefix('!').unwrap_or(value);
90    let mut bytes = keyword.bytes();
91    let valid = bytes
92        .next()
93        .is_some_and(|byte| byte.is_ascii_alphabetic() || byte == b'_')
94        && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
95    if valid {
96        Ok(())
97    } else {
98        Err(invalid("invalid STEP identifier"))
99    }
100}
101
102fn write_identifier<W: Write + ?Sized>(value: &str, output: &mut W) -> io::Result<()> {
103    require_identifier(value)?;
104    output.write_all(value.to_ascii_uppercase().as_bytes())
105}
106
107fn write_enumeration<W: Write + ?Sized>(value: &str, output: &mut W) -> io::Result<()> {
108    if value.starts_with('!') {
109        return Err(invalid("invalid STEP enumeration"));
110    }
111    write_identifier(value, output)
112}
113
114fn require_number(value: &str, real: bool) -> io::Result<()> {
115    let mut lexer = Lexer::new(value.as_bytes());
116    let valid = lexer.next().is_some_and(|token| {
117        token.is_ok_and(|token| {
118            token.span.start == 0
119                && token.span.end == value.len()
120                && if real {
121                    matches!(token.value, Token::Real(_))
122                } else {
123                    matches!(token.value, Token::Integer(_))
124                }
125        })
126    }) && lexer.next().is_none();
127    if valid {
128        Ok(())
129    } else {
130        Err(invalid("invalid STEP number"))
131    }
132}
133
134fn require_binary(value: &str) -> io::Result<()> {
135    let bytes = value.as_bytes();
136    if matches!(bytes.first(), Some(b'0'..=b'3')) && bytes[1..].iter().all(u8::is_ascii_hexdigit) {
137        Ok(())
138    } else {
139        Err(invalid("invalid STEP binary"))
140    }
141}
142
143fn write_record<S: AsRef<str>, W: Write + ?Sized>(
144    record: &Record<S>,
145    output: &mut W,
146) -> io::Result<()> {
147    write_identifier(record.name.as_ref(), output)?;
148    write!(output, "(")?;
149    write_parameters(&record.parameters, output, 0)?;
150    write!(output, ")")
151}
152
153fn write_parameters<S: AsRef<str>, W: Write + ?Sized>(
154    parameters: &[Parameter<S>],
155    output: &mut W,
156    depth: usize,
157) -> io::Result<()> {
158    for (index, parameter) in parameters.iter().enumerate() {
159        if index != 0 {
160            write!(output, ",")?;
161        }
162        write_parameter_at(parameter, output, depth)?;
163    }
164    Ok(())
165}
166
167/// Writes one generic parameter.
168///
169/// # Errors
170///
171/// Returns an I/O error from `output`, or [`std::io::ErrorKind::InvalidInput`]
172/// when `parameter` contains an invalid unescaped syntax value.
173pub fn write_parameter<S: AsRef<str>, W: Write + ?Sized>(
174    parameter: &Parameter<S>,
175    output: &mut W,
176) -> io::Result<()> {
177    write_parameter_at(parameter, output, 0)
178}
179
180fn write_parameter_at<S: AsRef<str>, W: Write + ?Sized>(
181    parameter: &Parameter<S>,
182    output: &mut W,
183    depth: usize,
184) -> io::Result<()> {
185    if depth > crate::MAX_PARAMETER_NESTING {
186        return Err(io::Error::new(
187            io::ErrorKind::InvalidInput,
188            "parameter nesting limit exceeded",
189        ));
190    }
191    match parameter {
192        Parameter::Null => write!(output, "$"),
193        Parameter::Derived => write!(output, "*"),
194        Parameter::Bool(true) => write!(output, ".T."),
195        Parameter::Bool(false) => write!(output, ".F."),
196        Parameter::LogicalUnknown => write!(output, ".U."),
197        Parameter::Integer(value) => {
198            require_number(value.as_ref(), false)?;
199            write!(output, "{}", value.as_ref())
200        }
201        Parameter::Real(value) => {
202            require_number(value.as_ref(), true)?;
203            write!(output, "{}", value.as_ref())
204        }
205        Parameter::Text(text) => write!(output, "'{}'", escape::encode(text.as_ref())),
206        Parameter::Binary(binary) => {
207            require_binary(binary.as_ref())?;
208            write!(output, "\"{}\"", binary.as_ref())
209        }
210        Parameter::Enum(value) => {
211            write!(output, ".")?;
212            write_enumeration(value.as_ref(), output)?;
213            write!(output, ".")
214        }
215        Parameter::Ref(id) => write!(output, "#{id_value}", id_value = id.as_str()),
216        Parameter::List(items) => {
217            write!(output, "(")?;
218            write_parameters(items, output, depth + 1)?;
219            write!(output, ")")
220        }
221        Parameter::Typed { type_name, value } => {
222            write_identifier(type_name.as_ref(), output)?;
223            write!(output, "(")?;
224            write_parameter_at(value, output, depth + 1)?;
225            write!(output, ")")
226        }
227    }
228}