From b9cd9198d797a16d795068011e98b3af8d916a02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20H=C3=BCttel?= Date: Mon, 2 Nov 2020 23:42:55 +0100 Subject: [PATCH] Finish first draft of this library * Implement decode function with tests * Write doxygen headers * Fix ifdef guards in include file * Fix test output formatting --- include/base64-lib/base64-lib.h | 49 ++++++++++++ src/base64-lib.c | 128 ++++++++++++++++++++++++++++++++ test/CMakeLists.txt | 2 +- test/src/tests.cpp | 53 +++++++++++++ 4 files changed, 231 insertions(+), 1 deletion(-) diff --git a/include/base64-lib/base64-lib.h b/include/base64-lib/base64-lib.h index b94ef11..7d2cbf3 100644 --- a/include/base64-lib/base64-lib.h +++ b/include/base64-lib/base64-lib.h @@ -1,7 +1,56 @@ +#ifndef _BASE64_LIB_H_ +#define _BASE64_LIB_H_ + #include +/** + * @defgroup base64-lib Base64 Encoding / Decoding Library + * @addtogroup base64-lib + * @{ + */ + +/** + * @brief Encode a byte sequence to base 64 + * @param src The source sequence + * @param dest The destination to write the base64 encoded data to. + * @param src_size Source size + * @param dest_size Available destination space + * @param output_length Actual count of written data + * @note The output is not null-terminated + * @return 0 if successful, -1000 if parameter error, -1 if destination too small, + */ int base64_encode(const char *src, char *dest, size_t src_size, size_t dest_size, size_t *output_length); +/** + * @brief Calcualte encoded size of \p unencoded_size amount of unencoded bytes + * @param unencoded_size Unencoded size + * @return Encoded size + */ size_t base64_calculate_encoded_size(size_t unencoded_size); +/** + * @brief Calculate the maximum decoded size. + * + * The true size may be up to 2 bytes smaller, due to possible padding. + * + * @param encoded_size Encoded size + * @return Decoded size + */ size_t base64_calculate_maximum_decoded_size(size_t encoded_size); + +/** + * @brief Decode a base64 encoded string + * @param base64_src Source data + * @param dest Destination buffer + * @param src_size Source size + * @param dest_size Destination size + * @param output_written Actual number of written bytes + * @return 0 if successful, -1 if input length does not match a base64 string, + * -2 if destination size is too small, -3 and -4 if a malformed character + * is found. + */ +int base64_decode(const char *base64_src, char *dest, size_t src_size, size_t dest_size, size_t *output_written); + +/** @} */ + +#endif /* _BASE64_LIB_H_ */ diff --git a/src/base64-lib.c b/src/base64-lib.c index 1cf7771..9bb68e4 100644 --- a/src/base64-lib.c +++ b/src/base64-lib.c @@ -11,6 +11,7 @@ static bool base64_lib_initialized = false; static char LOOKUP_SECTION_ATTR base64_encode_table[64]; +static char LOOKUP_SECTION_ATTR base64_decode_table[256]; static void initialize_encode_lookup_table(void) { @@ -27,12 +28,23 @@ static void initialize_encode_lookup_table(void) base64_encode_table[63] = '/'; } +static void initialize_decode_lookup_table(void) +{ + uint8_t i; + + for (i = 0; i < 64; i++) { + base64_decode_table[(unsigned int)base64_encode_table[i]] = (char)i; + } + base64_decode_table[(unsigned int)'='] = '\0'; +} + static void base64_init(void) { if (base64_lib_initialized) return; initialize_encode_lookup_table(); + initialize_decode_lookup_table(); base64_lib_initialized = true; } @@ -103,3 +115,119 @@ size_t base64_calculate_maximum_decoded_size(size_t encoded_size) return 0; return encoded_size / 4 * 3; } + +/** + * @brief This function checks, if a char is a valid character for base64 encoded data + * @note This function treats the padding '=' as an invalid character + */ +static bool is_valid_base64_character(char c) +{ + bool ret; + + switch (c) { + case 'A' ... 'Z': /* FALLTHRU */ + case 'a' ... 'z': /* FALLTHRU */ + case '0' ... '9': /* FALLTHRU */ + case '+': /* FALLTHRU */ + case '/': + ret = true; + break; + default: + ret = false; + break; + } + + return ret; +} + +static inline uint32_t base64_chars_to_three_bytes(char a, char b, char c, char d) +{ + return ((((uint32_t)base64_decode_table[(unsigned int)a] & 0x3F) << 18) ) | + ((((uint32_t)base64_decode_table[(unsigned int)b] & 0x3F) << 12)) | + ((((uint32_t)base64_decode_table[(unsigned int)c] & 0x3F) << 6)) | + ((((uint32_t)base64_decode_table[(unsigned int)d] & 0x3F))); +} + +int base64_decode(const char *base64_src, char *dest, size_t src_size, size_t dest_size, size_t *output_written) +{ + size_t output_len; + size_t idx; + char first, second, third, fourth; + uint32_t three_bytes; + bool padded = false; + size_t written = 0; + int ret = 0; + + if (!base64_src || !dest || !src_size || !dest_size) + return -1000; + + output_len = base64_calculate_maximum_decoded_size(src_size); + if (output_len == 0) { + ret = -1; + goto exit; + } + + base64_init(); + + /* Check paddings and correct output length */ + if (base64_src[src_size-1] == '=') { + padded = true; + output_len--; + } + + if (base64_src[src_size-2] == '=') { + output_len--; + padded = true; + } + + if (dest_size < output_len) { + ret = -2; + goto exit; + } + + /* Convert all full quartetts */ + for (idx = 0; idx < src_size - (padded ? 4 : 0);) { + /* Due to the call to base64_calculate_maximum_decoded_size(), it is guaranteed, that src_size is + * a multiple of four characters */ + first = base64_src[idx++]; + second = base64_src[idx++]; + third = base64_src[idx++]; + fourth = base64_src[idx++]; + if (is_valid_base64_character(first) && + is_valid_base64_character(second) && + is_valid_base64_character(third) && + is_valid_base64_character(fourth)) { + three_bytes = base64_chars_to_three_bytes(first, second, third, fourth); + dest[written++] = (three_bytes >> 16) & 0xFF; + dest[written++] = (three_bytes >> 8) & 0xFF; + dest[written++] = (three_bytes) & 0xFF; + } else { + ret = -3; + goto exit; + } + + } + + if (padded) { + first = base64_src[idx++]; + second = base64_src[idx++]; + third = base64_src[idx++]; + fourth = base64_src[idx++]; + if (!is_valid_base64_character(first) || !is_valid_base64_character(second)) { + ret = -4; + goto exit; + } + three_bytes = base64_chars_to_three_bytes(first, second, third, fourth); + dest[written++] = (three_bytes >> 16) & 0xFF; + if (third != '=') + dest[written++] = (three_bytes >> 8) & 0xFF; + if (fourth != '=') + dest[written++] = (three_bytes) & 0xFF; + } + +exit: + if (output_written) + *output_written = written; + + return ret; +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a2b103e..f6a2b48 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,6 +1,6 @@ project(base64-test) -add_custom_target("execute-${PROJECT_NAME}" "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}" "-r compact" "-s" DEPENDS ${PROJECT_NAME}) +add_custom_target("execute-${PROJECT_NAME}" "${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}" DEPENDS ${PROJECT_NAME}) include_directories("${CMAKE_CURRENT_SOURCE_DIR}/catch-framework") aux_source_directory("src" TEST_SOURCES) diff --git a/test/src/tests.cpp b/test/src/tests.cpp index 9489879..92c1032 100644 --- a/test/src/tests.cpp +++ b/test/src/tests.cpp @@ -73,3 +73,56 @@ TEST_CASE("base64/base64_calculate_maximum_decoded_size", "[DECODE]") REQUIRE(base64_calculate_maximum_decoded_size(4) == 3); REQUIRE(base64_calculate_maximum_decoded_size(160) == 120); } + +TEST_CASE("base64/base64_decode", "[DECODE]") +{ + char decoded[2048]; + size_t written; + int ret; + + ret = base64_decode(base64_reference.c_str(), decoded, base64_reference.length(), lorem_ipsum.length(), &written); + REQUIRE(ret == 0); + + REQUIRE(written == lorem_ipsum.length()); + + if (!ret) { + decoded[written] = 0; + REQUIRE(std::string(decoded) == lorem_ipsum); + } +} + +TEST_CASE("base64/base64_decode/error_src_to_short", "[DECODE]") +{ + char decoded[2048]; + size_t written; + int ret; + + ret = base64_decode(base64_reference.c_str(), decoded, base64_reference.length() -1, sizeof(decoded), &written); + REQUIRE(ret == -1); + REQUIRE(written == 0); +} + +TEST_CASE("base64/base64_decode/error_dest_to_short", "[DECODE]") +{ + char decoded[2048]; + size_t written; + int ret; + + ret = base64_decode(base64_reference.c_str(), decoded, base64_reference.length(), 12, &written); + REQUIRE(ret == -2); + REQUIRE(written == 0); +} + +TEST_CASE("base64/base64_decode/invalid_char", "[DECODE]") +{ + char decoded[2048]; + size_t written; + int ret; + + std::string ref = base64_reference; + ref[12] = '!'; + + ret = base64_decode(ref.c_str(), decoded, ref.length(), 2048, &written); + REQUIRE(ret == -3); + REQUIRE(written == 9); +}