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