master
   1const std = @import("std");
   2const assert = std.debug.assert;
   3const rc = @import("rc.zig");
   4const ResourceType = rc.ResourceType;
   5const CommonResourceAttributes = rc.CommonResourceAttributes;
   6const Allocator = std.mem.Allocator;
   7const windows1252 = @import("windows1252.zig");
   8const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
   9const literals = @import("literals.zig");
  10const SourceBytes = literals.SourceBytes;
  11const Codepoint = @import("code_pages.zig").Codepoint;
  12const lang = @import("lang.zig");
  13const isNonAsciiDigit = @import("utils.zig").isNonAsciiDigit;
  14
  15/// https://learn.microsoft.com/en-us/windows/win32/menurc/resource-types
  16pub 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
  80pub 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
 149pub 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
 183pub 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
 235pub 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: *std.Io.Writer) !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: *std.Io.Writer) !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.ArrayList(u16).initCapacity(allocator, @min(257, bytes.slice.len));
 287        errdefer buf.deinit(allocator);
 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(allocator, std.mem.nativeToLittle(u16, '�'));
 296            } else if (c < 0x7F) {
 297                // ASCII chars in names are always converted to uppercase
 298                try buf.append(allocator, std.mem.nativeToLittle(u16, std.ascii.toUpper(@intCast(c))));
 299            } else if (c < 0x10000) {
 300                const short: u16 = @intCast(c);
 301                try buf.append(allocator, std.mem.nativeToLittle(u16, short));
 302            } else {
 303                const high = @as(u16, @intCast((c - 0x10000) >> 10)) + 0xD800;
 304                try buf.append(allocator, 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(allocator, std.mem.nativeToLittle(u16, low));
 312            }
 313        }
 314
 315        return NameOrOrdinal{ .name = try buf.toOwnedSliceSentinel(allocator, 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.Alt(NameOrOrdinal, formatResourceType) {
 469        return .{ .data = type_value };
 470    }
 471};
 472
 473fn 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
 486test "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
 581test "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
 609pub 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
 649const 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
 683pub 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"").
 686pub 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
 774test "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
 995pub 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
1033test "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 = "", .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
1053pub 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: *std.Io.Writer) !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
1099test "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
1107pub const VersionNode = struct {
1108    pub const type_string: u16 = 1;
1109    pub const type_binary: u16 = 0;
1110};
1111
1112pub 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
1141pub 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
1153pub 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
1188pub 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
1194pub 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
1213pub 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
1222pub const LBS = struct {
1223    pub const NOTIFY: u32 = 0x0001;
1224};