Expand description
The UTF-8 ⇄ UTF-16 boundary for JS string indices.
A JS String is a sequence of UTF-16 code units, and every index-bearing
String.prototype operation counts in those units: length, charAt,
charCodeAt, codePointAt, at, indexOf/lastIndexOf, slice,
substring, substr, split, padStart/padEnd, s[i], and a RegExp’s
.index/lastIndex.
Indices are not the only place the unit sequence is observable. Relational
comparison (< <= > >=, 7.2.13 IsLessThan) and the default Array.prototype .sort comparator order strings by code unit too, which is a different
order than Rust’s str: Ord (code point / UTF-8 byte order) for any pair
that straddles U+E000: a surrogate is 0xD800..0xE000, so every astral
character sorts BELOW every BMP character from U+E000 up. See cmp_units.
node-js stores a JS string as a Rust String (UTF-8): fusevm::Value::Str
and the host heap’s JsObj::Str are both String, and fusevm is a pinned
external dependency, so the storage type is not ours to change. Every
JS-visible index therefore has to be translated, and this module is the one
place that translation happens. Indices are code-unit counts (U16Index);
Rust’s own string offsets are byte counts; the two are only equal on ASCII,
and the newtype exists so a function that has both in scope cannot silently
pass one where the other belongs.
§The lone-surrogate boundary
Rust String cannot hold an unpaired surrogate — char excludes
U+D800..=U+DFFF — so an operation that cuts a surrogate pair in half
cannot reproduce node’s result exactly. "𝒳".charAt(0) is the lone
surrogate \ud835 in node; here it is U+FFFD. This is deliberately the
narrowest possible gap:
- Index arithmetic is exact — a cut at a surrogate boundary still happens
at the right place, still yields a 1-unit string, and every surrounding
index still lines up.
"𝒳".lengthis2and"𝒳".charCodeAt(0)is55349, read from the intact original. - Printing is byte-identical: node itself writes
ef bf bd(U+FFFD) when a lone surrogate reaches stdout, verified withnode -e 'process.stdout.write("𝒳".charAt(0))' | xxd. - Only re-inspecting an extracted half differs —
"𝒳".charAt(0).charCodeAt(0)(65533 here, 55349 in node),JSON.stringify("𝒳".charAt(0)), and re-joining two halves back into the original astral character.
Closing that last gap means replacing String with a WTF-8 buffer
throughout fusevm and all 47 stdlib modules, which the pinned dependency
forbids.
Structs§
- U16Index
- An index into a JS string, counted in UTF-16 code units.
- Units
- A JS string decoded into its UTF-16 code units, so that index arithmetic can be done directly on the units JS counts.
Functions§
- byte_
of_ index - The UTF-8 byte offset corresponding to a UTF-16 index into
s. An index that falls inside a surrogate pair rounds down to the start of that code point, so the result is always a validstrboundary. - cmp_
units - Lexicographic order over UTF-16 code units — the order JS’s
</<=/>/>=and the defaultsortcomparator use (7.2.13 IsLessThan step 3.d compares “the code unit at index k”). - index_
of_ byte - The UTF-16 index corresponding to a UTF-8 byte offset into
s. - is_
js_ whitespace - ECMA-262
WhiteSpace(11.2) +LineTerminator(11.3): the exact character setString.prototype.trim,ToNumber(string),parseIntandparseFloatskip. - js_trim
swith leading and trailing JS whitespace removed.- js_
trim_ end swith trailing JS whitespace removed.- js_
trim_ start swith leading JS whitespace removed.- len
- The length of
sin UTF-16 code units — the value ofs.lengthin JS. - to_
string_ lossy - Decode UTF-16 code units back to a Rust
String, mapping any unpaired surrogate toU+FFFD— the same replacement node performs when a lone surrogate is written to stdout. - to_
uint16 ToUint16(n)— the modulo-2^16 wrapString.fromCharCodeapplies to each argument, sofromCharCode(0x1D4B3)produces U+D4B3 and not U+1D4B3.