authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-02-07 20:56:49-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-02-22 21:21:30-05:00
loga502301b5eb84329c8c3ecb4b68bc52048f99cdc
treead086cb9950be276b6688aef6abf6107391afb59
parent8683f25d24414f817e946ac5fe7c9f6eceb7bb09

zig rc: Add COFF object file creation for CMake cross-compilation use case

In #22522 I said: > RC="zig rc" will now work in combination with zig cc and CMake. Here's an example of cross-compiling a simple Windows GUI CMake project > > $ RC="zig rc" CC="zig cc --target=x86_64-windows-gnu" cmake .. -DCMAKE_SYSTEM_NAME=Windows -G Ninja However, I didn't realize that the time that this only works because of the `-G Ninja` part. When not using Ninja as the build tool, CMake adds a workaround for 'very long lists of object files' where it takes all object files and runs them through `ar` to combine them into one archive: https://github.com/Kitware/CMake/blob/4a11fd8dde745789f66d6500412d7f56607e9218/Modules/Platform/Windows-GNU.cmake#L141-L158 This is a problem for the Windows resource use-case, because `ar` doesn't know how to deal with `.res` files and so this object combining step fails with: unknown file type: foo.rc.res Only the linker knows what to do with .res files (since it has its own `.res` -> `.obj` ('cvtres') conversion mechanism). So, when using Ninja, this object file combining step is skipped and the .res file gets passed to the linker and everyone is happy. Note: When CMake thinks that its using `windres` as the Windows resource compiler, it will pass `-O coff` to windres which causes it to output a COFF object file instead of a `.res` file, which means that the `ar` step can succeed because it's only working on actual object files. --- This commit gives `zig rc` the ability to output COFF object files directly when `/:output-format coff` is provided as an argument. This effectively matches what happens when CMake uses `windres` for resource compilation, but requires the argument to be provided explicitly. So, after this change, the following CMake cross-compilation use case will work, even when not using Ninja as the generator: RC="zig rc /:output-format coff" CC="zig cc --target=x86_64-windows-gnu" cmake .. -DCMAKE_SYSTEM_NAME=Windows

3 files changed, 2013 insertions(+), 155 deletions(-)

lib/compiler/resinator/cli.zig+577-62
......@@ -5,6 +5,7 @@ const lang = @import("lang.zig");
55const res = @import("res.zig");
66const Allocator = std.mem.Allocator;
77const lex = @import("lex.zig");
8const cvtres = @import("cvtres.zig");
89
910/// This is what /SL 100 will set the maximum string literal length to
1011pub const max_string_literal_length_100_percent = 8192;
......@@ -59,6 +60,20 @@ pub const usage_string_after_command_name =
5960 \\ the .rc includes or otherwise depends on.
6061 \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set.
6162 \\ json (default) A top-level JSON array of paths
63 \\ /:input-format <value> If not specified, the input format is inferred.
64 \\ rc (default if input format cannot be inferred)
65 \\ res Compiled .rc file, implies /:output-format coff
66 \\ rcpp Preprocessed .rc file, implies /:no-preprocess
67 \\ /:output-format <value> If not specified, the output format is inferred.
68 \\ res (default if output format cannot be inferred)
69 \\ coff COFF object file (extension: .obj or .o)
70 \\ rcpp Preprocessed .rc file, implies /p
71 \\ /:target <arch> Set the target machine for COFF object files.
72 \\ Can be specified either as PE/COFF machine constant
73 \\ name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64,
74 \\ aarch64, etc). The default is X64 (aka x86_64).
75 \\ Also accepts a full Zig/LLVM triple, but everything
76 \\ except the architecture is ignored.
6277 \\
6378 \\Note: For compatibility reasons, all custom options start with :
6479 \\
......@@ -131,8 +146,8 @@ pub const Diagnostics = struct {
131146
132147pub const Options = struct {
133148 allocator: Allocator,
134 input_filename: []const u8 = &[_]u8{},
135 output_filename: []const u8 = &[_]u8{},
149 input_source: IoSource = .{ .filename = &[_]u8{} },
150 output_source: IoSource = .{ .filename = &[_]u8{} },
136151 extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty,
137152 ignore_include_env_var: bool = false,
138153 preprocess: Preprocess = .yes,
......@@ -149,9 +164,30 @@ pub const Options = struct {
149164 auto_includes: AutoIncludes = .any,
150165 depfile_path: ?[]const u8 = null,
151166 depfile_fmt: DepfileFormat = .json,
167 input_format: InputFormat = .rc,
168 output_format: OutputFormat = .res,
169 coff_options: cvtres.CoffOptions = .{},
152170
171 pub const IoSource = union(enum) {
172 stdio: std.fs.File,
173 filename: []const u8,
174 };
153175 pub const AutoIncludes = enum { any, msvc, gnu, none };
154176 pub const DepfileFormat = enum { json };
177 pub const InputFormat = enum { rc, res, rcpp };
178 pub const OutputFormat = enum {
179 res,
180 coff,
181 rcpp,
182
183 pub fn extension(format: OutputFormat) []const u8 {
184 return switch (format) {
185 .rcpp => ".rcpp",
186 .coff => ".obj",
187 .res => ".res",
188 };
189 }
190 };
155191 pub const Preprocess = enum { no, yes, only };
156192 pub const SymbolAction = enum { define, undefine };
157193 pub const SymbolValue = union(SymbolAction) {
......@@ -198,9 +234,10 @@ pub const Options = struct {
198234 try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
199235 }
200236
201 /// If the current input filename both:
237 /// If the current input filename:
202238 /// - does not have an extension, and
203 /// - does not exist in the cwd
239 /// - does not exist in the cwd, and
240 /// - the input format is .rc
204241 /// then this function will append `.rc` to the input filename
205242 ///
206243 /// Note: This behavior is different from the Win32 compiler.
......@@ -213,14 +250,18 @@ pub const Options = struct {
213250 /// of the .rc extension being omitted from the CLI args, but still
214251 /// work fine if the file itself does not have an extension.
215252 pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
216 if (std.fs.path.extension(options.input_filename).len == 0) {
217 cwd.access(options.input_filename, .{}) catch |err| switch (err) {
253 switch (options.input_source) {
254 .stdio => return,
255 .filename => {},
256 }
257 if (options.input_format == .rc and std.fs.path.extension(options.input_source.filename).len == 0) {
258 cwd.access(options.input_source.filename, .{}) catch |err| switch (err) {
218259 error.FileNotFound => {
219 var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3);
220 @memcpy(filename_bytes[0..options.input_filename.len], options.input_filename);
260 var filename_bytes = try options.allocator.alloc(u8, options.input_source.filename.len + 3);
261 @memcpy(filename_bytes[0..options.input_source.filename.len], options.input_source.filename);
221262 @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
222 options.allocator.free(options.input_filename);
223 options.input_filename = filename_bytes;
263 options.allocator.free(options.input_source.filename);
264 options.input_source = .{ .filename = filename_bytes };
224265 },
225266 else => {},
226267 };
......@@ -232,8 +273,14 @@ pub const Options = struct {
232273 self.allocator.free(extra_include_path);
233274 }
234275 self.extra_include_paths.deinit(self.allocator);
235 self.allocator.free(self.input_filename);
236 self.allocator.free(self.output_filename);
276 switch (self.input_source) {
277 .stdio => {},
278 .filename => |filename| self.allocator.free(filename),
279 }
280 switch (self.output_source) {
281 .stdio => {},
282 .filename => |filename| self.allocator.free(filename),
283 }
237284 var symbol_it = self.symbols.iterator();
238285 while (symbol_it.next()) |entry| {
239286 self.allocator.free(entry.key_ptr.*);
......@@ -243,11 +290,26 @@ pub const Options = struct {
243290 if (self.depfile_path) |depfile_path| {
244291 self.allocator.free(depfile_path);
245292 }
293 if (self.coff_options.define_external_symbol) |symbol_name| {
294 self.allocator.free(symbol_name);
295 }
246296 }
247297
248298 pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
249 try writer.print("Input filename: {s}\n", .{self.input_filename});
250 try writer.print("Output filename: {s}\n", .{self.output_filename});
299 const input_source_name = switch (self.input_source) {
300 .stdio => "<stdin>",
301 .filename => |filename| filename,
302 };
303 const output_source_name = switch (self.output_source) {
304 .stdio => "<stdout>",
305 .filename => |filename| filename,
306 };
307 try writer.print("Input filename: {s} (format={s})\n", .{ input_source_name, @tagName(self.input_format) });
308 try writer.print("Output filename: {s} (format={s})\n", .{ output_source_name, @tagName(self.output_format) });
309 if (self.output_format == .coff) {
310 try writer.print(" Target machine type for COFF: {s}\n", .{@tagName(self.coff_options.target)});
311 }
312
251313 if (self.extra_include_paths.items.len > 0) {
252314 try writer.writeAll(" Extra include paths:\n");
253315 for (self.extra_include_paths.items) |extra_include_path| {
......@@ -331,6 +393,7 @@ pub const Arg = struct {
331393 }
332394
333395 pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
396 if (option_len == 0) return self.name();
334397 return self.name()[0..option_len];
335398 }
336399
......@@ -380,6 +443,8 @@ pub const Arg = struct {
380443
381444 pub const Value = struct {
382445 slice: []const u8,
446 /// Amount to increment the arg index to skip over both the option and the value arg(s)
447 /// e.g. 1 if /<option><value>, 2 if /<option> <value>
383448 index_increment: u2 = 1,
384449
385450 pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
......@@ -414,6 +479,7 @@ pub const Arg = struct {
414479
415480 pub const Context = struct {
416481 index: usize,
482 option_len: usize,
417483 arg: Arg,
418484 value: Value,
419485 };
......@@ -428,7 +494,18 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
428494 errdefer options.deinit();
429495
430496 var output_filename: ?[]const u8 = null;
431 var output_filename_context: Arg.Context = undefined;
497 var output_filename_context: union(enum) {
498 unspecified: void,
499 positional: usize,
500 arg: Arg.Context,
501 } = .{ .unspecified = {} };
502 var output_format: ?Options.OutputFormat = null;
503 var output_format_context: Arg.Context = undefined;
504 var input_format: ?Options.InputFormat = null;
505 var input_format_context: Arg.Context = undefined;
506 var input_filename_arg_i: usize = undefined;
507 var preprocess_only_context: Arg.Context = undefined;
508 var depfile_context: Arg.Context = undefined;
432509
433510 var arg_i: usize = 0;
434511 next_arg: while (arg_i < args.len) {
......@@ -470,6 +547,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
470547 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
471548 options.preprocess = .no;
472549 arg.name_offset += ":no-preprocess".len;
550 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) {
551 const value = arg.value(":output-format".len, arg_i, args) catch {
552 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
553 var msg_writer = err_details.msg.writer(allocator);
554 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) });
555 try diagnostics.append(err_details);
556 arg_i += 1;
557 break :next_arg;
558 };
559 output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: {
560 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
561 var msg_writer = err_details.msg.writer(allocator);
562 try msg_writer.print("invalid output format setting: {s} ", .{value.slice});
563 try diagnostics.append(err_details);
564 break :blk output_format;
565 };
566 output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value };
567 arg_i += value.index_increment;
568 continue :next_arg;
473569 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
474570 const value = arg.value(":auto-includes".len, arg_i, args) catch {
475571 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
......@@ -488,6 +584,25 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
488584 };
489585 arg_i += value.index_increment;
490586 continue :next_arg;
587 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) {
588 const value = arg.value(":input-format".len, arg_i, args) catch {
589 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
590 var msg_writer = err_details.msg.writer(allocator);
591 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) });
592 try diagnostics.append(err_details);
593 arg_i += 1;
594 break :next_arg;
595 };
596 input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: {
597 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
598 var msg_writer = err_details.msg.writer(allocator);
599 try msg_writer.print("invalid input format setting: {s} ", .{value.slice});
600 try diagnostics.append(err_details);
601 break :blk input_format;
602 };
603 input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value };
604 arg_i += value.index_increment;
605 continue :next_arg;
491606 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
492607 const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
493608 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
......@@ -522,6 +637,31 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
522637 const path = try allocator.dupe(u8, value.slice);
523638 errdefer allocator.free(path);
524639 options.depfile_path = path;
640 depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value };
641 arg_i += value.index_increment;
642 continue :next_arg;
643 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) {
644 const value = arg.value(":target".len, arg_i, args) catch {
645 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
646 var msg_writer = err_details.msg.writer(allocator);
647 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) });
648 try diagnostics.append(err_details);
649 arg_i += 1;
650 break :next_arg;
651 };
652 // Take the substring up to the first dash so that a full target triple
653 // can be used, e.g. x86_64-windows-gnu becomes x86_64
654 var target_it = std.mem.splitScalar(u8, value.slice, '-');
655 const arch_str = target_it.first();
656 const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse {
657 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
658 var msg_writer = err_details.msg.writer(allocator);
659 try msg_writer.print("invalid or unsupported target architecture: {s}", .{arch_str});
660 try diagnostics.append(err_details);
661 arg_i += value.index_increment;
662 continue :next_arg;
663 };
664 options.coff_options.target = arch.toCoffMachineType();
525665 arg_i += value.index_increment;
526666 continue :next_arg;
527667 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
......@@ -620,7 +760,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
620760 arg_i += 1;
621761 break :next_arg;
622762 };
623 output_filename_context = .{ .index = arg_i, .arg = arg, .value = value };
763 output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } };
624764 output_filename = value.slice;
625765 arg_i += value.index_increment;
626766 continue :next_arg;
......@@ -812,6 +952,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
812952 arg.name_offset += 1;
813953 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
814954 options.preprocess = .only;
955 preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined };
815956 arg.name_offset += 1;
816957 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
817958 const value = arg.value(1, arg_i, args) catch {
......@@ -920,10 +1061,10 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
9201061
9211062 if (args.len > 0) {
9221063 const last_arg = args[args.len - 1];
923 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
1064 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) {
9241065 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
9251066 var note_writer = note_details.msg.writer(allocator);
926 try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing");
1067 try note_writer.writeAll("if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing");
9271068 try diagnostics.append(note_details);
9281069 }
9291070 }
......@@ -932,7 +1073,28 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
9321073 // things after this rely on the value of the input filename.
9331074 return error.ParseError;
9341075 }
935 options.input_filename = try allocator.dupe(u8, positionals[0]);
1076 options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) };
1077 input_filename_arg_i = arg_i;
1078
1079 const InputFormatSource = enum {
1080 inferred_from_input_filename,
1081 input_format_arg,
1082 };
1083
1084 var input_format_source: InputFormatSource = undefined;
1085 if (input_format == null) {
1086 const ext = std.fs.path.extension(options.input_source.filename);
1087 if (std.ascii.eqlIgnoreCase(ext, ".res")) {
1088 input_format = .res;
1089 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
1090 input_format = .rcpp;
1091 } else {
1092 input_format = .rc;
1093 }
1094 input_format_source = .inferred_from_input_filename;
1095 } else {
1096 input_format_source = .input_format_arg;
1097 }
9361098
9371099 if (positionals.len > 1) {
9381100 if (output_filename != null) {
......@@ -942,53 +1104,233 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
9421104 try diagnostics.append(err_details);
9431105 var note_details = Diagnostics.ErrorDetails{
9441106 .type = .note,
945 .arg_index = output_filename_context.value.index(output_filename_context.index),
946 .arg_span = output_filename_context.value.argSpan(output_filename_context.arg),
1107 .arg_index = output_filename_context.arg.index,
1108 .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg),
9471109 };
9481110 var note_writer = note_details.msg.writer(allocator);
9491111 try note_writer.writeAll("output filename previously specified here");
9501112 try diagnostics.append(note_details);
9511113 } else {
9521114 output_filename = positionals[1];
1115 output_filename_context = .{ .positional = arg_i + 1 };
9531116 }
9541117 }
1118
1119 const OutputFormatSource = enum {
1120 inferred_from_input_filename,
1121 inferred_from_output_filename,
1122 output_format_arg,
1123 unable_to_infer_from_input_filename,
1124 unable_to_infer_from_output_filename,
1125 inferred_from_preprocess_only,
1126 };
1127
1128 var output_format_source: OutputFormatSource = undefined;
9551129 if (output_filename == null) {
956 var buf = std.ArrayList(u8).init(allocator);
957 errdefer buf.deinit();
958
959 if (std.fs.path.dirname(options.input_filename)) |dirname| {
960 var end_pos = dirname.len;
961 // We want to ensure that we write a path separator at the end, so if the dirname
962 // doesn't end with a path sep then include the char after the dirname
963 // which must be a path sep.
964 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
965 try buf.appendSlice(options.input_filename[0..end_pos]);
1130 if (output_format == null) {
1131 output_format_source = .inferred_from_input_filename;
1132 const input_ext = std.fs.path.extension(options.input_source.filename);
1133 if (std.ascii.eqlIgnoreCase(input_ext, ".res")) {
1134 output_format = .coff;
1135 } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) {
1136 output_format = .rcpp;
1137 output_format_source = .inferred_from_preprocess_only;
1138 } else {
1139 if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) {
1140 output_format_source = .unable_to_infer_from_input_filename;
1141 }
1142 output_format = .res;
1143 }
9661144 }
967 try buf.appendSlice(std.fs.path.stem(options.input_filename));
968 if (options.preprocess == .only) {
969 try buf.appendSlice(".rcpp");
1145 options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) };
1146 } else {
1147 options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) };
1148 if (output_format == null) {
1149 output_format_source = .inferred_from_output_filename;
1150 const ext = std.fs.path.extension(options.output_source.filename);
1151 if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) {
1152 output_format = .coff;
1153 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
1154 output_format = .rcpp;
1155 } else {
1156 if (!std.ascii.eqlIgnoreCase(ext, ".res")) {
1157 output_format_source = .unable_to_infer_from_output_filename;
1158 }
1159 output_format = .res;
1160 }
9701161 } else {
971 try buf.appendSlice(".res");
1162 output_format_source = .output_format_arg;
9721163 }
1164 }
9731165
974 options.output_filename = try buf.toOwnedSlice();
975 } else {
976 options.output_filename = try allocator.dupe(u8, output_filename.?);
1166 options.input_format = input_format.?;
1167 options.output_format = output_format.?;
1168
1169 // Check for incompatible options
1170 var print_input_format_source_note: bool = false;
1171 var print_output_format_source_note: bool = false;
1172 if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) {
1173 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) };
1174 var msg_writer = err_details.msg.writer(allocator);
1175 if (options.input_format == .res) {
1176 try msg_writer.print("the {s}{s} option was ignored because the input format is '{s}'", .{
1177 depfile_context.arg.prefixSlice(),
1178 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
1179 @tagName(options.input_format),
1180 });
1181 print_input_format_source_note = true;
1182 } else if (options.output_format == .rcpp) {
1183 try msg_writer.print("the {s}{s} option was ignored because the output format is '{s}'", .{
1184 depfile_context.arg.prefixSlice(),
1185 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
1186 @tagName(options.output_format),
1187 });
1188 print_output_format_source_note = true;
1189 }
1190 try diagnostics.append(err_details);
1191 }
1192 if (!isSupportedTransformation(options.input_format, options.output_format)) {
1193 var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false };
1194 var msg_writer = err_details.msg.writer(allocator);
1195 try msg_writer.print("input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) });
1196 try diagnostics.append(err_details);
1197 print_input_format_source_note = true;
1198 print_output_format_source_note = true;
1199 }
1200 if (options.preprocess == .only and options.output_format != .rcpp) {
1201 var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index };
1202 var msg_writer = err_details.msg.writer(allocator);
1203 try msg_writer.print("the {s}{s} option cannot be used with output format '{s}'", .{
1204 preprocess_only_context.arg.prefixSlice(),
1205 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
1206 @tagName(options.output_format),
1207 });
1208 try diagnostics.append(err_details);
1209 print_output_format_source_note = true;
1210 }
1211 if (print_input_format_source_note) {
1212 switch (input_format_source) {
1213 .inferred_from_input_filename => {
1214 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
1215 var msg_writer = err_details.msg.writer(allocator);
1216 try msg_writer.writeAll("the input format was inferred from the input filename");
1217 try diagnostics.append(err_details);
1218 },
1219 .input_format_arg => {
1220 var err_details = Diagnostics.ErrorDetails{
1221 .type = .note,
1222 .arg_index = input_format_context.index,
1223 .arg_span = input_format_context.value.argSpan(input_format_context.arg),
1224 };
1225 var msg_writer = err_details.msg.writer(allocator);
1226 try msg_writer.writeAll("the input format was specified here");
1227 try diagnostics.append(err_details);
1228 },
1229 }
1230 }
1231 if (print_output_format_source_note) {
1232 switch (output_format_source) {
1233 .inferred_from_input_filename, .unable_to_infer_from_input_filename => {
1234 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
1235 var msg_writer = err_details.msg.writer(allocator);
1236 if (output_format_source == .inferred_from_input_filename) {
1237 try msg_writer.writeAll("the output format was inferred from the input filename");
1238 } else {
1239 try msg_writer.writeAll("the output format was unable to be inferred from the input filename, so the default was used");
1240 }
1241 try diagnostics.append(err_details);
1242 },
1243 .inferred_from_output_filename, .unable_to_infer_from_output_filename => {
1244 var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) {
1245 .positional => |i| .{ .type = .note, .arg_index = i },
1246 .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) },
1247 .unspecified => unreachable,
1248 };
1249 var msg_writer = err_details.msg.writer(allocator);
1250 if (output_format_source == .inferred_from_output_filename) {
1251 try msg_writer.writeAll("the output format was inferred from the output filename");
1252 } else {
1253 try msg_writer.writeAll("the output format was unable to be inferred from the output filename, so the default was used");
1254 }
1255 try diagnostics.append(err_details);
1256 },
1257 .output_format_arg => {
1258 var err_details = Diagnostics.ErrorDetails{
1259 .type = .note,
1260 .arg_index = output_format_context.index,
1261 .arg_span = output_format_context.value.argSpan(output_format_context.arg),
1262 };
1263 var msg_writer = err_details.msg.writer(allocator);
1264 try msg_writer.writeAll("the output format was specified here");
1265 try diagnostics.append(err_details);
1266 },
1267 .inferred_from_preprocess_only => {
1268 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index };
1269 var msg_writer = err_details.msg.writer(allocator);
1270 try msg_writer.print("the output format was inferred from the usage of the {s}{s} option", .{
1271 preprocess_only_context.arg.prefixSlice(),
1272 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
1273 });
1274 try diagnostics.append(err_details);
1275 },
1276 }
9771277 }
9781278
9791279 if (diagnostics.hasError()) {
9801280 return error.ParseError;
9811281 }
9821282
1283 // Implied settings from input/output formats
1284 if (options.output_format == .rcpp) options.preprocess = .only;
1285 if (options.input_format == .res) options.output_format = .coff;
1286 if (options.input_format == .rcpp) options.preprocess = .no;
1287
9831288 return options;
9841289}
9851290
1291pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 {
1292 var buf = std.ArrayList(u8).init(allocator);
1293 errdefer buf.deinit();
1294 if (std.fs.path.dirname(path)) |dirname| {
1295 var end_pos = dirname.len;
1296 // We want to ensure that we write a path separator at the end, so if the dirname
1297 // doesn't end with a path sep then include the char after the dirname
1298 // which must be a path sep.
1299 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
1300 try buf.appendSlice(path[0..end_pos]);
1301 }
1302 try buf.appendSlice(std.fs.path.stem(path));
1303 try buf.appendSlice(ext);
1304 return try buf.toOwnedSlice();
1305}
1306
9861307pub fn isSupportedInputExtension(ext: []const u8) bool {
9871308 if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true;
1309 if (std.ascii.eqlIgnoreCase(ext, ".res")) return true;
9881310 if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true;
9891311 return false;
9901312}
9911313
1314pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool {
1315 return switch (input) {
1316 .rc => switch (output) {
1317 .res => true,
1318 .coff => true,
1319 .rcpp => true,
1320 },
1321 .res => switch (output) {
1322 .res => false,
1323 .coff => true,
1324 .rcpp => false,
1325 },
1326 .rcpp => switch (output) {
1327 .res => true,
1328 .coff => true,
1329 .rcpp => false,
1330 },
1331 };
1332}
1333
9921334/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
9931335pub fn isValidIdentifier(str: []const u8) bool {
9941336 for (str, 0..) |c, i| switch (c) {
......@@ -1278,17 +1620,6 @@ test "parse errors: basic" {
12781620 \\
12791621 \\
12801622 );
1281 try testParseError(&.{"/some/absolute/path/parsed/as/an/option.rc"},
1282 \\<cli>: error: the /s option is unsupported
1283 \\ ... /some/absolute/path/parsed/as/an/option.rc
1284 \\ ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1285 \\<cli>: error: missing input filename
1286 \\
1287 \\<cli>: note: if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing
1288 \\ ... /some/absolute/path/parsed/as/an/option.rc
1289 \\ ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1290 \\
1291 );
12921623}
12931624
12941625test "inferred absolute filepaths" {
......@@ -1349,8 +1680,8 @@ test "parse: options" {
13491680 defer options.deinit();
13501681
13511682 try std.testing.expectEqual(true, options.verbose);
1352 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1353 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1683 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1684 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
13541685 }
13551686 {
13561687 var options = try testParse(&.{ "/vx", "foo.rc" });
......@@ -1358,8 +1689,8 @@ test "parse: options" {
13581689
13591690 try std.testing.expectEqual(true, options.verbose);
13601691 try std.testing.expectEqual(true, options.ignore_include_env_var);
1361 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1362 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1692 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1693 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
13631694 }
13641695 {
13651696 var options = try testParse(&.{ "/xv", "foo.rc" });
......@@ -1367,8 +1698,8 @@ test "parse: options" {
13671698
13681699 try std.testing.expectEqual(true, options.verbose);
13691700 try std.testing.expectEqual(true, options.ignore_include_env_var);
1370 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1371 try std.testing.expectEqualStrings("foo.res", options.output_filename);
1701 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1702 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
13721703 }
13731704 {
13741705 var options = try testParse(&.{ "/xvFObar.res", "foo.rc" });
......@@ -1376,8 +1707,8 @@ test "parse: options" {
13761707
13771708 try std.testing.expectEqual(true, options.verbose);
13781709 try std.testing.expectEqual(true, options.ignore_include_env_var);
1379 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
1380 try std.testing.expectEqualStrings("bar.res", options.output_filename);
1710 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1711 try std.testing.expectEqualStrings("bar.res", options.output_source.filename);
13811712 }
13821713}
13831714
......@@ -1541,24 +1872,208 @@ test "parse: unsupported LCX/LCE-related options" {
15411872 );
15421873}
15431874
1875test "parse: output filename specified twice" {
1876 try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" },
1877 \\<cli>: error: output filename already specified
1878 \\ ... foo.res
1879 \\ ^~~~~~~
1880 \\<cli>: note: output filename previously specified here
1881 \\ ... /fo foo.res ...
1882 \\ ~~~~^~~~~~~
1883 \\
1884 );
1885}
1886
1887test "parse: input and output formats" {
1888 {
1889 try testParseError(&.{ "/:output-format", "rcpp", "foo.res" },
1890 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
1891 \\
1892 \\<cli>: note: the input format was inferred from the input filename
1893 \\ ... foo.res
1894 \\ ^~~~~~~
1895 \\<cli>: note: the output format was specified here
1896 \\ ... /:output-format rcpp ...
1897 \\ ~~~~~~~~~~~~~~~~^~~~
1898 \\
1899 );
1900 }
1901 {
1902 try testParseError(&.{ "foo.res", "foo.rcpp" },
1903 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
1904 \\
1905 \\<cli>: note: the input format was inferred from the input filename
1906 \\ ... foo.res ...
1907 \\ ^~~~~~~
1908 \\<cli>: note: the output format was inferred from the output filename
1909 \\ ... foo.rcpp
1910 \\ ^~~~~~~~
1911 \\
1912 );
1913 }
1914 {
1915 try testParseError(&.{ "/:input-format", "res", "foo" },
1916 \\<cli>: error: input format 'res' cannot be converted to output format 'res'
1917 \\
1918 \\<cli>: note: the input format was specified here
1919 \\ ... /:input-format res ...
1920 \\ ~~~~~~~~~~~~~~~^~~
1921 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
1922 \\ ... foo
1923 \\ ^~~
1924 \\
1925 );
1926 }
1927 {
1928 try testParseError(&.{ "/p", "/:input-format", "res", "foo" },
1929 \\<cli>: error: input format 'res' cannot be converted to output format 'res'
1930 \\
1931 \\<cli>: error: the /p option cannot be used with output format 'res'
1932 \\ ... /p ...
1933 \\ ^~
1934 \\<cli>: note: the input format was specified here
1935 \\ ... /:input-format res ...
1936 \\ ~~~~~~~~~~~~~~~^~~
1937 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
1938 \\ ... foo
1939 \\ ^~~
1940 \\
1941 );
1942 }
1943 {
1944 try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" },
1945 \\<cli>: error: the /p option cannot be used with output format 'coff'
1946 \\ ... /p ...
1947 \\ ^~
1948 \\<cli>: note: the output format was specified here
1949 \\ ... /:output-format coff ...
1950 \\ ~~~~~~~~~~~~~~~~^~~~
1951 \\
1952 );
1953 }
1954 {
1955 try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" },
1956 \\<cli>: error: the /p option cannot be used with output format 'res'
1957 \\ ... /p ...
1958 \\ ^~
1959 \\<cli>: note: the output format was inferred from the output filename
1960 \\ ... /fo foo.res ...
1961 \\ ~~~~^~~~~~~
1962 \\
1963 );
1964 }
1965 {
1966 try testParseError(&.{ "/p", "foo.rc", "foo.o" },
1967 \\<cli>: error: the /p option cannot be used with output format 'coff'
1968 \\ ... /p ...
1969 \\ ^~
1970 \\<cli>: note: the output format was inferred from the output filename
1971 \\ ... foo.o
1972 \\ ^~~~~
1973 \\
1974 );
1975 }
1976 {
1977 var options = try testParse(&.{"foo.rc"});
1978 defer options.deinit();
1979
1980 try std.testing.expectEqual(.rc, options.input_format);
1981 try std.testing.expectEqual(.res, options.output_format);
1982 }
1983 {
1984 var options = try testParse(&.{"foo.rcpp"});
1985 defer options.deinit();
1986
1987 try std.testing.expectEqual(.no, options.preprocess);
1988 try std.testing.expectEqual(.rcpp, options.input_format);
1989 try std.testing.expectEqual(.res, options.output_format);
1990 }
1991 {
1992 var options = try testParse(&.{ "foo.rc", "foo.rcpp" });
1993 defer options.deinit();
1994
1995 try std.testing.expectEqual(.only, options.preprocess);
1996 try std.testing.expectEqual(.rc, options.input_format);
1997 try std.testing.expectEqual(.rcpp, options.output_format);
1998 }
1999 {
2000 var options = try testParse(&.{ "foo.rc", "foo.obj" });
2001 defer options.deinit();
2002
2003 try std.testing.expectEqual(.rc, options.input_format);
2004 try std.testing.expectEqual(.coff, options.output_format);
2005 }
2006 {
2007 var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" });
2008 defer options.deinit();
2009
2010 try std.testing.expectEqual(.rc, options.input_format);
2011 try std.testing.expectEqual(.coff, options.output_format);
2012 }
2013 {
2014 var options = try testParse(&.{"foo.res"});
2015 defer options.deinit();
2016
2017 try std.testing.expectEqual(.res, options.input_format);
2018 try std.testing.expectEqual(.coff, options.output_format);
2019 }
2020 {
2021 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" },
2022 \\<cli>: warning: the /:depfile option was ignored because the output format is 'rcpp'
2023 \\ ... /:depfile foo.json ...
2024 \\ ~~~~~~~~~~^~~~~~~~
2025 \\<cli>: note: the output format was inferred from the output filename
2026 \\ ... foo.rcpp
2027 \\ ^~~~~~~~
2028 \\
2029 );
2030 defer options.deinit();
2031
2032 try std.testing.expectEqual(.rc, options.input_format);
2033 try std.testing.expectEqual(.rcpp, options.output_format);
2034 }
2035 {
2036 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" },
2037 \\<cli>: warning: the /:depfile option was ignored because the input format is 'res'
2038 \\ ... /:depfile foo.json ...
2039 \\ ~~~~~~~~~~^~~~~~~~
2040 \\<cli>: note: the input format was inferred from the input filename
2041 \\ ... foo.res ...
2042 \\ ^~~~~~~
2043 \\
2044 );
2045 defer options.deinit();
2046
2047 try std.testing.expectEqual(.res, options.input_format);
2048 try std.testing.expectEqual(.coff, options.output_format);
2049 }
2050}
2051
15442052test "maybeAppendRC" {
15452053 var tmp = std.testing.tmpDir(.{});
15462054 defer tmp.cleanup();
15472055
15482056 var options = try testParse(&.{"foo"});
15492057 defer options.deinit();
1550 try std.testing.expectEqualStrings("foo", options.input_filename);
2058 try std.testing.expectEqualStrings("foo", options.input_source.filename);
15512059
15522060 // Create the file so that it's found. In this scenario, .rc should not get
15532061 // appended.
15542062 var file = try tmp.dir.createFile("foo", .{});
15552063 file.close();
15562064 try options.maybeAppendRC(tmp.dir);
1557 try std.testing.expectEqualStrings("foo", options.input_filename);
2065 try std.testing.expectEqualStrings("foo", options.input_source.filename);
15582066
1559 // Now delete the file and try again. Since the verbatim name is no longer found
1560 // and the input filename does not have an extension, .rc should get appended.
2067 // Now delete the file and try again. But this time change the input format
2068 // to non-rc.
15612069 try tmp.dir.deleteFile("foo");
2070 options.input_format = .res;
2071 try options.maybeAppendRC(tmp.dir);
2072 try std.testing.expectEqualStrings("foo", options.input_source.filename);
2073
2074 // Finally, reset the input format to rc. Since the verbatim name is no longer found
2075 // and the input filename does not have an extension, .rc should get appended.
2076 options.input_format = .rc;
15622077 try options.maybeAppendRC(tmp.dir);
1563 try std.testing.expectEqualStrings("foo.rc", options.input_filename);
2078 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
15642079}
lib/compiler/resinator/cvtres.zig created+1125
......@@ -0,0 +1,1125 @@
1const std = @import("std");
2const Allocator = std.mem.Allocator;
3const res = @import("res.zig");
4const NameOrOrdinal = res.NameOrOrdinal;
5const MemoryFlags = res.MemoryFlags;
6const Language = res.Language;
7const numPaddingBytesNeeded = @import("compile.zig").Compiler.numPaddingBytesNeeded;
8
9pub const Resource = struct {
10 type_value: NameOrOrdinal,
11 name_value: NameOrOrdinal,
12 data_version: u32,
13 memory_flags: MemoryFlags,
14 language: Language,
15 version: u32,
16 characteristics: u32,
17 data: []const u8,
18
19 pub fn deinit(self: Resource, allocator: Allocator) void {
20 self.name_value.deinit(allocator);
21 self.type_value.deinit(allocator);
22 allocator.free(self.data);
23 }
24
25 /// Returns true if all fields match the expected value of the resource at the
26 /// start of all .res files that distinguishes the .res file as 32-bit (as
27 /// opposed to 16-bit).
28 pub fn is32BitPreface(self: Resource) bool {
29 if (self.type_value != .ordinal or self.type_value.ordinal != 0) return false;
30 if (self.name_value != .ordinal or self.name_value.ordinal != 0) return false;
31 if (self.data_version != 0) return false;
32 if (@as(u16, @bitCast(self.memory_flags)) != 0) return false;
33 if (@as(u16, @bitCast(self.language)) != 0) return false;
34 if (self.version != 0) return false;
35 if (self.characteristics != 0) return false;
36 if (self.data.len != 0) return false;
37 return true;
38 }
39
40 pub fn isDlgInclude(resource: Resource) bool {
41 return resource.type_value == .ordinal and resource.type_value.ordinal == @intFromEnum(res.RT.DLGINCLUDE);
42 }
43};
44
45pub const ParsedResources = struct {
46 list: std.ArrayListUnmanaged(Resource) = .empty,
47 allocator: Allocator,
48
49 pub fn init(allocator: Allocator) ParsedResources {
50 return .{ .allocator = allocator };
51 }
52
53 pub fn deinit(self: *ParsedResources) void {
54 for (self.list.items) |*resource| {
55 resource.deinit(self.allocator);
56 }
57 self.list.deinit(self.allocator);
58 }
59};
60
61pub const ParseResOptions = struct {
62 skip_zero_data_resources: bool = true,
63 skip_dlginclude_resources: bool = true,
64 max_size: u64,
65};
66
67/// The returned ParsedResources should be freed by calling its `deinit` function.
68pub fn parseRes(allocator: Allocator, reader: anytype, options: ParseResOptions) !ParsedResources {
69 var resources = ParsedResources.init(allocator);
70 errdefer resources.deinit();
71
72 try parseResInto(&resources, reader, options);
73
74 return resources;
75}
76
77pub fn parseResInto(resources: *ParsedResources, reader: anytype, options: ParseResOptions) !void {
78 const allocator = resources.allocator;
79 var bytes_remaining: u64 = options.max_size;
80 {
81 const first_resource_and_size = try parseResource(allocator, reader, bytes_remaining);
82 defer first_resource_and_size.resource.deinit(allocator);
83 if (!first_resource_and_size.resource.is32BitPreface()) return error.InvalidPreface;
84 bytes_remaining -= first_resource_and_size.total_size;
85 }
86
87 while (bytes_remaining != 0) {
88 const resource_and_size = try parseResource(allocator, reader, bytes_remaining);
89 if (options.skip_zero_data_resources and resource_and_size.resource.data.len == 0) {
90 resource_and_size.resource.deinit(allocator);
91 } else if (options.skip_dlginclude_resources and resource_and_size.resource.isDlgInclude()) {
92 resource_and_size.resource.deinit(allocator);
93 } else {
94 errdefer resource_and_size.resource.deinit(allocator);
95 try resources.list.append(allocator, resource_and_size.resource);
96 }
97 bytes_remaining -= resource_and_size.total_size;
98 }
99}
100
101pub const ResourceAndSize = struct {
102 resource: Resource,
103 total_size: u64,
104};
105
106pub fn parseResource(allocator: Allocator, reader: anytype, max_size: u64) !ResourceAndSize {
107 var header_counting_reader = std.io.countingReader(reader);
108 const header_reader = header_counting_reader.reader();
109 const data_size = try header_reader.readInt(u32, .little);
110 const header_size = try header_reader.readInt(u32, .little);
111 const total_size: u64 = @as(u64, header_size) + data_size;
112 if (total_size > max_size) return error.ImpossibleSize;
113
114 var header_bytes_available = header_size -| 8;
115 var type_reader = std.io.limitedReader(header_reader, header_bytes_available);
116 const type_value = try parseNameOrOrdinal(allocator, type_reader.reader());
117 errdefer type_value.deinit(allocator);
118
119 header_bytes_available -|= @intCast(type_value.byteLen());
120 var name_reader = std.io.limitedReader(header_reader, header_bytes_available);
121 const name_value = try parseNameOrOrdinal(allocator, name_reader.reader());
122 errdefer name_value.deinit(allocator);
123
124 const padding_after_name = numPaddingBytesNeeded(@intCast(header_counting_reader.bytes_read));
125 try header_reader.skipBytes(padding_after_name, .{ .buf_size = 3 });
126
127 std.debug.assert(header_counting_reader.bytes_read % 4 == 0);
128 const data_version = try header_reader.readInt(u32, .little);
129 const memory_flags: MemoryFlags = @bitCast(try header_reader.readInt(u16, .little));
130 const language: Language = @bitCast(try header_reader.readInt(u16, .little));
131 const version = try header_reader.readInt(u32, .little);
132 const characteristics = try header_reader.readInt(u32, .little);
133
134 const header_bytes_read = header_counting_reader.bytes_read;
135 if (header_size != header_bytes_read) return error.HeaderSizeMismatch;
136
137 const data = try allocator.alloc(u8, data_size);
138 errdefer allocator.free(data);
139 try reader.readNoEof(data);
140
141 const padding_after_data = numPaddingBytesNeeded(@intCast(data_size));
142 try reader.skipBytes(padding_after_data, .{ .buf_size = 3 });
143
144 return .{
145 .resource = .{
146 .name_value = name_value,
147 .type_value = type_value,
148 .language = language,
149 .memory_flags = memory_flags,
150 .version = version,
151 .characteristics = characteristics,
152 .data_version = data_version,
153 .data = data,
154 },
155 .total_size = header_size + data.len + padding_after_data,
156 };
157}
158
159pub fn parseNameOrOrdinal(allocator: Allocator, reader: anytype) !NameOrOrdinal {
160 const first_code_unit = try reader.readInt(u16, .little);
161 if (first_code_unit == 0xFFFF) {
162 const ordinal_value = try reader.readInt(u16, .little);
163 return .{ .ordinal = ordinal_value };
164 }
165 var name_buf = try std.ArrayListUnmanaged(u16).initCapacity(allocator, 16);
166 errdefer name_buf.deinit(allocator);
167 var code_unit = first_code_unit;
168 while (code_unit != 0) {
169 try name_buf.append(allocator, std.mem.nativeToLittle(u16, code_unit));
170 code_unit = try reader.readInt(u16, .little);
171 }
172 return .{ .name = try name_buf.toOwnedSliceSentinel(allocator, 0) };
173}
174
175pub const CoffOptions = struct {
176 target: std.coff.MachineType = .X64,
177 /// If true, zeroes will be written to all timestamp fields
178 reproducible: bool = true,
179 /// If true, the MEM_WRITE flag will not be set in the .rsrc section header
180 read_only: bool = false,
181 /// If non-null, a symbol with this name and storage class EXTERNAL will be added to the symbol table.
182 define_external_symbol: ?[]const u8 = null,
183 /// Re-use data offsets for resources with data that is identical.
184 fold_duplicate_data: bool = false,
185};
186
187pub const Diagnostics = union {
188 none: void,
189 /// Contains the index of the second resource in a duplicate resource pair.
190 duplicate_resource: usize,
191 /// Contains the index of the resource that either has data that's too long or
192 /// caused the total data to overflow.
193 overflow_resource: usize,
194};
195
196pub fn writeCoff(allocator: Allocator, writer: anytype, resources: []const Resource, options: CoffOptions, diagnostics: ?*Diagnostics) !void {
197 var resource_tree = ResourceTree.init(allocator, options);
198 defer resource_tree.deinit();
199
200 for (resources, 0..) |*resource, i| {
201 resource_tree.put(resource, i) catch |err| {
202 switch (err) {
203 error.DuplicateResource => {
204 if (diagnostics) |d_ptr| d_ptr.* = .{ .duplicate_resource = i };
205 },
206 error.ResourceDataTooLong, error.TotalResourceDataTooLong => {
207 if (diagnostics) |d_ptr| d_ptr.* = .{ .overflow_resource = i };
208 },
209 else => {},
210 }
211 return err;
212 };
213 }
214
215 const lengths = resource_tree.dataLengths();
216 const byte_size_of_relocation = 10;
217 const relocations_len: u32 = @intCast(byte_size_of_relocation * resources.len);
218 const pointer_to_rsrc01_data = @sizeOf(std.coff.CoffHeader) + (@sizeOf(std.coff.SectionHeader) * 2);
219 const pointer_to_relocations = pointer_to_rsrc01_data + lengths.rsrc01;
220 const pointer_to_rsrc02_data = pointer_to_relocations + relocations_len;
221 const pointer_to_symbol_table = pointer_to_rsrc02_data + lengths.rsrc02;
222
223 const timestamp: i64 = if (options.reproducible) 0 else std.time.timestamp();
224 const size_of_optional_header = 0;
225 const machine_type: std.coff.MachineType = options.target;
226 const flags = std.coff.CoffHeaderFlags{
227 .@"32BIT_MACHINE" = 1,
228 };
229 const number_of_symbols = 5 + @as(u32, @intCast(resources.len)) + @intFromBool(options.define_external_symbol != null);
230 const coff_header = std.coff.CoffHeader{
231 .machine = machine_type,
232 .number_of_sections = 2,
233 .time_date_stamp = @as(u32, @truncate(@as(u64, @bitCast(timestamp)))),
234 .pointer_to_symbol_table = pointer_to_symbol_table,
235 .number_of_symbols = number_of_symbols,
236 .size_of_optional_header = size_of_optional_header,
237 .flags = flags,
238 };
239
240 try writer.writeStructEndian(coff_header, .little);
241
242 const rsrc01_header = std.coff.SectionHeader{
243 .name = ".rsrc$01".*,
244 .virtual_size = 0,
245 .virtual_address = 0,
246 .size_of_raw_data = lengths.rsrc01,
247 .pointer_to_raw_data = pointer_to_rsrc01_data,
248 .pointer_to_relocations = if (relocations_len != 0) pointer_to_relocations else 0,
249 .pointer_to_linenumbers = 0,
250 .number_of_relocations = @intCast(resources.len),
251 .number_of_linenumbers = 0,
252 .flags = .{
253 .CNT_INITIALIZED_DATA = 1,
254 .MEM_WRITE = @intFromBool(!options.read_only),
255 .MEM_READ = 1,
256 },
257 };
258 try writer.writeStructEndian(rsrc01_header, .little);
259
260 const rsrc02_header = std.coff.SectionHeader{
261 .name = ".rsrc$02".*,
262 .virtual_size = 0,
263 .virtual_address = 0,
264 .size_of_raw_data = lengths.rsrc02,
265 .pointer_to_raw_data = pointer_to_rsrc02_data,
266 .pointer_to_relocations = 0,
267 .pointer_to_linenumbers = 0,
268 .number_of_relocations = 0,
269 .number_of_linenumbers = 0,
270 .flags = .{
271 .CNT_INITIALIZED_DATA = 1,
272 .MEM_WRITE = @intFromBool(!options.read_only),
273 .MEM_READ = 1,
274 },
275 };
276 try writer.writeStructEndian(rsrc02_header, .little);
277
278 // TODO: test surrogate pairs
279 try resource_tree.sort();
280
281 var string_table = StringTable{};
282 defer string_table.deinit(allocator);
283 const resource_symbols = try resource_tree.writeCoff(
284 allocator,
285 writer,
286 resources,
287 lengths,
288 &string_table,
289 );
290 defer allocator.free(resource_symbols);
291
292 try writeSymbol(writer, .{
293 .name = "@feat.00".*,
294 .value = 0x11,
295 .section_number = .ABSOLUTE,
296 .type = .{
297 .base_type = .NULL,
298 .complex_type = .NULL,
299 },
300 .storage_class = .STATIC,
301 .number_of_aux_symbols = 0,
302 });
303
304 try writeSymbol(writer, .{
305 .name = ".rsrc$01".*,
306 .value = 0,
307 .section_number = @enumFromInt(1),
308 .type = .{
309 .base_type = .NULL,
310 .complex_type = .NULL,
311 },
312 .storage_class = .STATIC,
313 .number_of_aux_symbols = 1,
314 });
315 try writeSectionDefinition(writer, .{
316 .length = lengths.rsrc01,
317 .number_of_relocations = @intCast(resources.len),
318 .number_of_linenumbers = 0,
319 .checksum = 0,
320 .number = 0,
321 .selection = .NONE,
322 .unused = .{0} ** 3,
323 });
324
325 try writeSymbol(writer, .{
326 .name = ".rsrc$02".*,
327 .value = 0,
328 .section_number = @enumFromInt(2),
329 .type = .{
330 .base_type = .NULL,
331 .complex_type = .NULL,
332 },
333 .storage_class = .STATIC,
334 .number_of_aux_symbols = 1,
335 });
336 try writeSectionDefinition(writer, .{
337 .length = lengths.rsrc02,
338 .number_of_relocations = 0,
339 .number_of_linenumbers = 0,
340 .checksum = 0,
341 .number = 0,
342 .selection = .NONE,
343 .unused = .{0} ** 3,
344 });
345
346 for (resource_symbols) |resource_symbol| {
347 try writeSymbol(writer, resource_symbol);
348 }
349
350 if (options.define_external_symbol) |external_symbol_name| {
351 const name_bytes: [8]u8 = name_bytes: {
352 if (external_symbol_name.len > 8) {
353 const string_table_offset: u32 = try string_table.put(allocator, external_symbol_name);
354 var bytes = [_]u8{0} ** 8;
355 std.mem.writeInt(u32, bytes[4..8], string_table_offset, .little);
356 break :name_bytes bytes;
357 } else {
358 var symbol_shortname = [_]u8{0} ** 8;
359 @memcpy(symbol_shortname[0..external_symbol_name.len], external_symbol_name);
360 break :name_bytes symbol_shortname;
361 }
362 };
363
364 try writeSymbol(writer, .{
365 .name = name_bytes,
366 .value = 0,
367 .section_number = .ABSOLUTE,
368 .type = .{
369 .base_type = .NULL,
370 .complex_type = .NULL,
371 },
372 .storage_class = .EXTERNAL,
373 .number_of_aux_symbols = 0,
374 });
375 }
376
377 try writer.writeInt(u32, string_table.totalByteLength(), .little);
378 try writer.writeAll(string_table.bytes.items);
379}
380
381fn writeSymbol(writer: anytype, symbol: std.coff.Symbol) !void {
382 try writer.writeAll(&symbol.name);
383 try writer.writeInt(u32, symbol.value, .little);
384 try writer.writeInt(u16, @intFromEnum(symbol.section_number), .little);
385 try writer.writeInt(u8, @intFromEnum(symbol.type.base_type), .little);
386 try writer.writeInt(u8, @intFromEnum(symbol.type.complex_type), .little);
387 try writer.writeInt(u8, @intFromEnum(symbol.storage_class), .little);
388 try writer.writeInt(u8, symbol.number_of_aux_symbols, .little);
389}
390
391fn writeSectionDefinition(writer: anytype, def: std.coff.SectionDefinition) !void {
392 try writer.writeInt(u32, def.length, .little);
393 try writer.writeInt(u16, def.number_of_relocations, .little);
394 try writer.writeInt(u16, def.number_of_linenumbers, .little);
395 try writer.writeInt(u32, def.checksum, .little);
396 try writer.writeInt(u16, def.number, .little);
397 try writer.writeInt(u8, @intFromEnum(def.selection), .little);
398 try writer.writeAll(&def.unused);
399}
400
401pub const ResourceDirectoryTable = extern struct {
402 characteristics: u32,
403 timestamp: u32,
404 major_version: u16,
405 minor_version: u16,
406 number_of_name_entries: u16,
407 number_of_id_entries: u16,
408};
409
410pub const ResourceDirectoryEntry = extern struct {
411 entry: packed union {
412 name_offset: packed struct(u32) {
413 address: u31,
414 /// This is undocumented in the PE/COFF spec, but the high bit
415 /// is set by cvtres.exe for string addresses
416 to_string: bool = true,
417 },
418 integer_id: u32,
419 },
420 offset: packed struct(u32) {
421 address: u31,
422 to_subdirectory: bool,
423 },
424
425 pub fn writeCoff(self: ResourceDirectoryEntry, writer: anytype) !void {
426 try writer.writeInt(u32, @bitCast(self.entry), .little);
427 try writer.writeInt(u32, @bitCast(self.offset), .little);
428 }
429};
430
431pub const ResourceDataEntry = extern struct {
432 data_rva: u32,
433 size: u32,
434 codepage: u32,
435 reserved: u32 = 0,
436};
437
438/// type -> name -> language
439const ResourceTree = struct {
440 type_to_name_map: std.ArrayHashMapUnmanaged(NameOrOrdinal, NameToLanguageMap, NameOrOrdinalHashContext, true),
441 rsrc_string_table: std.ArrayHashMapUnmanaged(NameOrOrdinal, void, NameOrOrdinalHashContext, true),
442 deduplicated_data: std.StringArrayHashMapUnmanaged(u32),
443 data_offsets: std.ArrayListUnmanaged(u32),
444 rsrc02_len: u32,
445 coff_options: CoffOptions,
446 allocator: Allocator,
447
448 const RelocatableResource = struct {
449 resource: *const Resource,
450 original_index: usize,
451 };
452 const LanguageToResourceMap = std.AutoArrayHashMapUnmanaged(Language, RelocatableResource);
453 const NameToLanguageMap = std.ArrayHashMapUnmanaged(NameOrOrdinal, LanguageToResourceMap, NameOrOrdinalHashContext, true);
454
455 const NameOrOrdinalHashContext = struct {
456 pub fn hash(self: @This(), v: NameOrOrdinal) u32 {
457 _ = self;
458 var hasher = std.hash.Wyhash.init(0);
459 const tag = std.meta.activeTag(v);
460 hasher.update(std.mem.asBytes(&tag));
461 switch (v) {
462 .name => |name| {
463 hasher.update(std.mem.sliceAsBytes(name));
464 },
465 .ordinal => |*ordinal| {
466 hasher.update(std.mem.asBytes(ordinal));
467 },
468 }
469 return @truncate(hasher.final());
470 }
471 pub fn eql(self: @This(), a: NameOrOrdinal, b: NameOrOrdinal, b_index: usize) bool {
472 _ = self;
473 _ = b_index;
474 const tag_a = std.meta.activeTag(a);
475 const tag_b = std.meta.activeTag(b);
476 if (tag_a != tag_b) return false;
477
478 return switch (a) {
479 .name => std.mem.eql(u16, a.name, b.name),
480 .ordinal => a.ordinal == b.ordinal,
481 };
482 }
483 };
484
485 pub fn init(allocator: Allocator, coff_options: CoffOptions) ResourceTree {
486 return .{
487 .type_to_name_map = .empty,
488 .rsrc_string_table = .empty,
489 .deduplicated_data = .empty,
490 .data_offsets = .empty,
491 .rsrc02_len = 0,
492 .coff_options = coff_options,
493 .allocator = allocator,
494 };
495 }
496
497 pub fn deinit(self: *ResourceTree) void {
498 for (self.type_to_name_map.values()) |*name_to_lang_map| {
499 for (name_to_lang_map.values()) |*lang_to_resources_map| {
500 lang_to_resources_map.deinit(self.allocator);
501 }
502 name_to_lang_map.deinit(self.allocator);
503 }
504 self.type_to_name_map.deinit(self.allocator);
505 self.rsrc_string_table.deinit(self.allocator);
506 self.deduplicated_data.deinit(self.allocator);
507 self.data_offsets.deinit(self.allocator);
508 }
509
510 pub fn put(self: *ResourceTree, resource: *const Resource, original_index: usize) !void {
511 const name_to_lang_map = blk: {
512 const gop_result = try self.type_to_name_map.getOrPut(self.allocator, resource.type_value);
513 if (!gop_result.found_existing) {
514 gop_result.value_ptr.* = .empty;
515 }
516 break :blk gop_result.value_ptr;
517 };
518 const lang_to_resources_map = blk: {
519 const gop_result = try name_to_lang_map.getOrPut(self.allocator, resource.name_value);
520 if (!gop_result.found_existing) {
521 gop_result.value_ptr.* = .empty;
522 }
523 break :blk gop_result.value_ptr;
524 };
525 {
526 const gop_result = try lang_to_resources_map.getOrPut(self.allocator, resource.language);
527 if (gop_result.found_existing) return error.DuplicateResource;
528 gop_result.value_ptr.* = .{
529 .original_index = original_index,
530 .resource = resource,
531 };
532 }
533
534 // Resize the data_offsets list to accommodate the index, but only if necessary
535 try self.data_offsets.resize(self.allocator, @max(self.data_offsets.items.len, original_index + 1));
536 if (self.coff_options.fold_duplicate_data) {
537 const gop_result = try self.deduplicated_data.getOrPut(self.allocator, resource.data);
538 if (!gop_result.found_existing) {
539 gop_result.value_ptr.* = self.rsrc02_len;
540 try self.incrementRsrc02Len(resource);
541 }
542 self.data_offsets.items[original_index] = gop_result.value_ptr.*;
543 } else {
544 self.data_offsets.items[original_index] = self.rsrc02_len;
545 try self.incrementRsrc02Len(resource);
546 }
547
548 if (resource.type_value == .name and !self.rsrc_string_table.contains(resource.type_value)) {
549 try self.rsrc_string_table.putNoClobber(self.allocator, resource.type_value, {});
550 }
551 if (resource.name_value == .name and !self.rsrc_string_table.contains(resource.name_value)) {
552 try self.rsrc_string_table.putNoClobber(self.allocator, resource.name_value, {});
553 }
554 }
555
556 fn incrementRsrc02Len(self: *ResourceTree, resource: *const Resource) !void {
557 // Note: This @intCast is only safe if we assume that the resource was parsed from a .res file,
558 // since the maximum data length for a resource in the .res file format is maxInt(u32).
559 // TODO: Either codify this properly or use std.math.cast and return an error.
560 const data_len: u32 = @intCast(resource.data.len);
561 const data_len_including_padding: u32 = std.math.cast(u32, std.mem.alignForward(u33, data_len, 8)) orelse {
562 return error.ResourceDataTooLong;
563 };
564 // TODO: Verify that this corresponds to an actual PE/COFF limitation for resource data
565 // in the final linked binary. The limit may turn out to be shorter than u32 max if both
566 // the tree data and the resource data lengths together need to fit within a u32,
567 // or it may be longer in which case we would want to add more .rsrc$NN sections
568 // to the object file for the data that overflows .rsrc$02.
569 self.rsrc02_len = std.math.add(u32, self.rsrc02_len, data_len_including_padding) catch {
570 return error.TotalResourceDataTooLong;
571 };
572 }
573
574 const Lengths = struct {
575 level1: u32,
576 level2: u32,
577 level3: u32,
578 data_entries: u32,
579 strings: u32,
580 padding: u32,
581
582 rsrc01: u32,
583 rsrc02: u32,
584
585 fn stringsStart(self: Lengths) u32 {
586 return self.rsrc01 - self.strings - self.padding;
587 }
588 };
589
590 pub fn dataLengths(self: *const ResourceTree) Lengths {
591 var lengths: Lengths = .{
592 .level1 = 0,
593 .level2 = 0,
594 .level3 = 0,
595 .data_entries = 0,
596 .strings = 0,
597 .padding = 0,
598 .rsrc01 = undefined,
599 .rsrc02 = self.rsrc02_len,
600 };
601 lengths.level1 += @sizeOf(ResourceDirectoryTable);
602 for (self.type_to_name_map.values()) |name_to_lang_map| {
603 lengths.level1 += @sizeOf(ResourceDirectoryEntry);
604 lengths.level2 += @sizeOf(ResourceDirectoryTable);
605 for (name_to_lang_map.values()) |lang_to_resources_map| {
606 lengths.level2 += @sizeOf(ResourceDirectoryEntry);
607 lengths.level3 += @sizeOf(ResourceDirectoryTable);
608 for (lang_to_resources_map.values()) |_| {
609 lengths.level3 += @sizeOf(ResourceDirectoryEntry);
610 lengths.data_entries += @sizeOf(ResourceDataEntry);
611 }
612 }
613 }
614 for (self.rsrc_string_table.keys()) |v| {
615 lengths.strings += @sizeOf(u16); // string length
616 lengths.strings += @intCast(v.name.len * @sizeOf(u16));
617 }
618 lengths.rsrc01 = lengths.level1 + lengths.level2 + lengths.level3 + lengths.data_entries + lengths.strings;
619 lengths.padding = @intCast((4 -% lengths.rsrc01) % 4);
620 lengths.rsrc01 += lengths.padding;
621 return lengths;
622 }
623
624 pub fn sort(self: *ResourceTree) !void {
625 const NameOrOrdinalSortContext = struct {
626 keys: []NameOrOrdinal,
627
628 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
629 const a = ctx.keys[a_index];
630 const b = ctx.keys[b_index];
631 if (std.meta.activeTag(a) != std.meta.activeTag(b)) {
632 return if (a == .name) true else false;
633 }
634 switch (a) {
635 .name => {
636 const n = @min(a.name.len, b.name.len);
637 for (a.name[0..n], b.name[0..n]) |a_c, b_c| {
638 switch (std.math.order(std.mem.littleToNative(u16, a_c), std.mem.littleToNative(u16, b_c))) {
639 .eq => continue,
640 .lt => return true,
641 .gt => return false,
642 }
643 }
644 return a.name.len < b.name.len;
645 },
646 .ordinal => {
647 return a.ordinal < b.ordinal;
648 },
649 }
650 }
651 };
652 self.type_to_name_map.sortUnstable(NameOrOrdinalSortContext{ .keys = self.type_to_name_map.keys() });
653 for (self.type_to_name_map.values()) |*name_to_lang_map| {
654 name_to_lang_map.sortUnstable(NameOrOrdinalSortContext{ .keys = name_to_lang_map.keys() });
655 }
656 const LangSortContext = struct {
657 keys: []Language,
658
659 pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
660 return @as(u16, @bitCast(ctx.keys[a_index])) < @as(u16, @bitCast(ctx.keys[b_index]));
661 }
662 };
663 for (self.type_to_name_map.values()) |*name_to_lang_map| {
664 for (name_to_lang_map.values()) |*lang_to_resource_map| {
665 lang_to_resource_map.sortUnstable(LangSortContext{ .keys = lang_to_resource_map.keys() });
666 }
667 }
668 }
669
670 pub fn writeCoff(
671 self: *const ResourceTree,
672 allocator: Allocator,
673 writer: anytype,
674 resources_in_data_order: []const Resource,
675 lengths: Lengths,
676 coff_string_table: *StringTable,
677 ) ![]const std.coff.Symbol {
678 if (self.type_to_name_map.count() == 0) {
679 try writer.writeByteNTimes(0, 16);
680 return &.{};
681 }
682
683 var counting_writer = std.io.countingWriter(writer);
684 const w = counting_writer.writer();
685
686 var level2_list: std.ArrayListUnmanaged(*const NameToLanguageMap) = .empty;
687 defer level2_list.deinit(allocator);
688
689 var level3_list: std.ArrayListUnmanaged(*const LanguageToResourceMap) = .empty;
690 defer level3_list.deinit(allocator);
691
692 var resources_list: std.ArrayListUnmanaged(*const RelocatableResource) = .empty;
693 defer resources_list.deinit(allocator);
694
695 var relocations = Relocations.init(allocator);
696 defer relocations.deinit();
697
698 var string_offsets = try allocator.alloc(u31, self.rsrc_string_table.count());
699 const strings_start = lengths.stringsStart();
700 defer allocator.free(string_offsets);
701 {
702 var string_address: u31 = @intCast(strings_start);
703 for (self.rsrc_string_table.keys(), 0..) |v, i| {
704 string_offsets[i] = string_address;
705 string_address += @sizeOf(u16) + @as(u31, @intCast(v.name.len * @sizeOf(u16)));
706 }
707 }
708
709 const level2_start = lengths.level1;
710 var level2_address = level2_start;
711 {
712 const counts = entryTypeCounts(self.type_to_name_map.keys());
713 const table = ResourceDirectoryTable{
714 .characteristics = 0,
715 .timestamp = 0,
716 .major_version = 0,
717 .minor_version = 0,
718 .number_of_id_entries = counts.ids,
719 .number_of_name_entries = counts.names,
720 };
721 try w.writeStructEndian(table, .little);
722
723 var it = self.type_to_name_map.iterator();
724 while (it.next()) |entry| {
725 const type_value = entry.key_ptr;
726 const dir_entry = ResourceDirectoryEntry{
727 .entry = switch (type_value.*) {
728 .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(type_value.*).?] } },
729 .ordinal => .{ .integer_id = type_value.ordinal },
730 },
731 .offset = .{
732 .address = @intCast(level2_address),
733 .to_subdirectory = true,
734 },
735 };
736 try dir_entry.writeCoff(w);
737 level2_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry)));
738
739 const name_to_lang_map = entry.value_ptr;
740 try level2_list.append(allocator, name_to_lang_map);
741 }
742 }
743 std.debug.assert(counting_writer.bytes_written == level2_start);
744
745 const level3_start = level2_start + lengths.level2;
746 var level3_address = level3_start;
747 for (level2_list.items) |name_to_lang_map| {
748 const counts = entryTypeCounts(name_to_lang_map.keys());
749 const table = ResourceDirectoryTable{
750 .characteristics = 0,
751 .timestamp = 0,
752 .major_version = 0,
753 .minor_version = 0,
754 .number_of_id_entries = counts.ids,
755 .number_of_name_entries = counts.names,
756 };
757 try w.writeStructEndian(table, .little);
758
759 var it = name_to_lang_map.iterator();
760 while (it.next()) |entry| {
761 const name_value = entry.key_ptr;
762 const dir_entry = ResourceDirectoryEntry{
763 .entry = switch (name_value.*) {
764 .name => .{ .name_offset = .{ .address = string_offsets[self.rsrc_string_table.getIndex(name_value.*).?] } },
765 .ordinal => .{ .integer_id = name_value.ordinal },
766 },
767 .offset = .{
768 .address = @intCast(level3_address),
769 .to_subdirectory = true,
770 },
771 };
772 try dir_entry.writeCoff(w);
773 level3_address += @sizeOf(ResourceDirectoryTable) + @as(u32, @intCast(entry.value_ptr.count() * @sizeOf(ResourceDirectoryEntry)));
774
775 const lang_to_resources_map = entry.value_ptr;
776 try level3_list.append(allocator, lang_to_resources_map);
777 }
778 }
779 std.debug.assert(counting_writer.bytes_written == level3_start);
780
781 var reloc_addresses = try allocator.alloc(u32, resources_in_data_order.len);
782 defer allocator.free(reloc_addresses);
783
784 const data_entries_start = level3_start + lengths.level3;
785 var data_entry_address = data_entries_start;
786 for (level3_list.items) |lang_to_resources_map| {
787 const counts = EntryTypeCounts{
788 .names = 0,
789 .ids = @intCast(lang_to_resources_map.count()),
790 };
791 const table = ResourceDirectoryTable{
792 .characteristics = 0,
793 .timestamp = 0,
794 .major_version = 0,
795 .minor_version = 0,
796 .number_of_id_entries = counts.ids,
797 .number_of_name_entries = counts.names,
798 };
799 try w.writeStructEndian(table, .little);
800
801 var it = lang_to_resources_map.iterator();
802 while (it.next()) |entry| {
803 const lang = entry.key_ptr.*;
804 const dir_entry = ResourceDirectoryEntry{
805 .entry = .{ .integer_id = lang.asInt() },
806 .offset = .{
807 .address = @intCast(data_entry_address),
808 .to_subdirectory = false,
809 },
810 };
811
812 const reloc_resource = entry.value_ptr;
813 reloc_addresses[reloc_resource.original_index] = @intCast(data_entry_address);
814
815 try dir_entry.writeCoff(w);
816 data_entry_address += @sizeOf(ResourceDataEntry);
817
818 try resources_list.append(allocator, reloc_resource);
819 }
820 }
821 std.debug.assert(counting_writer.bytes_written == data_entries_start);
822
823 for (resources_list.items, 0..) |reloc_resource, i| {
824 // TODO: This logic works but is convoluted, would be good to clean this up
825 const orig_resource = &resources_in_data_order[reloc_resource.original_index];
826 const address: u32 = reloc_addresses[i];
827 try relocations.add(address, self.data_offsets.items[i]);
828 const data_entry = ResourceDataEntry{
829 .data_rva = 0, // relocation
830 .size = @intCast(orig_resource.data.len),
831 .codepage = 0,
832 };
833 try w.writeStructEndian(data_entry, .little);
834 }
835 std.debug.assert(counting_writer.bytes_written == strings_start);
836
837 for (self.rsrc_string_table.keys()) |v| {
838 const str = v.name;
839 try w.writeInt(u16, @intCast(str.len), .little);
840 try w.writeAll(std.mem.sliceAsBytes(str));
841 }
842
843 try w.writeByteNTimes(0, lengths.padding);
844
845 for (relocations.list.items) |relocation| {
846 try writeRelocation(w, std.coff.Relocation{
847 .virtual_address = relocation.relocation_address,
848 .symbol_table_index = relocation.symbol_index,
849 .type = supported_targets.rvaRelocationTypeIndicator(self.coff_options.target).?,
850 });
851 }
852
853 if (self.coff_options.fold_duplicate_data) {
854 for (self.deduplicated_data.keys()) |data| {
855 const padding_bytes: u4 = @intCast((8 -% data.len) % 8);
856 try w.writeAll(data);
857 try w.writeByteNTimes(0, padding_bytes);
858 }
859 } else {
860 for (resources_in_data_order) |resource| {
861 const padding_bytes: u4 = @intCast((8 -% resource.data.len) % 8);
862 try w.writeAll(resource.data);
863 try w.writeByteNTimes(0, padding_bytes);
864 }
865 }
866
867 var symbols = try allocator.alloc(std.coff.Symbol, resources_list.items.len);
868 errdefer allocator.free(symbols);
869
870 for (relocations.list.items, 0..) |relocation, i| {
871 // cvtres.exe writes the symbol names as $R<data offset as hexadecimal>.
872 //
873 // When the data offset would exceed 6 hex digits in cvtres.exe, it
874 // truncates the value down to 6 hex digits. This is bad behavior, since
875 // e.g. an initial resource with exactly 16 MiB of data and the
876 // resource following it would both have the symbol name $R000000.
877 //
878 // Instead, if the offset would exceed 6 hexadecimal digits,
879 // we put the longer name in the string table.
880 //
881 // Another option would be to adopt llvm-cvtres' behavior
882 // of $R000001, $R000002, etc. rather than using data offset values.
883 var name_buf: [8]u8 = undefined;
884 if (relocation.data_offset > std.math.maxInt(u24)) {
885 const name_slice = try std.fmt.allocPrint(allocator, "$R{X}", .{relocation.data_offset});
886 defer allocator.free(name_slice);
887 const string_table_offset: u32 = try coff_string_table.put(allocator, name_slice);
888 std.mem.writeInt(u32, name_buf[0..4], 0, .little);
889 std.mem.writeInt(u32, name_buf[4..8], string_table_offset, .little);
890 } else {
891 const name_slice = std.fmt.bufPrint(&name_buf, "$R{X:0>6}", .{relocation.data_offset}) catch unreachable;
892 std.debug.assert(name_slice.len == 8);
893 }
894
895 symbols[i] = .{
896 .name = name_buf,
897 .value = relocation.data_offset,
898 .section_number = @enumFromInt(2),
899 .type = .{
900 .base_type = .NULL,
901 .complex_type = .NULL,
902 },
903 .storage_class = .STATIC,
904 .number_of_aux_symbols = 0,
905 };
906 }
907
908 return symbols;
909 }
910
911 fn writeRelocation(writer: anytype, relocation: std.coff.Relocation) !void {
912 try writer.writeInt(u32, relocation.virtual_address, .little);
913 try writer.writeInt(u32, relocation.symbol_table_index, .little);
914 try writer.writeInt(u16, relocation.type, .little);
915 }
916
917 const EntryTypeCounts = struct {
918 names: u16,
919 ids: u16,
920 };
921
922 fn entryTypeCounts(s: []const NameOrOrdinal) EntryTypeCounts {
923 var names: u16 = 0;
924 var ordinals: u16 = 0;
925 for (s) |v| {
926 switch (v) {
927 .name => names += 1,
928 .ordinal => ordinals += 1,
929 }
930 }
931 return .{ .names = names, .ids = ordinals };
932 }
933};
934
935const Relocation = struct {
936 symbol_index: u32,
937 data_offset: u32,
938 relocation_address: u32,
939};
940
941const Relocations = struct {
942 allocator: Allocator,
943 list: std.ArrayListUnmanaged(Relocation) = .empty,
944 cur_symbol_index: u32 = 5,
945
946 pub fn init(allocator: Allocator) Relocations {
947 return .{ .allocator = allocator };
948 }
949
950 pub fn deinit(self: *Relocations) void {
951 self.list.deinit(self.allocator);
952 }
953
954 pub fn add(self: *Relocations, relocation_address: u32, data_offset: u32) !void {
955 try self.list.append(self.allocator, .{
956 .symbol_index = self.cur_symbol_index,
957 .data_offset = data_offset,
958 .relocation_address = relocation_address,
959 });
960 self.cur_symbol_index += 1;
961 }
962};
963
964/// Does not do deduplication (only because there's no chance of duplicate strings in this
965/// instance).
966const StringTable = struct {
967 bytes: std.ArrayListUnmanaged(u8) = .empty,
968
969 pub fn deinit(self: *StringTable, allocator: Allocator) void {
970 self.bytes.deinit(allocator);
971 }
972
973 /// Returns the byte offset of the string in the string table
974 pub fn put(self: *StringTable, allocator: Allocator, string: []const u8) !u32 {
975 const null_terminated_len = string.len + 1;
976 const start_offset = self.totalByteLength();
977 if (start_offset + null_terminated_len > std.math.maxInt(u32)) {
978 return error.StringTableOverflow;
979 }
980 try self.bytes.ensureUnusedCapacity(allocator, null_terminated_len);
981 self.bytes.appendSliceAssumeCapacity(string);
982 self.bytes.appendAssumeCapacity(0);
983 return start_offset;
984 }
985
986 /// Returns the total byte count of the string table, including the byte count of the size field
987 pub fn totalByteLength(self: StringTable) u32 {
988 return @intCast(4 + self.bytes.items.len);
989 }
990};
991
992pub const supported_targets = struct {
993 /// Enum containing a mixture of names that come from:
994 /// - Machine Types constants in the PE format spec:
995 /// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
996 /// - cvtres.exe /machine options
997 /// - Zig/LLVM arch names
998 /// All field names are lowercase regardless of their casing used in the above origins.
999 pub const Arch = enum {
1000 // cvtres.exe /machine names
1001 x64,
1002 x86,
1003 /// Note: Following cvtres.exe's lead, this corresponds to ARMNT, not ARM
1004 arm,
1005 arm64,
1006 arm64ec,
1007 arm64x,
1008 ia64,
1009 ebc,
1010
1011 // PE/COFF MACHINE constant names not covered above
1012 amd64,
1013 i386,
1014 armnt,
1015
1016 // Zig/LLVM names not already covered above
1017 x86_64,
1018 aarch64,
1019
1020 pub fn toCoffMachineType(arch: Arch) std.coff.MachineType {
1021 return switch (arch) {
1022 .x64, .amd64, .x86_64 => .X64,
1023 .x86, .i386 => .I386,
1024 .arm, .armnt => .ARMNT,
1025 .arm64, .aarch64 => .ARM64,
1026 .arm64ec => .ARM64EC,
1027 .arm64x => .ARM64X,
1028 .ia64 => .IA64,
1029 .ebc => .EBC,
1030 };
1031 }
1032
1033 pub fn description(arch: Arch) []const u8 {
1034 return switch (arch) {
1035 .x64, .amd64, .x86_64 => "64-bit X86",
1036 .x86, .i386 => "32-bit X86",
1037 .arm, .armnt => "ARM Thumb-2 little endian",
1038 .arm64, .aarch64 => "ARM64/AArch64 little endian",
1039 .arm64ec => "ARM64 \"Emulation Compatible\"",
1040 .arm64x => "ARM64 and ARM64EC together",
1041 .ia64 => "64-bit Intel Itanium",
1042 .ebc => "EFI Byte Code",
1043 };
1044 }
1045
1046 pub const ordered_for_display: []const Arch = &.{
1047 .x64,
1048 .x86_64,
1049 .amd64,
1050 .x86,
1051 .i386,
1052 .arm64,
1053 .aarch64,
1054 .arm,
1055 .armnt,
1056 .arm64ec,
1057 .arm64x,
1058 .ia64,
1059 .ebc,
1060 };
1061 comptime {
1062 for (@typeInfo(Arch).@"enum".fields) |enum_field| {
1063 _ = std.mem.indexOfScalar(Arch, ordered_for_display, @enumFromInt(enum_field.value)) orelse {
1064 @compileError(std.fmt.comptimePrint("'{s}' missing from ordered_for_display", .{enum_field.name}));
1065 };
1066 }
1067 }
1068
1069 pub const longest_name = blk: {
1070 var len = 0;
1071 for (@typeInfo(Arch).@"enum".fields) |field| {
1072 if (field.name.len > len) len = field.name.len;
1073 }
1074 break :blk len;
1075 };
1076
1077 pub fn fromStringIgnoreCase(str: []const u8) ?Arch {
1078 if (str.len > longest_name) return null;
1079 var lower_buf: [longest_name]u8 = undefined;
1080 const lower = std.ascii.lowerString(&lower_buf, str);
1081 return std.meta.stringToEnum(Arch, lower);
1082 }
1083
1084 test fromStringIgnoreCase {
1085 try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("x64").?);
1086 try std.testing.expectEqual(.x64, Arch.fromStringIgnoreCase("X64").?);
1087 try std.testing.expectEqual(.aarch64, Arch.fromStringIgnoreCase("Aarch64").?);
1088 try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("armzzz"));
1089 try std.testing.expectEqual(null, Arch.fromStringIgnoreCase("long string that is longer than any field"));
1090 }
1091 };
1092
1093 // https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#type-indicators
1094 pub fn rvaRelocationTypeIndicator(target: std.coff.MachineType) ?u16 {
1095 return switch (target) {
1096 .X64 => 0x3, // IMAGE_REL_AMD64_ADDR32NB
1097 .I386 => 0x7, // IMAGE_REL_I386_DIR32NB
1098 .ARMNT => 0x2, // IMAGE_REL_ARM_ADDR32NB
1099 .ARM64, .ARM64EC, .ARM64X => 0x2, // IMAGE_REL_ARM64_ADDR32NB
1100 .IA64 => 0x10, // IMAGE_REL_IA64_DIR32NB
1101 .EBC => 0x1, // This is what cvtres.exe writes for this target, unsure where it comes from
1102 else => null,
1103 };
1104 }
1105
1106 pub fn isSupported(target: std.coff.MachineType) bool {
1107 return rvaRelocationTypeIndicator(target) != null;
1108 }
1109
1110 comptime {
1111 // Enforce two things:
1112 // 1. Arch enum field names are all lowercase (necessary for how fromStringIgnoreCase is implemented)
1113 // 2. All enum fields in Arch have an associated RVA relocation type when converted to a coff.MachineType
1114 for (@typeInfo(Arch).@"enum".fields) |enum_field| {
1115 const all_lower = all_lower: for (enum_field.name) |c| {
1116 if (std.ascii.isUpper(c)) break :all_lower false;
1117 } else break :all_lower true;
1118 if (!all_lower) @compileError(std.fmt.comptimePrint("Arch field is not all lowercase: {s}", .{enum_field.name}));
1119 const coff_machine = @field(Arch, enum_field.name).toCoffMachineType();
1120 _ = rvaRelocationTypeIndicator(coff_machine) orelse {
1121 @compileError(std.fmt.comptimePrint("No RVA relocation for Arch: {s}", .{enum_field.name}));
1122 };
1123 }
1124 }
1125};
lib/compiler/resinator/main.zig+311-93
......@@ -7,7 +7,10 @@ const Diagnostics = @import("errors.zig").Diagnostics;
77const cli = @import("cli.zig");
88const preprocess = @import("preprocess.zig");
99const renderErrorMessage = @import("utils.zig").renderErrorMessage;
10const openFileNotDir = @import("utils.zig").openFileNotDir;
11const cvtres = @import("cvtres.zig");
1012const hasDisjointCodePage = @import("disjoint_code_page.zig").hasDisjointCodePage;
13const fmtResourceType = @import("res.zig").NameOrOrdinal.fmtResourceType;
1114const aro = @import("aro");
1215
1316pub fn main() !void {
......@@ -135,7 +138,10 @@ pub fn main() !void {
135138
136139 try argv.append("arocc"); // dummy command name
137140 try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths);
138 try argv.append(options.input_filename);
141 try argv.append(switch (options.input_source) {
142 .stdio => "-",
143 .filename => |filename| filename,
144 });
139145
140146 if (options.verbose) {
141147 try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n");
......@@ -164,120 +170,332 @@ pub fn main() !void {
164170
165171 break :full_input try preprocessed_buf.toOwnedSlice();
166172 } else {
167 break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| {
168 try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
169 std.process.exit(1);
170 };
173 switch (options.input_source) {
174 .stdio => |file| {
175 break :full_input file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch |err| {
176 try error_handler.emitMessage(allocator, .err, "unable to read input from stdin: {s}", .{@errorName(err)});
177 std.process.exit(1);
178 };
179 },
180 .filename => |input_filename| {
181 break :full_input std.fs.cwd().readFileAlloc(allocator, input_filename, std.math.maxInt(usize)) catch |err| {
182 try error_handler.emitMessage(allocator, .err, "unable to read input file path '{s}': {s}", .{ input_filename, @errorName(err) });
183 std.process.exit(1);
184 };
185 },
186 }
171187 }
172188 };
173189 defer allocator.free(full_input);
174190
175191 if (options.preprocess == .only) {
176 try std.fs.cwd().writeFile(.{ .sub_path = options.output_filename, .data = full_input });
192 switch (options.output_source) {
193 .stdio => |output_file| {
194 try output_file.writeAll(full_input);
195 },
196 .filename => |output_filename| {
197 try std.fs.cwd().writeFile(.{ .sub_path = output_filename, .data = full_input });
198 },
199 }
177200 return;
178201 }
179202
180 // Note: We still want to run this when no-preprocess is set because:
181 // 1. We want to print accurate line numbers after removing multiline comments
182 // 2. We want to be able to handle an already-preprocessed input with #line commands in it
183 var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename }) catch |err| switch (err) {
184 error.InvalidLineCommand => {
185 // TODO: Maybe output the invalid line command
186 try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{});
187 if (options.preprocess == .no) {
188 try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line <num> \"<path>\"", .{});
189 } else {
190 try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{});
203 var resources = resources: {
204 const need_intermediate_res = options.output_format == .coff and options.input_format != .res;
205 var res_stream = if (need_intermediate_res)
206 IoStream{
207 .name = "<in-memory intermediate res>",
208 .intermediate = true,
209 .source = .{ .memory = .empty },
191210 }
192 std.process.exit(1);
193 },
194 error.LineNumberOverflow => {
195 // TODO: Better error message
196 try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)});
197 std.process.exit(1);
198 },
199 error.OutOfMemory => |e| return e,
200 };
201 defer mapping_results.mappings.deinit(allocator);
211 else if (options.input_format == .res)
212 IoStream.fromIoSource(options.input_source, .input) catch |err| {
213 try error_handler.emitMessage(allocator, .err, "unable to read res file path '{s}': {s}", .{ options.input_source.filename, @errorName(err) });
214 std.process.exit(1);
215 }
216 else
217 IoStream.fromIoSource(options.output_source, .output) catch |err| {
218 try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) });
219 std.process.exit(1);
220 };
221 defer res_stream.deinit(allocator);
222
223 const res_data = res_data: {
224 if (options.input_format != .res) {
225 // Note: We still want to run this when no-preprocess is set because:
226 // 1. We want to print accurate line numbers after removing multiline comments
227 // 2. We want to be able to handle an already-preprocessed input with #line commands in it
228 var mapping_results = parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_source.filename }) catch |err| switch (err) {
229 error.InvalidLineCommand => {
230 // TODO: Maybe output the invalid line command
231 try error_handler.emitMessage(allocator, .err, "invalid line command in the preprocessed source", .{});
232 if (options.preprocess == .no) {
233 try error_handler.emitMessage(allocator, .note, "line commands must be of the format: #line <num> \"<path>\"", .{});
234 } else {
235 try error_handler.emitMessage(allocator, .note, "this is likely to be a bug, please report it", .{});
236 }
237 std.process.exit(1);
238 },
239 error.LineNumberOverflow => {
240 // TODO: Better error message
241 try error_handler.emitMessage(allocator, .err, "line number count exceeded maximum of {}", .{std.math.maxInt(usize)});
242 std.process.exit(1);
243 },
244 error.OutOfMemory => |e| return e,
245 };
246 defer mapping_results.mappings.deinit(allocator);
247
248 const default_code_page = options.default_code_page orelse .windows1252;
249 const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page);
250
251 const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
252
253 var diagnostics = Diagnostics.init(allocator);
254 defer diagnostics.deinit();
255
256 const res_stream_writer = res_stream.source.writer(allocator);
257 var output_buffered_stream = std.io.bufferedWriter(res_stream_writer);
258
259 compile(allocator, final_input, output_buffered_stream.writer(), .{
260 .cwd = std.fs.cwd(),
261 .diagnostics = &diagnostics,
262 .source_mappings = &mapping_results.mappings,
263 .dependencies_list = maybe_dependencies_list,
264 .ignore_include_env_var = options.ignore_include_env_var,
265 .extra_include_paths = options.extra_include_paths.items,
266 .system_include_paths = include_paths,
267 .default_language_id = options.default_language_id,
268 .default_code_page = default_code_page,
269 .disjoint_code_page = has_disjoint_code_page,
270 .verbose = options.verbose,
271 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
272 .max_string_literal_codepoints = options.max_string_literal_codepoints,
273 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
274 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
275 }) catch |err| switch (err) {
276 error.ParseError, error.CompileError => {
277 try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings);
278 // Delete the output file on error
279 res_stream.cleanupAfterError();
280 std.process.exit(1);
281 },
282 else => |e| return e,
283 };
202284
203 const default_code_page = options.default_code_page orelse .windows1252;
204 const has_disjoint_code_page = hasDisjointCodePage(mapping_results.result, &mapping_results.mappings, default_code_page);
285 try output_buffered_stream.flush();
205286
206 const final_input = try removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
287 // print any warnings/notes
288 if (!zig_integration) {
289 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
290 }
207291
208 var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
209 try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
292 // write the depfile
293 if (options.depfile_path) |depfile_path| {
294 var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| {
295 try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
296 std.process.exit(1);
297 };
298 defer depfile.close();
299
300 const depfile_writer = depfile.writer();
301 var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer);
302 switch (options.depfile_fmt) {
303 .json => {
304 var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 });
305 defer write_stream.deinit();
306
307 try write_stream.beginArray();
308 for (dependencies_list.items) |dep_path| {
309 try write_stream.write(dep_path);
310 }
311 try write_stream.endArray();
312 },
313 }
314 try depfile_buffered_writer.flush();
315 }
316 }
317
318 if (options.output_format != .coff) return;
319
320 break :res_data res_stream.source.readAll(allocator) catch |err| {
321 try error_handler.emitMessage(allocator, .err, "unable to read res from '{s}': {s}", .{ res_stream.name, @errorName(err) });
322 std.process.exit(1);
323 };
324 };
325 // No need to keep the res_data around after parsing the resources from it
326 defer res_data.deinit(allocator);
327
328 std.debug.assert(options.output_format == .coff);
329
330 // TODO: Maybe use a buffered file reader instead of reading file into memory -> fbs
331 var fbs = std.io.fixedBufferStream(res_data.bytes);
332 break :resources cvtres.parseRes(allocator, fbs.reader(), .{ .max_size = res_data.bytes.len }) catch |err| {
333 // TODO: Better errors
334 try error_handler.emitMessage(allocator, .err, "unable to parse res from '{s}': {s}", .{ res_stream.name, @errorName(err) });
335 std.process.exit(1);
336 };
337 };
338 defer resources.deinit();
339
340 var coff_stream = IoStream.fromIoSource(options.output_source, .output) catch |err| {
341 try error_handler.emitMessage(allocator, .err, "unable to create output file '{s}': {s}", .{ options.output_source.filename, @errorName(err) });
210342 std.process.exit(1);
211343 };
212 var output_file_closed = false;
213 defer if (!output_file_closed) output_file.close();
214
215 var diagnostics = Diagnostics.init(allocator);
216 defer diagnostics.deinit();
217
218 var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
219
220 compile(allocator, final_input, output_buffered_stream.writer(), .{
221 .cwd = std.fs.cwd(),
222 .diagnostics = &diagnostics,
223 .source_mappings = &mapping_results.mappings,
224 .dependencies_list = maybe_dependencies_list,
225 .ignore_include_env_var = options.ignore_include_env_var,
226 .extra_include_paths = options.extra_include_paths.items,
227 .system_include_paths = include_paths,
228 .default_language_id = options.default_language_id,
229 .default_code_page = default_code_page,
230 .disjoint_code_page = has_disjoint_code_page,
231 .verbose = options.verbose,
232 .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
233 .max_string_literal_codepoints = options.max_string_literal_codepoints,
234 .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
235 .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
236 }) catch |err| switch (err) {
237 error.ParseError, error.CompileError => {
238 try error_handler.emitDiagnostics(allocator, std.fs.cwd(), final_input, &diagnostics, mapping_results.mappings);
239 // Delete the output file on error
240 output_file.close();
241 output_file_closed = true;
242 // Failing to delete is not really a big deal, so swallow any errors
243 std.fs.cwd().deleteFile(options.output_filename) catch {};
244 std.process.exit(1);
245 },
246 else => |e| return e,
344 defer coff_stream.deinit(allocator);
345
346 var coff_output_buffered_stream = std.io.bufferedWriter(coff_stream.source.writer(allocator));
347
348 var cvtres_diagnostics: cvtres.Diagnostics = .{ .none = {} };
349 cvtres.writeCoff(allocator, coff_output_buffered_stream.writer(), resources.list.items, options.coff_options, &cvtres_diagnostics) catch |err| {
350 switch (err) {
351 error.DuplicateResource => {
352 const duplicate_resource = resources.list.items[cvtres_diagnostics.duplicate_resource];
353 try error_handler.emitMessage(allocator, .err, "duplicate resource [id: {}, type: {}, language: {}]", .{
354 duplicate_resource.name_value,
355 fmtResourceType(duplicate_resource.type_value),
356 duplicate_resource.language,
357 });
358 },
359 error.ResourceDataTooLong => {
360 const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource];
361 try error_handler.emitMessage(allocator, .err, "resource has a data length that is too large to be written into a coff section", .{});
362 try error_handler.emitMessage(allocator, .note, "the resource with the invalid size is [id: {}, type: {}, language: {}]", .{
363 overflow_resource.name_value,
364 fmtResourceType(overflow_resource.type_value),
365 overflow_resource.language,
366 });
367 },
368 error.TotalResourceDataTooLong => {
369 const overflow_resource = resources.list.items[cvtres_diagnostics.duplicate_resource];
370 try error_handler.emitMessage(allocator, .err, "total resource data exceeds the maximum of the coff 'size of raw data' field", .{});
371 try error_handler.emitMessage(allocator, .note, "size overflow occurred when attempting to write this resource: [id: {}, type: {}, language: {}]", .{
372 overflow_resource.name_value,
373 fmtResourceType(overflow_resource.type_value),
374 overflow_resource.language,
375 });
376 },
377 else => {
378 try error_handler.emitMessage(allocator, .err, "unable to write coff output file '{s}': {s}", .{ coff_stream.name, @errorName(err) });
379 },
380 }
381 // Delete the output file on error
382 coff_stream.cleanupAfterError();
383 std.process.exit(1);
247384 };
248385
249 try output_buffered_stream.flush();
386 try coff_output_buffered_stream.flush();
387}
250388
251 // print any warnings/notes
252 if (!zig_integration) {
253 diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
254 }
389const IoStream = struct {
390 name: []const u8,
391 intermediate: bool,
392 source: Source,
255393
256 // write the depfile
257 if (options.depfile_path) |depfile_path| {
258 var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| {
259 try error_handler.emitMessage(allocator, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
260 std.process.exit(1);
394 pub const IoDirection = enum { input, output };
395
396 pub fn fromIoSource(source: cli.Options.IoSource, io: IoDirection) !IoStream {
397 return .{
398 .name = switch (source) {
399 .filename => |filename| filename,
400 .stdio => switch (io) {
401 .input => "<stdin>",
402 .output => "<stdout>",
403 },
404 },
405 .intermediate = false,
406 .source = try Source.fromIoSource(source, io),
261407 };
262 defer depfile.close();
263
264 const depfile_writer = depfile.writer();
265 var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer);
266 switch (options.depfile_fmt) {
267 .json => {
268 var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 });
269 defer write_stream.deinit();
270
271 try write_stream.beginArray();
272 for (dependencies_list.items) |dep_path| {
273 try write_stream.write(dep_path);
274 }
275 try write_stream.endArray();
408 }
409
410 pub fn deinit(self: *IoStream, allocator: std.mem.Allocator) void {
411 self.source.deinit(allocator);
412 }
413
414 pub fn cleanupAfterError(self: *IoStream) void {
415 switch (self.source) {
416 .file => |file| {
417 // Delete the output file on error
418 file.close();
419 // Failing to delete is not really a big deal, so swallow any errors
420 std.fs.cwd().deleteFile(self.name) catch {};
276421 },
422 .stdio, .memory, .closed => return,
277423 }
278 try depfile_buffered_writer.flush();
279424 }
280}
425
426 pub const Source = union(enum) {
427 file: std.fs.File,
428 stdio: std.fs.File,
429 memory: std.ArrayListUnmanaged(u8),
430 /// The source has been closed and any usage of the Source in this state is illegal (except deinit).
431 closed: void,
432
433 pub fn fromIoSource(source: cli.Options.IoSource, io: IoDirection) !Source {
434 switch (source) {
435 .filename => |filename| return .{
436 .file = switch (io) {
437 .input => try openFileNotDir(std.fs.cwd(), filename, .{}),
438 .output => try std.fs.cwd().createFile(filename, .{}),
439 },
440 },
441 .stdio => |file| return .{ .stdio = file },
442 }
443 }
444
445 pub fn deinit(self: *Source, allocator: std.mem.Allocator) void {
446 switch (self.*) {
447 .file => |file| file.close(),
448 .stdio => {},
449 .memory => |*list| list.deinit(allocator),
450 .closed => {},
451 }
452 }
453
454 pub const Data = struct {
455 bytes: []const u8,
456 needs_free: bool,
457
458 pub fn deinit(self: Data, allocator: std.mem.Allocator) void {
459 if (self.needs_free) {
460 allocator.free(self.bytes);
461 }
462 }
463 };
464
465 pub fn readAll(self: Source, allocator: std.mem.Allocator) !Data {
466 return switch (self) {
467 inline .file, .stdio => |file| .{
468 .bytes = try file.readToEndAlloc(allocator, std.math.maxInt(usize)),
469 .needs_free = true,
470 },
471 .memory => |list| .{ .bytes = list.items, .needs_free = false },
472 .closed => unreachable,
473 };
474 }
475
476 pub const WriterContext = struct {
477 self: *Source,
478 allocator: std.mem.Allocator,
479 };
480 pub const WriteError = std.mem.Allocator.Error || std.fs.File.WriteError;
481 pub const Writer = std.io.Writer(WriterContext, WriteError, write);
482
483 pub fn write(ctx: WriterContext, bytes: []const u8) WriteError!usize {
484 switch (ctx.self.*) {
485 inline .file, .stdio => |file| return file.write(bytes),
486 .memory => |*list| {
487 try list.appendSlice(ctx.allocator, bytes);
488 return bytes.len;
489 },
490 .closed => unreachable,
491 }
492 }
493
494 pub fn writer(self: *Source, allocator: std.mem.Allocator) Writer {
495 return .{ .context = .{ .self = self, .allocator = allocator } };
496 }
497 };
498};
281499
282500fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 {
283501 var includes = auto_includes_option;