Skip to main content

hermes_support/
utf8.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! WTF-8 / UTF-8 → UTF-16 codec helpers, faithfully copied from the subset of
9//! `hermes_parser::utf8` (itself ported from `include/hermes/Support/UTF8.h`) that is
10//! needed by `JSONEmitter` and the forthcoming AST-dumper port.
11//!
12//! Keeping this copy in `support` means `json_emitter` and the AST-dumper can
13//! use it without taking a dependency on the `parser` crate, and without
14//! duplicating logic. The module is zero-`unsafe` (the `support` crate
15//! `forbid`s `unsafe_code`).
16
17use hermes_unicode::{
18    UNICODE_MAX_VALUE, UNICODE_REPLACEMENT_CHARACTER, UNICODE_SURROGATE_FIRST,
19    UNICODE_SURROGATE_LAST, UTF16_HIGH_SURROGATE, UTF16_LOW_SURROGATE,
20};
21
22/// Check whether a byte is a regular ASCII or a UTF8 starting byte.
23/// \return true if it is UTF8 starting byte.
24#[inline]
25pub fn is_utf8_start(ch: u8) -> bool {
26    (ch & 0x80) != 0
27}
28
29/// Read `bytes[i]`, or `0` if `i` is out of range. The lexer's buffer is
30/// NUL-terminated, so the only way to read past the end here is in unit tests
31/// of malformed/truncated sequences, where `0` (a non-continuation byte)
32/// reproduces the buffer's NUL terminator and the same rejection behavior.
33#[inline]
34fn at(bytes: &[u8], i: usize) -> u32 {
35    bytes.get(i).copied().unwrap_or(0) as u32
36}
37
38/// Decode a sequence of UTF8 encoded bytes when it is known that the first byte
39/// is a start of a UTF8 sequence. Port of `_decodeUTF8SlowPath` (UTF8.h:77-162),
40/// reading `bytes` from `*i` and advancing `*i` past the consumed bytes.
41/// On malformed input it invokes `error` and returns the replacement character.
42///
43/// \tparam ALLOW_SURROGATES when false, values in the surrogate range are
44///     reported as errors.
45// Keep the C++ `result >= FIRST && result <= LAST` surrogate-range check faithful
46// to UTF8.h rather than rewriting it as `(FIRST..=LAST).contains(..)`.
47#[allow(clippy::manual_range_contains)]
48pub fn decode_utf8_slow_path<const ALLOW_SURROGATES: bool>(
49    bytes: &[u8],
50    i: &mut usize,
51    mut error: impl FnMut(&str),
52) -> u32 {
53    let ch = at(bytes, *i);
54    let result: u32;
55
56    debug_assert!(is_utf8_start(ch as u8));
57
58    if (ch & 0xE0) == 0xC0 {
59        let ch1 = at(bytes, *i + 1);
60        if (ch1 & 0xC0) != 0x80 {
61            *i += 1;
62            error("Invalid UTF-8 continuation byte");
63            return UNICODE_REPLACEMENT_CHARACTER;
64        }
65
66        *i += 2;
67        result = ((ch & 0x1F) << 6) | (ch1 & 0x3F);
68        if result <= 0x7F {
69            error("Non-canonical UTF-8 encoding");
70            return UNICODE_REPLACEMENT_CHARACTER;
71        }
72    } else if (ch & 0xF0) == 0xE0 {
73        let ch1 = at(bytes, *i + 1);
74        if (ch1 & 0x40) != 0 || (ch1 & 0x80) == 0 {
75            *i += 1;
76            error("Invalid UTF-8 continuation byte");
77            return UNICODE_REPLACEMENT_CHARACTER;
78        }
79        let ch2 = at(bytes, *i + 2);
80        if (ch2 & 0x40) != 0 || (ch2 & 0x80) == 0 {
81            *i += 2;
82            error("Invalid UTF-8 continuation byte");
83            return UNICODE_REPLACEMENT_CHARACTER;
84        }
85        *i += 3;
86        result = ((ch & 0x0F) << 12) | ((ch1 & 0x3F) << 6) | (ch2 & 0x3F);
87        if result <= 0x7FF {
88            error("Non-canonical UTF-8 encoding");
89            return UNICODE_REPLACEMENT_CHARACTER;
90        }
91        if result >= UNICODE_SURROGATE_FIRST && result <= UNICODE_SURROGATE_LAST && !ALLOW_SURROGATES
92        {
93            error(&format!("Invalid UTF-8 code point 0x{:X}", result));
94            return UNICODE_REPLACEMENT_CHARACTER;
95        }
96    } else if (ch & 0xF8) == 0xF0 {
97        let ch1 = at(bytes, *i + 1);
98        if (ch1 & 0x40) != 0 || (ch1 & 0x80) == 0 {
99            *i += 1;
100            error("Invalid UTF-8 continuation byte");
101            return UNICODE_REPLACEMENT_CHARACTER;
102        }
103        let ch2 = at(bytes, *i + 2);
104        if (ch2 & 0x40) != 0 || (ch2 & 0x80) == 0 {
105            *i += 2;
106            error("Invalid UTF-8 continuation byte");
107            return UNICODE_REPLACEMENT_CHARACTER;
108        }
109        let ch3 = at(bytes, *i + 3);
110        if (ch3 & 0x40) != 0 || (ch3 & 0x80) == 0 {
111            *i += 3;
112            error("Invalid UTF-8 continuation byte");
113            return UNICODE_REPLACEMENT_CHARACTER;
114        }
115        *i += 4;
116        result =
117            ((ch & 0x07) << 18) | ((ch1 & 0x3F) << 12) | ((ch2 & 0x3F) << 6) | (ch3 & 0x3F);
118        if result <= 0xFFFF {
119            error("Non-canonical UTF-8 encoding");
120            return UNICODE_REPLACEMENT_CHARACTER;
121        }
122        if result > UNICODE_MAX_VALUE {
123            error(&format!("Invalid UTF-8 code point 0x{:X}", result));
124            return UNICODE_REPLACEMENT_CHARACTER;
125        }
126    } else {
127        *i += 1;
128        error(&format!("Invalid UTF-8 lead byte 0x{:X}", ch & 0xFF));
129        return UNICODE_REPLACEMENT_CHARACTER;
130    }
131
132    result
133}
134
135/// Decode a sequence of UTF8 encoded bytes into a Unicode codepoint, ASCII fast
136/// path. Port of `decodeUTF8` (UTF8.h:187-193). In case of decoding errors, the
137/// provided callback is invoked with an appropriate message and
138/// UNICODE_REPLACEMENT_CHARACTER is returned.
139///
140/// \tparam ALLOW_SURROGATES when false, values in the surrogate range are
141///     reported as errors.
142#[inline]
143pub fn decode_utf8<const ALLOW_SURROGATES: bool>(
144    bytes: &[u8],
145    i: &mut usize,
146    error: impl FnMut(&str),
147) -> u32 {
148    if *i < bytes.len() && (bytes[*i] & 0x80) == 0 {
149        // Ordinary ASCII?
150        let c = bytes[*i] as u32;
151        *i += 1;
152        return c;
153    }
154    decode_utf8_slow_path::<ALLOW_SURROGATES>(bytes, i, error)
155}
156
157/// Encode a 32-bit value into UTF-16, appending to `out`. If the value is a
158/// part of a surrogate pair, it is encoded without any conversion. Port of
159/// `encodeUTF16` (UTF8.h:197-210).
160#[inline]
161pub fn encode_utf16(out: &mut Vec<u16>, cp: u32) {
162    if cp < 0x10000 {
163        out.push(cp as u16);
164    } else {
165        debug_assert!(cp <= UNICODE_MAX_VALUE, "invalid Unicode value");
166        let cp = cp - 0x10000;
167        out.push((UTF16_HIGH_SURROGATE + ((cp >> 10) & 0x3FF)) as u16);
168        out.push((UTF16_LOW_SURROGATE + (cp & 0x3FF)) as u16);
169    }
170}
171
172/// Decode a UTF-8 sequence, which is assumed to be valid, but may possibly
173/// contain explicitly encoded surrogate pairs, into a UTF-16 sequence. Port of
174/// `convertUTF8WithSurrogatesToUTF16` (UTF8.h:216-225).
175pub fn convert_utf8_with_surrogates_to_utf16(bytes: &[u8]) -> Vec<u16> {
176    let mut out = Vec::with_capacity(bytes.len());
177    let mut i = 0usize;
178    while i < bytes.len() {
179        // Surrogates are ALLOWED; the input is assumed valid, so the error
180        // callback is unreachable (a no-op here).
181        let cp = decode_utf8::<true>(bytes, &mut i, |_| {});
182        encode_utf16(&mut out, cp);
183    }
184    out
185}
186
187#[cfg(test)]
188mod tests {
189    use super::convert_utf8_with_surrogates_to_utf16;
190
191    #[test]
192    fn ascii_passthrough() {
193        assert_eq!(convert_utf8_with_surrogates_to_utf16(b"abc"), vec![0x61, 0x62, 0x63]);
194    }
195
196    #[test]
197    fn bmp_non_ascii() {
198        // U+54C8 哈 = e5 93 88
199        assert_eq!(convert_utf8_with_surrogates_to_utf16(&[0xE5, 0x93, 0x88]), vec![0x54C8]);
200    }
201
202    #[test]
203    fn astral_4byte() {
204        // U+1F44B 👋 = f0 9f 91 8b -> surrogate pair D83D DC4B
205        assert_eq!(
206            convert_utf8_with_surrogates_to_utf16(&[0xF0, 0x9F, 0x91, 0x8B]),
207            vec![0xD83D, 0xDC4B]
208        );
209    }
210
211    #[test]
212    fn wtf8_lone_surrogate() {
213        // Lone high surrogate U+D800 as WTF-8 = ed a0 80 -> single unit 0xD800
214        assert_eq!(convert_utf8_with_surrogates_to_utf16(&[0xED, 0xA0, 0x80]), vec![0xD800]);
215    }
216}