Encodage Base64 en Rust : un guide complet
Vous êtes sur le point d'envoyer quelques octets à travers une porte réservée au texte, et le prix d'entrée est une chaîne de lettres, de chiffres, de plus et de barres obliques qui fait environ un tiers de plus que ce que vous aviez. Bienvenue dans le Base64, la barrière de péage d'Internet. La page d'accueil de ce site explique le format en profondeur, il ne reste donc qu'à redire ici la forme : le Base64 écrit trois octets d'entrée sous forme de quatre caractères tirés d'un alphabet de 64 symboles, et un ou deux caractères = à la queue disent au lecteur là où se sont terminées les vraies données. Cet échange de quatre contre trois est toute l'économie du format, et ce guide est une question de le faire bien en Rust.
Le premier point à savoir, c'est que la bibliothèque standard de Rust ne le fera pas à votre place. Il n'y a pas de base64_encode() caché dans std, et aucun use std::... qui changeera votre avis. L'écosystème s'est arrêté sur une crate unique, tout simplement nommée base64, et elle est devenue indispensable : la version 0.23.1 est sortie le 4 août 2026, la crate a publié 45 versions depuis décembre 2015, et son compteur de téléchargements avoisine 1,5 milliard. Tous les exemples d'encodage ci-dessous utilisent cette crate unique, plus deux petits compagnons pour l'enveloppement des lignes et l'armure PEM.
La chaîne d'outils et la crate
D'abord la chaîne d'outils, une commande par monde :
# Debian / Ubuntu
sudo apt install rustc cargo
# ou l'installateur officiel, qui met en place rustup et cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Puis la crate, dans n'importe quel projet cargo :
cargo new my-app
cd my-app
cargo add base64
Cette seule ligne est l'installation entière, et elle n'entraîne exactement aucune dépendance. La crate embarque trois fonctionnalités optionnelles qu'il faut connaître : std (activée par défaut ; donne le streaming de std::io, les impls standard de Error et l'allocation dans le tas), alloc (les API avec allocation pour les builds embarqués no_std) et simd-unsafe (activée par défaut ; les moteurs SIMD, qui apparaissent quelques sections plus bas). La version minimale de Rust prise en charge est 1.71.0, donc toute version récente suffit. Autour d'elle se tiennent les compagnons pour les tâches que le cœur refuse volontairement de faire :
- line-wrap (version 0.2) insère les retours à la ligne de 76 ou 64 caractères que MIME et PEM exigent ; la crate
base64elle-même refuse d'envelopper, exprès, comme vous allez le voir. - pem (version 4) construit et analyse les blocs
-----BEGIN ...-----pour les certificats et les clés ; elle dépend debase64en interne et ajoute l'armure et l'enveloppement. - base64ct (version 1.8) est le décodeur en temps constant du projet RustCrypto, pour quand la partie lecture d'un aller-retour est la moitié sensible.
- base64-turbo (version 0.3) est un codec plus récent, haut débit, qui dépasse les 100 Go/s au sommet sur du matériel moderne.
Un encode, quatre caractères
La cérémonie la plus petite qui soit ressemble à ça, et elle prouve déjà tout l'aller-retour :
use base64::prelude::*;
fn main() {
let packed = BASE64_STANDARD.encode("Hello, world!");
println!("{packed}");
// SGVsbG8sIHdvcmxkIQ==
let back = BASE64_STANDARD.decode(packed).unwrap();
println!("{}", String::from_utf8(back).unwrap());
// Hello, world!
}
Deux choses méritent un regard. Le module prelude vous remet discrètement deux choses d'un coup : le moteur BASE64_STANDARD et le trait Engine dont vous appelez les méthodes, et c'est pourquoi un simple use base64::prelude::*; suffit à cet exemple. Et encode() accepte tout ce qui peut se lire en octets, grâce à la borne AsRef<[u8]> : un &str, un littéral &[u8], un Vec<u8>, à vous de nommer. La moitié décodage de l'exemple est là juste pour garder l'oeil sur l'encodeur, parce que la direction opposée a son propre guide complet sur le site sœur. Si vous voulez le test de fumée de la crate elle-même, sa documentation encode asdf et obtient YXNkZg== en retour ; même alphabet, même math.
Le prix exact de chaque octet
Tous les encodeurs Base64 existants prélèvent le même impôt, et une fois que vous voyez la math, vous pouvez budgéter l'impôt. Chaque caractère de sortie porte 6 bits, chaque octet d'entrée en porte 8, et le plus petit tas qui est les deux fait 24 bits : exactement 3 octets en entrée, exactement 4 caractères en sortie. Ce ratio est tout le spectacle, donc un fichier de 3 kilooctets devient 4 kilooctets et un téléversement de 10 mégaoctets devient 13,3. Le padding est l'erreur d'arrondi rendue visible : quand l'entrée n'est pas un multiple de 3 octets, le groupe final a de la capacité en trop, et l'encodeur la remplit de = pour que la longueur de sortie reste un multiple de 4. Voici la table de vérité de la RFC 4648, que le moteur standard reproduit exactement :
| Entrée | Longueur mod 3 | Encodé | Longueur de sortie |
|---|---|---|---|
"" (vide) |
0 | "" (vide) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
Lisez cette première ligne deux fois, parce que c'est celle que tout le monde se trompe dans sa tête : l'entrée vide s'encode en chaîne vide, pas en AA==. La chaîne AA== est l'encodage d'exactement un octet, un NUL, qui est une charge vraiment différente. Et quand vous devez dimensionner un tampon avant d'encoder, la crate vous remet la math en const fn, donc vous pouvez même dimensionner des tableaux à la compilation :
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
Regardez les lignes de 13 et 14 octets, parce que ce sont celles qui font trébucher la math au dos de l'enveloppe : 13 octets demandent 18 caractères sans padding mais 20 avec, tandis que 14 octets en demandent 19 et 20. La fonction retourne un Option, qui est None seulement quand la math de longueur déborderait, donc un unwrap() est sûr pour n'importe quelle entrée qui pourrait réellement exister en mémoire. Pour le monde en forme d'email, l'impôt a un supplément : le MIME enveloppe les lignes à 76 caractères, et l'ancienne règle de pouce est que le Base64 enveloppé coûte environ 1,37 fois la taille d'origine, plus le surcoût des en-têtes. Le FAQ de la crate elle-même a un avis moins poli sur le padding lui-même : les octets = « n'affectent pas le décodage, sauf à donner l'occasion de dire 'ce padding est incorrect' », et « des exaoctets de stockage et de transfert ont sans doute été gaspillés en octets = inutiles ».
Padding : une décision à propos du lecteur
Dans base64 0.23, les fonctions d'encodage nues sont dépréciées - la façon actuelle est d'appeler une méthode sur un Engine, et un moteur est une politique : quel alphabet écrire, et quel padding ajouter. Les préréglages vivent dans base64::engine::general_purpose, avec les quatre populaires ré-exportés dans le prelude :
| Moteur | Alphabet | Ajoute du padding | Le mieux pour |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
oui | tout, le défaut |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
non | les charges maigres que vous consommez aussi |
URL_SAFE / BASE64_URL_SAFE |
- _ |
oui | le contenu URL qui veut encore du padding |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
non | JWT, URL, ID d'objets |
Encoder sans padding n'est pas un hack que la crate tolère ; c'est une position de première classe, avec des constantes de config NO_PAD et PAD préconfigurées aux côtés des frères *_INDIFFERENT ajoutés en 0.23.0. Si les préréglages ne vont pas, vous construisez votre propre moteur à partir d'un Alphabet et d'un GeneralPurposeConfig avec un seul réglage, et comme les moteurs sont peu coûteux à construire, vous rangez le résultat dans un const au lieu de le reconstruire par requête :
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
Maintenant, la décision devient une décision sur les décodeurs des autres, ce qui n'est jamais purement esthétique. Les règles de rigueur du côté décodage viennent de DecodePaddingMode, et le tableau ci-dessous répond à « l'autre côté peut-il lire ce que j'ai écrit ? » :
| Vous encodez avec | Un décodeur STANDARD strict |
Un décodeur STANDARD_NO_PAD |
Un décodeur INDIFFERENT |
|---|---|---|---|
STANDARD (avec padding) |
le lit | refuse le = |
le lit |
STANDARD_NO_PAD |
refuse : padding manquant | le lit | le lit |
URL_SAFE_NO_PAD |
refuse : mauvais alphabet | refuse : mauvais alphabet | le lit seulement avec l'alphabet URL |
Les règles pratiques tombent de ce tableau. Si vous contrôlez les deux extrémités, choisissez un moteur et utilisez-le partout, et préférez le sans-padding pour économiser des octets. Si vous consommez des données du monde extérieur, votre décodeur a voix au chapitre sur le moteur que vous devriez émettre : un décodeur STANDARD en l'état a besoin de votre padding, tandis qu'un décodeur STANDARD_PAD_INDIFFERENT accepte les deux. Et le choix a aussi une saveur de sécurité. Permettre les deux notations, avec et sans padding, de la même charge rend le Base64 malléable ; l'article de 2022 « La malléabilité du Base64 en pratique » (Chatzigiannis et Chalkias, ePrint 2022/361), auquel la documentation de la crate elle-même fait un lien, montre pourquoi. Un protocole où les mêmes données peuvent s'écrire de deux façons différentes a l'habitude de surprendre le code qui traite la chaîne encodée comme une identité, alors quand votre format définit une écriture canonique unique, faites-la respecter à la frontière.
Base64url pour les jetons et les liens
Les deux dernières lettres de l'alphabet du Base64 standard sont + et /, et dans une URL ce sont deux des caractères les plus chers du langage : le plus devient %2B, la barre oblique devient %2F, et le padding devient %3D. La section 5 de la RFC 4648 corrige ça avec l'alphabet sûr pour les URL et les noms de fichiers, qui échange les deux casseurs contre - et _ et saute d'habitude le padding aussi. Les moteurs rendent la distinction impossible à manquer :
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let packed = URL_SAFE_NO_PAD.encode(b"\xfb\xef\xbe");
println!("{packed}"); // ----
let back = URL_SAFE_NO_PAD.decode(packed).unwrap();
println!("{back:02x?}"); // [fb, ef, be]
Trois octets de l'entrée la plus vilaine possible deviennent une chaîne de quatre caractères que vous pouvez coller dans une URL, un nom de fichier, un cookie ou une clé de base de données sans la moindre échappade en pourcentage. C'est l'alphabet où vivent les JSON Web Tokens : un JWT est trois parties base64url jointes par des points, et en frapper un avec la crate jsonwebtoken (version 11 en 2026), ça ressemble à ça :
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // bien dans le futur
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
La version 11 a une exigence de mise en place qui mord les nouveaux : la crate a besoin que exactement une des fonctionnalités rust_crypto ou aws_lc_rs soit activée dans Cargo.toml, et si aucune n'est allumée, elle panique dès la première fois que vous signez ou vérifiez un jeton. Notez le claim exp dans le struct : la validation de la crate le traite comme requis par défaut, donc les vrais jetons en portent un de toute façon, et l'alphabet base64url dans le jeton est entièrement l'affaire de la bibliothèque. Si vous inspectez des jetons plutôt qu'en frapper, l'article sœur montre l'aperçu en cinq lignes. Et un rappel de la règle d'or, qui s'applique avec une force spéciale aux jetons : les trois parties d'un JWT sont toutes lisibles sans clé. Le Base64 est un siège côté fenêtre, pas un cadenas.
Du texte en entrée, des octets en sortie
Les encodeurs ne lisent pas dans les pensées, donc « encoder cette chaîne » veut toujours dire « encoder les octets UTF-8 de cette chaîne » en Rust, parce que c'est ce que remet str::as_bytes(). La bonne nouvelle, c'est que le web moderne est presque entièrement UTF-8, alors le chemin honnête est court et joyeux :
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
Les cas multi-octets se comportent tous bien :
| Texte original | Base64 | Aller-retour |
|---|---|---|
café |
Y2Fmw6k= |
propre |
日本語 |
5pel5pys6Kqe |
propre |
😀 |
8J+YgA== |
propre |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
propre |
La seule vraie décision, c'est les octets dont vous partez. Si les données arrivent en octets et pas en texte, un fichier lu depuis le disque ou un tampon d'un appel réseau, sautez la chaîne tout à fait et encodez le Vec<u8> directement ; c'est aussi la seule bonne réponse pour les charges non UTF-8 comme un PNG ou un protobuf. Déposez n'importe quelle image à côté de votre code et pointez la lecture dessus :
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// chaque PNG encodé commence par iVBORw0K
assert!(packed.starts_with("iVBORw0K"));
Cette dernière assertion est un test de santé gratuit et l'un des préfixes les plus reconnaissables d'Internet. Et si un jour vous encodez le même texte logique à travers deux charsets différents, ou si vous encodez des octets que vous avez mal lus dans un autre charset, l'aller-retour reviendra en charabia avec un visage impassible. L'encodeur ne ment jamais ; il encode simplement les octets que vous lui donnez, ce qui est à la fois sa plus grande force et son seul piège.
Quand un format veut des lignes
La crate base64 n'insère pas de retours à la ligne, exprès, et ce n'est pas la première fois qu'elle fait ce choix. La version 0.5.0 embarquait l'enveloppement des lignes MIME avec des fins de ligne configurables, et la version 0.10.0 l'a retiré, la bibliothèque décidant que l'enveloppement était trop d'avis pour une crate générale et compliquait l'histoire no_std. Si un format exige des lignes, la crate line-wrap existe exactement pour ça. Sa fonction unique, line_wrap(), prend votre tampon pré-alloué, la longueur d'entrée, la limite de colonne et la fin de ligne, et retourne le nombre d'octets de fin de ligne qu'elle a insérés :
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400 caractères
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 400 chars in, 410 bytes out, 10 line endings (cinq paires CRLF)
Pré-dimensionnez le tampon avec de la place pour les fins de ligne, appelez la fonction, et tronquez à la totalité rapportée ; les cinq paires CRLF sont le prix de la règle des 76 colonnes du MIME. Pour le PEM, échangez la limite et la fin, 64 colonnes et line_wrap::lf(), et vous avez le corps du texte de l'armure. Puis la crate pem ajoute les bannières en un appel :
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// et des fins de ligne à l'Unix, si le consommateur est pointilleux
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
Par défaut, pem::encode utilise CRLF, la convention historique du PEM ; le constructeur set_line_ending passe à LF pour les outils qui l'attendent. Remarquez ce que la crate pem ne fait pas : elle n'appelle jamais de fonction base64 que vous puissiez voir, parce que l'encodage est son affaire interne. Quand un format veut des lignes, l'architecture est une crate par tâche.
Streaming en espace constant
Pour des données trop grosses pour tenir dans une seule variable, la crate répond avec la même philosophie de streaming que le reste du io de Rust : le write::EncoderWriter enveloppe n'importe quel écriteur et encode en base64 tout ce que vous y écrivez, en espace constant. Le rituel complet pour un tampon ressemble à ça, et l'étoile de la section est l'appel finish() :
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
Pourquoi finish() est-il l'étoile ? Parce que c'est l'appel qui vide le groupe partiel final et ajoute le padding, et que l'encodeur a une méthode sœur qui ne le fait pas. La documentation de la crate elle-même le dit sans détour : finish() « encode les octets d'entrée restants et ajoute le padding si approprié. Elle est appelée automatiquement lors du désallouement (voir l'implémentation Drop), mais toute erreur qui survient lors de l'invocation de l'écriture sous-jacente sera supprimée. Si vous voulez gérer ce genre d'erreurs, appelez finish() vous-même. » L'implémentation Drop se comporte comme BufWriter : elle vide, mais elle ignore les erreurs pendant le drop. Donc le dernier groupe partiel n'est pas perdu, mais « ça a probablement marché » n'est pas une stratégie de livraison, parce que l'erreur d'écriture qu'elle aurait signalée est partie.
Le même flux fonctionne un cran plus loin à travers io::copy quand vous voulez toute la chaîne de traitement en un appel, et il y a un wrapper bonus pour les moments « j'ai juste besoin de ça dans une chaîne de format » :
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
Ce wrapper Base64Display est un petit joyau : il met en forme des octets en Base64 dans n'importe quelle chaîne de format sans la moindre allocation dans le tas, ce qui rend les lignes de log et la sortie de debug soudainement agréables.
L'allocation, et son absence
La méthode confortable alloue, et pour la plupart de votre vie c'est le bon arbitrage. Mais le trait Engine expose trois saveurs d'encodage, et le tableau ci-dessous est toute la matrice de décision :
| Méthode | Sortie | Alloue |
|---|---|---|
encode() |
un nouveau String |
toujours |
encode_string() |
ajoute à votre String |
seulement s'il doit grandir |
encode_slice() |
écrit dans votre &[u8] |
jamais |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// ou gardez le tampon entièrement sur la pile
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// et si vous l'avez mal dimensionné, vous obtenez une erreur, pas un débordement de tampon
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
Dimensionnez le tampon avec encoded_len(), écrivez avec encode_slice(), et si vous vous êtes trompé de taille, vous obtenez un EncodeSliceError::OutputSliceTooSmall propre au lieu d'un comportement indéfini, ce qui dans un langage système est la différence entre un après-midi ennuyeux et un long. Pour le travail embarqué, les mêmes fonctions existent derrière la fonctionnalité alloc, donc vous gardez l'API et vous jetez le tas.
La vitesse : les moteurs SIMD
La version 0.23.0, celle qui est sortie en juillet 2026, a apporté la fonctionnalité en Une : des moteurs accélérés SIMD pour les alphabets standard et URL-safe. Il y en a trois, et ils se séparent selon à quel point ils font confiance à votre matériel :
| Moteur | Détection à l'exécution | Fonctionne en no_std |
|---|---|---|
Simd |
oui, choisit AVX2 ou NEON, repli sur le moteur scalaire | non, a besoin de std pour la détection |
Avx2 |
non, suppose que le CPU a AVX2 | oui, sur les cibles x86_64 |
Neon |
non, suppose que le CPU a NEON | oui, sur les cibles aarch64 |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
Le constructeur Simd fait sa détection CPU une fois et retourne le meilleur noyau qu'il trouve, ou le moteur scalaire si aucun ne s'applique, alors construisez-le une fois dans un const ou au démarrage et réutilisez-le ; sur du matériel capable, il est plusieurs fois plus rapide que le chemin scalaire, pour l'encodage comme pour le décodage. Une note de bas de page honnête : le chemin SIMD est le seul endroit de la crate qui touche à unsafe, d'où le nom de la fonctionnalité simd-unsafe. Éteignez la fonctionnalité et toute la crate redevient #![forbid(unsafe_code)], le moteur scalaire continuant de faire un travail honnête. Si le débit brut est tout l'enjeu, la crate base64-turbo pousse plus loin, dépassant les 100 Go/s au sommet avec des noyaux AVX512, AVX2 et NEON derrière une détection à l'exécution, et un repli scalaire 100 % sûr sur tout le reste. La crate base64 est double-licenciée MIT/Apache-2.0, alors tout ça est gratuit, la vitesse incluse.
Quatre alphabets de plus
L'alphabet de la RFC est le défaut, mais la crate base64 en embarque quatre autres, chacun un petit monument à un vrai protocole qui avait besoin de sa propre variante :
| Alphabet | La variante | Qui l'utilise | abc 123 s'encode en |
|---|---|---|---|
alphabet::CRYPT |
./ en premier, puis chiffres et lettres, sans padding |
les hashes de mot de passe crypt(3) Unix classiques | MK7X612mAk |
alphabet::BCRYPT |
./ d'abord, puis lettres, puis chiffres |
les hashes de mot de passe bcrypt | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
une virgule tient lieu de barre oblique, sans padding | les noms de boîtes aux lettres UTF-7 modifié d'IMAP | YWJjIDEyMw |
alphabet::BIN_HEX |
un alphabet lourd en ponctuation qui évite les lettres confondues | BinHex 4, l'ancien enveloppeur de fichiers Macintosh | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
Même entrée, trois sorties différentes, toutes du Base64 valide dans leur propre dialecte. L'alphabet crypt est celui qui a un vrai super-pouvoir : parce que ses symboles sont ordonnés pour correspondre aux motifs de bits, trier les chaînes encodées vous donne le même ordre que trier les octets d'origine, et c'est pourquoi GEDCOM 5.5 (1996) l'a utilisé pour les champs multimédia - la révision 5.5.1 a retiré la fonctionnalité - et la crate embarque toujours l'alphabet pour vous. Et si le dialecte dont vous avez besoin n'est pas dans la crate, vous pouvez le définir avec une chaîne de 64 caractères, parce que Alphabet::new() construit les tables d'encodage et de décodage pour vous :
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// un base64 de monde étrange : +/ au début au lieu de la fin
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// tandis que le moteur standard dit :
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
Une mise en garde sur la route des alphabets personnalisés : le moment où vous inventez un dialecte, vous devenez la seule personne sur terre capable de lire vos données, alors faites-le seulement quand un protocole l'exige, et écrivez un commentaire qui dit lequel.
Où travaillent les encodeurs
L'encodage Base64 apparaît dans les projets Rust dans une distribution de situations prévisible :
- Téléversements de fichiers dans les API JSON, où le fichier est un champ d'octets déguisé en texte, l'usage le plus commun de loin.
- Data URIs dans le HTML et le CSS, du genre
data:image/png;base64,..., merveilleux pour les petites icônes, discutable pour les images héro. - JWT et OAuth, où base64url est le dialecte et la crate
jsonwebtokenest l'outil. - Blocs PEM pour les certificats et les clés, les sections
-----BEGIN CERTIFICATE-----qui enveloppent le Base64 à 64 caractères par ligne. - Binaire dans le XML et les fichiers de configuration, le motif
<data encoding="base64">que vous trouvez encore dans les marqueurs exportés et les dumps de réglages. - Fichiers LDAP et LDIF, qui utilisent le Base64 pour garder les valeurs d'attributs binaires sur une seule ligne.
- Charges de QR codes et passages de presse-papiers, où le texte survit au trajet et le binaire non.
- En-têtes HTTP Basic auth, où
Basic TWFuOnBhc3M=est une paire d'identifiants, et un rappel que c'est un problème d'emballage, pas un problème de cachette.
Et la règle d'or qui gouverne tout ça : le Base64 est un ruban adhésif, pas un cadenas. Ce n'est pas du chiffrement et ce n'est pas de la compression - c'est le contraire de la compression, et n'importe qui avec cet article peut retourner tout ce qu'il fait en une ligne. Encodez librement, mais n'encodez jamais un mot de passe, une clé API ou un secret et appelez ça protégé. S'il faut le cacher, utilisez un vrai chiffrement, et s'il est grand, considérez si un téléversement multipart n'aurait simplement pas été moins cher que l'impôt.
Une décennie de petites étapes
Le format est plus vieux que le web. En 1987, le protocole Privacy-Enhanced Mail (RFC 989) avait besoin de transporter des données binaires sur des canaux de messagerie 7 bits, et il a standardisé cet encodage avec des lignes d'exactement 64 caractères. Chaque bloc -----BEGIN CERTIFICATE----- d'Internet est un descendant de cette décision, et c'est pourquoi les lignes des fichiers PEM sont encore coupées à 64 aujourd'hui. En 1996, la spec MIME (RFC 2045) a adopté le schéma, l'a nommé « base64 » d'après son alphabet de 64 caractères, et a déplacé l'enveloppement à 76 caractères. Avant tout ça, les boîtes Unix embarquaient uuencode et les Macs embarquaient BinHex, chacun avec son alphabet, et les deux resurgissent encore dans les vieux systèmes comme des fossiles avec des en-têtes de fichier. En 2006, la RFC 4648 est devenue la norme que tout le monde cite, avec les tables d'alphabets, la variante base64url, et les règles d'encodage canoniques que chaque moteur de cet article implémente. Sa section 3.5 exige des encodeurs de mettre les bits de traîne inutilisés à zéro, et la crate le fait ; si votre charge déclenche plus tard la vérification InvalidLastSymbol d'un décodeur strict, la corruption est survenue en amont.
L'histoire de la crate elle-même rime. Elle est apparue sur crates.io en décembre 2015, et la version 0.5.0 a fièrement ajouté l'enveloppement des lignes MIME avec des fins de ligne configurables. Puis la version 0.10.0 en 2018 a retiré l'enveloppement et la gestion des espaces, la bibliothèque décidant qu'une crate générale devrait encoder et laisser la poésie à la couche application ; cette même sortie a ajouté le EncoderWriter streaming. La version 0.20.0 en 2022 a introduit l'abstraction de moteur et a fait du padding canonique le défaut, et la 0.21.0 a déprécié les vieilles fonctions libres au profit des méthodes de moteur, avec la note du compilateur « Use Engine::encode » (elles marchent toujours, et c'est pourquoi beaucoup de code hérité compile joyeusement). En 2024, la version 0.22.0 a affûté la sémantique des erreurs et accéléré le décodage de 5 à 10 pour cent. Et en juillet 2026, la version 0.23.0 est arrivée avec les moteurs SIMD, les symboles de padding sur mesure, un message d'erreur plus clair et la montée du MSRV à 1.71, avec le correctif 0.23.1 du 4 août qui répare la suite de tests pour les architectures non-SIMD.
Des choses qui valent un sourire
Parce qu'un guide complet doit finir sur un sourire :
- Le mot « base64 » s'encode en
YmFzZTY0. Un format qui se décrit lui-même est l'équivalent technique d'un miroir qui parle en Morse. - La chaîne vide s'encode en chaîne vide. Le rien est la seule entrée qui ne coûte rien, ce qui est une sorte de franchise fiscale.
AA==n'est pas l'encodage du rien ; c'est l'encodage d'un octet NUL. En Base64, « rien » et « un zéro » sont des créatures différentes, et les décodeurs les distinguent.- Chaque PNG encodé en Base64 commence par
iVBORw0K. C'est le nombre magique du PNG dans son ruban adhésif, l'un des préfixes les plus reconnaissables d'Internet. - Dans une URL, les caractères du Base64 standard ont besoin de déguisements d'échappement : le plus devient
%2B, la barre oblique devient%2F, et le padding devient%3D. Le base64url existe pour que les caractères puissent porter leur propre visage. - Les ID de vidéos YouTube sont du base64url sans padding : huit octets d'ID deviennent la chaîne de onze caractères que vous pouvez coller n'importe où. L'un des usages les plus visibles du mode sans padding sur Internet tout entier.
- L'ancien alphabet de mot de passe crypt(3) trie correctement : les chaînes encodées triées se tiennent dans le même ordre que le texte clair trié. GEDCOM 5.5 (1996) a utilisé cet alphabet pour ses champs multimédia, la révision 5.5.1 a retiré la fonctionnalité, et la crate l'embarque toujours pour vous.
- BinHex, l'ancien enveloppeur Macintosh, a construit son alphabet pour exclure les caractères visuellement confondus comme
7,O,geto. Un encodeur conçu pour les yeux humains, dans un monde avant la correction orthographique. - Le FAQ de la crate elle-même est direct sur le padding : des exaoctets de stockage et de transfert ont sans doute été gaspillés en octets
=inutiles. La barrière de péage encaisse depuis 1987. - Le Base64 n'est pas du chiffrement. Si c'était le cas, vous ne pourriez pas lire la sortie d'aucun exemple de cet article. C'est un siège côté fenêtre, pas un coffre-fort.
La version courte
Choisissez votre moteur selon la route que les données vont parcourir : BASE64_STANDARD pour tout ce que vous décodez aussi vous-même, les moteurs _NO_PAD quand vous contrôlez les deux extrémités et voulez les octets en retour, URL_SAFE_NO_PAD pour les jetons et les URL, et un Alphabet personnalisé seulement quand un protocole y tient. Dimensionnez vos tampons avec encoded_len(), streamez les grosses choses à travers EncoderWriter et terminez toujours avec finish(), enveloppez les lignes avec line-wrap et pem seulement quand un format l'exige, laissez les moteurs SIMD porter le lourd quand vous le pouvez, et souvenez-vous que l'échange de quatre contre trois est le prix pour passer la porte réservée au texte. Encodez tout, protégez seulement ce qui a besoin d'un vrai cadenas. Et quand vous devez aller dans l'autre direction, déballer une chaîne pour la ramener aux octets qui ont commencé le voyage, l'article sœur couvre le décodage en Rust, avec le tableau de scores complet des messages d'erreur exacts.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en Rust : un guide complet