1use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages};
11use crate::error::{Error, Result};
12use crate::geospatial::xml_tree::{XmlElement, XmlLimits, parse_xml_tree};
13use crate::table::{TableAlign, TableData};
14
15const MAX_PLMXML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_PLMXML_EVENTS: usize = 1_000_000;
17const MAX_PLMXML_NODES: usize = 500_000;
18const MAX_PLMXML_DEPTH: usize = 128;
19const MAX_PLMXML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_PLMXML_ROWS: usize = 200_000;
21const MAX_PLMXML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25 schema_version: String,
26 product_defs: usize,
27 parts: usize,
28 structures: usize,
29 instances: usize,
30 representations: usize,
31 geometry: usize,
32 construction_geometry: usize,
33 attributes: usize,
34 external_refs: usize,
35 rows: Vec<Vec<String>>,
36}
37
38struct PlmXmlPageSink<'a> {
39 inner: &'a mut dyn PageConsumer,
40 warnings: &'a [String],
41}
42
43impl PageConsumer for PlmXmlPageSink<'_> {
44 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
45 page.source_format = "plmxml".into();
46 if page.title.is_empty() {
47 page.title = "PLMXML product structure".into();
48 }
49 page.description = "PLMXML product and assembly metadata are rendered as bounded inert rows; referenced CAD geometry and external resources are not opened".into();
50 for warning in self.warnings {
51 page.warn(warning.clone());
52 }
53 self.inner.consume(page)
54 }
55}
56
57pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
58 let text = String::from_utf8_lossy(prefix).to_ascii_lowercase();
59 text.contains("<plmxml") && text.contains("plmxml.org/schemas/plmxmlschema")
60}
61
62pub(crate) fn convert(
63 path: &Path,
64 options: &ConvertOptions,
65 sink: &mut dyn PageConsumer,
66) -> Result<Vec<String>> {
67 let bytes = read_limited_file(
68 path,
69 options.max_input_bytes.min(MAX_PLMXML_BYTES),
70 "PLMXML input",
71 )?;
72 let root = parse_xml_tree(
73 &bytes,
74 &XmlLimits {
75 max_events: options.max_xml_events.min(MAX_PLMXML_EVENTS),
76 max_nodes: MAX_PLMXML_NODES,
77 max_depth: MAX_PLMXML_DEPTH,
78 max_text_bytes: MAX_PLMXML_TEXT_BYTES,
79 },
80 "PLMXML",
81 )?;
82 if !root.name.eq_ignore_ascii_case("PLMXML") {
83 return Err(Error::InvalidInput(
84 "PLMXML document must have a PLMXML root".into(),
85 ));
86 }
87 if !root.namespace.as_deref().is_some_and(|namespace| {
88 namespace.eq_ignore_ascii_case("http://www.plmxml.org/Schemas/PLMXMLSchema")
89 }) {
90 return Err(Error::InvalidInput(
91 "PLMXML root uses an unsupported namespace".into(),
92 ));
93 }
94 let mut summary = Summary {
95 schema_version: attr_local(&root, "schemaVersion")
96 .unwrap_or_default()
97 .to_owned(),
98 ..Summary::default()
99 };
100 summary.product_defs = count_named(&root, "ProductDef");
101 summary.parts = count_named(&root, "Part");
102 summary.structures = count_named(&root, "Structure");
103 summary.instances = count_named(&root, "Instance");
104 summary.representations = count_named(&root, "Representation");
105 summary.geometry = count_named(&root, "Geometry");
106 summary.construction_geometry = count_named(&root, "ConstructionGeometry");
107 summary.attributes = count_named(&root, "Attribute");
108 summary.external_refs =
109 count_named(&root, "ExternalReference").saturating_add(count_named(&root, "ExternalData"));
110 push_row(
111 &mut summary.rows,
112 "Document",
113 "PLMXML",
114 &format!(
115 "schemaVersion={} units=metres",
116 display_or_dash(&summary.schema_version)
117 ),
118 )?;
119 push_row(
120 &mut summary.rows,
121 "Product structure",
122 &summary.product_defs.to_string(),
123 &format!(
124 "parts={} structures={} instances={}",
125 summary.parts, summary.structures, summary.instances
126 ),
127 )?;
128 push_row(
129 &mut summary.rows,
130 "Representations",
131 &summary.representations.to_string(),
132 &format!(
133 "geometry={} constructionGeometry={}",
134 summary.geometry, summary.construction_geometry
135 ),
136 )?;
137 push_row(
138 &mut summary.rows,
139 "Metadata",
140 &summary.attributes.to_string(),
141 &format!("externalReferences={}", summary.external_refs),
142 )?;
143 let blocks = vec![
144 HtmlBlock::Heading {
145 level: 1,
146 text: "PLMXML product structure".into(),
147 },
148 HtmlBlock::Paragraph {
149 text: "PLMXML references product lifecycle and CAD data. This preview keeps the structure inert and does not resolve referenced geometry.".into(),
150 },
151 HtmlBlock::Table(TableData {
152 headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
153 rows: summary.rows,
154 alignments: vec![TableAlign::Left; 3],
155 raw_source: String::new(),
156 }),
157 ];
158 let warnings = vec![
159 "PLMXML product/assembly structure and representation counts are shown; IDs, attribute values, URLs and external reference payloads are omitted or redacted".into(),
160 "PLMXML geometry, XT/Parasolid files, tessellation, formulas, signatures and network resources are never opened or evaluated".into(),
161 ];
162 let mut page_sink = PlmXmlPageSink {
163 inner: sink,
164 warnings: &warnings,
165 };
166 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
167 Ok(warnings)
168}
169
170fn attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
171 element.attributes.iter().find_map(|(key, value)| {
172 key.rsplit(':')
173 .next()
174 .filter(|local| local.eq_ignore_ascii_case(name))
175 .map(|_| value.as_str())
176 })
177}
178
179fn count_named(element: &XmlElement, name: &str) -> usize {
180 element
181 .children
182 .iter()
183 .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
184 .sum()
185}
186
187fn display_or_dash(value: &str) -> String {
188 if value.is_empty() {
189 "-".into()
190 } else {
191 truncate(value)
192 }
193}
194
195fn truncate(value: &str) -> String {
196 if value.len() <= MAX_PLMXML_DISPLAY_BYTES {
197 value.to_owned()
198 } else {
199 let mut end = MAX_PLMXML_DISPLAY_BYTES;
200 while !value.is_char_boundary(end) {
201 end -= 1;
202 }
203 format!("{}…", &value[..end])
204 }
205}
206
207fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
208 if rows.len() >= MAX_PLMXML_ROWS {
209 return Err(Error::LimitExceeded(format!(
210 "PLMXML rows exceed {MAX_PLMXML_ROWS}"
211 )));
212 }
213 rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
214 Ok(())
215}