2
\$\begingroup\$

My Base64 encoder class converts a string with ascii characters to a base64 encoded string with padding if required. Note that I'm not using istream as the input because my goal was only to convert a std::string with ascii characters to another std::string with base64 encoded characters.

Only resource I used was this wikipedia article.

base64encoder.h:

#ifndef BASE64_BASE64_H
#define BASE64_BASE64_H

#include <array>

class Base64Encoder {
public:
    Base64Encoder() = default;

    ~Base64Encoder() = default;

    const std::string encode(const std::string s) const;

private:
    constexpr static std::array<const unsigned char, 64> base64_table = {'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', '+', '/'};

    constexpr static unsigned char get_base_64_char(ulong number_of_char) {
        return base64_table.at(number_of_char);
    }

    const static unsigned char next_ascii(size_t current_index, const std::string s, size_t length_of_s);

    const static size_t MINIMAL_B64_STRING_LENGTH = 4;
};


#endif //BASE64_BASE64_H

base64encoder.cpp:

#include <iostream>
#include <sstream>
#include "base64encoder.h"

// Takes a string and encodes it into a base64 string.
const std::string Base64Encoder::encode(const std::string s) const {
    std::stringstream encoding;
    const size_t length_of_s = s.length();
    size_t i = 0;
    // Base64 uses 6 bits for encoding. ASCII characters have a size of 8 bits.
    // We sometimes have to use bits of the next character a.k.a right character.
    size_t bit_offset = 0; // offset of used bits in current ascii character

    while (i < length_of_s) {
        // left_ascii is the current ASCII character which needs to be encoded.
        auto left_ascii = static_cast<unsigned char>(s.at(i));
        // b64 will contain the encoded b64 character.
        unsigned char b64_encoded = '\0';

        if (bit_offset == 0) {
            left_ascii = left_ascii >> 2;
            b64_encoded = get_base_64_char(static_cast<ulong>(left_ascii));
            bit_offset = 6;
            // no need to go to the next index, we didn't use any other information except the current char.
        } else if (bit_offset == 6) {
            left_ascii = left_ascii << 6;
            left_ascii = left_ascii >> 2;
            auto right_ascii = Base64Encoder::next_ascii(i, s, length_of_s);
            right_ascii = right_ascii >> 4;
            left_ascii = right_ascii | left_ascii;
            b64_encoded = get_base_64_char(static_cast<ulong>(left_ascii));
            bit_offset = 4;
            ++i;
        } else if (bit_offset == 4) {
            left_ascii = left_ascii << 4;
            left_ascii = left_ascii >> 2;
            auto right_ascii = Base64Encoder::next_ascii(i, s, length_of_s);
            right_ascii = right_ascii >> 6;
            left_ascii = left_ascii | right_ascii;
            b64_encoded = get_base_64_char(static_cast<ulong>(left_ascii));
            bit_offset = 2;
            ++i;
        } else { // offset is 2
            left_ascii = left_ascii << 2;
            left_ascii = left_ascii >> 2;
            b64_encoded = get_base_64_char(static_cast<ulong>(left_ascii));
            bit_offset = 0;
            ++i;
        }
        encoding << b64_encoded;
    }

    // Checking if padding in form of appending '=' is required.
    if (bit_offset || length_of_s < 2) {
        encoding << '=';
        if (length_of_s == 1) {
            encoding << '=';
        }
    }

    return encoding.str();
}

// returns next ascii character if index is in string or a zero byte which happens if we are at the end of string
// and there is no next ascii character filling up our 6 bits which we need for a base64 encoded char - in this case
// we also need padding.
const unsigned char Base64Encoder::next_ascii(size_t current_index, const std::string s, size_t length_of_s) {
    size_t next_index = ++current_index;
    if (next_index < length_of_s) {
        return static_cast<unsigned char>(s.at(next_index));
    }
    unsigned char zero_byte = 0;
    return zero_byte;
}

Example usage main.cpp:

#include <iostream>
#include "base64encoder.h"

int main() {
    Base64Encoder b;
    const std::string s("Hello Base64!");
    std::cout << b.encode(s);
}
\$\endgroup\$
  • 1
    \$\begingroup\$ I would change the comment. Your description of Base64 is not totally correct. => "Base64 encodes 4 bytes of ASCII A-Za-z0-9+/ characters into 3 bytes. It does this by using 6 bits to encode each character and thus has a limited character set. Messages are always a multiple of 3 in length and the '=' character is used to indicate terminator padding. \$\endgroup\$ – Martin York Feb 12 at 18:47
3
\$\begingroup\$
    Base64Encoder() = default;
    ~Base64Encoder() = default;

In general, if you want to explicitly provide special member functions, then explicitly provide them all. Others will advise that if you can avoid defining the special member functions, then do so. Read more here.

class Base64Encoder {
public:
    // ... no default operations declared ...
    const std::string encode(const std::string s) const;

private:
    // ...

    const std::string encode(const std::string s) const;

std::string requires <string> be included.

s is passed by value to const, which incurs an unnecessary copy. Consider using std::string_view if you have . Read more here.

    std::string encode(const std::string_view s) const;

Otherwise, pass by reference to const.

    std::string encode(const std::string& s) const;

    constexpr static unsigned char get_base_64_char(ulong number_of_char) {
        return base64_table.at(number_of_char);
    }

ulong is not a standard unsigned integer type. If you need a fixed-size integer, consider one of the types from <cstdint> (e.g. std::uint8_t). For this use-case, I'd just use std::size_t.

Is number_of_char a clear description of what the value represents? Would index be clearer?

Do you need the bounds checking of base64_table.at()?


    const static unsigned char next_ascii(size_t current_index, const std::string s, size_t length_of_s);

The first const is unnecessary.

size_t is not guaranteed by the standard to exist in the global namespace. Use std::size_t and include <cstddef>. Read more here.


    const static size_t MINIMAL_B64_STRING_LENGTH = 4;

Consider reserving upper case names for the preprocessor.

constexpr?


    std::stringstream encoding;

Do we need a std::stringstream? We can actually calculate the destination buffer length. For base64 encoding, every 3 octets maps to 4 sextets. To find the encoded length \$m\$, find the total number of octets to be read (integral ceiling) and multiply it by the length of each sextet.

$$m = 4 \dot ((n + 2) / 3)$$


    while (i < length_of_s) {
        // if first sextet, ...
        // else if second sextet, ...
        // else if third sextet, ...
        // else must be fourth sextet ...
    }

Instead of cycling through each branch on every loop until you get to the sextet you are at, consider a modulo approach. Loop through full sextet groups until you have a partial sextet group left at the end (the remainder). Then you can branch based on what you have left.

    for (auto remaining_sextets = s.size() / 3; remaining_sextets--;) {
        encoded += /* first sextet masked and shifted */
        encoded += /* second sextet masked and shifted */
        encoded += /* third sextet masked and shifted */
        encoded += /* fourth sextet masked and shifted */
    }

    switch (len % 3) {
    case 2:
        encoded += /* first sextet masked and shifted */
        encoded += /* second sextet masked and shifted */
        encoded += /* third sextet masked and shifted */
        encoded += '=';
        break;
    case 1:
        encoded += /* first sextet masked and shifted */
        encoded += /* second sextet masked and shifted */
        encoded += '=';
        encoded += '=';
        break;
    case 0:
        break;
    }

    return encoded;
\$\endgroup\$
  • \$\begingroup\$ Note: There are 3 encoded characters per 4 sextet. Thus code should only have 3 instances of encoded += /* STUFF */; in each section. \$\endgroup\$ – Martin York Feb 12 at 18:53
  • \$\begingroup\$ You may want to mention your loop unrolling and switch as an extension of Duff's device. \$\endgroup\$ – Martin York Feb 12 at 19:01
  • \$\begingroup\$ I didn't mention loop unrolling or duff's device because it is neither. This is loop unswitching. Each encoded operation is different, not duplicated, in the loop. The operations in the loop and in the switch only map directly if the switch case is 0 (in which switch case 0 does nothing). \$\endgroup\$ – Snowhawk Feb 12 at 21:29
  • \$\begingroup\$ On your note, I've interpreted the problem as encoding a series of octets as a padded string of sextets. That means for every group of 3 ASCII characters, there are 4 resulting Base64 encoded characters. ASCII Man encodes to TWFu in a 6-bit universe represented as ASCII AZaz09+/. Similarly, Ma encodes to TWE= and M encodes to TQ==. This is essentially how every base64 encoding library operates (plus extras dealing with line length, line separators, and checksums). \$\endgroup\$ – Snowhawk Feb 12 at 21:33
  • \$\begingroup\$ @Snowhawk You should use 1 \cdot 2 for \$1 \cdot 2\$ instead of 1 \dot 2, which looks like \$1 \dot 2\$. \$\endgroup\$ – L. F. Feb 13 at 11:38

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.