- Zig 99.2%
- Nix 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The same four references are in the zig-mobi Zotero collection. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01RFP6E8zmkwbNmmRgvmGsNH |
||
| LICENSES | ||
| src | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| REUSE.toml | ||
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_type1) keeps the book key in record zero, obfuscated with a fixed vector. Nothing is needed from the caller. - DRM (
encryption_type2) 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.zigandsrc/drm.zigwere 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 insrc/MOBI.zigre-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.mobiduringzig 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_flagsdescribes; 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.