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