Skip to content

Commit

Permalink
Implement atob() and btoa()
Browse files Browse the repository at this point in the history
  • Loading branch information
TooTallNate committed Jan 16, 2025
1 parent f5bfcf9 commit d4324c6
Show file tree
Hide file tree
Showing 7 changed files with 201 additions and 4 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-dryers-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@nx.js/runtime": patch
---

Implement `atob()` and `btoa()`
30 changes: 26 additions & 4 deletions apps/tests/src/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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();
4 changes: 4 additions & 0 deletions packages/runtime/src/$.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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;
}

Expand Down
17 changes: 17 additions & 0 deletions packages/runtime/src/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
ErrorEvent,
PromiseRejectionEvent,
} from './polyfills/event';
import { $ } from './$';

/**
* The `Window` class represents the global scope within the application.
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
2 changes: 2 additions & 0 deletions source/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "types.h"
#include "url.h"
#include "wasm.h"
#include "window.h"

#define LOG_FILENAME "nxjs-debug.log"

Expand Down Expand Up @@ -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),
Expand Down
143 changes: 143 additions & 0 deletions source/window.c
Original file line number Diff line number Diff line change
@@ -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));
}
4 changes: 4 additions & 0 deletions source/window.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#pragma once
#include "types.h"

void nx_init_window(JSContext *ctx, JSValueConst init_obj);

0 comments on commit d4324c6

Please sign in to comment.