Calcula hashes de archivos en paralelo con Rust, Rayon y SHA-256
Para calcular hashes de un lote de archivos en Rust, procesa cada archivo como un flujo con SHA-256 y deja que Rayon procese archivos distintos de forma concurrente. El comando siguiente imprime una suma de verificación SHA-256 estándar para cada archivo sin cargar archivos enteros en memoria. Los hilos de trabajo en paralelo pueden mejorar el rendimiento del lote, pero cada archivo sigue procesándose secuencialmente y el almacenamiento puede limitar cualquier mejora de velocidad.
Requisitos del sistema
Este tutorial usa Bash en Linux, Rust y Cargo 1.98.1,
ring 0.17.14 y Rayon 1.12.0. Se probó en un contenedor Debian 12 con ARM64.
Instala las herramientas de Rust y un compilador y un enlazador de C
para compilar ring; el paso de verificación también usa
sha256sum de GNU.
Cargo necesita acceso a la red para descargar las dependencias durante la primera compilación.
Crea el proyecto
Ejecuta lo siguiente desde el directorio principal donde quieras crear un nuevo proyecto
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 cadena && se detiene si falla la creación del proyecto o el cambio
de directorio. Si file-hashing ya existe, Cargo se niega a reemplazarlo.
Elige una ubicación que no esté en uso y vuelve a ejecutar todo el bloque. Continúa con los pasos
restantes solo cuando la preparación se complete correctamente, sin salir del nuevo directorio
del proyecto.
Los requisitos = fijan las versiones de las dos dependencias directas.
Conserva el archivo generado
Cargo.lock junto al
manifiesto para preservar también las versiones resueltas de sus dependencias.
Usa el algoritmo de suma de verificación que espera el destinatario
Este ejemplo usa SHA-256 para que puedas comparar su salida directamente con
sha256sum. Un resumen BLAKE2 no coincidiría con una referencia SHA-256,
independientemente de la biblioteca que lo calcule. Elige el algoritmo antes de optimizar su
implementación.
Procesa cada archivo como un flujo y paraleliza el lote
Reemplaza el archivo generado src/main.rs con este programa completo.
Esto reemplaza intencionalmente el código fuente provisional 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
}
}
}
Cada hilo de trabajo mantiene un búfer de lectura de 8 KiB y su propio
ring::digest::Context.
Las llamadas repetidas a update calculan el hash de los bytes del archivo
en orden; finish produce el resumen completo.
El bucle calcula el hash solo de los bytes que se han leído realmente, continúa tras las lecturas
cortas y reintenta la lectura cuando recibe
ErrorKind::Interrupted.
Otros errores de lectura devuelven un fallo en lugar de una suma de verificación de datos incompletos.
Rayon usa
try_for_each
para programar el procesamiento concurrente de distintas rutas. El orden de salida no está definido.
Si falla la apertura o la lectura de un archivo, el comando informa de la ruta en stderr y termina
con el código de salida 1. Rayon intenta detener el trabajo restante, pero es posible que otros
hilos de trabajo ya hayan impreso sumas de verificación; considera el lote incompleto.
Sin argumentos, el comando imprime las instrucciones de uso y termina con el código de salida 2.
Ejecútalo y comprueba los resúmenes
Crea dos archivos de prueba pequeños y ejecuta el programa. La opción
set -C del subshell impide sobrescribir archivos de ejemplo existentes,
y cada && evita que se ejecuten comandos posteriores tras un fallo:
(
set -C
printf abc > abc.txt &&
: > empty.txt &&
cargo run --release --locked -- abc.txt empty.txt
)
Deberías obtener estas dos líneas, en cualquier orden. El archivo de prueba
abc.txt contiene tres bytes, sin salto de línea:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad abc.txt
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.txt
Compara los resúmenes con una implementación independiente:
sha256sum abc.txt empty.txt
Para repetir el cálculo de los hashes, reutiliza los archivos:
cargo run --release --locked -- abc.txt empty.txt
El comando solo lee sus entradas e imprime en stdout. Cargo puede volver a compilar el ejecutable en
target/release/; no reemplaza tus archivos de entrada. Pasa tus propias rutas de
archivos después de -- y escribe entre comillas las rutas que contengan
espacios. Las rutas impresas solo sirven para mostrarlas: este ejemplo no implementa el escape de
nombres de archivo ni el modo de verificación de sumas de verificación de
sha256sum.
Compara distintas cantidades de hilos de trabajo en tu almacenamiento
Una vez que exista la compilación optimizada, mide el tiempo del ejecutable directamente para excluir
la compilación. Rayon 1.12 usa
RAYON_NUM_THREADS
para establecer la cantidad de hilos de trabajo:
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
Estos archivos de prueba diminutos sirven para comprobar los comandos, no el rendimiento. Reemplaza las rutas de ambos comandos por las de un mismo lote representativo antes de comparar los tiempos transcurridos. Repite las ejecuciones y alterna su orden: la caché de archivos del sistema operativo puede acelerar una ejecución posterior. Usar más hilos de trabajo puede ayudar cuando el cálculo de hashes es el cuello de botella, o ralentizar el procesamiento cuando las lecturas concurrentes compiten por el ancho de banda del almacenamiento. Usa la cantidad de hilos de trabajo que beneficie a tu carga de trabajo; no se promete ninguna mejora de velocidad.
Compara con una referencia de confianza
Para verificar una descarga, compara el resumen con el SHA-256 esperado que proporcione el publicador a través de una fuente de confianza. Una suma de verificación por sí sola no autentica un archivo si alguien puede reemplazar tanto el archivo como su referencia. Por ejemplo, Ubuntu verifica la firma de su archivo de sumas de verificación antes de comprobar la descarga.
Mantén los archivos sin cambios mientras calculas sus hashes: este programa lee un flujo, por lo que no puede darte una instantánea coherente de un archivo que otro proceso esté modificando.
