Implement base64 encoding and decoding

This commit is contained in:
Frédéric Mangano 2023-02-22 11:39:25 +09:00
parent 55e7e9b64e
commit a435a28e9f
5 changed files with 151 additions and 1 deletions

View File

@ -36,6 +36,7 @@ include_directories(BEFORE src "${CMAKE_BINARY_DIR}" ${OGG_INCLUDE_DIRS} ${Iconv
add_library(
ot
STATIC
src/base64.cc
src/cli.cc
src/ogg.cc
src/opus.cc

97
src/base64.cc Normal file
View File

@ -0,0 +1,97 @@
/**
* \file src/base64.cc
* \brief Base64 encoding/decoding (RFC 4648).
*
* Inspired by Jouni Malinens BSD implementation at
* <http://web.mit.edu/freebsd/head/contrib/wpa/src/utils/base64.c>.
*
* This implementation is used to decode the cover arts embedded in the tags. According to
* <https://wiki.xiph.org/VorbisComment>, line feeds are not allowed and padding is required.
*/
#include <opustags.h>
#include <cstring>
static const unsigned char base64_table[65] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string ot::encode_base64(std::string_view src)
{
size_t len = src.size();
size_t num_blocks = (len + 2) / 3; // Count of 3-byte blocks, rounded up.
size_t olen = num_blocks * 4; // Each 3-byte block becomes 4 base64 bytes.
if (olen < len)
throw std::overflow_error("failed to encode excessively long base64 block");
std::string out;
out.resize(olen);
const unsigned char* in = reinterpret_cast<const unsigned char*>(src.data());
const unsigned char* end = in + len;
unsigned char* pos = reinterpret_cast<unsigned char*>(out.data());
while (end - in >= 3) {
*pos++ = base64_table[in[0] >> 2];
*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)];
*pos++ = base64_table[in[2] & 0x3f];
in += 3;
}
if (end - in) {
*pos++ = base64_table[in[0] >> 2];
if (end - in == 1) {
*pos++ = base64_table[(in[0] & 0x03) << 4];
*pos++ = '=';
} else { // end - in == 2
*pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)];
*pos++ = base64_table[(in[1] & 0x0f) << 2];
}
*pos++ = '=';
}
return out;
}
std::string ot::decode_base64(std::string_view src)
{
// Remove the padding and rely on the string length instead.
while (src.back() == '=')
src.remove_suffix(1);
size_t olen = src.size() / 4 * 3; // Whole blocks;
switch (src.size() % 4) {
case 1: throw status {st::error, "invalid base64 block size"};
case 2: olen += 1; break;
case 3: olen += 2; break;
}
std::string out;
out.resize(olen);
unsigned char* pos = reinterpret_cast<unsigned char*>(out.data());
unsigned char dtable[256];
memset(dtable, 0x80, 256);
for (size_t i = 0; i < sizeof(base64_table) - 1; ++i)
dtable[base64_table[i]] = (unsigned char) i;
unsigned char block[4];
size_t count = 0;
for (unsigned char c : src) {
unsigned char tmp = dtable[c];
if (tmp == 0x80)
throw status {st::error, "invalid base64 character"};
block[count++] = tmp;
if (count == 2) {
*pos++ = (block[0] << 2) | (block[1] >> 4);
} else if (count == 3) {
*pos++ = (block[1] << 4) | (block[2] >> 2);
} else if (count == 4) {
*pos++ = (block[2] << 6) | block[3];
count = 0;
}
}
return out;
}

View File

@ -187,6 +187,9 @@ void run_editor(std::string_view editor, std::string_view path);
*/
timespec get_file_timestamp(const char* path);
std::string encode_base64(std::string_view src);
std::string decode_base64(std::string_view src);
/** \} */
/***********************************************************************************************//**

View File

@ -10,6 +10,9 @@ target_link_libraries(ogg.t ot)
add_executable(cli.t EXCLUDE_FROM_ALL cli.cc)
target_link_libraries(cli.t ot)
add_executable(base64.t EXCLUDE_FROM_ALL base64.cc)
target_link_libraries(base64.t ot)
add_executable(oggdump EXCLUDE_FROM_ALL oggdump.cc)
target_link_libraries(oggdump ot)
@ -18,5 +21,5 @@ configure_file(gobble.opus . COPYONLY)
add_custom_target(
check
COMMAND prove "${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS opustags gobble.opus system.t opus.t ogg.t cli.t
DEPENDS opustags gobble.opus system.t opus.t ogg.t cli.t base64.t
)

46
t/base64.cc Normal file
View File

@ -0,0 +1,46 @@
#include <opustags.h>
#include "tap.h"
static void check_encode_base64()
{
is(ot::encode_base64(""), "", "empty");
is(ot::encode_base64("a"), "YQ==", "1 character");
is(ot::encode_base64("aa"), "YWE=", "2 characters");
is(ot::encode_base64("aaa"), "YWFh", "3 characters");
is(ot::encode_base64("aaaa"), "YWFhYQ==", "4 characters");
is(ot::encode_base64("\xFF\xFF\xFE"), "///+", "RFC alphabet");
is(ot::encode_base64("\0x"sv), "AHg=", "embedded null bytes");
}
static void check_decode_base64()
{
is(ot::decode_base64(""), "", "empty");
is(ot::decode_base64("YQ=="), "a", "1 character");
is(ot::decode_base64("YWE="), "aa", "2 characters");
is(ot::decode_base64("YQ"), "a", "padless 1 character");
is(ot::decode_base64("YWE"), "aa", "padless 2 characters");
is(ot::decode_base64("YWFh"), "aaa", "3 characters");
is(ot::decode_base64("YWFhYQ=="), "aaaa", "4 characters");
is(ot::decode_base64("///+"), "\xFF\xFF\xFE", "RFC alphabet");
is(ot::decode_base64("AHg="), "\0x"sv, "embedded null bytes");
try {
ot::decode_base64("Y===");
throw failure("accepted a bad block size");
} catch (const ot::status& e) {
}
try {
ot::decode_base64("\xFF bad message!");
throw failure("accepted an invalid character");
} catch (const ot::status& e) {
}
}
int main(int argc, char **argv)
{
std::cout << "1..2\n";
run(check_encode_base64, "base64 encoding");
run(check_decode_base64, "base64 decoding");
return 0;
}