Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

ResourceTree

type -> name -> language

cvtres.ResourceTree
const ResourceTree = struct

File

Code

const ResourceTree = struct {
    type_to_name_map: std.array_hash_map.Custom(NameOrOrdinal, NameToLanguageMap, NameOrOrdinalHashContext, true),
    rsrc_string_table: std.array_hash_map.Custom(NameOrOrdinal, void, NameOrOrdinalHashContext, true),
    deduplicated_data: std.array_hash_map.String(u32),
    data_offsets: std.ArrayList(u32),
    rsrc02_len: u32,
    coff_options: CoffOptions,
    allocator: Allocator,

    const RelocatableResource = struct {
        resource: *const Resource,
        original_index: usize,
    };
    const LanguageToResourceMap = std.array_hash_map.Auto(Language, RelocatableResource);
    const NameToLanguageMap = std.array_hash_map.Custom(NameOrOrdinal, LanguageToResourceMap, NameOrOrdinalHashContext, true);

    const NameOrOrdinalHashContext = struct {
        pub fn hash(self: @This(), v: NameOrOrdinal) u32 {
            _ = self;
            var hasher = std.hash.Wyhash.init(0);
            const tag = std.meta.activeTag(v);
            hasher.update(std.mem.asBytes(&tag));
            switch (v) {
                .name => |name| {
                    hasher.update(std.mem.sliceAsBytes(name));
                },
                .ordinal => |*ordinal| {
                    hasher.update(std.mem.asBytes(ordinal));
                },
            }
            return @truncate(hasher.final());
        }
        pub fn eql(self: @This(), a: NameOrOrdinal, b: NameOrOrdinal, b_index: usize) bool {
            _ = self;
            _ = b_index;
            const tag_a = std.meta.activeTag(a);
            const tag_b = std.meta.activeTag(b);
            if (tag_a != tag_b) return false;

            return switch (a) {
                .name => std.mem.eql(u16, a.name, b.name),
                .ordinal => a.ordinal == b.ordinal,
            };
        }
    };

    pub fn init(allocator: Allocator, coff_options: CoffOptions) ResourceTree {
        return .{
            .type_to_name_map = .empty,
            .rsrc_string_table = .empty,
            .deduplicated_data = .empty,
            .data_offsets = .empty,
            .rsrc02_len = 0,
            .coff_options = coff_options,
            .allocator = allocator,
        };
    }

    pub fn deinit(self: *ResourceTree) void {
        for (self.type_to_name_map.values()) |*name_to_lang_map| {
            for (name_to_lang_map.values()) |*lang_to_resources_map| {
                lang_to_resources_map.deinit(self.allocator);
            }
            name_to_lang_map.deinit(self.allocator);
        }
        self.type_to_name_map.deinit(self.allocator);
        self.rsrc_string_table.deinit(self.allocator);
        self.deduplicated_data.deinit(self.allocator);
        self.data_offsets.deinit(self.allocator);
    }

    pub fn put(self: *ResourceTree, resource: *const Resource, original_index: usize) !void {
        const name_to_lang_map = blk: {
            const gop_result = try self.type_to_name_map.getOrPut(self.allocator, resource.type_value);
            if (!gop_result.found_existing) {
                gop_result.value_ptr.* = .empty;
            }
            break :blk gop_result.value_ptr;
        };
        const lang_to_resources_map = blk: {
            const gop_result = try name_to_lang_map.getOrPut(self.allocator, resource.name_value);
            if (!gop_result.found_existing) {
                gop_result.value_ptr.* = .empty;
            }
            break :blk gop_result.value_ptr;
        };
        {
            const gop_result = try lang_to_resources_map.getOrPut(self.allocator, resource.language);
            if (gop_result.found_existing) return error.DuplicateResource;
            gop_result.value_ptr.* = .{
                .original_index = original_index,
                .resource = resource,
            };
        }

        // Resize the data_offsets list to accommodate the index, but only if necessary
        try self.data_offsets.resize(self.allocator, @max(self.data_offsets.items.len, original_index + 1));
        if (self.coff_options.fold_duplicate_data) {
            const gop_result = try self.deduplicated_data.getOrPut(self.allocator, resource.data);
            if (!gop_result.found_existing) {
                gop_result.value_ptr.* = self.rsrc02_len;
                try self.incrementRsrc02Len(resource);
            }
            self.data_offsets.items[original_index] = gop_result.value_ptr.*;
        } else {
            self.data_offsets.items[original_index] = self.rsrc02_len;
            try self.incrementRsrc02Len(resource);
        }

        if (resource.type_value == .name and !self.rsrc_string_table.contains(resource.type_value)) {
            try self.rsrc_string_table.putNoClobber(self.allocator, resource.type_value, {});
        }
        if (resource.name_value == .name and !self.rsrc_string_table.contains(resource.name_value)) {
            try self.rsrc_string_table.putNoClobber(self.allocator, resource.name_value, {});
        }
    }

    fn incrementRsrc02Len(self: *ResourceTree, resource: *const Resource) !void {
        // Note: This @intCast is only safe if we assume that the resource was parsed from a .res file,
        // since the maximum data length for a resource in the .res file format is maxInt(u32).
        // TODO: Either codify this properly or use std.math.cast and return an error.
        const data_len: u32 = @intCast(resource.data.len);
        const data_len_including_padding: u32 = std.math.cast(u32, std.mem.alignForward(u33, data_len, 8)) orelse {
            return error.ResourceDataTooLong;
        };
        // TODO: Verify that this corresponds to an actual PE/COFF limitation for resource data
        //       in the final linked binary. The limit may turn out to be shorter than u32 max if both
        //       the tree data and the resource data lengths together need to fit within a u32,
        //       or it may be longer in which case we would want to add more .rsrc$NN sections
        //       to the object file for the data that overflows .rsrc$02.
        self.rsrc02_len = std.math.add(u32, self.rsrc02_len, data_len_including_padding) catch {
            return error.TotalResourceDataTooLong;
        };
    }

    const Lengths = struct {
        level1: u32,
        level2: u32,
        level3: u32,
        data_entries: u32,
        strings: u32,
        padding: u32,

        rsrc01: u32,
        rsrc02: u32,

        fn stringsStart(self: Lengths) u32 {
            return self.rsrc01 - self.strings - self.padding;
        }
    };

    pub fn dataLengths(self: *const ResourceTree) Lengths {
        var lengths: Lengths = .{
            .level1 = 0,
            .level2 = 0,
            .level3 = 0,
            .data_entries = 0,
            .strings = 0,
            .padding = 0,
            .rsrc01 = undefined,
            .rsrc02 = self.rsrc02_len,
        };
        lengths.level1 += @sizeOf(ResourceDirectoryTable);
        for (self.type_to_name_map.values()) |name_to_lang_map| {
            lengths.level1 += @sizeOf(ResourceDirectoryEntry);
            lengths.level2 += @sizeOf(ResourceDirectoryTable);
            for (name_to_lang_map.values()) |lang_to_resources_map| {
                lengths.level2 += @sizeOf(ResourceDirectoryEntry);
                lengths.level3 += @sizeOf(ResourceDirectoryTable);
                for (lang_to_resources_map.values()) |_| {
                    lengths.level3 += @sizeOf(ResourceDirectoryEntry);
                    lengths.data_entries += @sizeOf(ResourceDataEntry);
                }
            }
        }
        for (self.rsrc_string_table.keys()) |v| {
            lengths.strings += @sizeOf(u16); // string length
            lengths.strings += @intCast(v.name.len * @sizeOf(u16));
        }
        lengths.rsrc01 = lengths.level1 + lengths.level2 + lengths.level3 + lengths.data_entries + lengths.strings;
        lengths.padding = @intCast((4 -% lengths.rsrc01) % 4);
        lengths.rsrc01 += lengths.padding;
        return lengths;
    }

    pub fn sort(self: *ResourceTree) !void {
        const NameOrOrdinalSortContext = struct {
            keys: []NameOrOrdinal,

            pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
                const a = ctx.keys[a_index];
                const b = ctx.keys[b_index];
                if (std.meta.activeTag(a) != std.meta.activeTag(b)) {
                    return if (a == .name) true else false;
                }
                switch (a) {
                    .name => {
                        const n = @min(a.name.len, b.name.len);
                        for (a.name[0..n], b.name[0..n]) |a_c, b_c| {
                            switch (std.math.order(std.mem.littleToNative(u16, a_c), std.mem.littleToNative(u16, b_c))) {
                                .eq => continue,
                                .lt => return true,
                                .gt => return false,
                            }
                        }
                        return a.name.len < b.name.len;
                    },
                    .ordinal => {
                        return a.ordinal < b.ordinal;
                    },
                }
            }
        };
        self.type_to_name_map.sortUnstable(NameOrOrdinalSortContext{ .keys = self.type_to_name_map.keys() });
        for (self.type_to_name_map.values()) |*name_to_lang_map| {
            name_to_lang_map.sortUnstable(NameOrOrdinalSortContext{ .keys = name_to_lang_map.keys() });
        }
        const LangSortContext = struct {
            keys: []Language,

            pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
                return @as(u16, @bitCast(ctx.keys[a_index])) < @as(u16, @bitCast(ctx.keys[b_index]));
            }
        };
        for (self.type_to_name_map.values()) |*name_to_lang_map| {
            for (name_to_lang_map.values()) |*lang_to_resource_map| {
                lang_to_resource_map.sortUnstable(LangSortContext{ .keys = lang_to_resource_map.keys() });
            }
        }
    }

    pub fn writeCoff(
        self: *const ResourceTree,
        allocator: Allocator,
        w: *std.Io.Writer,
        resources_in_data_order: []const Resource,
        lengths: Lengths,
        coff_string_table: *StringTable,
    ) ![]const std.coff.Symbol {
        if (self.type_to_name_map.count() == 0) {
            try w.splatByteAll(0, 16);
            return &.{};
        }

        var level2_list: std.ArrayList(*const NameToLanguageMap) = .empty;
        defer level2_list.deinit(allocator);

        var level3_list: std.ArrayList(*const LanguageToResourceMap) = .empty;
        defer level3_list.deinit(allocator);

        var resources_list: std.ArrayList(*const RelocatableResource) = .empty;
        defer resources_list.deinit(allocator);

        var relocations = Relocations.init(allocator);
        defer relocations.deinit();

        var string_offsets = try allocator.alloc(u31, self.rsrc_string_table.count());
        const strings_start = lengths.stringsStart();
        defer allocator.free(string_offsets);
        {
            var string_address: u31 = @intCast(strings_start);
            for (self.rsrc_string_table.keys(), 0..) |v, i| {
                string_offsets[i] = string_address;
                string_address += @sizeOf(u16) + @as(u31, @intCast(v.name.len * @sizeOf(u16)));
            }
        }

        const level2_start = lengths.level1;
        var level2_address = level2_start;
        {
            const counts = entryTypeCounts(self.type_to_name_map.keys());
            const table = ResourceDirectoryTable{
                .characteristics = 0,
                .timestamp = 0,
                .major_version = 0,
                .minor_version = 0,
                .number_of_id_entries = counts.ids,
                .number_of_name_entries = counts.names,
            };
            try w.writeStruct(table, .little);

            var it = self.type_to_name_map.iterator();
            while (it.next()) |entry| {
                const type_value = entry.key_ptr;
                const dir_entry = ResourceDirectoryEntry{
                    .entry = switch (type_value.*) {
                        .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(type_value.*).?] } },
                        .ordinal => .{ .integer_id = type_value.ordinal },
                    },
                    .offset = .{
                        .address = @intCast(level2_address),
                        .to_subdirectory = true,
                    },
                };
                try dir_entry.writeCoff(w);
                level2_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry)));

                const name_to_lang_map = entry.value_ptr;
                try level2_list.append(allocator, name_to_lang_map);
            }
        }

        const level3_start = level2_start + lengths.level2;
        var level3_address = level3_start;
        for (level2_list.items) |name_to_lang_map| {
            const counts = entryTypeCounts(name_to_lang_map.keys());
            const table = ResourceDirectoryTable{
                .characteristics = 0,
                .timestamp = 0,
                .major_version = 0,
                .minor_version = 0,
                .number_of_id_entries = counts.ids,
                .number_of_name_entries = counts.names,
            };
            try w.writeStruct(table, .little);

            var it = name_to_lang_map.iterator();
            while (it.next()) |entry| {
                const name_value = entry.key_ptr;
                const dir_entry = ResourceDirectoryEntry{
                    .entry = switch (name_value.*) {
                        .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(name_value.*).?] } },
                        .ordinal => .{ .integer_id = name_value.ordinal },
                    },
                    .offset = .{
                        .address = @intCast(level3_address),
                        .to_subdirectory = true,
                    },
                };
                try dir_entry.writeCoff(w);
                level3_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry)));

                const lang_to_resources_map = entry.value_ptr;
                try level3_list.append(allocator, lang_to_resources_map);
            }
        }

        var reloc_addresses = try allocator.alloc(u32, resources_in_data_order.len);
        defer allocator.free(reloc_addresses);

        const data_entries_start = level3_start + lengths.level3;
        var data_entry_address = data_entries_start;
        for (level3_list.items) |lang_to_resources_map| {
            const counts = EntryTypeCounts{
                .names = 0,
                .ids = @intCast(lang_to_resources_map.count()),
            };
            const table = ResourceDirectoryTable{
                .characteristics = 0,
                .timestamp = 0,
                .major_version = 0,
                .minor_version = 0,
                .number_of_id_entries = counts.ids,
                .number_of_name_entries = counts.names,
            };
            try w.writeStruct(table, .little);

            var it = lang_to_resources_map.iterator();
            while (it.next()) |entry| {
                const lang = entry.key_ptr.*;
                const dir_entry = ResourceDirectoryEntry{
                    .entry = .{ .integer_id = lang.asInt() },
                    .offset = .{
                        .address = @intCast(data_entry_address),
                        .to_subdirectory = false,
                    },
                };

                const reloc_resource = entry.value_ptr;
                reloc_addresses[reloc_resource.original_index] = @intCast(data_entry_address);

                try dir_entry.writeCoff(w);
                data_entry_address += @sizeOf(ResourceDataEntry);

                try resources_list.append(allocator, reloc_resource);
            }
        }

        for (resources_list.items, 0..) |reloc_resource, i| {
            // TODO: This logic works but is convoluted, would be good to clean this up
            const orig_resource = &resources_in_data_order[reloc_resource.original_index];
            const address: u32 = reloc_addresses[i];
            try relocations.add(address, self.data_offsets.items[i]);
            const data_entry = ResourceDataEntry{
                .data_rva = 0, // relocation
                .size = @intCast(orig_resource.data.len),
                .codepage = 0,
            };
            try w.writeStruct(data_entry, .little);
        }

        for (self.rsrc_string_table.keys()) |v| {
            const str = v.name;
            try w.writeInt(u16, @intCast(str.len), .little);
            try w.writeAll(std.mem.sliceAsBytes(str));
        }

        try w.splatByteAll(0, lengths.padding);

        for (relocations.list.items) |relocation| {
            try writeRelocation(w, std.coff.Relocation{
                .virtual_address = relocation.relocation_address,
                .symbol_table_index = relocation.symbol_index,
                .type = supported_targets.rvaRelocationTypeIndicator(self.coff_options.target).?,
            });
        }

        if (self.coff_options.fold_duplicate_data) {
            for (self.deduplicated_data.keys()) |data| {
                const padding_bytes: u4 = @intCast((8 -% data.len) % 8);
                try w.writeAll(data);
                try w.splatByteAll(0, padding_bytes);
            }
        } else {
            for (resources_in_data_order) |resource| {
                const padding_bytes: u4 = @intCast((8 -% resource.data.len) % 8);
                try w.writeAll(resource.data);
                try w.splatByteAll(0, padding_bytes);
            }
        }

        var symbols = try allocator.alloc(std.coff.Symbol, resources_list.items.len);
        errdefer allocator.free(symbols);

        for (relocations.list.items, 0..) |relocation, i| {
            // cvtres.exe writes the symbol names as $R<data offset as hexadecimal>.
            //
            // When the data offset would exceed 6 hex digits in cvtres.exe, it
            // truncates the value down to 6 hex digits. This is bad behavior, since
            // e.g. an initial resource with exactly 16 MiB of data and the
            // resource following it would both have the symbol name $R000000.
            //
            // Instead, if the offset would exceed 6 hexadecimal digits,
            // we put the longer name in the string table.
            //
            // Another option would be to adopt llvm-cvtres' behavior
            // of $R000001, $R000002, etc. rather than using data offset values.
            var name_buf: [8]u8 = undefined;
            if (relocation.data_offset > std.math.maxInt(u24)) {
                const name_slice = try std.fmt.allocPrint(allocator, "$R{X}", .{relocation.data_offset});
                defer allocator.free(name_slice);
                const string_table_offset: u32 = try coff_string_table.put(allocator, name_slice);
                std.mem.writeInt(u32, name_buf[0..4], 0, .little);
                std.mem.writeInt(u32, name_buf[4..8], string_table_offset, .little);
            } else {
                const name_slice = std.fmt.bufPrint(&name_buf, "$R{X:0>6}", .{relocation.data_offset}) catch unreachable;
                std.debug.assert(name_slice.len == 8);
            }

            symbols[i] = .{
                .name = name_buf,
                .value = relocation.data_offset,
                .section_number = @fromBackingInt(@intCast(2)),
                .type = .{
                    .base_type = .NULL,
                    .complex_type = .NULL,
                },
                .storage_class = .STATIC,
                .number_of_aux_symbols = 0,
            };
        }

        return symbols;
    }

    fn writeRelocation(writer: *std.Io.Writer, relocation: std.coff.Relocation) !void {
        try writer.writeInt(u32, relocation.virtual_address, .little);
        try writer.writeInt(u32, relocation.symbol_table_index, .little);
        try writer.writeInt(u16, relocation.type, .little);
    }

    const EntryTypeCounts = struct {
        names: u16,
        ids: u16,
    };

    fn entryTypeCounts(s: []const NameOrOrdinal) EntryTypeCounts {
        var names: u16 = 0;
        var ordinals: u16 = 0;
        for (s) |v| {
            switch (v) {
                .name => names += 1,
                .ordinal => ordinals += 1,
            }
        }
        return .{ .names = names, .ids = ordinals };
    }
}