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_XPDL_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_XPDL_EVENTS: usize = 1_000_000;
17const MAX_XPDL_NODES: usize = 500_000;
18const MAX_XPDL_DEPTH: usize = 128;
19const MAX_XPDL_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_XPDL_ROWS: usize = 200_000;
21const MAX_XPDL_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25 version: String,
26 packages: usize,
27 processes: usize,
28 activities: usize,
29 transitions: usize,
30 applications: usize,
31 participants: usize,
32 data_fields: usize,
33 pools: usize,
34 lanes: usize,
35 connectors: usize,
36 external_refs: usize,
37 rows: Vec<Vec<String>>,
38}
39
40struct XpdlPageSink<'a> {
41 inner: &'a mut dyn PageConsumer,
42 warnings: &'a [String],
43}
44
45impl PageConsumer for XpdlPageSink<'_> {
46 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
47 page.source_format = "xpdl".into();
48 if page.title.is_empty() {
49 page.title = "XPDL workflow definition".into();
50 }
51 page.description =
52 "XPDL workflow structure is rendered as bounded inert metadata; expressions, scripts and external applications are not executed".into();
53 for warning in self.warnings {
54 page.warn(warning.clone());
55 }
56 self.inner.consume(page)
57 }
58}
59
60pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
61 crate::geospatial::xml_tree::looks_like_root(prefix, b"Package", None)
62 && String::from_utf8_lossy(prefix)
63 .to_ascii_lowercase()
64 .contains("wfmc.org")
65}
66
67pub(crate) fn convert(
68 path: &Path,
69 options: &ConvertOptions,
70 sink: &mut dyn PageConsumer,
71) -> Result<Vec<String>> {
72 let bytes = read_limited_file(
73 path,
74 options.max_input_bytes.min(MAX_XPDL_BYTES),
75 "XPDL input",
76 )?;
77 let root = parse_xml_tree(
78 &bytes,
79 &XmlLimits {
80 max_events: options.max_xml_events.min(MAX_XPDL_EVENTS),
81 max_nodes: MAX_XPDL_NODES,
82 max_depth: MAX_XPDL_DEPTH,
83 max_text_bytes: MAX_XPDL_TEXT_BYTES,
84 },
85 "XPDL",
86 )?;
87 if !root.name.eq_ignore_ascii_case("Package") {
88 return Err(Error::InvalidInput("XPDL root must be <Package>".into()));
89 }
90 if root
91 .namespace
92 .as_deref()
93 .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("wfmc.org"))
94 {
95 return Err(Error::InvalidInput(
96 "XPDL root uses an unsupported namespace".into(),
97 ));
98 }
99 let mut summary = Summary {
100 version: attr_local(&root, "xpdlVersion")
101 .or_else(|| attr_local(&root, "version"))
102 .map(truncate)
103 .unwrap_or_default(),
104 packages: 1,
105 processes: count_named(&root, "WorkflowProcess"),
106 activities: count_named(&root, "Activity"),
107 transitions: count_named(&root, "Transition"),
108 applications: count_named(&root, "Application"),
109 participants: count_named(&root, "Participant"),
110 data_fields: count_named(&root, "DataField") + count_named(&root, "FormalParameter"),
111 pools: count_named(&root, "Pool"),
112 lanes: count_named(&root, "Lane"),
113 connectors: count_named(&root, "Connector") + count_named(&root, "ExtendedAttribute"),
114 external_refs: count_named(&root, "ExternalReference")
115 + count_named(&root, "ExternalPackage"),
116 ..Summary::default()
117 };
118 push_row(
119 &mut summary.rows,
120 "Package",
121 &summary.packages.to_string(),
122 &format!("version={}", display_or_dash(&summary.version)),
123 )?;
124 push_row(
125 &mut summary.rows,
126 "Workflow",
127 &summary.processes.to_string(),
128 &format!(
129 "activities={} transitions={}",
130 summary.activities, summary.transitions
131 ),
132 )?;
133 push_row(
134 &mut summary.rows,
135 "Roles",
136 &summary.participants.to_string(),
137 &format!("pools={} lanes={}", summary.pools, summary.lanes),
138 )?;
139 push_row(
140 &mut summary.rows,
141 "Integration",
142 &summary.applications.to_string(),
143 &format!(
144 "dataFields={} connectors={} externalRefs={}",
145 summary.data_fields, summary.connectors, summary.external_refs
146 ),
147 )?;
148 let blocks = vec![
149 HtmlBlock::Heading {
150 level: 1,
151 text: "XPDL workflow definition".into(),
152 },
153 HtmlBlock::Paragraph {
154 text: "Workflow Process Definition Language structure is summarized for model review; expressions, scripts, applications and process execution remain inert.".into(),
155 },
156 HtmlBlock::Table(TableData {
157 headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
158 rows: summary.rows,
159 alignments: vec![TableAlign::Left; 3],
160 raw_source: String::new(),
161 }),
162 ];
163 let warnings = vec![
164 "XPDL process IDs, labels, expressions, scripts, URLs, application parameters and private values are omitted or redacted; only bounded structural counts are shown".into(),
165 "XPDL external packages, application services, scripts, expressions, schemas and workflow engines are never opened or executed".into(),
166 ];
167 let mut page_sink = XpdlPageSink {
168 inner: sink,
169 warnings: &warnings,
170 };
171 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
172 Ok(warnings)
173}
174
175fn count_named(element: &XmlElement, name: &str) -> usize {
176 element
177 .children
178 .iter()
179 .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
180 .sum()
181}
182
183fn attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
184 element.attributes.iter().find_map(|(key, value)| {
185 key.rsplit(':')
186 .next()
187 .filter(|local| local.eq_ignore_ascii_case(name))
188 .map(|_| value.as_str())
189 })
190}
191
192fn display_or_dash(value: &str) -> &str {
193 if value.is_empty() { "—" } else { value }
194}
195
196fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
197 if rows.len() >= MAX_XPDL_ROWS {
198 return Err(Error::LimitExceeded(format!(
199 "XPDL rows exceed {MAX_XPDL_ROWS}"
200 )));
201 }
202 rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
203 Ok(())
204}
205
206fn truncate(value: &str) -> String {
207 if value.len() <= MAX_XPDL_DISPLAY_BYTES {
208 value.to_owned()
209 } else {
210 let mut end = MAX_XPDL_DISPLAY_BYTES;
211 while end > 0 && !value.is_char_boundary(end) {
212 end -= 1;
213 }
214 format!("{}…", &value[..end])
215 }
216}