Fix xlsx: combine escaped UTF-16 surrogate pairs when decoding strings (#1025)

This commit is contained in:
MarekSpodBiedry
2026-10-13 18:04:09 +02:00
parent 84acf6293b
commit 8939e7817f
+35 -2
View File
@@ -304,6 +304,20 @@ fn looks_like_escape(chars: &[char], i: usize) -> bool {
chars.get(i + 1) == Some(&'x') && chars.get(i + 6) == Some(&'_') && (i + 2..i + 6).all(|k| chars.get(k).is_some_and(|c| c.is_ascii_hexdigit()))
}
fn escape_unit(chars: &[char], i: usize) -> Option<u32> {
let hex: String = chars.get(i + 2..i + 6)?.iter().collect();
u32::from_str_radix(&hex, 16).ok()
}
/// The character for a high surrogate escape at `i` (value `high`) followed directly by a low surrogate escape.
fn surrogate_pair(chars: &[char], i: usize, high: u32) -> Option<char> {
if !(0xD800..=0xDBFF).contains(&high) || chars.get(i + 7) != Some(&'_') || !looks_like_escape(chars, i + 7) {
return None;
}
let low = escape_unit(chars, i + 7).filter(|l| (0xDC00..=0xDFFF).contains(l))?;
char::from_u32(0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00))
}
/// Inverse of [`encode_xstring`].
pub fn decode_xstring(s: &str) -> String {
if !s.contains("_x") {
@@ -314,12 +328,18 @@ pub fn decode_xstring(s: &str) -> String {
let mut i = 0;
while i < chars.len() {
if chars[i] == '_' && looks_like_escape(&chars, i) {
let hex: String = chars[i + 2..i + 6].iter().collect();
if let Some(ch) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
let unit = escape_unit(&chars, i);
if let Some(ch) = unit.and_then(char::from_u32) {
o.push(ch);
i += 7;
continue;
}
// A high surrogate escape followed by a low surrogate escape is one UTF-16 pair.
if let Some(ch) = unit.and_then(|high| surrogate_pair(&chars, i, high)) {
o.push(ch);
i += 14;
continue;
}
}
o.push(chars[i]);
i += 1;
@@ -402,6 +422,19 @@ mod tests {
assert_eq!(decode_xstring("_x00"), "_x00");
}
#[test]
fn xstring_surrogate_pairs() {
assert_eq!(decode_xstring("a_xD83D__xDE00_b"), "a\u{1F600}b");
assert_eq!(decode_xstring("_xd83d__xde00_"), "\u{1F600}");
// Lone surrogates stay as literal escape text.
assert_eq!(decode_xstring("_xD83D_"), "_xD83D_");
assert_eq!(decode_xstring("_xDE00_"), "_xDE00_");
assert_eq!(decode_xstring("_xD83D_x"), "_xD83D_x");
assert_eq!(decode_xstring("_xD83D__x0041_"), "_xD83D_A");
// A protected literal escape is not a low surrogate.
assert_eq!(decode_xstring("_xD83D__x005F_xDE00_"), "_xD83D__xDE00_");
}
#[test]
fn numbers() {
assert_eq!(num(1.0), "1");