1use alloc::string::String;
8use alloc::vec::Vec;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
17#[non_exhaustive]
18pub struct DecodedUtf16 {
19 pub text: String,
21 pub unpaired_surrogates: usize,
23 pub dangling_byte: bool,
28}
29
30impl DecodedUtf16 {
31 #[must_use]
33 pub fn is_lossy(&self) -> bool {
34 self.dangling_byte || self.unpaired_surrogates > 0
35 }
36}
37
38fn units(bytes: &[u8], to_unit: fn([u8; 2]) -> u16) -> Vec<u16> {
41 bytes
42 .chunks_exact(2)
43 .filter_map(|chunk| <[u8; 2]>::try_from(chunk).ok())
45 .map(to_unit)
46 .collect()
47}
48
49fn has_dangling_byte(bytes: &[u8]) -> bool {
51 bytes.len() % 2 == 1
52}
53
54fn decode(units: &[u16], dangling_byte: bool) -> DecodedUtf16 {
57 let mut text = String::with_capacity(units.len());
58 let mut unpaired_surrogates = 0;
59 for unit in core::char::decode_utf16(units.iter().copied()) {
60 if let Ok(ch) = unit {
61 text.push(ch);
62 } else {
63 text.push(char::REPLACEMENT_CHARACTER);
64 unpaired_surrogates += 1;
65 }
66 }
67 DecodedUtf16 {
68 text,
69 unpaired_surrogates,
70 dangling_byte,
71 }
72}
73
74fn keep_nuls(bytes: &[u8], to_unit: fn([u8; 2]) -> u16) -> DecodedUtf16 {
75 decode(&units(bytes, to_unit), has_dangling_byte(bytes))
76}
77
78fn until_nul(bytes: &[u8], to_unit: fn([u8; 2]) -> u16) -> DecodedUtf16 {
79 let mut units = units(bytes, to_unit);
80 if let Some(nul) = units.iter().position(|&u| u == 0) {
81 units.truncate(nul);
82 }
83 decode(&units, has_dangling_byte(bytes))
84}
85
86fn trim_end_nuls(bytes: &[u8], to_unit: fn([u8; 2]) -> u16) -> DecodedUtf16 {
87 let mut units = units(bytes, to_unit);
88 let end = units
89 .iter()
90 .rposition(|&u| u != 0)
91 .map_or(0, |last| last + 1);
92 units.truncate(end);
93 decode(&units, has_dangling_byte(bytes))
94}
95
96fn split_on_nul(bytes: &[u8], to_unit: fn([u8; 2]) -> u16) -> Vec<DecodedUtf16> {
97 let units = units(bytes, to_unit);
98 let mut segments: Vec<DecodedUtf16> = units
99 .split(|&u| u == 0)
100 .map(|segment| decode(segment, false))
101 .collect();
102 if has_dangling_byte(bytes) {
105 if let Some(last) = segments.last_mut() {
106 last.dangling_byte = true;
107 }
108 }
109 segments
110}
111
112#[must_use]
117pub fn decode_utf16le_keep_nuls(bytes: &[u8]) -> DecodedUtf16 {
118 keep_nuls(bytes, u16::from_le_bytes)
119}
120
121#[must_use]
126pub fn decode_utf16le_until_nul(bytes: &[u8]) -> DecodedUtf16 {
127 until_nul(bytes, u16::from_le_bytes)
128}
129
130#[must_use]
134pub fn decode_utf16le_trim_end_nuls(bytes: &[u8]) -> DecodedUtf16 {
135 trim_end_nuls(bytes, u16::from_le_bytes)
136}
137
138#[must_use]
146pub fn split_utf16le_on_nul(bytes: &[u8]) -> Vec<DecodedUtf16> {
147 split_on_nul(bytes, u16::from_le_bytes)
148}
149
150#[must_use]
154pub fn decode_utf16be_keep_nuls(bytes: &[u8]) -> DecodedUtf16 {
155 keep_nuls(bytes, u16::from_be_bytes)
156}
157
158#[must_use]
162pub fn decode_utf16be_until_nul(bytes: &[u8]) -> DecodedUtf16 {
163 until_nul(bytes, u16::from_be_bytes)
164}
165
166#[must_use]
170pub fn decode_utf16be_trim_end_nuls(bytes: &[u8]) -> DecodedUtf16 {
171 trim_end_nuls(bytes, u16::from_be_bytes)
172}
173
174#[must_use]
178pub fn split_utf16be_on_nul(bytes: &[u8]) -> Vec<DecodedUtf16> {
179 split_on_nul(bytes, u16::from_be_bytes)
180}
181
182#[cfg(test)]
183mod tests {
184 use super::{
185 decode_utf16be_keep_nuls, decode_utf16be_trim_end_nuls, decode_utf16be_until_nul,
186 decode_utf16le_keep_nuls, decode_utf16le_trim_end_nuls, decode_utf16le_until_nul,
187 split_utf16be_on_nul, split_utf16le_on_nul,
188 };
189 use alloc::string::String;
190 use alloc::vec::Vec;
191
192 const FOUR_WAY_LE: &[u8] = &[0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00, 0x00];
195 const FOUR_WAY_BE: &[u8] = &[0x00, 0x41, 0x00, 0x00, 0x00, 0x42, 0x00, 0x00];
197
198 fn texts(parts: &[super::DecodedUtf16]) -> Vec<String> {
199 parts.iter().map(|d| d.text.clone()).collect()
200 }
201
202 #[test]
205 fn keep_nuls_keeps_every_nul_as_u0000() {
206 assert_eq!(decode_utf16le_keep_nuls(FOUR_WAY_LE).text, "A\0B\0");
207 }
208
209 #[test]
210 fn until_nul_stops_dead_at_the_first_nul() {
211 assert_eq!(decode_utf16le_until_nul(FOUR_WAY_LE).text, "A");
212 }
213
214 #[test]
215 fn trim_end_nuls_keeps_interior_nuls_and_drops_only_trailing_ones() {
216 assert_eq!(decode_utf16le_trim_end_nuls(FOUR_WAY_LE).text, "A\0B");
217 }
218
219 #[test]
220 fn split_on_nul_yields_every_segment_including_the_trailing_empty() {
221 assert_eq!(texts(&split_utf16le_on_nul(FOUR_WAY_LE)), ["A", "B", ""]);
222 }
223
224 #[test]
225 fn the_four_policies_produce_four_different_answers() {
226 let keep = decode_utf16le_keep_nuls(FOUR_WAY_LE).text;
227 let until = decode_utf16le_until_nul(FOUR_WAY_LE).text;
228 let trim = decode_utf16le_trim_end_nuls(FOUR_WAY_LE).text;
229 let split = texts(&split_utf16le_on_nul(FOUR_WAY_LE)).join("|");
230 let all = [keep.as_str(), until.as_str(), trim.as_str(), split.as_str()];
231 for (i, a) in all.iter().enumerate() {
232 for b in all.iter().skip(i + 1) {
233 assert_ne!(a, b, "two policies collapsed onto the same answer");
234 }
235 }
236 }
237
238 #[test]
241 fn big_endian_twins_match_the_little_endian_family() {
242 assert_eq!(
243 decode_utf16be_keep_nuls(FOUR_WAY_BE).text,
244 decode_utf16le_keep_nuls(FOUR_WAY_LE).text
245 );
246 assert_eq!(
247 decode_utf16be_until_nul(FOUR_WAY_BE).text,
248 decode_utf16le_until_nul(FOUR_WAY_LE).text
249 );
250 assert_eq!(
251 decode_utf16be_trim_end_nuls(FOUR_WAY_BE).text,
252 decode_utf16le_trim_end_nuls(FOUR_WAY_LE).text
253 );
254 assert_eq!(
255 texts(&split_utf16be_on_nul(FOUR_WAY_BE)),
256 texts(&split_utf16le_on_nul(FOUR_WAY_LE))
257 );
258 }
259
260 #[test]
261 fn endianness_actually_changes_the_result() {
262 assert_ne!(
264 decode_utf16be_keep_nuls(&[0x41, 0x00]).text,
265 decode_utf16le_keep_nuls(&[0x41, 0x00]).text
266 );
267 }
268
269 #[test]
272 fn well_formed_surrogate_pair_is_not_lossy() {
273 let d = decode_utf16le_keep_nuls(&[0x3D, 0xD8, 0x00, 0xDE]);
275 assert_eq!(d.text, "\u{1F600}");
276 assert_eq!(d.unpaired_surrogates, 0);
277 assert!(!d.dangling_byte);
278 assert!(!d.is_lossy());
279 }
280
281 #[test]
282 fn lone_high_surrogate_is_replaced_and_counted() {
283 let d = decode_utf16le_keep_nuls(&[0x00, 0xD8]);
284 assert_eq!(d.text, "\u{FFFD}");
285 assert_eq!(d.unpaired_surrogates, 1);
286 assert!(!d.dangling_byte);
287 assert!(d.is_lossy());
288 }
289
290 #[test]
291 fn lone_low_surrogate_is_replaced_and_counted() {
292 let d = decode_utf16le_keep_nuls(&[0x00, 0xDC]);
293 assert_eq!(d.text, "\u{FFFD}");
294 assert_eq!(d.unpaired_surrogates, 1);
295 assert!(d.is_lossy());
296 }
297
298 #[test]
299 fn unpaired_surrogate_before_a_valid_pair_counts_once() {
300 let d = decode_utf16le_keep_nuls(&[0x00, 0xD8, 0x3D, 0xD8, 0x00, 0xDE]);
302 assert_eq!(d.text, "\u{FFFD}\u{1F600}");
303 assert_eq!(d.unpaired_surrogates, 1);
304 assert!(d.is_lossy());
305 }
306
307 #[test]
308 fn odd_length_input_drops_the_trailing_byte_and_says_so() {
309 let d = decode_utf16le_keep_nuls(&[0x41, 0x00, 0x42]);
310 assert_eq!(d.text, "A");
311 assert!(d.dangling_byte);
312 assert_eq!(d.unpaired_surrogates, 0);
313 assert!(d.is_lossy());
314 }
315
316 #[test]
317 fn a_single_byte_decodes_to_nothing_but_reports_the_dangling_byte() {
318 let d = decode_utf16be_keep_nuls(&[0x41]);
319 assert_eq!(d.text, "");
320 assert!(d.dangling_byte);
321 assert!(d.is_lossy());
322 }
323
324 #[test]
325 fn every_policy_reports_the_dangling_byte() {
326 let odd: &[u8] = &[0x41, 0x00, 0x00, 0x00, 0x42];
327 assert!(decode_utf16le_keep_nuls(odd).dangling_byte);
328 assert!(decode_utf16le_until_nul(odd).dangling_byte);
329 assert!(decode_utf16le_trim_end_nuls(odd).dangling_byte);
330 assert!(decode_utf16be_keep_nuls(odd).dangling_byte);
331 assert!(decode_utf16be_until_nul(odd).dangling_byte);
332 assert!(decode_utf16be_trim_end_nuls(odd).dangling_byte);
333 }
334
335 #[test]
336 fn split_reports_the_dangling_byte_on_the_segment_that_lost_it() {
337 let parts = split_utf16le_on_nul(&[0x41, 0x00, 0x00, 0x00, 0x42]);
338 assert_eq!(texts(&parts), ["A", ""]);
339 assert!(!parts[0].dangling_byte);
340 assert!(parts[1].dangling_byte);
341 assert!(parts[1].is_lossy());
342 }
343
344 #[test]
345 fn split_counts_unpaired_surrogates_per_segment() {
346 let parts = split_utf16le_on_nul(&[0x41, 0x00, 0x00, 0x00, 0x00, 0xD8]);
348 assert_eq!(texts(&parts), ["A", "\u{FFFD}"]);
349 assert_eq!(parts[0].unpaired_surrogates, 0);
350 assert!(!parts[0].is_lossy());
351 assert_eq!(parts[1].unpaired_surrogates, 1);
352 assert!(parts[1].is_lossy());
353 }
354
355 #[test]
358 fn empty_input_decodes_to_empty_and_is_not_lossy() {
359 for d in [
360 decode_utf16le_keep_nuls(&[]),
361 decode_utf16le_until_nul(&[]),
362 decode_utf16le_trim_end_nuls(&[]),
363 decode_utf16be_keep_nuls(&[]),
364 decode_utf16be_until_nul(&[]),
365 decode_utf16be_trim_end_nuls(&[]),
366 ] {
367 assert_eq!(d.text, "");
368 assert!(!d.is_lossy());
369 }
370 }
371
372 #[test]
373 fn empty_input_splits_into_one_empty_segment() {
374 let parts = split_utf16le_on_nul(&[]);
375 assert_eq!(texts(&parts), [""]);
376 assert!(!parts[0].is_lossy());
377 }
378
379 #[test]
380 fn all_nuls_are_handled_by_each_policy() {
381 let nuls: &[u8] = &[0x00; 6];
382 assert_eq!(decode_utf16le_keep_nuls(nuls).text, "\0\0\0");
383 assert_eq!(decode_utf16le_until_nul(nuls).text, "");
384 assert_eq!(decode_utf16le_trim_end_nuls(nuls).text, "");
385 assert_eq!(texts(&split_utf16le_on_nul(nuls)), ["", "", "", ""]);
386 }
387
388 #[test]
389 fn until_nul_returns_the_whole_string_when_no_nul_is_present() {
390 let d = decode_utf16le_until_nul(&[0x41, 0x00, 0x42, 0x00]);
391 assert_eq!(d.text, "AB");
392 assert!(!d.is_lossy());
393 }
394
395 #[test]
396 fn trim_end_nuls_leaves_a_string_without_padding_untouched() {
397 assert_eq!(
398 decode_utf16le_trim_end_nuls(&[0x41, 0x00, 0x42, 0x00]).text,
399 "AB"
400 );
401 }
402
403 #[test]
404 fn decodes_a_realistic_nul_padded_path_field() {
405 let mut field: Vec<u8> = Vec::new();
407 for u in "C:\\ok".encode_utf16() {
408 field.extend_from_slice(&u.to_le_bytes());
409 }
410 field.resize(16, 0);
411 assert_eq!(decode_utf16le_trim_end_nuls(&field).text, "C:\\ok");
412 assert_eq!(decode_utf16le_until_nul(&field).text, "C:\\ok");
413 assert_eq!(decode_utf16le_keep_nuls(&field).text, "C:\\ok\0\0\0");
414 }
415}