ifc_step/codec.rs
1//! The STEP codec: format detection, parse policy, and model I/O.
2//!
3//! Two types rather than one configurable type. [`StepCodec`] is the strict
4//! reader and stays zero-sized, so it can be named as a value wherever a codec
5//! is needed. [`StepReader`] carries an explicit [`ParseOptions`] for consumers
6//! that opt into malformed-record recovery. Both implement [`Codec`], so either
7//! can be stored in a `Box<dyn Codec>` alongside the other formats.
8//!
9//! # Lazy loading
10//!
11//! A strict read validates every record and then decodes each entity on
12//! first access (see `lazy.rs`): opening a file costs a syntax pass instead of
13//! building every value, and memory holds the source plus what was touched.
14//! The model keeps its source, so [`Codec::read_bytes`] copies the input once;
15//! [`Codec::read_owned`] and [`Codec::read_path`] hand over or read a buffer
16//! without that copy. [`StepReader::eager`] restores decode-everything-now.
17
18use crate::lazy::{self, Bytes};
19use crate::{parser, writer};
20use ifc_model::{Codec, Model, ModelError};
21use openbim_step::{is_step_file, OnMalformed, ParseOptions};
22use std::io::Write;
23use std::path::Path;
24
25/// The STEP physical file codec.
26///
27/// Strict: a record this codec cannot read is an error, because an authoring
28/// tool that silently drops entities corrupts the file it edits.
29///
30/// A consumer that would rather load a damaged export uses
31/// [`StepCodec::lenient`] and reads [`Model::diagnostics`] to see what was
32/// dropped.
33///
34/// ```
35/// use ifc_model::Codec;
36/// use ifc_step::StepCodec;
37///
38/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
39/// # let bytes = b"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
40/// # FILE_NAME('n','t',(''),(''),'p','o','a');\nFILE_SCHEMA(('IFC4'));\nENDSEC;\n\
41/// # DATA;\n#1= IFCPERSON($,$,'a',$,$,$,$,$);\n#2\nENDSEC;\nEND-ISO-10303-21;\n";
42/// // A damaged export: strict reading refuses it.
43/// assert!(StepCodec.read_bytes(bytes).is_err());
44///
45/// // A viewer opts into recovery and reports what was lost.
46/// let model = StepCodec::lenient().read_bytes(bytes)?;
47/// assert_eq!(model.len(), 1);
48/// assert_eq!(model.diagnostics().len(), 1);
49/// # Ok(())
50/// # }
51/// ```
52#[derive(Debug, Clone, Copy, Default)]
53pub struct StepCodec;
54
55impl StepCodec {
56 /// A reader that skips unreadable data records and reports each one as a
57 /// [`Model`] diagnostic, and reads a REAL written without its decimal
58 /// point (`1E-05`) as the REAL it spells, with a diagnostic
59 /// ([`ParseOptions::lenient`]).
60 ///
61 /// Header structure and the physical-file marker remain fatal: a file
62 /// whose identity cannot be established is not partially readable.
63 #[must_use]
64 pub const fn lenient() -> StepReader {
65 StepReader::new(ParseOptions::lenient())
66 }
67
68 /// A reader with an explicit malformed-record policy.
69 #[must_use]
70 pub const fn with_options(options: ParseOptions) -> StepReader {
71 StepReader::new(options)
72 }
73}
74
75/// A STEP codec carrying an explicit parse policy.
76#[derive(Debug, Clone, Copy, Default)]
77pub struct StepReader {
78 options: ParseOptions,
79 /// Decode every entity during the read instead of on first access.
80 eager: bool,
81}
82
83impl StepReader {
84 /// A reader applying `options`.
85 #[must_use]
86 pub const fn new(options: ParseOptions) -> Self {
87 Self {
88 options,
89 eager: false,
90 }
91 }
92
93 /// Decodes every entity during the read, as before lazy loading.
94 ///
95 /// For a consumer that will touch nearly every entity anyway and wants
96 /// the source released after the read. Recovery and reference-check
97 /// options always read eagerly.
98 #[must_use]
99 pub const fn eager(mut self) -> Self {
100 self.eager = true;
101 self
102 }
103
104 /// Reads a memory-mapped file.
105 ///
106 /// Avoids copying the file: pages are loaded from the page cache as
107 /// they are touched and are not part of the process's own heap. A lazily
108 /// loaded model keeps the mapping and decodes from it for as long as it
109 /// lives.
110 ///
111 /// # Safety
112 ///
113 /// The file must not be modified or truncated while the returned model
114 /// -- or any clone of it -- is alive. Truncation can end the process
115 /// with `SIGBUS` when an entity is decoded; a rewrite makes decoding
116 /// panic or, if the new bytes still parse, read other content. Prefer
117 /// [`Codec::read_path`], which owns its copy, unless the file is known
118 /// to stay put.
119 ///
120 /// # Errors
121 ///
122 /// As [`Codec::read_path`].
123 pub unsafe fn read_path_mapped(&self, path: &Path) -> Result<Model, ModelError> {
124 let file = std::fs::File::open(path).map_err(|e| ModelError::Io(e.to_string()))?;
125 // SAFETY: the caller guarantees the file stays unchanged while the
126 // mapping lives, which is the model's lifetime when it keeps it.
127 let map =
128 unsafe { memmap2::Mmap::map(&file) }.map_err(|e| ModelError::Io(e.to_string()))?;
129 if !is_step_file(&map) {
130 return Err(wrong_format());
131 }
132 if self.is_lazy() {
133 return lazy::read(Bytes::Mapped(map), self.options).map_err(Into::into);
134 }
135 parser::parse(&map, self.options).map_err(Into::into)
136 }
137
138 fn is_lazy(&self) -> bool {
139 !self.eager && lazy::is_lazy(self.options)
140 }
141
142 /// Sets the malformed-record policy.
143 #[must_use]
144 pub const fn on_malformed_record(mut self, policy: OnMalformed) -> Self {
145 self.options = self.options.on_malformed_record(policy);
146 self
147 }
148
149 /// The policy this reader applies.
150 #[must_use]
151 pub const fn options(&self) -> ParseOptions {
152 self.options
153 }
154}
155
156impl Codec for StepCodec {
157 fn name(&self) -> &'static str {
158 StepReader::new(ParseOptions::strict()).name()
159 }
160
161 fn extensions(&self) -> &'static [&'static str] {
162 StepReader::new(ParseOptions::strict()).extensions()
163 }
164
165 fn detect(&self, bytes: &[u8]) -> bool {
166 is_step_file(bytes)
167 }
168
169 fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
170 StepReader::new(ParseOptions::strict()).read_bytes(bytes)
171 }
172
173 fn read_owned(&self, bytes: Vec<u8>) -> Result<Model, ModelError> {
174 StepReader::new(ParseOptions::strict()).read_owned(bytes)
175 }
176
177 fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
178 writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
179 }
180
181 fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
182 StepReader::new(ParseOptions::strict()).read_path(path)
183 }
184}
185
186fn wrong_format() -> ModelError {
187 ModelError::WrongFormat {
188 expected: "STEP",
189 detail: "missing ISO-10303-21 magic".into(),
190 }
191}
192
193impl Codec for StepReader {
194 fn name(&self) -> &'static str {
195 "STEP"
196 }
197
198 fn extensions(&self) -> &'static [&'static str] {
199 &["ifc", "step", "stp"]
200 }
201
202 fn detect(&self, bytes: &[u8]) -> bool {
203 is_step_file(bytes)
204 }
205
206 fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
207 if !is_step_file(bytes) {
208 return Err(wrong_format());
209 }
210 if self.is_lazy() {
211 // The model keeps its source, so the borrowed input is copied.
212 return lazy::read(Bytes::Owned(bytes.to_vec()), self.options).map_err(Into::into);
213 }
214 parser::parse(bytes, self.options).map_err(Into::into)
215 }
216
217 fn read_owned(&self, bytes: Vec<u8>) -> Result<Model, ModelError> {
218 if !is_step_file(&bytes) {
219 return Err(wrong_format());
220 }
221 if self.is_lazy() {
222 return lazy::read(Bytes::Owned(bytes), self.options).map_err(Into::into);
223 }
224 parser::parse(&bytes, self.options).map_err(Into::into)
225 }
226
227 fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
228 writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
229 }
230
231 /// Reads the file into a buffer the model owns.
232 ///
233 /// Not memory-mapped: a lazily loaded model decodes from its source for
234 /// as long as it lives, and a mapping of a file that another process
235 /// changes in that time is undefined behaviour. The mapped read is
236 /// [`StepReader::read_path_mapped`], an `unsafe` opt-in.
237 fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
238 let bytes = std::fs::read(path).map_err(|e| ModelError::Io(e.to_string()))?;
239 self.read_owned(bytes)
240 }
241}