Something went wrong. Try again.
A Zig library and CLI for reading Mobipocket (.mobi, .prc, .azw) e-books.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us>// SPDX-License-Identifier: MIT
//! PalmDOC (LZ77 variant) decompression, used by MOBI compression type 2.
const std = @import("std");
pub const Error = error{ CorruptCompressedData, OutOfMemory };
/// Decompress a single PalmDOC-compressed record, appending to `out`.pub fn decompressAppend(gpa: std.mem.Allocator, out: *std.ArrayList(u8), input: []const u8) Error!void { var i: usize = 0; while (i < input.len) { const c = input[i]; i += 1; switch (c) { // 0x00 is a literal NUL. 0x00 => try out.append(gpa, 0), // 0x01...0x08 introduce that many literal bytes. 0x01...0x08 => { const n = c; if (i + n > input.len) return error.CorruptCompressedData; try out.appendSlice(gpa, input[i .. i + n]); i += n; }, // Printable ASCII is stored as-is. 0x09...0x7f => try out.append(gpa, c), // 0x80...0xbf is a two byte back-reference. 0x80...0xbf => { if (i >= input.len) return error.CorruptCompressedData; const pair: u16 = (@as(u16, c) << 8) | input[i]; i += 1; const distance: usize = (pair >> 3) & 0x07ff; const length: usize = (pair & 0x0007) + 3; if (distance == 0 or distance > out.items.len) return error.CorruptCompressedData; // The copy may overlap the bytes it produces, so go one at a time. var start = out.items.len - distance; for (0..length) |_| { try out.append(gpa, out.items[start]); start += 1; } }, // 0xc0...0xff is a space followed by the low seven bits as ASCII. 0xc0...0xff => { try out.append(gpa, ' '); try out.append(gpa, c ^ 0x80); }, } }}
pub fn decompress(gpa: std.mem.Allocator, input: []const u8) Error![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); try decompressAppend(gpa, &out, input); return out.toOwnedSlice(gpa);}
test "literal run and back-reference" { const gpa = std.testing.allocator; // 0x04 "abcd" then a back-reference of distance 4, length 3. const pair: u16 = 0x8000 | (4 << 3) | (3 - 3); const input = [_]u8{ 0x04, 'a', 'b', 'c', 'd', @intCast(pair >> 8), @intCast(pair & 0xff) }; const out = try decompress(gpa, &input); defer gpa.free(out); try std.testing.expectEqualStrings("abcdabc", out);}
test "space compressed high bytes" { const gpa = std.testing.allocator; const input = [_]u8{ 'h', 'i', 0xc0 | 'y' }; const out = try decompress(gpa, &input); defer gpa.free(out); try std.testing.expectEqualStrings("hi y", out);}
test "overlapping back-reference repeats" { const gpa = std.testing.allocator; // distance 1, length 5 => the previous byte repeated. const pair: u16 = 0x8000 | (1 << 3) | (5 - 3); const input = [_]u8{ 'x', @intCast(pair >> 8), @intCast(pair & 0xff) }; const out = try decompress(gpa, &input); defer gpa.free(out); try std.testing.expectEqualStrings("xxxxxx", out);}
test "rejects out of range back-reference" { const gpa = std.testing.allocator; const pair: u16 = 0x8000 | (99 << 3); const input = [_]u8{ 'x', @intCast(pair >> 8), @intCast(pair & 0xff) }; try std.testing.expectError(error.CorruptCompressedData, decompress(gpa, &input));}