All formats

tar

LanguageRust
LicenseMIT or Apache-2.0
Repositorygithub.com/composefs/tar-rs
Documentationdocs.rs/tar
Depends onpure Rust (filetime, libc/xattr on Unix)
Read / writeyes / yes (ustar, GNU, pax extensions)

The tar crate reads and writes TAR archives as streams. Archive::new(reader).entries() iterates entries in order; each Entry implements Read for its data and has a header() with the type and size. Builder::new(writer) writes archives; append_path_with_name adds a file from disk under a new name. It’s the TAR library used by Cargo itself.

Reading is sequential. For random access in an uncompressed CBT, record entry.raw_file_position() during a first pass and seek there later.

Installation#

Shell
cargo add tar

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 (including macOS ._ files), and a natural, case-insensitive compare.

Rust
use std::fs::File;

fn list_pages(path: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let mut archive = tar::Archive::new(File::open(path)?);
    let mut pages = Vec::new();
    for entry in archive.entries()? {
        let entry = entry?;
        let name = entry.path()?.to_string_lossy().into_owned();
        if entry.header().entry_type().is_file() && is_page(&name) {
            pages.push(name);
        }
    }
    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.cbt>")?;
    for (i, name) in list_pages(&path)?.iter().enumerate() {
        println!("{:3} {}", i + 1, name);
    }
    Ok(())
}

How to read pages into memory#

Rust
use std::collections::HashMap;
use std::io::Read;

fn read_pages(path: &str) -> Result<Vec<(String, Vec<u8>)>, Box<dyn std::error::Error>> {
    let mut archive = tar::Archive::new(File::open(path)?);
    let mut data = HashMap::new();
    for entry in archive.entries()? {
        let mut entry = entry?;
        let name = entry.path()?.to_string_lossy().into_owned();
        if entry.header().entry_type().is_file() && is_page(&name) {
            let mut buf = Vec::with_capacity(entry.size() as usize);
            entry.read_to_end(&mut buf)?;
            data.insert(name, buf);
        }
    }
    let mut pages: Vec<(String, Vec<u8>)> = data.into_iter().collect();
    pages.sort_by(|a, b| natural_cmp(&a.0, &b.0));
    Ok(pages)
}

How to extract the cover#

Rust
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 archive = tar::Archive::new(File::open(path)?);
    for entry in archive.entries()? {
        let mut entry = entry?;
        if entry.path()?.to_string_lossy() == first {
            let ext = first.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
            entry.unpack(format!("cover.{ext}"))?;
            return Ok(());
        }
    }
    Err("cover not found".into())
}

How to read ComicInfo.xml#

Rust
fn print_comic_info(path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut archive = tar::Archive::new(File::open(path)?);
    for entry in archive.entries()? {
        let mut entry = entry?;
        if !entry.path()?.to_string_lossy().eq_ignore_ascii_case("ComicInfo.xml") {
            continue;
        }
        let mut xml = String::new();
        entry.read_to_string(&mut xml)?;
        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"));
        for p in root.descendants().filter(|n| n.has_tag_name("Page")) {
            if p.attribute("Type").unwrap_or("").contains("FrontCover") {
                println!("cover is page index {}", p.attribute("Image").unwrap_or("0"));
            }
        }
        break;
    }
    Ok(())
}

How to create a CBT from a folder of images#

Rust
fn make_cbt(dir: &str, out: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut files: Vec<String> = std::fs::read_dir(dir)?
        .filter_map(|e| e.ok())
        .filter(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false))
        .filter_map(|e| e.file_name().into_string().ok())
        .filter(|n| is_page(n))
        .collect();
    files.sort_by(|a, b| natural_cmp(a, b));

    let mut builder = tar::Builder::new(File::create(out)?);
    for (i, name) in files.iter().enumerate() {
        let ext = name.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
        builder.append_path_with_name(format!("{dir}/{name}"), format!("{:03}.{ext}", i + 1))?;
    }
    let info = format!("{dir}/ComicInfo.xml");
    if std::path::Path::new(&info).exists() {
        builder.append_path_with_name(&info, "ComicInfo.xml")?;
    }
    builder.finish()?; // writes the end-of-archive blocks
    Ok(())
}

Tips#