io-utf8.ts (2097B)
1 // Strict stateless UTF-8. No BOM stripping, replacement or normalization. 2 // Source carries the copied <=3-byte suffix between requests. 3 export class UTF8Error extends Error {} 4 export function decodeUTF8(bytes: readonly number[], final: boolean): { text: string; carry: number[] } { 5 if (bytes.length > 4096 || bytes.some(b => !Number.isInteger(b) || b < 0 || b > 255)) throw new UTF8Error('invalid byte chunk'); 6 const chars: string[] = []; 7 const bad = (offset: number, why: string): never => { throw new UTF8Error(`${why} at byte ${offset}`); }; 8 for (let offset = 0; offset < bytes.length;) { 9 const lead = bytes[offset]!; 10 if (lead <= 0x7f) { chars.push(String.fromCodePoint(lead)); offset++; continue; } 11 let count: number, low = 0x80, high = 0xbf, code: number; 12 if (lead >= 0xc2 && lead <= 0xdf) { count = 2; code = lead & 0x1f; } 13 else if (lead >= 0xe0 && lead <= 0xef) { 14 count = 3; code = lead & 0x0f; if (lead === 0xe0) low = 0xa0; if (lead === 0xed) high = 0x9f; 15 } else if (lead >= 0xf0 && lead <= 0xf4) { 16 count = 4; code = lead & 7; if (lead === 0xf0) low = 0x90; if (lead === 0xf4) high = 0x8f; 17 } else return bad(offset, 'invalid UTF-8 leading byte'); 18 const available = Math.min(count - 1, bytes.length - offset - 1); 19 for (let i = 1; i <= available; i++) { 20 const b = bytes[offset + i]!; 21 if (b < (i === 1 ? low : 0x80) || b > (i === 1 ? high : 0xbf)) return bad(offset, 'invalid UTF-8 sequence'); 22 code = code * 64 + (b & 63); 23 } 24 if (available < count - 1) { 25 if (final) return bad(offset, 'incomplete UTF-8 sequence'); 26 return { text: chars.join(''), carry: bytes.slice(offset) }; 27 } 28 chars.push(String.fromCodePoint(code)); offset += count; 29 } 30 return { text: chars.join(''), carry: [] }; 31 } 32 export function encodeUTF8(text: string): number[] { 33 let count = 0; 34 for (const char of text) { 35 const scalar = char.codePointAt(0)!; 36 if (++count > 1024 || scalar >= 0xd800 && scalar <= 0xdfff) throw new UTF8Error('invalid scalar chunk'); 37 } 38 return [...new TextEncoder().encode(text)]; 39 }