cvtres.zig (44109B) - Raw
1 const std = @import("std"); 2 const Allocator = std.mem.Allocator; 3 const res = @import("res.zig"); 4 const NameOrOrdinal = res.NameOrOrdinal; 5 const MemoryFlags = res.MemoryFlags; 6 const Language = res.Language; 7 const numPaddingBytesNeeded = @import("compile.zig").Compiler.numPaddingBytesNeeded; 8 9 pub const Resource = struct { 10 type_value: NameOrOrdinal, 11 name_value: NameOrOrdinal, 12 data_version: u32, 13 memory_flags: MemoryFlags, 14 language: Language, 15 version: u32, 16 characteristics: u32, 17 data: []const u8, 18 19 pub fn deinit(self: Resource, allocator: Allocator) void { 20 self.name_value.deinit(allocator); 21 self.type_value.deinit(allocator); 22 allocator.free(self.data); 23 } 24 25 /// Returns true if all fields match the expected value of the resource at the 26 /// start of all .res files that distinguishes the .res file as 32-bit (as 27 /// opposed to 16-bit). 28 pub fn is32BitPreface(self: Resource) bool { 29 if (self.type_value != .ordinal or self.type_value.ordinal != 0) return false; 30 if (self.name_value != .ordinal or self.name_value.ordinal != 0) return false; 31 if (self.data_version != 0) return false; 32 if (@as(u16, @bitCast(self.memory_flags)) != 0) return false; 33 if (@as(u16, @bitCast(self.language)) != 0) return false; 34 if (self.version != 0) return false; 35 if (self.characteristics != 0) return false; 36 if (self.data.len != 0) return false; 37 return true; 38 } 39 40 pub fn isDlgInclude(resource: Resource) bool { 41 return resource.type_value == .ordinal and resource.type_value.ordinal == @intFromEnum(res.RT.DLGINCLUDE); 42 } 43 }; 44 45 pub const ParsedResources = struct { 46 list: std.ArrayListUnmanaged(Resource) = .empty, 47 allocator: Allocator, 48 49 pub fn init(allocator: Allocator) ParsedResources { 50 return .{ .allocator = allocator }; 51 } 52 53 pub fn deinit(self: *ParsedResources) void { 54 for (self.list.items) |*resource| { 55 resource.deinit(self.allocator); 56 } 57 self.list.deinit(self.allocator); 58 } 59 }; 60 61 pub const ParseResOptions = struct { 62 skip_zero_data_resources: bool = true, 63 skip_dlginclude_resources: bool = true, 64 max_size: u64, 65 }; 66 67 /// The returned ParsedResources should be freed by calling its `deinit` function. 68 pub fn parseRes(allocator: Allocator, reader: *std.Io.Reader, options: ParseResOptions) !ParsedResources { 69 var resources = ParsedResources.init(allocator); 70 errdefer resources.deinit(); 71 72 try parseResInto(&resources, reader, options); 73 74 return resources; 75 } 76 77 pub fn parseResInto(resources: *ParsedResources, reader: *std.Io.Reader, options: ParseResOptions) !void { 78 const allocator = resources.allocator; 79 var bytes_remaining: u64 = options.max_size; 80 { 81 const first_resource_and_size = try parseResource(allocator, reader, bytes_remaining); 82 defer first_resource_and_size.resource.deinit(allocator); 83 if (!first_resource_and_size.resource.is32BitPreface()) return error.InvalidPreface; 84 bytes_remaining -= first_resource_and_size.total_size; 85 } 86 87 while (bytes_remaining != 0) { 88 const resource_and_size = try parseResource(allocator, reader, bytes_remaining); 89 if (options.skip_zero_data_resources and resource_and_size.resource.data.len == 0) { 90 resource_and_size.resource.deinit(allocator); 91 } else if (options.skip_dlginclude_resources and resource_and_size.resource.isDlgInclude()) { 92 resource_and_size.resource.deinit(allocator); 93 } else { 94 errdefer resource_and_size.resource.deinit(allocator); 95 try resources.list.append(allocator, resource_and_size.resource); 96 } 97 bytes_remaining -= resource_and_size.total_size; 98 } 99 } 100 101 pub const ResourceAndSize = struct { 102 resource: Resource, 103 total_size: u64, 104 }; 105 106 pub fn parseResource(allocator: Allocator, reader: *std.Io.Reader, max_size: u64) !ResourceAndSize { 107 const data_size = try reader.takeInt(u32, .little); 108 const header_size = try reader.takeInt(u32, .little); 109 const total_size: u64 = @as(u64, header_size) + data_size; 110 if (total_size > max_size) return error.ImpossibleSize; 111 112 const remaining_header_bytes = try reader.take(header_size -| 8); 113 var remaining_header_reader: std.Io.Reader = .fixed(remaining_header_bytes); 114 const type_value = try parseNameOrOrdinal(allocator, &remaining_header_reader); 115 errdefer type_value.deinit(allocator); 116 117 const name_value = try parseNameOrOrdinal(allocator, &remaining_header_reader); 118 errdefer name_value.deinit(allocator); 119 120 const padding_after_name = numPaddingBytesNeeded(@intCast(remaining_header_reader.seek)); 121 try remaining_header_reader.discardAll(padding_after_name); 122 123 std.debug.assert(remaining_header_reader.seek % 4 == 0); 124 const data_version = try remaining_header_reader.takeInt(u32, .little); 125 const memory_flags: MemoryFlags = @bitCast(try remaining_header_reader.takeInt(u16, .little)); 126 const language: Language = @bitCast(try remaining_header_reader.takeInt(u16, .little)); 127 const version = try remaining_header_reader.takeInt(u32, .little); 128 const characteristics = try remaining_header_reader.takeInt(u32, .little); 129 130 if (remaining_header_reader.seek != remaining_header_reader.end) return error.HeaderSizeMismatch; 131 132 const data = try allocator.alloc(u8, data_size); 133 errdefer allocator.free(data); 134 try reader.readSliceAll(data); 135 136 const padding_after_data = numPaddingBytesNeeded(@intCast(data_size)); 137 try reader.discardAll(padding_after_data); 138 139 return .{ 140 .resource = .{ 141 .name_value = name_value, 142 .type_value = type_value, 143 .language = language, 144 .memory_flags = memory_flags, 145 .version = version, 146 .characteristics = characteristics, 147 .data_version = data_version, 148 .data = data, 149 }, 150 .total_size = header_size + data.len + padding_after_data, 151 }; 152 } 153 154 pub fn parseNameOrOrdinal(allocator: Allocator, reader: *std.Io.Reader) !NameOrOrdinal { 155 const first_code_unit = try reader.takeInt(u16, .little); 156 if (first_code_unit == 0xFFFF) { 157 const ordinal_value = try reader.takeInt(u16, .little); 158 return .{ .ordinal = ordinal_value }; 159 } 160 var name_buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 16); 161 errdefer name_buf.deinit(allocator); 162 var code_unit = first_code_unit; 163 while (code_unit != 0) { 164 try name_buf.append(allocator, std.mem.nativeToLittle(u16, code_unit)); 165 code_unit = try reader.takeInt(u16, .little); 166 } 167 return .{ .name = try name_buf.toOwnedSliceSentinel(allocator, 0) }; 168 } 169 170 pub const CoffOptions = struct { 171 target: std.coff.MachineType = .X64, 172 /// If true, zeroes will be written to all timestamp fields 173 reproducible: bool = true, 174 /// If true, the MEM_WRITE flag will not be set in the .rsrc section header 175 read_only: bool = false, 176 /// If non-null, a symbol with this name and storage class EXTERNAL will be added to the symbol table. 177 define_external_symbol: ?[]const u8 = null, 178 /// Re-use data offsets for resources with data that is identical. 179 fold_duplicate_data: bool = false, 180 }; 181 182 pub const Diagnostics = union { 183 none: void, 184 /// Contains the index of the second resource in a duplicate resource pair. 185 duplicate_resource: usize, 186 /// Contains the index of the resource that either has data that's too long or 187 /// caused the total data to overflow. 188 overflow_resource: usize, 189 }; 190 191 pub fn writeCoff(allocator: Allocator, writer: *std.Io.Writer, resources: []const Resource, options: CoffOptions, diagnostics: ?*Diagnostics) !void { 192 var resource_tree = ResourceTree.init(allocator, options); 193 defer resource_tree.deinit(); 194 195 for (resources, 0..) |*resource, i| { 196 resource_tree.put(resource, i) catch |err| { 197 switch (err) { 198 error.DuplicateResource => { 199 if (diagnostics) |d_ptr| d_ptr.* = .{ .duplicate_resource = i }; 200 }, 201 error.ResourceDataTooLong, error.TotalResourceDataTooLong => { 202 if (diagnostics) |d_ptr| d_ptr.* = .{ .overflow_resource = i }; 203 }, 204 else => {}, 205 } 206 return err; 207 }; 208 } 209 210 const lengths = resource_tree.dataLengths(); 211 const byte_size_of_relocation = 10; 212 const relocations_len: u32 = @intCast(byte_size_of_relocation * resources.len); 213 const pointer_to_rsrc01_data = @sizeOf(std.coff.CoffHeader) + (@sizeOf(std.coff.SectionHeader) * 2); 214 const pointer_to_relocations = pointer_to_rsrc01_data + lengths.rsrc01; 215 const pointer_to_rsrc02_data = pointer_to_relocations + relocations_len; 216 const pointer_to_symbol_table = pointer_to_rsrc02_data + lengths.rsrc02; 217 218 const timestamp: i64 = if (options.reproducible) 0 else std.time.timestamp(); 219 const size_of_optional_header = 0; 220 const machine_type: std.coff.MachineType = options.target; 221 const flags = std.coff.CoffHeaderFlags{ 222 .@"32BIT_MACHINE" = 1, 223 }; 224 const number_of_symbols = 5 + @as(u32, @intCast(resources.len)) + @intFromBool(options.define_external_symbol != null); 225 const coff_header = std.coff.CoffHeader{ 226 .machine = machine_type, 227 .number_of_sections = 2, 228 .time_date_stamp = @as(u32, @truncate(@as(u64, @bitCast(timestamp)))), 229 .pointer_to_symbol_table = pointer_to_symbol_table, 230 .number_of_symbols = number_of_symbols, 231 .size_of_optional_header = size_of_optional_header, 232 .flags = flags, 233 }; 234 235 try writer.writeStruct(coff_header, .little); 236 237 const rsrc01_header = std.coff.SectionHeader{ 238 .name = ".rsrc$01".*, 239 .virtual_size = 0, 240 .virtual_address = 0, 241 .size_of_raw_data = lengths.rsrc01, 242 .pointer_to_raw_data = pointer_to_rsrc01_data, 243 .pointer_to_relocations = if (relocations_len != 0) pointer_to_relocations else 0, 244 .pointer_to_linenumbers = 0, 245 .number_of_relocations = @intCast(resources.len), 246 .number_of_linenumbers = 0, 247 .flags = .{ 248 .CNT_INITIALIZED_DATA = 1, 249 .MEM_WRITE = @intFromBool(!options.read_only), 250 .MEM_READ = 1, 251 }, 252 }; 253 try writer.writeStruct(rsrc01_header, .little); 254 255 const rsrc02_header = std.coff.SectionHeader{ 256 .name = ".rsrc$02".*, 257 .virtual_size = 0, 258 .virtual_address = 0, 259 .size_of_raw_data = lengths.rsrc02, 260 .pointer_to_raw_data = pointer_to_rsrc02_data, 261 .pointer_to_relocations = 0, 262 .pointer_to_linenumbers = 0, 263 .number_of_relocations = 0, 264 .number_of_linenumbers = 0, 265 .flags = .{ 266 .CNT_INITIALIZED_DATA = 1, 267 .MEM_WRITE = @intFromBool(!options.read_only), 268 .MEM_READ = 1, 269 }, 270 }; 271 try writer.writeStruct(rsrc02_header, .little); 272 273 // TODO: test surrogate pairs 274 try resource_tree.sort(); 275 276 var string_table = StringTable{}; 277 defer string_table.deinit(allocator); 278 const resource_symbols = try resource_tree.writeCoff( 279 allocator, 280 writer, 281 resources, 282 lengths, 283 &string_table, 284 ); 285 defer allocator.free(resource_symbols); 286 287 try writeSymbol(writer, .{ 288 .name = "@feat.00".*, 289 .value = 0x11, 290 .section_number = .ABSOLUTE, 291 .type = .{ 292 .base_type = .NULL, 293 .complex_type = .NULL, 294 }, 295 .storage_class = .STATIC, 296 .number_of_aux_symbols = 0, 297 }); 298 299 try writeSymbol(writer, .{ 300 .name = ".rsrc$01".*, 301 .value = 0, 302 .section_number = @enumFromInt(1), 303 .type = .{ 304 .base_type = .NULL, 305 .complex_type = .NULL, 306 }, 307 .storage_class = .STATIC, 308 .number_of_aux_symbols = 1, 309 }); 310 try writeSectionDefinition(writer, .{ 311 .length = lengths.rsrc01, 312 .number_of_relocations = @intCast(resources.len), 313 .number_of_linenumbers = 0, 314 .checksum = 0, 315 .number = 0, 316 .selection = .NONE, 317 .unused = .{0} ** 3, 318 }); 319 320 try writeSymbol(writer, .{ 321 .name = ".rsrc$02".*, 322 .value = 0, 323 .section_number = @enumFromInt(2), 324 .type = .{ 325 .base_type = .NULL, 326 .complex_type = .NULL, 327 }, 328 .storage_class = .STATIC, 329 .number_of_aux_symbols = 1, 330 }); 331 try writeSectionDefinition(writer, .{ 332 .length = lengths.rsrc02, 333 .number_of_relocations = 0, 334 .number_of_linenumbers = 0, 335 .checksum = 0, 336 .number = 0, 337 .selection = .NONE, 338 .unused = .{0} ** 3, 339 }); 340 341 for (resource_symbols) |resource_symbol| { 342 try writeSymbol(writer, resource_symbol); 343 } 344 345 if (options.define_external_symbol) |external_symbol_name| { 346 const name_bytes: [8]u8 = name_bytes: { 347 if (external_symbol_name.len > 8) { 348 const string_table_offset: u32 = try string_table.put(allocator, external_symbol_name); 349 var bytes = [_]u8{0} ** 8; 350 std.mem.writeInt(u32, bytes[4..8], string_table_offset, .little); 351 break :name_bytes bytes; 352 } else { 353 var symbol_shortname = [_]u8{0} ** 8; 354 @memcpy(symbol_shortname[0..external_symbol_name.len], external_symbol_name); 355 break :name_bytes symbol_shortname; 356 } 357 }; 358 359 try writeSymbol(writer, .{ 360 .name = name_bytes, 361 .value = 0, 362 .section_number = .ABSOLUTE, 363 .type = .{ 364 .base_type = .NULL, 365 .complex_type = .NULL, 366 }, 367 .storage_class = .EXTERNAL, 368 .number_of_aux_symbols = 0, 369 }); 370 } 371 372 try writer.writeInt(u32, string_table.totalByteLength(), .little); 373 try writer.writeAll(string_table.bytes.items); 374 } 375 376 fn writeSymbol(writer: anytype, symbol: std.coff.Symbol) !void { 377 try writer.writeAll(&symbol.name); 378 try writer.writeInt(u32, symbol.value, .little); 379 try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little); 380 try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little); 381 try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little); 382 try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little); 383 try writer.writeInt(u8, symbol.number_of_aux_symbols, .little); 384 } 385 386 fn writeSectionDefinition(writer: anytype, def: std.coff.SectionDefinition) !void { 387 try writer.writeInt(u32, def.length, .little); 388 try writer.writeInt(u16, def.number_of_relocations, .little); 389 try writer.writeInt(u16, def.number_of_linenumbers, .little); 390 try writer.writeInt(u32, def.checksum, .little); 391 try writer.writeInt(u16, def.number, .little); 392 try writer.writeInt(u8, @intFromEnum(def.selection), .little); 393 try writer.writeAll(&def.unused); 394 } 395 396 pub const ResourceDirectoryTable = extern struct { 397 characteristics: u32, 398 timestamp: u32, 399 major_version: u16, 400 minor_version: u16, 401 number_of_name_entries: u16, 402 number_of_id_entries: u16, 403 }; 404 405 pub const ResourceDirectoryEntry = extern struct { 406 entry: packed union { 407 name_offset: packed struct(u32) { 408 address: u31, 409 /// This is undocumented in the PE/COFF spec, but the high bit 410 /// is set by cvtres.exe for string addresses 411 to_string: bool = true, 412 }, 413 integer_id: u32, 414 }, 415 offset: packed struct(u32) { 416 address: u31, 417 to_subdirectory: bool, 418 }, 419 420 pub fn writeCoff(self: ResourceDirectoryEntry, writer: anytype) !void { 421 try writer.writeInt(u32, @bitCast(self.entry), .little); 422 try writer.writeInt(u32, @bitCast(self.offset), .little); 423 } 424 }; 425 426 pub const ResourceDataEntry = extern struct { 427 data_rva: u32, 428 size: u32, 429 codepage: u32, 430 reserved: u32 = 0, 431 }; 432 433 /// type -> name -> language 434 const ResourceTree = struct { 435 type_to_name_map: std.ArrayHashMapUnmanaged(NameOrOrdinal, NameToLanguageMap, NameOrOrdinalHashContext, true), 436 rsrc_string_table: std.ArrayHashMapUnmanaged(NameOrOrdinal, void, NameOrOrdinalHashContext, true), 437 deduplicated_data: std.StringArrayHashMapUnmanaged(u32), 438 data_offsets: std.ArrayListUnmanaged(u32), 439 rsrc02_len: u32, 440 coff_options: CoffOptions, 441 allocator: Allocator, 442 443 const RelocatableResource = struct { 444 resource: *const Resource, 445 original_index: usize, 446 }; 447 const LanguageToResourceMap = std.AutoArrayHashMapUnmanaged(Language, RelocatableResource); 448 const NameToLanguageMap = std.ArrayHashMapUnmanaged(NameOrOrdinal, LanguageToResourceMap, NameOrOrdinalHashContext, true); 449 450 const NameOrOrdinalHashContext = struct { 451 pub fn hash(self: @This(), v: NameOrOrdinal) u32 { 452 _ = self; 453 var hasher = std.hash.Wyhash.init(0); 454 const tag = std.meta.activeTag(v); 455 hasher.update(std.mem.asBytes(&tag)); 456 switch (v) { 457 .name => |name| { 458 hasher.update(std.mem.sliceAsBytes(name)); 459 }, 460 .ordinal => |*ordinal| { 461 hasher.update(std.mem.asBytes(ordinal)); 462 }, 463 } 464 return @truncate(hasher.final()); 465 } 466 pub fn eql(self: @This(), a: NameOrOrdinal, b: NameOrOrdinal, b_index: usize) bool { 467 _ = self; 468 _ = b_index; 469 const tag_a = std.meta.activeTag(a); 470 const tag_b = std.meta.activeTag(b); 471 if (tag_a != tag_b) return false; 472 473 return switch (a) { 474 .name => std.mem.eql(u16, a.name, b.name), 475 .ordinal => a.ordinal == b.ordinal, 476 }; 477 } 478 }; 479 480 pub fn init(allocator: Allocator, coff_options: CoffOptions) ResourceTree { 481 return .{ 482 .type_to_name_map = .empty, 483 .rsrc_string_table = .empty, 484 .deduplicated_data = .empty, 485 .data_offsets = .empty, 486 .rsrc02_len = 0, 487 .coff_options = coff_options, 488 .allocator = allocator, 489 }; 490 } 491 492 pub fn deinit(self: *ResourceTree) void { 493 for (self.type_to_name_map.values()) |*name_to_lang_map| { 494 for (name_to_lang_map.values()) |*lang_to_resources_map| { 495 lang_to_resources_map.deinit(self.allocator); 496 } 497 name_to_lang_map.deinit(self.allocator); 498 } 499 self.type_to_name_map.deinit(self.allocator); 500 self.rsrc_string_table.deinit(self.allocator); 501 self.deduplicated_data.deinit(self.allocator); 502 self.data_offsets.deinit(self.allocator); 503 } 504 505 pub fn put(self: *ResourceTree, resource: *const Resource, original_index: usize) !void { 506 const name_to_lang_map = blk: { 507 const gop_result = try self.type_to_name_map.getOrPut(self.allocator, resource.type_value); 508 if (!gop_result.found_existing) { 509 gop_result.value_ptr.* = .empty; 510 } 511 break :blk gop_result.value_ptr; 512 }; 513 const lang_to_resources_map = blk: { 514 const gop_result = try name_to_lang_map.getOrPut(self.allocator, resource.name_value); 515 if (!gop_result.found_existing) { 516 gop_result.value_ptr.* = .empty; 517 } 518 break :blk gop_result.value_ptr; 519 }; 520 { 521 const gop_result = try lang_to_resources_map.getOrPut(self.allocator, resource.language); 522 if (gop_result.found_existing) return error.DuplicateResource; 523 gop_result.value_ptr.* = .{ 524 .original_index = original_index, 525 .resource = resource, 526 }; 527 } 528 529 // Resize the data_offsets list to accommodate the index, but only if necessary 530 try self.data_offsets.resize(self.allocator, @max(self.data_offsets.items.len, original_index + 1)); 531 if (self.coff_options.fold_duplicate_data) { 532 const gop_result = try self.deduplicated_data.getOrPut(self.allocator, resource.data); 533 if (!gop_result.found_existing) { 534 gop_result.value_ptr.* = self.rsrc02_len; 535 try self.incrementRsrc02Len(resource); 536 } 537 self.data_offsets.items[original_index] = gop_result.value_ptr.*; 538 } else { 539 self.data_offsets.items[original_index] = self.rsrc02_len; 540 try self.incrementRsrc02Len(resource); 541 } 542 543 if (resource.type_value == .name and !self.rsrc_string_table.contains(resource.type_value)) { 544 try self.rsrc_string_table.putNoClobber(self.allocator, resource.type_value, {}); 545 } 546 if (resource.name_value == .name and !self.rsrc_string_table.contains(resource.name_value)) { 547 try self.rsrc_string_table.putNoClobber(self.allocator, resource.name_value, {}); 548 } 549 } 550 551 fn incrementRsrc02Len(self: *ResourceTree, resource: *const Resource) !void { 552 // Note: This @intCast is only safe if we assume that the resource was parsed from a .res file, 553 // since the maximum data length for a resource in the .res file format is maxInt(u32). 554 // TODO: Either codify this properly or use std.math.cast and return an error. 555 const data_len: u32 = @intCast(resource.data.len); 556 const data_len_including_padding: u32 = std.math.cast(u32, std.mem.alignForward(u33, data_len, 8)) orelse { 557 return error.ResourceDataTooLong; 558 }; 559 // TODO: Verify that this corresponds to an actual PE/COFF limitation for resource data 560 // in the final linked binary. The limit may turn out to be shorter than u32 max if both 561 // the tree data and the resource data lengths together need to fit within a u32, 562 // or it may be longer in which case we would want to add more .rsrc$NN sections 563 // to the object file for the data that overflows .rsrc$02. 564 self.rsrc02_len = std.math.add(u32, self.rsrc02_len, data_len_including_padding) catch { 565 return error.TotalResourceDataTooLong; 566 }; 567 } 568 569 const Lengths = struct { 570 level1: u32, 571 level2: u32, 572 level3: u32, 573 data_entries: u32, 574 strings: u32, 575 padding: u32, 576 577 rsrc01: u32, 578 rsrc02: u32, 579 580 fn stringsStart(self: Lengths) u32 { 581 return self.rsrc01 - self.strings - self.padding; 582 } 583 }; 584 585 pub fn dataLengths(self: *const ResourceTree) Lengths { 586 var lengths: Lengths = .{ 587 .level1 = 0, 588 .level2 = 0, 589 .level3 = 0, 590 .data_entries = 0, 591 .strings = 0, 592 .padding = 0, 593 .rsrc01 = undefined, 594 .rsrc02 = self.rsrc02_len, 595 }; 596 lengths.level1 += @sizeOf(ResourceDirectoryTable); 597 for (self.type_to_name_map.values()) |name_to_lang_map| { 598 lengths.level1 += @sizeOf(ResourceDirectoryEntry); 599 lengths.level2 += @sizeOf(ResourceDirectoryTable); 600 for (name_to_lang_map.values()) |lang_to_resources_map| { 601 lengths.level2 += @sizeOf(ResourceDirectoryEntry); 602 lengths.level3 += @sizeOf(ResourceDirectoryTable); 603 for (lang_to_resources_map.values()) |_| { 604 lengths.level3 += @sizeOf(ResourceDirectoryEntry); 605 lengths.data_entries += @sizeOf(ResourceDataEntry); 606 } 607 } 608 } 609 for (self.rsrc_string_table.keys()) |v| { 610 lengths.strings += @sizeOf(u16); // string length 611 lengths.strings += @intCast(v.name.len * @sizeOf(u16)); 612 } 613 lengths.rsrc01 = lengths.level1 + lengths.level2 + lengths.level3 + lengths.data_entries + lengths.strings; 614 lengths.padding = @intCast((4 -% lengths.rsrc01) % 4); 615 lengths.rsrc01 += lengths.padding; 616 return lengths; 617 } 618 619 pub fn sort(self: *ResourceTree) !void { 620 const NameOrOrdinalSortContext = struct { 621 keys: []NameOrOrdinal, 622 623 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { 624 const a = ctx.keys[a_index]; 625 const b = ctx.keys[b_index]; 626 if (std.meta.activeTag(a) != std.meta.activeTag(b)) { 627 return if (a == .name) true else false; 628 } 629 switch (a) { 630 .name => { 631 const n = @min(a.name.len, b.name.len); 632 for (a.name[0..n], b.name[0..n]) |a_c, b_c| { 633 switch (std.math.order(std.mem.littleToNative(u16, a_c), std.mem.littleToNative(u16, b_c))) { 634 .eq => continue, 635 .lt => return true, 636 .gt => return false, 637 } 638 } 639 return a.name.len < b.name.len; 640 }, 641 .ordinal => { 642 return a.ordinal < b.ordinal; 643 }, 644 } 645 } 646 }; 647 self.type_to_name_map.sortUnstable(NameOrOrdinalSortContext{ .keys = self.type_to_name_map.keys() }); 648 for (self.type_to_name_map.values()) |*name_to_lang_map| { 649 name_to_lang_map.sortUnstable(NameOrOrdinalSortContext{ .keys = name_to_lang_map.keys() }); 650 } 651 const LangSortContext = struct { 652 keys: []Language, 653 654 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool { 655 return @as(u16, @bitCast(ctx.keys[a_index])) < @as(u16, @bitCast(ctx.keys[b_index])); 656 } 657 }; 658 for (self.type_to_name_map.values()) |*name_to_lang_map| { 659 for (name_to_lang_map.values()) |*lang_to_resource_map| { 660 lang_to_resource_map.sortUnstable(LangSortContext{ .keys = lang_to_resource_map.keys() }); 661 } 662 } 663 } 664 665 pub fn writeCoff( 666 self: *const ResourceTree, 667 allocator: Allocator, 668 w: *std.Io.Writer, 669 resources_in_data_order: []const Resource, 670 lengths: Lengths, 671 coff_string_table: *StringTable, 672 ) ![]const std.coff.Symbol { 673 if (self.type_to_name_map.count() == 0) { 674 try w.splatByteAll(0, 16); 675 return &.{}; 676 } 677 678 var level2_list: std.ArrayListUnmanaged(*const NameToLanguageMap) = .empty; 679 defer level2_list.deinit(allocator); 680 681 var level3_list: std.ArrayListUnmanaged(*const LanguageToResourceMap) = .empty; 682 defer level3_list.deinit(allocator); 683 684 var resources_list: std.ArrayListUnmanaged(*const RelocatableResource) = .empty; 685 defer resources_list.deinit(allocator); 686 687 var relocations = Relocations.init(allocator); 688 defer relocations.deinit(); 689 690 var string_offsets = try allocator.alloc(u31, self.rsrc_string_table.count()); 691 const strings_start = lengths.stringsStart(); 692 defer allocator.free(string_offsets); 693 { 694 var string_address: u31 = @intCast(strings_start); 695 for (self.rsrc_string_table.keys(), 0..) |v, i| { 696 string_offsets[i] = string_address; 697 string_address += @sizeOf(u16) + @as(u31, @intCast(v.name.len * @sizeOf(u16))); 698 } 699 } 700 701 const level2_start = lengths.level1; 702 var level2_address = level2_start; 703 { 704 const counts = entryTypeCounts(self.type_to_name_map.keys()); 705 const table = ResourceDirectoryTable{ 706 .characteristics = 0, 707 .timestamp = 0, 708 .major_version = 0, 709 .minor_version = 0, 710 .number_of_id_entries = counts.ids, 711 .number_of_name_entries = counts.names, 712 }; 713 try w.writeStruct(table, .little); 714 715 var it = self.type_to_name_map.iterator(); 716 while (it.next()) |entry| { 717 const type_value = entry.key_ptr; 718 const dir_entry = ResourceDirectoryEntry{ 719 .entry = switch (type_value.*) { 720 .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(type_value.*).?] } }, 721 .ordinal => .{ .integer_id = type_value.ordinal }, 722 }, 723 .offset = .{ 724 .address = @intCast(level2_address), 725 .to_subdirectory = true, 726 }, 727 }; 728 try dir_entry.writeCoff(w); 729 level2_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry))); 730 731 const name_to_lang_map = entry.value_ptr; 732 try level2_list.append(allocator, name_to_lang_map); 733 } 734 } 735 736 const level3_start = level2_start + lengths.level2; 737 var level3_address = level3_start; 738 for (level2_list.items) |name_to_lang_map| { 739 const counts = entryTypeCounts(name_to_lang_map.keys()); 740 const table = ResourceDirectoryTable{ 741 .characteristics = 0, 742 .timestamp = 0, 743 .major_version = 0, 744 .minor_version = 0, 745 .number_of_id_entries = counts.ids, 746 .number_of_name_entries = counts.names, 747 }; 748 try w.writeStruct(table, .little); 749 750 var it = name_to_lang_map.iterator(); 751 while (it.next()) |entry| { 752 const name_value = entry.key_ptr; 753 const dir_entry = ResourceDirectoryEntry{ 754 .entry = switch (name_value.*) { 755 .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(name_value.*).?] } }, 756 .ordinal => .{ .integer_id = name_value.ordinal }, 757 }, 758 .offset = .{ 759 .address = @intCast(level3_address), 760 .to_subdirectory = true, 761 }, 762 }; 763 try dir_entry.writeCoff(w); 764 level3_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry))); 765 766 const lang_to_resources_map = entry.value_ptr; 767 try level3_list.append(allocator, lang_to_resources_map); 768 } 769 } 770 771 var reloc_addresses = try allocator.alloc(u32, resources_in_data_order.len); 772 defer allocator.free(reloc_addresses); 773 774 const data_entries_start = level3_start + lengths.level3; 775 var data_entry_address = data_entries_start; 776 for (level3_list.items) |lang_to_resources_map| { 777 const counts = EntryTypeCounts{ 778 .names = 0, 779 .ids = @intCast(lang_to_resources_map.count()), 780 }; 781 const table = ResourceDirectoryTable{ 782 .characteristics = 0, 783 .timestamp = 0, 784 .major_version = 0, 785 .minor_version = 0, 786 .number_of_id_entries = counts.ids, 787 .number_of_name_entries = counts.names, 788 }; 789 try w.writeStruct(table, .little); 790 791 var it = lang_to_resources_map.iterator(); 792 while (it.next()) |entry| { 793 const lang = entry.key_ptr.*; 794 const dir_entry = ResourceDirectoryEntry{ 795 .entry = .{ .integer_id = lang.asInt() }, 796 .offset = .{ 797 .address = @intCast(data_entry_address), 798 .to_subdirectory = false, 799 }, 800 }; 801 802 const reloc_resource = entry.value_ptr; 803 reloc_addresses[reloc_resource.original_index] = @intCast(data_entry_address); 804 805 try dir_entry.writeCoff(w); 806 data_entry_address += @sizeOf(ResourceDataEntry); 807 808 try resources_list.append(allocator, reloc_resource); 809 } 810 } 811 812 for (resources_list.items, 0..) |reloc_resource, i| { 813 // TODO: This logic works but is convoluted, would be good to clean this up 814 const orig_resource = &resources_in_data_order[reloc_resource.original_index]; 815 const address: u32 = reloc_addresses[i]; 816 try relocations.add(address, self.data_offsets.items[i]); 817 const data_entry = ResourceDataEntry{ 818 .data_rva = 0, // relocation 819 .size = @intCast(orig_resource.data.len), 820 .codepage = 0, 821 }; 822 try w.writeStruct(data_entry, .little); 823 } 824 825 for (self.rsrc_string_table.keys()) |v| { 826 const str = v.name; 827 try w.writeInt(u16, @intCast(str.len), .little); 828 try w.writeAll(std.mem.sliceAsBytes(str)); 829 } 830 831 try w.splatByteAll(0, lengths.padding); 832 833 for (relocations.list.items) |relocation| { 834 try writeRelocation(w, std.coff.Relocation{ 835 .virtual_address = relocation.relocation_address, 836 .symbol_table_index = relocation.symbol_index, 837 .type = supported_targets.rvaRelocationTypeIndicator(self.coff_options.target).?, 838 }); 839 } 840 841 if (self.coff_options.fold_duplicate_data) { 842 for (self.deduplicated_data.keys()) |data| { 843 const padding_bytes: u4 = @intCast((8 -% data.len) % 8); 844 try w.writeAll(data); 845 try w.splatByteAll(0, padding_bytes); 846 } 847 } else { 848 for (resources_in_data_order) |resource| { 849 const padding_bytes: u4 = @intCast((8 -% resource.data.len) % 8); 850 try w.writeAll(resource.data); 851 try w.splatByteAll(0, padding_bytes); 852 } 853 } 854 855 var symbols = try allocator.alloc(std.coff.Symbol, resources_list.items.len); 856 errdefer allocator.free(symbols); 857 858 for (relocations.list.items, 0..) |relocation, i| { 859 // cvtres.exe writes the symbol names as $R<data offset as hexadecimal>. 860 // 861 // When the data offset would exceed 6 hex digits in cvtres.exe, it 862 // truncates the value down to 6 hex digits. This is bad behavior, since 863 // e.g. an initial resource with exactly 16 MiB of data and the 864 // resource following it would both have the symbol name $R000000. 865 // 866 // Instead, if the offset would exceed 6 hexadecimal digits, 867 // we put the longer name in the string table. 868 // 869 // Another option would be to adopt llvm-cvtres' behavior 870 // of $R000001, $R000002, etc. rather than using data offset values. 871 var name_buf: [8]u8 = undefined; 872 if (relocation.data_offset > std.math.maxInt(u24)) { 873 const name_slice = try std.fmt.allocPrint(allocator, "$R{X}", .{relocation.data_offset}); 874 defer allocator.free(name_slice); 875 const string_table_offset: u32 = try coff_string_table.put(allocator, name_slice); 876 std.mem.writeInt(u32, name_buf[0..4], 0, .little); 877 std.mem.writeInt(u32, name_buf[4..8], string_table_offset, .little); 878 } else { 879 const name_slice = std.fmt.bufPrint(&name_buf, "$R{X:0>6}", .{relocation.data_offset}) catch unreachable; 880 std.debug.assert(name_slice.len == 8); 881 } 882 883 symbols[i] = .{ 884 .name = name_buf, 885 .value = relocation.data_offset, 886 .section_number = @enumFromInt(2), 887 .type = .{ 888 .base_type = .NULL, 889 .complex_type = .NULL, 890 }, 891 .storage_class = .STATIC, 892 .number_of_aux_symbols = 0, 893 }; 894 } 895 896 return symbols; 897 } 898 899 fn writeRelocation(writer: anytype, relocation: std.coff.Relocation) !void { 900 try writer.writeInt(u32, relocation.virtual_address, .little); 901 try writer.writeInt(u32, relocation.symbol_table_index, .little); 902 try writer.writeInt(u16, relocation.type, .little); 903 } 904 905 const EntryTypeCounts = struct { 906 names: u16, 907 ids: u16, 908 }; 909 910 fn entryTypeCounts(s: []const NameOrOrdinal) EntryTypeCounts { 911 var names: u16 = 0; 912 var ordinals: u16 = 0; 913 for (s) |v| { 914 switch (v) { 915 .name => names += 1, 916 .ordinal => ordinals += 1, 917 } 918 } 919 return .{ .names = names, .ids = ordinals }; 920 } 921 }; 922 923 const Relocation = struct { 924 symbol_index: u32, 925 data_offset: u32, 926 relocation_address: u32, 927 }; 928 929 const Relocations = struct { 930 allocator: Allocator, 931 list: std.ArrayListUnmanaged(Relocation) = .empty, 932 cur_symbol_index: u32 = 5, 933 934 pub fn init(allocator: Allocator) Relocations { 935 return .{ .allocator = allocator }; 936 } 937 938 pub fn deinit(self: *Relocations) void { 939 self.list.deinit(self.allocator); 940 } 941 942 pub fn add(self: *Relocations, relocation_address: u32, data_offset: u32) !void { 943 try self.list.append(self.allocator, .{ 944 .symbol_index = self.cur_symbol_index, 945 .data_offset = data_offset, 946 .relocation_address = relocation_address, 947 }); 948 self.cur_symbol_index += 1; 949 } 950 }; 951 952 /// Does not do deduplication (only because there's no chance of duplicate strings in this 953 /// instance). 954 const StringTable = struct { 955 bytes: std.ArrayListUnmanaged(u8) = .empty, 956 957 pub fn deinit(self: *StringTable, allocator: Allocator) void { 958 self.bytes.deinit(allocator); 959 } 960 961 /// Returns the byte offset of the string in the string table 962 pub fn put(self: *StringTable, allocator: Allocator, string: []const u8) !u32 { 963 const null_terminated_len = string.len + 1; 964 const start_offset = self.totalByteLength(); 965 if (start_offset + null_terminated_len > std.math.maxInt(u32)) { 966 return error.StringTableOverflow; 967 } 968 try self.bytes.ensureUnusedCapacity(allocator, null_terminated_len); 969 self.bytes.appendSliceAssumeCapacity(string); 970 self.bytes.appendAssumeCapacity(0); 971 return start_offset; 972 } 973 974 /// Returns the total byte count of the string table, including the byte count of the size field 975 pub fn totalByteLength(self: StringTable) u32 { 976 return @intCast(4 + self.bytes.items.len); 977 } 978 }; 979 980 pub const supported_targets = struct { 981 /// Enum containing a mixture of names that come from: 982 /// - Machine Types constants in the PE format spec: 983 /// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types 984 /// - cvtres.exe /machine options 985 /// - Zig/LLVM arch names 986 /// All field names are lowercase regardless of their casing used in the above origins. 987 pub const Arch = enum { 988 // cvtres.exe /machine names 989 x64, 990 x86, 991 /// Note: Following cvtres.exe's lead, this corresponds to ARMNT, not ARM 992 arm, 993 arm64, 994 arm64ec, 995 arm64x, 996 ia64, 997 ebc, 998 999 // PE/COFF MACHINE constant names not covered above 1000 amd64, 1001 i386, 1002 armnt, 1003 1004 // Zig/LLVM names not already covered above 1005 x86_64, 1006 aarch64, 1007 1008 pub fn toCoffMachineType(arch: Arch) std.coff.MachineType { 1009 return switch (arch) { 1010 .x64, .amd64, .x86_64 => .X64, 1011 .x86, .i386 => .I386, 1012 .arm, .armnt => .ARMNT, 1013 .arm64, .aarch64 => .ARM64, 1014 .arm64ec => .ARM64EC, 1015 .arm64x => .ARM64X, 1016 .ia64 => .IA64, 1017 .ebc => .EBC, 1018 }; 1019 } 1020 1021 pub fn description(arch: Arch) []const u8 { 1022 return switch (arch) { 1023 .x64, .amd64, .x86_64 => "64-bit X86", 1024 .x86, .i386 => "32-bit X86", 1025 .arm, .armnt => "ARM Thumb-2 little endian", 1026 .arm64, .aarch64 => "ARM64/AArch64 little endian", 1027 .arm64ec => "ARM64 \"Emulation Compatible\"", 1028 .arm64x => "ARM64 and ARM64EC together", 1029 .ia64 => "64-bit Intel Itanium", 1030 .ebc => "EFI Byte Code", 1031 }; 1032 } 1033 1034 pub const ordered_for_display: []const Arch = &.{ 1035 .x64, 1036 .x86_64, 1037 .amd64, 1038 .x86, 1039 .i386, 1040 .arm64, 1041 .aarch64, 1042 .arm, 1043 .armnt, 1044 .arm64ec, 1045 .arm64x, 1046 .ia64, 1047 .ebc, 1048 }; 1049 comptime { 1050 for (@typeInfo(Arch).@"enum".fields) |enum_field| { 1051 _ = std.mem.indexOfScalar(Arch, ordered_for_display, @enumFromInt(enum_field.value)) orelse { 1052 @compileError(std.fmt.comptimePrint("'{s}' missing from ordered_for_display", .{enum_field.name})); 1053 }; 1054 } 1055 } 1056 1057 pub const longest_name = blk: { 1058 var len = 0; 1059 for (@typeInfo(Arch).@"enum".fields) |field| { 1060 if (field.name.len > len) len = field.name.len; 1061 } 1062 break :blk len; 1063 }; 1064 1065 pub fn fromStringIgnoreCase(str: []const u8) ?Arch { 1066 if (str.len > longest_name) return null; 1067 var lower_buf: [longest_name]u8 = undefined; 1068 const lower = std.ascii.lowerString(&lower_buf, str); 1069 return std.meta.stringToEnum(Arch, lower); 1070 } 1071 1072 test fromStringIgnoreCase { 1073 try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("x64").?); 1074 try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("X64").?); 1075 try std.testing.expectEqual(.aarch64, Arch.fromStringIgnoreCase("Aarch64").?); 1076 try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("armzzz")); 1077 try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("long string that is longer than any field")); 1078 } 1079 }; 1080 1081 // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators 1082 pub fn rvaRelocationTypeIndicator(target: std.coff.MachineType) ?u16 { 1083 return switch (target) { 1084 .X64 => 0x3, // IMAGE_REL_AMD64_ADDR32NB 1085 .I386 => 0x7, // IMAGE_REL_I386_DIR32NB 1086 .ARMNT => 0x2, // IMAGE_REL_ARM_ADDR32NB 1087 .ARM64, .ARM64EC, .ARM64X => 0x2, // IMAGE_REL_ARM64_ADDR32NB 1088 .IA64 => 0x10, // IMAGE_REL_IA64_DIR32NB 1089 .EBC => 0x1, // This is what cvtres.exe writes for this target, unsure where it comes from 1090 else => null, 1091 }; 1092 } 1093 1094 pub fn isSupported(target: std.coff.MachineType) bool { 1095 return rvaRelocationTypeIndicator(target) != null; 1096 } 1097 1098 comptime { 1099 // Enforce two things: 1100 // 1. Arch enum field names are all lowercase (necessary for how fromStringIgnoreCase is implemented) 1101 // 2. All enum fields in Arch have an associated RVA relocation type when converted to a coff.MachineType 1102 for (@typeInfo(Arch).@"enum".fields) |enum_field| { 1103 const all_lower = all_lower: for (enum_field.name) |c| { 1104 if (std.ascii.isUpper(c)) break :all_lower false; 1105 } else break :all_lower true; 1106 if (!all_lower) @compileError(std.fmt.comptimePrint("Arch field is not all lowercase: {s}", .{enum_field.name})); 1107 const coff_machine = @field(Arch, enum_field.name).toCoffMachineType(); 1108 _ = rvaRelocationTypeIndicator(coff_machine) orelse { 1109 @compileError(std.fmt.comptimePrint("No RVA relocation for Arch: {s}", .{enum_field.name})); 1110 }; 1111 } 1112 } 1113 };