1use std::collections::{BTreeMap, HashMap, HashSet};
12
13use ifc_lite_core::EntityScanner;
14
15pub use crate::step_api::{
16 AttrMutation, CopyOnWriteMutation, PropMutation, StepOptions, StepStats,
17};
18
19use crate::schema_detect::detect_schema;
20use crate::step_text::{
21 apply_attr_mutations_counted, escape, merge_edits, refs_in_line_counted, renumber,
22};
23
24pub fn export_step(content: &[u8], opts: &StepOptions) -> std::io::Result<String> {
30 export_step_with_stats(content, opts).map(|(s, _)| s)
31}
32
33pub fn export_step_with_stats(content: &[u8], opts: &StepOptions) -> std::io::Result<(String, StepStats)> {
35 let mut buf = match opts.included {
39 None => Vec::with_capacity(content.len()),
40 Some(_) => Vec::new(),
41 };
42 let stats = emit(content, opts, &mut buf)?;
49 let out = String::from_utf8(buf).expect("the writer emits UTF-8");
52 Ok((out, stats))
53}
54
55#[allow(clippy::type_complexity)]
70pub fn export_step_to_writer<W: std::io::Write>(
71 content: &[u8],
72 opts: &StepOptions,
73 w: &mut W,
74) -> std::io::Result<StepStats> {
75 use std::io::Write as _;
76 let mut buffered = std::io::BufWriter::with_capacity(1 << 20, w);
77 let stats = emit(content, opts, &mut buffered)?;
78 buffered.flush()?;
81 Ok(stats)
82}
83
84fn write_synthesized<W: std::io::Write>(
94 out: &mut W,
95 line: String,
96 targets_ifc2x3: bool,
97 slot_fill: &mut crate::schema_ifc2x3_slots::Ifc2x3SlotFill,
98) -> std::io::Result<()> {
99 let line = if targets_ifc2x3 { slot_fill.apply(line) } else { line };
100 out.write_all(line.as_bytes())?;
101 out.write_all(b"\n")
102}
103
104#[allow(clippy::type_complexity)]
105fn emit<W: std::io::Write>(
106 content: &[u8],
107 opts: &StepOptions,
108 out: &mut W,
109) -> std::io::Result<StepStats> {
110 let mut order: Vec<u32> = Vec::new();
112 let mut line_of: HashMap<u32, (usize, usize)> = HashMap::new();
113 let mut max_id = 0u32;
114 let mut owner_histories: Vec<u32> = Vec::new();
115 let mut scanner = EntityScanner::new(content);
116 while let Some((id, type_name, start, end)) = scanner.next_entity() {
117 max_id = max_id.max(id);
118 if type_name.eq_ignore_ascii_case("IFCOWNERHISTORY") {
119 owner_histories.push(id);
120 }
121 if line_of.insert(id, (start, end)).is_none() {
122 order.push(id);
123 }
124 }
125
126 let mut refused_refs = 0usize;
128 let mut attribute_edits_refused = 0usize;
129 let included: HashSet<u32> = match &opts.included {
130 None => order.iter().copied().collect(),
131 Some(roots) => {
132 let mut keep: HashSet<u32> = HashSet::new();
133 let mut stack: Vec<u32> = roots.clone();
134 let mut refs = Vec::new();
135 while let Some(id) = stack.pop() {
136 if !keep.insert(id) {
137 continue;
138 }
139 if let Some(&(s, e)) = line_of.get(&id) {
140 refs.clear();
141 refs_in_line_counted(&content[s..e], &mut refs, &mut refused_refs);
142 for &r in &refs {
143 if !keep.contains(&r) {
144 stack.push(r);
145 }
146 }
147 }
148 }
149 keep
150 }
151 };
152
153 let source_header = crate::source_header::parse_source_header(content);
156 let source_schema = detect_schema(content);
157 let schema = opts.schema.clone().unwrap_or_else(|| source_schema.clone());
158 let converting = opts.schema.is_some()
160 && crate::schema_convert::needs_conversion(&source_schema, &schema);
161 let mut slot_fill = crate::schema_ifc2x3_slots::Ifc2x3SlotFill::new(
165 owner_histories.into_iter().find(|id| included.contains(id)),
166 );
167 let mut checks = crate::schema_enum::ConversionChecks::new();
169 let targets_ifc2x3 = crate::schema_convert::targets_ifc2x3(&schema);
174
175 let mut muts_by_id: HashMap<u32, BTreeMap<usize, String>> = HashMap::new();
180 for m in &opts.attribute_mutations {
181 muts_by_id.entry(m.express_id).or_default().insert(m.index, m.value.clone());
182 }
183
184 let resolved = crate::step_cow::resolve(
188 &opts.copy_on_write,
189 content,
190 &line_of,
191 &included,
192 &muts_by_id,
193 max_id.checked_add(1),
194 );
195 let next_id = resolved.next_id;
196 let copies_refused = resolved.refused;
197 let copies = resolved.copies;
198 let repointed = resolved.repointed;
199
200 let declared = source_header
205 .as_ref()
206 .and_then(|h| h.schema_identifiers.first())
207 .filter(|s| !s.is_empty());
208 let header_schema = match declared {
209 Some(token) if !converting => token.as_str(),
210 _ => crate::file_schema::file_schema_identifier(&schema),
211 };
212 crate::step_header::write_header(out, opts, source_header.as_ref(), header_schema)?;
213
214 let mut written = 0usize;
215 for id in &order {
216 if included.contains(id) {
217 if let Some(&(s, e)) = line_of.get(id) {
218 let raw = String::from_utf8_lossy(&content[s..e]);
219 let edited = match merge_edits(muts_by_id.get(id), repointed.get(id)) {
221 Some(edits) => {
222 apply_attr_mutations_counted(&raw, &edits, &mut attribute_edits_refused)
223 }
224 None => raw.into_owned(),
225 };
226 if converting {
227 let converted = crate::schema_convert::convert_step_line(
228 &edited,
229 &source_schema,
230 &schema,
231 *id,
232 &mut slot_fill,
233 Some(&mut checks),
234 )?;
235 out.write_all(converted.as_bytes())?;
236 } else {
237 out.write_all(edited.as_bytes())?;
238 }
239 out.write_all(b"\n")?;
240 written += 1;
241 }
242 }
243 }
244
245 for (copy_id, source_id, edits) in &copies {
248 if let Some(&(s0, e0)) = line_of.get(source_id) {
249 let raw = String::from_utf8_lossy(&content[s0..e0]);
250 let mut muts = muts_by_id.get(source_id).cloned().unwrap_or_default();
255 muts.extend(edits.iter().map(|(i, v)| (*i, v.clone())));
256 let edited = apply_attr_mutations_counted(&raw, &muts, &mut attribute_edits_refused);
257 let renumbered = renumber(&edited, *copy_id);
258 if converting {
259 let converted = crate::schema_convert::convert_step_line(
260 &renumbered,
261 &source_schema,
262 &schema,
263 *copy_id,
264 &mut slot_fill,
265 Some(&mut checks),
266 )?;
267 out.write_all(converted.as_bytes())?;
268 } else {
269 out.write_all(renumbered.as_bytes())?;
270 }
271 out.write_all(b"\n")?;
272 written += 1;
273 }
274 }
275
276 if !opts.property_mutations.is_empty() {
278 let mut groups: Vec<((u32, String), Vec<(&str, &str)>)> = Vec::new();
280 let mut index_of: HashMap<(u32, String), usize> = HashMap::new();
281 for m in &opts.property_mutations {
282 if !included.contains(&m.express_id) {
284 continue;
285 }
286 let key = (m.express_id, m.pset_name.clone());
287 let idx = *index_of.entry(key.clone()).or_insert_with(|| {
288 groups.push((key.clone(), Vec::new()));
289 groups.len() - 1
290 });
291 groups[idx].1.push((m.prop_name.as_str(), m.value.as_str()));
292 }
293
294 let Some(mut next) = next_id else {
297 out.write_all(b"ENDSEC;\nEND-ISO-10303-21;\n")?;
298 return Ok(StepStats {
299 total: order.len(),
300 written,
301 copies_refused,
302 refused_refs,
303 attribute_edits_refused,
304 owner_history_unfilled: slot_fill.owner_history_unfilled(),
305 required_slots_unfilled: slot_fill.required_slots_unfilled(),
306 ifc4_required_slots_unfilled: checks.ifc4_slots.required_slots_unfilled(),
307 enum_values_lost: checks.enums.lost(),
308 enum_values_refused: checks.enums.refused(),
309 });
310 };
311 for ((express_id, pset_name), props) in &groups {
312 let needed = u32::try_from(props.len()).ok().and_then(|n| n.checked_add(2));
319 match needed.and_then(|n| u32::MAX.checked_sub(n).map(|limit| next <= limit)) {
320 Some(true) => {}
321 _ => continue,
322 }
323 let mut prop_refs: Vec<u32> = Vec::with_capacity(props.len());
324 for (pname, value) in props {
325 let line = format!(
326 "#{next}=IFCPROPERTYSINGLEVALUE('{}',$,{},$);",
327 escape(pname),
328 value
329 );
330 write_synthesized(out, line, targets_ifc2x3, &mut slot_fill)?;
331 prop_refs.push(next);
332 next += 1;
333 written += 1;
334 }
335 let psid = next;
336 next += 1;
337 let refs_str = prop_refs.iter().map(|r| format!("#{r}")).collect::<Vec<_>>().join(",");
338 let line = format!(
339 "#{psid}=IFCPROPERTYSET('{}',$,'{}',$,({}));",
340 crate::schema_convert::placeholder_guid(psid),
341 escape(pset_name),
342 refs_str
343 );
344 write_synthesized(out, line, targets_ifc2x3, &mut slot_fill)?;
345 written += 1;
346 let rid = next;
347 next += 1;
348 let line = format!(
349 "#{rid}=IFCRELDEFINESBYPROPERTIES('{}',$,$,$,(#{express_id}),#{psid});",
350 crate::schema_convert::placeholder_guid(rid),
351 );
352 write_synthesized(out, line, targets_ifc2x3, &mut slot_fill)?;
353 written += 1;
354 }
355 }
356
357 out.write_all(b"ENDSEC;\nEND-ISO-10303-21;\n")?;
358
359 Ok(StepStats {
360 total: order.len(),
361 written,
362 copies_refused,
363 refused_refs,
364 attribute_edits_refused,
365 owner_history_unfilled: slot_fill.owner_history_unfilled(),
366 required_slots_unfilled: slot_fill.required_slots_unfilled(),
367 ifc4_required_slots_unfilled: checks.ifc4_slots.required_slots_unfilled(),
368 enum_values_lost: checks.enums.lost(),
369 enum_values_refused: checks.enums.refused(),
370 })
371}
372
373#[cfg(test)]
374#[path = "step_tests.rs"]
375mod tests;
376
377#[cfg(test)]
378#[path = "step_roundtrip_tests.rs"]
379mod roundtrip_tests;