Gerar hashes de arquivos em paralelo em Rust com Rayon e SHA-256
Para gerar o hash de um lote de arquivos em Rust, processe cada arquivo em streaming com SHA-256 e deixe o Rayon processar arquivos diferentes simultaneamente. O comando abaixo imprime um checksum SHA-256 padrão para cada arquivo sem carregar arquivos inteiros na memória. Workers paralelos podem melhorar a vazão do lote, mas um único arquivo continua sendo processado sequencialmente, e o armazenamento pode limitar qualquer ganho de velocidade.
Requisitos do sistema
Este passo a passo usa Bash no Linux, Rust e Cargo 1.98.1, ring 0.17.14 e Rayon 1.12.0. Ele foi
testado em um contêiner Debian 12, ARM64. Instale o toolchain do Rust e um
compilador e linker C para compilar o ring; a etapa de verificação também usa o sha256sum do GNU.
O Cargo precisa de acesso à rede para baixar as dependências na primeira compilação.
Criar o projeto
Execute isto a partir do diretório pai onde você quer criar um novo projeto file-hashing:
cargo new --bin --edition 2024 --vcs none file-hashing &&
cd file-hashing &&
cargo add ring@=0.17.14 rayon@=1.12.0
A cadeia && interrompe a execução se a criação do projeto ou a navegação falhar. Se file-hashing já
existir, o Cargo se recusa a substituí-lo. Escolha um local não utilizado e execute o bloco inteiro
novamente. Continue com as etapas restantes somente depois que a configuração for concluída com
sucesso, permanecendo dentro do novo diretório do projeto.
Os requisitos de = fixam as duas dependências diretas. Mantenha o
Cargo.lock gerado junto ao
manifesto para preservar também as versões resolvidas das dependências delas.
Usar o algoritmo de checksum que o destinatário espera
Este exemplo usa SHA-256 para que você possa comparar a saída diretamente com sha256sum. Um digest
BLAKE2 não corresponderia a uma referência SHA-256, independentemente da biblioteca que o calcule.
Escolha o algoritmo antes de otimizar a implementação dele.
Processar cada arquivo em streaming e paralelizar o lote
Substitua o src/main.rs gerado por este programa completo. Isso substitui intencionalmente o código-fonte
provisório do 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 worker mantém um buffer de leitura de 8 KiB e seu próprio
ring::digest::Context.
Chamadas repetidas de update calculam o hash dos bytes do arquivo em ordem; finish produz o digest
completo. O loop calcula o hash apenas dos bytes efetivamente lidos, continua após leituras parciais
e repete a leitura em caso de
ErrorKind::Interrupted.
Outros erros de leitura retornam uma falha em vez de um checksum de dados incompletos.
O
try_for_each
do Rayon agenda caminhos diferentes simultaneamente. A ordem da saída não é especificada. Se a
abertura ou a leitura de um arquivo falhar, o comando informa o caminho no stderr e termina com
status 1. O Rayon tenta interromper o trabalho restante, mas outros workers podem já ter impresso
checksums; trate o lote como incompleto. Sem argumentos, o comando imprime o modo de uso e termina
com status 2.
Executar e verificar os digests
Crie dois pequenos arquivos de teste e execute o programa. O set -C do subshell se recusa a
sobrescrever arquivos de exemplo existentes, e cada && impede que comandos posteriores sejam
executados após uma falha:
(
set -C
printf abc > abc.txt &&
: > empty.txt &&
cargo run --release --locked -- abc.txt empty.txt
)
Espere estas duas linhas, em qualquer ordem. O arquivo de teste abc.txt contém três bytes, sem quebra
de linha:
ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad abc.txt
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 empty.txt
Compare os digests com uma implementação independente:
sha256sum abc.txt empty.txt
Para repetir a operação de hash, reutilize os arquivos:
cargo run --release --locked -- abc.txt empty.txt
O comando apenas lê suas entradas e imprime no stdout. O Cargo pode recompilar o executável em
target/release/; ele não substitui seus arquivos de entrada. Passe seus próprios caminhos de arquivo depois de
--, colocando entre aspas os caminhos que contêm espaços. Os caminhos impressos servem apenas para
exibição: este exemplo não implementa o escape de nomes de arquivo nem o modo de verificação de
checksum do sha256sum.
Comparar quantidades de workers no seu armazenamento
Depois que o build de release existir, cronometre o executável diretamente para que a compilação
fique de fora. O Rayon 1.12 usa RAYON_NUM_THREADS
para definir a quantidade 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
Esses arquivos de teste minúsculos verificam os comandos, não o desempenho. Substitua os caminhos dos dois comandos pelo mesmo lote representativo antes de comparar os tempos decorridos. Repita as execuções e alterne a ordem delas: o cache de arquivos do sistema operacional pode deixar uma execução posterior mais rápida. Mais workers podem ajudar quando o cálculo do hash é o gargalo, ou deixar tudo mais lento quando leituras simultâneas disputam a largura de banda do armazenamento. Use a quantidade de workers que ajuda na sua carga de trabalho; não há promessa de ganho de velocidade.
Comparar com uma referência confiável
Para verificar um download, compare o digest com o SHA-256 de referência fornecido pelo publicador, obtido de uma fonte confiável. Um checksum sozinho não autentica um arquivo se alguém puder substituir tanto o arquivo quanto a referência dele. Por exemplo, o Ubuntu verifica a assinatura do seu arquivo de checksums antes de verificar o download.
Mantenha os arquivos inalterados enquanto calcula o hash deles: este programa lê um fluxo de dados, então não consegue fornecer um snapshot consistente de um arquivo que outro processo esteja modificando.
