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}