compress-tools
| Language | Rust |
|---|---|
| License | MIT or Apache-2.0 (crate); libarchive is BSD-2-Clause |
| Repository | github.com/OSSystems/compress-tools-rs |
| Documentation | docs.rs/compress-tools |
| Depends on | libarchive 3.2+ installed on the system (found with pkg-config, or vcpkg on Windows MSVC) |
| Read / write | yes / no (RAR4 and RAR5 through libarchive; also ZIP, 7z, TAR) |
compress-tools exposes a small, easy part of libarchive to Rust: list the files in an archive, extract one file to any Write, extract everything to a folder, or iterate over all entries and their data. Because libarchive detects the format from the content, the same functions read CBR, CB7, CBZ and CBT files. It has optional async support with tokio or futures.
It inherits libarchive’s RAR limits: RAR 2.9-4 and RAR5, but no password-protected archives. It can’t write archives. For every RAR feature, use the unrar crate.
Installation#
# libarchive first
sudo apt install libarchive-dev pkg-config # Debian / Ubuntu
brew install libarchive pkg-config # macOS (set PKG_CONFIG_PATH, see README)
vcpkg install libarchive # Windows MSVC
cargo add compress-tools
Basic usage: list pages in natural order#
is_page and natural_cmp are the helpers from the zip crate CBZ page: an image-extension filter that skips __MACOSX and dotfiles, and a natural, case-insensitive compare.
use compress_tools::list_archive_files;
use std::fs::File;
fn list_pages(path: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let mut pages: Vec<String> = list_archive_files(File::open(path)?)?
.into_iter()
.filter(|n| !n.ends_with('/') && is_page(n))
.collect();
pages.sort_by(|a, b| natural_cmp(a, b));
Ok(pages)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).ok_or("usage: pages <file.cbr>")?;
for (i, name) in list_pages(&path)?.iter().enumerate() {
println!("{:3} {}", i + 1, name);
}
Ok(())
}
How to extract the cover#
uncompress_archive_file writes one entry to any Write. Each call reads the archive from the start:
use compress_tools::uncompress_archive_file;
fn extract_cover(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let first = list_pages(path)?.into_iter().next().ok_or("no pages")?;
let mut data = Vec::new();
uncompress_archive_file(File::open(path)?, &mut data, &first)?;
let ext = first.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
std::fs::write(format!("cover.{ext}"), data)?;
Ok(())
}
How to read ComicInfo.xml#
fn print_comic_info(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let names = list_archive_files(File::open(path)?)?;
let Some(name) = names.iter().find(|n| n.eq_ignore_ascii_case("ComicInfo.xml")) else {
return Ok(());
};
let mut bytes = Vec::new();
uncompress_archive_file(File::open(path)?, &mut bytes, name)?;
let xml = String::from_utf8(bytes)?;
let doc = roxmltree::Document::parse(&xml)?;
let root = doc.root_element();
let get = |tag: &str| root.children().find(|n| n.has_tag_name(tag)).and_then(|n| n.text()).unwrap_or("");
println!("{} #{}: {}", get("Series"), get("Number"), get("Title"));
Ok(())
}
How to read all pages in one pass#
ArchiveIterator yields StartOfEntry, then DataChunks, then EndOfEntry for each file, in archive order. That decodes a solid RAR only once:
use compress_tools::{ArchiveContents, ArchiveIterator};
use std::collections::HashMap;
fn read_pages(path: &str) -> Result<Vec<(String, Vec<u8>)>, Box<dyn std::error::Error>> {
let mut pages = HashMap::new();
let mut current: Option<(String, Vec<u8>)> = None;
for item in ArchiveIterator::from_read(File::open(path)?)? {
match item {
ArchiveContents::StartOfEntry(name, _stat) => {
current = is_page(&name).then(|| (name, Vec::new()));
}
ArchiveContents::DataChunk(chunk) => {
if let Some((_, buf)) = current.as_mut() {
buf.extend_from_slice(&chunk);
}
}
ArchiveContents::EndOfEntry => {
if let Some((name, buf)) = current.take() {
pages.insert(name, buf);
}
}
ArchiveContents::Err(e) => return Err(e.into()),
}
}
let mut pages: Vec<(String, Vec<u8>)> = pages.into_iter().collect();
pages.sort_by(|a, b| natural_cmp(&a.0, &b.0));
Ok(pages)
}
Tips#
list_archive_entriesreturns names with uncompressed sizes; use it to reject huge entries before extracting.- Version 0.16 rejects non-archive input in the listing and extraction functions; a corrupt or non-archive file returns an error instead of a single entry named
data. - Enable the
tokio_supportorfutures_supportfeature for async versions of the functions.