Skip to content

Commit d83566f

Browse files
committed
Issue quickjs-ng#16 references not having atob and btoa (enc/dec base64)
I've started with naive implementation, there is significant discussion about how to make this faster: https://stackoverflow.com/questions/342409/ If there is interest I rewrite with some of these tricks.
1 parent 865ba1f commit d83566f

File tree

3 files changed

+177
-0
lines changed

3 files changed

+177
-0
lines changed

quickjs.c

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2321,6 +2321,7 @@ JSContext *JS_NewContext(JSRuntime *rt)
23212321
JS_AddIntrinsicPromise(ctx);
23222322
JS_AddIntrinsicBigInt(ctx);
23232323
JS_AddIntrinsicWeakRef(ctx);
2324+
JS_AddIntrinsicBase64(ctx);
23242325

23252326
JS_AddPerformance(ctx);
23262327

@@ -57422,6 +57423,144 @@ static void insert_weakref_record(JSValueConst target,
5742257423
*pwr = wr;
5742357424
}
5742457425

57426+
/* urlsafe_base64 atob/btoa */
57427+
57428+
// Base64 encoding table
57429+
static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
57430+
57431+
// btoa: binary to ASCII (base64 encode)
57432+
char* btoa(JSContext *ctx, const char* bin, size_t len) {
57433+
if (bin == NULL || len == 0) return NULL;
57434+
// Calculate output length (including padding)
57435+
size_t out_len = 4 * ((len + 2) / 3) + 1;
57436+
// Allocate memory for output string (plus null terminator)
57437+
char* out = (char*)js_mallocz(ctx, out_len);
57438+
57439+
if (out == NULL) return NULL;
57440+
57441+
// Base64 encoding process
57442+
size_t i, j;
57443+
57444+
for (i = 0, j = 0; i < len; i += 3, j += 4) {
57445+
uint32_t triple = (bin[i] << 16);
57446+
if (i + 1 < len) triple |= (bin[i + 1] << 8);
57447+
if (i + 2 < len) triple |= bin[i + 2];
57448+
57449+
out[j] = base64_table[(triple >> 18) & 0x3F];
57450+
out[j + 1] = base64_table[(triple >> 12) & 0x3F];
57451+
out[j + 2] = (i + 1 < len) ? base64_table[(triple >> 6) & 0x3F] : '=';
57452+
out[j + 3] = (i + 2 < len) ? base64_table[triple & 0x3F] : '=';
57453+
}
57454+
57455+
out[out_len] = '\0';
57456+
return out;
57457+
}
57458+
57459+
static JSValue js_base64_btoa(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
57460+
const char *fmt_str = NULL;
57461+
const char *result_str = NULL;
57462+
JSValue v;
57463+
57464+
if (argc > 0) {
57465+
fmt_str = JS_ToCString(ctx, argv[0]);
57466+
result_str = btoa(ctx, fmt_str, strlen(fmt_str));
57467+
v = JS_NewString(ctx, result_str);
57468+
JS_FreeCString(ctx, fmt_str);
57469+
return v;
57470+
} else {
57471+
return JS_ThrowTypeError(ctx, "ERR_MISSING_ARGS");
57472+
}
57473+
}
57474+
57475+
static int base64_index(char c) {
57476+
if (c >= 'A' && c <= 'Z')
57477+
return c - 'A';
57478+
if (c >= 'a' && c <= 'z')
57479+
return c - 'a' + 26;
57480+
if (c >= '0' && c <= '9')
57481+
return c - '0' + 52;
57482+
if (c == '-')
57483+
return 62;
57484+
if (c == '_')
57485+
return 63;
57486+
57487+
return -1; // Invalid character
57488+
}
57489+
57490+
// atob: ASCII to binary (base64 decode)
57491+
char* atob(JSContext *ctx, const char* str) {
57492+
if (str == NULL) return NULL;
57493+
57494+
size_t str_len = strlen(str);
57495+
57496+
if (str_len % 4 != 0) return NULL; // Invalid base64 string
57497+
57498+
// Calculate output length
57499+
size_t len = str_len / 4 * 3;
57500+
57501+
if (str[str_len - 1] == '=') len--;
57502+
57503+
if (str[str_len - 2] == '=') len--;
57504+
57505+
// Allocate memory for output data
57506+
size_t out_len = len+1;
57507+
char* out = (char*)js_mallocz(ctx, out_len);
57508+
57509+
if (out == NULL) return NULL;
57510+
57511+
// Base64 decoding process
57512+
size_t i, j;
57513+
57514+
for (i = 0, j = 0; i < str_len; i += 4, j += 3) {
57515+
int a = base64_index(str[i]);
57516+
int b = base64_index(str[i + 1]);
57517+
int c = str[i + 2] == '=' ? 0 : base64_index(str[i + 2]);
57518+
int d = str[i + 3] == '=' ? 0 : base64_index(str[i + 3]);
57519+
57520+
if (a == -1 || b == -1 || c == -1 || d == -1) {
57521+
js_free(ctx, out);
57522+
return NULL; // Invalid character
57523+
}
57524+
57525+
uint32_t triple = (a << 18) | (b << 12) | (c << 6) | d;
57526+
out[j] = (triple >> 16) & 0xFF;
57527+
57528+
if (j + 1 < len) out[j + 1] = (triple >> 8) & 0xFF;
57529+
if (j + 2 < len) out[j + 2] = triple & 0xFF;
57530+
}
57531+
57532+
out[out_len] = '\0';
57533+
return out;
57534+
}
57535+
57536+
57537+
static JSValue js_base64_atob(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
57538+
const char *fmt_str = NULL;
57539+
const char *result_str = NULL;
57540+
JSValue v;
57541+
57542+
if (argc > 0) {
57543+
fmt_str = JS_ToCString(ctx, argv[0]);
57544+
result_str = atob(ctx, fmt_str);
57545+
v = JS_NewString(ctx, result_str);
57546+
JS_FreeCString(ctx, fmt_str);
57547+
return v;
57548+
} else {
57549+
return JS_ThrowTypeError(ctx, "ERR_MISSING_ARGS");
57550+
}
57551+
}
57552+
57553+
static const JSCFunctionListEntry js_base64_funcs[] = {
57554+
JS_CFUNC_DEF("btoa", 1, js_base64_btoa ),
57555+
JS_CFUNC_DEF("atob", 1, js_base64_atob ),
57556+
};
57557+
57558+
void JS_AddIntrinsicBase64(JSContext *ctx)
57559+
{
57560+
JS_SetPropertyFunctionList(ctx, ctx->global_obj, js_base64_funcs, countof(js_base64_funcs));
57561+
}
57562+
57563+
5742557564
/* CallSite */
5742657565

5742757566
static void js_callsite_finalizer(JSRuntime *rt, JSValueConst val)

quickjs.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ JS_EXTERN void JS_AddIntrinsicTypedArrays(JSContext *ctx);
484484
JS_EXTERN void JS_AddIntrinsicPromise(JSContext *ctx);
485485
JS_EXTERN void JS_AddIntrinsicBigInt(JSContext *ctx);
486486
JS_EXTERN void JS_AddIntrinsicWeakRef(JSContext *ctx);
487+
JS_EXTERN void JS_AddIntrinsicBase64(JSContext *ctx);
487488
JS_EXTERN void JS_AddPerformance(JSContext *ctx);
488489

489490
/* for equality comparisons and sameness */

tests/test_base64.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { assert, assertArrayEquals, assertThrows } from "./assert.js";
2+
3+
let source_string = "QuickJS! //+!@#$%^&*()";
4+
5+
// created with node.
6+
let target_string = "UXVpY2tKUyEgLy8rIUAjJCVeJiooKQ==";
7+
8+
function test_atob() {
9+
let encoded_string = btoa(source_string);
10+
assert(encoded_string == target_string);
11+
}
12+
13+
function test_btoa() {
14+
let decoded_string = atob(target_string);
15+
assert(decoded_string == source_string);
16+
}
17+
18+
function test_btoatob() {
19+
assert(source_string == atob(btoa(source_string)));
20+
}
21+
22+
function test_atobtoa() {
23+
assert(target_string == btoa(atob(target_string)));
24+
}
25+
26+
function test_fromarray() {
27+
const binaryData = new Uint8Array([72, 101, 108, 108, 111]); // 'Hello'
28+
const text = String.fromCharCode.apply(null, binaryData);
29+
const targetText = "SGVsbG8=";
30+
assert(btoa(text) == targetText);
31+
}
32+
33+
test_atob();
34+
test_btoa();
35+
test_btoatob();
36+
test_atobtoa();
37+
test_fromarray();

0 commit comments

Comments
 (0)