From f0afdfc7d9f2f29fe351695f69ad95e8544d5d54 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 12 Jul 2025 10:31:54 -0500 Subject: [PATCH] add base64 and base64url encoder/decoders --- scripts/text.cm | 4 ++ source/qjs_text.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 159 insertions(+) diff --git a/scripts/text.cm b/scripts/text.cm index d081b623..5e93d7f4 100644 --- a/scripts/text.cm +++ b/scripts/text.cm @@ -411,5 +411,9 @@ function format_number(num, format) { } text.base32_to_blob = that.base32_to_blob +text.base64_to_blob = that.base64_to_blob +text.base64url_to_blob = that.base64url_to_blob +text.blob_to_base64 = that.blob_to_base64 +text.blob_to_base64url = that.blob_to_base64url return text; \ No newline at end of file diff --git a/source/qjs_text.c b/source/qjs_text.c index 57af724d..93395295 100644 --- a/source/qjs_text.c +++ b/source/qjs_text.c @@ -139,10 +139,165 @@ JSC_CCALL(text_base32_to_blob, return val; ) +static int base64_char_to_val_standard(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + return -1; +} +static int base64_char_to_val_url(char c) { + if (c >= 'A' && c <= 'Z') return c - 'A'; + if (c >= 'a' && c <= 'z') return c - 'a' + 26; + if (c >= '0' && c <= '9') return c - '0' + 52; + if (c == '-') return 62; + if (c == '_') return 63; + return -1; +} + +/*─── blob → Base64 (standard, with ‘+’ and ‘/’, padded) ───────────────────*/ +JSC_CCALL(text_blob_to_base64, + size_t blob_len; + void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); + if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); + const uint8_t *bytes = blob_data; + static const char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + size_t out_len = ((blob_len + 2) / 3) * 4; + char *out = malloc(out_len + 1); + if (!out) return JS_ThrowOutOfMemory(js); + + size_t in_i = 0, out_i = 0; + while (in_i < blob_len) { + uint32_t buf = 0; + int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3); + for (int j = 0; j < to_read; ++j) { + buf = (buf << 8) | bytes[in_i++]; + } + buf <<= 8 * (3 - to_read); + out[out_i++] = b64[(buf >> 18) & 0x3F]; + out[out_i++] = b64[(buf >> 12) & 0x3F]; + out[out_i++] = (to_read > 1 ? b64[(buf >> 6) & 0x3F] : '='); + out[out_i++] = (to_read > 2 ? b64[ buf & 0x3F] : '='); + } + out[out_len] = '\0'; + JSValue v = JS_NewString(js, out); + free(out); + return v; +) + +/*─── Base64 → blob (standard, expects ‘+’ and ‘/’, pads allowed) ────────────*/ +JSC_CCALL(text_base64_to_blob, + const char *str = JS_ToCString(js, argv[0]); + if (!str) return JS_ThrowTypeError(js, "Expected string"); + size_t len = strlen(str); + // strip padding for length calculation + size_t eff = len; + while (eff > 0 && str[eff-1] == '=') eff--; + size_t out_len = (eff * 6) / 8; + uint8_t *out = malloc(out_len); + if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); } + + size_t in_i = 0, out_i = 0; + while (in_i < eff) { + uint32_t buf = 0; + int to_read = (eff - in_i < 4 ? eff - in_i : 4); + for (int j = 0; j < to_read; ++j) { + int v = base64_char_to_val_standard(str[in_i++]); + if (v < 0) { free(out); JS_FreeCString(js, str); + return JS_ThrowTypeError(js, "Invalid base64 character"); } + buf = (buf << 6) | v; + } + buf <<= 6 * (4 - to_read); + int bytes_out = (to_read * 6) / 8; + for (int j = 0; j < bytes_out && out_i < out_len; ++j) { + out[out_i++] = (buf >> (16 - 8*j)) & 0xFF; + } + } + + JSValue v = js_new_blob_stoned_copy(js, out, out_len); + free(out); + JS_FreeCString(js, str); + return v; +) + +/*─── blob → Base64URL (no padding, ‘-’ and ‘_’) ─────────────────────────────*/ +JSC_CCALL(text_blob_to_base64url, + size_t blob_len; + void *blob_data = js_get_blob_data(js, &blob_len, argv[0]); + if (!blob_data) return JS_ThrowTypeError(js, "Expected stone blob"); + const uint8_t *bytes = blob_data; + static const char b64url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-_"; + size_t raw_len = ((blob_len + 2) / 3) * 4; + // we’ll drop any trailing '=' + char *out = malloc(raw_len + 1); + if (!out) return JS_ThrowOutOfMemory(js); + + size_t in_i = 0, out_i = 0; + while (in_i < blob_len) { + uint32_t buf = 0; + int to_read = (blob_len - in_i < 3 ? blob_len - in_i : 3); + for (int j = 0; j < to_read; ++j) { + buf = (buf << 8) | bytes[in_i++]; + } + buf <<= 8 * (3 - to_read); + out[out_i++] = b64url[(buf >> 18) & 0x3F]; + out[out_i++] = b64url[(buf >> 12) & 0x3F]; + if (to_read > 1) out[out_i++] = b64url[(buf >> 6) & 0x3F]; + if (to_read > 2) out[out_i++] = b64url[ buf & 0x3F]; + } + out[out_i] = '\0'; + JSValue v = JS_NewString(js, out); + free(out); + return v; +) + +/*─── Base64URL → blob (accepts ‘-’ / ‘_’, no padding needed) ─────────────────*/ +JSC_CCALL(text_base64url_to_blob, + const char *str = JS_ToCString(js, argv[0]); + if (!str) return JS_ThrowTypeError(js, "Expected string"); + size_t len = strlen(str); + size_t eff = len; // no '=' in URL‐safe, but strip if present + while (eff > 0 && str[eff-1] == '=') eff--; + size_t out_len = (eff * 6) / 8; + uint8_t *out = malloc(out_len); + if (!out) { JS_FreeCString(js, str); return JS_ThrowOutOfMemory(js); } + + size_t in_i = 0, out_i = 0; + while (in_i < eff) { + uint32_t buf = 0; + int to_read = (eff - in_i < 4 ? eff - in_i : 4); + for (int j = 0; j < to_read; ++j) { + int v = base64_char_to_val_url(str[in_i++]); + if (v < 0) { free(out); JS_FreeCString(js, str); + return JS_ThrowTypeError(js, "Invalid base64url character"); } + buf = (buf << 6) | v; + } + buf <<= 6 * (4 - to_read); + int bytes_out = (to_read * 6) / 8; + for (int j = 0; j < bytes_out && out_i < out_len; ++j) { + out[out_i++] = (buf >> (16 - 8*j)) & 0xFF; + } + } + + JSValue v = js_new_blob_stoned_copy(js, out, out_len); + free(out); + JS_FreeCString(js, str); + return v; +) + static const JSCFunctionListEntry js_text_funcs[] = { MIST_FUNC_DEF(text, blob_to_hex, 1), MIST_FUNC_DEF(text, blob_to_base32, 1), MIST_FUNC_DEF(text, base32_to_blob, 1), + MIST_FUNC_DEF(text, blob_to_base64, 1), + MIST_FUNC_DEF(text, base64_to_blob, 1), + MIST_FUNC_DEF(text, blob_to_base64url, 1), + MIST_FUNC_DEF(text, base64url_to_blob, 1), }; JSValue js_text_use(JSContext *js)