Para fazer um download de arquivo em Zig com segurança, não basta executar um GET e salvar qualquer resposta recebida. O fluxo robusto é: usar std.http.Client, aceitar apenas o status esperado, impor um limite de tamanho, calcular o SHA-256 dos bytes, gravar primeiro em um arquivo temporário e renomeá-lo somente depois que todas as validações passarem.
Esse padrão evita três problemas comuns: salvar uma página de erro como se fosse o artefato, consumir memória sem limite e deixar um arquivo final incompleto após uma queda. O exemplo deste guia é adequado para binários, arquivos de configuração, modelos pequenos, pacotes e assets com tamanho máximo conhecido. Para downloads grandes, a arquitetura continua igual, mas a leitura e o hash devem acontecer em streaming.
Como std.http pode mudar entre versões anteriores ao Zig 1.0, confira zig version e a definição de Client.fetch instalada no projeto. O contrato operacional — limite, status, checksum, temporário e rename — é mais durável que qualquer assinatura específica da biblioteca padrão.
Resposta rápida
| Necessidade | Decisão recomendada |
|---|---|
| Baixar arquivo pequeno ou médio | std.http.Client.fetch com resposta em buffer limitado |
| Evitar salvar uma resposta 404/500 | Validar result.status antes de escrever |
| Confirmar integridade | Comparar SHA-256 esperado com o hash calculado |
| Evitar arquivo final parcial | Gravar em .part e renomear após sucesso |
| Repetir o download com segurança | Remover ou substituir apenas o temporário |
| Baixar arquivo grande | Ler em blocos, atualizar o hash e escrever no disco em streaming |
| URL externa ou redirecionamento | Definir política explícita de hosts, HTTPS e redirects |
Se você ainda está começando com requisições, leia antes o tutorial de std.http.Client, GET, POST e headers. Para o caminho inverso, veja como fazer upload multipart em Zig.
Por que escrever em arquivo temporário?
Imagine que o programa escreva diretamente em zig.tar.xz. A conexão cai depois de 60% do download, mas o arquivo continua existindo com o nome definitivo. Outro processo pode encontrá-lo, tentar extrair o pacote e produzir um erro que parece ser de formato, não de rede.
Com um temporário, o estado fica explícito:
zig.tar.xz.part # download em andamento ou interrompido
zig.tar.xz # arquivo validado e publicado
O nome final só aparece depois de quatro condições:
- a requisição HTTP terminou;
- o status pertence ao conjunto aceito;
- o tamanho está dentro do limite;
- o checksum corresponde ao valor esperado, quando fornecido.
No mesmo filesystem, rename normalmente oferece a publicação atômica que queremos: consumidores observam o arquivo antigo ou o novo, não uma sequência de bytes parcialmente escrita. Isso não transforma todo filesystem em banco transacional, mas elimina a janela mais perigosa do fluxo comum.
Exemplo completo para arquivo com limite conhecido
O código abaixo baixa a resposta para memória, valida o SHA-256 opcional e só então grava o arquivo. Essa abordagem é simples e previsível quando o limite é pequeno o suficiente para o processo.
const std = @import("std");
const DownloadError = error{
StatusHttpInesperado,
RespostaVazia,
ArquivoGrandeDemais,
ChecksumInvalido,
};
fn sha256Hex(bytes: []const u8, output: *[64]u8) []const u8 {
var digest: [32]u8 = undefined;
std.crypto.hash.sha2.Sha256.hash(bytes, &digest, .{});
return std.fmt.bufPrint(output, "{s}", .{
std.fmt.fmtSliceHexLower(&digest),
}) catch unreachable;
}
fn writeAtomically(
destination: []const u8,
bytes: []const u8,
) !void {
var temp_name_buf: [std.fs.max_path_bytes]u8 = undefined;
const temp_name = try std.fmt.bufPrint(
&temp_name_buf,
"{s}.part",
.{destination},
);
const file = try std.fs.cwd().createFile(temp_name, .{
.truncate = true,
});
errdefer std.fs.cwd().deleteFile(temp_name) catch {};
defer file.close();
try file.writeAll(bytes);
try file.sync();
std.fs.cwd().rename(temp_name, destination) catch |err| {
// Em alguns ambientes, substituir um destino existente exige
// uma política própria. Não apague o arquivo antigo por surpresa.
return err;
};
}
fn download(
allocator: std.mem.Allocator,
url: []const u8,
destination: []const u8,
expected_sha256: ?[]const u8,
max_bytes: usize,
) !void {
var client = std.http.Client{ .allocator = allocator };
defer client.deinit();
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
const result = try client.fetch(.{
.location = .{ .url = url },
.extra_headers = &.{
.{ .name = "Accept", .value = "application/octet-stream" },
.{ .name = "User-Agent", .value = "zig-downloader/1.0" },
},
.response_storage = .{ .dynamic = &body },
.max_redirects = 3,
});
if (result.status != .ok) {
return DownloadError.StatusHttpInesperado;
}
if (body.items.len == 0) {
return DownloadError.RespostaVazia;
}
if (body.items.len > max_bytes) {
return DownloadError.ArquivoGrandeDemais;
}
var hash_buf: [64]u8 = undefined;
const actual_sha256 = sha256Hex(body.items, &hash_buf);
if (expected_sha256) |expected| {
if (!std.ascii.eqlIgnoreCase(expected, actual_sha256)) {
return DownloadError.ChecksumInvalido;
}
}
std.debug.print("SHA-256: {s}\n", .{actual_sha256});
try writeAtomically(destination, body.items);
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
try download(
allocator,
"https://downloads.example.com/ferramenta.tar.xz",
"ferramenta.tar.xz",
null, // Em produção, prefira informar o SHA-256 esperado.
32 * 1024 * 1024,
);
}
A opção exata usada para limitar uma resposta dinâmica varia conforme o release de Zig. A comparação posterior com max_bytes detecta excesso, mas não impede que um servidor faça o buffer crescer antes dessa validação. Em código de produção, aplique o teto durante a própria leitura ou use um writer que recuse bytes depois do limite. O modo streaming descrito abaixo é o caminho mais seguro quando a origem não é totalmente controlada.
Validando status HTTP antes do body
Um servidor pode devolver uma página HTML de erro com corpo perfeitamente válido. Se o programa olhar apenas para os bytes, pode salvar isto como ferramenta.tar.xz:
<h1>404 Not Found</h1>
Por isso o status vem primeiro. Para download comum, 200 OK é o resultado esperado. Respostas 206 Partial Content só devem ser aceitas quando seu código implementa retomada com Range e valida os metadados da parte recebida.
Redirecionamentos também merecem política. Seguir até três redirects pode ser conveniente para uma CDN, mas não permita que uma URL confiável leve silenciosamente a qualquer host. Em ferramentas internas, considere validar o hostname final, exigir HTTPS e bloquear destinos privados quando a URL vier de usuário. Essa última proteção reduz risco de SSRF.
SHA-256: integridade não é autenticidade por si só
O SHA-256 responde: “estes bytes são exatamente os bytes esperados?”. Ele não responde sozinho: “quem publicou esse hash é confiável?”. Se o arquivo e o checksum forem baixados do mesmo local comprometido, um invasor pode substituir ambos.
Use o checksum esperado a partir de uma fonte controlada, por exemplo:
- valor fixado no repositório e revisado em pull request;
- manifesto de release assinado;
- metadado entregue por um canal autenticado diferente;
- configuração gerenciada pelo seu sistema de deploy.
A comparação deve acontecer antes do rename. Se o hash falhar, remova o .part, registre o hash observado sem expor dados sensíveis e mantenha o destino anterior intacto.
Para artefatos críticos, combine checksum com assinatura digital e política de origem. O guia de supply chain e releases em Zig explica por que versão, URL e hash devem fazer parte do contrato de build.
Como fazer streaming para arquivos grandes
Carregar 2 GB em um ArrayList é uma má ideia mesmo quando a máquina possui memória suficiente. O download concorre com o restante do processo, pode fragmentar memória e duplica dados durante algumas operações.
No modo streaming, mantenha um buffer pequeno e repita:
ler bloco HTTP
-> verificar teto acumulado
-> atualizar SHA-256
-> escrever bloco no arquivo .part
-> continuar até EOF
sincronizar arquivo
comparar checksum
rename para o destino final
O estado de hash incremental segue esta ideia:
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
while (try reader.read(&buffer)) |n| {
if (n == 0) break;
total += n;
if (total > max_bytes) return error.ArquivoGrandeDemais;
hasher.update(buffer[0..n]);
try file.writeAll(buffer[0..n]);
}
var digest: [32]u8 = undefined;
hasher.final(&digest);
A obtenção do reader da resposta depende da API de baixo nível de std.http.Client disponível no release usado. Isole essa parte em uma função pequena. Assim, uma mudança da stdlib não contamina validação, hash, escrita temporária e publicação do arquivo.
Retomada com Range: só implemente com validação
Retomar downloads parece simples: descubra o tamanho do .part e envie Range: bytes=N-. Mas existem armadilhas:
- o arquivo remoto pode ter mudado;
- o servidor pode ignorar
Rangee devolver200com o arquivo inteiro; - o
Content-Rangepode não começar emN; - o temporário local pode pertencer a outra URL;
- o checksum final ainda precisa ser recalculado ou continuado corretamente.
Uma implementação segura armazena metadados como URL, tamanho esperado, ETag e Last-Modified. Ao retomar, envia If-Range, exige 206 Partial Content e valida Content-Range. Se qualquer condição divergir, descarte o temporário e recomece. Para arquivos pequenos, recomeçar costuma ser mais barato e menos arriscado que manter toda essa lógica.
Nome de arquivo e diretório de destino
Não derive o destino diretamente do último segmento de uma URL controlada por usuário. Normalize o nome e imponha um diretório previamente aberto. Bloqueie:
../e caminhos absolutos;- nomes vazios ou especiais;
- caracteres de controle;
- colisões com arquivos de configuração;
- symlinks inesperados no diretório de destino.
Quando possível, abra o diretório permitido e opere com caminhos relativos a ele. A mesma preocupação aparece em projetos práticos de Zig: path traversal não é apenas um problema de servidor; um downloader também pode sobrescrever locais indevidos se aceitar nomes sem validação.
Erros que devem aparecer separadamente
Não transforme tudo em DownloadFailed. Diferencie pelo menos:
| Erro | Significado operacional |
|---|---|
| DNS, TLS ou conexão | falha de transporte |
404 | URL ou versão inexistente |
401 ou 403 | credencial ou autorização |
429 | limite do provedor |
500, 502, 503, 504 | falha possivelmente transitória |
| limite excedido | resposta maior que a política local |
| checksum inválido | bytes corrompidos ou origem inesperada |
| erro de escrita | disco cheio, permissão ou filesystem |
| rename falhou | colisão ou política de substituição |
Retry só faz sentido para parte desses erros. Não repita automaticamente um checksum inválido vindo da mesma origem dezenas de vezes. Para uma política de tentativas, backoff e falhas transitórias, veja circuit breaker, timeout e retry em Zig.
Testes que valem a pena
Use um servidor HTTP local ou fake e cubra:
200com arquivo e hash corretos;404com corpo HTML;- corpo vazio;
- resposta um byte acima do limite;
- checksum incorreto;
- conexão interrompida no meio;
- destino existente;
- falha de rename;
- redirect acima do limite;
206recebido quando retomada não está habilitada.
Depois de cada falha, verifique o filesystem: o arquivo final antigo deve continuar íntegro e o temporário não deve ser confundido com resultado válido. Testar apenas o erro retornado deixa metade do contrato sem cobertura.
Checklist de produção
- A URL usa HTTPS e possui política de redirects.
- O status HTTP é validado antes de publicar o arquivo.
- Existe limite máximo de bytes aplicado durante a leitura.
- Downloads grandes usam streaming.
- O SHA-256 esperado vem de fonte confiável.
- O arquivo é escrito primeiro em um temporário.
- O temporário é sincronizado antes do rename quando durabilidade importa.
- O destino não é derivado de caminho não confiável.
- Logs não incluem tokens, cookies ou query strings sensíveis.
- Retry possui limite e classificação de erro.
- Métricas registram bytes, duração, status e motivo da falha.
- Testes confirmam que falhas não deixam um arquivo final parcial.
Conclusão
Um downloader confiável em Zig é uma pequena pipeline de validação, não apenas uma chamada HTTP. Receba os bytes com limite, valide o status, calcule o SHA-256, escreva em .part e publique com rename somente no fim. Para arquivos grandes, troque o buffer integral por streaming, mas preserve exatamente as mesmas etapas.
Esse desenho combina com Zig porque torna explícitos os pontos que costumam ficar escondidos: memória máxima, origem, integridade, durabilidade e estado parcial. Continue pela referência de std.http.Client, pelo tutorial de File I/O em Zig e pelo guia de TLS, HTTPS e certificados para adaptar o fluxo ao seu ambiente de produção.