zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

res.zig (49608B) - Raw


      1 const std = @import("std");
      2 const assert = std.debug.assert;
      3 const rc = @import("rc.zig");
      4 const ResourceType = rc.ResourceType;
      5 const CommonResourceAttributes = rc.CommonResourceAttributes;
      6 const Allocator = std.mem.Allocator;
      7 const windows1252 = @import("windows1252.zig");
      8 const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
      9 const literals = @import("literals.zig");
     10 const SourceBytes = literals.SourceBytes;
     11 const Codepoint = @import("code_pages.zig").Codepoint;
     12 const lang = @import("lang.zig");
     13 const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
     14 
     15 /// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
     16 pub const RT = enum(u8) {
     17     ACCELERATOR = 9,
     18     ANICURSOR = 21,
     19     ANIICON = 22,
     20     BITMAP = 2,
     21     CURSOR = 1,
     22     DIALOG = 5,
     23     DLGINCLUDE = 17,
     24     DLGINIT = 240,
     25     FONT = 8,
     26     FONTDIR = 7,
     27     GROUP_CURSOR = 1 + 11, // CURSOR + 11
     28     GROUP_ICON = 3 + 11, // ICON + 11
     29     HTML = 23,
     30     ICON = 3,
     31     MANIFEST = 24,
     32     MENU = 4,
     33     MESSAGETABLE = 11,
     34     PLUGPLAY = 19,
     35     RCDATA = 10,
     36     STRING = 6,
     37     TOOLBAR = 241,
     38     VERSION = 16,
     39     VXD = 20,
     40     _,
     41 
     42     /// Returns null if the resource type is user-defined
     43     /// Asserts that the resource is not `stringtable`
     44     pub fn fromResource(resource: ResourceType) ?RT {
     45         return switch (resource) {
     46             .accelerators => .ACCELERATOR,
     47             .bitmap => .BITMAP,
     48             .cursor => .GROUP_CURSOR,
     49             .dialog => .DIALOG,
     50             .dialogex => .DIALOG,
     51             .dlginclude => .DLGINCLUDE,
     52             .dlginit => .DLGINIT,
     53             .font => .FONT,
     54             .html => .HTML,
     55             .icon => .GROUP_ICON,
     56             .menu => .MENU,
     57             .menuex => .MENU,
     58             .messagetable => .MESSAGETABLE,
     59             .plugplay => .PLUGPLAY,
     60             .rcdata => .RCDATA,
     61             .stringtable => unreachable,
     62             .toolbar => .TOOLBAR,
     63             .user_defined => null,
     64             .versioninfo => .VERSION,
     65             .vxd => .VXD,
     66 
     67             .cursor_num => .CURSOR,
     68             .icon_num => .ICON,
     69             .string_num => .STRING,
     70             .anicursor_num => .ANICURSOR,
     71             .aniicon_num => .ANIICON,
     72             .fontdir_num => .FONTDIR,
     73             .manifest_num => .MANIFEST,
     74         };
     75     }
     76 };
     77 
     78 /// https://learn.microsoft.com/en-us/windows/win32/menurc/common-resource-attributes
     79 /// https://learn.microsoft.com/en-us/windows/win32/menurc/resourceheader
     80 pub const MemoryFlags = packed struct(u16) {
     81     value: u16,
     82 
     83     pub const MOVEABLE: u16 = 0x10;
     84     // TODO: SHARED and PURE seem to be the same thing? Testing seems to confirm this but
     85     //       would like to find mention of it somewhere.
     86     pub const SHARED: u16 = 0x20;
     87     pub const PURE: u16 = 0x20;
     88     pub const PRELOAD: u16 = 0x40;
     89     pub const DISCARDABLE: u16 = 0x1000;
     90 
     91     /// Note: The defaults can have combinations that are not possible to specify within
     92     ///       an .rc file, as the .rc attributes imply other values (i.e. specifying
     93     ///       DISCARDABLE always implies MOVEABLE and PURE/SHARED, and yet RT_ICON
     94     ///       has a default of only MOVEABLE | DISCARDABLE).
     95     pub fn defaults(predefined_resource_type: ?RT) MemoryFlags {
     96         if (predefined_resource_type == null) {
     97             return MemoryFlags{ .value = MOVEABLE | SHARED };
     98         } else {
     99             return switch (predefined_resource_type.?) {
    100                 // zig fmt: off
    101                 .RCDATA, .BITMAP, .HTML, .MANIFEST,
    102                 .ACCELERATOR, .VERSION, .MESSAGETABLE,
    103                 .DLGINIT, .TOOLBAR, .PLUGPLAY,
    104                 .VXD, => MemoryFlags{ .value = MOVEABLE | SHARED },
    105 
    106                 .GROUP_ICON, .GROUP_CURSOR,
    107                 .STRING, .FONT, .DIALOG, .MENU,
    108                 .DLGINCLUDE, => MemoryFlags{ .value = MOVEABLE | SHARED | DISCARDABLE },
    109 
    110                 .ICON, .CURSOR, .ANIICON, .ANICURSOR => MemoryFlags{ .value = MOVEABLE | DISCARDABLE },
    111                 .FONTDIR => MemoryFlags{ .value = MOVEABLE | PRELOAD },
    112                 // zig fmt: on
    113                 // Same as predefined_resource_type == null
    114                 _ => return MemoryFlags{ .value = MOVEABLE | SHARED },
    115             };
    116         }
    117     }
    118 
    119     pub fn set(self: *MemoryFlags, attribute: CommonResourceAttributes) void {
    120         switch (attribute) {
    121             .preload => self.value |= PRELOAD,
    122             .loadoncall => self.value &= ~PRELOAD,
    123             .moveable => self.value |= MOVEABLE,
    124             .fixed => self.value &= ~(MOVEABLE | DISCARDABLE),
    125             .shared => self.value |= SHARED,
    126             .nonshared => self.value &= ~(SHARED | DISCARDABLE),
    127             .pure => self.value |= PURE,
    128             .impure => self.value &= ~(PURE | DISCARDABLE),
    129             .discardable => self.value |= DISCARDABLE | MOVEABLE | PURE,
    130         }
    131     }
    132 
    133     pub fn setGroup(self: *MemoryFlags, attribute: CommonResourceAttributes, implied_shared_or_pure: bool) void {
    134         switch (attribute) {
    135             .preload => {
    136                 self.value |= PRELOAD;
    137                 if (implied_shared_or_pure) self.value &= ~SHARED;
    138             },
    139             .loadoncall => {
    140                 self.value &= ~PRELOAD;
    141                 if (implied_shared_or_pure) self.value |= SHARED;
    142             },
    143             else => self.set(attribute),
    144         }
    145     }
    146 };
    147 
    148 /// https://learn.microsoft.com/en-us/windows/win32/intl/language-identifiers
    149 pub const Language = packed struct(u16) {
    150     // Note: This is the default no matter what locale the current system is set to,
    151     //       e.g. even if the system's locale is en-GB, en-US will still be the
    152     //       default language for resources in the Win32 rc compiler.
    153     primary_language_id: u10 = lang.LANG_ENGLISH,
    154     sublanguage_id: u6 = lang.SUBLANG_ENGLISH_US,
    155 
    156     /// Default language ID as a u16
    157     pub const default: u16 = (Language{}).asInt();
    158 
    159     pub fn fromInt(int: u16) Language {
    160         return @bitCast(int);
    161     }
    162 
    163     pub fn asInt(self: Language) u16 {
    164         return @bitCast(self);
    165     }
    166 
    167     pub fn format(language: Language, w: *std.io.Writer) std.io.Writer.Error!void {
    168         const language_id = language.asInt();
    169         const language_name = language_name: {
    170             if (std.enums.fromInt(lang.LanguageId, language_id)) |lang_enum_val| {
    171                 break :language_name @tagName(lang_enum_val);
    172             }
    173             if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
    174                 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
    175             }
    176             break :language_name "<UNKNOWN>";
    177         };
    178         try w.print("{s} (0x{X})", .{ language_name, language_id });
    179     }
    180 };
    181 
    182 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-dlgitemtemplate#remarks
    183 pub const ControlClass = enum(u16) {
    184     button = 0x80,
    185     edit = 0x81,
    186     static = 0x82,
    187     listbox = 0x83,
    188     scrollbar = 0x84,
    189     combobox = 0x85,
    190 
    191     pub fn fromControl(control: rc.Control) ?ControlClass {
    192         return switch (control) {
    193             // zig fmt: off
    194             .auto3state, .autocheckbox, .autoradiobutton,
    195             .checkbox, .defpushbutton, .groupbox, .pushbox,
    196             .pushbutton, .radiobutton, .state3, .userbutton => .button,
    197             // zig fmt: on
    198             .combobox => .combobox,
    199             .control => null,
    200             .ctext, .icon, .ltext, .rtext => .static,
    201             .edittext, .hedit, .iedit => .edit,
    202             .listbox => .listbox,
    203             .scrollbar => .scrollbar,
    204         };
    205     }
    206 
    207     pub fn getImpliedStyle(control: rc.Control) u32 {
    208         var style = WS.CHILD | WS.VISIBLE;
    209         switch (control) {
    210             .auto3state => style |= BS.AUTO3STATE | WS.TABSTOP,
    211             .autocheckbox => style |= BS.AUTOCHECKBOX | WS.TABSTOP,
    212             .autoradiobutton => style |= BS.AUTORADIOBUTTON,
    213             .checkbox => style |= BS.CHECKBOX | WS.TABSTOP,
    214             .combobox => {},
    215             .control => {},
    216             .ctext => style |= SS.CENTER | WS.GROUP,
    217             .defpushbutton => style |= BS.DEFPUSHBUTTON | WS.TABSTOP,
    218             .edittext, .hedit, .iedit => style |= WS.TABSTOP | WS.BORDER,
    219             .groupbox => style |= BS.GROUPBOX,
    220             .icon => style |= SS.ICON,
    221             .listbox => style |= LBS.NOTIFY | WS.BORDER,
    222             .ltext => style |= WS.GROUP,
    223             .pushbox => style |= BS.PUSHBOX | WS.TABSTOP,
    224             .pushbutton => style |= WS.TABSTOP,
    225             .radiobutton => style |= BS.RADIOBUTTON,
    226             .rtext => style |= SS.RIGHT | WS.GROUP,
    227             .scrollbar => {},
    228             .state3 => style |= BS.@"3STATE" | WS.TABSTOP,
    229             .userbutton => style |= BS.USERBUTTON | WS.TABSTOP,
    230         }
    231         return style;
    232     }
    233 };
    234 
    235 pub const NameOrOrdinal = union(enum) {
    236     // UTF-16 LE
    237     name: [:0]const u16,
    238     ordinal: u16,
    239 
    240     pub fn deinit(self: NameOrOrdinal, allocator: Allocator) void {
    241         switch (self) {
    242             .name => |name| {
    243                 allocator.free(name);
    244             },
    245             .ordinal => {},
    246         }
    247     }
    248 
    249     /// Returns the full length of the amount of bytes that would be written by `write`
    250     /// (e.g. for an ordinal it will return the length including the 0xFFFF indicator)
    251     pub fn byteLen(self: NameOrOrdinal) usize {
    252         switch (self) {
    253             .name => |name| {
    254                 // + 1 for 0-terminated
    255                 return (name.len + 1) * @sizeOf(u16);
    256             },
    257             .ordinal => return 4,
    258         }
    259     }
    260 
    261     pub fn write(self: NameOrOrdinal, writer: anytype) !void {
    262         switch (self) {
    263             .name => |name| {
    264                 try writer.writeAll(std.mem.sliceAsBytes(name[0 .. name.len + 1]));
    265             },
    266             .ordinal => |ordinal| {
    267                 try writer.writeInt(u16, 0xffff, .little);
    268                 try writer.writeInt(u16, ordinal, .little);
    269             },
    270         }
    271     }
    272 
    273     pub fn writeEmpty(writer: anytype) !void {
    274         try writer.writeInt(u16, 0, .little);
    275     }
    276 
    277     pub fn fromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
    278         if (maybeOrdinalFromString(bytes)) |ordinal| {
    279             return ordinal;
    280         }
    281         return nameFromString(allocator, bytes);
    282     }
    283 
    284     pub fn nameFromString(allocator: Allocator, bytes: SourceBytes) !NameOrOrdinal {
    285         // Names have a limit of 256 UTF-16 code units + null terminator
    286         var buf = try std.array_list.Managed(u16).initCapacity(allocator, @min(257, bytes.slice.len));
    287         errdefer buf.deinit();
    288 
    289         var i: usize = 0;
    290         while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
    291             if (buf.items.len == 256) break;
    292 
    293             const c = codepoint.value;
    294             if (c == Codepoint.invalid) {
    295                 try buf.append(std.mem.nativeToLittle(u16, '�'));
    296             } else if (c < 0x7F) {
    297                 // ASCII chars in names are always converted to uppercase
    298                 try buf.append(std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c))));
    299             } else if (c < 0x10000) {
    300                 const short: u16 = @intCast(c);
    301                 try buf.append(std.mem.nativeToLittle(u16, short));
    302             } else {
    303                 const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
    304                 try buf.append(std.mem.nativeToLittle(u16, high));
    305 
    306                 // Note: This can cut-off in the middle of a UTF-16 surrogate pair,
    307                 //       i.e. it can make the string end with an unpaired high surrogate
    308                 if (buf.items.len == 256) break;
    309 
    310                 const low = @as(u16, @intCast(c & 0x3FF)) + 0xDC00;
    311                 try buf.append(std.mem.nativeToLittle(u16, low));
    312             }
    313         }
    314 
    315         return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(0) };
    316     }
    317 
    318     /// Returns `null` if the bytes do not form a valid number.
    319     /// Does not allow non-ASCII digits (which the Win32 RC compiler does allow
    320     /// in base 10 numbers, see `maybeNonAsciiOrdinalFromString`).
    321     pub fn maybeOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
    322         var buf = bytes.slice;
    323         var radix: u8 = 10;
    324         if (buf.len > 2 and buf[0] == '0') {
    325             switch (buf[1]) {
    326                 '0'...'9' => {},
    327                 'x', 'X' => {
    328                     radix = 16;
    329                     buf = buf[2..];
    330                     // only the first 4 hex digits matter, anything else is ignored
    331                     // i.e. 0x12345 is treated as if it were 0x1234
    332                     buf.len = @min(buf.len, 4);
    333                 },
    334                 else => return null,
    335             }
    336         }
    337 
    338         var i: usize = 0;
    339         var result: u16 = 0;
    340         while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
    341             const c = codepoint.value;
    342             const digit: u8 = switch (c) {
    343                 0x00...0x7F => std.fmt.charToDigit(@intCast(c), radix) catch switch (radix) {
    344                     10 => return null,
    345                     // non-hex-digits are treated as a terminator rather than invalidating
    346                     // the number (note: if there are no valid hex digits then the result
    347                     // will be zero which is not treated as a valid number)
    348                     16 => break,
    349                     else => unreachable,
    350                 },
    351                 else => if (radix == 10) return null else break,
    352             };
    353 
    354             if (result != 0) {
    355                 result *%= radix;
    356             }
    357             result +%= digit;
    358         }
    359 
    360         // Anything that resolves to zero is not interpretted as a number
    361         if (result == 0) return null;
    362         return NameOrOrdinal{ .ordinal = result };
    363     }
    364 
    365     /// The Win32 RC compiler uses `iswdigit` for digit detection for base 10
    366     /// numbers, which means that non-ASCII digits are 'accepted' but handled
    367     /// in a totally unintuitive manner, leading to arbitrary results.
    368     ///
    369     /// This function will return the value that such an ordinal 'would' have
    370     /// if it was run through the Win32 RC compiler. This allows us to disallow
    371     /// non-ASCII digits in number literals but still detect when the Win32
    372     /// RC compiler would have allowed them, so that a proper warning/error
    373     /// can be emitted.
    374     pub fn maybeNonAsciiOrdinalFromString(bytes: SourceBytes) ?NameOrOrdinal {
    375         const buf = bytes.slice;
    376         const radix = 10;
    377         if (buf.len > 2 and buf[0] == '0') {
    378             switch (buf[1]) {
    379                 // We only care about base 10 numbers here
    380                 'x', 'X' => return null,
    381                 else => {},
    382             }
    383         }
    384 
    385         var i: usize = 0;
    386         var result: u16 = 0;
    387         while (bytes.code_page.codepointAt(i, buf)) |codepoint| : (i += codepoint.byte_len) {
    388             const c = codepoint.value;
    389             const digit: u16 = digit: {
    390                 const is_digit = (c >= '0' and c <= '9') or isNonAsciiDigit(c);
    391                 if (!is_digit) return null;
    392                 break :digit @intCast(c - '0');
    393             };
    394 
    395             if (result != 0) {
    396                 result *%= radix;
    397             }
    398             result +%= digit;
    399         }
    400 
    401         // Anything that resolves to zero is not interpretted as a number
    402         if (result == 0) return null;
    403         return NameOrOrdinal{ .ordinal = result };
    404     }
    405 
    406     pub fn predefinedResourceType(self: NameOrOrdinal) ?RT {
    407         switch (self) {
    408             .ordinal => |ordinal| {
    409                 if (ordinal >= 256) return null;
    410                 switch (@as(RT, @enumFromInt(ordinal))) {
    411                     .ACCELERATOR,
    412                     .ANICURSOR,
    413                     .ANIICON,
    414                     .BITMAP,
    415                     .CURSOR,
    416                     .DIALOG,
    417                     .DLGINCLUDE,
    418                     .DLGINIT,
    419                     .FONT,
    420                     .FONTDIR,
    421                     .GROUP_CURSOR,
    422                     .GROUP_ICON,
    423                     .HTML,
    424                     .ICON,
    425                     .MANIFEST,
    426                     .MENU,
    427                     .MESSAGETABLE,
    428                     .PLUGPLAY,
    429                     .RCDATA,
    430                     .STRING,
    431                     .TOOLBAR,
    432                     .VERSION,
    433                     .VXD,
    434                     => |rt| return rt,
    435                     _ => return null,
    436                 }
    437             },
    438             .name => return null,
    439         }
    440     }
    441 
    442     pub fn format(self: NameOrOrdinal, w: *std.io.Writer) !void {
    443         switch (self) {
    444             .name => |name| {
    445                 try w.print("{f}", .{std.unicode.fmtUtf16Le(name)});
    446             },
    447             .ordinal => |ordinal| {
    448                 try w.print("{d}", .{ordinal});
    449             },
    450         }
    451     }
    452 
    453     fn formatResourceType(self: NameOrOrdinal, w: *std.io.Writer) std.io.Writer.Error!void {
    454         switch (self) {
    455             .name => |name| {
    456                 try w.print("{f}", .{std.unicode.fmtUtf16Le(name)});
    457             },
    458             .ordinal => |ordinal| {
    459                 if (std.enums.tagName(RT, @enumFromInt(ordinal))) |predefined_type_name| {
    460                     try w.print("{s}", .{predefined_type_name});
    461                 } else {
    462                     try w.print("{d}", .{ordinal});
    463                 }
    464             },
    465         }
    466     }
    467 
    468     pub fn fmtResourceType(type_value: NameOrOrdinal) std.fmt.Formatter(NameOrOrdinal, formatResourceType) {
    469         return .{ .data = type_value };
    470     }
    471 };
    472 
    473 fn expectNameOrOrdinal(expected: NameOrOrdinal, actual: NameOrOrdinal) !void {
    474     switch (expected) {
    475         .name => {
    476             if (actual != .name) return error.TestExpectedEqual;
    477             try std.testing.expectEqualSlices(u16, expected.name, actual.name);
    478         },
    479         .ordinal => {
    480             if (actual != .ordinal) return error.TestExpectedEqual;
    481             try std.testing.expectEqual(expected.ordinal, actual.ordinal);
    482         },
    483     }
    484 }
    485 
    486 test "NameOrOrdinal" {
    487     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    488     defer arena.deinit();
    489 
    490     const allocator = arena.allocator();
    491 
    492     // zero is treated as a string
    493     try expectNameOrOrdinal(
    494         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0") },
    495         try NameOrOrdinal.fromString(allocator, .{ .slice = "0", .code_page = .windows1252 }),
    496     );
    497     // any non-digit byte invalidates the number
    498     try expectNameOrOrdinal(
    499         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1A") },
    500         try NameOrOrdinal.fromString(allocator, .{ .slice = "1a", .code_page = .windows1252 }),
    501     );
    502     try expectNameOrOrdinal(
    503         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1ÿ") },
    504         try NameOrOrdinal.fromString(allocator, .{ .slice = "1\xff", .code_page = .windows1252 }),
    505     );
    506     try expectNameOrOrdinal(
    507         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1€") },
    508         try NameOrOrdinal.fromString(allocator, .{ .slice = "1€", .code_page = .utf8 }),
    509     );
    510     try expectNameOrOrdinal(
    511         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("1�") },
    512         try NameOrOrdinal.fromString(allocator, .{ .slice = "1\x80", .code_page = .utf8 }),
    513     );
    514     // same with overflow that resolves to 0
    515     try expectNameOrOrdinal(
    516         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("65536") },
    517         try NameOrOrdinal.fromString(allocator, .{ .slice = "65536", .code_page = .windows1252 }),
    518     );
    519     // hex zero is also treated as a string
    520     try expectNameOrOrdinal(
    521         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0X0") },
    522         try NameOrOrdinal.fromString(allocator, .{ .slice = "0x0", .code_page = .windows1252 }),
    523     );
    524     // hex numbers work
    525     try expectNameOrOrdinal(
    526         NameOrOrdinal{ .ordinal = 0x100 },
    527         try NameOrOrdinal.fromString(allocator, .{ .slice = "0x100", .code_page = .windows1252 }),
    528     );
    529     // only the first 4 hex digits matter
    530     try expectNameOrOrdinal(
    531         NameOrOrdinal{ .ordinal = 0x1234 },
    532         try NameOrOrdinal.fromString(allocator, .{ .slice = "0X12345", .code_page = .windows1252 }),
    533     );
    534     // octal is not supported so it gets treated as a string
    535     try expectNameOrOrdinal(
    536         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("0O1234") },
    537         try NameOrOrdinal.fromString(allocator, .{ .slice = "0o1234", .code_page = .windows1252 }),
    538     );
    539     // overflow wraps
    540     try expectNameOrOrdinal(
    541         NameOrOrdinal{ .ordinal = @truncate(65635) },
    542         try NameOrOrdinal.fromString(allocator, .{ .slice = "65635", .code_page = .windows1252 }),
    543     );
    544     // non-hex-digits in a hex literal are treated as a terminator
    545     try expectNameOrOrdinal(
    546         NameOrOrdinal{ .ordinal = 0x4 },
    547         try NameOrOrdinal.fromString(allocator, .{ .slice = "0x4n", .code_page = .windows1252 }),
    548     );
    549     try expectNameOrOrdinal(
    550         NameOrOrdinal{ .ordinal = 0xFA },
    551         try NameOrOrdinal.fromString(allocator, .{ .slice = "0xFAZ92348", .code_page = .windows1252 }),
    552     );
    553     // 0 at the start is allowed
    554     try expectNameOrOrdinal(
    555         NameOrOrdinal{ .ordinal = 50 },
    556         try NameOrOrdinal.fromString(allocator, .{ .slice = "050", .code_page = .windows1252 }),
    557     );
    558     // limit of 256 UTF-16 code units, can cut off between a surrogate pair
    559     {
    560         var expected = blk: {
    561             // the input before the 𐐷 character, but uppercased
    562             const expected_u8_bytes = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528QFFL7SHNSIETG0QKLR1UYPBTUV1PMFQRRA0VJDG354GQEDJMUPGPP1W1EXVNTZVEIZ6K3IPQM1AWGEYALMEODYVEZGOD3MFMGEY8FNR4JUETTB1PZDEWSNDRGZUA8SNXP3NGO";
    563             var buf: [256:0]u16 = undefined;
    564             for (expected_u8_bytes, 0..) |byte, i| {
    565                 buf[i] = std.mem.nativeToLittle(u16, byte);
    566             }
    567             // surrogate pair that is now orphaned
    568             buf[255] = std.mem.nativeToLittle(u16, 0xD801);
    569             break :blk buf;
    570         };
    571         try expectNameOrOrdinal(
    572             NameOrOrdinal{ .name = &expected },
    573             try NameOrOrdinal.fromString(allocator, .{
    574                 .slice = "00614982008907933748980730280674788429543776231864944218790698304852300002973622122844631429099469274282385299397783838528qffL7ShnSIETg0qkLr1UYpbtuv1PMFQRRa0VjDG354GQedJmUPgpp1w1ExVnTzVEiz6K3iPqM1AWGeYALmeODyvEZGOD3MfmGey8fnR4jUeTtB1PzdeWsNDrGzuA8Snxp3NGO𐐷",
    575                 .code_page = .utf8,
    576             }),
    577         );
    578     }
    579 }
    580 
    581 test "NameOrOrdinal code page awareness" {
    582     var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    583     defer arena.deinit();
    584 
    585     const allocator = arena.allocator();
    586 
    587     try expectNameOrOrdinal(
    588         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("��𐐷") },
    589         try NameOrOrdinal.fromString(allocator, .{
    590             .slice = "\xF0\x80\x80𐐷",
    591             .code_page = .utf8,
    592         }),
    593     );
    594     try expectNameOrOrdinal(
    595         // The UTF-8 representation of 𐐷 is 0xF0 0x90 0x90 0xB7. In order to provide valid
    596         // UTF-8 to utf8ToUtf16LeStringLiteral, it uses the UTF-8 representation of the codepoint
    597         // <U+0x90> which is 0xC2 0x90. The code units in the expected UTF-16 string are:
    598         // { 0x00F0, 0x20AC, 0x20AC, 0x00F0, 0x0090, 0x0090, 0x00B7 }
    599         NameOrOrdinal{ .name = std.unicode.utf8ToUtf16LeStringLiteral("ð€€ð\xC2\x90\xC2\x90·") },
    600         try NameOrOrdinal.fromString(allocator, .{
    601             .slice = "\xF0\x80\x80𐐷",
    602             .code_page = .windows1252,
    603         }),
    604     );
    605 }
    606 
    607 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-accel#members
    608 /// https://devblogs.microsoft.com/oldnewthing/20070316-00/?p=27593
    609 pub const AcceleratorModifiers = struct {
    610     value: u8 = 0,
    611     explicit_ascii_or_virtkey: bool = false,
    612 
    613     pub const ASCII = 0;
    614     pub const VIRTKEY = 1;
    615     pub const NOINVERT = 1 << 1;
    616     pub const SHIFT = 1 << 2;
    617     pub const CONTROL = 1 << 3;
    618     pub const ALT = 1 << 4;
    619     /// Marker for the last accelerator in an accelerator table
    620     pub const last_accelerator_in_table = 1 << 7;
    621 
    622     pub fn apply(self: *AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) void {
    623         if (modifier == .ascii or modifier == .virtkey) self.explicit_ascii_or_virtkey = true;
    624         self.value |= modifierValue(modifier);
    625     }
    626 
    627     pub fn isSet(self: AcceleratorModifiers, modifier: rc.AcceleratorTypeAndOptions) bool {
    628         // ASCII is set whenever VIRTKEY is not
    629         if (modifier == .ascii) return self.value & modifierValue(.virtkey) == 0;
    630         return self.value & modifierValue(modifier) != 0;
    631     }
    632 
    633     fn modifierValue(modifier: rc.AcceleratorTypeAndOptions) u8 {
    634         return switch (modifier) {
    635             .ascii => ASCII,
    636             .virtkey => VIRTKEY,
    637             .noinvert => NOINVERT,
    638             .shift => SHIFT,
    639             .control => CONTROL,
    640             .alt => ALT,
    641         };
    642     }
    643 
    644     pub fn markLast(self: *AcceleratorModifiers) void {
    645         self.value |= last_accelerator_in_table;
    646     }
    647 };
    648 
    649 const AcceleratorKeyCodepointTranslator = struct {
    650     string_type: literals.StringType,
    651     output_code_page: SupportedCodePage,
    652 
    653     pub fn translate(self: @This(), maybe_parsed: ?literals.IterativeStringParser.ParsedCodepoint) ?u21 {
    654         const parsed = maybe_parsed orelse return null;
    655         if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
    656         if (parsed.from_escaped_integer) {
    657             switch (self.string_type) {
    658                 .ascii => {
    659                     const truncated: u8 = @truncate(parsed.codepoint);
    660                     switch (self.output_code_page) {
    661                         .utf8 => switch (truncated) {
    662                             0...0x7F => return truncated,
    663                             else => return 0xFFFD,
    664                         },
    665                         .windows1252 => return windows1252.toCodepoint(truncated),
    666                     }
    667                 },
    668                 .wide => {
    669                     const truncated: u16 = @truncate(parsed.codepoint);
    670                     return truncated;
    671                 },
    672             }
    673         }
    674         if (parsed.escaped_surrogate_pair) {
    675             // The codepoint of only the low surrogate
    676             const low = @as(u16, @intCast(parsed.codepoint & 0x3FF)) + 0xDC00;
    677             return low;
    678         }
    679         return parsed.codepoint;
    680     }
    681 };
    682 
    683 pub const ParseAcceleratorKeyStringError = error{ EmptyAccelerator, AcceleratorTooLong, InvalidControlCharacter, ControlCharacterOutOfRange };
    684 
    685 /// Expects bytes to be the full bytes of a string literal token (e.g. including the "" or L"").
    686 pub fn parseAcceleratorKeyString(bytes: SourceBytes, is_virt: bool, options: literals.StringParseOptions) (ParseAcceleratorKeyStringError || Allocator.Error)!u16 {
    687     if (bytes.slice.len == 0) {
    688         return error.EmptyAccelerator;
    689     }
    690 
    691     var parser = literals.IterativeStringParser.init(bytes, options);
    692     var translator = AcceleratorKeyCodepointTranslator{
    693         .string_type = parser.declared_string_type,
    694         .output_code_page = options.output_code_page,
    695     };
    696 
    697     const first_codepoint = translator.translate(try parser.next()) orelse return error.EmptyAccelerator;
    698     // 0 is treated as a terminator, so this is equivalent to an empty string
    699     if (first_codepoint == 0) return error.EmptyAccelerator;
    700 
    701     if (first_codepoint == '^') {
    702         // Note: Emitting this warning unconditionally whenever ^ is the first character
    703         //       matches the Win32 RC behavior, but it's questionable whether or not
    704         //       the warning should be emitted for ^^ since that results in the ASCII
    705         //       character ^ being written to the .res.
    706         if (is_virt and options.diagnostics != null) {
    707             try options.diagnostics.?.diagnostics.append(.{
    708                 .err = .ascii_character_not_equivalent_to_virtual_key_code,
    709                 .type = .warning,
    710                 .code_page = bytes.code_page,
    711                 .token = options.diagnostics.?.token,
    712             });
    713         }
    714 
    715         const c = translator.translate(try parser.next()) orelse return error.InvalidControlCharacter;
    716 
    717         const third_codepoint = translator.translate(try parser.next());
    718         // 0 is treated as a terminator, so a 0 in the third position is fine but
    719         // anything else is too many codepoints for an accelerator
    720         if (third_codepoint != null and third_codepoint.? != 0) return error.InvalidControlCharacter;
    721 
    722         switch (c) {
    723             '^' => return '^', // special case
    724             'a'...'z', 'A'...'Z' => return std.ascii.toUpper(@intCast(c)) - 0x40,
    725             // Note: The Windows RC compiler allows more than just A-Z, but what it allows
    726             //       seems to be tied to some sort of Unicode-aware 'is character' function or something.
    727             //       The full list of codepoints that trigger an out-of-range error can be found here:
    728             //       https://gist.github.com/squeek502/2e9d0a4728a83eed074ad9785a209fd0
    729             //       For codepoints >= 0x80 that don't trigger the error, the Windows RC compiler takes the
    730             //       codepoint and does the `- 0x40` transformation as if it were A-Z which couldn't lead
    731             //       to anything useable, so there's no point in emulating that behavior--erroring for
    732             //       all non-[a-zA-Z] makes much more sense and is what was probably intended by the
    733             //       Windows RC compiler.
    734             else => return error.ControlCharacterOutOfRange,
    735         }
    736         @compileError("this should be unreachable");
    737     }
    738 
    739     const second_codepoint = translator.translate(try parser.next());
    740 
    741     var result: u32 = initial_value: {
    742         if (first_codepoint >= 0x10000) {
    743             if (second_codepoint != null and second_codepoint.? != 0) return error.AcceleratorTooLong;
    744             // No idea why it works this way, but this seems to match the Windows RC
    745             // behavior for codepoints >= 0x10000
    746             const low = @as(u16, @intCast(first_codepoint & 0x3FF)) + 0xDC00;
    747             const extra = (first_codepoint - 0x10000) / 0x400;
    748             break :initial_value low + extra * 0x100;
    749         }
    750         break :initial_value first_codepoint;
    751     };
    752 
    753     // 0 is treated as a terminator
    754     if (second_codepoint != null and second_codepoint.? == 0) return @truncate(result);
    755 
    756     const third_codepoint = translator.translate(try parser.next());
    757     // 0 is treated as a terminator, so a 0 in the third position is fine but
    758     // anything else is too many codepoints for an accelerator
    759     if (third_codepoint != null and third_codepoint.? != 0) return error.AcceleratorTooLong;
    760 
    761     if (second_codepoint) |c| {
    762         if (c >= 0x10000) return error.AcceleratorTooLong;
    763         result <<= 8;
    764         result += c;
    765     } else if (is_virt) {
    766         switch (result) {
    767             'a'...'z' => result -= 0x20, // toUpper
    768             else => {},
    769         }
    770     }
    771     return @truncate(result);
    772 }
    773 
    774 test "accelerator keys" {
    775     try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
    776         .{ .slice = "\"^a\"", .code_page = .windows1252 },
    777         false,
    778         .{ .output_code_page = .windows1252 },
    779     ));
    780     try std.testing.expectEqual(@as(u16, 1), try parseAcceleratorKeyString(
    781         .{ .slice = "\"^A\"", .code_page = .windows1252 },
    782         false,
    783         .{ .output_code_page = .windows1252 },
    784     ));
    785     try std.testing.expectEqual(@as(u16, 26), try parseAcceleratorKeyString(
    786         .{ .slice = "\"^Z\"", .code_page = .windows1252 },
    787         false,
    788         .{ .output_code_page = .windows1252 },
    789     ));
    790     try std.testing.expectEqual(@as(u16, '^'), try parseAcceleratorKeyString(
    791         .{ .slice = "\"^^\"", .code_page = .windows1252 },
    792         false,
    793         .{ .output_code_page = .windows1252 },
    794     ));
    795 
    796     try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
    797         .{ .slice = "\"a\"", .code_page = .windows1252 },
    798         false,
    799         .{ .output_code_page = .windows1252 },
    800     ));
    801     try std.testing.expectEqual(@as(u16, 0x6162), try parseAcceleratorKeyString(
    802         .{ .slice = "\"ab\"", .code_page = .windows1252 },
    803         false,
    804         .{ .output_code_page = .windows1252 },
    805     ));
    806 
    807     try std.testing.expectEqual(@as(u16, 'C'), try parseAcceleratorKeyString(
    808         .{ .slice = "\"c\"", .code_page = .windows1252 },
    809         true,
    810         .{ .output_code_page = .windows1252 },
    811     ));
    812     try std.testing.expectEqual(@as(u16, 0x6363), try parseAcceleratorKeyString(
    813         .{ .slice = "\"cc\"", .code_page = .windows1252 },
    814         true,
    815         .{ .output_code_page = .windows1252 },
    816     ));
    817 
    818     // \x00 or any escape that evaluates to zero acts as a terminator, everything past it
    819     // is ignored
    820     try std.testing.expectEqual(@as(u16, 'a'), try parseAcceleratorKeyString(
    821         .{ .slice = "\"a\\0bcdef\"", .code_page = .windows1252 },
    822         false,
    823         .{ .output_code_page = .windows1252 },
    824     ));
    825 
    826     // \x80 is € in Windows-1252, which is Unicode codepoint 20AC
    827     try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
    828         .{ .slice = "\"\x80\"", .code_page = .windows1252 },
    829         false,
    830         .{ .output_code_page = .windows1252 },
    831     ));
    832     // This depends on the code page, though, with codepage 65001, \x80
    833     // on its own is invalid UTF-8 so it gets converted to the replacement character
    834     try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
    835         .{ .slice = "\"\x80\"", .code_page = .utf8 },
    836         false,
    837         .{ .output_code_page = .windows1252 },
    838     ));
    839     try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
    840         .{ .slice = "\"\x80\x80\"", .code_page = .windows1252 },
    841         false,
    842         .{ .output_code_page = .windows1252 },
    843     ));
    844     // This also behaves the same with escaped characters
    845     try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
    846         .{ .slice = "\"\\x80\"", .code_page = .windows1252 },
    847         false,
    848         .{ .output_code_page = .windows1252 },
    849     ));
    850     // Even with utf8 code page
    851     try std.testing.expectEqual(@as(u16, 0x20AC), try parseAcceleratorKeyString(
    852         .{ .slice = "\"\\x80\"", .code_page = .utf8 },
    853         false,
    854         .{ .output_code_page = .windows1252 },
    855     ));
    856     try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
    857         .{ .slice = "\"\\x80\\x80\"", .code_page = .windows1252 },
    858         false,
    859         .{ .output_code_page = .windows1252 },
    860     ));
    861     // Wide string with the actual characters behaves like the ASCII string version
    862     try std.testing.expectEqual(@as(u16, 0xCCAC), try parseAcceleratorKeyString(
    863         .{ .slice = "L\"\x80\x80\"", .code_page = .windows1252 },
    864         false,
    865         .{ .output_code_page = .windows1252 },
    866     ));
    867     // But wide string with escapes behaves differently
    868     try std.testing.expectEqual(@as(u16, 0x8080), try parseAcceleratorKeyString(
    869         .{ .slice = "L\"\\x80\\x80\"", .code_page = .windows1252 },
    870         false,
    871         .{ .output_code_page = .windows1252 },
    872     ));
    873     // and invalid escapes within wide strings get skipped
    874     try std.testing.expectEqual(@as(u16, 'z'), try parseAcceleratorKeyString(
    875         .{ .slice = "L\"\\Hz\"", .code_page = .windows1252 },
    876         false,
    877         .{ .output_code_page = .windows1252 },
    878     ));
    879 
    880     // any non-A-Z codepoints are illegal
    881     try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
    882         .{ .slice = "\"^\x83\"", .code_page = .windows1252 },
    883         false,
    884         .{ .output_code_page = .windows1252 },
    885     ));
    886     try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
    887         .{ .slice = "\"^1\"", .code_page = .windows1252 },
    888         false,
    889         .{ .output_code_page = .windows1252 },
    890     ));
    891     try std.testing.expectError(error.InvalidControlCharacter, parseAcceleratorKeyString(
    892         .{ .slice = "\"^\"", .code_page = .windows1252 },
    893         false,
    894         .{ .output_code_page = .windows1252 },
    895     ));
    896     try std.testing.expectError(error.EmptyAccelerator, parseAcceleratorKeyString(
    897         .{ .slice = "\"\"", .code_page = .windows1252 },
    898         false,
    899         .{ .output_code_page = .windows1252 },
    900     ));
    901     try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
    902         .{ .slice = "\"hello\"", .code_page = .windows1252 },
    903         false,
    904         .{ .output_code_page = .windows1252 },
    905     ));
    906     try std.testing.expectError(error.ControlCharacterOutOfRange, parseAcceleratorKeyString(
    907         .{ .slice = "\"^\x80\"", .code_page = .windows1252 },
    908         false,
    909         .{ .output_code_page = .windows1252 },
    910     ));
    911 
    912     // Invalid UTF-8 gets converted to 0xFFFD, multiple invalids get shifted and added together
    913     // The behavior is the same for ascii and wide strings
    914     try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
    915         .{ .slice = "\"\x80\x80\"", .code_page = .utf8 },
    916         false,
    917         .{ .output_code_page = .windows1252 },
    918     ));
    919     try std.testing.expectEqual(@as(u16, 0xFCFD), try parseAcceleratorKeyString(
    920         .{ .slice = "L\"\x80\x80\"", .code_page = .utf8 },
    921         false,
    922         .{ .output_code_page = .windows1252 },
    923     ));
    924 
    925     // Codepoints >= 0x10000
    926     try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
    927         .{ .slice = "\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
    928         false,
    929         .{ .output_code_page = .windows1252 },
    930     ));
    931     try std.testing.expectEqual(@as(u16, 0xDD00), try parseAcceleratorKeyString(
    932         .{ .slice = "L\"\xF0\x90\x84\x80\"", .code_page = .utf8 },
    933         false,
    934         .{ .output_code_page = .windows1252 },
    935     ));
    936     try std.testing.expectEqual(@as(u16, 0x9C01), try parseAcceleratorKeyString(
    937         .{ .slice = "\"\xF4\x80\x80\x81\"", .code_page = .utf8 },
    938         false,
    939         .{ .output_code_page = .windows1252 },
    940     ));
    941     // anything before or after a codepoint >= 0x10000 causes an error
    942     try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
    943         .{ .slice = "\"a\xF0\x90\x80\x80\"", .code_page = .utf8 },
    944         false,
    945         .{ .output_code_page = .windows1252 },
    946     ));
    947     try std.testing.expectError(error.AcceleratorTooLong, parseAcceleratorKeyString(
    948         .{ .slice = "\"\xF0\x90\x80\x80a\"", .code_page = .utf8 },
    949         false,
    950         .{ .output_code_page = .windows1252 },
    951     ));
    952 
    953     // Misc special cases
    954     try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
    955         .{ .slice = "\"\\777\"", .code_page = .utf8 },
    956         false,
    957         .{ .output_code_page = .utf8 },
    958     ));
    959     try std.testing.expectEqual(@as(u16, 0xFFFF), try parseAcceleratorKeyString(
    960         .{ .slice = "L\"\\7777777\"", .code_page = .utf8 },
    961         false,
    962         .{ .output_code_page = .utf8 },
    963     ));
    964     try std.testing.expectEqual(@as(u16, 0x01), try parseAcceleratorKeyString(
    965         .{ .slice = "L\"\\200001\"", .code_page = .utf8 },
    966         false,
    967         .{ .output_code_page = .utf8 },
    968     ));
    969     // Escape of a codepoint >= 0x10000 omits the high surrogate pair
    970     try std.testing.expectEqual(@as(u16, 0xDF48), try parseAcceleratorKeyString(
    971         .{ .slice = "L\"\\𐍈\"", .code_page = .utf8 },
    972         false,
    973         .{ .output_code_page = .utf8 },
    974     ));
    975     // Invalid escape code is skipped, allows for 2 codepoints afterwards
    976     try std.testing.expectEqual(@as(u16, 0x7878), try parseAcceleratorKeyString(
    977         .{ .slice = "L\"\\kxx\"", .code_page = .utf8 },
    978         false,
    979         .{ .output_code_page = .utf8 },
    980     ));
    981     // Escape of a codepoint >= 0x10000 allows for a codepoint afterwards
    982     try std.testing.expectEqual(@as(u16, 0x4878), try parseAcceleratorKeyString(
    983         .{ .slice = "L\"\\𐍈x\"", .code_page = .utf8 },
    984         false,
    985         .{ .output_code_page = .utf8 },
    986     ));
    987     // Input code page of 1252, output code page of utf-8
    988     try std.testing.expectEqual(@as(u16, 0xFFFD), try parseAcceleratorKeyString(
    989         .{ .slice = "\"\\270\"", .code_page = .windows1252 },
    990         false,
    991         .{ .output_code_page = .utf8 },
    992     ));
    993 }
    994 
    995 pub const ForcedOrdinal = struct {
    996     pub fn fromBytes(bytes: SourceBytes) u16 {
    997         var i: usize = 0;
    998         var result: u21 = 0;
    999         while (bytes.code_page.codepointAt(i, bytes.slice)) |codepoint| : (i += codepoint.byte_len) {
   1000             const c = switch (codepoint.value) {
   1001                 // Codepoints that would need a surrogate pair in UTF-16 are
   1002                 // broken up into their UTF-16 code units and each code unit
   1003                 // is interpreted as a digit.
   1004                 0x10000...0x10FFFF => {
   1005                     const high = @as(u16, @intCast((codepoint.value - 0x10000) >> 10)) + 0xD800;
   1006                     if (result != 0) result *%= 10;
   1007                     result +%= high -% '0';
   1008 
   1009                     const low = @as(u16, @intCast(codepoint.value & 0x3FF)) + 0xDC00;
   1010                     if (result != 0) result *%= 10;
   1011                     result +%= low -% '0';
   1012                     continue;
   1013                 },
   1014                 Codepoint.invalid => 0xFFFD,
   1015                 else => codepoint.value,
   1016             };
   1017             if (result != 0) result *%= 10;
   1018             result +%= c -% '0';
   1019         }
   1020         return @truncate(result);
   1021     }
   1022 
   1023     pub fn fromUtf16Le(utf16: [:0]const u16) u16 {
   1024         var result: u16 = 0;
   1025         for (utf16) |code_unit| {
   1026             if (result != 0) result *%= 10;
   1027             result +%= std.mem.littleToNative(u16, code_unit) -% '0';
   1028         }
   1029         return result;
   1030     }
   1031 };
   1032 
   1033 test "forced ordinal" {
   1034     try std.testing.expectEqual(@as(u16, 3200), ForcedOrdinal.fromBytes(.{ .slice = "3200", .code_page = .windows1252 }));
   1035     try std.testing.expectEqual(@as(u16, 0x33), ForcedOrdinal.fromBytes(.{ .slice = "1+1", .code_page = .windows1252 }));
   1036     try std.testing.expectEqual(@as(u16, 65531), ForcedOrdinal.fromBytes(.{ .slice = "1!", .code_page = .windows1252 }));
   1037 
   1038     try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0\x8C", .code_page = .windows1252 }));
   1039     try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromBytes(.{ .slice = "0Œ", .code_page = .utf8 }));
   1040 
   1041     // invalid UTF-8 gets converted to 0xFFFD (replacement char) and then interpreted as a digit
   1042     try std.testing.expectEqual(@as(u16, 0xFFCD), ForcedOrdinal.fromBytes(.{ .slice = "0\x81", .code_page = .utf8 }));
   1043     // codepoints >= 0x10000
   1044     try std.testing.expectEqual(@as(u16, 0x49F2), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10002}", .code_page = .utf8 }));
   1045     try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromBytes(.{ .slice = "0\u{10100}", .code_page = .utf8 }));
   1046 
   1047     // From UTF-16
   1048     try std.testing.expectEqual(@as(u16, 0x122), ForcedOrdinal.fromUtf16Le(&[_:0]u16{ std.mem.nativeToLittle(u16, '0'), std.mem.nativeToLittle(u16, 'Œ') }));
   1049     try std.testing.expectEqual(@as(u16, 0x4AF0), ForcedOrdinal.fromUtf16Le(std.unicode.utf8ToUtf16LeStringLiteral("0\u{10100}")));
   1050 }
   1051 
   1052 /// https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
   1053 pub const FixedFileInfo = struct {
   1054     file_version: Version = .{},
   1055     product_version: Version = .{},
   1056     file_flags_mask: u32 = 0,
   1057     file_flags: u32 = 0,
   1058     file_os: u32 = 0,
   1059     file_type: u32 = 0,
   1060     file_subtype: u32 = 0,
   1061     file_date: Version = .{}, // TODO: I think this is always all zeroes?
   1062 
   1063     pub const signature = 0xFEEF04BD;
   1064     // Note: This corresponds to a version of 1.0
   1065     pub const version = 0x00010000;
   1066 
   1067     pub const byte_len = 0x34;
   1068     pub const key = std.unicode.utf8ToUtf16LeStringLiteral("VS_VERSION_INFO");
   1069 
   1070     pub const Version = struct {
   1071         parts: [4]u16 = [_]u16{0} ** 4,
   1072 
   1073         pub fn mostSignificantCombinedParts(self: Version) u32 {
   1074             return (@as(u32, self.parts[0]) << 16) + self.parts[1];
   1075         }
   1076 
   1077         pub fn leastSignificantCombinedParts(self: Version) u32 {
   1078             return (@as(u32, self.parts[2]) << 16) + self.parts[3];
   1079         }
   1080     };
   1081 
   1082     pub fn write(self: FixedFileInfo, writer: anytype) !void {
   1083         try writer.writeInt(u32, signature, .little);
   1084         try writer.writeInt(u32, version, .little);
   1085         try writer.writeInt(u32, self.file_version.mostSignificantCombinedParts(), .little);
   1086         try writer.writeInt(u32, self.file_version.leastSignificantCombinedParts(), .little);
   1087         try writer.writeInt(u32, self.product_version.mostSignificantCombinedParts(), .little);
   1088         try writer.writeInt(u32, self.product_version.leastSignificantCombinedParts(), .little);
   1089         try writer.writeInt(u32, self.file_flags_mask, .little);
   1090         try writer.writeInt(u32, self.file_flags, .little);
   1091         try writer.writeInt(u32, self.file_os, .little);
   1092         try writer.writeInt(u32, self.file_type, .little);
   1093         try writer.writeInt(u32, self.file_subtype, .little);
   1094         try writer.writeInt(u32, self.file_date.mostSignificantCombinedParts(), .little);
   1095         try writer.writeInt(u32, self.file_date.leastSignificantCombinedParts(), .little);
   1096     }
   1097 };
   1098 
   1099 test "FixedFileInfo.Version" {
   1100     const version = FixedFileInfo.Version{
   1101         .parts = .{ 1, 2, 3, 4 },
   1102     };
   1103     try std.testing.expectEqual(@as(u32, 0x00010002), version.mostSignificantCombinedParts());
   1104     try std.testing.expectEqual(@as(u32, 0x00030004), version.leastSignificantCombinedParts());
   1105 }
   1106 
   1107 pub const VersionNode = struct {
   1108     pub const type_string: u16 = 1;
   1109     pub const type_binary: u16 = 0;
   1110 };
   1111 
   1112 pub const MenuItemFlags = struct {
   1113     value: u16 = 0,
   1114 
   1115     pub fn apply(self: *MenuItemFlags, option: rc.MenuItem.Option) void {
   1116         self.value |= optionValue(option);
   1117     }
   1118 
   1119     pub fn isSet(self: MenuItemFlags, option: rc.MenuItem.Option) bool {
   1120         return self.value & optionValue(option) != 0;
   1121     }
   1122 
   1123     fn optionValue(option: rc.MenuItem.Option) u16 {
   1124         return @intCast(switch (option) {
   1125             .checked => MF.CHECKED,
   1126             .grayed => MF.GRAYED,
   1127             .help => MF.HELP,
   1128             .inactive => MF.DISABLED,
   1129             .menubarbreak => MF.MENUBARBREAK,
   1130             .menubreak => MF.MENUBREAK,
   1131         });
   1132     }
   1133 
   1134     pub fn markLast(self: *MenuItemFlags) void {
   1135         self.value |= @intCast(MF.END);
   1136     }
   1137 };
   1138 
   1139 /// Menu Flags from WinUser.h
   1140 /// This is not complete, it only contains what is needed
   1141 pub const MF = struct {
   1142     pub const GRAYED: u32 = 0x00000001;
   1143     pub const DISABLED: u32 = 0x00000002;
   1144     pub const CHECKED: u32 = 0x00000008;
   1145     pub const POPUP: u32 = 0x00000010;
   1146     pub const MENUBARBREAK: u32 = 0x00000020;
   1147     pub const MENUBREAK: u32 = 0x00000040;
   1148     pub const HELP: u32 = 0x00004000;
   1149     pub const END: u32 = 0x00000080;
   1150 };
   1151 
   1152 /// Window Styles from WinUser.h
   1153 pub const WS = struct {
   1154     pub const OVERLAPPED: u32 = 0x00000000;
   1155     pub const POPUP: u32 = 0x80000000;
   1156     pub const CHILD: u32 = 0x40000000;
   1157     pub const MINIMIZE: u32 = 0x20000000;
   1158     pub const VISIBLE: u32 = 0x10000000;
   1159     pub const DISABLED: u32 = 0x08000000;
   1160     pub const CLIPSIBLINGS: u32 = 0x04000000;
   1161     pub const CLIPCHILDREN: u32 = 0x02000000;
   1162     pub const MAXIMIZE: u32 = 0x01000000;
   1163     pub const CAPTION: u32 = BORDER | DLGFRAME;
   1164     pub const BORDER: u32 = 0x00800000;
   1165     pub const DLGFRAME: u32 = 0x00400000;
   1166     pub const VSCROLL: u32 = 0x00200000;
   1167     pub const HSCROLL: u32 = 0x00100000;
   1168     pub const SYSMENU: u32 = 0x00080000;
   1169     pub const THICKFRAME: u32 = 0x00040000;
   1170     pub const GROUP: u32 = 0x00020000;
   1171     pub const TABSTOP: u32 = 0x00010000;
   1172 
   1173     pub const MINIMIZEBOX: u32 = 0x00020000;
   1174     pub const MAXIMIZEBOX: u32 = 0x00010000;
   1175 
   1176     pub const TILED: u32 = OVERLAPPED;
   1177     pub const ICONIC: u32 = MINIMIZE;
   1178     pub const SIZEBOX: u32 = THICKFRAME;
   1179     pub const TILEDWINDOW: u32 = OVERLAPPEDWINDOW;
   1180 
   1181     // Common Window Styles
   1182     pub const OVERLAPPEDWINDOW: u32 = OVERLAPPED | CAPTION | SYSMENU | THICKFRAME | MINIMIZEBOX | MAXIMIZEBOX;
   1183     pub const POPUPWINDOW: u32 = POPUP | BORDER | SYSMENU;
   1184     pub const CHILDWINDOW: u32 = CHILD;
   1185 };
   1186 
   1187 /// Dialog Box Template Styles from WinUser.h
   1188 pub const DS = struct {
   1189     pub const SETFONT: u32 = 0x40;
   1190 };
   1191 
   1192 /// Button Control Styles from WinUser.h
   1193 /// This is not complete, it only contains what is needed
   1194 pub const BS = struct {
   1195     pub const PUSHBUTTON: u32 = 0x00000000;
   1196     pub const DEFPUSHBUTTON: u32 = 0x00000001;
   1197     pub const CHECKBOX: u32 = 0x00000002;
   1198     pub const AUTOCHECKBOX: u32 = 0x00000003;
   1199     pub const RADIOBUTTON: u32 = 0x00000004;
   1200     pub const @"3STATE": u32 = 0x00000005;
   1201     pub const AUTO3STATE: u32 = 0x00000006;
   1202     pub const GROUPBOX: u32 = 0x00000007;
   1203     pub const USERBUTTON: u32 = 0x00000008;
   1204     pub const AUTORADIOBUTTON: u32 = 0x00000009;
   1205     pub const PUSHBOX: u32 = 0x0000000A;
   1206     pub const OWNERDRAW: u32 = 0x0000000B;
   1207     pub const TYPEMASK: u32 = 0x0000000F;
   1208     pub const LEFTTEXT: u32 = 0x00000020;
   1209 };
   1210 
   1211 /// Static Control Constants from WinUser.h
   1212 /// This is not complete, it only contains what is needed
   1213 pub const SS = struct {
   1214     pub const LEFT: u32 = 0x00000000;
   1215     pub const CENTER: u32 = 0x00000001;
   1216     pub const RIGHT: u32 = 0x00000002;
   1217     pub const ICON: u32 = 0x00000003;
   1218 };
   1219 
   1220 /// Listbox Styles from WinUser.h
   1221 /// This is not complete, it only contains what is needed
   1222 pub const LBS = struct {
   1223     pub const NOTIFY: u32 = 0x0001;
   1224 };