#996·obscura

btoa encodes input as UTF-8 instead of Latin-1 (wrong output for 128-255, no InvalidCharacterError above 255)

Author: mnazaCreated Sep 16, 2026Updated Sep 16, 2026

Summary

The btoa polyfill encodes its input as UTF-8 instead of Latin-1, so any code point in 128–255 produces the wrong base64, and code points above 255 are silently multi-byte-encoded instead of throwing InvalidCharacterError. Per the HTML spec, btoa maps each code unit to one byte and throws for any code point > 0xFF.

obscura runs on deno_core (no deno_runtime), so V8 does not provide btoa/atob — this polyfill is the active implementation (the atob_decodes_large_payload test exercises the pair).

Details

crates/obscura-js/js/bootstrap.js:

javascript
globalThis.btoa = globalThis.btoa || ((s) => { const b = new TextEncoder().encode(s); ... });
  • btoa("é") (U+00E9) returns "w6k=" (base64 of the UTF-8 bytes 0xC3 0xA9) instead of the correct "6Q==" (Latin-1 byte 0xE9).
  • btoa("\\\u{1F600}") (an emoji) encodes 4 UTF-8 bytes and returns a string, when it should throw InvalidCharacterError.

Fix

Build the byte array with a charCodeAt loop: each code unit 0–255 maps to one byte; throw DOMException("...", "InvalidCharacterError") for any code unit > 255. Keep the existing base64 encoding loop.

Affected

  • crates/obscura-js/js/bootstrap.jsglobalThis.btoa