From 9b93c609b9d78254256e6bfedf2f6a5fd515c680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20Mittelst=C3=A4dt?= <62167861+TheGalitube@users.noreply.github.com> Date: Sun, 1 Nov 2026 09:25:21 +0000 Subject: [PATCH 1/2] Decode DXF surrogate pairs Found while testing crates/cad/src/import/text.rs:80 Co-Authored-By: GPT-5.6-Luna Co-Authored-By: GPT-6-Luna --- crates/cad/src/import/text.rs | 39 +++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/crates/cad/src/import/text.rs b/crates/cad/src/import/text.rs index 137d9cc31..5a658564e 100644 --- a/crates/cad/src/import/text.rs +++ b/crates/cad/src/import/text.rs @@ -66,6 +66,31 @@ fn until_semicolon(chars: &mut std::iter::Peekable>) -> Stri /// and caret escapes: `^ ` is a caret, `^I` a tab; other control characters are dropped. In /// multiline text (`mtext`) `\\` is an escaped backslash and is kept as is, so that a `U+` after /// it stays literal, and the `^` in a stacked fraction `\Snum^den;` is left for the caller. +fn unicode_escape(after: &str) -> Option<(char, usize)> { + let hex = after.strip_prefix("U+").or_else(|| after.strip_prefix("u+"))?.get(..4)?; + if !hex.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + let value = u32::from_str_radix(hex, 16).ok()?; + if (0xD800..=0xDBFF).contains(&value) { + let next = after.get(6..)?; + let low = next.strip_prefix("\\U+").or_else(|| next.strip_prefix("\\u+"))?.get(..4)?; + if !low.bytes().all(|b| b.is_ascii_hexdigit()) { + return None; + } + let low = u32::from_str_radix(low, 16).ok()?; + if !(0xDC00..=0xDFFF).contains(&low) { + return None; + } + let scalar = 0x1_0000 + ((value - 0xD800) << 10) + (low - 0xDC00); + return char::from_u32(scalar).map(|ch| (ch, 13)); + } + if (0xDC00..=0xDFFF).contains(&value) { + return None; + } + char::from_u32(value).map(|ch| (ch, 6)) +} + fn escapes(s: &str, mtext: bool) -> String { let mut out = String::with_capacity(s.len()); let mut rest = s; @@ -77,12 +102,10 @@ fn escapes(s: &str, mtext: bool) -> String { continue; } if c == '\\' - && let Some(hex) = after.strip_prefix("U+").or_else(|| after.strip_prefix("u+")).and_then(|h| h.get(..4)) - && hex.bytes().all(|b| b.is_ascii_hexdigit()) - && let Some(ch) = u32::from_str_radix(hex, 16).ok().and_then(char::from_u32) + && let Some((ch, consumed)) = unicode_escape(after) { out.push(ch); - rest = after.get(6..).unwrap_or(""); + rest = after.get(consumed..).unwrap_or(""); continue; } if mtext @@ -161,6 +184,14 @@ mod tests { assert_eq!(plain("\\U+00C9"), "É"); } + #[test] + fn unicode_escape_decodes_surrogate_pairs() { + assert_eq!(plain("\\U+D83D\\U+DE00"), "😀"); + assert_eq!(plain("\\U+D83D"), "\\U+D83D"); + assert_eq!(plain("\\U+DE00"), "\\U+DE00"); + assert_eq!(plain("\\U+D83D\\U+0041"), "\\U+D83DA"); + } + #[test] fn control_codes_and_escapes() { assert_eq!(plain("45%%d %%p0.1 %%c20 100%%% %%uunder%%u"), "45° ±0.1 ⌀20 100% under"); From 87979644cddfd97a862f86269ab62254240f0634 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 2 Nov 2026 13:44:48 +0700 Subject: [PATCH 2/2] DXF: unicode_escape gets its own doc comment, above escapes' --- crates/cad/src/import/text.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/cad/src/import/text.rs b/crates/cad/src/import/text.rs index 5a658564e..d6d574612 100644 --- a/crates/cad/src/import/text.rs +++ b/crates/cad/src/import/text.rs @@ -62,10 +62,8 @@ fn until_semicolon(chars: &mut std::iter::Peekable>) -> Stri arg } -/// `\U+XXXX` Unicode escapes (how files before DXF 2007 hold characters outside their code page) -/// and caret escapes: `^ ` is a caret, `^I` a tab; other control characters are dropped. In -/// multiline text (`mtext`) `\\` is an escaped backslash and is kept as is, so that a `U+` after -/// it stays literal, and the `^` in a stacked fraction `\Snum^den;` is left for the caller. +/// The character a `\U+XXXX` escape writes (`after` is the text just after its backslash), with +/// the escape's length; a UTF-16 surrogate pair written as two escapes is one character. fn unicode_escape(after: &str) -> Option<(char, usize)> { let hex = after.strip_prefix("U+").or_else(|| after.strip_prefix("u+"))?.get(..4)?; if !hex.bytes().all(|b| b.is_ascii_hexdigit()) { @@ -91,6 +89,10 @@ fn unicode_escape(after: &str) -> Option<(char, usize)> { char::from_u32(value).map(|ch| (ch, 6)) } +/// `\U+XXXX` Unicode escapes (how files before DXF 2007 hold characters outside their code page) +/// and caret escapes: `^ ` is a caret, `^I` a tab; other control characters are dropped. In +/// multiline text (`mtext`) `\\` is an escaped backslash and is kept as is, so that a `U+` after +/// it stays literal, and the `^` in a stacked fraction `\Snum^den;` is left for the caller. fn escapes(s: &str, mtext: bool) -> String { let mut out = String::with_capacity(s.len()); let mut rest = s;