All formats

chmdec

LanguageC (usable from C++)
LicenseLGPL-2.1-or-later, like CHMLib it is derived from
Repositorygithub.com/kjk/chmdec
Documentationsrc/chm.h
Depends onnothing
Read / writeyes / 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:

Shell
git clone https://github.com/kjk/chmdec
cp chmdec/dist/chm.h chmdec/dist/chm.c myproject/

Basic usage: list files in a CHM#

C
#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#

C
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:

C
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#

C
#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#