Codificador
Cada 3 bytes de entrada se convierten en 4 caracteres de 6 bits cada uno. Un grupo final de 1 o 2 bytes se rellena con =. La página cómo funciona Base64 recorre la disposición de los bits paso a paso.
#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';
}
La condición del bucle i + 2 < n en lugar de i < n - 2 es importante: con size_t, n - 2 da la vuelta y se convierte en un número enorme cuando n vale 0 o 1. Las conversiones a uint32_t evitan que in[i] << 16 se calcule sobre un int con signo.
Decodificador estricto
El decodificador rechaza todo lo que no sea Base64 canónico con relleno: longitud incorrecta, caracteres fuera del alfabeto, relleno en mitad de la cadena y bits de relleno distintos de cero.
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 recibe un unsigned char para que los bytes por encima de 127 no se conviertan en índices negativos en las plataformas donde char tiene signo. Una tabla de búsqueda de 256 entradas es más rápida; la cadena de if es más fácil de revisar a ojo.
Uso y resultados
#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 no tiene noción de codificación de texto: los bytes del literal de cadena son los que contenga el archivo fuente. Con un archivo fuente en UTF-8 y el juego de caracteres de ejecución por defecto de GCC, obtienes UTF-8.
Lo que devuelve b64_decode con algunas entradas típicas:
| Entrada | Devuelve | Por qué |
|---|---|---|
"" | 0 | la cadena vacía es válida |
aGk= | 2 | hi |
aGk | -1 | la longitud no es múltiplo de 4 |
aGk=!! | -1 | longitud y caracteres |
aGl= | -1 | bits de relleno distintos de cero |
PDw_Pz8-Pg== | -1 | caracteres URL-safe |
a=== | -1 | demasiado relleno |
Compilamos con gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined, hicimos ida y vuelta con 2000 búferes aleatorios y comparamos la decisión de aceptar o rechazar en 20 000 cadenas aleatorias con b64decode(validate=True) de Python más una comprobación de recodificación. Ninguna diferencia y ningún aviso de los sanitizers. Para probar tus propias cadenas, el validador Base64 aplica las mismas reglas.
Codificar un archivo o stdin por bloques
Lee bloques cuyo tamaño sea múltiplo de 3 y la salida concatenada será igual que codificar el archivo entero de una vez:
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;
}
Con un archivo aleatorio de 1 000 003 bytes, ./b64 < big.bin produjo la misma salida que base64 -w 0 big.bin. fread sigue leyendo hasta llenar el búfer o llegar al final de la entrada, así que las lecturas cortas desde una tubería no insertan relleno. En Windows, llama primero a _setmode(_fileno(stdin), _O_BINARY); si no, la traducción de CRLF corrompe la entrada binaria.
Salida URL-safe
Convierte después de codificar: cambia dos caracteres y quita el relleno.
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" */
Para decodificar, asigna - a 62 y _ a 63 en b64_val y acepta un último grupo de 2 o 3 caracteres sin =. Consulta Base64 frente a Base64URL y el relleno explicado.
Errores habituales en Base64 escrito a mano
- Búferes con un byte de menos. Olvidar el terminador NUL, o dimensionar la salida como
n * 4 / 3, que se queda corto siempre quenno es múltiplo de 3. La calculadora de tamaño y la guía del aumento de tamaño dan las cifras exactas. - Búsquedas con
charcon signo.table[c]con uncnegativo lee fuera del array. strlensobre datos binarios. La salida decodificada puede contener0x00; usa la longitud devuelta, nostrlen.- Trígrafos. En los modos estrictos C99/C11, una cadena de prueba como
"<<???>>"contiene el trígrafo??>, que el compilador convierte en}. GCC avisa con-Wtrigraphs. Escápala como"<<?\?\?>>"o usa un array de bytes. C23 eliminó los trígrafos.
La versión en C++ de este código, con std::string y std::vector, está en Base64 en C++.
Preguntas frecuentes
¿Qué tamaño debe tener el búfer de salida?
Para codificar, 4 * ((n + 2) / 3) caracteres más un byte para el NUL final. Para decodificar, 3 * (len / 4) bytes siempre bastan; la longitud real puede ser hasta dos bytes menor por el relleno.
¿Hay alguna función Base64 en OpenSSL o glibc que pueda usar en su lugar?
OpenSSL tiene EVP_EncodeBlock y EVP_DecodeBlock, y su capa BIO puede procesar Base64 en flujo. glibc no tiene una API pública de Base64. Si tu proyecto ya enlaza una de estas bibliotecas, tiene sentido usarla; si no, el código de abajo es más sencillo que añadir una dependencia nueva.
¿Por qué mi decodificador acepta cadenas como aGl=?
aGl= y aGk= se decodifican ambas como hi; la primera tiene bits de relleno distintos de cero. Los decodificadores tolerantes ignoran esos bits. El decodificador de esta página los rechaza comprobando (c & 3) y (b & 15) en el último grupo.