mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix(cabinet): harden global encoders against lone UTF-16 surrogates
A lone (unpaired) UTF-16 surrogate — a truncated emoji in a backend name/remark
embedded in a subscription/connection URL — makes encodeURI/encodeURIComponent throw
a URIError on iOS WebKit (V8: 'URI malformed'; Safari/JSC: 'String contained an
illegal UTF-16 sequence'). qrcode.react calls encodeURI internally, so such a value
crashed the QR render and tripped the page-level ErrorBoundary ('Something went
wrong / String contained an illegal UTF-16 sequence / Try again').
Rather than wrap each (current and future) call site — and third-party libs we can't
edit — sanitise at the single chokepoint: patch the global encodeURI/encodeURIComponent
at bootstrap to replace lone surrogates with U+FFFD (same remedy as toWellFormed()).
Fail-safe, not fail-broken: verified byte-identical output for all well-formed strings
(URLs unaffected) and no-throw on lone surrogates for the real call patterns (qrcode.react
and btoa(unescape(encodeURIComponent(...)))). A fast path skips the rewrite when no
surrogate code units are present, so the hot path (every request URL) is untouched.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
39
src/utils/installEncodingSurrogateGuard.ts
Normal file
39
src/utils/installEncodingSurrogateGuard.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { sanitizeSurrogates } from './sanitizeSurrogates';
|
||||
|
||||
/**
|
||||
* App-wide guard against the "String contained an illegal UTF-16 sequence" crash.
|
||||
*
|
||||
* A lone (unpaired) UTF-16 surrogate — typically a truncated emoji in a backend
|
||||
* name/remark that ends up in a subscription/connection URL — makes encodeURI /
|
||||
* encodeURIComponent throw a URIError on iOS WebKit/JavaScriptCore (V8: "URI
|
||||
* malformed"; Safari/JSC: "String contained an illegal UTF-16 sequence"). Any
|
||||
* code path that encodes such a string crashes — including third-party libs we
|
||||
* can't edit (e.g. qrcode.react calls encodeURI internally) and any encode call
|
||||
* added later. The base64 idiom btoa(unescape(encodeURIComponent(x))) is covered
|
||||
* transitively, since its encodeURIComponent is guarded here. (We do NOT patch
|
||||
* btoa itself: it rejects every char > U+00FF — Cyrillic, emoji, even the U+FFFD
|
||||
* replacement — with a separate "Invalid character" error that sanitising
|
||||
* surrogates cannot fix, and nothing base64-encodes raw Unicode directly.)
|
||||
*
|
||||
* Instead of wrapping every (current and future) call site, we sanitise at the
|
||||
* single chokepoint: the global encoders themselves. This is fail-safe, not
|
||||
* fail-broken — for any well-formed string there are no lone surrogates, so the
|
||||
* output is byte-for-byte identical; only strings that would otherwise have
|
||||
* thrown get their lone surrogates replaced with U+FFFD (the same remedy as
|
||||
* String.prototype.toWellFormed()).
|
||||
*
|
||||
* Must run before any rendering or network call. Idempotent.
|
||||
*/
|
||||
export function installEncodingSurrogateGuard(): void {
|
||||
const flag = '__surrogateEncoderGuardInstalled';
|
||||
const g = globalThis as typeof globalThis & Record<string, unknown>;
|
||||
if (g[flag]) return;
|
||||
g[flag] = true;
|
||||
|
||||
const nativeEncodeURI = globalThis.encodeURI;
|
||||
const nativeEncodeURIComponent = globalThis.encodeURIComponent;
|
||||
|
||||
globalThis.encodeURI = (uri: string): string => nativeEncodeURI(sanitizeSurrogates(String(uri)));
|
||||
globalThis.encodeURIComponent = (uriComponent: string | number | boolean): string =>
|
||||
nativeEncodeURIComponent(sanitizeSurrogates(String(uriComponent)));
|
||||
}
|
||||
45
src/utils/sanitizeSurrogates.ts
Normal file
45
src/utils/sanitizeSurrogates.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Replace unpaired UTF-16 surrogates with the Unicode replacement char (U+FFFD).
|
||||
*
|
||||
* Why: a lone surrogate — e.g. a truncated emoji in a server/profile name embedded
|
||||
* in a subscription URL's `#remark` — makes `encodeURI()` throw. On V8 the message is
|
||||
* "URI malformed"; on iOS WebKit (JavaScriptCore, used by Telegram on iOS) it is
|
||||
* "String contained an illegal UTF-16 sequence". `qrcode.react` calls `encodeURI`
|
||||
* internally (qrcodegen `toUtf8ByteArray`), so such a string crashes the QR render and
|
||||
* trips the page-level ErrorBoundary. `btoa(unescape(encodeURIComponent(...)))` and the
|
||||
* Telegram native bridge throw on the same input.
|
||||
*
|
||||
* Sanitising keeps the link working (the surrogate only ever lives in a cosmetic remark)
|
||||
* and renders a replacement glyph instead of taking the whole page down. Implemented
|
||||
* without lookbehind for broad WebView compatibility.
|
||||
*/
|
||||
// Any surrogate code unit at all (paired or not). The vast majority of strings
|
||||
// (ASCII URLs, ids, tokens) contain none, so this lets us bail out in O(1) before
|
||||
// the per-char rebuild — important because the global encoder guard runs this on
|
||||
// every encodeURIComponent/encodeURI/btoa call.
|
||||
const SURROGATE_RANGE = /[\uD800-\uDFFF]/;
|
||||
|
||||
export function sanitizeSurrogates(value: string): string {
|
||||
if (!SURROGATE_RANGE.test(value)) return value;
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const code = value.charCodeAt(i);
|
||||
if (code >= 0xd800 && code <= 0xdbff) {
|
||||
// High surrogate: valid only if immediately followed by a low surrogate.
|
||||
const next = value.charCodeAt(i + 1);
|
||||
if (next >= 0xdc00 && next <= 0xdfff) {
|
||||
result += value[i] + value[i + 1];
|
||||
i++;
|
||||
} else {
|
||||
result += '<27>';
|
||||
}
|
||||
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
||||
// Low surrogate with no preceding high surrogate.
|
||||
result += '<27>';
|
||||
} else {
|
||||
result += value[i];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user