A Zig library and CLI for reading Mobipocket (.mobi, .prc, .azw) e-books.

Files
README.md

zig-mobi #

A Zig library and CLI for reading Mobipocket (.mobi / .prc / .azw) e-books.

Requires Zig 0.17. For Zig 0.16, use the zig-0.16 branch (release v0.1.1), which holds the last of zig-mobi to build with it.

The library itself has no dependencies. The test suite pulls in tls.zig, used by a small build-time tool that downloads the test book over HTTPS.

Where this lives #

The repository lives in three places that carry the same history. The Forgejo instance at https://git.jcollie.dev/jeff/zig-mobi is the web-visible one:

$ git clone https://git.jcollie.dev/jeff/zig-mobi.git

it is mirrored on Tangled at https://tangled.org/jcollie.dev/zig-mobi, and it is also on the Radicle network, where the repository's identifier is

rad:z4NpiYm2hUnk8xwoJXgy7RrpH8Nw9

and rad clone rad:z4NpiYm2hUnk8xwoJXgy7RrpH8Nw9 fetches it from any node that seeds it. Any of the three is the whole project.

What it does #

  • Parses the Palm database, PalmDOC, MOBI and EXTH headers.
  • Decompresses book text: uncompressed, PalmDOC (LZ77), and HUFF/CDIC.
  • Strips the trailing data entries that text records carry, as described by the MOBI header's extra_record_data_flags.
  • Transcodes CP-1252 text to UTF-8.
  • Extracts embedded images (JPEG, PNG, GIF, BMP), skipping the non-image resource records that sit alongside them.
  • Reports the KF8 (AZW3) boundary in dual-format files so the older half can be read on its own.
  • Decrypts both Mobipocket encryption schemes (see below).

Encryption #

Mobipocket files use one of two schemes, both built on the PC1 stream cipher:

  • Legacy (encryption_type 1) keeps the book key in record zero, obfuscated with a fixed vector. Nothing is needed from the caller.
  • DRM (encryption_type 2) keeps the key in a voucher. Some vouchers are sealed with a fixed "reader" key and open on their own; others are bound to a device and need that device's serial number, from which a PID is derived.

unlock() handles everything that needs no device. unlockWithSerial() and unlockWithPid() supply device material, and unlockWithKey() takes a book key directly. Until one of them succeeds, readText returns error.Locked.

Validity windows found in a voucher are reported through drm_validity but are not enforced — the library will decrypt an expired book and let you decide.

Amazon's newer Kindle protection (KFX, Topaz, account-bound AZW) is a different container with different key derivation, and is not handled here.

Whether removing DRM is lawful depends on where you are; in the US the DMCA restricts circumvention even for books you own. That is your call, not this library's.

CLI #

zig build
./zig-out/bin/mobi info   book.mobi
./zig-out/bin/mobi text   book.mobi > book.html
./zig-out/bin/mobi images book.mobi ./images

# encrypted books bound to a device
./zig-out/bin/mobi text book.mobi --serial B0011T2ZQA123456
./zig-out/bin/mobi text book.mobi --pid YFEAZXZ*KZ

Library #

const MOBI = @import("mobi").MOBI;

const bytes = try std.Io.Dir.cwd().readFileAlloc(io, "book.mobi", gpa, .unlimited);
defer gpa.free(bytes);

var book: MOBI = try .parse(gpa, bytes);
defer book.deinit(gpa);

std.debug.print("{s} by {s}\n", .{ book.title(), book.author() orelse "unknown" });

const html = try book.readTextUtf8(gpa);
defer gpa.free(html);

var it = book.images();
while (it.next()) |img| {
    // img.index, img.format, img.data
}

parse borrows bytes rather than copying, so the file contents must outlive the MOBI value. Everything it hands back — the title, EXTH values, image data — points into that buffer. readText, readTextUtf8 and the conversions in encoding allocate, and the caller owns those results.

Tests #

zig build test

The book the tests read is downloaded during zig build test rather than kept in the repository, by a small HTTPS client in tools/fetch.zig built on tls.zig. The download is content-checked against a SHA-256 in build.zig, cached between runs, and never touched by a plain zig build, which stays offline.

Zig's own package manager cannot do this job, for two independent reasons: it only accepts archives rather than a bare file, and its HTTP client fails to handshake with gutenberg.org even though the host negotiates TLS 1.3 fine under OpenSSL. Hence the separate tool.

The pinned hash will go stale. Project Gutenberg regenerates these files periodically — the current one was rebuilt the day before it was pinned. When the hash stops matching, the fetch fails with a message naming the expected and actual digests; check the new file over and update testdata_sha256. The book tests are deliberately written against invariants (the decompressed length matching text_length, the text containing "Charles Darwin") rather than exact record counts, so a new build of the book should not require touching them. For a stable pin, host a frozen copy somewhere immutable and point the URL there.

Two areas have no sample file to test against and so are covered synthetically:

  • HUFF/CDIC — the tests build the Huffman tables and phrase dictionary by hand in src/huffcdic.zig.
  • Encryption — the PC1 vectors and PID derivations in src/pc1.zig and src/drm.zig were generated by compiling libmobi's reference C routines, so they pin this implementation against a known-good one rather than against itself. The end-to-end tests in src/MOBI.zig re-encrypt the fetched book and read it back.

One thing remains unverified: legacyKey in src/MOBI.zig locates the embedded key at an offset that varies with file version. Those offsets follow libmobi but could not be checked against a real legacy-encrypted file. Treat that path as the least certain part of the library.

References cited #

  • Darwin, C. The Origin of Species by Means of Natural Selection: Or, the Preservation of Favoured Races in the Struggle for Life. 6th ed. Project Gutenberg EBook #2009, December 1999, last updated October 2026. https://www.gutenberg.org/ebooks/2009. The book the tests read, fetched as pg2009-images.mobi during zig build test: a real Mobipocket file, in the public domain, so the tests run against what a reader actually meets.
  • Fabiszewski, B. libmobi. Version 0.12, June 2024. C, LGPL-3.0. https://github.com/bfabiszewski/libmobi. The known good implementation the PC1 and PID test vectors were generated from, and the source of the file version rules and the version-dependent offsets of the legacy encryption key.
  • MobileRead Wiki contributors. MOBI. MobileRead Wiki, last edited April 2026. https://wiki.mobileread.com/wiki/MOBI. The layout of the Palm database, PalmDOC, MOBI and EXTH headers; the trailing entries that extra_record_data_flags describes; HUFF/CDIC compression; and the encryption schemes and PIDs.
  • MobileRead Wiki contributors. PalmDOC. MobileRead Wiki, last edited January 2018. https://wiki.mobileread.com/wiki/PalmDOC. The LZ77 variant that MOBI compression type 2 uses.

License #

MIT; see LICENSES/MIT.txt. The project follows the REUSE specification and passes reuse lint.