mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-19 02:10:25 -05:00
Merge PR #2144: Strip XML 1.0-forbidden characters from XMP sidecar output
This commit is contained in:
commit
70100a34c5
1 file changed
+36
-5
+36
-5
@@ -185,11 +185,22 @@ fn entity(name: &str) -> Option<char> {
|
||||
Some(h) => u32::from_str_radix(h, 16).ok()?,
|
||||
None => n.parse().ok()?,
|
||||
};
|
||||
char::from_u32(v)?
|
||||
char::from_u32(v).filter(|&c| is_xml_char(c))?
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// The XML 1.0 `Char` production (the surrogate range can't occur in a `char`).
|
||||
fn is_xml_char(c: char) -> bool {
|
||||
matches!(c, '\t' | '\n' | '\r') || (c >= '\u{20}' && c != '\u{FFFE}' && c != '\u{FFFF}')
|
||||
}
|
||||
|
||||
/// Drop the characters XML 1.0 forbids, which strict parsers (Adobe XMP Toolkit, expat, libxml2) reject the
|
||||
/// whole document for.
|
||||
fn xml_safe(s: &str) -> String {
|
||||
s.chars().filter(|&c| is_xml_char(c)).collect()
|
||||
}
|
||||
|
||||
/// Unescape attribute values (predefined + numeric references; unknown references kept verbatim).
|
||||
fn unescape_attr(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
@@ -620,12 +631,12 @@ pub fn write_xmp_lc(meta: &Metadata, lc: &[(&str, &str)]) -> String {
|
||||
}
|
||||
x.push_str(">\n");
|
||||
for (k, v) in &simple {
|
||||
x.push_str(&format!(" <{k}>{}</{k}>\n", escape(v.as_str())));
|
||||
x.push_str(&format!(" <{k}>{}</{k}>\n", escape(xml_safe(v))));
|
||||
}
|
||||
for (k, v) in lc {
|
||||
if !k.is_empty() && k.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
|
||||
// element content: quotes needn't be escaped, which keeps the JSON readable
|
||||
x.push_str(&format!(" <lc:{k}>{}</lc:{k}>\n", partial_escape(*v)));
|
||||
x.push_str(&format!(" <lc:{k}>{}</lc:{k}>\n", partial_escape(xml_safe(v))));
|
||||
}
|
||||
}
|
||||
let array = |x: &mut String, k: &str, kind: &str, items: &[String], lang: bool| {
|
||||
@@ -635,9 +646,9 @@ pub fn write_xmp_lc(meta: &Metadata, lc: &[(&str, &str)]) -> String {
|
||||
x.push_str(&format!(" <{k}>\n <rdf:{kind}>\n"));
|
||||
for it in items {
|
||||
if lang {
|
||||
x.push_str(&format!(" <rdf:li xml:lang=\"x-default\">{}</rdf:li>\n", escape(it.as_str())));
|
||||
x.push_str(&format!(" <rdf:li xml:lang=\"x-default\">{}</rdf:li>\n", escape(xml_safe(it))));
|
||||
} else {
|
||||
x.push_str(&format!(" <rdf:li>{}</rdf:li>\n", escape(it.as_str())));
|
||||
x.push_str(&format!(" <rdf:li>{}</rdf:li>\n", escape(xml_safe(it))));
|
||||
}
|
||||
}
|
||||
x.push_str(&format!(" </rdf:{kind}>\n </{k}>\n"));
|
||||
@@ -1054,4 +1065,24 @@ mod tests {
|
||||
a.overlay_user_fields(&b);
|
||||
assert_eq!(a.regions, vec![with_region]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writer_drops_characters_xml_1_0_forbids() {
|
||||
let bad = "a\u{1}b\u{0}c\u{FFFE}d\u{FFFF}e\u{1F}\tf\ng\rh";
|
||||
let m = Metadata { make: Some(bad.into()), caption: Some(bad.into()), keywords: vec![bad.into()], ..Default::default() };
|
||||
let x = write_xmp_lc(&m, &[("settings", bad)]);
|
||||
let illegal: Vec<char> =
|
||||
x.chars().filter(|&c| (c < '\u{20}' && !matches!(c, '\t' | '\n' | '\r')) || c == '\u{FFFE}' || c == '\u{FFFF}').collect();
|
||||
assert!(illegal.is_empty(), "illegal XML chars in output: {illegal:?}");
|
||||
assert!(x.contains(">abcde\tf\ng\rh<"), "allowed characters must survive: {x}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_references_to_forbidden_characters_are_not_decoded() {
|
||||
assert_eq!(entity("#1"), None);
|
||||
assert_eq!(entity("#0"), None);
|
||||
assert_eq!(entity("#xFFFE"), None);
|
||||
assert_eq!(entity("#x41"), Some('A'));
|
||||
assert_eq!(entity("#9"), Some('\t'));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user