diff --git a/src/smda/common/labelprovider/rust_demangler/rust_v0.py b/src/smda/common/labelprovider/rust_demangler/rust_v0.py index 25a22637..fbe0f6e4 100644 --- a/src/smda/common/labelprovider/rust_demangler/rust_v0.py +++ b/src/smda/common/labelprovider/rust_demangler/rust_v0.py @@ -101,7 +101,7 @@ def insert(self, i: int, c: str) -> bool: self.out[i] = c return True - def punycode_decode(self) -> Optional[None]: + def punycode_decode(self) -> Optional[bool]: count = 0 punycode_bytes = self.punycode try: @@ -151,10 +151,12 @@ def punycode_decode(self) -> Optional[None]: n += i // lent i %= lent - try: - c = chr(n) - except (ValueError, OverflowError): + # mirror char::from_u32: a scalar value, so no surrogate and nothing past the + # last code point. chr() accepts surrogates, and a name carrying one cannot be + # encoded as UTF-8 by whoever consumes the report. + if not 0 <= n <= 0x10FFFF or 0xD800 <= n <= 0xDFFF: return None + c = chr(n) if not self.insert(i, c): return None @@ -163,7 +165,7 @@ def punycode_decode(self) -> Optional[None]: try: punycode_bytes[count] except IndexError: - return + return True delta = delta // damp damp = 2 diff --git a/tests/testRustSymbolProvider.py b/tests/testRustSymbolProvider.py index d9dffdef..58a0287a 100644 --- a/tests/testRustSymbolProvider.py +++ b/tests/testRustSymbolProvider.py @@ -11,6 +11,7 @@ from smda.common.labelprovider.rust_demangler.rust import TypeNotFoundError from smda.common.labelprovider.rust_demangler.rust_legacy import LegacyDemangler, UnableToLegacyDemangle from smda.common.labelprovider.rust_demangler.rust_v0 import ( + Ident, Parser, Printer, UnableTov0Demangle, @@ -113,6 +114,28 @@ def test_v0_unnamed_closure_has_no_stray_colon(self): # unnamed closures/shims must render as {closure#N}, not {closure:#N} self.assertEqual(demangle("_RNCNvC8rustc_v01fs_0"), "rustc_v0::f::{closure#1}") + def test_v0_non_ascii_identifier_is_decoded(self): + # the decode itself always worked; its success was reported as None, + # which is what every failure path returns, so the caller fell back + name = "_RNqCs4fqI2P2rA04_11utf8_identsu30____7hkackfecea1cbdathfdh9hlq6y" + self.assertEqual(demangle(name), "utf8_idents::საჭმელად_გემრიელი_სადილი") + self.assertNotIn("punycode{", demangle(name)) + + def test_v0_punycode_never_decodes_to_a_surrogate(self): + # chr() accepts 0xD800-0xDFFF where Rust's char::from_u32 refuses; a name carrying + # one cannot be encoded as UTF-8 by whoever consumes the report + name = "_RNvCu4_ib9b4main" + + demangled = demangle(name) + + self.assertNotIn("\ud800", demangled) + demangled.encode("utf-8") + + def test_v0_undecodable_punycode_still_falls_back_to_the_raw_form(self): + ident = Ident("", "!not-punycode!") + ident.display() + self.assertEqual(ident.disp, "punycode{!not-punycode!}") + def test_v0_empty_const_hex_nibbles_raise_demangler_error(self): # `Kh_` (u8 const with zero hex nibbles) previously escaped as a bare # ValueError from int("", 16) instead of the demangler's own error type diff --git a/tests/test_fuzz_rust_demangler.py b/tests/test_fuzz_rust_demangler.py index d6b1b1ee..7e075345 100644 --- a/tests/test_fuzz_rust_demangler.py +++ b/tests/test_fuzz_rust_demangler.py @@ -26,6 +26,9 @@ def test_rust_demangler_never_hangs(s): """ try: result = demangle(s) + # a demangled name is handed to consumers as text; a lone surrogate would make it + # unencodable, so the demangler must never produce one + result.encode("utf-8") except (TypeNotFoundError, UnableTov0Demangle, UnableToLegacyDemangle): return except RecursionError: