OverviewFilesCommitsIssuesDocsReleasesPeers

favela-surface-android / lib / host / utf8.js

110 lines · javascript ref:
// 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
  }
}