1use std::collections::BTreeMap;
5
6use cadmpeg_core::{CodecError, ContainerEntry, ContainerSummary};
7use cadmpeg_ir::codec::{
8 CodecBackend, Confidence, DecodeOptions, DecodeResult, EncodeInput, Encoder, ExportPlan,
9};
10use cadmpeg_ir::FidelityResolution;
11
12use crate::archive;
13use crate::export::write_step;
14use crate::options::{StepSchema, StepWriteOptions};
15use crate::parse;
16use crate::reader;
17
18#[derive(Debug, Clone, Default)]
20pub struct StepCodec {
21 pub options: StepWriteOptions,
23}
24
25impl Encoder for StepCodec {
26 fn id(&self) -> &'static str {
27 "step"
28 }
29
30 fn plan<'a>(&self, input: EncodeInput<'a>) -> Result<ExportPlan<'a>, CodecError> {
31 let mut bytes = Vec::new();
32 let mut report =
33 write_step(input.ir, &mut bytes, &self.options).map_err(CodecError::from)?;
34 report.fidelity = if input.fidelity.is_some() {
38 FidelityResolution::NotConsumed
39 } else {
40 FidelityResolution::NotProvided
41 };
42 Ok(ExportPlan::buffered(report, bytes))
43 }
44}
45
46impl CodecBackend for StepCodec {
47 fn id(&self) -> &'static str {
48 "step"
49 }
50
51 fn detect(&self, prefix: &[u8]) -> Confidence {
52 if starts_with_step_magic(prefix) {
53 Confidence::High
54 } else if archive::has_root_marker(prefix) || is_part28_xml(prefix) {
55 Confidence::Medium
56 } else {
57 Confidence::No
58 }
59 }
60
61 fn inspect_impl(
67 &self,
68 ctx: &cadmpeg_core::decode::DecodeContext<'_>,
69 root: cadmpeg_core::decode::View<'_>,
70 ) -> Result<ContainerSummary, CodecError> {
71 let bytes = root.window();
72 if archive::has_zip_magic(bytes) {
73 return inspect_zip(ctx, root);
74 }
75 refuse_alternate_encoding(bytes)?;
76 if self.detect(bytes) == Confidence::No {
77 return Err(CodecError::WrongFormat("missing ISO-10303-21 magic".into()));
78 }
79 let (mut exchange, diagnostics) = parse::parse_with_context(bytes, ctx)?;
80 let (decoded, opaque_offsets) =
81 reader::analyze_exchange(bytes, &mut exchange, &diagnostics, Some(ctx))?;
82 let mut entries = vec![ContainerEntry {
83 name: "HEADER".into(),
84 role: "metadata".into(),
85 compression: "none".into(),
86 compressed_size: 0,
87 uncompressed_size: 0,
88 attributes: BTreeMap::default(),
89 }];
90 if !exchange.anchors.is_empty() {
91 let mut attributes = std::collections::BTreeMap::new();
92 attributes.insert("anchor_count".into(), exchange.anchors.len().to_string());
93 entries.push(ContainerEntry {
94 name: "ANCHOR".into(),
95 role: "in_file_anchors".into(),
96 compression: "none".into(),
97 compressed_size: 0,
98 uncompressed_size: 0,
99 attributes,
100 });
101 }
102 if !exchange.references.is_empty() {
103 let mut attributes = std::collections::BTreeMap::new();
104 attributes.insert(
105 "external_count".into(),
106 exchange.references.len().to_string(),
107 );
108 attributes.insert(
109 "external_uris".into(),
110 exchange
111 .references
112 .iter()
113 .map(|entry| entry.uri.as_str())
114 .collect::<Vec<_>>()
115 .join(","),
116 );
117 entries.push(ContainerEntry {
118 name: "REFERENCE".into(),
119 role: "external_references".into(),
120 compression: "none".into(),
121 compressed_size: 0,
122 uncompressed_size: 0,
123 attributes,
124 });
125 }
126 for (index, section) in exchange.data.iter().enumerate() {
127 let mut counts = std::collections::BTreeMap::<String, usize>::new();
128 for id in §ion.records {
129 if !opaque_offsets.contains(&exchange.records[id].span.start) {
130 continue;
131 }
132 for partial in &exchange.records[id].partials {
133 *counts.entry(partial.name.clone()).or_default() += 1;
134 }
135 }
136 let unknown = counts
137 .iter()
138 .map(|(name, count)| format!("{name}:{count}"))
139 .collect::<Vec<_>>()
140 .join(",");
141 let mut attributes = std::collections::BTreeMap::new();
142 attributes.insert("entity_count".into(), section.records.len().to_string());
143 attributes.insert("unknown_entities".into(), unknown);
144 entries.push(ContainerEntry {
145 name: format!("DATA[{index}]"),
146 role: "entity_records".into(),
147 compression: "none".into(),
148 compressed_size: 0,
149 uncompressed_size: 0,
150 attributes,
151 });
152 }
153 let external_dependencies = decoded
154 .report()
155 .notes
156 .iter()
157 .filter(|note| {
158 note.starts_with("external document ") || note.starts_with("external source ")
159 })
160 .cloned()
161 .collect::<Vec<_>>();
162 if !external_dependencies.is_empty() {
163 let mut attributes = std::collections::BTreeMap::new();
164 attributes.insert(
165 "dependency_count".into(),
166 external_dependencies.len().to_string(),
167 );
168 attributes.insert("dependencies".into(), external_dependencies.join(","));
169 entries.push(ContainerEntry {
170 name: "EXTERNAL_DEPENDENCIES".into(),
171 role: "external_references".into(),
172 compression: "none".into(),
173 compressed_size: 0,
174 uncompressed_size: 0,
175 attributes,
176 });
177 }
178 for (index, _) in exchange.signatures.iter().enumerate() {
179 entries.push(ContainerEntry {
180 name: if index == 0 {
181 "SIGNATURE".into()
182 } else {
183 format!("SIGNATURE[{index}]")
184 },
185 role: "signature".into(),
186 compression: "none".into(),
187 compressed_size: 0,
188 uncompressed_size: 0,
189 attributes: BTreeMap::default(),
190 });
191 }
192 let identifiers = reader::schema_identifiers(&exchange);
193 let schema = if identifiers.is_empty() {
194 "unspecified".into()
195 } else {
196 identifiers.join(",")
197 };
198 let edition = identifiers
199 .first()
200 .and_then(|identifier| StepSchema::ap242_edition(identifier))
201 .unwrap_or("edition unspecified");
202 let mut notes = vec![format!("schema {schema}; {edition}")];
203 notes.extend(diagnostics.into_iter().map(|diagnostic| diagnostic.message));
204 Ok(ContainerSummary {
205 format: "step".into(),
206 container_kind: "iso-10303-21-clear-text".into(),
207 entries,
208 notes,
209 })
210 }
211
212 fn decode_impl(
213 &self,
214 ctx: &cadmpeg_core::decode::DecodeContext<'_>,
215 root: cadmpeg_core::decode::View<'_>,
216 ) -> Result<DecodeResult, CodecError> {
217 let bytes = root.window();
218 if archive::has_zip_magic(bytes) {
219 return decode_zip(ctx, root);
220 }
221 refuse_alternate_encoding(bytes)?;
222 if self.detect(bytes) == Confidence::No {
223 return Err(CodecError::WrongFormat("missing ISO-10303-21 magic".into()));
224 }
225 reader::decode(
226 bytes,
227 DecodeOptions {
228 container_only: ctx.container_only(),
229 policy: *ctx.policy(),
230 },
231 ctx,
232 )
233 }
234}
235
236fn starts_with_step_magic(bytes: &[u8]) -> bool {
237 let mut at = 0;
238 loop {
239 while bytes.get(at).is_some_and(u8::is_ascii_whitespace) {
240 at += 1;
241 }
242 if bytes.get(at..at + 2) != Some(b"/*") {
243 break;
244 }
245 let Some(relative_end) = bytes[at + 2..]
246 .windows(2)
247 .position(|window| window == b"*/")
248 else {
249 return false;
250 };
251 at += relative_end + 4;
252 }
253 let magic = b"ISO-10303-21;";
254 bytes
255 .get(at..at + magic.len())
256 .is_some_and(|candidate| candidate.eq_ignore_ascii_case(magic))
257}
258
259fn inspect_zip(
260 ctx: &cadmpeg_core::decode::DecodeContext<'_>,
261 root: cadmpeg_core::decode::View<'_>,
262) -> Result<ContainerSummary, CodecError> {
263 let (archive, root_view) = archive::open_root(ctx, root)?;
264 let root_summary = StepCodec::default().inspect_impl(ctx, root_view)?;
265 let resource_notes = archive::root_reference_notes(&archive, root_view.window())?;
266 let entry_count = archive.entries().len();
267 let root_entry = archive
268 .entry(archive::ROOT_NAME)
269 .expect("validated STEP ZIP root");
270 let root_data_offset = root_entry.data_start;
271 let logical_entries = root_summary
272 .entries
273 .iter()
274 .map(|entry| entry.name.as_str())
275 .collect::<Vec<_>>()
276 .join(",");
277 let mut entries = archive.container_entries(archive::classify_entry);
278 if let Some(root_entry) = entries
279 .iter_mut()
280 .find(|entry| entry.name == archive::ROOT_NAME)
281 {
282 root_entry
283 .attributes
284 .insert("logical_sections".into(), logical_entries);
285 }
286 let mut notes = vec![
287 format!("root {}", archive::ROOT_NAME),
288 format!("archive entries={entry_count}; root data offset={root_data_offset}"),
289 ];
290 notes.extend(root_summary.notes);
291 notes.extend(resource_notes);
292 Ok(ContainerSummary {
293 format: "step".into(),
294 container_kind: "iso-10303-21-zip".into(),
295 entries,
296 notes,
297 })
298}
299
300fn decode_zip(
301 ctx: &cadmpeg_core::decode::DecodeContext<'_>,
302 root: cadmpeg_core::decode::View<'_>,
303) -> Result<DecodeResult, CodecError> {
304 let (archive, root_view) = archive::open_root(ctx, root)?;
305 let resource_notes = archive::root_reference_notes(&archive, root_view.window())?;
306 let entry_count = archive.entries().len();
307 let root_entry = archive
308 .entry(archive::ROOT_NAME)
309 .expect("validated STEP ZIP root");
310 let root_data_offset = root_entry.data_start;
311 let mut result = reader::decode(
312 root_view.window(),
313 DecodeOptions {
314 container_only: ctx.container_only(),
315 policy: *ctx.policy(),
316 },
317 ctx,
318 )?;
319 if let Some(source) = &mut result.ir_mut().source {
320 source
321 .attributes
322 .insert("container_kind".into(), "iso-10303-21-zip".into());
323 source
324 .attributes
325 .insert("archive_root".into(), archive::ROOT_NAME.into());
326 source
327 .attributes
328 .insert("archive_entries".into(), entry_count.to_string());
329 source.attributes.insert(
330 "archive_root_data_offset".into(),
331 root_data_offset.to_string(),
332 );
333 }
334 result.report_mut().notes.push(format!(
335 "container root {}; archive entries={entry_count}",
336 archive::ROOT_NAME
337 ));
338 result.report_mut().notes.extend(resource_notes);
339 Ok(result)
340}
341
342fn refuse_alternate_encoding(bytes: &[u8]) -> Result<(), CodecError> {
343 if bytes.starts_with(b"\x89HDF\r\n\x1a\n") {
344 return Err(CodecError::NotImplemented(
345 "STEP Part 26 binary/HDF5 encoding".into(),
346 ));
347 }
348 if is_part28_xml(bytes) {
349 return Err(CodecError::NotImplemented(
350 "STEP Part 28 XML encoding".into(),
351 ));
352 }
353 let lower = bytes
354 .iter()
355 .take(4096)
356 .map(u8::to_ascii_lowercase)
357 .collect::<Vec<_>>();
358 if lower.starts_with(b"<?xml")
359 && (lower
360 .windows(21)
361 .any(|window| window == b"business_object_model")
362 || lower.windows(14).any(|window| window == b"ap242_bo_model"))
363 {
364 return Err(CodecError::NotImplemented(
365 "AP242 BO-Model XML sidecar".into(),
366 ));
367 }
368 Ok(())
369}
370
371fn is_part28_xml(bytes: &[u8]) -> bool {
372 let lower = bytes
373 .iter()
374 .take(4096)
375 .map(u8::to_ascii_lowercase)
376 .collect::<Vec<_>>();
377 lower.starts_with(b"<?xml")
378 && (lower.windows(12).any(|window| window == b"iso_10303_28")
379 || lower
380 .windows(21)
381 .any(|window| window == b"iso:std:iso:10303:-28"))
382}