From d4324c6be50302b3642fd2f0e5f0cd87f44fa772 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Wed, 15 Jan 2025 16:30:30 -0800 Subject: [PATCH] Implement `atob()` and `btoa()` --- .changeset/wise-dryers-swim.md | 5 ++ apps/tests/src/window.test.ts | 30 ++++++- packages/runtime/src/$.ts | 4 + packages/runtime/src/window.ts | 17 ++++ source/main.c | 2 + source/window.c | 143 +++++++++++++++++++++++++++++++++ source/window.h | 4 + 7 files changed, 201 insertions(+), 4 deletions(-) create mode 100644 .changeset/wise-dryers-swim.md create mode 100644 source/window.c create mode 100644 source/window.h diff --git a/.changeset/wise-dryers-swim.md b/.changeset/wise-dryers-swim.md new file mode 100644 index 00000000..de048927 --- /dev/null +++ b/.changeset/wise-dryers-swim.md @@ -0,0 +1,5 @@ +--- +"@nx.js/runtime": patch +--- + +Implement `atob()` and `btoa()` diff --git a/apps/tests/src/window.test.ts b/apps/tests/src/window.test.ts index 45efe027..211b983d 100644 --- a/apps/tests/src/window.test.ts +++ b/apps/tests/src/window.test.ts @@ -4,10 +4,14 @@ import * as assert from 'uvu/assert'; const test = suite('window'); test('instanceof', () => { - assert.equal(window instanceof Window, true); - assert.equal(globalThis instanceof Window, true); - assert.equal(globalThis === window, true); - assert.equal(Object.prototype.toString.call(window), '[object Window]'); + assert.instance(globalThis, Window); + assert.equal(Object.prototype.toString.call(globalThis), '[object Window]'); + + // Deno v2 doesn't have a `window` global + if (typeof window !== 'undefined') { + assert.equal(window instanceof Window, true); + assert.equal(globalThis === window, true); + } }); test('throws illegal constructor', () => { @@ -33,4 +37,22 @@ test('supports global events', () => { assert.ok(e.defaultPrevented); }); +test('atob', () => { + assert.equal(globalThis.hasOwnProperty('atob'), true); + + assert.equal(atob.length, 1); + + const result = atob('SGVsbG8sIHdvcmxk'); + assert.equal(result, 'Hello, world'); +}); + +test('btoa', () => { + assert.equal(globalThis.hasOwnProperty('btoa'), true); + + assert.equal(btoa.length, 1); + + const result = btoa('Hello, world'); + assert.equal(result, 'SGVsbG8sIHdvcmxk'); +}); + test.run(); diff --git a/packages/runtime/src/$.ts b/packages/runtime/src/$.ts index ff033763..3d90f2ec 100644 --- a/packages/runtime/src/$.ts +++ b/packages/runtime/src/$.ts @@ -41,6 +41,7 @@ import type { DOMPoint, DOMPointInit } from './dompoint'; import type { DOMMatrix, DOMMatrixReadOnly, DOMMatrixInit } from './dommatrix'; import type { Gamepad, GamepadButton } from './navigator/gamepad'; import type { Crypto, CryptoKey, SubtleCrypto } from './crypto'; +import type { Window } from './window'; import type { Algorithm, BufferSource } from './types'; import type { PromiseState } from '@nx.js/inspect'; @@ -346,6 +347,9 @@ export interface Init { wasmGlobalGet(g: WasmGlobalOpaque): any; wasmGlobalSet(g: WasmGlobalOpaque, v: any): void; + // window.c + windowInit(c: Window): void; + applyPath: any; } diff --git a/packages/runtime/src/window.ts b/packages/runtime/src/window.ts index 1e953e86..f767740b 100644 --- a/packages/runtime/src/window.ts +++ b/packages/runtime/src/window.ts @@ -10,6 +10,7 @@ import type { ErrorEvent, PromiseRejectionEvent, } from './polyfills/event'; +import { $ } from './$'; /** * The `Window` class represents the global scope within the application. @@ -29,6 +30,7 @@ def(Window); export var window: Window & typeof globalThis = globalThis; def(proto(window, Window), 'window'); +$.windowInit(window); Object.defineProperty(window, Symbol.toStringTag, { get() { @@ -149,3 +151,18 @@ export declare function removeEventListener( * @see {@link https://developer.mozilla.org/docs/Web/API/EventTarget/dispatchEvent | MDN Reference} */ export declare function dispatchEvent(event: Event): boolean; + +/** + * Decodes a string of data which has been encoded using Base64 encoding. + * + * @see https://developer.mozilla.org/docs/Web/API/Window/atob + */ +export declare function atob(s: string): string; + +/** + * Creates a Base64-encoded ASCII string from a binary string (i.e., a string in + * which each character in the string is treated as a byte of binary data). + * + * @see https://developer.mozilla.org/docs/Web/API/Window/btoa + */ +export declare function btoa(s: string): string; diff --git a/source/main.c b/source/main.c index c1d4021e..ec0140ac 100644 --- a/source/main.c +++ b/source/main.c @@ -34,6 +34,7 @@ #include "types.h" #include "url.h" #include "wasm.h" +#include "window.h" #define LOG_FILENAME "nxjs-debug.log" @@ -644,6 +645,7 @@ int main(int argc, char *argv[]) { nx_init_url(ctx, nx_ctx->init_obj); nx_init_swkbd(ctx, nx_ctx->init_obj); nx_init_wasm(ctx, nx_ctx->init_obj); + nx_init_window(ctx, nx_ctx->init_obj); const JSCFunctionListEntry init_function_list[] = { JS_CFUNC_DEF("exit", 0, js_exit), JS_CFUNC_DEF("cwd", 0, js_cwd), diff --git a/source/window.c b/source/window.c new file mode 100644 index 00000000..bbd1f091 --- /dev/null +++ b/source/window.c @@ -0,0 +1,143 @@ +#include "window.h" + +static JSValue nx_atob(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) { + size_t input_len; + const char *input = JS_ToCStringLen(ctx, &input_len, argv[0]); + if (!input) + return JS_EXCEPTION; + + // Calculate decoded length (removing padding) + size_t padding = 0; + if (input_len > 0 && input[input_len - 1] == '=') + padding++; + if (input_len > 1 && input[input_len - 2] == '=') + padding++; + size_t output_len = (input_len * 3) / 4 - padding; + + // Allocate output buffer + uint8_t *output = js_malloc(ctx, output_len + 1); + if (!output) { + JS_FreeCString(ctx, input); + return JS_EXCEPTION; + } + + // Base64 decoding lookup table + static const int8_t b64_table[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, // 0-15 + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, // 16-31 + -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, 62, -1, -1, -1, 63, // 32-47 + 52, 53, 54, 55, 56, 57, 58, 59, + 60, 61, -1, -1, -1, -1, -1, -1, // 48-63 + -1, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, // 64-79 + 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, -1, -1, -1, -1, -1, // 80-95 + -1, 26, 27, 28, 29, 30, 31, 32, + 33, 34, 35, 36, 37, 38, 39, 40, // 96-111 + 41, 42, 43, 44, 45, 46, 47, 48, + 49, 50, 51, -1, -1, -1, -1, -1 // 112-127 + }; + + // Decode + size_t i = 0, j = 0; + uint32_t accum = 0; + int bits = 0; + + while (i < input_len) { + int c = input[i++]; + int x = b64_table[c & 0x7F]; + + if (x == -1) { + js_free(ctx, output); + JS_FreeCString(ctx, input); + return JS_ThrowSyntaxError(ctx, "Invalid base64 character"); + } + + accum = (accum << 6) | x; + bits += 6; + + if (bits >= 8) { + bits -= 8; + output[j++] = (accum >> bits) & 0xFF; + } + } + + output[output_len] = 0; + JS_FreeCString(ctx, input); + + JSValue result = JS_NewStringLen(ctx, (char *)output, output_len); + js_free(ctx, output); + return result; +} + +static JSValue nx_btoa(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) { + const char *input = JS_ToCString(ctx, argv[0]); + if (!input) { + return JS_EXCEPTION; + } + + size_t input_len = strlen(input); + size_t output_len = ((input_len + 2) / 3) * 4; + unsigned char *output = js_malloc(ctx, output_len + 1); + if (!output) { + JS_FreeCString(ctx, input); + return JS_EXCEPTION; + } + + static const char b64_chars[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + // Encode + size_t i = 0, j = 0; + uint32_t accum = 0; + int bits = 0; + + while (i < input_len) { + accum = (accum << 8) | (input[i++] & 0xFF); + bits += 8; + + while (bits >= 6) { + bits -= 6; + output[j++] = b64_chars[(accum >> bits) & 0x3F]; + } + } + + // Handle remaining bits + if (bits > 0) { + accum <<= (6 - bits); + output[j++] = b64_chars[accum & 0x3F]; + } + + // Add padding + while (j < output_len) { + output[j++] = '='; + } + + output[output_len] = 0; + JS_FreeCString(ctx, input); + + JSValue result = JS_NewString(ctx, (char *)output); + js_free(ctx, output); + return result; +} + +static JSValue nx_window_init(JSContext *ctx, JSValueConst this_val, int argc, + JSValueConst *argv) { + NX_DEF_FUNC(argv[0], "atob", nx_atob, 1); + NX_DEF_FUNC(argv[0], "btoa", nx_btoa, 1); + return JS_UNDEFINED; +} + +static const JSCFunctionListEntry function_list[] = { + JS_CFUNC_DEF("windowInit", 1, nx_window_init), +}; + +void nx_init_window(JSContext *ctx, JSValueConst init_obj) { + JS_SetPropertyFunctionList(ctx, init_obj, function_list, + countof(function_list)); +} diff --git a/source/window.h b/source/window.h new file mode 100644 index 00000000..95108305 --- /dev/null +++ b/source/window.h @@ -0,0 +1,4 @@ +#pragma once +#include "types.h" + +void nx_init_window(JSContext *ctx, JSValueConst init_obj);