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
@@ -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('&', "&").replace('<', "<").replace('>', ">").replace('"', """)+ let mut out = String::with_capacity(s.len());+ for c in s.chars() {+ match c {+ '&' => out.push_str("&"),+ '<' => out.push_str("<"),+ '>' => out.push_str(">"),+ '"' => out.push_str("""),+ '\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();