All formats

compress-tools

LanguageRust
LicenseMIT or Apache-2.0 (crate); libarchive is BSD-2-Clause
Repositorygithub.com/OSSystems/compress-tools-rs
Documentationdocs.rs/compress-tools
Depends onlibarchive 3.2+ installed on the system (found with pkg-config, or vcpkg on Windows MSVC)
Read / writeyes / no (7z through libarchive; also RAR, ZIP, 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 CB7, CBR, CBZ and CBT files. It has optional async support with tokio or futures.

It can’t write archives and doesn’t read encrypted 7z. For a pure Rust reader and writer without a system library, use sevenz-rust2.

Installation#

Shell
# 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.

Rust
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.cb7>")?;
    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:

Rust
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#

Rust
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 7z block only once:

Rust
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#