Hacher des fichiers en parallèle en Rust avec Rayon et SHA-256
Pour hacher un lot de fichiers en Rust, lisez chaque fichier en flux à travers SHA-256 et laissez Rayon traiter des fichiers distincts simultanément. La commande ci-dessous affiche une somme de contrôle SHA-256 standard pour chaque fichier sans charger les fichiers entiers en mémoire. Des workers parallèles peuvent améliorer le débit du lot, mais un fichier unique reste traité de façon séquentielle, et le stockage peut limiter tout gain de vitesse.
Configuration requise
Ce tutoriel utilise Bash sous Linux, Rust et Cargo 1.98.1, ring 0.17.14 et Rayon 1.12.0. Il a été
testé dans un conteneur Debian 12 ARM64. Installez la chaîne d’outils Rust ainsi qu’un
compilateur et un éditeur de liens C pour compiler ring ; l’étape de vérification utilise aussi sha256sum de GNU.
Cargo a besoin d’un accès réseau pour télécharger les dépendances lors de la première compilation.
Créer le projet
Exécutez ce bloc depuis le répertoire parent dans lequel vous souhaitez créer le nouveau projet file-hashing :
cargo new --bin --edition 2024 --vcs none file-hashing &&
cd file-hashing &&
cargo add ring@=0.17.14 rayon@=1.12.0
La chaîne && s’arrête si la création du projet ou le changement de répertoire échoue. Si file-hashing existe déjà,
Cargo refuse de le remplacer. Choisissez un emplacement inutilisé et relancez le bloc entier. Ne poursuivez
les étapes suivantes qu’une fois la configuration réussie, en restant dans le nouveau répertoire du projet.
Les contraintes de = fixent les versions des deux dépendances directes. Conservez le fichier
Cargo.lock généré à côté du
manifeste pour préserver également les versions résolues de leurs propres dépendances.
Utiliser l’algorithme de somme de contrôle attendu par votre destinataire
Cet exemple utilise SHA-256 pour que vous puissiez comparer directement sa sortie avec sha256sum. Une empreinte BLAKE2
ne correspondrait pas à une référence SHA-256, quelle que soit la bibliothèque qui la calcule. Choisissez
l’algorithme avant d’optimiser son implémentation.
Lire chaque fichier en flux et paralléliser le lot
Remplacez le fichier src/main.rs généré par ce programme complet. Cela remplace volontairement le code
source provisoire de Cargo :
use rayon::prelude::*;
use ring::digest::{Context, SHA256};
use std::fs::File;
use std::io::{self, ErrorKind, Read};
use std::path::PathBuf;
use std::process::ExitCode;
fn sha256_digest<R: Read>(mut reader: R) -> io::Result<String> {
let mut context = Context::new(&SHA256);
let mut buffer = [0u8; 8192];
loop {
let count = match reader.read(&mut buffer) {
Ok(count) => count,
Err(error) if error.kind() == ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
};
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context
.finish()
.as_ref()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn main() -> ExitCode {
let files: Vec<PathBuf> = std::env::args_os().skip(1).map(PathBuf::from).collect();
if files.is_empty() {
eprintln!("Usage: file-hashing <file> [file ...]");
return ExitCode::from(2);
}
let result = files.par_iter().try_for_each(|path| -> io::Result<()> {
let digest = File::open(path).and_then(sha256_digest).map_err(|error| {
io::Error::new(error.kind(), format!("{}: {error}", path.display()))
})?;
println!("{digest} {}", path.display());
Ok(())
});
match result {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
eprintln!("{error}");
ExitCode::FAILURE
}
}
}
Chaque worker conserve un tampon de lecture de 8 KiB et son propre
ring::digest::Context.
Les appels successifs à update hachent les octets du fichier dans l’ordre ; finish produit l’empreinte complète.
La boucle ne hache que les octets effectivement lus, continue après des lectures partielles et réessaie en cas de
ErrorKind::Interrupted.
Les autres erreurs de lecture renvoient un échec au lieu d’une somme de contrôle calculée sur des données incomplètes.
Avec
try_for_each,
Rayon planifie le traitement simultané de chemins différents. L’ordre de sortie n’est pas spécifié. Si
l’ouverture ou la lecture d’un fichier échoue, la commande signale le chemin sur stderr et se termine avec
le code de sortie 1. Rayon tente d’arrêter le travail restant, mais d’autres workers peuvent déjà avoir
affiché des sommes de contrôle ; considérez le lot comme incomplet.
Sans argument, la commande affiche son mode d’emploi et se termine avec le code de sortie 2.
Exécuter le programme et vérifier les empreintes
Créez deux petits fichiers de test et exécutez le programme. Dans le sous-shell, set -C refuse d’écraser des
fichiers d’exemple existants, et chaque && empêche l’exécution des commandes suivantes après un échec :
(
set -C
printf abc > abc.txt &&
: > empty.txt &&
cargo run --release --locked -- abc.txt empty.txt
)
Vous devriez obtenir ces deux lignes, dans n’importe quel ordre. Le fichier de test abc.txt contient trois octets, sans retour à la ligne :
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad abc.txt
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.txt
Comparez les empreintes avec une implémentation indépendante :
sha256sum abc.txt empty.txt
Pour répéter l’opération de hachage, réutilisez les fichiers :
cargo run --release --locked -- abc.txt empty.txt
La commande se contente de lire ses entrées et d’écrire sur stdout. Cargo peut recompiler l’exécutable dans
target/release/ ; il ne remplace pas vos fichiers d’entrée. Passez vos propres chemins de fichiers après --, en mettant
entre guillemets les chemins qui contiennent des espaces. Les chemins sont affichés à titre indicatif : cet
exemple n’implémente ni l’échappement des noms de fichiers ni le mode de vérification des sommes de contrôle de
sha256sum.
Comparer le nombre de workers sur votre stockage
Une fois la version release compilée, chronométrez directement l’exécutable afin d’exclure la compilation. Rayon 1.12
utilise RAYON_NUM_THREADS
pour définir son nombre de workers :
time env RAYON_NUM_THREADS=1 ./target/release/file-hashing abc.txt empty.txt &&
time env RAYON_NUM_THREADS=4 ./target/release/file-hashing abc.txt empty.txt
Ces petits fichiers de test vérifient les commandes, pas les performances. Remplacez les chemins des deux commandes par le même lot représentatif avant de comparer les temps écoulés. Répétez les exécutions en alternant leur ordre : le cache de fichiers du système d’exploitation peut accélérer une exécution ultérieure. Davantage de workers peuvent aider lorsque le hachage constitue le goulot d’étranglement, ou au contraire ralentir l’ensemble lorsque des lectures simultanées se disputent la bande passante du stockage. Utilisez le nombre de workers qui profite à votre charge de travail ; aucun gain de vitesse n’est garanti.
Comparer avec une référence de confiance
Pour vérifier un téléchargement, comparez l’empreinte avec la valeur SHA-256 attendue par l’éditeur, obtenue auprès d’une source de confiance. Une somme de contrôle seule n’authentifie pas un fichier si quelqu’un peut remplacer à la fois le fichier et sa référence. Par exemple, Ubuntu vérifie la signature de son fichier de sommes de contrôle avant de contrôler le téléchargement.
Ne modifiez pas les fichiers pendant leur hachage : ce programme lit un flux, il ne peut donc pas vous fournir un instantané cohérent d’un fichier qu’un autre processus est en train de modifier.
