From a1acf0fabdec4970e9f31c38170792ab7fe222e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C3=ABl=20Zasso?= Date: Sun, 8 Aug 2021 11:44:24 +0200 Subject: [PATCH] fixup! buffer: add base64url encoding option Backport parts of https://github.com/nodejs/node/commit/dae283d96fd31ad0f30840a7e55ac97294f505ac --- src/base64-inl.h | 20 ++++++++++-------- src/base64.h | 34 +++++++++++++++++++++++++++--- src/node.h | 13 +++++++++++- src/string_bytes.cc | 20 ++++++++++++++++++ test/cctest/test_base64.cc | 15 +++++++++++++ test/parallel/test-buffer-alloc.js | 2 +- 6 files changed, 90 insertions(+), 14 deletions(-) diff --git a/src/base64-inl.h b/src/base64-inl.h index 9efb9d076ea8e5..1b6cdd93f002a4 100644 --- a/src/base64-inl.h +++ b/src/base64-inl.h @@ -123,12 +123,13 @@ size_t base64_decode(char* const dst, const size_t dstlen, inline size_t base64_encode(const char* src, size_t slen, char* dst, - size_t dlen) { + size_t dlen, + Base64Mode mode) { // We know how much we'll write, just make sure that there's space. - CHECK(dlen >= base64_encoded_size(slen) && + CHECK(dlen >= base64_encoded_size(slen, mode) && "not enough space provided for base64 encode"); - dlen = base64_encoded_size(slen); + dlen = base64_encoded_size(slen, mode); unsigned a; unsigned b; @@ -137,9 +138,7 @@ inline size_t base64_encode(const char* src, unsigned k; unsigned n; - static const char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "abcdefghijklmnopqrstuvwxyz" - "0123456789+/"; + const char* table = base64_select_table(mode); i = 0; k = 0; @@ -164,8 +163,10 @@ inline size_t base64_encode(const char* src, a = src[i + 0] & 0xff; dst[k + 0] = table[a >> 2]; dst[k + 1] = table[(a & 3) << 4]; - dst[k + 2] = '='; - dst[k + 3] = '='; + if (mode == Base64Mode::NORMAL) { + dst[k + 2] = '='; + dst[k + 3] = '='; + } break; case 2: a = src[i + 0] & 0xff; @@ -173,7 +174,8 @@ inline size_t base64_encode(const char* src, dst[k + 0] = table[a >> 2]; dst[k + 1] = table[((a & 3) << 4) | (b >> 4)]; dst[k + 2] = table[(b & 0x0f) << 2]; - dst[k + 3] = '='; + if (mode == Base64Mode::NORMAL) + dst[k + 3] = '='; break; } diff --git a/src/base64.h b/src/base64.h index e58baa5f6e4692..cf6e82539a5f91 100644 --- a/src/base64.h +++ b/src/base64.h @@ -5,13 +5,40 @@ #include "util.h" +#include #include #include namespace node { //// Base 64 //// -static inline constexpr size_t base64_encoded_size(size_t size) { - return ((size + 2) / 3 * 4); + +enum class Base64Mode { + NORMAL, + URL +}; + +static constexpr char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789+/"; + +static constexpr char base64_table_url[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "0123456789-_"; + +static inline const char* base64_select_table(Base64Mode mode) { + switch (mode) { + case Base64Mode::NORMAL: return base64_table; + case Base64Mode::URL: return base64_table_url; + default: UNREACHABLE(); + } +} + +static inline constexpr size_t base64_encoded_size( + size_t size, + Base64Mode mode = Base64Mode::NORMAL) { + return mode == Base64Mode::NORMAL + ? ((size + 2) / 3 * 4) + : std::ceil(static_cast(size * 4) / 3); } // Doesn't check for padding at the end. Can be 1-2 bytes over. @@ -32,7 +59,8 @@ size_t base64_decode(char* const dst, const size_t dstlen, inline size_t base64_encode(const char* src, size_t slen, char* dst, - size_t dlen); + size_t dlen, + Base64Mode mode = Base64Mode::NORMAL); } // namespace node diff --git a/src/node.h b/src/node.h index f1b11aaa3edc90..d19deab7cc7f64 100644 --- a/src/node.h +++ b/src/node.h @@ -664,7 +664,18 @@ inline void NODE_SET_PROTOTYPE_METHOD(v8::Local recv, #define NODE_SET_PROTOTYPE_METHOD node::NODE_SET_PROTOTYPE_METHOD // BINARY is a deprecated alias of LATIN1. -enum encoding {ASCII, UTF8, BASE64, UCS2, BINARY, HEX, BUFFER, LATIN1 = BINARY}; +// BASE64URL is not currently exposed to the JavaScript side. +enum encoding { + ASCII, + UTF8, + BASE64, + UCS2, + BINARY, + HEX, + BUFFER, + BASE64URL, + LATIN1 = BINARY +}; NODE_EXTERN enum encoding ParseEncoding( v8::Isolate* isolate, diff --git a/src/string_bytes.cc b/src/string_bytes.cc index c1b3229e6077c9..daff1424d22e5a 100644 --- a/src/string_bytes.cc +++ b/src/string_bytes.cc @@ -358,6 +358,8 @@ size_t StringBytes::Write(Isolate* isolate, break; } + case BASE64URL: + // Fall through case BASE64: if (str->IsExternalOneByte()) { auto ext = str->GetExternalOneByteStringResource(); @@ -425,6 +427,8 @@ Maybe StringBytes::StorageSize(Isolate* isolate, data_size = str->Length() * sizeof(uint16_t); break; + case BASE64URL: + // Fall through case BASE64: data_size = base64_decoded_size_fast(str->Length()); break; @@ -466,6 +470,8 @@ Maybe StringBytes::Size(Isolate* isolate, case UCS2: return Just(str->Length() * sizeof(uint16_t)); + case BASE64URL: + // Fall through case BASE64: { String::Value value(isolate, str); return Just(base64_decoded_size(*value, value.length())); @@ -691,6 +697,20 @@ MaybeLocal StringBytes::Encode(Isolate* isolate, return ExternOneByteString::New(isolate, dst, dlen, error); } + case BASE64URL: { + size_t dlen = base64_encoded_size(buflen, Base64Mode::URL); + char* dst = node::UncheckedMalloc(dlen); + if (dst == nullptr) { + *error = node::ERR_MEMORY_ALLOCATION_FAILED(isolate); + return MaybeLocal(); + } + + size_t written = base64_encode(buf, buflen, dst, dlen, Base64Mode::URL); + CHECK_EQ(written, dlen); + + return ExternOneByteString::New(isolate, dst, dlen, error); + } + case HEX: { size_t dlen = buflen * 2; char* dst = node::UncheckedMalloc(dlen); diff --git a/test/cctest/test_base64.cc b/test/cctest/test_base64.cc index e92498da61b871..167e5e27bb05fe 100644 --- a/test/cctest/test_base64.cc +++ b/test/cctest/test_base64.cc @@ -44,6 +44,20 @@ TEST(Base64Test, Encode) { "IGRlc2VydW50IG1vbGxpdCBhbmltIGlkIGVzdCBsYWJvcnVtLg=="); } +TEST(Base64Test, EncodeURL) { + auto test = [](const char* string, const char* base64_string) { + const size_t len = strlen(base64_string); + char* const buffer = new char[len + 1]; + buffer[len] = 0; + base64_encode(string, strlen(string), buffer, len, node::Base64Mode::URL); + EXPECT_STREQ(base64_string, buffer); + delete[] buffer; + }; + + test("\x68\xd9\x16\x25\x5c\x1e\x40\x92\x2d\xfb", "aNkWJVweQJIt-w"); + test("\xac\xc7\x93\xaa\x83\x6f\xc3\xe3\x3f\x75", "rMeTqoNvw-M_dQ"); +} + TEST(Base64Test, Decode) { auto test = [](const char* base64_string, const char* string) { const size_t len = strlen(string); @@ -75,6 +89,7 @@ TEST(Base64Test, Decode) { test("YWJj ZGVm", "abcdef"); test("Y W J j Z G V m", "abcdef"); test("Y W\n JjZ \nG Vm", "abcdef"); + test("rMeTqoNvw-M_dQ", "\xac\xc7\x93\xaa\x83\x6f\xc3\xe3\x3f\x75"); const char* text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " diff --git a/test/parallel/test-buffer-alloc.js b/test/parallel/test-buffer-alloc.js index 93a2d5e33e523d..d07d15125bc546 100644 --- a/test/parallel/test-buffer-alloc.js +++ b/test/parallel/test-buffer-alloc.js @@ -345,7 +345,7 @@ const base64flavors = ['base64', 'base64url']; assert.strictEqual(Buffer.from(quote).toString('base64'), expected); assert.strictEqual( Buffer.from(quote).toString('base64url'), - expected.replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '') + expected.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') ); base64flavors.forEach((encoding) => {