Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Décodage Base64 en Rust : un guide complet

Une chaîne atterrit dans votre programme Rust : lettres et chiffres, un plus et une barre oblique de-ci de-là, et un ou deux = douteux qui pendent à la fin. C'est du Base64, et ce guide est là pour récupérer les octets d'origine sans mauvaise surprise. La page d'accueil de ce site détaille le format en profondeur, il ne reste donc qu'à redire ici la forme de l'échange : quatre caractères de l'alphabet représentent trois octets d'entrée, et un ou deux caractères = à la queue marquent là où se sont terminées les vraies données. Le décodage déroule cet échange en sens inverse, et tout ce qui suit est une question de le faire avec méthode.

Le seul détail qui distingue Rust de la plupart des langages : la bibliothèque standard ne contient aucun Base64. Il n'y a pas de base64_decode() caché dans std, et aucun use std::... pour en faire apparaître un. 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 sa toute première version en décembre 2015, et son compteur de téléchargements avoisine 1,5 milliard. Vous décodez presque certainement du Base64 via cette crate déjà, directement ou entraîné par quelque chose comme jsonwebtoken, pem ou serde_with, qui en dépendent tous.

La crate unique et son cercle

Si Rust lui-même n'est pas encore sur la machine, votre système d'exploitation le fournit : rustc et cargo sur Debian et Ubuntu, un paquet ou un installateur sur macOS et Windows, ou l'installateur officiel qui met en place rustup :

# 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. Cette seule ligne est l'installation entière, et elle n'entraîne exactement aucune dépendance :

cargo new my-app
cd my-app
cargo add base64

Trois fonctionnalités optionnelles façonnent la compilation. std est activée par défaut et active les types de streaming de std::io, les implémentations standard de Error et l'allocation dans le tas. alloc fournit les API avec allocation pour les builds embarqués no_std sans bibliothèque standard complète. simd-unsafe est activée par défaut et contrôle les moteurs vectorisés que vous croiserez plus loin. La version minimale de Rust prise en charge est 1.71.0, donc toute version récente suffit. Autour de la crate principale orbite un petit cercle de spécialistes, chacun pour un aspect que la crate laisse volontairement à votre charge :

Crate Version (2026) Ce qu'elle apporte À utiliser quand
base64ct 1.8 Décodage en temps constant issu du projet RustCrypto ; les API du tas se cachent derrière la fonctionnalité alloc Les octets que vous décodez pourraient fuiter des informations par le temps de calcul, comme du matériel de clé
data-encoding 2.11 Base64 aux côtés de base32, hex et leurs cousins, avec des variantes MIME permissives et un encodage/décodage au niveau des tranches Un composant doit analyser une entrée sale, enveloppée ou multi-protocole
base64-turbo 0.3 Un codec plus récent qui dépasse les 100 Go/s au sommet, avec des noyaux AVX512, AVX2 et NEON plus un repli scalaire sûr Le débit est tout l'enjeu et le moteur standard laisse des cycles sur la table

Aucun d'eux ne remplace le travail de tous les jours. Pour l'écrasante majorité des programmes Rust, base64 tout seul est la réponse correcte et complète, et le reste de cet article utilise cette crate unique pour le décodage lui-même, en appelant les spécialistes du cercle seulement quand le travail dépasse le base64.

Trois lignes pour vos octets

Neuf fois sur dix, la vie du décodage tient en trois lignes. Le test de fumée canonique utilise la célèbre chaîne TWFu :

use base64::prelude::*;
fn main() {
  let packed = "TWFu";
  let bytes = BASE64_STANDARD.decode(packed).expect("valid base64");
  println!("{}", String::from_utf8(bytes).expect("valid utf-8"));
}

La sortie est Man, et trois choses de cette petite cérémonie méritent de rester en tête. Premièrement, decode() vous remet toujours un Vec<u8>, jamais une chaîne. C'est une fonctionnalité, pas un accident : le Base64 peut porter une phrase, un JPEG ou un certificat, et aucun d'eux ne doit être traité différemment avant de savoir ce que vous tenez. Deuxièmement, le saut des octets au texte est une étape distincte et délibérée via String::from_utf8(), et c'est à ce moment que se prend la décision du charset. Troisièmement, 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. Si vous préférez des imports explicites, use base64::engine::general_purpose::STANDARD; accompagné de use base64::Engine; est la même porte, avec l'enseigne.

Et comme vous finirez par décoder quelque chose que vous avez encodé vous-même, voici l'aller-retour qui prouve que les deux directions se mettent d'accord. L'encodage a son propre guide complet sur le site sœur ; il n'apparaît ici que pour fabriquer des données de test :

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!
}

Gardez TWFu dans votre poche comme test de fumée pour n'importe quelle voie de décodage que vous écrirez : si elle transforme TWFu en Man, la machine est honnête.

Quatre façons de dire non

C'est la section qui vous sauvera à 2 heures du matin, parce que quand une chaîne en production explose, vous voulez savoir précisément de quoi la crate se plaint. Bonne nouvelle : elle se plaint fort et précisément. DecodeError a exactement quatre variantes, et voici comment chacune sonne face à une famille de fautifs typiques, tous passés dans le moteur standard strict :

Entrée Ce qui ne va pas Erreur exacte
"SGVs bG8s" un espace s'est glissé Invalid symbol 32, offset 4.
"SGVs\nbG8s" un retour à la ligne s'est glissé Invalid symbol 10, offset 4.
"SG=VsbG8="" du padding au milieu de la chaîne Invalid symbol 61, offset 2.
"SGVsbG8sIHdvcmxkIQ==xx" de la saleté qui traîne après le padding Invalid symbol 61, offset 18.
"S" un seul symbole ne peut pas former un octet Invalid input length: 1
"SGV" trois symboles sans le padding qui doit suivre Invalid padding
"SGVs$bG8="" un $ n'est pas dans l'alphabet Invalid symbol 36, offset 4.

Remarquez comme le message Invalid symbol vous donne à la fois la valeur de l'octet fautif et son offset, pour que vous puissiez sauter directement sur les lieux du crime. La variante InvalidLength est la pointilleuse : depuis la version 0.22.0, elle se déclenche précisément quand le nombre de symboles valides est impossible, c'est-à-dire une longueur qui dépasse d'une unité un multiple de quatre, tandis que les autres mauvaises longueurs apparaissent comme des erreurs de padding. Voici le match complet, pour les jours où vous voulez traiter chaque échec différemment :

use base64::DecodeError;
use base64::prelude::*;
fn triage(dirty: &str) {
  match BASE64_STANDARD.decode(dirty) {
    Ok(_) => println!("{dirty:?} sailed through"),
    Err(DecodeError::InvalidByte(offset, byte)) =>
      println!("{dirty:?}: symbol {byte} at {offset} is not in the alphabet"),
    Err(DecodeError::InvalidLength(symbols)) =>
      println!("{dirty:?}: {symbols} valid symbols is impossible"),
    Err(DecodeError::InvalidLastSymbol { offset, .. }) =>
      println!("{dirty:?}: trailing bits at {offset} suggest truncation"),
    Err(DecodeError::InvalidPadding) =>
      println!("{dirty:?}: padding is wrong or missing"),
  }
}

Cette rigueur assumée remonte à la norme elle-même. La section 12 de la RFC 4648 avertit que les caractères hors de l'alphabet peuvent être détournés en canal caché pour glisser des informations hors bande, ou pour réveiller les bugs des parseurs négligents, et elle recommande aux décodeurs de les refuser. La spec MIME est l'exception célèbre, qui dit explicitement aux décodeurs d'ignorer les caractères errants, et c'est cette forme d'entrée que la section email ci-dessous vous montre comment apprivoiser.

Trois positions sur le padding

Toute chaîne Base64 dans la nature fait une promesse silencieuse sur le padding, et la version 0.23 vous laisse choisir laquelle faire respecter grâce à l'enum DecodePaddingMode. Il y a trois modes, et la différence de comportement mérite d'être mémorisée. Voici le tableau de scores pour Zm8, c'est-à-dire le mot fo sans son = :

Mode "Zm8", sans padding "Zm8=", avec padding À utiliser quand
RequireCanonical, le défaut Err(Invalid padding) Ok([102, 111]) Vous produisez et consommez vous-même les données
Indifferent Ok([102, 111]) Ok([102, 111]) Vous recevez des données de sources mêlées
RequireNone Ok([102, 111]) Err(Invalid padding) Vous suivez un protocole sans padding
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, STANDARD_PAD_INDIFFERENT};
use base64::engine::DecodePaddingMode;
use base64::prelude::*;
let strict = BASE64_STANDARD;  // RequireCanonical est le défaut
let flexible = STANDARD_PAD_INDIFFERENT;
let bare = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireNone),
);
println!("{:?}", strict.decode("Zm8"));    // Err(Invalid padding)
println!("{:?}", flexible.decode("Zm8"));  // Ok([102, 111])
println!("{:?}", flexible.decode("Zm8=")); // Ok([102, 111])
println!("{:?}", bare.decode("Zm8="));     // Err(Invalid padding)

Les valeurs par défaut suivent la norme : la section 3.2 de la RFC 4648 dit que les implémentations doivent inclure les caractères de padding appropriés à la fin des données encodées, sauf si la spécification environnante dit le contraire, et c'est pourquoi un moteur STANDARD en l'état les exige. Et le choix compte pour la sécurité, pas seulement par pointillerie. Accepter les deux notations, avec et sans padding, des mêmes données rend le Base64 malléable : la même charge logique peut s'écrire de deux façons, et n'importe quel code qui suppose une écriture canonique unique d'une valeur peut être surpris. L'article de 2022 « La malléabilité du Base64 en pratique » (Chatzigiannis et Chalkias, ePrint 2022/361) documente des conséquences réelles, et la documentation de la crate elle-même y fait un lien. La règle pratique : choisissez un mode par protocole, et soyez strict à chaque frontière où vous ne contrôlez pas le producteur.

Les bits cachés du dernier symbole

Voici une corruption qui survit à toutes les vérifications de caractères. Chaque symbole Base64 porte 6 bits, et 3 octets d'entrée (24 bits) deviennent exactement 4 symboles. Quand l'entrée ne fait que 1 ou 2 octets, le symbole final a des bits inutilisés, et le RFC est clair : les encodeurs conformes doivent mettre ces bits de surplus à zéro. Un encodeur bogué ou malveillant peut au contraire y laisser de la saleté, et le résultat passe quand même le test de l'alphabet, le test de longueur et le test de padding, tout en portant discrètement une queue corrompue. Le moteur strict est à vos côtés, avec une erreur d'un détail unique qui vous montre même les bits suspects :

use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
println!("{:?}", BASE64_STANDARD.decode("MT=="));
// Err(Invalid last symbol 0x54 ('T') at offset 1, decoded as 0b00010011.)
let lenient = GeneralPurpose::new(
  &base64::alphabet::STANDARD,
  GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
);
println!("{}", String::from_utf8_lossy(&lenient.decode("MT==").unwrap()));
// 1

Ce 0b00010011 de l'erreur est la valeur décodée du symbole fautif, bits hauts illégaux inclus, et la version 0.23.0 a rendu ce détail visible précisément parce qu'il est si difficile à déboguer autrement. Si vous savez que vos producteurs sont négligés, with_decode_allow_trailing_bits(true) avale la saleté au lieu de la refuser. Les navigateurs ont fait le pari inverse : l'algorithme forgiving-base64 de WHATWG, celui derrière atob() de JavaScript, est explicitement tolérant sur les bits de traîne, tandis que le défaut de Rust est l'expert en médecine légale. Sachez de quel côté de la table vous vous asseyez.

Base64url : l'alphabet qui voyage

Le Base64 standard consacre ses deux derniers emplacements d'alphabet à + et /, et c'est exactement là que les URL ne les veulent pas : dans une chaîne de requête, le plus veut dire espace, la barre oblique commence un nouveau segment de chemin, et un = en laisse se lit comme un séparateur. La section 5 de la RFC 4648 définit donc l'alphabet sûr pour les URL et les noms de fichiers, qui échange les deux casseurs contre - et _ et, comme la longueur est d'habitude récupérable, supprime d'habitude le padding aussi. Le RFC avertit même que cet encodage ne doit pas être considéré comme le même que le Base64 standard, et les noms des moteurs sont d'accord :

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]
// le moteur standard refuse la même entrée,
// parce qu'un tiret n'est tout simplement pas dans son alphabet
println!("{:?}", base64::prelude::BASE64_STANDARD.decode("----"));
// Err(Invalid symbol 45, offset 0.)

Et voici la raison pour laquelle la plupart des développeurs croisent le base64url : les JSON Web Tokens. Un JWT est trois parties base64url jointes par des points, et jeter un oeil dedans est une affaire de cinq lignes :

use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let parts: Vec<&str> = jwt.split('.').collect();
let header = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[0]).unwrap()).unwrap();
let payload = String::from_utf8(URL_SAFE_NO_PAD.decode(parts[1]).unwrap()).unwrap();
println!("{header}");
println!("{payload}");
// {"alg":"HS256","typ":"JWT"}
// {"sub":"1234567890","name":"John Doe","iat":1516239022}

Deux avertissements honnêtes. Décoder un JWT, c'est jeter un oeil, pas faire confiance : la troisième partie est la signature, et elle ne veut rien dire tant qu'elle n'est pas vérifiée contre une clé, ce qui est le travail de la crate jsonwebtoken (version 11 en 2026). La version 11 a un angle vif : elle exige que exactement une des fonctionnalités rust_crypto ou aws_lc_rs soit activée dans Cargo.toml, sinon elle panique dès la première fois que vous signez ou vérifiez un jeton, et son constructeur Validation traite le claim exp comme requis par défaut, donc les jetons frappés pour d'autres bibliothèques peuvent avoir besoin d'une validation ajustée. Et le décodage est là où le choix d'alphabet mord : donnez une chaîne URL-safe à BASE64_STANDARD, ou l'inverse, et vous obtenez un refus, parce que -, _ et le padding manquant sont tous invalides pour l'autre moteur. Alignez le moteur sur le protocole, à chaque fois.

Les octets ne sont pas des mots

Chaque décodeur de cet article s'arrête volontairement aux octets, et en Rust c'est plus simple que dans la plupart des langages, parce qu'il n'y a pas d'étape de charset cachée qui peut mal tourner. Le Base64 est un format d'octets, point final. La question « quel texte était-ce ? » vous revient à répondre, et la réponse par défaut du web moderne est UTF-8, qu'il suffit de mettre sous contrôle en une ligne de bibliothèque standard :

use base64::prelude::*;
let packed = "Y2Fmw6k=";   // le mot cafe avec un accent, emballé
let bytes = BASE64_STANDARD.decode(packed).unwrap();
match std::str::from_utf8(&bytes) {
  Ok(text) => println!("{text}"),
  Err(_) => eprintln!("not utf-8: {bytes:02x?}"),
}

Le chemin du succès pour les octets multiples couvre tout ce que vous croiserez sur le réseau :

Texte original Base64 Se re-décode
café Y2Fmw6k= oui
日本語 5pel5pys6Kqe oui
naïve résumé bmHDr3ZlIHLDqXN1bcOp oui
😀 8J+YgA== oui
π ≈ 3.14159 z4Ag4omIIDMuMTQxNTk= oui

Et quand la charge n'est pas du texte du tout, le même code a juste une autre fin. Voici le nombre magique d'un fichier PNG, les quatre octets 89 50 4E 47 plus la paire CRLF qui suit :

use base64::prelude::*;
let packed = "iVBORw0KGgo=";
let bytes = BASE64_STANDARD.decode(packed).unwrap();
println!("{bytes:02x?}");                          // [89, 50, 4e, 47, 0d, 0a, 1a, 0a]
assert!(std::str::from_utf8(&bytes).is_err());
std::fs::write("sprite.png", &bytes).unwrap();     // des octets en sortie, pas du texte en sortie

La règle est courte : supposez UTF-8, vérifiez avec std::str::from_utf8(), et traitez tout ce qui échoue comme une charge d'octets pour fs::write, un blob de base de données ou n'importe quelle destination d'où il vient. Le seul moment où vous allez chercher un vrai charset, c'est pour des données héritées qui n'ont jamais migré. La crate encoding_rs (version 0.8) nomme les vieux encodages et convertit :

use base64::prelude::*;
use encoding_rs::Encoding;
let packed = "Y2Fm6Q==";   // cafe avec un accent, emballé depuis des octets Latin-1
let bytes = BASE64_STANDARD.decode(packed).unwrap();
let (text, _, _) = Encoding::for_label(b"windows-1252").unwrap().decode(&bytes);
println!("{text}");   // cafe avec un accent, en UTF-8

Il n'y a pas de mode « décoder en Latin-1 » à mal configurer dans la crate base64, parce qu'elle ne devine jamais à votre place. C'est la discipline que vous gardez : la crate vous donne des octets, et c'est vous qui décidez ce qu'ils veulent dire.

L'entrée qui a survécu à l'email

Le Base64 qui a survécu à un système de messagerie porte des retours à la ligne : le MIME enveloppe à 76 caractères par ligne (les blocs PEM à 64), et la spec MIME dit explicitement aux décodeurs conformes d'ignorer les caractères hors de l'alphabet, retours à la ligne compris. Notre moteur est l'opposé du conforme-MIME : il refuse déjà le tout premier retour à la ligne, et le lecteur streaming rapporte le refus comme une erreur I/O qui enveloppe la même DecodeError précise :

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
let wrapped_in = "SGVs\nbG8s";
let mut reader = DecoderReader::new(wrapped_in.as_bytes(), &BASE64_STANDARD);
let mut out = Vec::new();
println!("{:?}", reader.read_to_end(&mut out).map(|_| out));
// Err(Custom { kind: InvalidData, error: Invalid symbol 10, offset 4. })

Les deux positions remontent à la même RFC, qui laisse le choix à la spécification environnante, et la crate base64 a choisi d'être la stricte. Ce n'est pas la première fois qu'elle change d'avis : la version 0.5.0 embarquait l'enveloppement des lignes MIME intégré et la gestion des espaces, et la version 0.10.0 a retiré les deux, au motif que l'enveloppement était trop d'avis pour une bibliothèque générale et compliquait l'histoire no_std. La recette pour l'entrée enveloppée de la vraie vie est donc la même que celle que la documentation de la crate suggère : retirer d'abord les caractères hors alphabet, puis décoder. Pour une chaîne en mémoire, c'est un filtre :

use base64::prelude::*;
fn strip_non_b64(input: &[u8]) -> Vec<u8> {
  input.iter().copied().filter(|b| !b" \n\r\t\x0b\x0c".contains(b)).collect()
}
fn main() {
  let wrapped = "SGVs\nbG8s\r\nIHN0\nYW5kYXJk";
  let clean = strip_non_b64(wrapped.as_bytes());
  let bytes = BASE64_STANDARD.decode(clean).unwrap();
  println!("{}", String::from_utf8_lossy(&bytes));   // Hello, standard
}

Si vous préférez un décodeur qui regarde droit à travers les retours à la ligne, la constante BASE64_MIME_PERMISSIVE de la crate data-encoding est exactement ça : elle décode "SGVsbG8s\r\nd29ybGQh\r\n" en Hello,world! sans que vous touchiez une ligne. Pour les flux où vous ne pouvez pas retenir toute l'entrée, le FAQ de la crate pointe vers la crate iter_read pour filtrer le flux d'octets, ou vers l'écriture d'un petit wrapper Read qui jette les octets indésirables à leur arrivée. Une mise en garde avant de construire un « décodeur tolérant » à la main : ignorer en silence les caractères hors alphabet est précisément le comportement que la section 12 de la RFC 4648 pointe comme canal caché, alors retirez seulement les espaces attendus, et refusez tout le reste.

Quand le message entier est en main, un parseur de mail fait le base64 à votre place. La crate mail-parser (version 0.11) décode chaque partie Content-Transfer-Encoding: base64 pendant qu'elle analyse, alors les pièces jointes reviennent en octets bruts, déjà déballées et décodées :

use mail_parser::MessageParser;
let email = br#"From: art@vandelay.com
To: jane@example.com
Subject: gift
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="festivus"

--festivus
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: base64

SGVsbG8gZnJvbSBlbWFpbA==
--festivus
Content-Type: image/gif
Content-Transfer-Encoding: Base64
Content-Disposition: attachment; filename="tiny.gif"

R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
--festivus--
"#;
let message = MessageParser::default().parse(email).unwrap();
for part in message.attachments() {
  let name = part
    .headers()
    .iter()
    .find(|h| h.name().eq_ignore_ascii_case("content-disposition"))
    .and_then(|h| h.value().clone().unwrap_content_type()
      .attribute("filename").map(|n| n.to_string()));
  let bytes = part.contents().to_vec();
  println!("{name:?}: {} bytes", bytes.len());
}
// Some("tiny.gif"): 42 bytes

Cette pièce jointe GIF se décode en 42 octets commençant par les quatre octets 47 49 46 38, les lettres ASCII GIF8. Vous n'avez jamais écrit une ligne de code Base64, et c'est tout l'intérêt d'utiliser un parseur : les détails d'encodage sont le problème de la bibliothèque.

La grosse charge

Les chaînes, c'est facile ; les fichiers, c'est là que le Base64 gagne sa vie, et la crate répond avec la même philosophie de streaming que le reste du io de Rust. Le read::DecoderReader enveloppe n'importe quel lecteur et remet des octets décodés en toute transparence pendant que vous lisez, alors un fichier encodé de plusieurs gigaoctets n'a jamais besoin de tenir en mémoire. Pour l'exemple ci-dessous, enregistrez la chaîne dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw== dans un simple fichier texte nommé fox.b64 :

use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn main() {
  let packed = std::fs::read("fox.b64").unwrap();
  let mut decoder = DecoderReader::new(&packed[..], &BASE64_STANDARD);
  let mut plain = Vec::new();
  decoder.read_to_end(&mut plain).unwrap();
  println!("{}", String::from_utf8_lossy(&plain));
  // the quick brown fox jumps over the lazy dog
}

La même idée se résume à une ligne avec io::copy : construisez un DecoderReader autour d'un fichier et copiez-le dans n'importe quel écriteur, et le décodage a lieu en chemin. Il y a aussi une jolie astuce de la documentation officielle pour valider une charge en espace constant, avec un tampon de taille statique et aucune allocation de données décodées du tout :

use std::io::Cursor;
use std::io::Read;
use base64::prelude::*;
use base64::read::DecoderReader;
fn is_valid_base64(input: &str) -> bool {
  let mut cursor = Cursor::new(input.as_bytes());
  let mut decoder = DecoderReader::new(&mut cursor, &BASE64_STANDARD);
  let mut buf = [0u8; 128];
  loop {
    match decoder.read(&mut buf) {
      Ok(0) => return true,   // lu jusqu'à la fin sans erreur
      Ok(_) => continue,
      Err(_) => return false, // quelque chose n'était pas du base64
    }
  }
}
fn main() {
  println!("{}", is_valid_base64("SGVsbG8sIHdvcmxkIQ=="));  // true
  println!("{}", is_valid_base64("dt=="));                  // false
}

Pour les charges qui sont grandes mais tiennent encore dans un tampon que vous gérez, l'API des tranches est l'option zéro surprise : base64::decoded_len_estimate(len) donne une taille décodée maximale prudente pour len symboles, et decode_slice() écrit directement dans votre tampon pré-alloué, en retournant exactement le nombre d'octets écrits :

use base64::prelude::*;
let packed = "SGVsbG8sIHdvcmxkIQ==";
let cap = base64::decoded_len_estimate(packed.len());  // 15, max prudent
let mut buf = vec![0u8; cap];
let written = BASE64_STANDARD.decode_slice(packed, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap());    // Hello, world!

Si votre tampon est trop petit, vous obtenez un DecodeSliceError::OutputSliceTooSmall propre au lieu d'une panique, et il existe une variante decode_slice_unchecked() qui panique par conception, pour les endroits où « trop petit » est une erreur de programmation sur laquelle vous préférez crasher plutôt que danser. Depuis la version 0.22.0, la vérification des tranches est prudente en votre faveur : elle n'échoue que quand la sortie ne tient vraiment pas, donc un tampon de taille exacte fonctionne.

Où vont les octets décodés

Le Base64 apparaît dans les projets Rust bien plus souvent que « une chaîne aléatoire » ne le laisse penser :

  • Réponses API et webhooks qui embarquent du binaire ou du JSON imbriqué en texte Base64 dans leurs charges, le motif classique du téléversement de fichier en JSON.
  • Inspection de JWT, où vous décodez l'en-tête et le payload pour jeter un oeil aux claims et remettre la signature à jsonwebtoken pour la partie qui veut vraiment dire quelque chose.
  • Des données en forme d'email : pièces jointes MIME et tout ce qui a traversé un système de messagerie, d'où l'existence de la section sur les espaces.
  • Blocs PEM dans les certificats et les clés, les sections -----BEGIN CERTIFICATE----- que chaque pile TLS mâche ; la crate pem les analyse à votre place et, avec justesse, se construit sur la crate base64 en interne.
  • Data URIs cachés dans le HTML et le CSS que vous scrapez ou rendez, du genre data:image/png;base64,....
  • En-têtes HTTP Basic auth, où Basic TWFuOnBhc3M= n'est que Man:pass déguisé.
  • Bases de données et fichiers de configuration, où quelqu'un voulait du binaire dans une colonne texte ou une variable d'environnement.
  • Passages de témoin entre langages : un service Python emballe un blob, Rust le déballage, et les deux côtés parlent le même alphabet par définition.

Pour le cas JSON, il y a un raccourci qui vaut le coup de le connaître : la crate serde_with (version 3) peut annoter un champ de struct pour que serde gère le Base64 dans les deux sens, en encodant les champs Vec<u8> en texte à la sortie et en les décodant à nouveau à l'entrée, avec une variante URL-safe à un paramètre de là :

use serde::{Deserialize, Serialize};
use serde_with::serde_as;
#[serde_as]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Config {
  #[serde_as(as = "serde_with::base64::Base64")]
  blob: Vec<u8>,
}
let cfg = Config { blob: b"stored in a database".to_vec() };
let json = serde_json::to_string(&cfg).unwrap();
// {"blob":"c3RvcmVkIGluIGEgZGF0YWJhc2U="}
let back: Config = serde_json::from_str(&json).unwrap();
assert_eq!(back, cfg);

Les data URIs prennent une opération de chaîne en deux étapes suivie d'un décodage ordinaire : trouvez la virgule, gardez ce qui est après, et vérifiez que les métadonnées finissent par le mot base64 :

use base64::prelude::*;
let uri = "data:image/png;base64,iVBORw0KGgo=";
let comma = uri.find(',').unwrap();
let meta = &uri[..comma];
let payload = &uri[comma + 1..];
let is_b64 = meta.rsplit(';').next().unwrap() == "base64";
let bytes = BASE64_STANDARD.decode(payload).unwrap();
println!("{is_b64}: {} bytes from {meta}", bytes.len());
// true: 8 bytes from data:image/png;base64

Et la règle unique qui gouverne tout ça, à répéter parce qu'elle prend encore des gens en flagrant délit : 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. Décodez librement, faites confiance sélectivement.

Une décennie de petites étapes soignées

L'histoire de la crate elle-même se lit comme un serrage lent des vis. Elle est apparue pour la première fois sur crates.io en décembre 2015, et la version 0.5.0 a fièrement ajouté le support MIME avec fins de ligne et enveloppement 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 décoder et laisser la poésie à la couche application ; cette même sortie a ajouté l'encodeur streaming et la détection des symboles de traîne invalides. La version 0.20.0 en 2022 a introduit l'abstraction de moteur et a inversé la valeur par défaut du padding, de sorte que le moteur en l'état exige le padding canonique ; la 0.21.0 a déprécié les vieilles fonctions libres comme base64::decode() au profit des méthodes de moteur, avec la note du compilateur « Use Engine::decode » (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, précisé ce que signifie InvalidLength, 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, le message InvalidLastSymbol 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. Une décennie de petites étapes soignées, et la crate qui a commencé comme « C'est du base64. Que pourrait-on vouloir de plus ? » embarque maintenant des noyaux vectorisés.

Le format est plus vieux que le web, et c'est pourquoi la rigueur a un goût personnel. 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 de 64 caractères ; chaque bloc -----BEGIN CERTIFICATE----- en qui votre pile TLS a jamais mis sa confiance est un descendant direct de cette décision. 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 fixé la longueur de ligne de 76 caractères qui enveloppe encore vos pièces jointes aujourd'hui. En 2006, la RFC 4648 est devenue la norme que tout le monde cite : les tables d'alphabets, la variante base64url en section 5, et les règles de rigueur que cette crate implémente avec un tel plaisir visible.

Le coin des faits amusants

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 se décode en zéro octet, mais "AA==" se décode en un octet : un NUL. En Base64, « rien » et « un zéro » sont des créatures différentes.
  • Chaque PNG encodé en Base64 que vous avez jamais vu commence par iVBORw0K. C'est le nombre magique du PNG déguisé, et c'est l'un des préfixes les plus reconnaissables d'Internet.
  • Les ID de vidéos YouTube sont du base64url sans padding : une valeur de 8 octets donne douze caractères Base64, et retirer le padding de traîne laisse le familier ID de onze caractères que vous pouvez coller n'importe où dans une URL. L'un des usages les plus visibles du mode sans padding sur Internet tout entier.
  • Bash compte en base 64 depuis des années : le littéral arithmétique $((64#...)) prend ses chiffres dans l'ordre 0-9, a-z, A-Z, et enfin @ et _ pour les valeurs 62 et 63, alors votre shell porte un alphabet de 64 caractères en plein jour.
  • Les vieux hashes de mot de passe crypt(3) utilisaient une variante Base64 dont l'alphabet commence par ./, et elle a une propriété adorable : trier les chaînes encodées donne le même ordre que trier les octets d'origine. Les fichiers de généalogie utilisaient le même alphabet pour la multimédia embarquée (GEDCOM 5.5 ; la révision 5.5.1 l'a retiré), et la crate base64 l'embarque comme alphabet::CRYPT.
  • La math du MIME, telle que l'ancienne règle de pouce la calcule encore : une charge d'email enveloppée coûte environ 1,37 fois sa taille d'origine, plus de l'ordre de quelques centaines d'octets d'en-têtes. L'infrastructure de messagerie des années 1990 prenait vraiment ce péage sur chaque pièce jointe.
  • Toute la crate est #![forbid(unsafe_code)], sauf la fonctionnalité simd-unsafe activée par défaut, dont on sort plutôt qu'on n'y entre. Un mot, « unsafe », et c'est le nom d'un drapeau de fonctionnalité.
  • Le Base64 est malléable d'une façon qui effraie les chercheurs en sécurité : les mêmes octets peuvent s'écrire avec ou sans padding, et avec de la saleté dans les bits de traîne, et les décodeurs tolérants ne s'en apercevront pas. Un article de 2022 a démontré des conséquences dans le monde réel, et c'est pourquoi le défaut strict de cette crate a l'air d'un garde du corps.
  • 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.

En résumé

Choisissez votre moteur selon la compagnie que vous fréquentez : BASE64_STANDARD pour tout ce que vous produisez et contrôlez, STANDARD_PAD_INDIFFERENT pour la frontière à sources mêlées, URL_SAFE_NO_PAD pour les jetons et les URL, et un GeneralPurpose fait main quand le protocole exige ses propres règles. Laissez les quatre variantes DecodeError faire leurs plaintes précises, filtrez vos octets à travers std::str::from_utf8() avant de les appeler du texte, streamez les grosses charges avec DecoderReader, retirez seulement les espaces attendus, et allez vers base64ct quand le temps de calcul est une menace. Décodez tout, ne faites confiance qu'à ce qui se vérifie. Et si un jour vous devez aller dans l'autre direction, emballer des octets dans une chaîne pour la route au lieu de la déballer, l'article sœur couvre l'encodage en Rust, de la math de taille à la fin en streaming.

Dernière mise à jour : 2026-09-08

Article associé : Encodage Base64 en Rust : un guide complet