chmdec
| Language | C (usable from C++) |
|---|---|
| License | LGPL-2.1-or-later, like CHMLib it is derived from |
| Repository | github.com/kjk/chmdec |
| Documentation | src/chm.h |
| Depends on | nothing |
| Read / write | yes / no |
chmdec reads CHM (Compiled HTML Help) and ITSS archives: list the entries and read any of them, decompressing LZX as needed. It’s a port of CHMLib with a cleaned-up API, custom allocators, an amalgamated build, fuzzing for robustness, and a WebAssembly build for web apps. SumatraPDF uses it.
It works on a file held in memory: there is no incremental I/O, so a CHM is loaded whole. It doesn’t parse the table of contents (.hhc) or index (.hhk) for you: those are HTML files inside the archive that you read like any other entry.
Installation#
Copy dist/chm.h and dist/chm.c from the repository into your project and compile chm.c with your code:
git clone https://github.com/kjk/chmdec
cp chmdec/dist/chm.h chmdec/dist/chm.c myproject/
Basic usage: list files in a CHM#
#include "chm.h"
#include <stdio.h>
#include <stdlib.h>
static uint8_t *read_file(const char *path, size_t *len) {
FILE *f = fopen(path, "rb");
if (!f) return NULL;
fseek(f, 0, SEEK_END);
*len = (size_t)ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *d = malloc(*len);
if (d && fread(d, 1, *len, f) != *len) { free(d); d = NULL; }
fclose(f);
return d;
}
int main(void) {
size_t len;
uint8_t *data = read_file("help.chm", &len);
if (!data) return 1;
chm_ctx *ctx = chm_ctx_new(NULL, NULL, NULL, NULL);
if (!chm_open(ctx, data, len)) { /* data must stay valid until chm_close */
fprintf(stderr, "not a CHM file\n");
return 1;
}
struct chm_entry **entries = NULL;
int n = chm_get_entries(ctx, &entries); /* array is owned by ctx */
for (int i = 0; i < n; i++) {
struct chm_entry *e = entries[i];
if (e->is_file && e->is_normal)
printf("%10llu %s\n", (unsigned long long)e->length, e->path);
}
chm_close(ctx);
chm_ctx_free(ctx);
free(data);
return 0;
}
is_normal is set for content files (paths like /index.html). Entries starting with :: (is_special) and # (is_meta) are the archive’s internal metadata: #SYSTEM, #TOPICS, #STRINGS, the LZX reset table and so on.
How to extract one page#
struct chm_entry **entries = NULL;
int n = chm_get_entries(ctx, &entries);
for (int i = 0; i < n; i++) {
if (strcmp(entries[i]->path, "/index.html") != 0) continue;
uint8_t *buf = malloc(entries[i]->length);
if (buf && chm_read_entry(ctx, entries[i], buf) == (int64_t)entries[i]->length) {
fwrite(buf, 1, entries[i]->length, stdout);
}
free(buf);
break;
}
Paths in CHM files are case-insensitive in practice: links inside pages often differ in case from the stored name, so compare with a case-insensitive function when you resolve links.
How to find the table of contents#
The table of contents is an HTML file with a .hhc extension, the index a .hhk file. Their names are stored in the #SYSTEM entry, but looking for the extension works for almost every file:
for (int i = 0; i < n; i++) {
const char *p = entries[i]->path;
size_t l = strlen(p);
if (l > 4 && _stricmp(p + l - 4, ".hhc") == 0) /* strcasecmp on Linux/macOS */
printf("table of contents: %s\n", p);
}
The .hhc file is HTML with <OBJECT type="text/sitemap"> elements; each has Name and Local parameters, the title and the page path.
How to extract all files#
#include <string.h>
for (int i = 0; i < n; i++) {
struct chm_entry *e = entries[i];
if (!e->is_file || !e->is_normal || e->length == 0) continue;
uint8_t *buf = malloc(e->length);
if (buf && chm_read_entry(ctx, e, buf) == (int64_t)e->length) { /* short read = damaged file */
/* create directories for e->path under the output folder, then write buf */
printf("extracted %s\n", e->path);
}
free(buf);
}
Sanitize paths before writing: a malicious file can contain .. segments.
Tips#
- Pass an error callback to
chm_ctx_newto get diagnostics for damaged files. - Pass your own allocator callbacks to
chm_ctx_newto cap how much memory a malicious file can make the decoder use.