326 lines
11 KiB
C++
326 lines
11 KiB
C++
// NOLINTBEGIN
|
|
#ifndef MACARON_BASE64_H_
|
|
#define MACARON_BASE64_H_
|
|
|
|
/**
|
|
* The MIT License (MIT)
|
|
* Copyright (c) 2016 tomykaira
|
|
* Copyright (c) 2025 scott.e.graves@protonmail.com
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
* a copy of this software and associated documentation files (the
|
|
* "Software"), to deal in the Software without restriction, including
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
* the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic push
|
|
#pragma clang diagnostic ignored "-Wunknown-warning-option"
|
|
#endif
|
|
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wconversion"
|
|
#pragma GCC diagnostic ignored "-Wold-style-cast"
|
|
#pragma GCC diagnostic ignored "-Wuseless-cast"
|
|
#endif
|
|
|
|
#include <array>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <vector>
|
|
|
|
namespace macaron::Base64 {
|
|
|
|
// --- Alphabets --------------------------------------------------------------
|
|
|
|
static constexpr std::array<unsigned char, 64U> kStdAlphabet{
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/',
|
|
};
|
|
|
|
static constexpr std::array<unsigned char, 64U> kUrlAlphabet{
|
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '_',
|
|
};
|
|
|
|
// Decoding table that accepts BOTH standard and URL-safe alphabets.
|
|
static constexpr std::array<unsigned char, 256U> kDecodingTable = [] {
|
|
std::array<unsigned char, 256U> t{};
|
|
t.fill(64U);
|
|
// 'A'-'Z'
|
|
for (unsigned char c = 'A'; c <= 'Z'; ++c)
|
|
t[c] = static_cast<unsigned char>(c - 'A');
|
|
// 'a'-'z'
|
|
for (unsigned char c = 'a'; c <= 'z'; ++c)
|
|
t[c] = static_cast<unsigned char>(26 + c - 'a');
|
|
// '0'-'9'
|
|
for (unsigned char c = '0'; c <= '9'; ++c)
|
|
t[c] = static_cast<unsigned char>(52 + c - '0');
|
|
// Standard extras
|
|
t[static_cast<unsigned char>('+')] = 62U;
|
|
t[static_cast<unsigned char>('/')] = 63U;
|
|
// URL-safe extras
|
|
t[static_cast<unsigned char>('-')] = 62U;
|
|
t[static_cast<unsigned char>('_')] = 63U;
|
|
return t;
|
|
}();
|
|
|
|
// --- Encoding ---------------------------------------------------------------
|
|
|
|
/**
|
|
* Encode to Base64.
|
|
* @param data pointer to bytes
|
|
* @param len number of bytes
|
|
* @param url_safe if true, use URL-safe alphabet ("-","_") instead of ("+","/")
|
|
* @param pad if true, add '=' padding; if false, omit padding (RFC 4648
|
|
* §5)
|
|
*/
|
|
static std::string Encode(const unsigned char *data, std::size_t len,
|
|
bool url_safe = false, bool pad = true) {
|
|
const auto &alpha = url_safe ? kUrlAlphabet : kStdAlphabet;
|
|
|
|
std::string out;
|
|
if (len == 0U) {
|
|
return out;
|
|
}
|
|
|
|
const std::size_t full_blocks = len / 3U;
|
|
const std::size_t rem = len % 3U;
|
|
|
|
std::size_t out_len{};
|
|
if (pad) {
|
|
out_len = 4U * ((len + 2U) / 3U);
|
|
} else {
|
|
// Unpadded length per RFC 4648 §5
|
|
out_len = 4U * full_blocks + (rem == 0U ? 0U : (rem == 1U ? 2U : 3U));
|
|
}
|
|
out.assign(out_len, '\0');
|
|
|
|
auto *p = reinterpret_cast<unsigned char *>(out.data());
|
|
std::size_t i = 0;
|
|
|
|
// Full 3-byte blocks -> 4 chars
|
|
for (; i + 2U < len; i += 3U) {
|
|
const unsigned char b0 = data[i + 0U];
|
|
const unsigned char b1 = data[i + 1U];
|
|
const unsigned char b2 = data[i + 2U];
|
|
|
|
*p++ = alpha[(b0 >> 2U) & 0x3F];
|
|
*p++ = alpha[((b0 & 0x03U) << 4U) | ((b1 >> 4U) & 0x0FU)];
|
|
*p++ = alpha[((b1 & 0x0FU) << 2U) | ((b2 >> 6U) & 0x03U)];
|
|
*p++ = alpha[b2 & 0x3FU];
|
|
}
|
|
|
|
// Remainder
|
|
if (rem == 1U) {
|
|
const unsigned char b0 = data[i];
|
|
*p++ = alpha[(b0 >> 2U) & 0x3F];
|
|
*p++ = alpha[(b0 & 0x03U) << 4U];
|
|
if (pad) {
|
|
*p++ = '=';
|
|
*p++ = '=';
|
|
}
|
|
} else if (rem == 2U) {
|
|
const unsigned char b0 = data[i + 0U];
|
|
const unsigned char b1 = data[i + 1U];
|
|
*p++ = alpha[(b0 >> 2U) & 0x3F];
|
|
*p++ = alpha[((b0 & 0x03U) << 4U) | ((b1 >> 4U) & 0x0FU)];
|
|
*p++ = alpha[(b1 & 0x0FU) << 2U];
|
|
if (pad) {
|
|
*p++ = '=';
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
[[maybe_unused]] static std::string
|
|
Encode(std::string_view data, bool url_safe = false, bool pad = true) {
|
|
return Encode(reinterpret_cast<const unsigned char *>(data.data()),
|
|
data.size(), url_safe, pad);
|
|
}
|
|
|
|
[[maybe_unused]] static std::string
|
|
EncodeUrlSafe(const unsigned char *data, std::size_t len, bool pad = false) {
|
|
return Encode(data, len, /*url_safe=*/true, /*pad=*/pad);
|
|
}
|
|
|
|
[[maybe_unused]] static std::string EncodeUrlSafe(std::string_view data,
|
|
bool pad = false) {
|
|
return Encode(reinterpret_cast<const unsigned char *>(data.data()),
|
|
data.size(), /*url_safe=*/true, /*pad=*/pad);
|
|
}
|
|
|
|
// --- Decoding ---------------------------------------------------------------
|
|
|
|
/**
|
|
* Decode standard OR URL-safe Base64.
|
|
* Accepts inputs with or without '=' padding.
|
|
* Throws std::runtime_error on malformed input.
|
|
*/
|
|
[[maybe_unused]] static std::vector<unsigned char>
|
|
Decode(std::string_view input) {
|
|
std::vector<unsigned char> out;
|
|
if (input.empty()) {
|
|
return out;
|
|
}
|
|
|
|
std::size_t inLen = input.size();
|
|
std::size_t rem = inLen % 4U;
|
|
|
|
// padded if multiple of 4 and last char is '='
|
|
bool hasPadding = (rem == 0U) && (inLen >= 4U) && (input[inLen - 1U] == '=');
|
|
|
|
// compute output length
|
|
std::size_t outLen{};
|
|
if (hasPadding) {
|
|
outLen = (inLen / 4U) * 3U;
|
|
if (input[inLen - 1U] == '=')
|
|
outLen--;
|
|
if (input[inLen - 2U] == '=')
|
|
outLen--;
|
|
} else {
|
|
if (rem == 1U) {
|
|
throw std::runtime_error("Invalid Base64 length (mod 4 == 1)");
|
|
}
|
|
outLen = (inLen / 4U) * 3U + (rem == 0U ? 0U : (rem == 2U ? 1U : 2U));
|
|
}
|
|
|
|
out.resize(outLen);
|
|
|
|
auto readVal = [](unsigned char c) -> unsigned char {
|
|
unsigned char v = kDecodingTable[c];
|
|
if (v == 64U) {
|
|
throw std::runtime_error("Invalid Base64 character");
|
|
}
|
|
return v;
|
|
};
|
|
|
|
std::size_t i = 0U;
|
|
std::size_t j = 0U;
|
|
|
|
// process all full unpadded quartets
|
|
std::size_t lastFull =
|
|
hasPadding ? (inLen - 4U) : (rem == 0U ? inLen : (inLen - rem));
|
|
|
|
while (i + 4U <= lastFull) {
|
|
unsigned char a = readVal(static_cast<unsigned char>(input[i + 0U]));
|
|
unsigned char b = readVal(static_cast<unsigned char>(input[i + 1U]));
|
|
unsigned char c = readVal(static_cast<unsigned char>(input[i + 2U]));
|
|
unsigned char d = readVal(static_cast<unsigned char>(input[i + 3U]));
|
|
i += 4U;
|
|
|
|
std::uint32_t triple = (static_cast<std::uint32_t>(a) << 18U) |
|
|
(static_cast<std::uint32_t>(b) << 12U) |
|
|
(static_cast<std::uint32_t>(c) << 6U) |
|
|
(static_cast<std::uint32_t>(d));
|
|
|
|
if (j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 16U) & 0xFFU);
|
|
if (j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 8U) & 0xFFU);
|
|
if (j < outLen)
|
|
out[j++] = static_cast<unsigned char>(triple & 0xFFU);
|
|
}
|
|
|
|
// tail: padded quartet or unpadded remainder
|
|
if (i < inLen) {
|
|
std::size_t left = inLen - i;
|
|
|
|
if (left == 4U) {
|
|
bool thirdIsPad = (input[i + 2U] == '=');
|
|
bool fourthIsPad = (input[i + 3U] == '=');
|
|
|
|
// '=' is never allowed in positions 1 or 2 of any quartet
|
|
if (input[i + 0U] == '=' || input[i + 1U] == '=') {
|
|
throw std::runtime_error("Invalid Base64 padding placement");
|
|
}
|
|
|
|
unsigned char a = readVal(static_cast<unsigned char>(input[i + 0U]));
|
|
unsigned char b = readVal(static_cast<unsigned char>(input[i + 1U]));
|
|
unsigned char c = 0U;
|
|
unsigned char d = 0U;
|
|
|
|
if (!thirdIsPad) {
|
|
c = readVal(static_cast<unsigned char>(input[i + 2U]));
|
|
if (!fourthIsPad) {
|
|
d = readVal(static_cast<unsigned char>(input[i + 3U]));
|
|
}
|
|
} else {
|
|
// if the 3rd is '=', the 4th must also be '='
|
|
if (!fourthIsPad) {
|
|
throw std::runtime_error("Invalid Base64 padding placement");
|
|
}
|
|
}
|
|
i += 4U;
|
|
|
|
std::uint32_t triple = (static_cast<std::uint32_t>(a) << 18U) |
|
|
(static_cast<std::uint32_t>(b) << 12U) |
|
|
(static_cast<std::uint32_t>(c) << 6U) |
|
|
(static_cast<std::uint32_t>(d));
|
|
|
|
if (j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 16U) & 0xFFU);
|
|
if (!thirdIsPad && j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 8U) & 0xFFU);
|
|
if (!fourthIsPad && !thirdIsPad && j < outLen)
|
|
out[j++] = static_cast<unsigned char>(triple & 0xFFU);
|
|
|
|
} else if (left == 2U || left == 3U) {
|
|
unsigned char a = readVal(static_cast<unsigned char>(input[i + 0U]));
|
|
unsigned char b = readVal(static_cast<unsigned char>(input[i + 1U]));
|
|
unsigned char c = (left == 3U)
|
|
? readVal(static_cast<unsigned char>(input[i + 2U]))
|
|
: 0U;
|
|
i += left;
|
|
|
|
std::uint32_t triple = (static_cast<std::uint32_t>(a) << 18U) |
|
|
(static_cast<std::uint32_t>(b) << 12U) |
|
|
(static_cast<std::uint32_t>(c) << 6U);
|
|
|
|
if (j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 16U) & 0xFFU);
|
|
if (left == 3U && j < outLen)
|
|
out[j++] = static_cast<unsigned char>((triple >> 8U) & 0xFFU);
|
|
} else {
|
|
throw std::runtime_error("Invalid Base64 length (mod 4 == 1)");
|
|
}
|
|
}
|
|
|
|
return out;
|
|
}
|
|
} // namespace macaron::Base64
|
|
|
|
#ifdef __GNUC__
|
|
#pragma GCC diagnostic pop
|
|
#endif
|
|
|
|
#ifdef __clang__
|
|
#pragma clang diagnostic pop
|
|
#endif
|
|
|
|
#endif /* MACARON_BASE64_H_ */
|
|
// NOLINTEND
|