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