From afeea2f643f015d8010b5b67b06ccb46cd5f55c1 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:42:22 +0000 Subject: [PATCH] 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 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/file_cmds.rs | 37 +++++++++++++++++++++++++++++- crates/engine/tests/xmp_charref.rs | 10 ++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 crates/engine/tests/xmp_charref.rs diff --git a/crates/engine/src/file_cmds.rs b/crates/engine/src/file_cmds.rs index 2299aa3e49..248e017689 100644 --- a/crates/engine/src/file_cmds.rs +++ b/crates/engine/src/file_cmds.rs @@ -657,7 +657,42 @@ fn xml_escape(s: &str) -> String { } fn xml_unescape(s: &str) -> String { - s.replace("<", "<").replace(">", ">").replace(""", "\"").replace("'", "'").replace("&", "&") + 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 `…` (first occurrence) as (start, end, inner). diff --git a/crates/engine/tests/xmp_charref.rs b/crates/engine/tests/xmp_charref.rs new file mode 100644 index 0000000000..5f167e8c4f --- /dev/null +++ b/crates/engine/tests/xmp_charref.rs @@ -0,0 +1,10 @@ +use photocraft_engine::file_cmds::read_file_info; + +#[test] +fn numeric_character_references_are_decoded() { + let xmp = "\ + Line one Line two © & co A &#38;\ + "; + let info = read_file_info(Some(xmp)); + assert_eq!(info["description"], "Line one\nLine two © & co A &"); +}