1use std::fmt;
10
11use serde::{Deserialize, Serialize};
12
13use super::error::OrkError;
14use super::warning::{Imported, Warning, WarningKind};
15
16pub const MAX_KNOWN_MINOR: u32 = 11;
22
23pub const MAX_DEPTH: usize = 64;
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
38pub struct SchemaVersion {
39 pub major: u32,
41 pub minor: u32,
43}
44
45impl SchemaVersion {
46 pub fn parse(text: &str) -> Result<Self, OrkError> {
48 let version = || OrkError::Version {
49 text: text.to_owned(),
50 };
51 let (major, minor) = text.split_once('.').ok_or_else(version)?;
52 let number = |field: &str| {
53 if field.is_empty() || !field.bytes().all(|byte| byte.is_ascii_digit()) {
54 return Err(version());
55 }
56 field.parse::<u32>().map_err(|_| version())
57 };
58 Ok(Self {
59 major: number(major)?,
60 minor: number(minor)?,
61 })
62 }
63
64 pub fn is_known(self) -> bool {
66 self.major == 1 && self.minor <= MAX_KNOWN_MINOR
67 }
68}
69
70impl fmt::Display for SchemaVersion {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 write!(f, "{}.{}", self.major, self.minor)
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct Document {
79 pub version: SchemaVersion,
81 pub creator: Option<String>,
84 pub root: Element,
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
90#[serde(deny_unknown_fields)]
91pub struct Element {
92 pub name: String,
94 pub attributes: Vec<(String, String)>,
96 pub children: Vec<Node>,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
102#[serde(rename_all = "snake_case", tag = "kind")]
103#[serde(deny_unknown_fields)]
104pub enum Node {
105 Element(Element),
107 Text {
109 text: String,
111 },
112}
113
114impl Element {
115 pub fn attribute(&self, name: &str) -> Option<&str> {
117 super::reads::note_attribute(self, name);
118 self.attributes
119 .iter()
120 .find(|(key, _)| key == name)
121 .map(|(_, value)| value.as_str())
122 }
123
124 pub fn children_named<'a>(&'a self, name: &str) -> impl Iterator<Item = &'a Element> {
126 let name = name.to_owned();
127 self.elements().filter(move |child| child.name == name)
128 }
129
130 pub fn child<'a>(&'a self, name: &str) -> Option<&'a Element> {
132 self.elements().find(|child| child.name == name)
133 }
134
135 pub fn elements(&self) -> impl Iterator<Item = &Element> {
137 self.children.iter().filter_map(|child| match child {
138 Node::Element(element) => Some(element),
139 Node::Text { .. } => None,
140 })
141 }
142
143 pub fn text(&self) -> String {
148 let mut text = String::new();
149 for child in &self.children {
150 if let Node::Text { text: part } = child {
151 text.push_str(part);
152 }
153 }
154 text
155 }
156
157 fn has_element_child(&self) -> bool {
158 self.elements().next().is_some()
159 }
160
161 fn has_text_child(&self) -> bool {
162 self.children
163 .iter()
164 .any(|child| matches!(child, Node::Text { .. }))
165 }
166}
167
168impl Document {
169 pub fn parse(text: &str) -> Result<Imported<Self>, OrkError> {
179 let deepest = deepest_nesting(text);
180 if deepest > MAX_DEPTH {
181 return Err(OrkError::TooDeep {
182 limit: MAX_DEPTH,
183 depth: deepest,
184 });
185 }
186 let parsed = roxmltree::Document::parse(text).map_err(|error| OrkError::Xml {
187 reason: error.to_string(),
188 })?;
189 let root = parsed.root_element();
190 if root.tag_name().name() != "openrocket" {
191 return Err(OrkError::NotOpenRocket {
192 root: root.tag_name().name().to_owned(),
193 });
194 }
195 let version = SchemaVersion::parse(root.attribute("version").unwrap_or_default())?;
196 let creator = root.attribute("creator").map(str::to_owned);
197
198 let mut warnings = Vec::new();
199 if !version.is_known() {
200 warnings.push(Warning::new(
201 "openrocket",
202 WarningKind::Unusual,
203 if version.major == 1 {
204 format!(
205 "schema version {version} is newer than 1.{MAX_KNOWN_MINOR}, the newest \
206 this reader knows; it was read as far as it is understood"
207 )
208 } else {
209 format!(
210 "schema version {version} is not one this reader knows (1.0 to \
211 1.{MAX_KNOWN_MINOR}); it was read as far as it is understood"
212 )
213 },
214 ));
215 }
216 if creator.is_none() {
217 warnings.push(Warning::new(
218 "openrocket",
219 WarningKind::Unusual,
220 "the root element has no `creator` attribute, so the program that wrote this \
221 design is unknown",
222 ));
223 }
224 if root.tag_name().namespace().is_some()
225 || parsed
226 .descendants()
227 .any(|node| node.namespaces().next().is_some())
228 {
229 warnings.push(Warning::new(
230 "openrocket",
231 WarningKind::Dropped,
232 "this document declares XML namespaces; prefixes and declarations are dropped, \
233 and two attributes that differ only by prefix become one. No `.ork` OpenRocket \
234 writes uses them",
235 ));
236 }
237 let root = read_element(root, "", &mut warnings);
238 Ok(Imported {
239 value: Self {
240 version,
241 creator,
242 root,
243 },
244 warnings,
245 })
246 }
247
248 pub fn to_xml(&self) -> String {
262 let mut root = self.root.clone();
263 set_attribute(&mut root, "version", Some(self.version.to_string()));
264 set_attribute(&mut root, "creator", self.creator.clone());
265 let mut out = String::from("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
266 write_element(&root, 0, &mut out);
267 out.push('\n');
268 out
269 }
270}
271
272fn read_element(
274 node: roxmltree::Node<'_, '_>,
275 parent: &str,
276 warnings: &mut Vec<Warning>,
277) -> Element {
278 let name = node.tag_name().name().to_owned();
279 let at = if parent.is_empty() {
280 name.clone()
281 } else {
282 format!("{parent}/{name}")
283 };
284 let attributes = node
285 .attributes()
286 .map(|attribute| (attribute.name().to_owned(), attribute.value().to_owned()))
287 .collect();
288
289 let keeps_blank_text = !node.children().any(|child| child.is_element());
290 let mut children = Vec::new();
291 for child in node.children() {
292 if child.is_element() {
293 children.push(Node::Element(read_element(child, &at, warnings)));
294 } else if child.is_text() {
295 let text = child.text().unwrap_or_default();
296 if let Some(Node::Text { text: last }) = children.last_mut()
300 && (keeps_blank_text || !text.trim().is_empty())
301 {
302 last.push_str(text);
303 continue;
304 }
305 if keeps_blank_text {
306 children.push(Node::Text {
307 text: text.to_owned(),
308 });
309 } else if !text.trim().is_empty() {
310 children.push(Node::Text {
314 text: text.to_owned(),
315 });
316 }
317 } else if child.is_comment() {
318 warnings.push(Warning::new(
319 at.clone(),
320 WarningKind::Dropped,
321 "an XML comment was dropped; `.ork` carries no meaning in comments",
322 ));
323 } else if child.is_pi() {
324 warnings.push(Warning::new(
325 at.clone(),
326 WarningKind::Dropped,
327 "an XML processing instruction was dropped",
328 ));
329 }
330 }
331 Element {
332 name,
333 attributes,
334 children,
335 }
336}
337
338pub(crate) fn deepest_nesting(text: &str) -> usize {
346 let bytes = text.as_bytes();
347 let mut index = 0;
348 let mut depth = 0usize;
349 let mut deepest = 0usize;
350 while index < bytes.len() {
351 if bytes[index] != b'<' {
352 index += 1;
353 continue;
354 }
355 let rest = &bytes[index..];
356 if let Some(skip) = skipped(rest, b"<!--", b"-->")
357 .or_else(|| skipped(rest, b"<![CDATA[", b"]]>"))
358 .or_else(|| skipped(rest, b"<?", b"?>"))
359 {
360 index += skip;
361 continue;
362 }
363 let closing = rest.starts_with(b"</");
364 let declaration = rest.starts_with(b"<!");
366
367 let mut cursor = index + 1;
368 let mut quote: Option<u8> = None;
369 let mut last = 0u8;
370 while cursor < bytes.len() {
371 let byte = bytes[cursor];
372 match quote {
373 Some(open) if byte == open => quote = None,
374 Some(_) => {}
375 None if byte == b'"' || byte == b'\'' => quote = Some(byte),
376 None if byte == b'>' => break,
377 None => {}
378 }
379 if !byte.is_ascii_whitespace() {
380 last = byte;
381 }
382 cursor += 1;
383 }
384 if !declaration {
385 if closing {
386 depth = depth.saturating_sub(1);
387 } else {
388 depth += 1;
389 deepest = deepest.max(depth);
390 if last == b'/' {
391 depth -= 1;
392 }
393 }
394 }
395 index = cursor + 1;
396 }
397 deepest
398}
399
400fn skipped(rest: &[u8], opening: &[u8], closing: &[u8]) -> Option<usize> {
403 if !rest.starts_with(opening) {
404 return None;
405 }
406 let from = opening.len();
407 let found = rest[from..]
408 .windows(closing.len())
409 .position(|window| window == closing)
410 .map(|at| from + at + closing.len());
411 Some(found.unwrap_or(rest.len()))
412}
413
414fn set_attribute(element: &mut Element, name: &str, value: Option<String>) {
416 let at = element.attributes.iter().position(|(key, _)| key == name);
417 match (at, value) {
418 (Some(at), Some(value)) => element.attributes[at].1 = value,
419 (Some(at), None) => {
420 element.attributes.remove(at);
421 }
422 (None, Some(value)) => element.attributes.push((name.to_owned(), value)),
423 (None, None) => {}
424 }
425}
426
427fn write_element(element: &Element, depth: usize, out: &mut String) {
429 let mixed = element.has_element_child() && element.has_text_child();
430 if mixed {
431 write_compact(element, out);
434 return;
435 }
436 for _ in 0..depth {
437 out.push_str(" ");
438 }
439 write_open_tag(element, out);
440 if element.children.is_empty() {
441 out.truncate(out.len() - 1);
442 out.push_str("/>");
443 return;
444 }
445 if element.has_element_child() {
446 for child in &element.children {
447 if let Node::Element(child) = child {
448 out.push('\n');
449 write_element(child, depth + 1, out);
450 }
451 }
452 out.push('\n');
453 for _ in 0..depth {
454 out.push_str(" ");
455 }
456 } else {
457 out.push_str(&escape_text(&element.text()));
458 }
459 out.push_str("</");
460 out.push_str(&element.name);
461 out.push('>');
462}
463
464fn write_compact(element: &Element, out: &mut String) {
466 write_open_tag(element, out);
467 if element.children.is_empty() {
468 out.truncate(out.len() - 1);
469 out.push_str("/>");
470 return;
471 }
472 for child in &element.children {
473 match child {
474 Node::Element(child) => write_compact(child, out),
475 Node::Text { text } => out.push_str(&escape_text(text)),
476 }
477 }
478 out.push_str("</");
479 out.push_str(&element.name);
480 out.push('>');
481}
482
483fn write_open_tag(element: &Element, out: &mut String) {
484 out.push('<');
485 out.push_str(&element.name);
486 for (name, value) in &element.attributes {
487 out.push(' ');
488 out.push_str(name);
489 out.push_str("=\"");
490 out.push_str(&escape_attribute(value));
491 out.push('"');
492 }
493 out.push('>');
494}
495
496fn escape_text(text: &str) -> String {
499 let mut out = String::with_capacity(text.len());
500 for character in text.chars() {
501 match character {
502 '&' => out.push_str("&"),
503 '<' => out.push_str("<"),
504 '>' => out.push_str(">"),
505 '\r' => out.push_str(" "),
506 other => out.push(other),
507 }
508 }
509 out
510}
511
512fn escape_attribute(value: &str) -> String {
515 let mut out = String::with_capacity(value.len());
516 for character in value.chars() {
517 match character {
518 '&' => out.push_str("&"),
519 '<' => out.push_str("<"),
520 '>' => out.push_str(">"),
521 '"' => out.push_str("""),
522 '\t' => out.push_str("	"),
523 '\n' => out.push_str(" "),
524 '\r' => out.push_str(" "),
525 other => out.push(other),
526 }
527 }
528 out
529}