# 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`](https://github.com/ianic/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 is the web-visible one: ```console $ git clone https://git.jcollie.dev/jeff/zig-mobi.git ``` it is mirrored on Tangled at , 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 ```zig 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. . 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. . 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. . 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. . The LZ77 variant that MOBI compression type 2 uses. ## License MIT; see [`LICENSES/MIT.txt`](LICENSES/MIT.txt). The project follows the [REUSE](https://reuse.software/) specification and passes `reuse lint`.