Crear y extraer un archivo ZIP en Rust

El crate zip escribe y lee archivos comprimidos sin depender de nada del sistema. Lo que conviene entender es que un ZIP no guarda carpetas de verdad: la jerarquía está en el nombre de cada entrada.

La dependencia

cargo add zip
[dependencies]
zip = "8.6.0"

Crear el ZIP y volver a leerlo

use std::fs::File;
use std::io::{Read, Write};
use zip::write::SimpleFileOptions;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut zip = zip::ZipWriter::new(File::create("copia.zip")?);
    let opciones = SimpleFileOptions::default();

    for (nombre, contenido) in [("notas.txt", "primera nota"), ("datos/lista.txt", "a,b,c")] {
        zip.start_file(nombre, opciones)?;
        zip.write_all(contenido.as_bytes())?;
    }
    zip.finish()?;
    println!("copia.zip creado");

    let mut archivo = zip::ZipArchive::new(File::open("copia.zip")?)?;
    println!("contiene {} entradas:", archivo.len());
    for i in 0..archivo.len() {
        let mut e = archivo.by_index(i)?;
        let mut texto = String::new();
        e.read_to_string(&mut texto)?;
        println!("  {} ({} bytes): {}", e.name(), e.size(), texto);
    }
    Ok(())
}
salidacopia.zip creado
contiene 2 entradas:
  notas.txt (12 bytes): primera nota
  datos/lista.txt (5 bytes): a,b,c

Fíjate en la segunda entrada: datos/lista.txt. No hubo que crear ninguna carpeta datos, basta con poner la barra en el nombre. Así funciona el formato, y por eso al extraer hay que crear las carpetas intermedias antes de escribir cada archivo.

finish() es obligatorio: escribe el índice central del ZIP, que va al final del archivo. Sin esa llamada el archivo queda incompleto y ningún descompresor lo abre.

Al extraer, nunca confíes en los nombres del ZIP. Una entrada puede llamarse ../../etc/algo y, si escribes donde diga el nombre sin comprobarlo, un ZIP malicioso te deja archivos fuera de la carpeta de destino. Es un problema clásico, conocido como Zip Slip. El crate ofrece enclosed_name(), que devuelve None cuando el nombre se sale de la carpeta; conviene usar ese y no name() al extraer a disco.

Opciones y variantes

Qué quieres Cómo
Añadir un archivo desde disco start_file y copiar con io::copy
Una carpeta vacía dentro del ZIP add_directory("nombre/", opciones)
Guardar sin comprimir .compression_method(CompressionMethod::Stored)
Nombre seguro al extraer e.enclosed_name()
Extraer todo de golpe archivo.extract(carpeta)
Trabajar en memoria Cursor<Vec<u8>> en vez de File

El código de esta página se compiló y ejecutó con rustc 1.96.1 y zip 8.6.0 antes de publicarla; la salida está copiada de la ejecución real. Documentación oficial: zip en docs.rs.