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.
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}