How do I work with ZIP archives in Rust?
Walkthrough
The zip crate provides comprehensive support for reading and writing ZIP archives in Rust. ZIP is one of the most widely used archive formats, supporting multiple compression methods including Deflate (the most common), Bzip2, and stored (uncompressed). The crate handles the complexities of the ZIP format, including central directory structures, file headers, CRC checksums, and various encoding issues.
Key concepts:
- ZipWriter — creates new ZIP archives with files and directories
- ZipArchive — reads existing ZIP archives and extracts files
- FileOptions — configures compression method and level for each file
- ZipFile — represents a single file within an archive for reading
- Compression methods — Stored (no compression), Deflate, Bzip2, Zstd
Code Example
# Cargo.toml
[dependencies]
zip = "0.6"use std::io::prelude::*;
use std::fs::File;
use zip::ZipWriter;
use zip::write::FileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("archive.zip")?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default().compression_method(zip::CompressionMethod::Deflated);
zip.start_file("hello.txt", options)?;
zip.write_all(b"Hello, World!")?;
zip.finish()?;
println!("Created archive.zip");
Ok(())
}Creating ZIP Archives
use std::io::prelude::*;
use std::fs::File;
use std::path::Path;
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("example.zip")?;
let mut zip = ZipWriter::new(file);
// Default options with Deflate compression
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(6)); // 0-9, higher = smaller but slower
// Add a simple text file
zip.start_file("readme.txt", options)?;
zip.write_all(b"This is a readme file.\nIt contains multiple lines.")?;
// Add another file
zip.start_file("data/numbers.txt", options)?;
zip.write_all(b"1\n2\n3\n4\n5")?;
// Add a directory entry
zip.add_directory("data/", options)?;
// Add a file with no compression
let stored_options = FileOptions::default()
.compression_method(CompressionMethod::Stored);
zip.start_file("raw.bin", stored_options)?;
zip.write_all(&[0, 1, 2, 3, 4, 5])?;
// Finish writing (important!)
zip.finish()?;
println!("Created example.zip");
Ok(())
}Reading ZIP Archives
use std::fs::File;
use std::io::prelude::*;
use zip::ZipArchive;
fn main() -> std::io::Result<()> {
let file = File::open("example.zip")?;
let mut archive = ZipArchive::new(file)?;
// List all files in the archive
println!("Files in archive:");
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {} ({} bytes)", file.name(), file.size());
}
// Read a 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);
Ok(())
}Extracting Files
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::Path;
use zip::ZipArchive;
fn extract_archive(archive_path: &str, dest_dir: &str) -> std::io::Result<()> {
let file = File::open(archive_path)?;
let mut archive = ZipArchive::new(file)?;
let dest_path = Path::new(dest_dir);
fs::create_dir_all(dest_path)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let file_path = dest_path.join(file.name());
if file.name().ends_with('/') {
// It's a directory
fs::create_dir_all(&file_path)?;
println!("Created directory: {}", file_path.display());
} else {
// It's a file
if let Some(parent) = file_path.parent() {
fs::create_dir_all(parent)?;
}
let mut outfile = File::create(&file_path)?;
std::io::copy(&mut file, &mut outfile)?;
println!("Extracted: {}", file_path.display());
}
}
Ok(())
}
fn main() -> std::io::Result<()> {
extract_archive("example.zip", "extracted")?;
Ok(())
}Adding Files from Disk
use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use walkdir::WalkDir;
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
// Note: Add walkdir = "2.4" to Cargo.toml
fn create_zip_from_directory(source_dir: &str, output_path: &str) -> std::io::Result<()> {
let file = File::create(output_path)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(6));
let source_path = Path::new(source_dir);
for entry in WalkDir::new(source_dir)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path();
let name = path.strip_prefix(source_path).unwrap().to_str().unwrap();
if path.is_file() {
zip.start_file(name, options)?;
let mut f = File::open(path)?;
std::io::copy(&mut f, &mut zip)?;
println!("Added: {}", name);
} else if !name.is_empty() {
zip.add_directory(&format!("{}/", name), options)?;
println!("Added directory: {}/", name);
}
}
zip.finish()?;
println!("Created {}", output_path);
Ok(())
}
fn main() -> std::io::Result<()> {
// Create a test directory first
std::fs::create_dir_all("test_dir/subdir")?;
std::fs::write("test_dir/file1.txt", "Content of file 1")?;
std::fs::write("test_dir/subdir/file2.txt", "Content of file 2")?;
create_zip_from_directory("test_dir", "backup.zip")?;
Ok(())
}File Metadata and Options
use std::io::prelude::*;
use std::fs::File;
use zip::{ZipWriter, ZipArchive, CompressionMethod};
use zip::write::FileOptions;
use zip::DateTime;
fn main() -> std::io::Result<()> {
let file = File::create("metadata.zip")?;
let mut zip = ZipWriter::new(file);
// Set various file options
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(9)) // Maximum compression
.last_modified_time(DateTime::from_date_and_time(2024, 1, 15, 10, 30, 0)?)
.unix_permissions(0o644) // rw-r--r--
.large_file(false); // Use ZIP64 for large files
zip.start_file("document.txt", options)?;
zip.write_all(b"Document with metadata")?;
// File with password (AES encryption requires feature flag)
// Note: Basic ZipCrypto is supported by default
let encrypted_options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.with_deprecated_encryption(b"secret_password");
zip.start_file("secret.txt", encrypted_options)?;
zip.write_all(b"Encrypted content")?;
zip.finish()?;
// Read back metadata
let file = File::open("metadata.zip")?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!("File: {}", file.name());
println!(" Size: {} bytes", file.size());
println!(" Compressed: {} bytes", file.compressed_size());
println!(" Compression: {:?}", file.compression());
println!(" Modified: {:?}", file.last_modified());
println!(" Encrypted: {}", file.encrypted());
}
Ok(())
}Compression Methods
use std::io::prelude::*;
use std::fs::File;
use zip::{ZipWriter, ZipArchive, CompressionMethod};
use zip::write::FileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("compression_test.zip")?;
let mut zip = ZipWriter::new(file);
let data = b"This is some repeated text that will compress well. "
.repeat(100);
// Stored (no compression)
let stored_options = FileOptions::default()
.compression_method(CompressionMethod::Stored);
zip.start_file("stored.txt", stored_options)?;
zip.write_all(&data)?;
// Deflate (standard)
let deflate_options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(6));
zip.start_file("deflated.txt", deflate_options)?;
zip.write_all(&data)?;
// Deflate with maximum compression
let deflate_max_options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(9));
zip.start_file("deflated_max.txt", deflate_max_options)?;
zip.write_all(&data)?;
// Bzip2 (requires feature flag)
// Add to Cargo.toml: zip = { version = "0.6", features = ["bzip2"] }
#[cfg(feature = "bzip2")]
{
let bzip2_options = FileOptions::default()
.compression_method(CompressionMethod::Bzip2);
zip.start_file("bzip2.txt", bzip2_options)?;
zip.write_all(&data)?;
}
// Zstd (requires feature flag)
// Add to Cargo.toml: zip = { version = "0.6", features = ["zstd"] }
#[cfg(feature = "zstd")]
{
let zstd_options = FileOptions::default()
.compression_method(CompressionMethod::Zstd);
zip.start_file("zstd.txt", zstd_options)?;
zip.write_all(&data)?;
}
zip.finish()?;
// Compare sizes
let file = File::open("compression_test.zip")?;
let archive = ZipArchive::new(file)?;
println!("Compression comparison (original: {} bytes):", data.len());
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {}: {} bytes ({:.1}% of original)",
file.name(),
file.compressed_size(),
(file.compressed_size() as f64 / data.len() as f64) * 100.0
);
}
Ok(())
}Streaming ZIP Creation
use std::io::prelude::*;
use std::fs::File;
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
// Writing ZIP to a Vec (in memory)
fn create_zip_in_memory() -> Vec<u8> {
let mut buffer = Vec::new();
{
let mut cursor = std::io::Cursor::new(&mut buffer);
let mut zip = ZipWriter::new(&mut cursor);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("file1.txt", options).unwrap();
zip.write_all(b"Content 1").unwrap();
zip.start_file("file2.txt", options).unwrap();
zip.write_all(b"Content 2").unwrap();
zip.finish().unwrap();
}
buffer
}
// Reading ZIP from memory
fn read_zip_from_memory(data: &[u8]) -> zip::ZipArchive<std::io::Cursor<&[u8]>> {
let cursor = std::io::Cursor::new(data);
ZipArchive::new(cursor).unwrap()
}
fn main() -> std::io::Result<()> {
// Create in memory
let zip_data = create_zip_in_memory();
println!("ZIP size in memory: {} bytes", zip_data.len());
// Read from memory
let mut archive = read_zip_from_memory(&zip_data);
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
println!("{}: {}", file.name(), contents);
}
Ok(())
}Appending to Existing Archives
use std::fs::{self, File, OpenOptions};
use std::io::prelude::*;
use std::path::Path;
use zip::{ZipWriter, ZipArchive, CompressionMethod};
use zip::write::FileOptions;
fn append_to_archive(archive_path: &str, filename: &str, content: &[u8]) -> std::io::Result<()> {
// Read existing archive
let file = File::open(archive_path)?;
let mut existing = ZipArchive::new(file)?;
// Create new archive
let temp_path = format!("{}.tmp", archive_path);
let temp_file = File::create(&temp_path)?;
let mut new_zip = ZipWriter::new(temp_file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated);
// Copy existing files
for i in 0..existing.len() {
let mut file = existing.by_index(i)?;
let name = file.name().to_string();
if name != filename {
new_zip.start_file(&name, options.clone())?;
std::io::copy(&mut file, &mut new_zip)?;
}
}
// Add new file
new_zip.start_file(filename, options)?;
new_zip.write_all(content)?;
new_zip.finish()?;
// Replace original with new
fs::rename(&temp_path, archive_path)?;
Ok(())
}
fn main() -> std::io::Result<()> {
// Create initial archive
let file = File::create("append.zip")?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("original.txt", options.clone())?;
zip.write_all(b"Original content")?;
zip.finish()?;
// Append new file
append_to_archive("append.zip", "added.txt", b"Added content")?;
// Verify
let file = File::open("append.zip")?;
let archive = ZipArchive::new(file)?;
println!("Files in archive:");
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {}", file.name());
}
Ok(())
}Reading Encrypted Files
use std::fs::File;
use std::io::prelude::*;
use zip::ZipArchive;
fn main() -> std::io::Result<()> {
// First create an encrypted archive
{
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
let file = File::create("encrypted.zip")?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.with_deprecated_encryption(b"my_password");
zip.start_file("secret.txt", options)?;
zip.write_all(b"This is secret content")?;
zip.finish()?;
}
// Read encrypted archive
let file = File::open("encrypted.zip")?;
let mut archive = ZipArchive::new(file)?;
// Try without password (fails)
// let mut file = archive.by_name("secret.txt")?; // Error: password required
// Read with password
let mut file = archive.by_name_decrypt("secret.txt", b"my_password")?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
println!("Decrypted content: {}", contents);
Ok(())
}Working with ZIP File Information
use std::fs::File;
use std::io::prelude::*;
use zip::ZipArchive;
fn print_zip_info(archive_path: &str) -> std::io::Result<()> {
let file = File::open(archive_path)?;
let archive = ZipArchive::new(file)?;
println!("Archive: {}", archive_path);
println!("Number of files: {}", archive.len());
println!();
let mut total_size = 0u64;
let mut total_compressed = 0u64;
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!("File: {}", file.name());
println!(" Size: {} bytes", file.size());
println!(" Compressed size: {} bytes", file.compressed_size());
println!(" Compression method: {:?}", file.compression());
println!(" Modified: {:?}", file.last_modified());
println!(" Is directory: {}", file.is_dir());
println!(" Is file: {}", file.is_file());
println!(" Encrypted: {}", file.encrypted());
// Check for comments
if !file.comment().is_empty() {
println!(" Comment: {}", file.comment());
}
total_size += file.size();
total_compressed += file.compressed_size();
println!();
}
println!("Total:");
println!(" Uncompressed: {} bytes", total_size);
println!(" Compressed: {} bytes", total_compressed);
println!(" Ratio: {:.1}%",
(total_compressed as f64 / total_size as f64) * 100.0
);
Ok(())
}
fn main() -> std::io::Result<()> {
// Create a test archive first
{
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
let file = File::create("info_test.zip")?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated);
zip.start_file("file1.txt", options.clone())?;
zip.write_all(b"Content of file 1")?;
zip.add_directory("subdir/", options.clone())?;
zip.start_file("subdir/file2.txt", options)?;
zip.write_all(b"Content of file 2 in subdir")?;
zip.finish()?;
}
print_zip_info("info_test.zip")?;
Ok(())
}Real-World Example: Backup Tool
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::{Path, PathBuf};
use chrono::Local;
use zip::{ZipWriter, ZipArchive, CompressionMethod};
use zip::write::FileOptions;
// Note: Add chrono = "0.4" to Cargo.toml
struct BackupConfig {
source_dir: PathBuf,
backup_file: PathBuf,
compression_level: Option<i32>,
exclude_patterns: Vec<String>,
}
fn should_exclude(path: &Path, patterns: &[String]) -> bool {
let path_str = path.to_string_lossy();
patterns.iter().any(|p| path_str.contains(p))
}
fn create_backup(config: &BackupConfig) -> std::io::Result<()> {
let file = File::create(&config.backup_file)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(config.compression_level);
let mut file_count = 0;
let mut total_bytes = 0u64;
fn add_dir<P: AsRef<Path>>(
zip: &mut ZipWriter<File>,
dir: P,
base: &Path,
options: FileOptions,
exclude: &[String],
stats: &mut (usize, u64),
) -> std::io::Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let relative = path.strip_prefix(base).unwrap();
if should_exclude(&path, exclude) {
continue;
}
if path.is_dir() {
let name = relative.to_str().unwrap();
zip.add_directory(&format!("{}/", name), options.clone())?;
add_dir(zip, &path, base, options.clone(), exclude, stats)?;
} else {
let name = relative.to_str().unwrap();
zip.start_file(name, options.clone())?;
let mut file = File::open(&path)?;
let bytes = std::io::copy(&mut file, zip)?;
stats.0 += 1;
stats.1 += bytes;
}
}
Ok(())
}
let mut stats = (0, 0u64);
add_dir(
&mut zip,
&config.source_dir,
&config.source_dir,
options,
&config.exclude_patterns,
&mut stats,
)?;
zip.finish()?;
file_count = stats.0;
total_bytes = stats.1;
let backup_size = fs::metadata(&config.backup_file)?.len();
println!("Backup created: {}", config.backup_file.display());
println!(" Files: {}", file_count);
println!(" Original size: {} bytes", total_bytes);
println!(" Backup size: {} bytes", backup_size);
println!(" Compression ratio: {:.1}%",
(backup_size as f64 / total_bytes as f64) * 100.0
);
Ok(())
}
fn restore_backup(backup_file: &str, dest_dir: &str) -> std::io::Result<()> {
let file = File::open(backup_file)?;
let mut archive = ZipArchive::new(file)?;
let dest = Path::new(dest_dir);
fs::create_dir_all(dest)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = dest.join(file.name());
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
fs::create_dir_all(p)?;
}
let mut outfile = File::create(&outpath)?;
std::io::copy(&mut file, &mut outfile)?;
}
}
println!("Restored {} files to {}", archive.len(), dest_dir);
Ok(())
}
fn main() -> std::io::Result<()> {
// Create test directory
fs::create_dir_all("backup_test/subdir")?;
fs::write("backup_test/file1.txt", "Important data")?;
fs::write("backup_test/subdir/file2.txt", "More important data")?;
let config = BackupConfig {
source_dir: PathBuf::from("backup_test"),
backup_file: PathBuf::from(format!("backup_{}.zip", Local::now().format("%Y%m%d_%H%M%S"))),
compression_level: Some(9),
exclude_patterns: vec![".git".to_string(), "target".to_string()],
};
create_backup(&config)?;
Ok(())
}Real-World Example: Log Rotator
use std::fs::{self, File};
use std::io::prelude::*;
use std::path::PathBuf;
use zip::{ZipWriter, CompressionMethod};
use zip::write::FileOptions;
fn rotate_logs(log_dir: &str, archive_name: &str) -> std::io::Result<()> {
let log_path = PathBuf::from(log_dir);
let archive_path = log_path.join(archive_name);
let file = File::create(&archive_path)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated)
.compression_level(Some(6));
let mut archived_count = 0;
for entry in fs::read_dir(&log_path)? {
let entry = entry?;
let path = entry.path();
// Skip directories and the archive itself
if path.is_dir() || path.extension().map(|e| e == "zip").unwrap_or(false) {
continue;
}
let filename = path.file_name().unwrap().to_str().unwrap();
// Add to archive
zip.start_file(filename, options)?;
let mut log_file = File::open(&path)?;
std::io::copy(&mut log_file, &mut zip)?;
// Delete original
fs::remove_file(&path)?;
archived_count += 1;
println!("Archived: {}", filename);
}
zip.finish()?;
println!("Archived {} log files to {}", archived_count, archive_path.display());
Ok(())
}
fn main() -> std::io::Result<()> {
// Create test logs
fs::create_dir_all("logs")?;
fs::write("logs/app.log", "2024-01-15 Log entry 1\n2024-01-15 Log entry 2")?;
fs::write("logs/error.log", "2024-01-15 Error: something failed")?;
fs::write("logs/access.log", "2024-01-15 GET /index.html")?;
rotate_logs("logs", "logs_archive.zip")?;
Ok(())
}Error Handling
use std::fs::File;
use std::io::prelude::*;
use zip::ZipArchive;
use zip::result::ZipError;
fn read_zip_safely(path: &str) -> Result<Vec<(String, Vec<u8>)>, Box<dyn std::error::Error>> {
let file = File::open(path).map_err(|e| format!("Failed to open file: {}", e))?;
let mut archive = ZipArchive::new(file)
.map_err(|e| format!("Invalid ZIP archive: {}", e))?;
let mut files = Vec::new();
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let name = file.name().to_string();
// Skip directories
if file.is_dir() {
continue;
}
// Check for reasonable file size
if file.size() > 100_000_000 { // 100 MB
return Err(format!("File {} is too large", name).into());
}
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
files.push((name, contents));
}
Ok(files)
}
fn extract_specific_file(path: &str, filename: &str) -> Result<Vec<u8>, ZipError> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
let mut file = archive.by_name(filename)?;
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
Ok(contents)
}
fn main() {
match read_zip_safely("nonexistent.zip") {
Ok(files) => {
for (name, contents) in files {
println!("{}: {} bytes", name, contents.len());
}
}
Err(e) => println!("Error: {}", e),
}
}Unicode File Names
use std::fs::File;
use std::io::prelude::*;
use zip::{ZipWriter, ZipArchive, CompressionMethod};
use zip::write::FileOptions;
fn main() -> std::io::Result<()> {
let file = File::create("unicode.zip")?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default()
.compression_method(CompressionMethod::Deflated);
// Files with Unicode names
zip.start_file("文档/说明.txt", options.clone())?;
zip.write_all("中文内容".as_bytes())?;
zip.start_file("ドキュメント/説明.txt", options.clone())?;
zip.write_all("日本語コンテンツ".as_bytes())?;
zip.start_file("документ/описание.txt", options.clone())?;
zip.write_all("Русский контент".as_bytes())?;
zip.start_file("emoji/😀.txt", options)?;
zip.write_all("Emoji support! 🎉".as_bytes())?;
zip.finish()?;
// Read back
let file = File::open("unicode.zip")?;
let archive = ZipArchive::new(file)?;
println!("Files with Unicode names:");
for i in 0..archive.len() {
let file = archive.by_index(i)?;
println!(" {}", file.name());
}
Ok(())
}Summary
- Use
ZipWriterto create archives,ZipArchiveto read them FileOptionsconfigures compression method and level per file- Common compression methods:
Deflated(standard),Stored(none),Bzip2,Zstd - Always call
finish()onZipWriterto finalize the archive - Use
by_name()orby_index()to access files in an archive is_dir()andis_file()distinguish files from directorieswith_deprecated_encryption()adds basic password protection (ZipCrypto)- For AES encryption, enable the
aes-cryptofeature - Handle large files with
.large_file(true)for ZIP64 support - Unicode filenames are supported automatically
- Set
unix_permissions()to preserve file permissions - Use
last_modified_time()to set timestamps - Streaming works with any
Writeimplementation (includingVec<u8>) - Error handling covers invalid archives, missing files, and password requirements
