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
9use crate::{parser, writer};
10use ifc_model::{Codec, Model, ModelError};
11use openbim_step::{is_step_file, OnMalformed, ParseOptions};
12use std::io::Write;
13use std::path::Path;
14
15/// The STEP physical file codec.
16///
17/// Strict: a record this codec cannot read is an error, because an authoring
18/// tool that silently drops entities corrupts the file it edits.
19///
20/// A consumer that would rather load a damaged export uses
21/// [`StepCodec::lenient`] and reads [`Model::diagnostics`] to see what was
22/// dropped.
23///
24/// ```
25/// use ifc_model::Codec;
26/// use ifc_step::StepCodec;
27///
28/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
29/// # let bytes = b"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
30/// # FILE_NAME('n','t',(''),(''),'p','o','a');\nFILE_SCHEMA(('IFC4'));\nENDSEC;\n\
31/// # DATA;\n#1= IFCPERSON($,$,'a',$,$,$,$,$);\n#2\nENDSEC;\nEND-ISO-10303-21;\n";
32/// // A damaged export: strict reading refuses it.
33/// assert!(StepCodec.read_bytes(bytes).is_err());
34///
35/// // A viewer opts into recovery and reports what was lost.
36/// let model = StepCodec::lenient().read_bytes(bytes)?;
37/// assert_eq!(model.len(), 1);
38/// assert_eq!(model.diagnostics().len(), 1);
39/// # Ok(())
40/// # }
41/// ```
42#[derive(Debug, Clone, Copy, Default)]
43pub struct StepCodec;
44
45impl StepCodec {
46    /// A reader that skips unreadable data records and reports each one as a
47    /// [`Model`] diagnostic.
48    ///
49    /// Header structure and the physical-file marker remain fatal: a file
50    /// whose identity cannot be established is not partially readable.
51    #[must_use]
52    pub const fn lenient() -> StepReader {
53        StepReader::new(ParseOptions::lenient())
54    }
55
56    /// A reader with an explicit malformed-record policy.
57    #[must_use]
58    pub const fn with_options(options: ParseOptions) -> StepReader {
59        StepReader::new(options)
60    }
61}
62
63/// A STEP codec carrying an explicit parse policy.
64#[derive(Debug, Clone, Copy, Default)]
65pub struct StepReader {
66    options: ParseOptions,
67}
68
69impl StepReader {
70    /// A reader applying `options`.
71    #[must_use]
72    pub const fn new(options: ParseOptions) -> Self {
73        Self { options }
74    }
75
76    /// Sets the malformed-record policy.
77    #[must_use]
78    pub const fn on_malformed_record(mut self, policy: OnMalformed) -> Self {
79        self.options = self.options.on_malformed_record(policy);
80        self
81    }
82
83    /// The policy this reader applies.
84    #[must_use]
85    pub const fn options(&self) -> ParseOptions {
86        self.options
87    }
88}
89
90impl Codec for StepCodec {
91    fn name(&self) -> &'static str {
92        StepReader::new(ParseOptions::strict()).name()
93    }
94
95    fn extensions(&self) -> &'static [&'static str] {
96        StepReader::new(ParseOptions::strict()).extensions()
97    }
98
99    fn detect(&self, bytes: &[u8]) -> bool {
100        is_step_file(bytes)
101    }
102
103    fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
104        StepReader::new(ParseOptions::strict()).read_bytes(bytes)
105    }
106
107    fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
108        writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
109    }
110
111    fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
112        StepReader::new(ParseOptions::strict()).read_path(path)
113    }
114}
115
116impl Codec for StepReader {
117    fn name(&self) -> &'static str {
118        "STEP"
119    }
120
121    fn extensions(&self) -> &'static [&'static str] {
122        &["ifc", "step", "stp"]
123    }
124
125    fn detect(&self, bytes: &[u8]) -> bool {
126        is_step_file(bytes)
127    }
128
129    fn read_bytes(&self, bytes: &[u8]) -> Result<Model, ModelError> {
130        if !is_step_file(bytes) {
131            return Err(ModelError::WrongFormat {
132                expected: "STEP",
133                detail: "missing ISO-10303-21 magic".into(),
134            });
135        }
136        parser::parse(bytes, self.options).map_err(Into::into)
137    }
138
139    fn write(&self, model: &Model, out: &mut dyn Write) -> Result<(), ModelError> {
140        writer::write(model, out).map_err(|e| ModelError::Write(e.to_string()))
141    }
142
143    /// Memory-maps the file rather than reading it into a heap buffer.
144    ///
145    /// Large models are hundreds of megabytes; mapping avoids a full copy and
146    /// lets the OS page in only what the parse touches.
147    fn read_path(&self, path: &Path) -> Result<Model, ModelError> {
148        let file = std::fs::File::open(path).map_err(|e| ModelError::Io(e.to_string()))?;
149        // SAFETY: the file is opened read-only and not mutated for the
150        // lifetime of the mapping; truncation by another process would be
151        // required to invalidate it, which we accept as out of scope.
152        let mmap =
153            unsafe { memmap2::Mmap::map(&file) }.map_err(|e| ModelError::Io(e.to_string()))?;
154        self.read_bytes(&mmap)
155    }
156}