unrar
| Language | Rust |
|---|---|
| License | MIT (crate); bundled UnRAR source under the UnRAR license (freeware, not OSI-approved) |
| Repository | github.com/muja/unrar.rs |
| Documentation | docs.rs/unrar |
| Depends on | a C++ compiler (builds the bundled UnRAR library through unrar_sys) |
| Read / write | yes / 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#
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.
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:
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#
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#
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#
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#
- Password-protected archives:
Archive::with_password(path, "secret"). - For multi-volume sets,
Archive::new(path).as_first_part()finds the first volume from any part name. header.entry().unpacked_sizeis the uncompressed size; check it beforeread()on untrusted files.