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