// SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT //! The code pages an installer package's strings can be stored in, and the //! encoding of text into them. //! //! zixl reads WiX sources as UTF-8 and stores their text in the code page //! the WiX source chooses: the database's for the tables, and the summary //! information's for that. The single-byte Windows code pages are the ones //! supported; 0, the neutral code page, holds ASCII alone, and 65001, //! UTF-8, is accepted though Windows Installer does not officially support //! it. const std = @import("std"); const Allocator = std.mem.Allocator; const charset = @import("zxml").charset; pub const neutral = 0; pub const utf8 = 65001; /// Whether `cp` is one zixl can store strings in. pub fn isSupported(cp: u16) bool { return cp == neutral or cp == utf8 or encoding(cp) != null; } fn encoding(cp: u16) ?charset.Encoding { return switch (cp) { 874 => .windows_874, 1250 => .windows_1250, 1251 => .windows_1251, 1252 => .windows_1252, 1253 => .windows_1253, 1254 => .windows_1254, 1255 => .windows_1255, 1256 => .windows_1256, 1257 => .windows_1257, 1258 => .windows_1258, else => null, }; } pub const Error = error{ /// The text holds a character the code page cannot store. Unencodable, } || Allocator.Error; /// `text`, UTF-8, as it is stored in code page `cp`. ASCII is the same in /// every one of them, and comes back as it is. pub fn encode(gpa: Allocator, cp: u16, text: []const u8) Error![]const u8 { for (text) |c| { if (c >= 0x80) break; } else return text; if (cp == utf8) return text; const e = encoding(cp) orelse return error.Unencodable; return charset.encodeAlloc(gpa, e, text, .{ .error_mode = .fatal }) catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, else => error.Unencodable, }; } /// `bytes`, stored in code page `cp`, as UTF-8. Reading accepts the /// double-byte East Asian code pages too, which zixl does not write. The /// neutral code page holds ASCII alone, but other tools store more in it: /// text that is valid UTF-8 is read as UTF-8, and anything else as Windows /// reads it on a machine whose ANSI code page is 1252. Bytes /// the code page cannot decode, and every non-ASCII byte of a code page zixl /// does not know, become U+FFFD; `known` says whether it knew it. pub fn decode(gpa: Allocator, cp: u16, bytes: []const u8, known: ?*bool) Allocator.Error![]const u8 { if (known) |k| k.* = true; for (bytes) |c| { if (c >= 0x80) break; } else return bytes; const e: charset.Encoding = if (cp == utf8) .utf8 else if (cp == neutral) (if (std.unicode.utf8ValidateSlice(bytes)) .utf8 else .windows_1252) else encoding(cp) orelse switch (cp) { 932 => .shift_jis, 936 => .gbk, 949 => .euc_kr, 950 => .big5, else => { if (known) |k| k.* = false; var out: std.ArrayList(u8) = .empty; for (bytes) |c| { if (c < 0x80) try out.append(gpa, c) else try out.appendSlice(gpa, "\u{FFFD}"); } return out.toOwnedSlice(gpa); }, }; return charset.decodeAlloc(gpa, e, bytes, .{ .error_mode = .replacement }) catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, // Replacement never fails otherwise. else => unreachable, }; } test "decoding" { const gpa = std.testing.allocator; const cafe = try decode(gpa, 1252, "Caf\xe9", null); defer gpa.free(cafe); try std.testing.expectEqualStrings("Café", cafe); try std.testing.expectEqualStrings("plain", try decode(gpa, 0, "plain", null)); const neutral_utf8 = try decode(gpa, neutral, "Café", null); defer gpa.free(neutral_utf8); try std.testing.expectEqualStrings("Café", neutral_utf8); const neutral_ansi = try decode(gpa, neutral, "Caf\xe9", null); defer gpa.free(neutral_ansi); try std.testing.expectEqualStrings("Café", neutral_ansi); var known = true; const unknown = try decode(gpa, 437, "a\x80", &known); defer gpa.free(unknown); try std.testing.expect(!known); try std.testing.expectEqualStrings("a\u{FFFD}", unknown); const bad = try decode(gpa, utf8, "a\xff", null); defer gpa.free(bad); try std.testing.expectEqualStrings("a\u{FFFD}", bad); } test "ASCII is the same everywhere" { try std.testing.expectEqualStrings("plain", try encode(std.testing.allocator, neutral, "plain")); } test "a single-byte code page" { const got = try encode(std.testing.allocator, 1252, "Café"); defer std.testing.allocator.free(got); try std.testing.expectEqualStrings("Caf\xe9", got); const cyrillic = try encode(std.testing.allocator, 1251, "Привет"); defer std.testing.allocator.free(cyrillic); try std.testing.expectEqualSlices(u8, &.{ 0xCF, 0xF0, 0xE8, 0xE2, 0xE5, 0xF2 }, cyrillic); } test "what a code page cannot hold" { try std.testing.expectError(error.Unencodable, encode(std.testing.allocator, 1252, "Ω")); try std.testing.expectError(error.Unencodable, encode(std.testing.allocator, neutral, "é")); }