How do I work with ZIP archives in Rust?
Walkthrough
The zip crate provides functionality to read and write ZIP archives in Rust. It supports various compression methods (Stored, Deflate, Bzip2, Zstd), handles file metadata, and allows for password-protected archives. You can create new ZIP files, extract existing ones, or modify archives in place. The crate streams data efficiently, making it suitable for both small and large archives. It's commonly used for backup systems, application packaging, and data exchange.
Key concepts:
- ZipWriter — creates new ZIP archives
- ZipArchive — reads existing ZIP archives
- Compression methods — Stored (no compression), Deflate, Bzip2, Zstd
- File options — compression method, permissions, timestamps
- Streaming — data is read/written incrementally, not loaded entirely into memory
Code Example
# Cargo.toml
[dependencies]
zip = "0.6"use std::fs::File;
use std::io::prelude::*;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("archive.zip")?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
zip.start_file("hello.txt", options)?;
zip.write_all(b"Hello, World!")?;
zip.finish()?;
Ok(())
}Creating a ZIP Archive
use std::fs::File;
use std::io::prelude::*;
use std::io::{Read, Write};
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("example.zip")?;
let mut zip = ZipWriter::new(file);
// Use deflate compression
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
// Add a text file
zip.start_file("readme.txt", options)?;
zip.write_all(b"This is a readme file.\nIt contains documentation.")?;
// Add another file
zip.start_file("data/config.json", options)?;
zip.write_all(b"{\"setting\": \"value\", \"enabled\": true}")?;
// Add an uncompressed file
zip.start_file("raw.txt", SimpleFileOptions::default())?;
zip.write_all(b"This file is not compressed.")?;
// Finish writing
zip.finish()?;
println!("Archive created successfully");
Ok(())
}Reading a ZIP Archive
use std::fs::File;
use zip::ZipArchive;
use std::io::prelude::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::open("example.zip")?;
let mut archive = ZipArchive::new(file)?;
// List files in archive
println!("Files in archive:");
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {} ({} bytes)", file.name(), file.size());
}
// Read specific file by name
let mut file = archive.by_name("readme.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
println!("\nContents of readme.txt:\n{}", contents);
// Read file by index
let mut file = archive.by_index(0)?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer)?;
println!("\nFirst file has {} bytes", buffer.len());
Ok(())
}Compression Methods
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, CompressionMethod};
use zip::write::SimpleFileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("compressed.zip")?;
let mut zip = ZipWriter::new(file);
// Stored - no compression
zip.start_file("stored.txt",
SimpleFileOptions::default().compression_method(CompressionMethod::Stored))?;
zip.write_all(b"This file has no compression.")?;
// Deflate - standard compression
zip.start_file("deflated.txt",
SimpleFileOptions::default().compression_method(CompressionMethod::Deflated))?;
zip.write_all(b"This file uses deflate compression.")?;
// Bzip2 - better compression, slower
zip.start_file("bzip2.txt",
SimpleFileOptions::default().compression_method(CompressionMethod::Bzip2))?;
zip.write_all(b"This file uses bzip2 compression.")?;
// Zstd - modern compression
zip.start_file("zstd.txt",
SimpleFileOptions::default().compression_method(CompressionMethod::Zstd))?;
zip.write_all(b"This file uses zstd compression.")?;
zip.finish()?;
println!("Archive with various compression methods created");
Ok(())
}Extracting Files
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::Path;
use zip::ZipArchive;
fn extract_all(archive_path: &str, dest_dir: &str) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(archive_path)?;
let mut archive = ZipArchive::new(file)?;
// Create destination directory
fs::create_dir_all(dest_dir)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_name = file.mangled_name();
let out_path = Path::new(dest_dir).join(file_name);
if file.name().ends_with('/') {
// It's a directory
fs::create_dir_all(&out_path)?;
} else {
// It's a file
if let Some(parent) = out_path.parent() {
fs::create_dir_all(parent)?;
}
let mut out_file = File::create(&out_path)?;
std::io::copy(&mut file, &mut out_file)?;
}
}
println!("Extracted {} files to {}", archive.len(), dest_dir);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// First create an archive for testing
let file = File::create("test_archive.zip")?;
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("file1.txt", options)?;
zip.write_all(b"Content of file 1")?;
zip.start_file("subdir/file2.txt", options)?;
zip.write_all(b"Content of file 2")?;
zip.finish()?;
// Now extract
extract_all("test_archive.zip", "output")?;
// Verify
let content = fs::read_to_string("output/file1.txt")?;
println!("Extracted content: {}", content);
Ok(())
}Adding Directories
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use walkdir::WalkDir;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
fn create_archive_from_directory(
source_dir: &str,
archive_path: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let file = File::create(archive_path)?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
let source_path = Path::new(source_dir);
for entry in WalkDir::new(source_dir) {
let entry = entry?;
let path = entry.path();
// Skip the root directory itself
if path == source_path {
continue;
}
// Get relative path
let relative = path.strip_prefix(source_path)?;
let archive_name = relative.to_string_lossy();
if path.is_dir() {
// Add directory entry (ends with /)
zip.add_directory(&format!("{}/", archive_name), options)?;
} else {
// Add file
zip.start_file(&archive_name, options)?;
let mut file = File::open(path)?;
std::io::copy(&mut file, &mut zip)?;
}
}
zip.finish()?;
println!("Archive created from {}", source_dir);
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create test directory
std::fs::create_dir_all("test_dir/subdir")?;
std::fs::write("test_dir/file1.txt", b"Content 1")?;
std::fs::write("test_dir/subdir/file2.txt", b"Content 2")?;
create_archive_from_directory("test_dir", "directory.zip")?;
// Cleanup
std::fs::remove_dir_all("test_dir")?;
Ok(())
}Password-Protected Archives
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
use zip::read::ZipFile;
fn create_encrypted_archive(path: &str, password: &str) -> std::io::Result<()> {
let file = File::create(path)?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.with_deprecated_encryption(password.as_bytes());
zip.start_file("secret.txt", options)?;
zip.write_all(b"This content is encrypted.")?;
zip.finish()?;
println!("Encrypted archive created");
Ok(())
}
fn read_encrypted_archive(path: &str, password: &str) -> Result<String, Box<dyn std::error::Error>> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
let mut encrypted_file = archive.by_name_decrypt("secret.txt", password.as_bytes())?;
let mut contents = String::new();
encrypted_file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
create_encrypted_archive("secret.zip", "mypassword")?;
let contents = read_encrypted_archive("secret.zip", "mypassword")?;
println!("Decrypted: {}", contents);
// Note: AES encryption requires the `aes-crypto` feature
Ok(())
}File Metadata and Timestamps
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::create("metadata.zip")?;
let mut zip = ZipWriter::new(file);
// Create file with metadata
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644); // rw-r--r--
zip.start_file("document.txt", options)?;
zip.write_all(b"File with metadata")?;
zip.finish()?;
// Read metadata
let file = File::open("metadata.zip")?;
let archive = ZipArchive::new(file)?;
println!("Archive contains {} files", archive.len());
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!("\nFile: {}", file.name());
println!(" Compressed size: {}", file.compressed_size());
println!(" Original size: {}", file.size());
println!(" Compression: {:?}", file.compression());
println!(" Modified: {:?}", file.last_modified());
println!(" Is directory: {}", file.is_dir());
println!(" Is file: {}", file.is_file());
}
Ok(())
}Streaming Large Files
use std::fs::File;
use std::io::{self, Read, Write};
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
fn create_large_archive() -> io::Result<()> {
let file = File::create("large.zip")?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
// Stream a large file into the archive
zip.start_file("large_file.bin", options)?;
// Write in chunks to avoid loading everything into memory
let chunk_size = 1024 * 1024; // 1MB chunks
for i in 0..10 {
let chunk = vec![i as u8; chunk_size];
zip.write_all(&chunk)?;
}
zip.finish()?;
println!("Large archive created");
Ok(())
}
fn read_large_archive() -> io::Result<()> {
let file = File::open("large.zip")?;
let mut archive = ZipArchive::new(file)?;
let mut file = archive.by_name("large_file.bin")?;
// Read in chunks
let mut buffer = [0u8; 4096];
let mut total = 0;
loop {
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 {
break;
}
total += bytes_read;
}
println!("Read {} bytes", total);
Ok(())
}
fn main() -> io::Result<()> {
create_large_archive()?;
read_large_archive()
}Appending to Existing Archives
use std::fs::File;
use std::io::prelude::*;
use std::fs::OpenOptions;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create initial archive
{
let file = File::create("append.zip")?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default();
zip.start_file("original.txt", options)?;
zip.write_all(b"Original content")?;
zip.finish()?;
}
// Read existing archive
let existing_file = File::open("append.zip")?;
let archive = ZipArchive::new(existing_file)?;
// Note: To truly append, you need to copy existing entries
// Here's a simpler approach: create new archive with old + new content
let new_file = File::create("append_new.zip")?;
let mut new_zip = ZipWriter::new(new_file);
let options = SimpleFileOptions::default();
// Copy old entries
let old_archive = ZipArchive::new(File::open("append.zip")?)?;
for i in 0..old_archive.len() {
let mut old_file = old_archive.by_index(i)?;
new_zip.start_file(old_file.name(), options)?;
std::io::copy(&mut old_file, &mut new_zip)?;
}
// Add new entry
new_zip.start_file("added.txt", options)?;
new_zip.write_all(b"Added later")?;
new_zip.finish()?;
// Replace old file
std::fs::rename("append_new.zip", "append.zip")?;
println!("Archive updated");
Ok(())
}Handling Symlinks
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
#[cfg(unix)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::create("symlinks.zip")?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default();
// Add a regular file
zip.start_file("original.txt", options)?;
zip.write_all(b"Original content")?;
// Add a symlink entry (on Unix)
#[cfg(unix)]
{
let symlink_options = SimpleFileOptions::default()
.unix_permissions(0o120777); // Symlink permissions
zip.start_file("link_to_original.txt", symlink_options)?;
// The symlink target is stored as the file content
zip.write_all(b"original.txt")?;
}
zip.finish()?;
println!("Archive with symlink created");
Ok(())
}Real-World: Backup System
use std::fs::File;
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
struct Backup {
archive_path: PathBuf,
}
impl Backup {
fn new<P: AsRef<Path>>(path: P) -> Self {
Self { archive_path: path.as_ref().to_path_buf() }
}
fn create(&self, source_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
let file = File::create(&self.archive_path)?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
self.add_directory(source_dir, source_dir, &mut zip, options)?;
zip.finish()?;
println!("Backup created: {}", self.archive_path.display());
Ok(())
}
fn add_directory<W: std::io::Write>(
&self,
dir: &Path,
base: &Path,
zip: &mut ZipWriter<W>,
options: SimpleFileOptions,
) -> Result<(), Box<dyn std::error::Error>> {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let relative = path.strip_prefix(base)?;
if path.is_dir() {
self.add_directory(&path, base, zip, options)?;
} else {
zip.start_file(relative.to_string_lossy(), options)?;
let mut file = File::open(&path)?;
std::io::copy(&mut file, zip)?;
}
}
Ok(())
}
fn restore(&self, dest_dir: &Path) -> Result<(), Box<dyn std::error::Error>> {
std::fs::create_dir_all(dest_dir)?;
let file = File::open(&self.archive_path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let out_path = dest_dir.join(file.mangled_name());
if file.name().ends_with('/') {
std::fs::create_dir_all(&out_path)?;
} else {
if let Some(parent) = out_path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut out_file = File::create(&out_path)?;
std::io::copy(&mut file, &mut out_file)?;
}
}
println!("Backup restored to {}", dest_dir.display());
Ok(())
}
fn list(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let file = File::open(&self.archive_path)?;
let archive = ZipArchive::new(file)?;
let mut files = Vec::new();
for i in 0..archive.len() {
let file = archive.by_index(i)?;
files.push(format!("{} ({} bytes)", file.name(), file.size()));
}
Ok(files)
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create test data
std::fs::create_dir_all("backup_source/subdir")?;
std::fs::write("backup_source/file1.txt", b"File 1 content")?;
std::fs::write("backup_source/subdir/file2.txt", b"File 2 content")?;
// Create backup
let backup = Backup::new("backup.zip");
backup.create(Path::new("backup_source"))?;
// List contents
println!("\nBackup contents:");
for file in backup.list()? {
println!(" {}", file);
}
// Restore
backup.restore(Path::new("backup_restore"))?;
// Cleanup
std::fs::remove_dir_all("backup_source")?;
std::fs::remove_dir_all("backup_restore")?;
std::fs::remove_file("backup.zip")?;
Ok(())
}Real-World: Configuration Archive
use std::fs::File;
use std::io::prelude::*;
use std::collections::HashMap;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
pub struct ConfigArchive {
configs: HashMap<String, String>,
}
impl ConfigArchive {
pub fn new() -> Self {
Self { configs: HashMap::new() }
}
pub fn set(&mut self, key: &str, value: &str) {
self.configs.insert(key.to_string(), value.to_string());
}
pub fn get(&self, key: &str) -> Option<&String> {
self.configs.get(key)
}
pub fn save(&self, path: &str) -> std::io::Result<()> {
let file = File::create(path)?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
for (name, content) in &self.configs {
let file_name = format!("configs/{}.json", name);
zip.start_file(&file_name, options)?;
zip.write_all(content.as_bytes())?;
}
zip.finish()?;
Ok(())
}
pub fn load(&mut self, path: &str) -> Result<(), Box<dyn std::error::Error>> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let name = file.name().to_string();
if name.starts_with("configs/") && name.ends_with(".json") {
let key = name
.strip_prefix("configs/")
.unwrap()
.strip_suffix(".json")
.unwrap();
let mut content = String::new();
file.read_to_string(&mut content)?;
self.configs.insert(key.to_string(), content);
}
}
Ok(())
}
pub fn list(&self) -> Vec<&String> {
self.configs.keys().collect()
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut config = ConfigArchive::new();
config.set("database", r"{\"host\": \"localhost\", \"port\": 5432}");
config.set("app", r"{\"name\": \"MyApp\", \"version\": \"1.0\"}");
config.save("configs.zip")?;
println!("Saved {} configs", config.list().len());
// Load into new instance
let mut loaded = ConfigArchive::new();
loaded.load("configs.zip")?;
println!("Loaded configs:");
for key in loaded.list() {
println!(" {}: {}", key, loaded.get(key).unwrap());
}
std::fs::remove_file("configs.zip")?;
Ok(())
}Real-World: Self-Extracting Archive
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, ZipArchive};
use zip::write::SimpleFileOptions;
fn create_self_extracting_archive(
source_path: &str,
output_path: &str,
) -> std::io::Result<()> {
let file = File::create(output_path)?;
let mut zip = ZipWriter::new(file);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
// Add files
zip.start_file("data/file1.txt", options)?;
zip.write_all(b"Content 1")?;
zip.start_file("data/file2.txt", options)?;
zip.write_all(b"Content 2")?;
// Add extract script (for shell)
zip.start_file("extract.sh", options.clone())?;
let script = r"#!/bin/bash
# Self-extracting archive extraction script
# The ZIP data follows this script
SCRIPT_LEN=$(wc -l < "$0")
ARCHIVE="$(dirname "$0")/archive.zip"
# Extract the ZIP portion
tail -n +$((SCRIPT_LEN + 1)) "$0" > "$ARCHIVE"
# Unzip
unzip -o "$ARCHIVE" -d "extracted"
rm "$ARCHIVE"
exit 0
";
zip.write_all(script.as_bytes())?;
zip.finish()?;
println!("Self-extracting archive created: {}", output_path);
Ok(())
}
fn main() -> std::io::Result<()> {
create_self_extracting_archive("input", "extract.sh")?;
println!("Run 'bash extract.sh' to extract");
Ok(())
}Error Handling
use std::fs::File;
use zip::ZipArchive;
use zip::result::ZipError;
fn main() {
match read_zip_contents("nonexistent.zip") {
Ok(contents) => println!("Contents: {}", contents),
Err(ZipError::Io(io_err)) => eprintln!("IO error: {}", io_err),
Err(ZipError::InvalidArchive(msg)) => eprintln!("Invalid archive: {}", msg),
Err(ZipError::UnsupportedArchive(msg)) => eprintln!("Unsupported: {}", msg),
Err(other) => eprintln!("Error: {}", other),
}
}
fn read_zip_contents(path: &str) -> Result<String, ZipError> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
let mut file = archive.by_name("missing_file.txt")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}Checking Archive Integrity
use std::fs::File;
use zip::ZipArchive;
fn verify_archive(path: &str) -> Result<bool, Box<dyn std::error::Error>> {
let file = File::open(path)?;
let archive = ZipArchive::new(file)?;
println!("Archive: {}", path);
println!("Contains {} files", archive.len());
let mut total_compressed = 0;
let mut total_original = 0;
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {}", file.name());
println!(" Compressed: {} bytes", file.compressed_size());
println!(" Original: {} bytes", file.size());
println!(" Method: {:?}", file.compression());
total_compressed += file.compressed_size();
total_original += file.size();
}
if total_original > 0 {
let ratio = 100.0 * (1.0 - (total_compressed as f64 / total_original as f64));
println!("\nCompression ratio: {:.1}%", ratio);
}
Ok(true)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create test archive
let file = File::create("verify.zip")?;
let mut zip = zip::ZipWriter::new(file);
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
zip.start_file("file1.txt", options)?;
zip.write_all(b"AAAAAAAAAAAAAAAAAAAAAAAA")?; // Highly compressible
zip.start_file("file2.txt", options)?;
zip.write_all(b"BBBBBBBBBBBBBBBBBBBBBBBB")?;
zip.finish()?;
// Verify
verify_archive("verify.zip")?;
std::fs::remove_file("verify.zip")?;
Ok(())
}Summary
ZipWriter::new(file)creates a new ZIP archiveZipArchive::new(file)opens an existing archivezip.start_file(name, options)begins writing a file entryCompressionMethod::Deflatedis standard compressionCompressionMethod::Storedstores without compressionarchive.by_name(name)retrieves a file by namearchive.by_index(i)retrieves a file by index- Use
.with_deprecated_encryption(password)for password protection .unix_permissions(mode)sets file permissions- Iterate with
archive.len()andby_index(i) - Stream large files to avoid memory issues
- Handle errors with
ZipErrorenum - Great for backup systems, configuration archives, and data exchange
