libchm
| Language | Rust (1.87+) |
|---|---|
| License | MIT |
| Repository | github.com/trypsynth/libchm |
| Documentation | docs.rs/libchm |
| Depends on | nothing (own LZX decoder, no unsafe code) |
| Read / write | yes / no |
libchm opens a CHM file, lists its directory, and reads entries, decompressing the LZX section as needed. The API is small: ChmFile::open, entries, entries_in, find, read and read_path. Each Entry has its path, uncompressed length, kind (file or directory) and category (normal, special or metadata).
CHM files often come from untrusted sources; libchm checks every header value that drives an allocation, index or division, and reports bad files as ChmError. The crate is new (2025) and at version 0.x. A Python wrapper, pychmrs, is built on it.
Installation#
Shell
cargo add libchm
Basic usage: list files and read a page#
Rust
use libchm::{ChmFile, EntrySel};
fn main() -> libchm::Result<()> {
let mut chm = ChmFile::open("help.chm")?;
// ordinary content files, without directories and internal metadata
for entry in chm.entries(EntrySel::NORMAL | EntrySel::FILES)? {
println!("{:>8} {}", entry.length, entry.path);
}
let html = chm.read_path("/index.html")?; // case-insensitive, absolute path
println!("{}", String::from_utf8_lossy(&html));
Ok(())
}
How to check a file before reading it#
find returns the entry’s metadata without decompressing it:
Rust
let entry = chm.find("/index.html")?;
println!("{} bytes, compressed: {}", entry.length, entry.is_compressed());
let bytes = chm.read(&entry)?;
How to extract all files from a CHM#
Rust
use libchm::{ChmFile, EntrySel};
use std::{fs, path::Path};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut chm = ChmFile::open("help.chm")?;
let out = Path::new("out");
for entry in chm.entries(EntrySel::NORMAL | EntrySel::FILES)? {
let rel = entry.path.trim_start_matches('/');
if rel.split('/').any(|part| part == "..") {
continue; // skip paths that escape the output folder
}
let dest = out.join(rel);
if let Some(parent) = dest.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&dest, chm.read(&entry)?)?;
}
Ok(())
}
How to list one folder or the internal files#
Rust
// everything under /images
let images = chm.entries_in("/images", EntrySel::NORMAL | EntrySel::FILES)?;
// internal files such as /#SYSTEM, /#TOPICS, /$FIftiMain
let special = chm.entries(EntrySel::SPECIAL | EntrySel::FILES)?;
EntrySel must name both a category (NORMAL, SPECIAL, META) and a kind (FILES, DIRS); EntrySel::ALL selects everything.
Tips#
- Find the table of contents by looking for an entry whose path ends in
.hhc(the index ends in.hhk), then parse it as HTML. - Text inside a CHM is usually in a Windows code page, not UTF-8; use a crate like
encoding_rsto decode pages.