Decode numeric character references in File Info XMP text

Found while testing crates/engine/src/file_cmds.rs:690 (xml_unescape), used by li_values, find_attr and read_file_info

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
NetterSilver
2026-10-20 13:42:22 +00:00
co-authored by Claude Haiku 5.5
parent 46e538b3a2
commit afeea2f643
2 changed files with 46 additions and 1 deletions
+36 -1
View File
@@ -657,7 +657,42 @@ fn xml_escape(s: &str) -> String {
}
fn xml_unescape(s: &str) -> String {
s.replace("&lt;", "<").replace("&gt;", ">").replace("&quot;", "\"").replace("&apos;", "'").replace("&amp;", "&")
let mut out = String::with_capacity(s.len());
let mut rest = s;
while let Some(i) = rest.find('&') {
out.push_str(&rest[..i]);
rest = &rest[i..];
let decoded = rest.find(';').filter(|&e| e <= 12).and_then(|e| {
let c = match &rest[1..e] {
"lt" => '<',
"gt" => '>',
"quot" => '"',
"apos" => '\'',
"amp" => '&',
n => {
let n = n.strip_prefix('#')?;
let code = match n.strip_prefix(['x', 'X']) {
Some(h) => u32::from_str_radix(h, 16).ok()?,
None => n.parse().ok()?,
};
char::from_u32(code)?
}
};
Some((c, e + 1))
});
match decoded {
Some((c, len)) => {
out.push(c);
rest = &rest[len..];
}
None => {
out.push('&');
rest = &rest[1..];
}
}
}
out.push_str(rest);
out
}
/// The element `<tag …>…</tag>` (first occurrence) as (start, end, inner).
+10
View File
@@ -0,0 +1,10 @@
use photocraft_engine::file_cmds::read_file_info;
#[test]
fn numeric_character_references_are_decoded() {
let xmp = "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF><rdf:Description>\
<dc:description><rdf:Alt><rdf:li xml:lang=\"x-default\">Line one&#xA;Line two &#169; &#38; co &#x41; &amp;#38;</rdf:li></rdf:Alt></dc:description>\
</rdf:Description></rdf:RDF></x:xmpmeta>";
let info = read_file_info(Some(xmp));
assert_eq!(info["description"], "Line one\nLine two © & co A &#38;");
}