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_SEDML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_SEDML_EVENTS: usize = 1_000_000;
17const MAX_SEDML_NODES: usize = 500_000;
18const MAX_SEDML_DEPTH: usize = 128;
19const MAX_SEDML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_SEDML_ROWS: usize = 200_000;
21const MAX_SEDML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25 level: String,
26 version: String,
27 models: usize,
28 simulations: usize,
29 tasks: usize,
30 repeated_tasks: usize,
31 data_descriptions: usize,
32 data_generators: usize,
33 outputs: usize,
34 plots: usize,
35 reports: usize,
36 changes: usize,
37 ranges: usize,
38 variables: usize,
39 parameters: usize,
40 algorithms: usize,
41 rows: Vec<Vec<String>>,
42}
43
44struct SedmlPageSink<'a> {
45 inner: &'a mut dyn PageConsumer,
46 warnings: &'a [String],
47}
48
49impl PageConsumer for SedmlPageSink<'_> {
50 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
51 page.source_format = "sedml".into();
52 if page.title.is_empty() {
53 page.title = "SED-ML experiment".into();
54 }
55 page.description =
56 "SED-ML experiment structure is rendered as bounded inert metadata; models, algorithms and simulations are not executed".into();
57 for warning in self.warnings {
58 page.warn(warning.clone());
59 }
60 self.inner.consume(page)
61 }
62}
63
64pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
65 crate::geospatial::xml_tree::looks_like_root(prefix, b"sedML", None)
66 && String::from_utf8_lossy(prefix)
67 .to_ascii_lowercase()
68 .contains("sed-ml.org/sed-ml")
69}
70
71pub(crate) fn convert(
72 path: &Path,
73 options: &ConvertOptions,
74 sink: &mut dyn PageConsumer,
75) -> Result<Vec<String>> {
76 let bytes = read_limited_file(
77 path,
78 options.max_input_bytes.min(MAX_SEDML_BYTES),
79 "SED-ML input",
80 )?;
81 let root = parse_xml_tree(
82 &bytes,
83 &XmlLimits {
84 max_events: options.max_xml_events.min(MAX_SEDML_EVENTS),
85 max_nodes: MAX_SEDML_NODES,
86 max_depth: MAX_SEDML_DEPTH,
87 max_text_bytes: MAX_SEDML_TEXT_BYTES,
88 },
89 "SED-ML",
90 )?;
91 if !root.name.eq_ignore_ascii_case("sedML") {
92 return Err(Error::InvalidInput("SED-ML root must be <sedML>".into()));
93 }
94 if root
95 .namespace
96 .as_deref()
97 .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("sed-ml.org/sed-ml"))
98 {
99 return Err(Error::InvalidInput(
100 "SED-ML root uses an unsupported namespace".into(),
101 ));
102 }
103 let mut summary = Summary {
104 level: attr_local(&root, "level").map(truncate).unwrap_or_default(),
105 version: attr_local(&root, "version")
106 .map(truncate)
107 .unwrap_or_default(),
108 models: count_named(&root, "model"),
109 simulations: count_named(&root, "simulation")
110 + count_named(&root, "uniformTimeCourse")
111 + count_named(&root, "oneStep")
112 + count_named(&root, "steadyState")
113 + count_named(&root, "uniformTimeCourseSimulation"),
114 tasks: count_named(&root, "task"),
115 repeated_tasks: count_named(&root, "repeatedTask"),
116 data_descriptions: count_named(&root, "dataDescription"),
117 data_generators: count_named(&root, "dataGenerator"),
118 outputs: count_named(&root, "output"),
119 plots: count_named(&root, "plot2D") + count_named(&root, "plot3D"),
120 reports: count_named(&root, "report"),
121 changes: count_named(&root, "changeAttribute")
122 + count_named(&root, "computeChange")
123 + count_named(&root, "addXML")
124 + count_named(&root, "removeXML")
125 + count_named(&root, "changeXML"),
126 ranges: count_named(&root, "uniformRange")
127 + count_named(&root, "vectorRange")
128 + count_named(&root, "functionalRange"),
129 variables: count_named(&root, "variable"),
130 parameters: count_named(&root, "parameter"),
131 algorithms: count_named(&root, "algorithm"),
132 ..Summary::default()
133 };
134 push_row(
135 &mut summary.rows,
136 "Experiment",
137 &format!(
138 "level={} version={}",
139 display_or_dash(&summary.level),
140 display_or_dash(&summary.version)
141 ),
142 &format!(
143 "models={} simulations={}",
144 summary.models, summary.simulations
145 ),
146 )?;
147 push_row(
148 &mut summary.rows,
149 "Tasks",
150 &summary.tasks.to_string(),
151 &format!(
152 "repeatedTasks={} changes={} ranges={}",
153 summary.repeated_tasks, summary.changes, summary.ranges
154 ),
155 )?;
156 push_row(
157 &mut summary.rows,
158 "Analysis",
159 &summary.data_generators.to_string(),
160 &format!(
161 "dataDescriptions={} variables={} parameters={}",
162 summary.data_descriptions, summary.variables, summary.parameters
163 ),
164 )?;
165 push_row(
166 &mut summary.rows,
167 "Outputs",
168 &summary.outputs.to_string(),
169 &format!(
170 "plots={} reports={} algorithms={}",
171 summary.plots, summary.reports, summary.algorithms
172 ),
173 )?;
174 let blocks = vec![
175 HtmlBlock::Heading {
176 level: 1,
177 text: "SED-ML experiment".into(),
178 },
179 HtmlBlock::Paragraph {
180 text: "Simulation Experiment Description Markup Language structure is summarized without executing models, changes, algorithms or simulations.".into(),
181 },
182 HtmlBlock::Table(TableData {
183 headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
184 rows: summary.rows,
185 alignments: vec![TableAlign::Left; 3],
186 raw_source: String::new(),
187 }),
188 ];
189 let warnings = vec![
190 "SED-ML IDs, model paths, algorithm URIs, MathML, variable/parameter values, XPath changes and output data are omitted or redacted".into(),
191 "SED-ML model imports, external resources, numerical solvers, plotting, data processing and simulation execution never run".into(),
192 ];
193 let mut page_sink = SedmlPageSink {
194 inner: sink,
195 warnings: &warnings,
196 };
197 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
198 Ok(warnings)
199}
200
201fn count_named(element: &XmlElement, name: &str) -> usize {
202 element
203 .children
204 .iter()
205 .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
206 .sum()
207}
208
209fn attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
210 element.attributes.iter().find_map(|(key, value)| {
211 key.rsplit(':')
212 .next()
213 .filter(|local| local.eq_ignore_ascii_case(name))
214 .map(|_| value.as_str())
215 })
216}
217
218fn display_or_dash(value: &str) -> &str {
219 if value.is_empty() { "—" } else { value }
220}
221
222fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
223 if rows.len() >= MAX_SEDML_ROWS {
224 return Err(Error::LimitExceeded(format!(
225 "SED-ML rows exceed {MAX_SEDML_ROWS}"
226 )));
227 }
228 rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
229 Ok(())
230}
231
232fn truncate(value: &str) -> String {
233 if value.len() <= MAX_SEDML_DISPLAY_BYTES {
234 value.to_owned()
235 } else {
236 let mut end = MAX_SEDML_DISPLAY_BYTES;
237 while end > 0 && !value.is_char_boundary(end) {
238 end -= 1;
239 }
240 format!("{}…", &value[..end])
241 }
242}