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.
58 ///
59 /// Header structure and the physical-file marker remain fatal: a file
60 /// whose identity cannot be established is not partially readable.
61 #[must_use]
62 pub const fn lenient() -> StepReader {
63 StepReader::new(ParseOptions::lenient())
64 }
65
66 /// A reader with an explicit malformed-record policy.
67 #[must_use]
68 pub const fn with_options(options: ParseOptions) -> StepReader {
69 StepReader::new(options)
70 }
71}
72
73/// A STEP codec carrying an explicit parse policy.
74#[derive(Debug, Clone, Copy, Default)]
75pub struct StepReader {
76 options: ParseOptions,
77 /// Decode every entity during the read instead of on first access.
78 eager: bool,
79}
80
81impl StepReader {
82 /// A reader applying `options`.
83 #[must_use]
84 pub const fn new(options: ParseOptions) -> Self {
85 Self {
86 options,
87 eager: false,
88 }
89 }
90
91 /// Decodes every entity during the read, as before lazy loading.
92 ///
93 /// For a consumer that will touch nearly every entity anyway and wants
94 /// the source released after the read. Recovery and reference-check
95 /// options always read eagerly.
96 #[must_use]
97 pub const fn eager(mut self) -> Self {
98 self.eager = true;
99 self
100 }
101
102 /// Reads a memory-mapped file.
103 ///
104 /// Avoids copying the file: pages are loaded from the page cache as
105 /// they are touched and are not part of the process's own heap. A lazily
106 /// loaded model keeps the mapping and decodes from it for as long as it
107 /// lives.
108 ///
109 /// # Safety
110 ///
111 /// The file must not be modified or truncated while the returned model
112 /// -- or any clone of it -- is alive. Truncation can end the process
113 /// with `SIGBUS` when an entity is decoded; a rewrite makes decoding
114 /// panic or, if the new bytes still parse, read other content. Prefer
115 /// [`Codec::read_path`], which owns its copy, unless the file is known
116 /// to stay put.
117 ///
118 /// # Errors
119 ///
120 /// As [`Codec::read_path`].
121 pub unsafe fn read_path_mapped(&self, path: &Path) -> Result<Model, ModelError> {
122 let file = std::fs::File::open(path).map_err(|e| ModelError::Io(e.to_string()))?;
123 // SAFETY: the caller guarantees the file stays unchanged while the
124 // mapping lives, which is the model's lifetime when it keeps it.
125 let map =
126 unsafe { memmap2::Mmap::map(&file) }.map_err(|e| ModelError::Io(e.to_string()))?;
127 if !is_step_file(&map) {
128 return Err(wrong_format());
129 }
130 if self.is_lazy() {
131 return lazy::read(Bytes::Mapped(map)).map_err(Into::into);
132 }
133 parser::parse(&map, self.options).map_err(Into::into)
134 }
135
136 fn is_lazy(&self) -> bool {
137 !self.eager && lazy::is_lazy(self.options)
138 }
139
140 /// Sets the malformed-record policy.
141 #[must_use]
142 pub const fn on_malformed_record(mut self, policy: OnMalformed) -> Self {
143 self.options = self.options.on_malformed_record(policy);
144 self
145 }
146
147 /// The policy this reader applies.
148 #[must_use]
149 pub const fn options(&self) -> ParseOptions {
150 self.options
151 }
152}
153
154impl Codec for StepCodec {
155 fn name(&self) -> &'static str {
156 StepReader::new(ParseOptions::strict()).name()
157 }
158
159 fn extensions(&self) -> &'static [&'static str] {
160 StepReader::new(ParseOptions::strict()).extensions()
161 }
162
163 fn detect(&self, bytes: &[u8]) -> bool {
164 is_step_file(bytes)
165 }
166
167 fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
168 StepReader::new(ParseOptions::strict()).read_bytes(bytes)
169 }
170
171 fn read_owned(&self, bytes: Vec<u8>) -> Result<Model, ModelError> {
172 StepReader::new(ParseOptions::strict()).read_owned(bytes)
173 }
174
175 fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
176 writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
177 }
178
179 fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
180 StepReader::new(ParseOptions::strict()).read_path(path)
181 }
182}
183
184fn wrong_format() -> ModelError {
185 ModelError::WrongFormat {
186 expected: "STEP",
187 detail: "missing ISO-10303-21 magic".into(),
188 }
189}
190
191impl Codec for StepReader {
192 fn name(&self) -> &'static str {
193 "STEP"
194 }
195
196 fn extensions(&self) -> &'static [&'static str] {
197 &["ifc", "step", "stp"]
198 }
199
200 fn detect(&self, bytes: &[u8]) -> bool {
201 is_step_file(bytes)
202 }
203
204 fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
205 if !is_step_file(bytes) {
206 return Err(wrong_format());
207 }
208 if self.is_lazy() {
209 // The model keeps its source, so the borrowed input is copied.
210 return lazy::read(Bytes::Owned(bytes.to_vec())).map_err(Into::into);
211 }
212 parser::parse(bytes, self.options).map_err(Into::into)
213 }
214
215 fn read_owned(&self, bytes: Vec<u8>) -> Result<Model, ModelError> {
216 if !is_step_file(&bytes) {
217 return Err(wrong_format());
218 }
219 if self.is_lazy() {
220 return lazy::read(Bytes::Owned(bytes)).map_err(Into::into);
221 }
222 parser::parse(&bytes, self.options).map_err(Into::into)
223 }
224
225 fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
226 writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
227 }
228
229 /// Reads the file into a buffer the model owns.
230 ///
231 /// Not memory-mapped: a lazily loaded model decodes from its source for
232 /// as long as it lives, and a mapping of a file that another process
233 /// changes in that time is undefined behaviour. The mapped read is
234 /// [`StepReader::read_path_mapped`], an `unsafe` opt-in.
235 fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
236 let bytes = std::fs::read(path).map_err(|e| ModelError::Io(e.to_string()))?;
237 self.read_owned(bytes)
238 }
239}