tar
| Language | Rust |
|---|---|
| License | MIT or Apache-2.0 |
| Repository | github.com/composefs/tar-rs |
| Documentation | docs.rs/tar |
| Depends on | pure Rust (filetime, libc/xattr on Unix) |
| Read / write | yes / 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#
Builder::mode(tar::HeaderMode::Deterministic)writes fixed timestamps and owners, so the same pages give a byte-identical CBT.entry.unpack_in(dir)refuses paths that escapedir; prefer it overunpackfor untrusted archives.- For gzip-compressed tar, wrap the reader in
flate2::read::GzDecoder; a proper CBT is not compressed.