diff --git a/CMakeLists.txt b/CMakeLists.txt index b7f46eb..40e8b3c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/src/base64.cc b/src/base64.cc new file mode 100644 index 0000000..12f24df --- /dev/null +++ b/src/base64.cc @@ -0,0 +1,97 @@ +/** + * \file src/base64.cc + * \brief Base64 encoding/decoding (RFC 4648). + * + * Inspired by Jouni Malinen’s BSD implementation at + * . + * + * This implementation is used to decode the cover arts embedded in the tags. According to + * , line feeds are not allowed and padding is required. + */ + +#include + +#include + +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(src.data()); + const unsigned char* end = in + len; + unsigned char* pos = reinterpret_cast(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(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; +} diff --git a/src/opustags.h b/src/opustags.h index d9b9cc1..082c0ab 100644 --- a/src/opustags.h +++ b/src/opustags.h @@ -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); + /** \} */ /***********************************************************************************************//** diff --git a/t/CMakeLists.txt b/t/CMakeLists.txt index 42d6703..55dce09 100644 --- a/t/CMakeLists.txt +++ b/t/CMakeLists.txt @@ -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 ) diff --git a/t/base64.cc b/t/base64.cc new file mode 100644 index 0000000..7b0c686 --- /dev/null +++ b/t/base64.cc @@ -0,0 +1,46 @@ +#include +#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; +}