Upload a file
  1. base64hub
  2. Base64 in Code
  3. C
Code

Base64 in C

The C standard library has no Base64 functions, so most projects carry their own. Here is a short implementation that we fuzzed against Python’s base64 module, plus the details that usually go wrong.

Encoder

Every 3 input bytes become 4 characters, each taking 6 bits. A final group of 1 or 2 bytes is padded with =. The how Base64 works page walks through the bit layout.

#include <stddef.h>
#include <stdint.h>

static const char B64[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* Output size for n input bytes, without the terminating NUL. */
size_t b64_encoded_len(size_t n) { return 4 * ((n + 2) / 3); }

/* Writes b64_encoded_len(n) chars plus a NUL to out. */
void b64_encode(const uint8_t *in, size_t n, char *out) {
    size_t i = 0;
    for (; i + 2 < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (uint32_t)in[i + 1] << 8 | in[i + 2];
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = B64[v >> 6 & 63];
        *out++ = B64[v & 63];
    }
    if (i < n) {                         /* 1 or 2 bytes left */
        uint32_t v = (uint32_t)in[i] << 16;
        if (i + 1 < n) v |= (uint32_t)in[i + 1] << 8;
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = (i + 1 < n) ? B64[v >> 6 & 63] : '=';
        *out++ = '=';
    }
    *out = '\0';
}

The loop condition i + 2 < n instead of i < n - 2 matters: with size_t, n - 2 wraps around to a huge number when n is 0 or 1. The casts to uint32_t prevent in[i] << 16 from being done on a signed int.

Strict decoder

The decoder rejects anything that is not canonical, padded Base64: wrong length, characters outside the alphabet, padding in the middle, and non-zero padding bits.

static int b64_val(unsigned char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/* Returns the number of bytes written, or -1 on invalid input.
   out needs room for 3 * (len / 4) bytes. */
long b64_decode(const char *in, size_t len, uint8_t *out) {
    if (len % 4 != 0) return -1;
    size_t o = 0;
    for (size_t i = 0; i < len; i += 4) {
        int a = b64_val(in[i]), b = b64_val(in[i + 1]);
        int c = b64_val(in[i + 2]), d = b64_val(in[i + 3]);
        int last = (i + 4 == len);
        if (a < 0 || b < 0) return -1;
        if (c < 0) {                     /* must be "xx==" at the end */
            if (!last || in[i + 2] != '=' || in[i + 3] != '=' || (b & 15)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
        } else if (d < 0) {              /* must be "xxx=" at the end */
            if (!last || in[i + 3] != '=' || (c & 3)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
        } else {
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
            out[o++] = (uint8_t)(c << 6 | d);
        }
    }
    return (long)o;
}

b64_val takes an unsigned char so that bytes above 127 do not turn into negative indexes on platforms where char is signed. A lookup table of 256 entries is faster; the if chain is easier to check by eye.

Usage and results

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "héllo wörld";          /* source file saved as UTF-8 */
    char enc[64];
    b64_encode((const uint8_t *)s, strlen(s), enc);
    printf("%s\n", enc);                     /* aMOpbGxvIHfDtnJsZA== */

    uint8_t dec[64];
    long n = b64_decode(enc, strlen(enc), dec);
    printf("%ld %.*s\n", n, (int)n, dec);    /* 13 héllo wörld */
    return 0;
}

C has no notion of text encoding: the bytes in the string literal are whatever the source file contains. With a UTF-8 source file and GCC’s default execution charset, you get UTF-8.

What b64_decode returns for some typical inputs:

InputReturnWhy
""0empty is valid
aGk=2hi
aGk-1length not a multiple of 4
aGk=!!-1length and characters
aGl=-1non-zero padding bits
PDw_Pz8-Pg==-1URL-safe characters
a===-1too much padding

We compiled with gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined, round-tripped 2,000 random buffers and compared the accept/reject decision on 20,000 random strings against Python’s b64decode(validate=True) plus a re-encode check. No differences and no sanitizer reports. To test your own strings, the Base64 validator applies the same rules.

Encode a file or stdin in chunks

Read blocks whose size is a multiple of 3, and the concatenated output equals encoding the whole file at once:

int main(void) {
    uint8_t in[3 * 1024];                /* multiple of 3: no padding mid-stream */
    char out[4 * 1024 + 1];
    size_t n;
    while ((n = fread(in, 1, sizeof in, stdin)) > 0) {
        b64_encode(in, n, out);
        fputs(out, stdout);
    }
    return ferror(stdin) ? 1 : 0;
}

On a 1,000,003-byte random file, ./b64 < big.bin produced the same output as base64 -w 0 big.bin. fread keeps reading until the buffer is full or the input ends, so short reads from a pipe do not insert padding. On Windows, call _setmode(_fileno(stdin), _O_BINARY) first or CRLF translation corrupts binary input.

URL-safe output

Convert after encoding: swap two characters and drop the padding.

void b64_to_url(char *s) {
    char *w = s;
    for (char *r = s; *r; r++) {
        if (*r == '=') break;
        *w++ = (*r == '+') ? '-' : (*r == '/') ? '_' : *r;
    }
    *w = '\0';
}
/* bytes {0xfb, 0xff}: "+/8=" -> "-_8" */

For decoding, map - to 62 and _ to 63 in b64_val and accept a final group of 2 or 3 characters without =. See Base64 vs Base64URL and padding explained.

Common bugs in hand-written Base64

  • Off-by-one buffers. Forgetting the NUL terminator, or sizing the output as n * 4 / 3, which is too small whenever n is not a multiple of 3. The size calculator and the size overhead guide give exact numbers.
  • Signed char lookups. table[c] with a negative c reads outside the array.
  • strlen on binary data. Decoded output can contain 0x00; use the returned length, not strlen.
  • Trigraphs. In strict C99/C11 modes, a test string such as "<<???>>" contains the trigraph ??>, which the compiler turns into }. GCC warns with -Wtrigraphs. Escape it as "<<?\?\?>>" or use a byte array. C23 removed trigraphs.

The C++ version of this code, with std::string and std::vector, is on Base64 in C++.

FAQ

How big must the output buffer be?

For encoding, 4 * ((n + 2) / 3) characters plus one byte for the terminating NUL. For decoding, 3 * (len / 4) bytes is always enough; the actual length is up to two bytes less because of padding.

Is there a Base64 function in OpenSSL or glibc I can use instead?

OpenSSL has EVP_EncodeBlock and EVP_DecodeBlock, and its BIO layer can stream Base64. glibc has no public Base64 API. If your project already links one of these libraries, using it is reasonable; otherwise the code below is easier than a new dependency.

Why does my decoder accept strings like aGl=?

aGl= and aGk= both decode to hi; the first has non-zero padding bits. Lenient decoders ignore those bits. The decoder on this page rejects them by checking (c & 3) and (b & 15) in the final group.

Keep reading