// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! Text encodings that appear in MOBI files. const std = @import("std"); pub const Encoding = enum(u32) { cp1252 = 1252, utf8 = 65001, _, }; /// The Unicode code points for CP-1252 bytes 0x80...0x9f. Everything outside /// that window matches Latin-1, where the byte value is the code point. const cp1252_high = [32]u21{ 0x20ac, 0xfffd, 0x201a, 0x0192, 0x201e, 0x2026, 0x2020, 0x2021, 0x02c6, 0x2030, 0x0160, 0x2039, 0x0152, 0xfffd, 0x017d, 0xfffd, 0xfffd, 0x2018, 0x2019, 0x201c, 0x201d, 0x2022, 0x2013, 0x2014, 0x02dc, 0x2122, 0x0161, 0x203a, 0x0153, 0xfffd, 0x017e, 0x0178, }; pub fn cp1252ToUtf8(gpa: std.mem.Allocator, input: []const u8) error{OutOfMemory}![]u8 { var out: std.ArrayList(u8) = .empty; errdefer out.deinit(gpa); try out.ensureTotalCapacity(gpa, input.len); var scratch: [4]u8 = undefined; for (input) |b| { if (b < 0x80) { try out.append(gpa, b); continue; } const cp: u21 = if (b < 0xa0) cp1252_high[b - 0x80] else b; const n = std.unicode.utf8Encode(cp, &scratch) catch unreachable; try out.appendSlice(gpa, scratch[0..n]); } return out.toOwnedSlice(gpa); } /// Convert `input` from `encoding` to UTF-8. The caller owns the result. pub fn toUtf8(gpa: std.mem.Allocator, encoding: Encoding, input: []const u8) error{OutOfMemory}![]u8 { return switch (encoding) { // Anything that is not CP-1252 is passed through; UTF-8 needs no work and // an unknown encoding is better left untouched than mangled. .cp1252 => cp1252ToUtf8(gpa, input), else => gpa.dupe(u8, input), }; } test "cp1252 high bytes" { const gpa = std.testing.allocator; const out = try cp1252ToUtf8(gpa, &.{ 'a', 0x93, 'b', 0x94, 0xe9 }); defer gpa.free(out); try std.testing.expectEqualStrings("a\u{201c}b\u{201d}\u{e9}", out); } test "ascii passes through unchanged" { const gpa = std.testing.allocator; const out = try toUtf8(gpa, .cp1252, "plain ascii"); defer gpa.free(out); try std.testing.expectEqualStrings("plain ascii", out); }