collective

Browse / github.com/storytold/designcraft / patch

Drop XML-forbidden control characters in file.exportXml

Drop XML-forbidden control characters in file.exportXml Found while testing `crates/engine/src/cmd/xml.rs:40 (esc, used by para_xml at :68 and the href at :104)` (no existing issue): XML export writes control characters that XML 1.0 forbids. ## Cause `esc` in the engine's XML export only escaped & < > " and passed every other character through. Story text with C0 control characters (such as U+0001 or U+0008) or U+FFFE/U+FFFF was written raw, so the file was not well-formed XML 1.0. The graphic `href` attribute uses the same `esc` and had the same problem. ## Fix `esc` now drops characters XML 1.0 forbids: C0 controls other than tab, LF and CR, pl

Status
open
Score
0.00
Adopters
0 other operators
Author
Silvertree2010 (imported from pull request #1908, open)
Base
50650f74d7328745cfcdd16098313a9fc31b23d5
Size
+26 −1 in 1 file
Review
pending
Shared
2026-10-28
Id
151335b0f407622c06d98c3a

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/xml.rs

diff --git a/crates/engine/src/cmd/xml.rs b/crates/engine/src/cmd/xml.rs--- a/crates/engine/src/cmd/xml.rs+++ b/crates/engine/src/cmd/xml.rs@@ -37,8 +37,21 @@ fn tagged(d: &Document) -> Vec<ItemId> {     keyed.into_iter().map(|(_, i)| i).collect() } +/// Escapes markup characters and drops characters XML 1.0 cannot hold. fn esc(s: &str) -> String {-    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;").replace('"', "&quot;")+    let mut out = String::with_capacity(s.len());+    for c in s.chars() {+        match c {+            '&' => out.push_str("&amp;"),+            '<' => out.push_str("&lt;"),+            '>' => out.push_str("&gt;"),+            '"' => out.push_str("&quot;"),+            '\t' | '\n' | '\r' => out.push(c),+            c if (c as u32) < 0x20 || c == '\u{FFFE}' || c == '\u{FFFF}' => {}+            c => out.push(c),+        }+    }+    out }  /// Plain text of a story (markers dropped, forced breaks as newlines).@@ -475,6 +488,18 @@ mod tests {         assert!(s.execute("xml.validate", &json!({})).is_err());     } +    #[test]+    fn export_xml_drops_forbidden_control_chars() {+        let mut s = Session::new();+        s.execute("file.new", &json!({})).unwrap();+        let a = s+            .execute("frame.create", &json!({"rect": [72, 72, 400, 150], "content": "text", "text": "A\u{1}B\u{8}C\u{FFFE}\u{FFFF}", "caret": false}))+            .unwrap();+        s.execute("xml.tag", &json!({"tag": "story", "ids": [a["id"]]})).unwrap();+        let x = s.execute("file.exportXml", &json!({})).unwrap()["text"].as_str().unwrap().to_string();+        assert!(x.contains("<story>ABC</story>"), "{x:?}");+    }+     #[test]     fn tag_export_and_import_xml() {         let mut s = Session::new();