// Copied from favela-surface-android/lib/host/utf8.js (2026-10-02), unchanged.
// UTF-8 ↔ bytes, dependency-free.
//
// ⛔ WHY THIS EXISTS AT ALL. react-native-bare-kit's IPC is a byte stream on both sides and does NOT
// convert: `_read` pushes `new Uint8Array(data)` (index.js:59) and `_write` reads `.buffer` /
// `.byteOffset` / `.byteLength` off whatever it is given (index.js:65). So:
//
// - `data.toString()` on a read chunk is Array.prototype.toString — it yields "123,34,116,..." (the
// byte VALUES, comma-separated), not text. This silently fed garbage to the message parser and is
// what made a fully-booted node look like "worklet did not become ready in 120000ms".
// - writing a string writes nothing coherent, because a string has no `.buffer`.
//
// ⛔ NOT `b4a` and NOT `Buffer`. b4a's react-native entry does `require('react-native-b4a')` inside a
// try/catch, and Metro resolves statically — an unresolvable require fails the BUILD, catch or no
// catch. `Buffer` is not a React Native global. TextDecoder is not guaranteed present either. Hence
// hand-rolled, and hence testable off-device (test/utf8.test.mjs).
/**
* Encode a string to UTF-8 bytes.
*/
export function encodeUtf8 (str) {
const out = []
for (let i = 0; i < str.length; i++) {
let c = str.charCodeAt(i)
// Surrogate pair → one code point.
if (c >= 0xd800 && c <= 0xdbff && i + 1 < str.length) {
const next = str.charCodeAt(i + 1)
if (next >= 0xdc00 && next <= 0xdfff) {
c = 0x10000 + ((c - 0xd800) << 10) + (next - 0xdc00)
i++
}
}
if (c < 0x80) out.push(c)
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f))
else if (c < 0x10000) out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
else {
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f))
}
}
return new Uint8Array(out)
}
/**
* Decode COMPLETE UTF-8 bytes to a string. For a whole buffer that is known not to end mid-character
* (a facet slice, a config file) — a stream needs createUtf8Decoder below instead.
*/
export function decodeUtf8 (bytes) {
const d = createUtf8Decoder()
return d(bytes)
}
/**
* A STATEFUL decoder, because this decodes a STREAM.
*
* IPC chunk boundaries are not character boundaries any more than they are message boundaries: a
* multi-byte character can straddle two reads. A stateless per-chunk decode would corrupt exactly those
* characters (the node's own log lines contain an em dash, so this is not hypothetical). Incomplete
* trailing sequences are therefore held back until their remaining bytes arrive.
*
* Returns a function: (Uint8Array) => string.
*/
export function createUtf8Decoder () {
let pending = new Uint8Array(0)
return function decode (chunk) {
let bytes = chunk
if (pending.length > 0) {
bytes = new Uint8Array(pending.length + chunk.length)
bytes.set(pending, 0)
bytes.set(chunk, pending.length)
pending = new Uint8Array(0)
}
// Find the start of a trailing incomplete sequence, if any, and hold it back.
let end = bytes.length
for (let i = bytes.length - 1; i >= 0 && i >= bytes.length - 3; i--) {
const b = bytes[i]
if ((b & 0xc0) === 0x80) continue // continuation byte — keep walking back
const need = b < 0x80 ? 1 : b < 0xe0 ? 2 : b < 0xf0 ? 3 : 4
if (i + need > bytes.length) end = i // sequence is cut off by the chunk boundary
break
}
if (end < bytes.length) {
pending = bytes.slice(end)
bytes = bytes.subarray(0, end)
}
let str = ''
for (let i = 0; i < bytes.length;) {
const b = bytes[i]
let c
if (b < 0x80) { c = b; i += 1 } else if (b < 0xe0) {
c = ((b & 0x1f) << 6) | (bytes[i + 1] & 0x3f); i += 2
} else if (b < 0xf0) {
c = ((b & 0x0f) << 12) | ((bytes[i + 1] & 0x3f) << 6) | (bytes[i + 2] & 0x3f); i += 3
} else {
c = ((b & 0x07) << 18) | ((bytes[i + 1] & 0x3f) << 12) | ((bytes[i + 2] & 0x3f) << 6) |
(bytes[i + 3] & 0x3f)
i += 4
}
if (c > 0xffff) {
c -= 0x10000
str += String.fromCharCode(0xd800 + (c >> 10), 0xdc00 + (c & 0x3ff))
} else {
str += String.fromCharCode(c)
}
}
return str
}
}