/** * The text decoder alphaTab's importers use in RiffDex's importer bundle (in place of the global `TextDecoder`). * * alphaTab decodes file text with `new TextDecoder(encoding)`. React Native's Hermes engine has not always had * TextDecoder, so this uses the platform's decoder when it has one for the encoding, and otherwise decodes UTF-8, * UTF-16 and windows-1252 itself, following the WHATWG Encoding Standard's decoders: invalid bytes become U+FFFD * exactly as a browser's TextDecoder makes them, so a file reads the same either way. src/vendor/alphatab/README.md * records the check that every test file reads identically with and without a native TextDecoder. */ const Native: (new (label?: string) => { decode(input?: Uint8Array): string }) | undefined = (globalThis as any).TextDecoder; function utf8(bytes: Uint8Array): string { const out: number[] = []; let needed = 0, seen = 0, cp = 0, lower = 0x80, upper = 0xbf; let i = bytes.length >= 3 && bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf ? 3 : 0; for (; i < bytes.length; i++) { const b = bytes[i]; if (needed === 0) { if (b <= 0x7f) out.push(b); else if (b >= 0xc2 && b <= 0xdf) { needed = 1; cp = b & 0x1f; } else if (b >= 0xe0 && b <= 0xef) { if (b === 0xe0) lower = 0xa0; if (b === 0xed) upper = 0x9f; needed = 2; cp = b & 0x0f; } else if (b >= 0xf0 && b <= 0xf4) { if (b === 0xf0) lower = 0x90; if (b === 0xf4) upper = 0x8f; needed = 3; cp = b & 0x07; } else out.push(0xfffd); continue; } if (b < lower || b > upper) { cp = needed = seen = 0; lower = 0x80; upper = 0xbf; out.push(0xfffd); i--; // the byte starts again as a new sequence continue; } lower = 0x80; upper = 0xbf; cp = (cp << 6) | (b & 0x3f); if (++seen === needed) { out.push(cp); cp = needed = seen = 0; } } if (needed) out.push(0xfffd); let text = ''; for (let k = 0; k < out.length; k += 8192) text += String.fromCodePoint(...out.slice(k, k + 8192)); return text; } // The standard's shared UTF-16 decoder: a lead surrogate without a trail surrogate, a lone trail surrogate, and an odd // byte at the end each become one U+FFFD (an odd byte after an unpaired lead surrogate makes just one between them). function utf16(bytes: Uint8Array, littleEndian: boolean): string { const out: number[] = []; let lead: number | null = null; let i = bytes.length >= 2 && ((littleEndian && bytes[0] === 0xff && bytes[1] === 0xfe) || (!littleEndian && bytes[0] === 0xfe && bytes[1] === 0xff)) ? 2 : 0; for (; i + 1 < bytes.length; i += 2) { const unit = littleEndian ? bytes[i] | (bytes[i + 1] << 8) : (bytes[i] << 8) | bytes[i + 1]; if (lead !== null) { if (unit >= 0xdc00 && unit <= 0xdfff) { out.push(lead, unit); lead = null; continue; } out.push(0xfffd); lead = null; } if (unit >= 0xd800 && unit <= 0xdbff) lead = unit; else if (unit >= 0xdc00 && unit <= 0xdfff) out.push(0xfffd); else out.push(unit); } if (lead !== null || i < bytes.length) out.push(0xfffd); let text = ''; for (let k = 0; k < out.length; k += 8192) text += String.fromCharCode(...out.slice(k, k + 8192)); return text; } // windows-1252 bytes 0x80–0x9F (the standard maps "latin1" and "iso-8859-1" to windows-1252 too). const W1252 = [0x20ac, 0x81, 0x201a, 0x192, 0x201e, 0x2026, 0x2020, 0x2021, 0x2c6, 0x2030, 0x160, 0x2039, 0x152, 0x8d, 0x17d, 0x8f, 0x90, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x2dc, 0x2122, 0x161, 0x203a, 0x153, 0x9d, 0x17e, 0x178]; function windows1252(bytes: Uint8Array): string { const units: number[] = []; for (const b of bytes) units.push(b >= 0x80 && b <= 0x9f ? W1252[b - 0x80] : b); let text = ''; for (let k = 0; k < units.length; k += 8192) text += String.fromCharCode(...units.slice(k, k + 8192)); return text; } // Labels the WHATWG Encoding Standard gives these encodings (it treats latin1 and ascii as windows-1252). type Encoding = 'utf-8' | 'utf-16le' | 'utf-16be' | 'windows-1252'; const LABELS = new Map(); for (const label of ['unicode-1-1-utf-8', 'unicode11utf8', 'unicode20utf8', 'utf-8', 'utf8', 'x-unicode20utf8']) LABELS.set(label, 'utf-8'); for (const label of ['csunicode', 'iso-10646-ucs-2', 'ucs-2', 'unicode', 'unicodefeff', 'utf-16', 'utf-16le']) LABELS.set(label, 'utf-16le'); for (const label of ['unicodefffe', 'utf-16be']) LABELS.set(label, 'utf-16be'); for (const label of ['ansi_x3.4-1968', 'ascii', 'cp1252', 'cp819', 'csisolatin1', 'ibm819', 'iso-8859-1', 'iso-ir-100', 'iso8859-1', 'iso88591', 'iso_8859-1', 'iso_8859-1:1987', 'l1', 'latin1', 'us-ascii', 'windows-1252', 'x-cp1252']) LABELS.set(label, 'windows-1252'); export class RiffDexTextDecoder { private readonly encoding: Encoding | null; private readonly native: { decode(input?: Uint8Array): string } | null; public constructor(label: string = 'utf-8') { this.encoding = LABELS.get(String(label).trim().toLowerCase()) ?? null; let native: { decode(input?: Uint8Array): string } | null = null; if (Native) { try { native = new Native(label); } catch (e) { if (!this.encoding) throw e; } } // As a browser's TextDecoder does for an encoding it doesn't know (alphaTab then reports the file as unreadable). if (!native && !this.encoding) throw new RangeError(`The encoding label provided ('${label}') is invalid.`); this.native = native; } public decode(input?: Uint8Array): string { const bytes = input ?? new Uint8Array(0); if (this.native) return this.native.decode(bytes); switch (this.encoding) { case 'utf-16le': return utf16(bytes, true); case 'utf-16be': return utf16(bytes, false); case 'windows-1252': return windows1252(bytes); default: return utf8(bytes); } } }