Skip to main content

document_svg/document/
cellml.rs

1//! Bounded CellML model previews.
2//!
3//! CellML describes reusable computational physiology models. This adapter
4//! reports component/variable/connection structure without evaluating MathML,
5//! units, imports or simulation experiments.
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_CELLML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_CELLML_EVENTS: usize = 1_000_000;
17const MAX_CELLML_NODES: usize = 500_000;
18const MAX_CELLML_DEPTH: usize = 128;
19const MAX_CELLML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_CELLML_ROWS: usize = 200_000;
21const MAX_CELLML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25    models: usize,
26    components: usize,
27    variables: usize,
28    units: usize,
29    connections: usize,
30    mappings: usize,
31    imports: usize,
32    encapsulations: usize,
33    resets: usize,
34    maths: usize,
35    annotations: usize,
36    rows: Vec<Vec<String>>,
37}
38
39struct CellmlPageSink<'a> {
40    inner: &'a mut dyn PageConsumer,
41    warnings: &'a [String],
42}
43
44impl PageConsumer for CellmlPageSink<'_> {
45    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
46        page.source_format = "cellml".into();
47        if page.title.is_empty() {
48            page.title = "CellML model".into();
49        }
50        page.description =
51            "CellML model structure is rendered as bounded inert metadata; equations, imports and simulations are not evaluated".into();
52        for warning in self.warnings {
53            page.warn(warning.clone());
54        }
55        self.inner.consume(page)
56    }
57}
58
59pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
60    crate::geospatial::xml_tree::looks_like_root(prefix, b"model", None)
61        && String::from_utf8_lossy(prefix)
62            .to_ascii_lowercase()
63            .contains("cellml.org/cellml")
64}
65
66pub(crate) fn convert(
67    path: &Path,
68    options: &ConvertOptions,
69    sink: &mut dyn PageConsumer,
70) -> Result<Vec<String>> {
71    let bytes = read_limited_file(
72        path,
73        options.max_input_bytes.min(MAX_CELLML_BYTES),
74        "CellML input",
75    )?;
76    let root = parse_xml_tree(
77        &bytes,
78        &XmlLimits {
79            max_events: options.max_xml_events.min(MAX_CELLML_EVENTS),
80            max_nodes: MAX_CELLML_NODES,
81            max_depth: MAX_CELLML_DEPTH,
82            max_text_bytes: MAX_CELLML_TEXT_BYTES,
83        },
84        "CellML",
85    )?;
86    if !root.name.eq_ignore_ascii_case("model") {
87        return Err(Error::InvalidInput("CellML root must be <model>".into()));
88    }
89    if root
90        .namespace
91        .as_deref()
92        .is_none_or(|namespace| !namespace.to_ascii_lowercase().contains("cellml.org/cellml"))
93    {
94        return Err(Error::InvalidInput(
95            "CellML root uses an unsupported namespace".into(),
96        ));
97    }
98    let mut summary = Summary {
99        models: 1,
100        components: count_named(&root, "component"),
101        variables: count_named(&root, "variable"),
102        units: count_named(&root, "units") + count_named(&root, "unit"),
103        connections: count_named(&root, "connection"),
104        mappings: count_named(&root, "map_components") + count_named(&root, "map_variables"),
105        imports: count_named(&root, "import"),
106        encapsulations: count_named(&root, "encapsulation")
107            + count_named(&root, "encapsulation_2_0"),
108        resets: count_named(&root, "reset"),
109        maths: count_named(&root, "math"),
110        annotations: count_named(&root, "rdf") + count_named(&root, "annotation"),
111        ..Summary::default()
112    };
113    push_row(
114        &mut summary.rows,
115        "Model",
116        &summary.models.to_string(),
117        &format!(
118            "components={} variables={}",
119            summary.components, summary.variables
120        ),
121    )?;
122    push_row(
123        &mut summary.rows,
124        "Units",
125        &summary.units.to_string(),
126        &format!(
127            "connections={} mappings={}",
128            summary.connections, summary.mappings
129        ),
130    )?;
131    push_row(
132        &mut summary.rows,
133        "Imports",
134        &summary.imports.to_string(),
135        &format!(
136            "encapsulation={} resets={}",
137            summary.encapsulations, summary.resets
138        ),
139    )?;
140    push_row(
141        &mut summary.rows,
142        "Equations",
143        &summary.maths.to_string(),
144        &format!("annotations={} MathML omitted", summary.annotations),
145    )?;
146    let blocks = vec![
147        HtmlBlock::Heading {
148            level: 1,
149            text: "CellML model".into(),
150        },
151        HtmlBlock::Paragraph {
152            text: "CellML component and connection structure is summarized without evaluating MathML or simulation metadata.".into(),
153        },
154        HtmlBlock::Table(TableData {
155            headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
156            rows: summary.rows,
157            alignments: vec![TableAlign::Left; 3],
158            raw_source: String::new(),
159        }),
160    ];
161    let warnings = vec![
162        "CellML component names, variable values, units, MathML equations, annotations, URLs and private model data are omitted or redacted".into(),
163        "CellML imports, external models, MathML, metadata schemas and simulation experiments are never fetched or executed".into(),
164    ];
165    let mut page_sink = CellmlPageSink {
166        inner: sink,
167        warnings: &warnings,
168    };
169    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
170    Ok(warnings)
171}
172
173fn count_named(element: &XmlElement, name: &str) -> usize {
174    element
175        .children
176        .iter()
177        .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
178        .sum()
179}
180
181fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
182    if rows.len() >= MAX_CELLML_ROWS {
183        return Err(Error::LimitExceeded(format!(
184            "CellML rows exceed {MAX_CELLML_ROWS}"
185        )));
186    }
187    rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
188    Ok(())
189}
190
191fn truncate(value: &str) -> String {
192    if value.len() <= MAX_CELLML_DISPLAY_BYTES {
193        value.to_owned()
194    } else {
195        let mut end = MAX_CELLML_DISPLAY_BYTES;
196        while end > 0 && !value.is_char_boundary(end) {
197            end -= 1;
198        }
199        format!("{}…", &value[..end])
200    }
201}