Encodeur
Chaque groupe de 3 octets d’entrée devient 4 caractères de 6 bits chacun. Un dernier groupe de 1 ou 2 octets est complété par =. La page le fonctionnement de Base64 détaille la disposition des bits.
#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 condition de boucle i + 2 < n plutôt que i < n - 2 a son importance : avec size_t, n - 2 reboucle vers un nombre énorme quand n vaut 0 ou 1. Les conversions en uint32_t évitent que in[i] << 16 soit calculé sur un int signé.
Décodeur strict
Le décodeur rejette tout ce qui n’est pas du Base64 canonique avec remplissage : longueur incorrecte, caractères hors de l’alphabet, remplissage au milieu et bits de remplissage non nuls.
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 prend un unsigned char pour que les octets au-delà de 127 ne deviennent pas des index négatifs sur les plateformes où char est signé. Une table de correspondance de 256 entrées est plus rapide ; la suite de if est plus facile à vérifier à l’œil.
Utilisation et résultats
#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;
}
Le C n’a aucune notion d’encodage de texte : les octets du littéral de chaîne sont ceux que contient le fichier source. Avec un fichier source en UTF-8 et le jeu de caractères d’exécution par défaut de GCC, vous obtenez de l’UTF-8.
Ce que renvoie b64_decode pour quelques entrées typiques :
| Entrée | Retour | Pourquoi |
|---|---|---|
"" | 0 | une chaîne vide est valide |
aGk= | 2 | hi |
aGk | -1 | longueur non multiple de 4 |
aGk=!! | -1 | longueur et caractères |
aGl= | -1 | bits de remplissage non nuls |
PDw_Pz8-Pg== | -1 | caractères URL-safe |
a=== | -1 | trop de remplissage |
Nous avons compilé avec gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined, fait l’aller-retour sur 2 000 tampons aléatoires et comparé la décision d’acceptation ou de rejet sur 20 000 chaînes aléatoires avec b64decode(validate=True) de Python suivi d’un contrôle par réencodage. Aucune différence et aucun signalement des sanitizers. Pour tester vos propres chaînes, le validateur Base64 applique les mêmes règles.
Encoder un fichier ou stdin par morceaux
Lisez des blocs dont la taille est un multiple de 3 : la sortie concaténée est alors identique à l’encodage du fichier entier d’un seul coup.
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;
}
Sur un fichier aléatoire de 1 000 003 octets, ./b64 < big.bin a produit la même sortie que base64 -w 0 big.bin. fread continue de lire jusqu’à ce que le tampon soit plein ou que l’entrée se termine : les lectures partielles depuis un pipe n’insèrent donc pas de remplissage. Sous Windows, appelez d’abord _setmode(_fileno(stdin), _O_BINARY), sinon la conversion CRLF corrompt l’entrée binaire.
Sortie URL-safe
Convertissez après l’encodage : remplacez deux caractères et supprimez le remplissage.
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" */
Pour le décodage, associez - à 62 et _ à 63 dans b64_val et acceptez un dernier groupe de 2 ou 3 caractères sans =. Voir Base64 ou Base64URL et le remplissage expliqué.
Bugs courants dans un Base64 écrit à la main
- Tampons décalés d’un octet. Oublier le terminateur NUL, ou dimensionner la sortie à
n * 4 / 3, ce qui est trop petit dès quenn’est pas un multiple de 3. Le calculateur de taille et le guide du surcoût de taille donnent les chiffres exacts. - Recherches avec un
charsigné.table[c]avec uncnégatif lit en dehors du tableau. strlensur des données binaires. La sortie décodée peut contenir0x00; utilisez la longueur renvoyée, passtrlen.- Trigraphes. En mode C99/C11 strict, une chaîne de test comme
"<<???>>"contient le trigraphe??>, que le compilateur transforme en}. GCC prévient avec-Wtrigraphs. Échappez-le en"<<?\?\?>>"ou utilisez un tableau d’octets. C23 a supprimé les trigraphes.
La version C++ de ce code, avec std::string et std::vector, se trouve sur Base64 en C++.
FAQ
Quelle taille doit faire le tampon de sortie ?
Pour l’encodage, 4 * ((n + 2) / 3) caractères plus un octet pour le NUL final. Pour le décodage, 3 * (len / 4) octets suffisent toujours ; la longueur réelle peut être inférieure de deux octets au plus à cause du remplissage.
Existe-t-il une fonction Base64 dans OpenSSL ou la glibc ?
OpenSSL propose EVP_EncodeBlock et EVP_DecodeBlock, et sa couche BIO sait traiter du Base64 en flux. La glibc n’a pas d’API Base64 publique. Si votre projet est déjà lié à l’une de ces bibliothèques, l’utiliser est raisonnable ; sinon, le code ci-dessous est plus simple qu’une nouvelle dépendance.
Pourquoi mon décodeur accepte-t-il des chaînes comme aGl= ?
aGl= et aGk= se décodent tous deux en hi ; le premier a des bits de remplissage non nuls. Les décodeurs tolérants ignorent ces bits. Le décodeur de cette page les rejette en vérifiant (c & 3) et (b & 15) dans le dernier groupe.