Skip to main content

document_svg/document/
sbml.rs

1//! Bounded Systems Biology Markup Language (SBML) previews.
2//!
3//! SBML models describe biological reaction networks and simulation metadata.
4//! The preview reports model structure only; mathematical expressions,
5//! parameter values, annotations and simulation engines remain inert.
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_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}