Skip to main content

document_svg/document/
plmxml.rs

1//! Bounded Siemens PLM XML product-structure metadata preview.
2//!
3//! PLMXML carries product definitions, assembly structures and references to
4//! CAD representations. The converter reports that structure without opening
5//! referenced XT/Parasolid files, URLs or geometry payloads.
6
7use 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}