All formats

unrar

LanguageRust
LicenseMIT (crate); bundled UnRAR source under the UnRAR license (freeware, not OSI-approved)
Repositorygithub.com/muja/unrar.rs
Documentationdocs.rs/unrar
Depends ona C++ compiler (builds the bundled UnRAR library through unrar_sys)
Read / writeyes / no (all RAR versions including RAR5, solid, encrypted, multi-volume)

The unrar crate wraps RARLAB’s UnRAR library, the reference RAR decoder. It compiles UnRAR from source during the build, so there’s nothing to install at runtime, and it reads everything WinRAR creates.

The API uses the typestate pattern: an archive opened for processing alternates between “before header” and “before file” states. read_header() moves to the next entry; then you must read(), extract() or skip() it. Entries come in archive order, and there’s no random access, which matches how solid RAR archives work. open_for_listing() gives a plain iterator over headers.

Installation#

Shell
cargo add unrar

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 unrar::Archive;

fn list_pages(path: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
    let mut pages = Vec::new();
    for entry in Archive::new(path).open_for_listing()? {
        let header = entry?;
        let name = header.filename.to_string_lossy().into_owned();
        if !header.is_directory() && 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.cbr>")?;
    for (i, name) in list_pages(&path)?.iter().enumerate() {
        println!("{:3} {}", i + 1, name);
    }
    Ok(())
}

How to read all pages into memory#

Open for processing and walk the headers once, reading pages and skipping everything else:

Rust
use std::collections::HashMap;
use unrar::Archive;

fn read_pages(path: &str) -> Result<Vec<(String, Vec<u8>)>, Box<dyn std::error::Error>> {
    let mut data = HashMap::new();
    let mut archive = Archive::new(path).open_for_processing()?;
    while let Some(header) = archive.read_header()? {
        let name = header.entry().filename.to_string_lossy().into_owned();
        archive = if !header.entry().is_directory() && is_page(&name) {
            let (bytes, rest) = header.read()?;
            data.insert(name, bytes);
            rest
        } else {
            header.skip()?
        };
    }
    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 = Archive::new(path).open_for_processing()?;
    while let Some(header) = archive.read_header()? {
        if header.entry().filename.to_string_lossy() == first {
            let ext = first.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default();
            header.extract_to(format!("cover.{ext}"))?;
            return Ok(());
        }
        archive = header.skip()?;
    }
    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 = Archive::new(path).open_for_processing()?;
    while let Some(header) = archive.read_header()? {
        if header.entry().filename.to_string_lossy().eq_ignore_ascii_case("ComicInfo.xml") {
            let (bytes, _) = header.read()?;
            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"));
            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"));
                }
            }
            return Ok(());
        }
        archive = header.skip()?;
    }
    Ok(()) // no metadata
}

How to convert CBR to CBZ#

Rust
use std::io::Write;
use zip::write::SimpleFileOptions;

fn cbr_to_cbz(cbr: &str, cbz: &str) -> Result<(), Box<dyn std::error::Error>> {
    let mut zip = zip::ZipWriter::new(std::fs::File::create(cbz)?);
    let stored = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Stored);
    let mut archive = Archive::new(cbr).open_for_processing()?;
    while let Some(header) = archive.read_header()? {
        if header.entry().is_directory() {
            archive = header.skip()?;
            continue;
        }
        let name = header.entry().filename.to_string_lossy().replace('\\', "/");
        let (bytes, rest) = header.read()?;
        zip.start_file(name, stored)?;
        zip.write_all(&bytes)?;
        archive = rest;
    }
    zip.finish()?;
    Ok(())
}

Tips#