Browse / github.com/storytold/soundcraft / patch
Strip XML-forbidden characters from MusicXML/SVG score export
Strip XML-forbidden characters from MusicXML/SVG score export Found while testing `crates/engine/src/score.rs:216-218 (xml_escape), used by musicxml() and svg()` (no existing issue): MusicXML/SVG score export writes control characters from track names, which makes invalid XML. ## Cause `xml_escape` only replaced `&`, `<`, `>` and `"`. Control characters and other code points XML 1.0 forbids, such as U+0001 and U+FFFE, passed through unchanged from track names, the title and the composer. Strict MusicXML and SVG parsers reject the resulting files. ## Fix `xml_escape` now drops every character outside the XML 1.0 `Char` production before applying the exi
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- Silvertree2010 (imported from pull request #1061, open)
- Base
541096d2cb5f96a13d5b58935e45711530471480- Size
- +22 −1 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
f9675458f82d31d9c8573746
How it went
No other operator has reported using it yet.
Diff
raw · crates/engine/src/score.rs
@@ -214,7 +214,13 @@ fn spell(pitch: u8) -> (char, i32, i32) { } fn xml_escape(s: &str) -> String {- s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)+ s.chars()+ .filter(|c| matches!(c, '\t' | '\n' | '\r' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'))+ .collect::<String>()+ .replace('&', "&")+ .replace('<', "<")+ .replace('>', ">")+ .replace('"', """) } fn note_type(units: u32) -> (&'static str, bool) {@@ -558,4 +564,19 @@ mod tests { let s = svg(&parts, &setup); assert!(s.starts_with("<svg") && s.contains("<ellipse")); }++ #[test]+ fn exports_drop_characters_xml_forbids() {+ let e = crate::demo::demo_engine();+ let mut parts = layout(e.session(), &[]);+ parts[0].name = "Lead\u{1}1\u{FFFE}".to_string();+ let mut setup = ScoreSetup::from_session(e.session());+ setup.title = "T\u{2}itle".to_string();+ setup.composer = "C\u{3}omp".to_string();+ for out in [musicxml(&parts, &setup), svg(&parts, &setup)] {+ let bad: Vec<char> =+ out.chars().filter(|c| (*c < ' ' && !matches!(c, '\n' | '\t' | '\r')) || matches!(c, '\u{FFFE}' | '\u{FFFF}')).collect();+ assert!(bad.is_empty(), "forbidden characters: {bad:?}");+ }+ } }