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_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}