zig

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

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 };