zig

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

blob 0c71b020 (169832B) - Raw


      1 const std = @import("std");
      2 const builtin = @import("builtin");
      3 const Allocator = std.mem.Allocator;
      4 const Node = @import("ast.zig").Node;
      5 const lex = @import("lex.zig");
      6 const Parser = @import("parse.zig").Parser;
      7 const Resource = @import("rc.zig").Resource;
      8 const Token = @import("lex.zig").Token;
      9 const literals = @import("literals.zig");
     10 const Number = literals.Number;
     11 const SourceBytes = literals.SourceBytes;
     12 const Diagnostics = @import("errors.zig").Diagnostics;
     13 const ErrorDetails = @import("errors.zig").ErrorDetails;
     14 const MemoryFlags = @import("res.zig").MemoryFlags;
     15 const rc = @import("rc.zig");
     16 const res = @import("res.zig");
     17 const ico = @import("ico.zig");
     18 const ani = @import("ani.zig");
     19 const bmp = @import("bmp.zig");
     20 const WORD = std.os.windows.WORD;
     21 const DWORD = std.os.windows.DWORD;
     22 const utils = @import("utils.zig");
     23 const NameOrOrdinal = res.NameOrOrdinal;
     24 const CodePage = @import("code_pages.zig").CodePage;
     25 const CodePageLookup = @import("ast.zig").CodePageLookup;
     26 const SourceMappings = @import("source_mapping.zig").SourceMappings;
     27 const windows1252 = @import("windows1252.zig");
     28 const lang = @import("lang.zig");
     29 const code_pages = @import("code_pages.zig");
     30 const errors = @import("errors.zig");
     31 const native_endian = builtin.cpu.arch.endian();
     32 
     33 pub const CompileOptions = struct {
     34     cwd: std.fs.Dir,
     35     diagnostics: *Diagnostics,
     36     source_mappings: ?*SourceMappings = null,
     37     /// List of paths (absolute or relative to `cwd`) for every file that the resources within the .rc file depend on.
     38     /// Items within the list will be allocated using the allocator of the ArrayList and must be
     39     /// freed by the caller.
     40     /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
     41     dependencies_list: ?*std.ArrayList([]const u8) = null,
     42     default_code_page: CodePage = .windows1252,
     43     ignore_include_env_var: bool = false,
     44     extra_include_paths: []const []const u8 = &.{},
     45     /// This is just an API convenience to allow separately passing 'system' (i.e. those
     46     /// that would normally be gotten from the INCLUDE env var) include paths. This is mostly
     47     /// intended for use when setting `ignore_include_env_var = true`. When `ignore_include_env_var`
     48     /// is false, `system_include_paths` will be searched before the paths in the INCLUDE env var.
     49     system_include_paths: []const []const u8 = &.{},
     50     default_language_id: ?u16 = null,
     51     // TODO: Implement verbose output
     52     verbose: bool = false,
     53     null_terminate_string_table_strings: bool = false,
     54     /// Note: This is a u15 to ensure that the maximum number of UTF-16 code units
     55     ///       plus a null-terminator can always fit into a u16.
     56     max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
     57     silent_duplicate_control_ids: bool = false,
     58     warn_instead_of_error_on_invalid_code_page: bool = false,
     59 };
     60 
     61 pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, options: CompileOptions) !void {
     62     var lexer = lex.Lexer.init(source, .{
     63         .default_code_page = options.default_code_page,
     64         .source_mappings = options.source_mappings,
     65         .max_string_literal_codepoints = options.max_string_literal_codepoints,
     66     });
     67     var parser = Parser.init(&lexer, .{
     68         .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
     69     });
     70     var tree = try parser.parse(allocator, options.diagnostics);
     71     defer tree.deinit();
     72 
     73     var search_dirs = std.ArrayList(SearchDir).init(allocator);
     74     defer {
     75         for (search_dirs.items) |*search_dir| {
     76             search_dir.deinit(allocator);
     77         }
     78         search_dirs.deinit();
     79     }
     80 
     81     if (options.source_mappings) |source_mappings| {
     82         const root_path = source_mappings.files.get(source_mappings.root_filename_offset);
     83         // If dirname returns null, then the root path will be the same as
     84         // the cwd so we don't need to add it as a distinct search path.
     85         if (std.fs.path.dirname(root_path)) |root_dir_path| {
     86             var root_dir = try options.cwd.openDir(root_dir_path, .{});
     87             errdefer root_dir.close();
     88             try search_dirs.append(.{ .dir = root_dir, .path = try allocator.dupe(u8, root_dir_path) });
     89         }
     90     }
     91     // Re-open the passed in cwd since we want to be able to close it (std.fs.cwd() shouldn't be closed)
     92     const cwd_dir = options.cwd.openDir(".", .{}) catch |err| {
     93         try options.diagnostics.append(.{
     94             .err = .failed_to_open_cwd,
     95             .token = .{
     96                 .id = .invalid,
     97                 .start = 0,
     98                 .end = 0,
     99                 .line_number = 1,
    100             },
    101             .print_source_line = false,
    102             .extra = .{ .file_open_error = .{
    103                 .err = ErrorDetails.FileOpenError.enumFromError(err),
    104                 .filename_string_index = undefined,
    105             } },
    106         });
    107         return error.CompileError;
    108     };
    109     try search_dirs.append(.{ .dir = cwd_dir, .path = null });
    110     for (options.extra_include_paths) |extra_include_path| {
    111         var dir = openSearchPathDir(options.cwd, extra_include_path) catch {
    112             // TODO: maybe a warning that the search path is skipped?
    113             continue;
    114         };
    115         errdefer dir.close();
    116         try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, extra_include_path) });
    117     }
    118     for (options.system_include_paths) |system_include_path| {
    119         var dir = openSearchPathDir(options.cwd, system_include_path) catch {
    120             // TODO: maybe a warning that the search path is skipped?
    121             continue;
    122         };
    123         errdefer dir.close();
    124         try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, system_include_path) });
    125     }
    126     if (!options.ignore_include_env_var) {
    127         const INCLUDE = std.process.getEnvVarOwned(allocator, "INCLUDE") catch "";
    128         defer allocator.free(INCLUDE);
    129 
    130         // The only precedence here is llvm-rc which also uses the platform-specific
    131         // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
    132         const delimiter = switch (builtin.os.tag) {
    133             .windows => ';',
    134             else => ':',
    135         };
    136         var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
    137         while (it.next()) |search_path| {
    138             var dir = openSearchPathDir(options.cwd, search_path) catch continue;
    139             errdefer dir.close();
    140             try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, search_path) });
    141         }
    142     }
    143 
    144     var arena_allocator = std.heap.ArenaAllocator.init(allocator);
    145     defer arena_allocator.deinit();
    146     const arena = arena_allocator.allocator();
    147 
    148     var compiler = Compiler{
    149         .source = source,
    150         .arena = arena,
    151         .allocator = allocator,
    152         .cwd = options.cwd,
    153         .diagnostics = options.diagnostics,
    154         .dependencies_list = options.dependencies_list,
    155         .input_code_pages = &tree.input_code_pages,
    156         .output_code_pages = &tree.output_code_pages,
    157         // This is only safe because we know search_dirs won't be modified past this point
    158         .search_dirs = search_dirs.items,
    159         .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
    160         .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
    161     };
    162     if (options.default_language_id) |default_language_id| {
    163         compiler.state.language = res.Language.fromInt(default_language_id);
    164     }
    165 
    166     try compiler.writeRoot(tree.root(), writer);
    167 }
    168 
    169 pub const Compiler = struct {
    170     source: []const u8,
    171     arena: Allocator,
    172     allocator: Allocator,
    173     cwd: std.fs.Dir,
    174     state: State = .{},
    175     diagnostics: *Diagnostics,
    176     dependencies_list: ?*std.ArrayList([]const u8),
    177     input_code_pages: *const CodePageLookup,
    178     output_code_pages: *const CodePageLookup,
    179     search_dirs: []SearchDir,
    180     null_terminate_string_table_strings: bool,
    181     silent_duplicate_control_ids: bool,
    182 
    183     pub const State = struct {
    184         icon_id: u16 = 1,
    185         string_tables: StringTablesByLanguage = .{},
    186         language: res.Language = .{},
    187         font_dir: FontDir = .{},
    188         version: u32 = 0,
    189         characteristics: u32 = 0,
    190     };
    191 
    192     pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: anytype) !void {
    193         try writeEmptyResource(writer);
    194         for (root.body) |node| {
    195             try self.writeNode(node, writer);
    196         }
    197 
    198         // now write the FONTDIR (if it has anything in it)
    199         try self.state.font_dir.writeResData(self, writer);
    200         if (self.state.font_dir.fonts.items.len != 0) {
    201             // The Win32 RC compiler may write a different FONTDIR resource than us,
    202             // due to it sometimes writing a non-zero-length device name/face name
    203             // whereas we *always* write them both as zero-length.
    204             //
    205             // In practical terms, this doesn't matter, since for various reasons the format
    206             // of the FONTDIR cannot be relied on and is seemingly not actually used by anything
    207             // anymore. We still want to emit some sort of diagnostic for the purposes of being able
    208             // to know that our .RES is intentionally not meant to be byte-for-byte identical with
    209             // the rc.exe output.
    210             //
    211             // By using the hint type here, we allow this diagnostic to be detected in code,
    212             // but it will not be printed since the end-user doesn't need to care.
    213             try self.addErrorDetails(.{
    214                 .err = .result_contains_fontdir,
    215                 .type = .hint,
    216                 .token = undefined,
    217             });
    218         }
    219         // once we've written every else out, we can write out the finalized STRINGTABLE resources
    220         var string_tables_it = self.state.string_tables.tables.iterator();
    221         while (string_tables_it.next()) |string_table_entry| {
    222             var string_table_it = string_table_entry.value_ptr.blocks.iterator();
    223             while (string_table_it.next()) |entry| {
    224                 try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
    225             }
    226         }
    227     }
    228 
    229     pub fn writeNode(self: *Compiler, node: *Node, writer: anytype) !void {
    230         switch (node.id) {
    231             .root => unreachable, // writeRoot should be called directly instead
    232             .resource_external => try self.writeResourceExternal(@fieldParentPtr(Node.ResourceExternal, "base", node), writer),
    233             .resource_raw_data => try self.writeResourceRawData(@fieldParentPtr(Node.ResourceRawData, "base", node), writer),
    234             .literal => unreachable, // this is context dependent and should be handled by its parent
    235             .binary_expression => unreachable,
    236             .grouped_expression => unreachable,
    237             .not_expression => unreachable,
    238             .invalid => {}, // no-op, currently only used for dangling literals at EOF
    239             .accelerators => try self.writeAccelerators(@fieldParentPtr(Node.Accelerators, "base", node), writer),
    240             .accelerator => unreachable, // handled by writeAccelerators
    241             .dialog => try self.writeDialog(@fieldParentPtr(Node.Dialog, "base", node), writer),
    242             .control_statement => unreachable,
    243             .toolbar => try self.writeToolbar(@fieldParentPtr(Node.Toolbar, "base", node), writer),
    244             .menu => try self.writeMenu(@fieldParentPtr(Node.Menu, "base", node), writer),
    245             .menu_item => unreachable,
    246             .menu_item_separator => unreachable,
    247             .menu_item_ex => unreachable,
    248             .popup => unreachable,
    249             .popup_ex => unreachable,
    250             .version_info => try self.writeVersionInfo(@fieldParentPtr(Node.VersionInfo, "base", node), writer),
    251             .version_statement => unreachable,
    252             .block => unreachable,
    253             .block_value => unreachable,
    254             .block_value_value => unreachable,
    255             .string_table => try self.writeStringTable(@fieldParentPtr(Node.StringTable, "base", node)),
    256             .string_table_string => unreachable, // handled by writeStringTable
    257             .language_statement => self.writeLanguageStatement(@fieldParentPtr(Node.LanguageStatement, "base", node)),
    258             .font_statement => unreachable,
    259             .simple_statement => self.writeTopLevelSimpleStatement(@fieldParentPtr(Node.SimpleStatement, "base", node)),
    260         }
    261     }
    262 
    263     /// Returns the filename encoded as UTF-8 (allocated by self.allocator)
    264     pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
    265         switch (expression_node.id) {
    266             .literal => {
    267                 const literal_node = expression_node.cast(.literal).?;
    268                 switch (literal_node.token.id) {
    269                     .literal, .number => {
    270                         const slice = literal_node.token.slice(self.source);
    271                         const code_page = self.input_code_pages.getForToken(literal_node.token);
    272                         var buf = try std.ArrayList(u8).initCapacity(self.allocator, slice.len);
    273                         errdefer buf.deinit();
    274 
    275                         var index: usize = 0;
    276                         while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
    277                             const c = codepoint.value;
    278                             if (c == code_pages.Codepoint.invalid) {
    279                                 try buf.appendSlice("�");
    280                             } else {
    281                                 // Anything that is not returned as an invalid codepoint must be encodable as UTF-8.
    282                                 const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
    283                                 try buf.ensureUnusedCapacity(utf8_len);
    284                                 _ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
    285                                 buf.items.len += utf8_len;
    286                             }
    287                         }
    288 
    289                         return buf.toOwnedSlice();
    290                     },
    291                     .quoted_ascii_string, .quoted_wide_string => {
    292                         const slice = literal_node.token.slice(self.source);
    293                         const column = literal_node.token.calculateColumn(self.source, 8, null);
    294                         const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
    295 
    296                         var buf = std.ArrayList(u8).init(self.allocator);
    297                         errdefer buf.deinit();
    298 
    299                         // Filenames are sort-of parsed as if they were wide strings, but the max escape width of
    300                         // hex/octal escapes is still determined by the L prefix. Since we want to end up with
    301                         // UTF-8, we can parse either string type directly to UTF-8.
    302                         var parser = literals.IterativeStringParser.init(bytes, .{
    303                             .start_column = column,
    304                             .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
    305                         });
    306 
    307                         while (try parser.nextUnchecked()) |parsed| {
    308                             const c = parsed.codepoint;
    309                             if (c == code_pages.Codepoint.invalid) {
    310                                 try buf.appendSlice("�");
    311                             } else {
    312                                 var codepoint_buf: [4]u8 = undefined;
    313                                 // If the codepoint cannot be encoded, we fall back to �
    314                                 if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
    315                                     try buf.appendSlice(codepoint_buf[0..len]);
    316                                 } else |_| {
    317                                     try buf.appendSlice("�");
    318                                 }
    319                             }
    320                         }
    321 
    322                         return buf.toOwnedSlice();
    323                     },
    324                     else => {
    325                         std.debug.print("unexpected filename token type: {}\n", .{literal_node.token});
    326                         unreachable; // no other token types should be in a filename literal node
    327                     },
    328                 }
    329             },
    330             .binary_expression => {
    331                 const binary_expression_node = expression_node.cast(.binary_expression).?;
    332                 return self.evaluateFilenameExpression(binary_expression_node.right);
    333             },
    334             .grouped_expression => {
    335                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
    336                 return self.evaluateFilenameExpression(grouped_expression_node.expression);
    337             },
    338             else => unreachable,
    339         }
    340     }
    341 
    342     /// https://learn.microsoft.com/en-us/windows/win32/menurc/searching-for-files
    343     ///
    344     /// Searches, in this order:
    345     ///  Directory of the 'root' .rc file (if different from CWD)
    346     ///  CWD
    347     ///  extra_include_paths (resolved relative to CWD)
    348     ///  system_include_paths (resolve relative to CWD)
    349     ///  INCLUDE environment var paths (only if ignore_include_env_var is false; resolved relative to CWD)
    350     ///
    351     /// Note: The CWD being searched *in addition to* the directory of the 'root' .rc file
    352     ///       is also how the Win32 RC compiler preprocessor searches for includes, but that
    353     ///       differs from how the clang preprocessor searches for includes.
    354     ///
    355     /// Note: This will always return the first matching file that can be opened.
    356     ///       This matches the Win32 RC compiler, which will fail with an error if the first
    357     ///       matching file is invalid. That is, it does not do the `cmd` PATH searching
    358     ///       thing of continuing to look for matching files until it finds a valid
    359     ///       one if a matching file is invalid.
    360     fn searchForFile(self: *Compiler, path: []const u8) !std.fs.File {
    361         // If the path is absolute, then it is not resolved relative to any search
    362         // paths, so there's no point in checking them.
    363         //
    364         // This behavior was determined/confirmed with the following test:
    365         // - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
    366         // - A `test.bin` file at `C:\test.bin`
    367         // - A `test.bin` file at `inc\test.bin` relative to the .rc file
    368         // - Invoking `rc` with `rc /i inc test.rc`
    369         //
    370         // This results in a .res file with the contents of `C:\test.bin`, not
    371         // the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
    372         // then it start failing to find `/test.bin`, meaning that it does not resolve
    373         // `/test.bin` relative to include paths and instead only treats it as
    374         // an absolute path.
    375         if (std.fs.path.isAbsolute(path)) {
    376             const file = try utils.openFileNotDir(std.fs.cwd(), path, .{});
    377             errdefer file.close();
    378 
    379             if (self.dependencies_list) |dependencies_list| {
    380                 const duped_path = try dependencies_list.allocator.dupe(u8, path);
    381                 errdefer dependencies_list.allocator.free(duped_path);
    382                 try dependencies_list.append(duped_path);
    383             }
    384         }
    385 
    386         var first_error: ?std.fs.File.OpenError = null;
    387         for (self.search_dirs) |search_dir| {
    388             if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| {
    389                 errdefer file.close();
    390 
    391                 if (self.dependencies_list) |dependencies_list| {
    392                     const searched_file_path = try std.fs.path.join(dependencies_list.allocator, &.{
    393                         search_dir.path orelse "", path,
    394                     });
    395                     errdefer dependencies_list.allocator.free(searched_file_path);
    396                     try dependencies_list.append(searched_file_path);
    397                 }
    398 
    399                 return file;
    400             } else |err| if (first_error == null) {
    401                 first_error = err;
    402             }
    403         }
    404         return first_error orelse error.FileNotFound;
    405     }
    406 
    407     pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
    408         // Init header with data size zero for now, will need to fill it in later
    409         var header = try self.resourceHeader(node.id, node.type, .{});
    410         defer header.deinit(self.allocator);
    411 
    412         const maybe_predefined_type = header.predefinedResourceType();
    413 
    414         // DLGINCLUDE has special handling that doesn't actually need the file to exist
    415         if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
    416             const filename_token = node.filename.cast(.literal).?.token;
    417             const parsed_filename = try self.parseQuotedStringAsAsciiString(filename_token);
    418             defer self.allocator.free(parsed_filename);
    419 
    420             header.applyMemoryFlags(node.common_resource_attributes, self.source);
    421             header.data_size = @intCast(parsed_filename.len + 1);
    422             try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    423             try writer.writeAll(parsed_filename);
    424             try writer.writeByte(0);
    425             try writeDataPadding(writer, header.data_size);
    426             return;
    427         }
    428 
    429         const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
    430         defer self.allocator.free(filename_utf8);
    431 
    432         // TODO: More robust checking of the validity of the filename.
    433         //       This currently only checks for NUL bytes, but it should probably also check for
    434         //       platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
    435         //       Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
    436         if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
    437             return self.addErrorDetailsAndFail(.{
    438                 .err = .invalid_filename,
    439                 .token = node.filename.getFirstToken(),
    440                 .token_span_end = node.filename.getLastToken(),
    441                 .extra = .{ .number = 0 },
    442             });
    443         }
    444 
    445         // Allow plain number literals, but complex number expressions are evaluated strangely
    446         // and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
    447         // to the filename '-1'), so error if the filename node is a grouped/binary expression.
    448         // Note: This is done here instead of during parsing so that we can easily include
    449         //       the evaluated filename as part of the error messages.
    450         if (node.filename.id != .literal) {
    451             const filename_string_index = try self.diagnostics.putString(filename_utf8);
    452             try self.addErrorDetails(.{
    453                 .err = .number_expression_as_filename,
    454                 .token = node.filename.getFirstToken(),
    455                 .token_span_end = node.filename.getLastToken(),
    456                 .extra = .{ .number = filename_string_index },
    457             });
    458             return self.addErrorDetailsAndFail(.{
    459                 .err = .number_expression_as_filename,
    460                 .type = .note,
    461                 .token = node.filename.getFirstToken(),
    462                 .token_span_end = node.filename.getLastToken(),
    463                 .print_source_line = false,
    464                 .extra = .{ .number = filename_string_index },
    465             });
    466         }
    467         // From here on out, we know that the filename must be comprised of a single token,
    468         // so get it here to simplify future usage.
    469         const filename_token = node.filename.getFirstToken();
    470 
    471         const file = self.searchForFile(filename_utf8) catch |err| switch (err) {
    472             error.OutOfMemory => |e| return e,
    473             else => |e| {
    474                 const filename_string_index = try self.diagnostics.putString(filename_utf8);
    475                 return self.addErrorDetailsAndFail(.{
    476                     .err = .file_open_error,
    477                     .token = filename_token,
    478                     .extra = .{ .file_open_error = .{
    479                         .err = ErrorDetails.FileOpenError.enumFromError(e),
    480                         .filename_string_index = filename_string_index,
    481                     } },
    482                 });
    483             },
    484         };
    485         defer file.close();
    486 
    487         if (maybe_predefined_type) |predefined_type| {
    488             switch (predefined_type) {
    489                 .GROUP_ICON, .GROUP_CURSOR => {
    490                     // Check for animated icon first
    491                     if (ani.isAnimatedIcon(file.reader())) {
    492                         // Animated icons are just put into the resource unmodified,
    493                         // and the resource type changes to ANIICON/ANICURSOR
    494 
    495                         const new_predefined_type: res.RT = switch (predefined_type) {
    496                             .GROUP_ICON => .ANIICON,
    497                             .GROUP_CURSOR => .ANICURSOR,
    498                             else => unreachable,
    499                         };
    500                         header.type_value.ordinal = @intFromEnum(new_predefined_type);
    501                         header.memory_flags = MemoryFlags.defaults(new_predefined_type);
    502                         header.applyMemoryFlags(node.common_resource_attributes, self.source);
    503                         header.data_size = @intCast(try file.getEndPos());
    504 
    505                         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    506                         try file.seekTo(0);
    507                         try writeResourceData(writer, file.reader(), header.data_size);
    508                         return;
    509                     }
    510 
    511                     // isAnimatedIcon moved the file cursor so reset to the start
    512                     try file.seekTo(0);
    513 
    514                     const icon_dir = ico.read(self.allocator, file.reader(), try file.getEndPos()) catch |err| switch (err) {
    515                         error.OutOfMemory => |e| return e,
    516                         else => |e| {
    517                             return self.iconReadError(
    518                                 e,
    519                                 filename_utf8,
    520                                 filename_token,
    521                                 predefined_type,
    522                             );
    523                         },
    524                     };
    525                     defer icon_dir.deinit();
    526 
    527                     // This limit is inherent to the ico format since number of entries is a u16 field.
    528                     std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
    529 
    530                     // Note: The Win32 RC compiler will compile the resource as whatever type is
    531                     //       in the icon_dir regardless of the type of resource specified in the .rc.
    532                     //       This leads to unusable .res files when the types mismatch, so
    533                     //       we error instead.
    534                     const res_types_match = switch (predefined_type) {
    535                         .GROUP_ICON => icon_dir.image_type == .icon,
    536                         .GROUP_CURSOR => icon_dir.image_type == .cursor,
    537                         else => unreachable,
    538                     };
    539                     if (!res_types_match) {
    540                         return self.addErrorDetailsAndFail(.{
    541                             .err = .icon_dir_and_resource_type_mismatch,
    542                             .token = filename_token,
    543                             .extra = .{ .resource = switch (predefined_type) {
    544                                 .GROUP_ICON => .icon,
    545                                 .GROUP_CURSOR => .cursor,
    546                                 else => unreachable,
    547                             } },
    548                         });
    549                     }
    550 
    551                     // Memory flags affect the RT_ICON and the RT_GROUP_ICON differently
    552                     var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
    553                     applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
    554                     applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
    555 
    556                     const first_icon_id = self.state.icon_id;
    557                     const entry_type = if (predefined_type == .GROUP_ICON) @intFromEnum(res.RT.ICON) else @intFromEnum(res.RT.CURSOR);
    558                     for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
    559                         // We know that the entry index must fit within a u16, so
    560                         // cast it here to simplify usage sites.
    561                         const entry_i: u16 = @intCast(entry_i_usize);
    562                         var full_data_size = entry.data_size_in_bytes;
    563                         if (icon_dir.image_type == .cursor) {
    564                             full_data_size = std.math.add(u32, full_data_size, 4) catch {
    565                                 return self.addErrorDetailsAndFail(.{
    566                                     .err = .resource_data_size_exceeds_max,
    567                                     .token = node.id,
    568                                 });
    569                             };
    570                         }
    571 
    572                         const image_header = ResourceHeader{
    573                             .type_value = .{ .ordinal = entry_type },
    574                             .name_value = .{ .ordinal = self.state.icon_id },
    575                             .data_size = full_data_size,
    576                             .memory_flags = icon_memory_flags,
    577                             .language = self.state.language,
    578                             .version = self.state.version,
    579                             .characteristics = self.state.characteristics,
    580                         };
    581                         try image_header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    582 
    583                         // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
    584                         // > The LOCALHEADER structure is the first data written to the RT_CURSOR
    585                         // > resource if a RESDIR structure contains information about a cursor.
    586                         // where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
    587                         if (icon_dir.image_type == .cursor) {
    588                             try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_x, .little);
    589                             try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little);
    590                         }
    591 
    592                         try file.seekTo(entry.data_offset_from_start_of_file);
    593                         var header_bytes = file.reader().readBytesNoEof(16) catch {
    594                             return self.iconReadError(
    595                                 error.UnexpectedEOF,
    596                                 filename_utf8,
    597                                 filename_token,
    598                                 predefined_type,
    599                             );
    600                         };
    601 
    602                         const image_format = ico.ImageFormat.detect(&header_bytes);
    603                         if (!image_format.validate(&header_bytes)) {
    604                             return self.iconReadError(
    605                                 error.InvalidHeader,
    606                                 filename_utf8,
    607                                 filename_token,
    608                                 predefined_type,
    609                             );
    610                         }
    611                         switch (image_format) {
    612                             .riff => switch (icon_dir.image_type) {
    613                                 .icon => {
    614                                     // The Win32 RC compiler treats this as an error, but icon dirs
    615                                     // with RIFF encoded icons within them work ~okay (they work
    616                                     // in some places but not others, they may not animate, etc) if they are
    617                                     // allowed to be compiled.
    618                                     try self.addErrorDetails(.{
    619                                         .err = .rc_would_error_on_icon_dir,
    620                                         .type = .warning,
    621                                         .token = filename_token,
    622                                         .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
    623                                     });
    624                                     try self.addErrorDetails(.{
    625                                         .err = .rc_would_error_on_icon_dir,
    626                                         .type = .note,
    627                                         .print_source_line = false,
    628                                         .token = filename_token,
    629                                         .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
    630                                     });
    631                                 },
    632                                 .cursor => {
    633                                     // The Win32 RC compiler errors in this case too, but we only error
    634                                     // here because the cursor would fail to be loaded at runtime if we
    635                                     // compiled it.
    636                                     return self.addErrorDetailsAndFail(.{
    637                                         .err = .format_not_supported_in_icon_dir,
    638                                         .token = filename_token,
    639                                         .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
    640                                     });
    641                                 },
    642                             },
    643                             .png => switch (icon_dir.image_type) {
    644                                 .icon => {
    645                                     // PNG always seems to have 1 for color planes no matter what
    646                                     entry.type_specific_data.icon.color_planes = 1;
    647                                     // These seem to be the only values of num_colors that
    648                                     // get treated specially
    649                                     entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
    650                                         2 => 1,
    651                                         8 => 3,
    652                                         16 => 4,
    653                                         else => entry.type_specific_data.icon.bits_per_pixel,
    654                                     };
    655                                 },
    656                                 .cursor => {
    657                                     // The Win32 RC compiler treats this as an error, but cursor dirs
    658                                     // with PNG encoded icons within them work fine if they are
    659                                     // allowed to be compiled.
    660                                     try self.addErrorDetails(.{
    661                                         .err = .rc_would_error_on_icon_dir,
    662                                         .type = .warning,
    663                                         .token = filename_token,
    664                                         .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
    665                                     });
    666                                 },
    667                             },
    668                             .dib => {
    669                                 const bitmap_header: *ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
    670                                 if (native_endian == .big) {
    671                                     std.mem.byteSwapAllFields(ico.BitmapHeader, bitmap_header);
    672                                 }
    673                                 const bitmap_version = ico.BitmapHeader.Version.get(bitmap_header.bcSize);
    674 
    675                                 // The Win32 RC compiler only allows headers with
    676                                 // `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
    677                                 // that there's a good reason for that outside of too-old
    678                                 // bitmap headers.
    679                                 // TODO: Need to test V4 and V5 bitmaps to check they actually work
    680                                 if (bitmap_version == .@"win2.0") {
    681                                     return self.addErrorDetailsAndFail(.{
    682                                         .err = .rc_would_error_on_bitmap_version,
    683                                         .token = filename_token,
    684                                         .extra = .{ .icon_dir = .{
    685                                             .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    686                                             .icon_format = image_format,
    687                                             .index = entry_i,
    688                                             .bitmap_version = bitmap_version,
    689                                         } },
    690                                     });
    691                                 } else if (bitmap_version != .@"nt3.1") {
    692                                     try self.addErrorDetails(.{
    693                                         .err = .rc_would_error_on_bitmap_version,
    694                                         .type = .warning,
    695                                         .token = filename_token,
    696                                         .extra = .{ .icon_dir = .{
    697                                             .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    698                                             .icon_format = image_format,
    699                                             .index = entry_i,
    700                                             .bitmap_version = bitmap_version,
    701                                         } },
    702                                     });
    703                                 }
    704 
    705                                 switch (icon_dir.image_type) {
    706                                     .icon => {
    707                                         // The values in the icon's BITMAPINFOHEADER always take precedence over
    708                                         // the values in the IconDir, but not in the LOCALHEADER (see above).
    709                                         entry.type_specific_data.icon.color_planes = bitmap_header.bcPlanes;
    710                                         entry.type_specific_data.icon.bits_per_pixel = bitmap_header.bcBitCount;
    711                                     },
    712                                     .cursor => {
    713                                         // Only cursors get the width/height from BITMAPINFOHEADER (icons don't)
    714                                         entry.width = @intCast(bitmap_header.bcWidth);
    715                                         entry.height = @intCast(bitmap_header.bcHeight);
    716                                         entry.type_specific_data.cursor.hotspot_x = bitmap_header.bcPlanes;
    717                                         entry.type_specific_data.cursor.hotspot_y = bitmap_header.bcBitCount;
    718                                     },
    719                                 }
    720                             },
    721                         }
    722 
    723                         try file.seekTo(entry.data_offset_from_start_of_file);
    724                         try writeResourceDataNoPadding(writer, file.reader(), entry.data_size_in_bytes);
    725                         try writeDataPadding(writer, full_data_size);
    726 
    727                         if (self.state.icon_id == std.math.maxInt(u16)) {
    728                             try self.addErrorDetails(.{
    729                                 .err = .max_icon_ids_exhausted,
    730                                 .print_source_line = false,
    731                                 .token = filename_token,
    732                                 .extra = .{ .icon_dir = .{
    733                                     .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    734                                     .icon_format = image_format,
    735                                     .index = entry_i,
    736                                 } },
    737                             });
    738                             return self.addErrorDetailsAndFail(.{
    739                                 .err = .max_icon_ids_exhausted,
    740                                 .type = .note,
    741                                 .token = filename_token,
    742                                 .extra = .{ .icon_dir = .{
    743                                     .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    744                                     .icon_format = image_format,
    745                                     .index = entry_i,
    746                                 } },
    747                             });
    748                         }
    749                         self.state.icon_id += 1;
    750                     }
    751 
    752                     header.data_size = icon_dir.getResDataSize();
    753 
    754                     try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    755                     try icon_dir.writeResData(writer, first_icon_id);
    756                     try writeDataPadding(writer, header.data_size);
    757                     return;
    758                 },
    759                 .RCDATA, .HTML, .MANIFEST, .MESSAGETABLE, .DLGINIT, .PLUGPLAY => {
    760                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    761                 },
    762                 .BITMAP => {
    763                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    764                     const file_size = try file.getEndPos();
    765 
    766                     const bitmap_info = bmp.read(file.reader(), file_size) catch |err| {
    767                         const filename_string_index = try self.diagnostics.putString(filename_utf8);
    768                         return self.addErrorDetailsAndFail(.{
    769                             .err = .bmp_read_error,
    770                             .token = filename_token,
    771                             .extra = .{ .bmp_read_error = .{
    772                                 .err = ErrorDetails.BitmapReadError.enumFromError(err),
    773                                 .filename_string_index = filename_string_index,
    774                             } },
    775                         });
    776                     };
    777 
    778                     if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
    779                         const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
    780                         var number_as_bytes: [8]u8 = undefined;
    781                         std.mem.writeInt(u64, &number_as_bytes, num_ignored_bytes, native_endian);
    782                         const value_string_index = try self.diagnostics.putString(&number_as_bytes);
    783                         try self.addErrorDetails(.{
    784                             .err = .bmp_ignored_palette_bytes,
    785                             .type = .warning,
    786                             .token = filename_token,
    787                             .extra = .{ .number = value_string_index },
    788                         });
    789                     } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
    790                         const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
    791 
    792                         // TODO: Make this configurable (command line option)
    793                         const max_missing_bytes = 4096;
    794                         if (num_padding_bytes > max_missing_bytes) {
    795                             var numbers_as_bytes: [16]u8 = undefined;
    796                             std.mem.writeInt(u64, numbers_as_bytes[0..8], num_padding_bytes, native_endian);
    797                             std.mem.writeInt(u64, numbers_as_bytes[8..16], max_missing_bytes, native_endian);
    798                             const values_string_index = try self.diagnostics.putString(&numbers_as_bytes);
    799                             try self.addErrorDetails(.{
    800                                 .err = .bmp_too_many_missing_palette_bytes,
    801                                 .token = filename_token,
    802                                 .extra = .{ .number = values_string_index },
    803                             });
    804                             return self.addErrorDetailsAndFail(.{
    805                                 .err = .bmp_too_many_missing_palette_bytes,
    806                                 .type = .note,
    807                                 .print_source_line = false,
    808                                 .token = filename_token,
    809                             });
    810                         }
    811 
    812                         var number_as_bytes: [8]u8 = undefined;
    813                         std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
    814                         const value_string_index = try self.diagnostics.putString(&number_as_bytes);
    815                         try self.addErrorDetails(.{
    816                             .err = .bmp_missing_palette_bytes,
    817                             .type = .warning,
    818                             .token = filename_token,
    819                             .extra = .{ .number = value_string_index },
    820                         });
    821                         const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
    822                         if (pixel_data_len > 0) {
    823                             const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
    824                             std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
    825                             const miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
    826                             try self.addErrorDetails(.{
    827                                 .err = .rc_would_miscompile_bmp_palette_padding,
    828                                 .type = .warning,
    829                                 .token = filename_token,
    830                                 .extra = .{ .number = miscompiled_bytes_string_index },
    831                             });
    832                         }
    833                     }
    834 
    835                     // TODO: It might be possible that the calculation done in this function
    836                     //       could underflow if the underlying file is modified while reading
    837                     //       it, but need to think about it more to determine if that's a
    838                     //       real possibility
    839                     const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
    840 
    841                     header.data_size = bmp_bytes_to_write;
    842                     try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    843                     try file.seekTo(bmp.file_header_len);
    844                     const file_reader = file.reader();
    845                     try writeResourceDataNoPadding(writer, file_reader, bitmap_info.dib_header_size);
    846                     if (bitmap_info.getBitmasksByteLen() > 0) {
    847                         try writeResourceDataNoPadding(writer, file_reader, bitmap_info.getBitmasksByteLen());
    848                     }
    849                     if (bitmap_info.getExpectedPaletteByteLen() > 0) {
    850                         try writeResourceDataNoPadding(writer, file_reader, @intCast(bitmap_info.getActualPaletteByteLen()));
    851                         // We know that the number of missing palette bytes is <= 4096
    852                         // (see `bmp_too_many_missing_palette_bytes` error case above)
    853                         const padding_bytes: usize = @intCast(bitmap_info.getMissingPaletteByteLen());
    854                         if (padding_bytes > 0) {
    855                             try writer.writeByteNTimes(0, padding_bytes);
    856                         }
    857                     }
    858                     try file.seekTo(bitmap_info.pixel_data_offset);
    859                     const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
    860                     try writeResourceDataNoPadding(writer, file_reader, pixel_bytes);
    861                     try writeDataPadding(writer, bmp_bytes_to_write);
    862                     return;
    863                 },
    864                 .FONT => {
    865                     if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
    866                         // Add warning and skip this resource
    867                         // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
    868                         // and the duplicate resource is skipped.
    869                         try self.addErrorDetails(ErrorDetails{
    870                             .err = .font_id_already_defined,
    871                             .token = node.id,
    872                             .type = .warning,
    873                             .extra = .{ .number = header.name_value.ordinal },
    874                         });
    875                         try self.addErrorDetails(ErrorDetails{
    876                             .err = .font_id_already_defined,
    877                             .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
    878                             .type = .note,
    879                             .extra = .{ .number = header.name_value.ordinal },
    880                         });
    881                         return;
    882                     }
    883                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    884                     const file_size = try file.getEndPos();
    885                     if (file_size > std.math.maxInt(u32)) {
    886                         return self.addErrorDetailsAndFail(.{
    887                             .err = .resource_data_size_exceeds_max,
    888                             .token = node.id,
    889                         });
    890                     }
    891 
    892                     // We now know that the data size will fit in a u32
    893                     header.data_size = @intCast(file_size);
    894                     try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    895 
    896                     var header_slurping_reader = headerSlurpingReader(148, file.reader());
    897                     try writeResourceData(writer, header_slurping_reader.reader(), header.data_size);
    898 
    899                     try self.state.font_dir.add(self.arena, FontDir.Font{
    900                         .id = header.name_value.ordinal,
    901                         .header_bytes = header_slurping_reader.slurped_header,
    902                     }, node.id);
    903                     return;
    904                 },
    905                 .ACCELERATOR,
    906                 .ANICURSOR,
    907                 .ANIICON,
    908                 .CURSOR,
    909                 .DIALOG,
    910                 .DLGINCLUDE,
    911                 .FONTDIR,
    912                 .ICON,
    913                 .MENU,
    914                 .STRING,
    915                 .TOOLBAR,
    916                 .VERSION,
    917                 .VXD,
    918                 => unreachable,
    919                 _ => unreachable,
    920             }
    921         } else {
    922             header.applyMemoryFlags(node.common_resource_attributes, self.source);
    923         }
    924 
    925         // Fallback to just writing out the entire contents of the file
    926         const data_size = try file.getEndPos();
    927         if (data_size > std.math.maxInt(u32)) {
    928             return self.addErrorDetailsAndFail(.{
    929                 .err = .resource_data_size_exceeds_max,
    930                 .token = node.id,
    931             });
    932         }
    933         // We now know that the data size will fit in a u32
    934         header.data_size = @intCast(data_size);
    935         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
    936         try writeResourceData(writer, file.reader(), header.data_size);
    937     }
    938 
    939     fn iconReadError(
    940         self: *Compiler,
    941         err: ico.ReadError,
    942         filename: []const u8,
    943         token: Token,
    944         predefined_type: res.RT,
    945     ) error{ CompileError, OutOfMemory } {
    946         const filename_string_index = try self.diagnostics.putString(filename);
    947         return self.addErrorDetailsAndFail(.{
    948             .err = .icon_read_error,
    949             .token = token,
    950             .extra = .{ .icon_read_error = .{
    951                 .err = ErrorDetails.IconReadError.enumFromError(err),
    952                 .icon_type = switch (predefined_type) {
    953                     .GROUP_ICON => .icon,
    954                     .GROUP_CURSOR => .cursor,
    955                     else => unreachable,
    956                 },
    957                 .filename_string_index = filename_string_index,
    958             } },
    959         });
    960     }
    961 
    962     pub const DataType = enum {
    963         number,
    964         ascii_string,
    965         wide_string,
    966     };
    967 
    968     pub const Data = union(DataType) {
    969         number: Number,
    970         ascii_string: []const u8,
    971         wide_string: [:0]const u16,
    972 
    973         pub fn deinit(self: Data, allocator: Allocator) void {
    974             switch (self) {
    975                 .wide_string => |wide_string| {
    976                     allocator.free(wide_string);
    977                 },
    978                 .ascii_string => |ascii_string| {
    979                     allocator.free(ascii_string);
    980                 },
    981                 else => {},
    982             }
    983         }
    984 
    985         pub fn write(self: Data, writer: anytype) !void {
    986             switch (self) {
    987                 .number => |number| switch (number.is_long) {
    988                     false => try writer.writeInt(WORD, number.asWord(), .little),
    989                     true => try writer.writeInt(DWORD, number.value, .little),
    990                 },
    991                 .ascii_string => |ascii_string| {
    992                     try writer.writeAll(ascii_string);
    993                 },
    994                 .wide_string => |wide_string| {
    995                     try writer.writeAll(std.mem.sliceAsBytes(wide_string));
    996                 },
    997             }
    998         }
    999     };
   1000 
   1001     /// Assumes that the node is a number or number expression
   1002     pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
   1003         switch (expression_node.id) {
   1004             .literal => {
   1005                 const literal_node = expression_node.cast(.literal).?;
   1006                 std.debug.assert(literal_node.token.id == .number);
   1007                 const bytes = SourceBytes{
   1008                     .slice = literal_node.token.slice(source),
   1009                     .code_page = code_page_lookup.getForToken(literal_node.token),
   1010                 };
   1011                 return literals.parseNumberLiteral(bytes);
   1012             },
   1013             .binary_expression => {
   1014                 const binary_expression_node = expression_node.cast(.binary_expression).?;
   1015                 const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
   1016                 const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
   1017                 const operator_char = binary_expression_node.operator.slice(source)[0];
   1018                 return lhs.evaluateOperator(operator_char, rhs);
   1019             },
   1020             .grouped_expression => {
   1021                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
   1022                 return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
   1023             },
   1024             else => unreachable,
   1025         }
   1026     }
   1027 
   1028     const FlagsNumber = struct {
   1029         value: u32,
   1030         not_mask: u32 = 0xFFFFFFFF,
   1031 
   1032         pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
   1033             const result = switch (operator_char) {
   1034                 '-' => lhs.value -% rhs.value,
   1035                 '+' => lhs.value +% rhs.value,
   1036                 '|' => lhs.value | rhs.value,
   1037                 '&' => lhs.value & rhs.value,
   1038                 else => unreachable, // invalid operator, this would be a lexer/parser bug
   1039             };
   1040             return .{
   1041                 .value = result,
   1042                 .not_mask = lhs.not_mask & rhs.not_mask,
   1043             };
   1044         }
   1045 
   1046         pub fn applyNotMask(self: FlagsNumber) u32 {
   1047             return self.value & self.not_mask;
   1048         }
   1049     };
   1050 
   1051     pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
   1052         var context = FlagsExpressionContext{ .initial_value = default };
   1053         const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
   1054         return number.value;
   1055     }
   1056 
   1057     pub const FlagsExpressionContext = struct {
   1058         initial_value: u32 = 0,
   1059         initial_value_used: bool = false,
   1060     };
   1061 
   1062     /// Assumes that the node is a number expression (which can contain not_expressions)
   1063     pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
   1064         switch (expression_node.id) {
   1065             .literal => {
   1066                 const literal_node = expression_node.cast(.literal).?;
   1067                 std.debug.assert(literal_node.token.id == .number);
   1068                 const bytes = SourceBytes{
   1069                     .slice = literal_node.token.slice(source),
   1070                     .code_page = code_page_lookup.getForToken(literal_node.token),
   1071                 };
   1072                 var value = literals.parseNumberLiteral(bytes).value;
   1073                 if (!context.initial_value_used) {
   1074                     context.initial_value_used = true;
   1075                     value |= context.initial_value;
   1076                 }
   1077                 return .{ .value = value };
   1078             },
   1079             .binary_expression => {
   1080                 const binary_expression_node = expression_node.cast(.binary_expression).?;
   1081                 const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
   1082                 const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
   1083                 const operator_char = binary_expression_node.operator.slice(source)[0];
   1084                 const result = lhs.evaluateOperator(operator_char, rhs);
   1085                 return .{ .value = result.applyNotMask() };
   1086             },
   1087             .grouped_expression => {
   1088                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
   1089                 return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
   1090             },
   1091             .not_expression => {
   1092                 const not_expression = expression_node.cast(.not_expression).?;
   1093                 const bytes = SourceBytes{
   1094                     .slice = not_expression.number_token.slice(source),
   1095                     .code_page = code_page_lookup.getForToken(not_expression.number_token),
   1096                 };
   1097                 const not_number = literals.parseNumberLiteral(bytes);
   1098                 if (!context.initial_value_used) {
   1099                     context.initial_value_used = true;
   1100                     return .{ .value = context.initial_value & ~not_number.value };
   1101                 }
   1102                 return .{ .value = 0, .not_mask = ~not_number.value };
   1103             },
   1104             else => unreachable,
   1105         }
   1106     }
   1107 
   1108     pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
   1109         switch (expression_node.id) {
   1110             .literal => {
   1111                 const literal_node = expression_node.cast(.literal).?;
   1112                 switch (literal_node.token.id) {
   1113                     .number => {
   1114                         const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
   1115                         return .{ .number = number };
   1116                     },
   1117                     .quoted_ascii_string => {
   1118                         const column = literal_node.token.calculateColumn(self.source, 8, null);
   1119                         const bytes = SourceBytes{
   1120                             .slice = literal_node.token.slice(self.source),
   1121                             .code_page = self.input_code_pages.getForToken(literal_node.token),
   1122                         };
   1123                         const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
   1124                             .start_column = column,
   1125                             .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
   1126                             .output_code_page = self.output_code_pages.getForToken(literal_node.token),
   1127                         });
   1128                         errdefer self.allocator.free(parsed);
   1129                         return .{ .ascii_string = parsed };
   1130                     },
   1131                     .quoted_wide_string => {
   1132                         const column = literal_node.token.calculateColumn(self.source, 8, null);
   1133                         const bytes = SourceBytes{
   1134                             .slice = literal_node.token.slice(self.source),
   1135                             .code_page = self.input_code_pages.getForToken(literal_node.token),
   1136                         };
   1137                         const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
   1138                             .start_column = column,
   1139                             .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal_node.token },
   1140                         });
   1141                         errdefer self.allocator.free(parsed_string);
   1142                         return .{ .wide_string = parsed_string };
   1143                     },
   1144                     else => {
   1145                         std.debug.print("unexpected token in literal node: {}\n", .{literal_node.token});
   1146                         unreachable; // no other token types should be in a data literal node
   1147                     },
   1148                 }
   1149             },
   1150             .binary_expression, .grouped_expression => {
   1151                 const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
   1152                 return .{ .number = result };
   1153             },
   1154             .not_expression => unreachable,
   1155             else => {
   1156                 std.debug.print("{}\n", .{expression_node.id});
   1157                 @panic("TODO: evaluateDataExpression");
   1158             },
   1159         }
   1160     }
   1161 
   1162     pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: anytype) !void {
   1163         var data_buffer = std.ArrayList(u8).init(self.allocator);
   1164         defer data_buffer.deinit();
   1165         // The header's data length field is a u32 so limit the resource's data size so that
   1166         // we know we can always specify the real size.
   1167         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1168         const data_writer = limited_writer.writer();
   1169 
   1170         for (node.raw_data) |expression| {
   1171             const data = try self.evaluateDataExpression(expression);
   1172             defer data.deinit(self.allocator);
   1173             data.write(data_writer) catch |err| switch (err) {
   1174                 error.NoSpaceLeft => {
   1175                     return self.addErrorDetailsAndFail(.{
   1176                         .err = .resource_data_size_exceeds_max,
   1177                         .token = node.id,
   1178                     });
   1179                 },
   1180                 else => |e| return e,
   1181             };
   1182         }
   1183 
   1184         // This intCast can't fail because the limitedWriter above guarantees that
   1185         // we will never write more than maxInt(u32) bytes.
   1186         const data_len: u32 = @intCast(data_buffer.items.len);
   1187         try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
   1188 
   1189         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   1190         try writeResourceData(writer, data_fbs.reader(), data_len);
   1191     }
   1192 
   1193     pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
   1194         var header = try self.resourceHeader(id_token, type_token, .{
   1195             .language = language,
   1196             .data_size = data_size,
   1197         });
   1198         defer header.deinit(self.allocator);
   1199 
   1200         header.applyMemoryFlags(common_resource_attributes, self.source);
   1201 
   1202         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = id_token });
   1203     }
   1204 
   1205     pub fn writeResourceDataNoPadding(writer: anytype, data_reader: anytype, data_size: u32) !void {
   1206         var limited_reader = std.io.limitedReader(data_reader, data_size);
   1207 
   1208         const FifoBuffer = std.fifo.LinearFifo(u8, .{ .Static = 4096 });
   1209         var fifo = FifoBuffer.init();
   1210         try fifo.pump(limited_reader.reader(), writer);
   1211     }
   1212 
   1213     pub fn writeResourceData(writer: anytype, data_reader: anytype, data_size: u32) !void {
   1214         try writeResourceDataNoPadding(writer, data_reader, data_size);
   1215         try writeDataPadding(writer, data_size);
   1216     }
   1217 
   1218     pub fn writeDataPadding(writer: anytype, data_size: u32) !void {
   1219         try writer.writeByteNTimes(0, numPaddingBytesNeeded(data_size));
   1220     }
   1221 
   1222     pub fn numPaddingBytesNeeded(data_size: u32) u2 {
   1223         // Result is guaranteed to be between 0 and 3.
   1224         return @intCast((4 -% data_size) % 4);
   1225     }
   1226 
   1227     pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
   1228         if (node.isNumberExpression()) {
   1229             return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
   1230         } else {
   1231             std.debug.assert(node.isStringLiteral());
   1232             const literal = @fieldParentPtr(Node.Literal, "base", node);
   1233             const bytes = SourceBytes{
   1234                 .slice = literal.token.slice(self.source),
   1235                 .code_page = self.input_code_pages.getForToken(literal.token),
   1236             };
   1237             const column = literal.token.calculateColumn(self.source, 8, null);
   1238             return res.parseAcceleratorKeyString(bytes, is_virt, .{
   1239                 .start_column = column,
   1240                 .diagnostics = .{ .diagnostics = self.diagnostics, .token = literal.token },
   1241             });
   1242         }
   1243     }
   1244 
   1245     pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: anytype) !void {
   1246         var data_buffer = std.ArrayList(u8).init(self.allocator);
   1247         defer data_buffer.deinit();
   1248 
   1249         // The header's data length field is a u32 so limit the resource's data size so that
   1250         // we know we can always specify the real size.
   1251         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1252         const data_writer = limited_writer.writer();
   1253 
   1254         self.writeAcceleratorsData(node, data_writer) catch |err| switch (err) {
   1255             error.NoSpaceLeft => {
   1256                 return self.addErrorDetailsAndFail(.{
   1257                     .err = .resource_data_size_exceeds_max,
   1258                     .token = node.id,
   1259                 });
   1260             },
   1261             else => |e| return e,
   1262         };
   1263 
   1264         // This intCast can't fail because the limitedWriter above guarantees that
   1265         // we will never write more than maxInt(u32) bytes.
   1266         const data_size: u32 = @intCast(data_buffer.items.len);
   1267         var header = try self.resourceHeader(node.id, node.type, .{
   1268             .data_size = data_size,
   1269         });
   1270         defer header.deinit(self.allocator);
   1271 
   1272         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   1273         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   1274 
   1275         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
   1276 
   1277         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   1278         try writeResourceData(writer, data_fbs.reader(), data_size);
   1279     }
   1280 
   1281     /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
   1282     /// the writer within this function could return error.NoSpaceLeft
   1283     pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: anytype) !void {
   1284         for (node.accelerators, 0..) |accel_node, i| {
   1285             const accelerator = @fieldParentPtr(Node.Accelerator, "base", accel_node);
   1286             var modifiers = res.AcceleratorModifiers{};
   1287             for (accelerator.type_and_options) |type_or_option| {
   1288                 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
   1289                 modifiers.apply(modifier);
   1290             }
   1291             if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
   1292                 return self.addErrorDetailsAndFail(.{
   1293                     .err = .accelerator_type_required,
   1294                     .token = accelerator.event.getFirstToken(),
   1295                     .token_span_end = accelerator.event.getLastToken(),
   1296                 });
   1297             }
   1298             const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
   1299                 error.OutOfMemory => |e| return e,
   1300                 else => |e| {
   1301                     return self.addErrorDetailsAndFail(.{
   1302                         .err = .invalid_accelerator_key,
   1303                         .token = accelerator.event.getFirstToken(),
   1304                         .token_span_end = accelerator.event.getLastToken(),
   1305                         .extra = .{ .accelerator_error = .{
   1306                             .err = ErrorDetails.AcceleratorError.enumFromError(e),
   1307                         } },
   1308                     });
   1309                 },
   1310             };
   1311             const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
   1312 
   1313             if (i == node.accelerators.len - 1) {
   1314                 modifiers.markLast();
   1315             }
   1316 
   1317             try data_writer.writeByte(modifiers.value);
   1318             try data_writer.writeByte(0); // padding
   1319             try data_writer.writeInt(u16, key, .little);
   1320             try data_writer.writeInt(u16, cmd_id.asWord(), .little);
   1321             try data_writer.writeInt(u16, 0, .little); // padding
   1322         }
   1323     }
   1324 
   1325     const DialogOptionalStatementValues = struct {
   1326         style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
   1327         exstyle: u32 = 0,
   1328         class: ?NameOrOrdinal = null,
   1329         menu: ?NameOrOrdinal = null,
   1330         font: ?FontStatementValues = null,
   1331         caption: ?Token = null,
   1332     };
   1333 
   1334     pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: anytype) !void {
   1335         var data_buffer = std.ArrayList(u8).init(self.allocator);
   1336         defer data_buffer.deinit();
   1337         // The header's data length field is a u32 so limit the resource's data size so that
   1338         // we know we can always specify the real size.
   1339         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1340         const data_writer = limited_writer.writer();
   1341 
   1342         const resource = Resource.fromString(.{
   1343             .slice = node.type.slice(self.source),
   1344             .code_page = self.input_code_pages.getForToken(node.type),
   1345         });
   1346         std.debug.assert(resource == .dialog or resource == .dialogex);
   1347 
   1348         var optional_statement_values: DialogOptionalStatementValues = .{};
   1349         defer {
   1350             if (optional_statement_values.class) |class| {
   1351                 class.deinit(self.allocator);
   1352             }
   1353             if (optional_statement_values.menu) |menu| {
   1354                 menu.deinit(self.allocator);
   1355             }
   1356         }
   1357         var skipped_menu_or_classes = std.ArrayList(*Node.SimpleStatement).init(self.allocator);
   1358         defer skipped_menu_or_classes.deinit();
   1359         var last_menu: *Node.SimpleStatement = undefined;
   1360         var last_class: *Node.SimpleStatement = undefined;
   1361         var last_menu_would_be_forced_ordinal = false;
   1362         var last_menu_has_digit_as_first_char = false;
   1363         var last_menu_did_uppercase = false;
   1364         var last_class_would_be_forced_ordinal = false;
   1365 
   1366         for (node.optional_statements) |optional_statement| {
   1367             switch (optional_statement.id) {
   1368                 .simple_statement => {
   1369                     const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", optional_statement);
   1370                     const statement_identifier = simple_statement.identifier;
   1371                     const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
   1372                     switch (statement_type) {
   1373                         .style, .exstyle => {
   1374                             const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
   1375                             if (statement_type == .style) {
   1376                                 optional_statement_values.style = style;
   1377                             } else {
   1378                                 optional_statement_values.exstyle = style;
   1379                             }
   1380                         },
   1381                         .caption => {
   1382                             std.debug.assert(simple_statement.value.id == .literal);
   1383                             const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
   1384                             optional_statement_values.caption = literal_node.token;
   1385                         },
   1386                         .class => {
   1387                             const is_duplicate = optional_statement_values.class != null;
   1388                             if (is_duplicate) {
   1389                                 try skipped_menu_or_classes.append(last_class);
   1390                             }
   1391                             const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
   1392                             // In the Win32 RC compiler, if any CLASS values that are interpreted as
   1393                             // an ordinal exist, it affects all future CLASS statements and forces
   1394                             // them to be treated as an ordinal no matter what.
   1395                             if (forced_ordinal) {
   1396                                 last_class_would_be_forced_ordinal = true;
   1397                             }
   1398                             // clear out the old one if it exists
   1399                             if (optional_statement_values.class) |prev| {
   1400                                 prev.deinit(self.allocator);
   1401                                 optional_statement_values.class = null;
   1402                             }
   1403 
   1404                             if (simple_statement.value.isNumberExpression()) {
   1405                                 const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
   1406                                 optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
   1407                             } else {
   1408                                 std.debug.assert(simple_statement.value.isStringLiteral());
   1409                                 const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
   1410                                 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
   1411                                 optional_statement_values.class = NameOrOrdinal{ .name = parsed };
   1412                             }
   1413 
   1414                             last_class = simple_statement;
   1415                         },
   1416                         .menu => {
   1417                             const is_duplicate = optional_statement_values.menu != null;
   1418                             if (is_duplicate) {
   1419                                 try skipped_menu_or_classes.append(last_menu);
   1420                             }
   1421                             const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
   1422                             // In the Win32 RC compiler, if any MENU values that are interpreted as
   1423                             // an ordinal exist, it affects all future MENU statements and forces
   1424                             // them to be treated as an ordinal no matter what.
   1425                             if (forced_ordinal) {
   1426                                 last_menu_would_be_forced_ordinal = true;
   1427                             }
   1428                             // clear out the old one if it exists
   1429                             if (optional_statement_values.menu) |prev| {
   1430                                 prev.deinit(self.allocator);
   1431                                 optional_statement_values.menu = null;
   1432                             }
   1433 
   1434                             std.debug.assert(simple_statement.value.id == .literal);
   1435                             const literal_node = @fieldParentPtr(Node.Literal, "base", simple_statement.value);
   1436 
   1437                             const token_slice = literal_node.token.slice(self.source);
   1438                             const bytes = SourceBytes{
   1439                                 .slice = token_slice,
   1440                                 .code_page = self.input_code_pages.getForToken(literal_node.token),
   1441                             };
   1442                             optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
   1443 
   1444                             if (optional_statement_values.menu.? == .name) {
   1445                                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
   1446                                     try self.addErrorDetails(.{
   1447                                         .err = .invalid_digit_character_in_ordinal,
   1448                                         .type = .err,
   1449                                         .token = literal_node.token,
   1450                                     });
   1451                                     return self.addErrorDetailsAndFail(.{
   1452                                         .err = .win32_non_ascii_ordinal,
   1453                                         .type = .note,
   1454                                         .token = literal_node.token,
   1455                                         .print_source_line = false,
   1456                                         .extra = .{ .number = win32_rc_ordinal.ordinal },
   1457                                     });
   1458                                 }
   1459                             }
   1460 
   1461                             // Need to keep track of some properties of the value
   1462                             // in order to emit the appropriate warning(s) later on.
   1463                             // See where the warning are emitted below (outside this loop)
   1464                             // for the full explanation.
   1465                             var did_uppercase = false;
   1466                             var codepoint_i: usize = 0;
   1467                             while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
   1468                                 const c = codepoint.value;
   1469                                 switch (c) {
   1470                                     'a'...'z' => {
   1471                                         did_uppercase = true;
   1472                                         break;
   1473                                     },
   1474                                     else => {},
   1475                                 }
   1476                             }
   1477                             last_menu_did_uppercase = did_uppercase;
   1478                             last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
   1479                             last_menu = simple_statement;
   1480                         },
   1481                         else => {},
   1482                     }
   1483                 },
   1484                 .font_statement => {
   1485                     const font = @fieldParentPtr(Node.FontStatement, "base", optional_statement);
   1486                     if (optional_statement_values.font != null) {
   1487                         optional_statement_values.font.?.node = font;
   1488                     } else {
   1489                         optional_statement_values.font = FontStatementValues{ .node = font };
   1490                     }
   1491                     if (font.weight) |weight| {
   1492                         const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
   1493                         optional_statement_values.font.?.weight = value.asWord();
   1494                     }
   1495                     if (font.italic) |italic| {
   1496                         const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
   1497                         optional_statement_values.font.?.italic = value.asWord() != 0;
   1498                     }
   1499                 },
   1500                 else => {},
   1501             }
   1502         }
   1503 
   1504         for (skipped_menu_or_classes.items) |simple_statement| {
   1505             const statement_identifier = simple_statement.identifier;
   1506             const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
   1507             try self.addErrorDetails(.{
   1508                 .err = .duplicate_menu_or_class_skipped,
   1509                 .type = .warning,
   1510                 .token = simple_statement.identifier,
   1511                 .token_span_start = simple_statement.base.getFirstToken(),
   1512                 .token_span_end = simple_statement.base.getLastToken(),
   1513                 .extra = .{ .menu_or_class = switch (statement_type) {
   1514                     .menu => .menu,
   1515                     .class => .class,
   1516                     else => unreachable,
   1517                 } },
   1518             });
   1519         }
   1520         // The Win32 RC compiler miscompiles the value in the following scenario:
   1521         // Multiple CLASS parameters are specified and any of them are treated as a number, then
   1522         // the last CLASS is always treated as a number no matter what
   1523         if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
   1524             const literal_node = @fieldParentPtr(Node.Literal, "base", last_class.value);
   1525             const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
   1526 
   1527             try self.addErrorDetails(.{
   1528                 .err = .rc_would_miscompile_dialog_class,
   1529                 .type = .warning,
   1530                 .token = literal_node.token,
   1531                 .extra = .{ .number = ordinal_value },
   1532             });
   1533             try self.addErrorDetails(.{
   1534                 .err = .rc_would_miscompile_dialog_class,
   1535                 .type = .note,
   1536                 .print_source_line = false,
   1537                 .token = literal_node.token,
   1538                 .extra = .{ .number = ordinal_value },
   1539             });
   1540             try self.addErrorDetails(.{
   1541                 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
   1542                 .type = .note,
   1543                 .print_source_line = false,
   1544                 .token = literal_node.token,
   1545                 .extra = .{ .menu_or_class = .class },
   1546             });
   1547         }
   1548         // The Win32 RC compiler miscompiles the id in two different scenarios:
   1549         // 1. The first character of the ID is a digit, in which case it is always treated as a number
   1550         //    no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
   1551         // 2. Multiple MENU parameters are specified and any of them are treated as a number, then
   1552         //    the last MENU is always treated as a number no matter what
   1553         if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
   1554             const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
   1555             const token_slice = literal_node.token.slice(self.source);
   1556             const bytes = SourceBytes{
   1557                 .slice = token_slice,
   1558                 .code_page = self.input_code_pages.getForToken(literal_node.token),
   1559             };
   1560             const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
   1561 
   1562             try self.addErrorDetails(.{
   1563                 .err = .rc_would_miscompile_dialog_menu_id,
   1564                 .type = .warning,
   1565                 .token = literal_node.token,
   1566                 .extra = .{ .number = ordinal_value },
   1567             });
   1568             try self.addErrorDetails(.{
   1569                 .err = .rc_would_miscompile_dialog_menu_id,
   1570                 .type = .note,
   1571                 .print_source_line = false,
   1572                 .token = literal_node.token,
   1573                 .extra = .{ .number = ordinal_value },
   1574             });
   1575             if (last_menu_would_be_forced_ordinal) {
   1576                 try self.addErrorDetails(.{
   1577                     .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
   1578                     .type = .note,
   1579                     .print_source_line = false,
   1580                     .token = literal_node.token,
   1581                     .extra = .{ .menu_or_class = .menu },
   1582                 });
   1583             } else {
   1584                 try self.addErrorDetails(.{
   1585                     .err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
   1586                     .type = .note,
   1587                     .print_source_line = false,
   1588                     .token = literal_node.token,
   1589                 });
   1590             }
   1591         }
   1592         // The MENU id parsing uses the exact same logic as the MENU/MENUEX resource id parsing,
   1593         // which means that it will convert ASCII characters to uppercase during the 'name' parsing.
   1594         // This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
   1595         // but it still makes sense to share the uppercasing logic since the MENU parameter
   1596         // here is just a reference to a MENU/MENUEX id within the .exe.
   1597         // So, because this is an intentional but inconsequential-to-the-user difference
   1598         // between resinator and the Win32 RC compiler, we only emit a hint instead of
   1599         // a warning.
   1600         if (last_menu_did_uppercase) {
   1601             const literal_node = @fieldParentPtr(Node.Literal, "base", last_menu.value);
   1602             try self.addErrorDetails(.{
   1603                 .err = .dialog_menu_id_was_uppercased,
   1604                 .type = .hint,
   1605                 .token = literal_node.token,
   1606             });
   1607         }
   1608 
   1609         const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
   1610         const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
   1611         const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
   1612         const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
   1613 
   1614         // FONT statement requires DS_SETFONT, and if it's not present DS_SETFRONT must be unset
   1615         if (optional_statement_values.font) |_| {
   1616             optional_statement_values.style |= res.DS.SETFONT;
   1617         } else {
   1618             optional_statement_values.style &= ~res.DS.SETFONT;
   1619         }
   1620         // CAPTION statement implies WS_CAPTION
   1621         if (optional_statement_values.caption) |_| {
   1622             optional_statement_values.style |= res.WS.CAPTION;
   1623         }
   1624 
   1625         self.writeDialogHeaderAndStrings(
   1626             node,
   1627             data_writer,
   1628             resource,
   1629             &optional_statement_values,
   1630             x,
   1631             y,
   1632             width,
   1633             height,
   1634         ) catch |err| switch (err) {
   1635             // Dialog header and menu/class/title strings can never exceed u32 bytes
   1636             // on their own, so this error is unreachable.
   1637             error.NoSpaceLeft => unreachable,
   1638             else => |e| return e,
   1639         };
   1640 
   1641         var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
   1642         // Number of controls are guaranteed by the parser to be within maxInt(u16).
   1643         try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
   1644         defer controls_by_id.deinit();
   1645 
   1646         for (node.controls) |control_node| {
   1647             const control = @fieldParentPtr(Node.ControlStatement, "base", control_node);
   1648 
   1649             self.writeDialogControl(
   1650                 control,
   1651                 data_writer,
   1652                 resource,
   1653                 // We know the data_buffer len is limited to u32 max.
   1654                 @intCast(data_buffer.items.len),
   1655                 &controls_by_id,
   1656             ) catch |err| switch (err) {
   1657                 error.NoSpaceLeft => {
   1658                     try self.addErrorDetails(.{
   1659                         .err = .resource_data_size_exceeds_max,
   1660                         .token = node.id,
   1661                     });
   1662                     return self.addErrorDetailsAndFail(.{
   1663                         .err = .resource_data_size_exceeds_max,
   1664                         .type = .note,
   1665                         .token = control.type,
   1666                     });
   1667                 },
   1668                 else => |e| return e,
   1669             };
   1670         }
   1671 
   1672         const data_size: u32 = @intCast(data_buffer.items.len);
   1673         var header = try self.resourceHeader(node.id, node.type, .{
   1674             .data_size = data_size,
   1675         });
   1676         defer header.deinit(self.allocator);
   1677 
   1678         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   1679         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   1680 
   1681         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
   1682 
   1683         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   1684         try writeResourceData(writer, data_fbs.reader(), data_size);
   1685     }
   1686 
   1687     fn writeDialogHeaderAndStrings(
   1688         self: *Compiler,
   1689         node: *Node.Dialog,
   1690         data_writer: anytype,
   1691         resource: Resource,
   1692         optional_statement_values: *const DialogOptionalStatementValues,
   1693         x: Number,
   1694         y: Number,
   1695         width: Number,
   1696         height: Number,
   1697     ) !void {
   1698         // Header
   1699         if (resource == .dialogex) {
   1700             const help_id: u32 = help_id: {
   1701                 if (node.help_id == null) break :help_id 0;
   1702                 break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
   1703             };
   1704             try data_writer.writeInt(u16, 1, .little); // version number, always 1
   1705             try data_writer.writeInt(u16, 0xFFFF, .little); // signature, always 0xFFFF
   1706             try data_writer.writeInt(u32, help_id, .little);
   1707             try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
   1708             try data_writer.writeInt(u32, optional_statement_values.style, .little);
   1709         } else {
   1710             try data_writer.writeInt(u32, optional_statement_values.style, .little);
   1711             try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
   1712         }
   1713         // This limit is enforced by the parser, so we know the number of controls
   1714         // is within the range of a u16.
   1715         try data_writer.writeInt(u16, @as(u16, @intCast(node.controls.len)), .little);
   1716         try data_writer.writeInt(u16, x.asWord(), .little);
   1717         try data_writer.writeInt(u16, y.asWord(), .little);
   1718         try data_writer.writeInt(u16, width.asWord(), .little);
   1719         try data_writer.writeInt(u16, height.asWord(), .little);
   1720 
   1721         // Menu
   1722         if (optional_statement_values.menu) |menu| {
   1723             try menu.write(data_writer);
   1724         } else {
   1725             try data_writer.writeInt(u16, 0, .little);
   1726         }
   1727         // Class
   1728         if (optional_statement_values.class) |class| {
   1729             try class.write(data_writer);
   1730         } else {
   1731             try data_writer.writeInt(u16, 0, .little);
   1732         }
   1733         // Caption
   1734         if (optional_statement_values.caption) |caption| {
   1735             const parsed = try self.parseQuotedStringAsWideString(caption);
   1736             defer self.allocator.free(parsed);
   1737             try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
   1738         } else {
   1739             try data_writer.writeInt(u16, 0, .little);
   1740         }
   1741         // Font
   1742         if (optional_statement_values.font) |font| {
   1743             try self.writeDialogFont(resource, font, data_writer);
   1744         }
   1745     }
   1746 
   1747     fn writeDialogControl(
   1748         self: *Compiler,
   1749         control: *Node.ControlStatement,
   1750         data_writer: anytype,
   1751         resource: Resource,
   1752         bytes_written_so_far: u32,
   1753         controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
   1754     ) !void {
   1755         const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
   1756 
   1757         // Each control must be at a 4-byte boundary. However, the Windows RC
   1758         // compiler will miscompile controls if their extra data ends on an odd offset.
   1759         // We will avoid the miscompilation and emit a warning.
   1760         const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
   1761         if (num_padding == 1 or num_padding == 3) {
   1762             try self.addErrorDetails(.{
   1763                 .err = .rc_would_miscompile_control_padding,
   1764                 .type = .warning,
   1765                 .token = control.type,
   1766             });
   1767             try self.addErrorDetails(.{
   1768                 .err = .rc_would_miscompile_control_padding,
   1769                 .type = .note,
   1770                 .print_source_line = false,
   1771                 .token = control.type,
   1772             });
   1773         }
   1774         try data_writer.writeByteNTimes(0, num_padding);
   1775 
   1776         const style = if (control.style) |style_expression|
   1777             // Certain styles are implied by the control type
   1778             evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
   1779         else
   1780             res.ControlClass.getImpliedStyle(control_type);
   1781 
   1782         const exstyle = if (control.exstyle) |exstyle_expression|
   1783             evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
   1784         else
   1785             0;
   1786 
   1787         switch (resource) {
   1788             .dialog => {
   1789                 // Note: Reverse order from DIALOGEX
   1790                 try data_writer.writeInt(u32, style, .little);
   1791                 try data_writer.writeInt(u32, exstyle, .little);
   1792             },
   1793             .dialogex => {
   1794                 const help_id: u32 = if (control.help_id) |help_id_expression|
   1795                     evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
   1796                 else
   1797                     0;
   1798                 try data_writer.writeInt(u32, help_id, .little);
   1799                 // Note: Reverse order from DIALOG
   1800                 try data_writer.writeInt(u32, exstyle, .little);
   1801                 try data_writer.writeInt(u32, style, .little);
   1802             },
   1803             else => unreachable,
   1804         }
   1805 
   1806         const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
   1807         const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
   1808         const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
   1809         const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
   1810 
   1811         try data_writer.writeInt(u16, control_x.asWord(), .little);
   1812         try data_writer.writeInt(u16, control_y.asWord(), .little);
   1813         try data_writer.writeInt(u16, control_width.asWord(), .little);
   1814         try data_writer.writeInt(u16, control_height.asWord(), .little);
   1815 
   1816         const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
   1817         switch (resource) {
   1818             .dialog => try data_writer.writeInt(u16, control_id.asWord(), .little),
   1819             .dialogex => try data_writer.writeInt(u32, control_id.value, .little),
   1820             else => unreachable,
   1821         }
   1822 
   1823         const control_id_for_map: u32 = switch (resource) {
   1824             .dialog => control_id.asWord(),
   1825             .dialogex => control_id.value,
   1826             else => unreachable,
   1827         };
   1828         const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
   1829         if (result.found_existing) {
   1830             if (!self.silent_duplicate_control_ids) {
   1831                 try self.addErrorDetails(.{
   1832                     .err = .control_id_already_defined,
   1833                     .type = .warning,
   1834                     .token = control.id.getFirstToken(),
   1835                     .token_span_end = control.id.getLastToken(),
   1836                     .extra = .{ .number = control_id_for_map },
   1837                 });
   1838                 try self.addErrorDetails(.{
   1839                     .err = .control_id_already_defined,
   1840                     .type = .note,
   1841                     .token = result.value_ptr.*.id.getFirstToken(),
   1842                     .token_span_end = result.value_ptr.*.id.getLastToken(),
   1843                     .extra = .{ .number = control_id_for_map },
   1844                 });
   1845             }
   1846         } else {
   1847             result.value_ptr.* = control;
   1848         }
   1849 
   1850         if (res.ControlClass.fromControl(control_type)) |control_class| {
   1851             const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1852             try ordinal.write(data_writer);
   1853         } else {
   1854             const class_node = control.class.?;
   1855             if (class_node.isNumberExpression()) {
   1856                 const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
   1857                 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
   1858                 // This is different from how the Windows RC compiles ordinals here,
   1859                 // but I think that's a miscompilation/bug of the Windows implementation.
   1860                 // The Windows behavior is (where LSB = least significant byte):
   1861                 // - If the LSB is 0x00 => 0xFFFF0000
   1862                 // - If the LSB is < 0x80 => 0x000000<LSB>
   1863                 // - If the LSB is >= 0x80 => 0x0000FF<LSB>
   1864                 //
   1865                 // Because of this, we emit a warning about the potential miscompilation
   1866                 try self.addErrorDetails(.{
   1867                     .err = .rc_would_miscompile_control_class_ordinal,
   1868                     .type = .warning,
   1869                     .token = class_node.getFirstToken(),
   1870                     .token_span_end = class_node.getLastToken(),
   1871                 });
   1872                 try self.addErrorDetails(.{
   1873                     .err = .rc_would_miscompile_control_class_ordinal,
   1874                     .type = .note,
   1875                     .print_source_line = false,
   1876                     .token = class_node.getFirstToken(),
   1877                     .token_span_end = class_node.getLastToken(),
   1878                 });
   1879                 // And then write out the ordinal using a proper a NameOrOrdinal encoding.
   1880                 try ordinal.write(data_writer);
   1881             } else if (class_node.isStringLiteral()) {
   1882                 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
   1883                 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
   1884                 defer self.allocator.free(parsed);
   1885                 if (rc.ControlClass.fromWideString(parsed)) |control_class| {
   1886                     const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1887                     try ordinal.write(data_writer);
   1888                 } else {
   1889                     // NUL acts as a terminator
   1890                     // TODO: Maybe warn when parsed_terminated.len != parsed.len, since
   1891                     //       it seems unlikely that NUL-termination is something intentional
   1892                     const parsed_terminated = std.mem.sliceTo(parsed, 0);
   1893                     const name = NameOrOrdinal{ .name = parsed_terminated };
   1894                     try name.write(data_writer);
   1895                 }
   1896             } else {
   1897                 const literal_node = @fieldParentPtr(Node.Literal, "base", class_node);
   1898                 const literal_slice = literal_node.token.slice(self.source);
   1899                 // This succeeding is guaranteed by the parser
   1900                 const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
   1901                 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1902                 try ordinal.write(data_writer);
   1903             }
   1904         }
   1905 
   1906         if (control.text) |text_token| {
   1907             const bytes = SourceBytes{
   1908                 .slice = text_token.slice(self.source),
   1909                 .code_page = self.input_code_pages.getForToken(text_token),
   1910             };
   1911             if (text_token.isStringLiteral()) {
   1912                 const text = try self.parseQuotedStringAsWideString(text_token);
   1913                 defer self.allocator.free(text);
   1914                 const name = NameOrOrdinal{ .name = text };
   1915                 try name.write(data_writer);
   1916             } else {
   1917                 std.debug.assert(text_token.id == .number);
   1918                 const number = literals.parseNumberLiteral(bytes);
   1919                 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
   1920                 try ordinal.write(data_writer);
   1921             }
   1922         } else {
   1923             try NameOrOrdinal.writeEmpty(data_writer);
   1924         }
   1925 
   1926         var extra_data_buf = std.ArrayList(u8).init(self.allocator);
   1927         defer extra_data_buf.deinit();
   1928         // The extra data byte length must be able to fit within a u16.
   1929         var limited_extra_data_writer = limitedWriter(extra_data_buf.writer(), std.math.maxInt(u16));
   1930         const extra_data_writer = limited_extra_data_writer.writer();
   1931         for (control.extra_data) |data_expression| {
   1932             const data = try self.evaluateDataExpression(data_expression);
   1933             defer data.deinit(self.allocator);
   1934             data.write(extra_data_writer) catch |err| switch (err) {
   1935                 error.NoSpaceLeft => {
   1936                     try self.addErrorDetails(.{
   1937                         .err = .control_extra_data_size_exceeds_max,
   1938                         .token = control.type,
   1939                     });
   1940                     return self.addErrorDetailsAndFail(.{
   1941                         .err = .control_extra_data_size_exceeds_max,
   1942                         .type = .note,
   1943                         .token = data_expression.getFirstToken(),
   1944                         .token_span_end = data_expression.getLastToken(),
   1945                     });
   1946                 },
   1947                 else => |e| return e,
   1948             };
   1949         }
   1950         // We know the extra_data_buf size fits within a u16.
   1951         const extra_data_size: u16 = @intCast(extra_data_buf.items.len);
   1952         try data_writer.writeInt(u16, extra_data_size, .little);
   1953         try data_writer.writeAll(extra_data_buf.items);
   1954     }
   1955 
   1956     pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: anytype) !void {
   1957         var data_buffer = std.ArrayList(u8).init(self.allocator);
   1958         defer data_buffer.deinit();
   1959         const data_writer = data_buffer.writer();
   1960 
   1961         const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
   1962         const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
   1963 
   1964         // I'm assuming this is some sort of version
   1965         // TODO: Try to find something mentioning this
   1966         try data_writer.writeInt(u16, 1, .little);
   1967         try data_writer.writeInt(u16, button_width.asWord(), .little);
   1968         try data_writer.writeInt(u16, button_height.asWord(), .little);
   1969         try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
   1970 
   1971         for (node.buttons) |button_or_sep| {
   1972             switch (button_or_sep.id) {
   1973                 .literal => { // This is always SEPARATOR
   1974                     std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
   1975                     try data_writer.writeInt(u16, 0, .little);
   1976                 },
   1977                 .simple_statement => {
   1978                     const value_node = button_or_sep.cast(.simple_statement).?.value;
   1979                     const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
   1980                     try data_writer.writeInt(u16, value.asWord(), .little);
   1981                 },
   1982                 else => unreachable, // This is a bug in the parser
   1983             }
   1984         }
   1985 
   1986         const data_size: u32 = @intCast(data_buffer.items.len);
   1987         var header = try self.resourceHeader(node.id, node.type, .{
   1988             .data_size = data_size,
   1989         });
   1990         defer header.deinit(self.allocator);
   1991 
   1992         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   1993 
   1994         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
   1995 
   1996         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   1997         try writeResourceData(writer, data_fbs.reader(), data_size);
   1998     }
   1999 
   2000     /// Weight and italic carry over from previous FONT statements within a single resource,
   2001     /// so they need to be parsed ahead-of-time and stored
   2002     const FontStatementValues = struct {
   2003         weight: u16 = 0,
   2004         italic: bool = false,
   2005         node: *Node.FontStatement,
   2006     };
   2007 
   2008     pub fn writeDialogFont(self: *Compiler, resource: Resource, values: FontStatementValues, writer: anytype) !void {
   2009         const node = values.node;
   2010         const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
   2011         try writer.writeInt(u16, point_size.asWord(), .little);
   2012 
   2013         if (resource == .dialogex) {
   2014             try writer.writeInt(u16, values.weight, .little);
   2015         }
   2016 
   2017         if (resource == .dialogex) {
   2018             try writer.writeInt(u8, @intFromBool(values.italic), .little);
   2019         }
   2020 
   2021         if (node.char_set) |char_set| {
   2022             const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
   2023             try writer.writeInt(u8, @as(u8, @truncate(value.value)), .little);
   2024         } else if (resource == .dialogex) {
   2025             try writer.writeInt(u8, 1, .little); // DEFAULT_CHARSET
   2026         }
   2027 
   2028         const typeface = try self.parseQuotedStringAsWideString(node.typeface);
   2029         defer self.allocator.free(typeface);
   2030         try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
   2031     }
   2032 
   2033     pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: anytype) !void {
   2034         var data_buffer = std.ArrayList(u8).init(self.allocator);
   2035         defer data_buffer.deinit();
   2036         // The header's data length field is a u32 so limit the resource's data size so that
   2037         // we know we can always specify the real size.
   2038         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   2039         const data_writer = limited_writer.writer();
   2040 
   2041         const type_bytes = SourceBytes{
   2042             .slice = node.type.slice(self.source),
   2043             .code_page = self.input_code_pages.getForToken(node.type),
   2044         };
   2045         const resource = Resource.fromString(type_bytes);
   2046         std.debug.assert(resource == .menu or resource == .menuex);
   2047 
   2048         self.writeMenuData(node, data_writer, resource) catch |err| switch (err) {
   2049             error.NoSpaceLeft => {
   2050                 return self.addErrorDetailsAndFail(.{
   2051                     .err = .resource_data_size_exceeds_max,
   2052                     .token = node.id,
   2053                 });
   2054             },
   2055             else => |e| return e,
   2056         };
   2057 
   2058         // This intCast can't fail because the limitedWriter above guarantees that
   2059         // we will never write more than maxInt(u32) bytes.
   2060         const data_size: u32 = @intCast(data_buffer.items.len);
   2061         var header = try self.resourceHeader(node.id, node.type, .{
   2062             .data_size = data_size,
   2063         });
   2064         defer header.deinit(self.allocator);
   2065 
   2066         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   2067         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   2068 
   2069         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
   2070 
   2071         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   2072         try writeResourceData(writer, data_fbs.reader(), data_size);
   2073     }
   2074 
   2075     /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
   2076     /// the writer within this function could return error.NoSpaceLeft
   2077     pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: anytype, resource: Resource) !void {
   2078         // menu header
   2079         const version: u16 = if (resource == .menu) 0 else 1;
   2080         try data_writer.writeInt(u16, version, .little);
   2081         const header_size: u16 = if (resource == .menu) 0 else 4;
   2082         try data_writer.writeInt(u16, header_size, .little); // cbHeaderSize
   2083         // Note: There can be extra bytes at the end of this header (`rgbExtra`),
   2084         //       but they are always zero-length for us, so we don't write anything
   2085         //       (the length of the rgbExtra field is inferred from the header_size).
   2086         // MENU   => rgbExtra: [cbHeaderSize]u8
   2087         // MENUEX => rgbExtra: [cbHeaderSize-4]u8
   2088 
   2089         if (resource == .menuex) {
   2090             if (node.help_id) |help_id_node| {
   2091                 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
   2092                 try data_writer.writeInt(u32, help_id.value, .little);
   2093             } else {
   2094                 try data_writer.writeInt(u32, 0, .little);
   2095             }
   2096         }
   2097 
   2098         for (node.items, 0..) |item, i| {
   2099             const is_last = i == node.items.len - 1;
   2100             try self.writeMenuItem(item, data_writer, is_last);
   2101         }
   2102     }
   2103 
   2104     pub fn writeMenuItem(self: *Compiler, node: *Node, writer: anytype, is_last_of_parent: bool) !void {
   2105         switch (node.id) {
   2106             .menu_item_separator => {
   2107                 // This is the 'alternate compability form' of the separator, see
   2108                 // https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
   2109                 //
   2110                 // The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
   2111                 // compiler still uses this alternate form, so that's what we use too.
   2112                 var flags = res.MenuItemFlags{};
   2113                 if (is_last_of_parent) flags.markLast();
   2114                 try writer.writeInt(u16, flags.value, .little);
   2115                 try writer.writeInt(u16, 0, .little); // id
   2116                 try writer.writeInt(u16, 0, .little); // null-terminated UTF-16 text
   2117             },
   2118             .menu_item => {
   2119                 const menu_item = @fieldParentPtr(Node.MenuItem, "base", node);
   2120                 var flags = res.MenuItemFlags{};
   2121                 for (menu_item.option_list) |option_token| {
   2122                     // This failing would be a bug in the parser
   2123                     const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
   2124                     flags.apply(option);
   2125                 }
   2126                 if (is_last_of_parent) flags.markLast();
   2127                 try writer.writeInt(u16, flags.value, .little);
   2128 
   2129                 var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
   2130                 try writer.writeInt(u16, result.asWord(), .little);
   2131 
   2132                 var text = try self.parseQuotedStringAsWideString(menu_item.text);
   2133                 defer self.allocator.free(text);
   2134                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2135             },
   2136             .popup => {
   2137                 const popup = @fieldParentPtr(Node.Popup, "base", node);
   2138                 var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
   2139                 for (popup.option_list) |option_token| {
   2140                     // This failing would be a bug in the parser
   2141                     const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
   2142                     flags.apply(option);
   2143                 }
   2144                 if (is_last_of_parent) flags.markLast();
   2145                 try writer.writeInt(u16, flags.value, .little);
   2146 
   2147                 var text = try self.parseQuotedStringAsWideString(popup.text);
   2148                 defer self.allocator.free(text);
   2149                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2150 
   2151                 for (popup.items, 0..) |item, i| {
   2152                     const is_last = i == popup.items.len - 1;
   2153                     try self.writeMenuItem(item, writer, is_last);
   2154                 }
   2155             },
   2156             inline .menu_item_ex, .popup_ex => |node_type| {
   2157                 const menu_item = @fieldParentPtr(node_type.Type(), "base", node);
   2158 
   2159                 if (menu_item.type) |flags| {
   2160                     const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
   2161                     try writer.writeInt(u32, value.value, .little);
   2162                 } else {
   2163                     try writer.writeInt(u32, 0, .little);
   2164                 }
   2165 
   2166                 if (menu_item.state) |state| {
   2167                     const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
   2168                     try writer.writeInt(u32, value.value, .little);
   2169                 } else {
   2170                     try writer.writeInt(u32, 0, .little);
   2171                 }
   2172 
   2173                 if (menu_item.id) |id| {
   2174                     const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
   2175                     try writer.writeInt(u32, value.value, .little);
   2176                 } else {
   2177                     try writer.writeInt(u32, 0, .little);
   2178                 }
   2179 
   2180                 var flags: u16 = 0;
   2181                 if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
   2182                 // This constant doesn't seem to have a named #define, it's different than MF_POPUP
   2183                 if (node_type == .popup_ex) flags |= 0x01;
   2184                 try writer.writeInt(u16, flags, .little);
   2185 
   2186                 var text = try self.parseQuotedStringAsWideString(menu_item.text);
   2187                 defer self.allocator.free(text);
   2188                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2189 
   2190                 // Only the combination of the flags u16 and the text bytes can cause
   2191                 // non-DWORD alignment, so we can just use the byte length of those
   2192                 // two values to realign to DWORD alignment.
   2193                 const relevant_bytes = 2 + (text.len + 1) * 2;
   2194                 try writeDataPadding(writer, @intCast(relevant_bytes));
   2195 
   2196                 if (node_type == .popup_ex) {
   2197                     if (menu_item.help_id) |help_id_node| {
   2198                         const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
   2199                         try writer.writeInt(u32, help_id.value, .little);
   2200                     } else {
   2201                         try writer.writeInt(u32, 0, .little);
   2202                     }
   2203 
   2204                     for (menu_item.items, 0..) |item, i| {
   2205                         const is_last = i == menu_item.items.len - 1;
   2206                         try self.writeMenuItem(item, writer, is_last);
   2207                     }
   2208                 }
   2209             },
   2210             else => unreachable,
   2211         }
   2212     }
   2213 
   2214     pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: anytype) !void {
   2215         var data_buffer = std.ArrayList(u8).init(self.allocator);
   2216         defer data_buffer.deinit();
   2217         // The node's length field (which is inclusive of the length of all of its children) is a u16
   2218         // so limit the node's data size so that we know we can always specify the real size.
   2219         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u16));
   2220         const data_writer = limited_writer.writer();
   2221 
   2222         try data_writer.writeInt(u16, 0, .little); // placeholder size
   2223         try data_writer.writeInt(u16, res.FixedFileInfo.byte_len, .little);
   2224         try data_writer.writeInt(u16, res.VersionNode.type_binary, .little);
   2225         const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
   2226         try data_writer.writeAll(key_bytes);
   2227         // The number of bytes written up to this point is always the same, since the name
   2228         // of the node is a constant (FixedFileInfo.key). The total number of bytes
   2229         // written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
   2230         try data_writer.writeInt(u16, 0, .little);
   2231 
   2232         var fixed_file_info = res.FixedFileInfo{};
   2233         for (node.fixed_info) |fixed_info| {
   2234             switch (fixed_info.id) {
   2235                 .version_statement => {
   2236                     const version_statement = @fieldParentPtr(Node.VersionStatement, "base", fixed_info);
   2237                     const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
   2238 
   2239                     // Ensure that all parts are cleared for each version, to properly account for
   2240                     // potential duplicate PRODUCTVERSION/FILEVERSION statements
   2241                     switch (version_type) {
   2242                         .file_version => @memset(&fixed_file_info.file_version.parts, 0),
   2243                         .product_version => @memset(&fixed_file_info.product_version.parts, 0),
   2244                         else => unreachable,
   2245                     }
   2246 
   2247                     for (version_statement.parts, 0..) |part, i| {
   2248                         const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
   2249                         if (part_value.is_long) {
   2250                             try self.addErrorDetails(.{
   2251                                 .err = .rc_would_error_u16_with_l_suffix,
   2252                                 .type = .warning,
   2253                                 .token = part.getFirstToken(),
   2254                                 .token_span_end = part.getLastToken(),
   2255                                 .extra = .{ .statement_with_u16_param = switch (version_type) {
   2256                                     .file_version => .fileversion,
   2257                                     .product_version => .productversion,
   2258                                     else => unreachable,
   2259                                 } },
   2260                             });
   2261                             try self.addErrorDetails(.{
   2262                                 .err = .rc_would_error_u16_with_l_suffix,
   2263                                 .print_source_line = false,
   2264                                 .type = .note,
   2265                                 .token = part.getFirstToken(),
   2266                                 .token_span_end = part.getLastToken(),
   2267                                 .extra = .{ .statement_with_u16_param = switch (version_type) {
   2268                                     .file_version => .fileversion,
   2269                                     .product_version => .productversion,
   2270                                     else => unreachable,
   2271                                 } },
   2272                             });
   2273                         }
   2274                         switch (version_type) {
   2275                             .file_version => {
   2276                                 fixed_file_info.file_version.parts[i] = part_value.asWord();
   2277                             },
   2278                             .product_version => {
   2279                                 fixed_file_info.product_version.parts[i] = part_value.asWord();
   2280                             },
   2281                             else => unreachable,
   2282                         }
   2283                     }
   2284                 },
   2285                 .simple_statement => {
   2286                     const statement = @fieldParentPtr(Node.SimpleStatement, "base", fixed_info);
   2287                     const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
   2288                     const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
   2289                     switch (statement_type) {
   2290                         .file_flags_mask => fixed_file_info.file_flags_mask = value.value,
   2291                         .file_flags => fixed_file_info.file_flags = value.value,
   2292                         .file_os => fixed_file_info.file_os = value.value,
   2293                         .file_type => fixed_file_info.file_type = value.value,
   2294                         .file_subtype => fixed_file_info.file_subtype = value.value,
   2295                         else => unreachable,
   2296                     }
   2297                 },
   2298                 else => unreachable,
   2299             }
   2300         }
   2301         try fixed_file_info.write(data_writer);
   2302 
   2303         for (node.block_statements) |statement| {
   2304             self.writeVersionNode(statement, data_writer, &data_buffer) catch |err| switch (err) {
   2305                 error.NoSpaceLeft => {
   2306                     try self.addErrorDetails(.{
   2307                         .err = .version_node_size_exceeds_max,
   2308                         .token = node.id,
   2309                     });
   2310                     return self.addErrorDetailsAndFail(.{
   2311                         .err = .version_node_size_exceeds_max,
   2312                         .type = .note,
   2313                         .token = statement.getFirstToken(),
   2314                         .token_span_end = statement.getLastToken(),
   2315                     });
   2316                 },
   2317                 else => |e| return e,
   2318             };
   2319         }
   2320 
   2321         // We know that data_buffer.items.len is within the limits of a u16, since we
   2322         // limited the writer to maxInt(u16)
   2323         const data_size: u16 = @intCast(data_buffer.items.len);
   2324         // And now that we know the full size of this node (including its children), set its size
   2325         std.mem.writeInt(u16, data_buffer.items[0..2], data_size, .little);
   2326 
   2327         var header = try self.resourceHeader(node.id, node.versioninfo, .{
   2328             .data_size = data_size,
   2329         });
   2330         defer header.deinit(self.allocator);
   2331 
   2332         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   2333 
   2334         try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
   2335 
   2336         var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   2337         try writeResourceData(writer, data_fbs.reader(), data_size);
   2338     }
   2339 
   2340     /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to
   2341     /// the writer within this function could return error.NoSpaceLeft, and that buf.items.len
   2342     /// will never be able to exceed maxInt(u16).
   2343     pub fn writeVersionNode(self: *Compiler, node: *Node, writer: anytype, buf: *std.ArrayList(u8)) !void {
   2344         // We can assume that buf.items.len will never be able to exceed the limits of a u16
   2345         try writeDataPadding(writer, @as(u16, @intCast(buf.items.len)));
   2346 
   2347         const node_and_children_size_offset = buf.items.len;
   2348         try writer.writeInt(u16, 0, .little); // placeholder for size
   2349         const data_size_offset = buf.items.len;
   2350         try writer.writeInt(u16, 0, .little); // placeholder for data size
   2351         const data_type_offset = buf.items.len;
   2352         // Data type is string unless the node contains values that are numbers.
   2353         try writer.writeInt(u16, res.VersionNode.type_string, .little);
   2354 
   2355         switch (node.id) {
   2356             inline .block, .block_value => |node_type| {
   2357                 const block_or_value = @fieldParentPtr(node_type.Type(), "base", node);
   2358                 const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
   2359                 defer self.allocator.free(parsed_key);
   2360 
   2361                 const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
   2362                 try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
   2363 
   2364                 var has_number_value: bool = false;
   2365                 for (block_or_value.values) |value_value_node_uncasted| {
   2366                     const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
   2367                     if (value_value_node.expression.isNumberExpression()) {
   2368                         has_number_value = true;
   2369                         break;
   2370                     }
   2371                 }
   2372                 // The units used here are dependent on the type. If there are any numbers, then
   2373                 // this is a byte count. If there are only strings, then this is a count of
   2374                 // UTF-16 code units.
   2375                 //
   2376                 // The Win32 RC compiler miscompiles this count in the case of values that
   2377                 // have a mix of numbers and strings. This is detected and a warning is emitted
   2378                 // during parsing, so we can just do the correct thing here.
   2379                 var values_size: usize = 0;
   2380 
   2381                 try writeDataPadding(writer, @intCast(buf.items.len));
   2382 
   2383                 for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
   2384                     const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
   2385                     const value_node = value_value_node.expression;
   2386                     if (value_node.isNumberExpression()) {
   2387                         const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
   2388                         // This is used to write u16 or u32 depending on the number's suffix
   2389                         const data_wrapper = Data{ .number = number };
   2390                         try data_wrapper.write(writer);
   2391                         // Numbers use byte count
   2392                         values_size += if (number.is_long) 4 else 2;
   2393                     } else {
   2394                         std.debug.assert(value_node.isStringLiteral());
   2395                         const literal_node = value_node.cast(.literal).?;
   2396                         const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
   2397                         defer self.allocator.free(parsed_value);
   2398 
   2399                         const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
   2400                         try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
   2401                         // Strings use UTF-16 code-unit count including the null-terminator, but
   2402                         // only if there are no number values in the list.
   2403                         var value_size = parsed_to_first_null.len;
   2404                         if (has_number_value) value_size *= 2; // 2 bytes per UTF-16 code unit
   2405                         values_size += value_size;
   2406                         // The null-terminator is only included if there's a trailing comma
   2407                         // or this is the last value. If the value evaluates to empty, then
   2408                         // it never gets a null terminator. If there was an explicit null-terminator
   2409                         // in the string, we still need to potentially add one since we already
   2410                         // sliced to the terminator.
   2411                         const is_last = i == block_or_value.values.len - 1;
   2412                         const is_empty = parsed_to_first_null.len == 0;
   2413                         const is_only = block_or_value.values.len == 1;
   2414                         if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
   2415                             try writer.writeInt(u16, 0, .little);
   2416                             values_size += if (has_number_value) 2 else 1;
   2417                         }
   2418                     }
   2419                 }
   2420                 var data_size_slice = buf.items[data_size_offset..];
   2421                 std.mem.writeInt(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)), .little);
   2422 
   2423                 if (has_number_value) {
   2424                     const data_type_slice = buf.items[data_type_offset..];
   2425                     std.mem.writeInt(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary, .little);
   2426                 }
   2427 
   2428                 if (node_type == .block) {
   2429                     const block = block_or_value;
   2430                     for (block.children) |child| {
   2431                         try self.writeVersionNode(child, writer, buf);
   2432                     }
   2433                 }
   2434             },
   2435             else => unreachable,
   2436         }
   2437 
   2438         const node_and_children_size = buf.items.len - node_and_children_size_offset;
   2439         const node_and_children_size_slice = buf.items[node_and_children_size_offset..];
   2440         std.mem.writeInt(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)), .little);
   2441     }
   2442 
   2443     pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
   2444         const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
   2445 
   2446         for (node.strings) |string_node| {
   2447             const string = @fieldParentPtr(Node.StringTableString, "base", string_node);
   2448             const string_id_data = try self.evaluateDataExpression(string.id);
   2449             const string_id = string_id_data.number.asWord();
   2450 
   2451             self.state.string_tables.set(
   2452                 self.arena,
   2453                 language,
   2454                 string_id,
   2455                 string.string,
   2456                 &node.base,
   2457                 self.source,
   2458                 self.input_code_pages,
   2459                 self.state.version,
   2460                 self.state.characteristics,
   2461             ) catch |err| switch (err) {
   2462                 error.StringAlreadyDefined => {
   2463                     // It might be nice to have these errors point to the ids rather than the
   2464                     // string tokens, but that would mean storing the id token of each string
   2465                     // which doesn't seem worth it just for slightly better error messages.
   2466                     try self.addErrorDetails(ErrorDetails{
   2467                         .err = .string_already_defined,
   2468                         .token = string.string,
   2469                         .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
   2470                     });
   2471                     const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
   2472                     const existing_definition = existing_def_table.get(string_id).?;
   2473                     return self.addErrorDetailsAndFail(ErrorDetails{
   2474                         .err = .string_already_defined,
   2475                         .type = .note,
   2476                         .token = existing_definition,
   2477                         .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
   2478                     });
   2479                 },
   2480                 error.OutOfMemory => |e| return e,
   2481             };
   2482         }
   2483     }
   2484 
   2485     /// Expects this to be a top-level LANGUAGE statement
   2486     pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
   2487         const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
   2488         const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
   2489         self.state.language.primary_language_id = @truncate(primary.value);
   2490         self.state.language.sublanguage_id = @truncate(sublanguage.value);
   2491     }
   2492 
   2493     /// Expects this to be a top-level VERSION or CHARACTERISTICS statement
   2494     pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
   2495         const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
   2496         const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
   2497         switch (statement_type) {
   2498             .characteristics => self.state.characteristics = value.value,
   2499             .version => self.state.version = value.value,
   2500             else => unreachable,
   2501         }
   2502     }
   2503 
   2504     pub const ResourceHeaderOptions = struct {
   2505         language: ?res.Language = null,
   2506         data_size: DWORD = 0,
   2507     };
   2508 
   2509     pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
   2510         const id_bytes = self.sourceBytesForToken(id_token);
   2511         const type_bytes = self.sourceBytesForToken(type_token);
   2512         return ResourceHeader.init(
   2513             self.allocator,
   2514             id_bytes,
   2515             type_bytes,
   2516             options.data_size,
   2517             options.language orelse self.state.language,
   2518             self.state.version,
   2519             self.state.characteristics,
   2520         ) catch |err| switch (err) {
   2521             error.OutOfMemory => |e| return e,
   2522             error.TypeNonAsciiOrdinal => {
   2523                 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
   2524                 try self.addErrorDetails(.{
   2525                     .err = .invalid_digit_character_in_ordinal,
   2526                     .type = .err,
   2527                     .token = type_token,
   2528                 });
   2529                 return self.addErrorDetailsAndFail(.{
   2530                     .err = .win32_non_ascii_ordinal,
   2531                     .type = .note,
   2532                     .token = type_token,
   2533                     .print_source_line = false,
   2534                     .extra = .{ .number = win32_rc_ordinal.ordinal },
   2535                 });
   2536             },
   2537             error.IdNonAsciiOrdinal => {
   2538                 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
   2539                 try self.addErrorDetails(.{
   2540                     .err = .invalid_digit_character_in_ordinal,
   2541                     .type = .err,
   2542                     .token = id_token,
   2543                 });
   2544                 return self.addErrorDetailsAndFail(.{
   2545                     .err = .win32_non_ascii_ordinal,
   2546                     .type = .note,
   2547                     .token = id_token,
   2548                     .print_source_line = false,
   2549                     .extra = .{ .number = win32_rc_ordinal.ordinal },
   2550                 });
   2551             },
   2552         };
   2553     }
   2554 
   2555     pub const ResourceHeader = struct {
   2556         name_value: NameOrOrdinal,
   2557         type_value: NameOrOrdinal,
   2558         language: res.Language,
   2559         memory_flags: MemoryFlags,
   2560         data_size: DWORD,
   2561         version: DWORD,
   2562         characteristics: DWORD,
   2563         data_version: DWORD = 0,
   2564 
   2565         pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
   2566 
   2567         pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
   2568             const type_value = type: {
   2569                 const resource_type = Resource.fromString(type_bytes);
   2570                 if (res.RT.fromResource(resource_type)) |rt_constant| {
   2571                     break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
   2572                 } else {
   2573                     break :type try NameOrOrdinal.fromString(allocator, type_bytes);
   2574                 }
   2575             };
   2576             errdefer type_value.deinit(allocator);
   2577             if (type_value == .name) {
   2578                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
   2579                     return error.TypeNonAsciiOrdinal;
   2580                 }
   2581             }
   2582 
   2583             const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
   2584             errdefer name_value.deinit(allocator);
   2585             if (name_value == .name) {
   2586                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
   2587                     return error.IdNonAsciiOrdinal;
   2588                 }
   2589             }
   2590 
   2591             const predefined_resource_type = type_value.predefinedResourceType();
   2592 
   2593             return ResourceHeader{
   2594                 .name_value = name_value,
   2595                 .type_value = type_value,
   2596                 .data_size = data_size,
   2597                 .memory_flags = MemoryFlags.defaults(predefined_resource_type),
   2598                 .language = language,
   2599                 .version = version,
   2600                 .characteristics = characteristics,
   2601             };
   2602         }
   2603 
   2604         pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
   2605             self.name_value.deinit(allocator);
   2606             self.type_value.deinit(allocator);
   2607         }
   2608 
   2609         pub const SizeInfo = struct {
   2610             bytes: u32,
   2611             padding_after_name: u2,
   2612         };
   2613 
   2614         fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
   2615             var header_size: u32 = 8;
   2616             header_size = try std.math.add(
   2617                 u32,
   2618                 header_size,
   2619                 std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
   2620             );
   2621             header_size = try std.math.add(
   2622                 u32,
   2623                 header_size,
   2624                 std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
   2625             );
   2626             const padding_after_name = numPaddingBytesNeeded(header_size);
   2627             header_size = try std.math.add(u32, header_size, padding_after_name);
   2628             header_size = try std.math.add(u32, header_size, 16);
   2629             return .{ .bytes = header_size, .padding_after_name = padding_after_name };
   2630         }
   2631 
   2632         pub fn writeAssertNoOverflow(self: ResourceHeader, writer: anytype) !void {
   2633             return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
   2634         }
   2635 
   2636         pub fn write(self: ResourceHeader, writer: anytype, err_ctx: errors.DiagnosticsContext) !void {
   2637             const size_info = self.calcSize() catch {
   2638                 try err_ctx.diagnostics.append(.{
   2639                     .err = .resource_data_size_exceeds_max,
   2640                     .token = err_ctx.token,
   2641                 });
   2642                 return error.CompileError;
   2643             };
   2644             return self.writeSizeInfo(writer, size_info);
   2645         }
   2646 
   2647         fn writeSizeInfo(self: ResourceHeader, writer: anytype, size_info: SizeInfo) !void {
   2648             try writer.writeInt(DWORD, self.data_size, .little); // DataSize
   2649             try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize
   2650             try self.type_value.write(writer); // TYPE
   2651             try self.name_value.write(writer); // NAME
   2652             try writer.writeByteNTimes(0, size_info.padding_after_name);
   2653 
   2654             try writer.writeInt(DWORD, self.data_version, .little); // DataVersion
   2655             try writer.writeInt(WORD, self.memory_flags.value, .little); // MemoryFlags
   2656             try writer.writeInt(WORD, self.language.asInt(), .little); // LanguageId
   2657             try writer.writeInt(DWORD, self.version, .little); // Version
   2658             try writer.writeInt(DWORD, self.characteristics, .little); // Characteristics
   2659         }
   2660 
   2661         pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
   2662             return self.type_value.predefinedResourceType();
   2663         }
   2664 
   2665         pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
   2666             applyToMemoryFlags(&self.memory_flags, tokens, source);
   2667         }
   2668 
   2669         pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   2670             applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
   2671         }
   2672     };
   2673 
   2674     fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
   2675         for (tokens) |token| {
   2676             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2677             flags.set(attribute);
   2678         }
   2679     }
   2680 
   2681     /// RT_GROUP_ICON and RT_GROUP_CURSOR have their own special rules for memory flags
   2682     fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
   2683         // There's probably a cleaner implementation of this, but this will result in the same
   2684         // flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
   2685         // for an ICON resource.
   2686         //
   2687         // This was arrived at by iterating over the permutations and creating a
   2688         // list where each line looks something like this:
   2689         // MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
   2690         //
   2691         // and then noticing a few things:
   2692 
   2693         // 1. Any permutation that does not have PRELOAD in it just uses the
   2694         //    default flags.
   2695         const initial_flags = flags.*;
   2696         var flags_set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
   2697         for (tokens) |token| {
   2698             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2699             flags_set.insert(attribute);
   2700         }
   2701         if (!flags_set.contains(.preload)) return;
   2702 
   2703         // 2. Any permutation of flags where applying only the PRELOAD and LOADONCALL flags
   2704         //    results in no actual change by the end will just use the default flags.
   2705         //    For example, `PRELOAD LOADONCALL` will result in default flags, but
   2706         //    `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
   2707         for (tokens) |token| {
   2708             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2709             switch (attribute) {
   2710                 .preload, .loadoncall => flags.set(attribute),
   2711                 else => {},
   2712             }
   2713         }
   2714         if (flags.value == initial_flags.value) return;
   2715 
   2716         // 3. If none of DISCARDABLE, SHARED, or PURE is specified, then PRELOAD
   2717         //    implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
   2718         const shared_set = comptime blk: {
   2719             var set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
   2720             set.insert(.discardable);
   2721             set.insert(.shared);
   2722             set.insert(.pure);
   2723             break :blk set;
   2724         };
   2725         const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
   2726         for (tokens) |token| {
   2727             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2728             flags.setGroup(attribute, !discardable_shared_or_pure_specified);
   2729         }
   2730     }
   2731 
   2732     /// Only handles the 'base' optional statements that are shared between resource types.
   2733     fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   2734         for (statements) |node| switch (node.id) {
   2735             .language_statement => {
   2736                 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
   2737                 language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
   2738             },
   2739             .simple_statement => {
   2740                 const simple_statement = @fieldParentPtr(Node.SimpleStatement, "base", node);
   2741                 const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
   2742                 const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
   2743                 switch (statement_type) {
   2744                     .version => version.* = result.value,
   2745                     .characteristics => characteristics.* = result.value,
   2746                     else => unreachable, // only VERSION and CHARACTERISTICS should be in an optional statements list
   2747                 }
   2748             },
   2749             else => {},
   2750         };
   2751     }
   2752 
   2753     pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
   2754         const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
   2755         const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
   2756         return .{
   2757             .primary_language_id = @truncate(primary.value),
   2758             .sublanguage_id = @truncate(sublanguage.value),
   2759         };
   2760     }
   2761 
   2762     pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
   2763         for (statements) |node| switch (node.id) {
   2764             .language_statement => {
   2765                 const language_statement = @fieldParentPtr(Node.LanguageStatement, "base", node);
   2766                 return languageFromLanguageStatement(language_statement, source, code_page_lookup);
   2767             },
   2768             else => continue,
   2769         };
   2770         return null;
   2771     }
   2772 
   2773     pub fn writeEmptyResource(writer: anytype) !void {
   2774         const header = ResourceHeader{
   2775             .name_value = .{ .ordinal = 0 },
   2776             .type_value = .{ .ordinal = 0 },
   2777             .language = .{
   2778                 .primary_language_id = 0,
   2779                 .sublanguage_id = 0,
   2780             },
   2781             .memory_flags = .{ .value = 0 },
   2782             .data_size = 0,
   2783             .version = 0,
   2784             .characteristics = 0,
   2785         };
   2786         try header.writeAssertNoOverflow(writer);
   2787     }
   2788 
   2789     pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
   2790         return .{
   2791             .slice = token.slice(self.source),
   2792             .code_page = self.input_code_pages.getForToken(token),
   2793         };
   2794     }
   2795 
   2796     /// Helper that calls parseQuotedStringAsWideString with the relevant context
   2797     /// Resulting slice is allocated by `self.allocator`.
   2798     pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
   2799         return literals.parseQuotedStringAsWideString(
   2800             self.allocator,
   2801             self.sourceBytesForToken(token),
   2802             .{
   2803                 .start_column = token.calculateColumn(self.source, 8, null),
   2804                 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
   2805             },
   2806         );
   2807     }
   2808 
   2809     /// Helper that calls parseQuotedStringAsAsciiString with the relevant context
   2810     /// Resulting slice is allocated by `self.allocator`.
   2811     pub fn parseQuotedStringAsAsciiString(self: *Compiler, token: Token) ![]u8 {
   2812         return literals.parseQuotedStringAsAsciiString(
   2813             self.allocator,
   2814             self.sourceBytesForToken(token),
   2815             .{
   2816                 .start_column = token.calculateColumn(self.source, 8, null),
   2817                 .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
   2818             },
   2819         );
   2820     }
   2821 
   2822     fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
   2823         try self.diagnostics.append(details);
   2824     }
   2825 
   2826     fn addErrorDetailsAndFail(self: *Compiler, details: ErrorDetails) error{ CompileError, OutOfMemory } {
   2827         try self.addErrorDetails(details);
   2828         return error.CompileError;
   2829     }
   2830 };
   2831 
   2832 pub const OpenSearchPathError = std.fs.Dir.OpenError;
   2833 
   2834 fn openSearchPathDir(dir: std.fs.Dir, path: []const u8) OpenSearchPathError!std.fs.Dir {
   2835     // Validate the search path to avoid possible unreachable on invalid paths,
   2836     // see https://github.com/ziglang/zig/issues/15607 for why this is currently necessary.
   2837     try validateSearchPath(path);
   2838     return dir.openDir(path, .{});
   2839 }
   2840 
   2841 /// Very crude attempt at validating a path. This is imperfect
   2842 /// and AFAIK it is effectively impossible to implement perfect path
   2843 /// validation, since it ultimately depends on the underlying filesystem.
   2844 /// Note that this function won't be necessary if/when
   2845 /// https://github.com/ziglang/zig/issues/15607
   2846 /// is accepted/implemented.
   2847 fn validateSearchPath(path: []const u8) error{BadPathName}!void {
   2848     switch (builtin.os.tag) {
   2849         .windows => {
   2850             // This will return error.BadPathName on non-Win32 namespaced paths
   2851             // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
   2852             // Those path types are something of an unavoidable way to
   2853             // still hit unreachable during the openDir call.
   2854             var component_iterator = try std.fs.path.componentIterator(path);
   2855             while (component_iterator.next()) |component| {
   2856                 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
   2857                 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
   2858             }
   2859         },
   2860         else => {
   2861             if (std.mem.indexOfScalar(u8, path, 0) != null) return error.BadPathName;
   2862         },
   2863     }
   2864 }
   2865 
   2866 pub const SearchDir = struct {
   2867     dir: std.fs.Dir,
   2868     path: ?[]const u8,
   2869 
   2870     pub fn deinit(self: *SearchDir, allocator: Allocator) void {
   2871         self.dir.close();
   2872         if (self.path) |path| {
   2873             allocator.free(path);
   2874         }
   2875     }
   2876 };
   2877 
   2878 /// Slurps the first `size` bytes read into `slurped_header`
   2879 pub fn HeaderSlurpingReader(comptime size: usize, comptime ReaderType: anytype) type {
   2880     return struct {
   2881         child_reader: ReaderType,
   2882         bytes_read: usize = 0,
   2883         slurped_header: [size]u8 = [_]u8{0x00} ** size,
   2884 
   2885         pub const Error = ReaderType.Error;
   2886         pub const Reader = std.io.Reader(*@This(), Error, read);
   2887 
   2888         pub fn read(self: *@This(), buf: []u8) Error!usize {
   2889             const amt = try self.child_reader.read(buf);
   2890             if (self.bytes_read < size) {
   2891                 const bytes_to_add = @min(amt, size - self.bytes_read);
   2892                 const end_index = self.bytes_read + bytes_to_add;
   2893                 @memcpy(self.slurped_header[self.bytes_read..end_index], buf[0..bytes_to_add]);
   2894             }
   2895             self.bytes_read +|= amt;
   2896             return amt;
   2897         }
   2898 
   2899         pub fn reader(self: *@This()) Reader {
   2900             return .{ .context = self };
   2901         }
   2902     };
   2903 }
   2904 
   2905 pub fn headerSlurpingReader(comptime size: usize, reader: anytype) HeaderSlurpingReader(size, @TypeOf(reader)) {
   2906     return .{ .child_reader = reader };
   2907 }
   2908 
   2909 /// Sort of like std.io.LimitedReader, but a Writer.
   2910 /// Returns an error if writing the requested number of bytes
   2911 /// would ever exceed bytes_left, i.e. it does not always
   2912 /// write up to the limit and instead will error if the
   2913 /// limit would be breached if the entire slice was written.
   2914 pub fn LimitedWriter(comptime WriterType: type) type {
   2915     return struct {
   2916         inner_writer: WriterType,
   2917         bytes_left: u64,
   2918 
   2919         pub const Error = error{NoSpaceLeft} || WriterType.Error;
   2920         pub const Writer = std.io.Writer(*Self, Error, write);
   2921 
   2922         const Self = @This();
   2923 
   2924         pub fn write(self: *Self, bytes: []const u8) Error!usize {
   2925             if (bytes.len > self.bytes_left) return error.NoSpaceLeft;
   2926             const amt = try self.inner_writer.write(bytes);
   2927             self.bytes_left -= amt;
   2928             return amt;
   2929         }
   2930 
   2931         pub fn writer(self: *Self) Writer {
   2932             return .{ .context = self };
   2933         }
   2934     };
   2935 }
   2936 
   2937 /// Returns an initialised `LimitedWriter`
   2938 /// `bytes_left` is a `u64` to be able to take 64 bit file offsets
   2939 pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) {
   2940     return .{ .inner_writer = inner_writer, .bytes_left = bytes_left };
   2941 }
   2942 
   2943 test "limitedWriter basic usage" {
   2944     var buf: [4]u8 = undefined;
   2945     var fbs = std.io.fixedBufferStream(&buf);
   2946     var limited_stream = limitedWriter(fbs.writer(), 4);
   2947     var writer = limited_stream.writer();
   2948 
   2949     try std.testing.expectEqual(@as(usize, 3), try writer.write("123"));
   2950     try std.testing.expectEqualSlices(u8, "123", buf[0..3]);
   2951     try std.testing.expectError(error.NoSpaceLeft, writer.write("45"));
   2952     try std.testing.expectEqual(@as(usize, 1), try writer.write("4"));
   2953     try std.testing.expectEqualSlices(u8, "1234", buf[0..4]);
   2954     try std.testing.expectError(error.NoSpaceLeft, writer.write("5"));
   2955 }
   2956 
   2957 pub const FontDir = struct {
   2958     fonts: std.ArrayListUnmanaged(Font) = .{},
   2959     /// To keep track of which ids are set and where they were set from
   2960     ids: std.AutoHashMapUnmanaged(u16, Token) = .{},
   2961 
   2962     pub const Font = struct {
   2963         id: u16,
   2964         header_bytes: [148]u8,
   2965     };
   2966 
   2967     pub fn deinit(self: *FontDir, allocator: Allocator) void {
   2968         self.fonts.deinit(allocator);
   2969     }
   2970 
   2971     pub fn add(self: *FontDir, allocator: Allocator, font: Font, id_token: Token) !void {
   2972         try self.ids.putNoClobber(allocator, font.id, id_token);
   2973         try self.fonts.append(allocator, font);
   2974     }
   2975 
   2976     pub fn writeResData(self: *FontDir, compiler: *Compiler, writer: anytype) !void {
   2977         if (self.fonts.items.len == 0) return;
   2978 
   2979         // We know the number of fonts is limited to maxInt(u16) because fonts
   2980         // must have a valid and unique u16 ordinal ID (trying to specify a FONT
   2981         // with e.g. id 65537 will wrap around to 1 and be ignored if there's already
   2982         // a font with that ID in the file).
   2983         const num_fonts: u16 = @intCast(self.fonts.items.len);
   2984 
   2985         // u16 count + [(u16 id + 150 bytes) for each font]
   2986         // Note: This works out to a maximum data_size of 9,961,322.
   2987         const data_size: u32 = 2 + (2 + 150) * num_fonts;
   2988 
   2989         var header = Compiler.ResourceHeader{
   2990             .name_value = try NameOrOrdinal.nameFromString(compiler.allocator, .{ .slice = "FONTDIR", .code_page = .windows1252 }),
   2991             .type_value = NameOrOrdinal{ .ordinal = @intFromEnum(res.RT.FONTDIR) },
   2992             .memory_flags = res.MemoryFlags.defaults(res.RT.FONTDIR),
   2993             .language = compiler.state.language,
   2994             .version = compiler.state.version,
   2995             .characteristics = compiler.state.characteristics,
   2996             .data_size = data_size,
   2997         };
   2998         defer header.deinit(compiler.allocator);
   2999 
   3000         try header.writeAssertNoOverflow(writer);
   3001         try writer.writeInt(u16, num_fonts, .little);
   3002         for (self.fonts.items) |font| {
   3003             // The format of the FONTDIR is a strange beast.
   3004             // Technically, each FONT is seemingly meant to be written as a
   3005             // FONTDIRENTRY with two trailing NUL-terminated strings corresponding to
   3006             // the 'device name' and 'face name' of the .FNT file, but:
   3007             //
   3008             // 1. When dealing with .FNT files, the Win32 implementation
   3009             //    gets the device name and face name from the wrong locations,
   3010             //    so it's basically never going to write the real device/face name
   3011             //    strings.
   3012             // 2. When dealing with files 76-140 bytes long, the Win32 implementation
   3013             //    can just crash (if there are no NUL bytes in the file).
   3014             // 3. The 32-bit Win32 rc.exe uses a 148 byte size for the portion of
   3015             //    the FONTDIRENTRY before the NUL-terminated strings, which
   3016             //    does not match the documented FONTDIRENTRY size that (presumably)
   3017             //    this format is meant to be using, so anything iterating the
   3018             //    FONTDIR according to the available documentation will get bogus results.
   3019             // 4. The FONT resource can be used for non-.FNT types like TTF and OTF,
   3020             //    in which case emulating the Win32 behavior of unconditionally
   3021             //    interpreting the bytes as a .FNT and trying to grab device/face names
   3022             //    from random bytes in the TTF/OTF file can lead to weird behavior
   3023             //    and errors in the Win32 implementation (for example, the device/face
   3024             //    name fields are offsets into the file where the NUL-terminated
   3025             //    string is located, but the Win32 implementation actually treats
   3026             //    them as signed so if they are negative then the Win32 implementation
   3027             //    will error; this happening for TTF fonts would just be a bug
   3028             //    since the TTF could otherwise be valid)
   3029             // 5. The FONTDIR resource doesn't actually seem to be used at all by
   3030             //    anything that I've found, and instead in Windows 3.0 and newer
   3031             //    it seems like the FONT resources are always just iterated/accessed
   3032             //    directly without ever looking at the FONTDIR.
   3033             //
   3034             // All of these combined means that we:
   3035             // - Do not need or want to emulate Win32 behavior here
   3036             // - For maximum simplicity and compatibility, we just write the first
   3037             //   148 bytes of the file without any interpretation (padded with
   3038             //   zeroes to get up to 148 bytes if necessary), and then
   3039             //   unconditionally write two NUL bytes, meaning that we always
   3040             //   write 'device name' and 'face name' as if they were 0-length
   3041             //   strings.
   3042             //
   3043             // This gives us byte-for-byte .RES compatibility in the common case while
   3044             // allowing us to avoid any erroneous errors caused by trying to read
   3045             // the face/device name from a bogus location. Note that the Win32
   3046             // implementation never actually writes the real device/face name here
   3047             // anyway (except in the bizarre case that a .FNT file has the proper
   3048             // device/face name offsets within a reserved section of the .FNT file)
   3049             // so there's no feasible way that anything can actually think that the
   3050             // device name/face name in the FONTDIR is reliable.
   3051 
   3052             // First, the ID is written, though
   3053             try writer.writeInt(u16, font.id, .little);
   3054             try writer.writeAll(&font.header_bytes);
   3055             try writer.writeByteNTimes(0, 2);
   3056         }
   3057         try Compiler.writeDataPadding(writer, data_size);
   3058     }
   3059 };
   3060 
   3061 pub const StringTablesByLanguage = struct {
   3062     /// String tables for each language are written to the .res file in order depending on
   3063     /// when the first STRINGTABLE for the language was defined, and all blocks for a given
   3064     /// language are written contiguously.
   3065     /// Using an ArrayHashMap here gives us this property for free.
   3066     tables: std.AutoArrayHashMapUnmanaged(res.Language, StringTable) = .{},
   3067 
   3068     pub fn deinit(self: *StringTablesByLanguage, allocator: Allocator) void {
   3069         self.tables.deinit(allocator);
   3070     }
   3071 
   3072     pub fn set(
   3073         self: *StringTablesByLanguage,
   3074         allocator: Allocator,
   3075         language: res.Language,
   3076         id: u16,
   3077         string_token: Token,
   3078         node: *Node,
   3079         source: []const u8,
   3080         code_page_lookup: *const CodePageLookup,
   3081         version: u32,
   3082         characteristics: u32,
   3083     ) StringTable.SetError!void {
   3084         var get_or_put_result = try self.tables.getOrPut(allocator, language);
   3085         if (!get_or_put_result.found_existing) {
   3086             get_or_put_result.value_ptr.* = StringTable{};
   3087         }
   3088         return get_or_put_result.value_ptr.set(allocator, id, string_token, node, source, code_page_lookup, version, characteristics);
   3089     }
   3090 };
   3091 
   3092 pub const StringTable = struct {
   3093     /// Blocks are written to the .res file in order depending on when the first string
   3094     /// was added to the block (i.e. `STRINGTABLE { 16 "b" 0 "a" }` would then get written
   3095     /// with block ID 2 (the one with "b") first and block ID 1 (the one with "a") second).
   3096     /// Using an ArrayHashMap here gives us this property for free.
   3097     blocks: std.AutoArrayHashMapUnmanaged(u16, Block) = .{},
   3098 
   3099     pub const Block = struct {
   3100         strings: std.ArrayListUnmanaged(Token) = .{},
   3101         set_indexes: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 },
   3102         memory_flags: MemoryFlags = MemoryFlags.defaults(res.RT.STRING),
   3103         characteristics: u32,
   3104         version: u32,
   3105 
   3106         /// Returns the index to insert the string into the `strings` list.
   3107         /// Returns null if the string should be appended.
   3108         fn getInsertionIndex(self: *Block, index: u8) ?u8 {
   3109             std.debug.assert(!self.set_indexes.isSet(index));
   3110 
   3111             const first_set = self.set_indexes.findFirstSet() orelse return null;
   3112             if (first_set > index) return 0;
   3113 
   3114             const last_set = 15 - @clz(self.set_indexes.mask);
   3115             if (index > last_set) return null;
   3116 
   3117             var bit = first_set + 1;
   3118             var insertion_index: u8 = 1;
   3119             while (bit != index) : (bit += 1) {
   3120                 if (self.set_indexes.isSet(bit)) insertion_index += 1;
   3121             }
   3122             return insertion_index;
   3123         }
   3124 
   3125         fn getTokenIndex(self: *Block, string_index: u8) ?u8 {
   3126             const count = self.strings.items.len;
   3127             if (count == 0) return null;
   3128             if (count == 1) return 0;
   3129 
   3130             const first_set = self.set_indexes.findFirstSet() orelse unreachable;
   3131             if (first_set == string_index) return 0;
   3132             const last_set = 15 - @clz(self.set_indexes.mask);
   3133             if (last_set == string_index) return @intCast(count - 1);
   3134 
   3135             if (first_set == last_set) return null;
   3136 
   3137             var bit = first_set + 1;
   3138             var token_index: u8 = 1;
   3139             while (bit < last_set) : (bit += 1) {
   3140                 if (!self.set_indexes.isSet(bit)) continue;
   3141                 if (bit == string_index) return token_index;
   3142                 token_index += 1;
   3143             }
   3144             return null;
   3145         }
   3146 
   3147         fn dump(self: *Block) void {
   3148             var bit_it = self.set_indexes.iterator(.{});
   3149             var string_index: usize = 0;
   3150             while (bit_it.next()) |bit_index| {
   3151                 const token = self.strings.items[string_index];
   3152                 std.debug.print("{}: [{}] {any}\n", .{ bit_index, string_index, token });
   3153                 string_index += 1;
   3154             }
   3155         }
   3156 
   3157         pub fn applyAttributes(self: *Block, string_table: *Node.StringTable, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   3158             Compiler.applyToMemoryFlags(&self.memory_flags, string_table.common_resource_attributes, source);
   3159             var dummy_language: res.Language = undefined;
   3160             Compiler.applyToOptionalStatements(&dummy_language, &self.version, &self.characteristics, string_table.optional_statements, source, code_page_lookup);
   3161         }
   3162 
   3163         fn trimToDoubleNUL(comptime T: type, str: []const T) []const T {
   3164             var last_was_null = false;
   3165             for (str, 0..) |c, i| {
   3166                 if (c == 0) {
   3167                     if (last_was_null) return str[0 .. i - 1];
   3168                     last_was_null = true;
   3169                 } else {
   3170                     last_was_null = false;
   3171                 }
   3172             }
   3173             return str;
   3174         }
   3175 
   3176         test "trimToDoubleNUL" {
   3177             try std.testing.expectEqualStrings("a\x00b", trimToDoubleNUL(u8, "a\x00b"));
   3178             try std.testing.expectEqualStrings("a", trimToDoubleNUL(u8, "a\x00\x00b"));
   3179         }
   3180 
   3181         pub fn writeResData(self: *Block, compiler: *Compiler, language: res.Language, block_id: u16, writer: anytype) !void {
   3182             var data_buffer = std.ArrayList(u8).init(compiler.allocator);
   3183             defer data_buffer.deinit();
   3184             const data_writer = data_buffer.writer();
   3185 
   3186             var i: u8 = 0;
   3187             var string_i: u8 = 0;
   3188             while (true) : (i += 1) {
   3189                 if (!self.set_indexes.isSet(i)) {
   3190                     try data_writer.writeInt(u16, 0, .little);
   3191                     if (i == 15) break else continue;
   3192                 }
   3193 
   3194                 const string_token = self.strings.items[string_i];
   3195                 const slice = string_token.slice(compiler.source);
   3196                 const column = string_token.calculateColumn(compiler.source, 8, null);
   3197                 const code_page = compiler.input_code_pages.getForToken(string_token);
   3198                 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
   3199                 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
   3200                     .start_column = column,
   3201                     .diagnostics = .{ .diagnostics = compiler.diagnostics, .token = string_token },
   3202                 });
   3203                 defer compiler.allocator.free(utf16_string);
   3204 
   3205                 const trimmed_string = trim: {
   3206                     // Two NUL characters in a row act as a terminator
   3207                     // Note: This is only the case for STRINGTABLE strings
   3208                     const trimmed = trimToDoubleNUL(u16, utf16_string);
   3209                     // We also want to trim any trailing NUL characters
   3210                     break :trim std.mem.trimRight(u16, trimmed, &[_]u16{0});
   3211                 };
   3212 
   3213                 // String literals are limited to maxInt(u15) codepoints, so these UTF-16 encoded
   3214                 // strings are limited to maxInt(u15) * 2 = 65,534 code units (since 2 is the
   3215                 // maximum number of UTF-16 code units per codepoint).
   3216                 // This leaves room for exactly one NUL terminator.
   3217                 var string_len_in_utf16_code_units: u16 = @intCast(trimmed_string.len);
   3218                 // If the option is set, then a NUL terminator is added unconditionally.
   3219                 // We already trimmed any trailing NULs, so we know it will be a new addition to the string.
   3220                 if (compiler.null_terminate_string_table_strings) string_len_in_utf16_code_units += 1;
   3221                 try data_writer.writeInt(u16, string_len_in_utf16_code_units, .little);
   3222                 try data_writer.writeAll(std.mem.sliceAsBytes(trimmed_string));
   3223                 if (compiler.null_terminate_string_table_strings) {
   3224                     try data_writer.writeInt(u16, 0, .little);
   3225                 }
   3226 
   3227                 if (i == 15) break;
   3228                 string_i += 1;
   3229             }
   3230 
   3231             // This intCast will never be able to fail due to the length constraints on string literals.
   3232             //
   3233             // - STRINGTABLE resource definitions can can only provide one string literal per index.
   3234             // - STRINGTABLE strings are limited to maxInt(u16) UTF-16 code units (see 'string_len_in_utf16_code_units'
   3235             //   above), which means that the maximum number of bytes per string literal is
   3236             //   2 * maxInt(u16) = 131,070 (since there are 2 bytes per UTF-16 code unit).
   3237             // - Each Block/RT_STRING resource includes exactly 16 strings and each have a 2 byte
   3238             //   length field, so the maximum number of total bytes in a RT_STRING resource's data is
   3239             //   16 * (131,070 + 2) = 2,097,152 which is well within the u32 max.
   3240             //
   3241             // Note: The string literal maximum length is enforced by the lexer.
   3242             const data_size: u32 = @intCast(data_buffer.items.len);
   3243 
   3244             const header = Compiler.ResourceHeader{
   3245                 .name_value = .{ .ordinal = block_id },
   3246                 .type_value = .{ .ordinal = @intFromEnum(res.RT.STRING) },
   3247                 .memory_flags = self.memory_flags,
   3248                 .language = language,
   3249                 .version = self.version,
   3250                 .characteristics = self.characteristics,
   3251                 .data_size = data_size,
   3252             };
   3253             // The only variable parts of the header are name and type, which in this case
   3254             // we fully control and know are numbers, so they have a fixed size.
   3255             try header.writeAssertNoOverflow(writer);
   3256 
   3257             var data_fbs = std.io.fixedBufferStream(data_buffer.items);
   3258             try Compiler.writeResourceData(writer, data_fbs.reader(), data_size);
   3259         }
   3260     };
   3261 
   3262     pub fn deinit(self: *StringTable, allocator: Allocator) void {
   3263         var it = self.blocks.iterator();
   3264         while (it.next()) |entry| {
   3265             entry.value_ptr.strings.deinit(allocator);
   3266         }
   3267         self.blocks.deinit(allocator);
   3268     }
   3269 
   3270     const SetError = error{StringAlreadyDefined} || Allocator.Error;
   3271 
   3272     pub fn set(
   3273         self: *StringTable,
   3274         allocator: Allocator,
   3275         id: u16,
   3276         string_token: Token,
   3277         node: *Node,
   3278         source: []const u8,
   3279         code_page_lookup: *const CodePageLookup,
   3280         version: u32,
   3281         characteristics: u32,
   3282     ) SetError!void {
   3283         const block_id = (id / 16) + 1;
   3284         const string_index: u8 = @intCast(id & 0xF);
   3285 
   3286         var get_or_put_result = try self.blocks.getOrPut(allocator, block_id);
   3287         if (!get_or_put_result.found_existing) {
   3288             get_or_put_result.value_ptr.* = Block{ .version = version, .characteristics = characteristics };
   3289             get_or_put_result.value_ptr.applyAttributes(node.cast(.string_table).?, source, code_page_lookup);
   3290         } else {
   3291             if (get_or_put_result.value_ptr.set_indexes.isSet(string_index)) {
   3292                 return error.StringAlreadyDefined;
   3293             }
   3294         }
   3295 
   3296         var block = get_or_put_result.value_ptr;
   3297         if (block.getInsertionIndex(string_index)) |insertion_index| {
   3298             try block.strings.insert(allocator, insertion_index, string_token);
   3299         } else {
   3300             try block.strings.append(allocator, string_token);
   3301         }
   3302         block.set_indexes.set(string_index);
   3303     }
   3304 
   3305     pub fn get(self: *StringTable, id: u16) ?Token {
   3306         const block_id = (id / 16) + 1;
   3307         const string_index: u8 = @intCast(id & 0xF);
   3308 
   3309         const block = self.blocks.getPtr(block_id) orelse return null;
   3310         const token_index = block.getTokenIndex(string_index) orelse return null;
   3311         return block.strings.items[token_index];
   3312     }
   3313 
   3314     pub fn dump(self: *StringTable) !void {
   3315         var it = self.iterator();
   3316         while (it.next()) |entry| {
   3317             std.debug.print("block: {}\n", .{entry.key_ptr.*});
   3318             entry.value_ptr.dump();
   3319         }
   3320     }
   3321 };
   3322 
   3323 test "StringTable" {
   3324     const S = struct {
   3325         fn makeDummyToken(id: usize) Token {
   3326             return Token{
   3327                 .id = .invalid,
   3328                 .start = id,
   3329                 .end = id,
   3330                 .line_number = id,
   3331             };
   3332         }
   3333     };
   3334     const allocator = std.testing.allocator;
   3335     var string_table = StringTable{};
   3336     defer string_table.deinit(allocator);
   3337 
   3338     var code_page_lookup = CodePageLookup.init(allocator, .windows1252);
   3339     defer code_page_lookup.deinit();
   3340 
   3341     var dummy_node = Node.StringTable{
   3342         .type = S.makeDummyToken(0),
   3343         .common_resource_attributes = &.{},
   3344         .optional_statements = &.{},
   3345         .begin_token = S.makeDummyToken(0),
   3346         .strings = &.{},
   3347         .end_token = S.makeDummyToken(0),
   3348     };
   3349 
   3350     // randomize an array of ids 0-99
   3351     var ids = ids: {
   3352         var buf: [100]u16 = undefined;
   3353         var i: u16 = 0;
   3354         while (i < buf.len) : (i += 1) {
   3355             buf[i] = i;
   3356         }
   3357         break :ids buf;
   3358     };
   3359     var prng = std.Random.DefaultPrng.init(0);
   3360     var random = prng.random();
   3361     random.shuffle(u16, &ids);
   3362 
   3363     // set each one in the randomized order
   3364     for (ids) |id| {
   3365         try string_table.set(allocator, id, S.makeDummyToken(id), &dummy_node.base, "", &code_page_lookup, 0, 0);
   3366     }
   3367 
   3368     // make sure each one exists and is the right value when gotten
   3369     var id: u16 = 0;
   3370     while (id < 100) : (id += 1) {
   3371         const dummy = S.makeDummyToken(id);
   3372         try std.testing.expectError(error.StringAlreadyDefined, string_table.set(allocator, id, dummy, &dummy_node.base, "", &code_page_lookup, 0, 0));
   3373         try std.testing.expectEqual(dummy, string_table.get(id).?);
   3374     }
   3375 
   3376     // make sure non-existent string ids are not found
   3377     try std.testing.expectEqual(@as(?Token, null), string_table.get(100));
   3378 }