1mod element;
10mod group_link;
11mod macros;
12mod menu_mux;
13mod model;
14mod opi;
15mod opi_file_script;
16pub mod project;
17mod source;
18mod trend;
19
20use std::path::{Path, PathBuf};
21
22pub use element::{Element, MAX_DEPTH as MAX_XML_DEPTH, depth as xml_depth};
23pub use group_link::{GroupLink, LinkSets, group_device, group_frame};
24pub use macros::{MAX_LOAD_BYTES, Macros, expand, rearm_warning, spend, warn_spent};
25pub use menu_mux::{MAX_SETS as MAX_MUX_SETS, MenuMux, MuxSet};
26pub use model::{Action, Color, Font, FontStyle, Rule, RuleExpr, named_color};
27pub use source::Source;
28pub use trend::{databrowser_pvs, strip_tool_pvs};
29
30const MAX_FILE_SIZE: u64 = 16 << 20;
32
33const MAX_NODES: u32 = 1 << 20;
35
36#[derive(Clone, Debug)]
38pub struct Display {
39 pub name: String,
40 pub width: f64,
41 pub height: f64,
42 pub background: Color,
43 pub macros: Macros,
44 pub widgets: Vec<Widget>,
45 pub source: Option<Source>,
47 pub legacy: bool,
50 pub size: usize,
52}
53
54#[derive(Clone, Debug)]
56pub struct Widget {
57 pub kind: String,
58 pub props: Element,
59 pub children: Vec<Widget>,
60}
61
62#[derive(Debug)]
63pub enum Error {
64 Io(PathBuf, std::io::Error),
65 Xml(roxmltree::Error),
66 NotADisplay(String),
67 TooDeep,
69 Fetch(String, String),
71}
72
73impl std::fmt::Display for Error {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 match self {
76 Error::Io(p, e) => write!(f, "cannot read {}: {e}", p.display()),
77 Error::Xml(e) => write!(f, "invalid XML: {e}"),
78 Error::TooDeep => write!(f, "display nests elements more than {} deep", MAX_XML_DEPTH),
79 Error::NotADisplay(root) => write!(f, "expected a <display>, found <{root}>"),
80 Error::Fetch(url, why) => write!(f, "cannot fetch {url}: {why}"),
81 }
82 }
83}
84
85impl std::error::Error for Error {}
86
87impl Display {
88 pub fn load(path: &Path) -> Result<Self, Error> {
94 let real = project::locate(path);
95 use std::io::Read;
96 let io = |e| Error::Io(path.into(), e);
97 let file = std::fs::File::open(&real).map_err(io)?;
98 let mut text = String::new();
99 file.take(MAX_FILE_SIZE + 1)
100 .read_to_string(&mut text)
101 .map_err(io)?;
102 if text.len() as u64 > MAX_FILE_SIZE {
103 let why = format!("larger than {} MiB", MAX_FILE_SIZE >> 20);
104 return Err(io(std::io::Error::other(why)));
105 }
106 let mut display = Self::parse(&text)?;
107 let path = if display.legacy {
110 project::launcher_path(path).unwrap_or_else(|| path.to_path_buf())
111 } else if project::through_launcher(path) {
112 real
113 } else {
114 path.to_path_buf()
115 };
116 display.source = Some(Source::File(path));
117 Ok(display)
118 }
119
120 pub fn parse(xml: &str) -> Result<Self, Error> {
121 if xml_depth(xml) > MAX_XML_DEPTH {
123 return Err(Error::TooDeep);
124 }
125 let options = roxmltree::ParsingOptions {
126 nodes_limit: MAX_NODES,
127 ..Default::default()
128 };
129 let doc = roxmltree::Document::parse_with_options(xml, options).map_err(Error::Xml)?;
130 let mut root = Element::from_node(doc.root_element());
131 if root.name != "display" {
132 return Err(Error::NotADisplay(root.name));
133 }
134 let legacy = opi::is_opi(&root);
135 if legacy {
136 root = opi::translate(root);
137 }
138 let (props, widgets) = split_widgets(root);
139 let as_widget = Widget {
140 kind: "display".into(),
141 props,
142 children: Vec::new(),
143 };
144 Ok(Self {
145 name: as_widget.text("name").unwrap_or("").to_string(),
146 width: as_widget.f64("width").unwrap_or(800.0),
147 height: as_widget.f64("height").unwrap_or(600.0),
148 background: as_widget
149 .color("background_color")
150 .unwrap_or(named_color("Background")),
151 macros: as_widget.macros(),
152 widgets,
153 source: None,
154 legacy,
155 size: xml.len(),
156 })
157 }
158
159 pub fn resolve(&self, file: &str) -> Source {
161 match &self.source {
162 Some(s) => s.resolve(file),
163 None => Source::parse(file),
164 }
165 }
166
167 pub fn all_widgets(&self) -> Vec<&Widget> {
169 fn walk<'a>(ws: &'a [Widget], out: &mut Vec<&'a Widget>) {
170 for w in ws {
171 out.push(w);
172 walk(&w.children, out);
173 }
174 }
175 let mut out = Vec::new();
176 walk(&self.widgets, &mut out);
177 out
178 }
179}
180
181fn split_widgets(mut el: Element) -> (Element, Vec<Widget>) {
186 let mut widgets = Vec::new();
187 let mut props: Vec<Element> = Vec::new();
188 let mut index: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
190 for child in el.children.drain(..) {
191 if child.name == "widget" {
192 widgets.push(Widget::from_element(child));
193 } else if let Some(&i) = index.get(&child.name) {
194 props[i] = child;
195 } else {
196 index.insert(child.name.clone(), props.len());
197 props.push(child);
198 }
199 }
200 el.children = props;
201 (el, widgets)
202}
203
204impl Widget {
205 pub fn from_element(el: Element) -> Self {
206 let kind = el.attr("type").unwrap_or("unknown").to_string();
207 let (props, children) = split_widgets(el);
208 Self {
209 kind,
210 props,
211 children,
212 }
213 }
214
215 pub fn get(&self, name: &str) -> Option<&Element> {
217 self.props.child(name)
218 }
219
220 pub fn text(&self, name: &str) -> Option<&str> {
222 self.get(name).map(|e| e.text.as_str())
223 }
224
225 pub fn f64(&self, name: &str) -> Option<f64> {
226 self.text(name).and_then(|t| t.trim().parse().ok())
227 }
228
229 pub fn i64(&self, name: &str) -> Option<i64> {
230 self.f64(name).map(|v| v as i64)
231 }
232
233 pub fn bool(&self, name: &str) -> Option<bool> {
237 self.text(name).and_then(parse_bool)
238 }
239
240 pub fn color(&self, name: &str) -> Option<Color> {
241 self.get(name)?.child("color").map(Color::from_element)
242 }
243
244 pub fn font(&self, name: &str) -> Option<Font> {
245 self.get(name)?.child("font").map(Font::from_element)
246 }
247
248 pub fn name(&self) -> &str {
249 self.text("name").unwrap_or("")
250 }
251
252 pub fn macros(&self) -> Macros {
254 self.get("macros")
255 .map(Macros::from_element)
256 .unwrap_or_default()
257 }
258
259 pub fn items(&self) -> Vec<String> {
261 self.list("items", "item")
262 }
263
264 pub fn list(&self, outer: &str, inner: &str) -> Vec<String> {
266 self.get(outer)
267 .map(|e| {
268 e.children
269 .iter()
270 .filter(|c| c.name == inner)
271 .map(|c| c.text.clone())
272 .collect()
273 })
274 .unwrap_or_default()
275 }
276
277 pub fn points(&self) -> Vec<(f64, f64)> {
279 let Some(points) = self.get("points") else {
280 return Vec::new();
281 };
282 points
283 .children
284 .iter()
285 .filter(|p| p.name == "point")
286 .filter_map(|p| Some((p.attr("x")?.parse().ok()?, p.attr("y")?.parse().ok()?)))
287 .collect()
288 }
289
290 pub fn actions(&self) -> Vec<Action> {
291 self.get("actions")
292 .map(|a| {
293 a.children
294 .iter()
295 .filter(|c| c.name == "action")
296 .map(Action::from_element)
297 .collect()
298 })
299 .unwrap_or_default()
300 }
301
302 pub fn rules(&self) -> Vec<Rule> {
303 self.get("rules")
304 .map(|r| {
305 r.children
306 .iter()
307 .filter(|c| c.name == "rule")
308 .map(Rule::from_element)
309 .collect()
310 })
311 .unwrap_or_default()
312 }
313
314 pub fn has_scripts(&self) -> bool {
315 self.get("scripts").is_some_and(|s| !s.children.is_empty())
316 }
317}
318
319pub fn parse_bool(text: &str) -> Option<bool> {
322 match text.trim() {
323 t if t.eq_ignore_ascii_case("true") || t.eq_ignore_ascii_case("yes") || t == "1" => {
324 Some(true)
325 }
326 t if t.eq_ignore_ascii_case("false") || t.eq_ignore_ascii_case("no") || t == "0" => {
327 Some(false)
328 }
329 _ => None,
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn booleans_parse_as_phoebus_does() {
339 for (text, want) in [
340 ("true", Some(true)),
341 (" TRUE ", Some(true)),
342 ("Yes", Some(true)),
343 ("1", Some(true)),
344 ("false", Some(false)),
345 ("No", Some(false)),
346 ("0", Some(false)),
347 ("5", None),
348 ("1.0", None),
349 ("", None),
350 ] {
351 assert_eq!(parse_bool(text), want, "{text:?}");
352 }
353 }
354
355 #[test]
356 fn deeply_nested_xml_is_an_error_not_a_stack_overflow() {
357 std::thread::Builder::new()
359 .stack_size(2 * 1024 * 1024)
360 .spawn(|| {
361 let n = 50_000;
362 let xml = format!(
363 "<display>{}{}</display>",
364 "<widget type=\"group\">".repeat(n),
365 "</widget>".repeat(n)
366 );
367 assert!(matches!(Display::parse(&xml), Err(Error::TooDeep)));
368 let n = 40; let xml = format!(
371 "<display>{}{}</display>",
372 "<widget type=\"group\">".repeat(n),
373 "</widget>".repeat(n)
374 );
375 assert_eq!(Display::parse(&xml).unwrap().all_widgets().len(), n);
376 })
377 .unwrap()
378 .join()
379 .unwrap();
380 }
381
382 #[test]
383 fn many_properties_keep_the_last_value_quickly() {
384 let n = 100_000;
385 let mut xml = String::from("<display>");
386 for i in 0..n {
387 xml.push_str(&format!("<p{}>a</p{}>", i % 1000, i % 1000));
388 }
389 xml.push_str("<p0>last</p0></display>");
390 let d = Display::parse(&xml).unwrap();
391 assert!(d.widgets.is_empty());
392 }
393
394 const SAMPLE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
395<display version="2.0.0">
396 <name>Test</name>
397 <width>400</width>
398 <macros><P>BL01T</P></macros>
399 <widget type="group" version="3.0.0">
400 <name>G</name>
401 <macros><R>:DET:</R></macros>
402 <x>10</x>
403 <widget type="textupdate" version="2.0.0">
404 <name>TU</name>
405 <pv_name>$(P)$(R)Acquire_RBV</pv_name>
406 <foreground_color><color name="Text" red="1" green="2" blue="3"></color></foreground_color>
407 <font><font name="Default Bold" family="Liberation Sans" style="BOLD" size="14.0"></font></font>
408 </widget>
409 </widget>
410 <widget type="polyline" version="2.0.0">
411 <points><point x="0.0" y="1.0"></point><point x="5.0" y="6.0"></point></points>
412 </widget>
413</display>"#;
414
415 #[test]
416 fn parses_tree_and_properties() {
417 let d = Display::parse(SAMPLE).unwrap();
418 assert_eq!(d.name, "Test");
419 assert_eq!(d.width, 400.0);
420 assert_eq!(d.height, 600.0);
421 assert_eq!(d.macros.get("P"), Some("BL01T"));
422 let g = &d.widgets[0];
423 assert_eq!(g.kind, "group");
424 assert_eq!(g.f64("x"), Some(10.0));
425 assert_eq!(g.macros().get("R"), Some(":DET:"));
426 let tu = &g.children[0];
427 assert_eq!(tu.text("pv_name"), Some("$(P)$(R)Acquire_RBV"));
428 assert_eq!(tu.color("foreground_color").unwrap().rgba(), [1, 2, 3, 255]);
429 let f = tu.font("font").unwrap();
430 assert_eq!((f.size, f.style), (14.0, FontStyle::Bold));
431 assert_eq!(d.widgets[1].points(), vec![(0.0, 1.0), (5.0, 6.0)]);
432 assert_eq!(d.all_widgets().len(), 3);
433 }
434
435 #[test]
436 fn rejects_other_roots() {
437 assert!(matches!(Display::parse("<x/>"), Err(Error::NotADisplay(_))));
438 }
439
440 #[test]
441 fn repeated_property_keeps_the_last() {
442 let d = Display::parse(
443 r#"<display version="2.0.0"><widget type="combo">
444 <items><item>item 0</item><item>Item 1</item><item>Item 2</item></items>
445 <x>1</x>
446 <items><item>Disable</item><item>Enable</item></items>
447 </widget></display>"#,
448 )
449 .unwrap();
450 let w = &d.widgets[0];
451 assert_eq!(w.items(), vec!["Disable", "Enable"]);
452 assert_eq!(w.f64("x"), Some(1.0));
453 }
454}