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