diff --git a/crates/format/src/rtf.rs b/crates/format/src/rtf.rs index d122fcfd..e6812317 100644 --- a/crates/format/src/rtf.rs +++ b/crates/format/src/rtf.rs @@ -197,8 +197,8 @@ pub fn rtf_to_outline(rtf: &str) -> String { "uc" => st.uc = arg.unwrap_or(1).clamp(0, 8) as usize, "u" => { let v = arg.unwrap_or(0); - let v = if v < 0 { v + 65536 } else { v } as u32; - push_utf16(&mut line, v); + // RTF writes UTF-16 units as signed 16-bit values. + push_utf16(&mut line, (v & 0xffff) as u32); pending_skip = 0; // Skip the fallback characters. let mut k = st.uc; @@ -227,6 +227,7 @@ pub fn rtf_to_outline(rtf: &str) -> String { i = advance(rtf, i, 3); if !st.skip && let Some(h) = hex + && h != 0 { line.push(cp1252(h)); } @@ -252,7 +253,7 @@ pub fn rtf_to_outline(rtf: &str) -> String { if !st.skip && let Some(t) = rtf.get(s..i) { - line.push_str(t); + line.push_str(&t.replace('\u{0}', "")); } } } @@ -261,8 +262,12 @@ pub fn rtf_to_outline(rtf: &str) -> String { finish_surrogates(out) } -/// Push a UTF-16 unit; surrogate halves are parked as private markers and joined afterwards. +/// Push a UTF-16 unit; surrogate halves are parked as private markers (NUL plus four hex digits) +/// and joined afterwards. NUL itself is the marker, so a NUL unit is dropped. fn push_utf16(line: &mut String, v: u32) { + if v == 0 { + return; + } match char::from_u32(v) { Some(c) => line.push(c), None => line.push_str(&format!("\u{0}{v:04x}")), @@ -286,7 +291,10 @@ fn finish_surrogates(s: String) -> String { } _ => { out.push('\u{fffd}'); - rest = rest.get(p + 5..).unwrap_or(""); + // Skip the NUL and, when four hex digits follow, the digits; otherwise + // resume right after the NUL so no following character is lost. + let skip = if hi.is_some() { p + 5 } else { p + 1 }; + rest = rest.get(skip..).unwrap_or(""); } } } @@ -320,6 +328,15 @@ mod tests { } } + /// NUL is the surrogate marker, so NULs from `\u0`, `\'00` or raw bytes must not reach `finish_surrogates`. + #[test] + fn nul_escapes_do_not_drop_text_or_add_a_slide() { + assert_eq!(rtf_to_outline(r"{\rtf1 \u0?abcé\par}"), "abcé\n"); + assert_eq!(rtf_to_outline(r"{\rtf1 a\'00bé\par}"), "abé\n"); + assert_eq!(rtf_to_outline("{\\rtf1 a\0b\\par}\0\0"), "ab\n"); + assert_eq!(rtf_to_outline(r"{\rtf1 x\u-70000?y\par}"), "x\u{ee90}y\n"); + } + #[test] fn export_reads_back_as_the_same_outline() { let mut p = deckcraft_model::defaults::blank_presentation(deckcraft_model::defaults::WIDE, Default::default(), false);