Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
fixup! buffer: add base64url encoding option
Backport parts of dae283d
  • Loading branch information
targos committed Aug 13, 2021
commit 84500373e53cc176fa61cdb559c401f45d93110c
20 changes: 11 additions & 9 deletions src/base64-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -164,16 +163,19 @@ 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;
b = src[i + 1] & 0xff;
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;
}

Expand Down
34 changes: 31 additions & 3 deletions src/base64.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,40 @@

#include "util.h"

#include <cmath>
#include <cstddef>
#include <cstdint>

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<double>(size * 4) / 3);
}

// Doesn't check for padding at the end. Can be 1-2 bytes over.
Expand All @@ -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


Expand Down
13 changes: 12 additions & 1 deletion src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -664,7 +664,18 @@ inline void NODE_SET_PROTOTYPE_METHOD(v8::Local<v8::FunctionTemplate> 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
};
Comment on lines -667 to +678
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@addaleax IIUC, this is OK ABI-wise because the new element doesn't change the other values of the enum? But ParseEncoding can now return this new value and programs that don't expect it would break? Can we do something about it (other than not backporting the feature)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@targos Yes, exactly, it’s ABI-compatible but not API-compatible, strictly speaking, and I don’t think we could really do something about it other than modifying ParseEncoding() to not return that value when called from userland code.

The problem with that is that, since we fall back to a default encoding in that case, ParseEncoding(isolate, "base64url") == LATIN1, which would seem like a bug.

I’d probably just leave this as-is, it’s unlikely to cause problems for a lot of addons (and even then only when somebody actually uses base64url with it).


NODE_EXTERN enum encoding ParseEncoding(
v8::Isolate* isolate,
Expand Down
20 changes: 20 additions & 0 deletions src/string_bytes.cc
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ size_t StringBytes::Write(Isolate* isolate,
break;
}

case BASE64URL:
// Fall through
case BASE64:
if (str->IsExternalOneByte()) {
auto ext = str->GetExternalOneByteStringResource();
Expand Down Expand Up @@ -425,6 +427,8 @@ Maybe<size_t> 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;
Expand Down Expand Up @@ -466,6 +470,8 @@ Maybe<size_t> 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()));
Expand Down Expand Up @@ -691,6 +697,20 @@ MaybeLocal<Value> 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<Value>();
}

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);
Expand Down
15 changes: 15 additions & 0 deletions test/cctest/test_base64.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 "
Expand Down
2 changes: 1 addition & 1 deletion test/parallel/test-buffer-alloc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down