Code change:
} } } <ins> #[inline] fn len_utf8(code: u32) -> usize { if code < MAX_ONE_B { 1 } else if code < MAX_TWO_B { 2 } else if code < MAX_THREE_B { 3 } else { 4 } } /// Encodes a raw u32 value as UTF-8 into the provided byte buffer, /// and then returns the subslice of the buffer that contains the encoded character. /// /// Unlike `char::encode_utf8`, this method also handles codepoints in the surrogate range. /// (Creating a `char` in the surrogate range is UB.) /// The result is valid [generalized UTF-8] but not valid UTF-8. /// /// [generalized UTF-8]: https://simonsapin.github.io/wtf-8/#generalized-utf8 /// /// # Panics /// /// Panics if the buffer is not large enough. /// A buffer of length four is large enough to encode any `char`. #[unstable(feature = "char_internals", reason = "exposed only for libstd", issue = "none")] #[doc(hidden)] #[inline] pub fn encode_utf8_raw(code: u32, dst: &mut [u8]) -> &mut [u8] { let len = len_utf8(code); match (len, &mut dst[..]) { (1, [a, ..]) => { *a = code as u8; } (2, [a, b, ..]) => { *a = (code >> 6 & 0x1F) as u8 | TAG_TWO_B; *b = (code & 0x3F) as u8 | TAG_CONT; } (3, [a, b, c, ..]) => { *a = (code >> 12 & 0x0F) as u8 | TAG_THREE_B; *b = (code >> 6 & 0x3F) as u8 | TAG_CONT; *c = (code & 0x3F) as u8 | TAG_CONT; } (4, [a, b, c, d, ..]) => { *a = (code >> 18 & 0x07) as u8 | TAG_FOUR_B; *b = (code >> 12 & 0x3F) as u8 | TAG_CONT; *c = (code >> 6 & 0x3F) as u8 | TAG_CONT; *d = (code & 0x3F) as u8 | TAG_CONT; } _ => panic!( "encode_utf8: need {} bytes to encode U+{:X}, but the buffer has {}", len, code, dst.len(), ), }; &mut dst[..len] } /// Encodes a raw u32 value as UTF-16 into the provided `u16` buffer, /// and then returns the subslice of the buffer that contains the encoded character. /// /// Unlike `char::encode_utf16`, this method also handles codepoints in the surrogate range. /// (Creating a `char` in the surrogate range is UB.) /// /// # Panics /// /// Panics if the buffer is not large enough. /// A buffer of length 2 is large enough to encode any `char`. #[unstable(feature = "char_internals", reason = "exposed only for libstd", issue = "none")] #[doc(hidden)] #[inline] pub fn encode_utf16_raw(mut code: u32, dst: &mut [u16]) -> &mut [u16] { // SAFETY: each arm checks whether there are enough bits to write into unsafe { if (code & 0xFFFF) == code && !dst.is_empty() { // The BMP falls through *dst.get_unchecked_mut(0) = code as u16; slice::from_raw_parts_mut(dst.as_mut_ptr(), 1) } else if dst.len() >= 2 { // Supplementary planes break into surrogates. code -= 0x1_0000; *dst.get_unchecked_mut(0) = 0xD800 | ((code >> 10) as u16); *dst.get_unchecked_mut(1) = 0xDC00 | ((code as u16) & 0x3FF); slice::from_raw_parts_mut(dst.as_mut_ptr(), 2) } else { panic!( "encode_utf16: need {} units to encode U+{:X}, but the buffer has {}", from_u32_unchecked(code).len_utf16(), code, dst.len(), ) } } } </ins>

Related discussion:
text: The type contains a (generalized-UTF-8-encoded) sequence of code point (including potentially surrogates) that doesn’t form a surrogate pair. doesn’t check for surrogate pairs. is a public method that is documented as returning a scalar value, therefore excluding all surrogates. Ok I looked into it, there are three different people involved. The original code in 2014 was YOLO Later in 2014, moved to duplicating logic instead. In 2015, PR which first imported WTF-8 support in libstd deduplicated that logic by adding a function that takes . (I knew that approach sounded familiar…) In 2016, PR changed the signature of and in passing removed . It made use + instead, presumably in the middle of updating many callers other.
Okay, so the fix would be to revert the part of that removed ?
That or copy into . I find them both not great, so meh.
Even with that done, there's still UB in another testcase: This is converting something to a before calling -- so probably the same issue as the above, just for a different encoding.