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_SBML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_SBML_EVENTS: usize = 1_000_000;
17const MAX_SBML_NODES: usize = 500_000;
18const MAX_SBML_DEPTH: usize = 128;
19const MAX_SBML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_SBML_ROWS: usize = 200_000;
21const MAX_SBML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25 level: String,
26 version: String,
27 models: usize,
28 compartments: usize,
29 species: usize,
30 reactions: usize,
31 parameters: usize,
32 rules: usize,
33 events: usize,
34 units: usize,
35 functions: usize,
36 annotations: usize,
37 rows: Vec<Vec<String>>,
38}
39
40struct SbmlPageSink<'a> {
41 inner: &'a mut dyn PageConsumer,
42 warnings: &'a [String],
43}
44
45impl PageConsumer for SbmlPageSink<'_> {
46 fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
47 page.source_format = "sbml".into();
48 if page.title.is_empty() {
49 page.title = "SBML model".into();
50 }
51 page.description =
52 "SBML model structure is rendered as bounded inert metadata; equations and simulation values are not evaluated".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"sbml", None)
62 && String::from_utf8_lossy(prefix)
63 .to_ascii_lowercase()
64 .contains("sbml.org/sbml")
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_SBML_BYTES),
75 "SBML input",
76 )?;
77 let root = parse_xml_tree(
78 &bytes,
79 &XmlLimits {
80 max_events: options.max_xml_events.min(MAX_SBML_EVENTS),
81 max_nodes: MAX_SBML_NODES,
82 max_depth: MAX_SBML_DEPTH,
83 max_text_bytes: MAX_SBML_TEXT_BYTES,
84 },
85 "SBML",
86 )?;
87 if !root.name.eq_ignore_ascii_case("sbml") {
88 return Err(Error::InvalidInput("SBML root must be <sbml>".into()));
89 }
90 if root
91 .namespace
92 .as_deref()
93 .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("sbml.org/sbml"))
94 {
95 return Err(Error::InvalidInput(
96 "SBML root uses an unsupported namespace".into(),
97 ));
98 }
99 let mut summary = Summary {
100 level: attr_local(&root, "level").map(truncate).unwrap_or_default(),
101 version: attr_local(&root, "version")
102 .map(truncate)
103 .unwrap_or_default(),
104 models: count_named(&root, "model"),
105 compartments: count_named(&root, "compartment"),
106 species: count_named(&root, "species"),
107 reactions: count_named(&root, "reaction"),
108 parameters: count_named(&root, "parameter"),
109 rules: count_named(&root, "assignmentRule")
110 + count_named(&root, "rateRule")
111 + count_named(&root, "algebraicRule"),
112 events: count_named(&root, "event"),
113 units: count_named(&root, "unitDefinition"),
114 functions: count_named(&root, "functionDefinition"),
115 annotations: count_named(&root, "annotation") + count_named(&root, "notes"),
116 ..Summary::default()
117 };
118 push_row(
119 &mut summary.rows,
120 "Model",
121 &summary.models.to_string(),
122 &format!(
123 "level={} version={}",
124 display_or_dash(&summary.level),
125 display_or_dash(&summary.version)
126 ),
127 )?;
128 push_row(
129 &mut summary.rows,
130 "Network",
131 &format!("species={}", summary.species),
132 &format!(
133 "compartments={} reactions={}",
134 summary.compartments, summary.reactions
135 ),
136 )?;
137 push_row(
138 &mut summary.rows,
139 "Parameters",
140 &summary.parameters.to_string(),
141 &format!(
142 "rules={} events={} units={}",
143 summary.rules, summary.events, summary.units
144 ),
145 )?;
146 push_row(
147 &mut summary.rows,
148 "Extensions",
149 &summary.functions.to_string(),
150 &format!("annotations={} package values omitted", summary.annotations),
151 )?;
152 let blocks = vec![
153 HtmlBlock::Heading {
154 level: 1,
155 text: "SBML model".into(),
156 },
157 HtmlBlock::Paragraph {
158 text: "Systems Biology Markup Language model structure is summarized without evaluating equations, parameters or simulations.".into(),
159 },
160 HtmlBlock::Table(TableData {
161 headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
162 rows: summary.rows,
163 alignments: vec![TableAlign::Left; 3],
164 raw_source: String::new(),
165 }),
166 ];
167 let warnings = vec![
168 "SBML species, parameter, compartment, equation, annotation, identifier and model values are omitted or redacted; only bounded structure is shown".into(),
169 "SBML MathML, package schemas, annotations, external resources, rule evaluation and numerical simulation never run".into(),
170 ];
171 let mut page_sink = SbmlPageSink {
172 inner: sink,
173 warnings: &warnings,
174 };
175 render_blocks_to_pages(&blocks, &mut page_sink, options)?;
176 Ok(warnings)
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 attr_local<'a>(element: &'a XmlElement, name: &str) -> Option<&'a str> {
188 element.attributes.iter().find_map(|(key, value)| {
189 key.rsplit(':')
190 .next()
191 .filter(|local| local.eq_ignore_ascii_case(name))
192 .map(|_| value.as_str())
193 })
194}
195
196fn display_or_dash(value: &str) -> &str {
197 if value.is_empty() { "—" } else { value }
198}
199
200fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
201 if rows.len() >= MAX_SBML_ROWS {
202 return Err(Error::LimitExceeded(format!(
203 "SBML rows exceed {MAX_SBML_ROWS}"
204 )));
205 }
206 rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
207 Ok(())
208}
209
210fn truncate(value: &str) -> String {
211 if value.len() <= MAX_SBML_DISPLAY_BYTES {
212 value.to_owned()
213 } else {
214 let mut end = MAX_SBML_DISPLAY_BYTES;
215 while end > 0 && !value.is_char_boundary(end) {
216 end -= 1;
217 }
218 format!("{}…", &value[..end])
219 }
220}