Skip to main content

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}