master
1const std = @import("std");
2const code_pages = @import("code_pages.zig");
3const SupportedCodePage = code_pages.SupportedCodePage;
4const lang = @import("lang.zig");
5const res = @import("res.zig");
6const Allocator = std.mem.Allocator;
7const lex = @import("lex.zig");
8const cvtres = @import("cvtres.zig");
9
10/// This is what /SL 100 will set the maximum string literal length to
11pub const max_string_literal_length_100_percent = 8192;
12
13pub const usage_string_after_command_name =
14 \\ [options] [--] <INPUT> [<OUTPUT>]
15 \\
16 \\The sequence -- can be used to signify when to stop parsing options.
17 \\This is necessary when the input path begins with a forward slash.
18 \\
19 \\Supported option prefixes are /, -, and --, so e.g. /h, -h, and --h all work.
20 \\Drop-in compatible with the Microsoft Resource Compiler.
21 \\
22 \\Supported Win32 RC Options:
23 \\ /?, /h Print this help and exit.
24 \\ /v Verbose (print progress messages).
25 \\ /d <name>[=<value>] Define a symbol (during preprocessing).
26 \\ /u <name> Undefine a symbol (during preprocessing).
27 \\ /fo <value> Specify output file path.
28 \\ /l <value> Set default language using hexadecimal id (ex: 409).
29 \\ /ln <value> Set default language using language name (ex: en-us).
30 \\ /i <value> Add an include path.
31 \\ /x Ignore INCLUDE environment variable.
32 \\ /c <value> Set default code page (ex: 65001).
33 \\ /w Warn on invalid code page in .rc (instead of error).
34 \\ /y Suppress warnings for duplicate control IDs.
35 \\ /n Null-terminate all strings in string tables.
36 \\ /sl <value> Specify string literal length limit in percentage (1-100)
37 \\ where 100 corresponds to a limit of 8192. If the /sl
38 \\ option is not specified, the default limit is 4097.
39 \\ /p Only run the preprocessor and output a .rcpp file.
40 \\
41 \\No-op Win32 RC Options:
42 \\ /nologo, /a, /r Options that are recognized but do nothing.
43 \\
44 \\Unsupported Win32 RC Options:
45 \\ /fm, /q, /g, /gn, /g1, /g2 Unsupported MUI-related options.
46 \\ /?c, /hc, /t, /tp:<prefix>, Unsupported LCX/LCE-related options.
47 \\ /tn, /tm, /tc, /tw, /te,
48 \\ /ti, /ta
49 \\ /z Unsupported font-substitution-related option.
50 \\ /s Unsupported HWB-related option.
51 \\
52 \\Custom Options (resinator-specific):
53 \\ /:no-preprocess Do not run the preprocessor.
54 \\ /:debug Output the preprocessed .rc file and the parsed AST.
55 \\ /:auto-includes <value> Set the automatic include path detection behavior.
56 \\ any (default) Use MSVC if available, fall back to MinGW
57 \\ msvc Use MSVC include paths (must be present on the system)
58 \\ gnu Use MinGW include paths
59 \\ none Do not use any autodetected include paths
60 \\ /:depfile <path> Output a file containing a list of all the files that
61 \\ the .rc includes or otherwise depends on.
62 \\ /:depfile-fmt <value> Output format of the depfile, if /:depfile is set.
63 \\ json (default) A top-level JSON array of paths
64 \\ /:input-format <value> If not specified, the input format is inferred.
65 \\ rc (default if input format cannot be inferred)
66 \\ res Compiled .rc file, implies /:output-format coff
67 \\ rcpp Preprocessed .rc file, implies /:no-preprocess
68 \\ /:output-format <value> If not specified, the output format is inferred.
69 \\ res (default if output format cannot be inferred)
70 \\ coff COFF object file (extension: .obj or .o)
71 \\ rcpp Preprocessed .rc file, implies /p
72 \\ /:target <arch> Set the target machine for COFF object files.
73 \\ Can be specified either as PE/COFF machine constant
74 \\ name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64,
75 \\ aarch64, etc). The default is X64 (aka x86_64).
76 \\ Also accepts a full Zig/LLVM triple, but everything
77 \\ except the architecture is ignored.
78 \\
79 \\Note: For compatibility reasons, all custom options start with :
80 \\
81;
82
83pub fn writeUsage(writer: *std.Io.Writer, command_name: []const u8) !void {
84 try writer.writeAll("Usage: ");
85 try writer.writeAll(command_name);
86 try writer.writeAll(usage_string_after_command_name);
87}
88
89pub const Diagnostics = struct {
90 errors: std.ArrayList(ErrorDetails) = .empty,
91 allocator: Allocator,
92
93 pub const ErrorDetails = struct {
94 arg_index: usize,
95 arg_span: ArgSpan = .{},
96 msg: std.ArrayList(u8) = .empty,
97 type: Type = .err,
98 print_args: bool = true,
99
100 pub const Type = enum { err, warning, note };
101 pub const ArgSpan = struct {
102 point_at_next_arg: bool = false,
103 name_offset: usize = 0,
104 prefix_len: usize = 0,
105 value_offset: usize = 0,
106 name_len: usize = 0,
107 };
108 };
109
110 pub fn init(allocator: Allocator) Diagnostics {
111 return .{
112 .allocator = allocator,
113 };
114 }
115
116 pub fn deinit(self: *Diagnostics) void {
117 for (self.errors.items) |*details| {
118 details.msg.deinit(self.allocator);
119 }
120 self.errors.deinit(self.allocator);
121 }
122
123 pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
124 try self.errors.append(self.allocator, error_details);
125 }
126
127 pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8) void {
128 const stderr, const ttyconf = std.debug.lockStderrWriter(&.{});
129 defer std.debug.unlockStderrWriter();
130 self.renderToWriter(args, stderr, ttyconf) catch return;
131 }
132
133 pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.Io.Writer, config: std.Io.tty.Config) !void {
134 for (self.errors.items) |err_details| {
135 try renderErrorMessage(writer, config, err_details, args);
136 }
137 }
138
139 pub fn hasError(self: *const Diagnostics) bool {
140 for (self.errors.items) |err| {
141 if (err.type == .err) return true;
142 }
143 return false;
144 }
145};
146
147pub const Options = struct {
148 allocator: Allocator,
149 input_source: IoSource = .{ .filename = &[_]u8{} },
150 output_source: IoSource = .{ .filename = &[_]u8{} },
151 extra_include_paths: std.ArrayList([]const u8) = .empty,
152 ignore_include_env_var: bool = false,
153 preprocess: Preprocess = .yes,
154 default_language_id: ?u16 = null,
155 default_code_page: ?SupportedCodePage = null,
156 verbose: bool = false,
157 symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .empty,
158 null_terminate_string_table_strings: bool = false,
159 max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
160 silent_duplicate_control_ids: bool = false,
161 warn_instead_of_error_on_invalid_code_page: bool = false,
162 debug: bool = false,
163 print_help_and_exit: bool = false,
164 auto_includes: AutoIncludes = .any,
165 depfile_path: ?[]const u8 = null,
166 depfile_fmt: DepfileFormat = .json,
167 input_format: InputFormat = .rc,
168 output_format: OutputFormat = .res,
169 coff_options: cvtres.CoffOptions = .{},
170
171 pub const IoSource = union(enum) {
172 stdio: std.fs.File,
173 filename: []const u8,
174 };
175 pub const AutoIncludes = enum { any, msvc, gnu, none };
176 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 };
191 pub const Preprocess = enum { no, yes, only };
192 pub const SymbolAction = enum { define, undefine };
193 pub const SymbolValue = union(SymbolAction) {
194 define: []const u8,
195 undefine: void,
196
197 pub fn deinit(self: SymbolValue, allocator: Allocator) void {
198 switch (self) {
199 .define => |value| allocator.free(value),
200 .undefine => {},
201 }
202 }
203 };
204
205 /// Does not check that identifier contains only valid characters
206 pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void {
207 if (self.symbols.getPtr(identifier)) |val_ptr| {
208 // If the symbol is undefined, then that always takes precedence so
209 // we shouldn't change anything.
210 if (val_ptr.* == .undefine) return;
211 // Otherwise, the new value takes precedence.
212 const duped_value = try self.allocator.dupe(u8, value);
213 errdefer self.allocator.free(duped_value);
214 val_ptr.deinit(self.allocator);
215 val_ptr.* = .{ .define = duped_value };
216 return;
217 }
218 const duped_key = try self.allocator.dupe(u8, identifier);
219 errdefer self.allocator.free(duped_key);
220 const duped_value = try self.allocator.dupe(u8, value);
221 errdefer self.allocator.free(duped_value);
222 try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value });
223 }
224
225 /// Does not check that identifier contains only valid characters
226 pub fn undefine(self: *Options, identifier: []const u8) !void {
227 if (self.symbols.getPtr(identifier)) |action| {
228 action.deinit(self.allocator);
229 action.* = .{ .undefine = {} };
230 return;
231 }
232 const duped_key = try self.allocator.dupe(u8, identifier);
233 errdefer self.allocator.free(duped_key);
234 try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} });
235 }
236
237 /// If the current input filename:
238 /// - does not have an extension, and
239 /// - does not exist in the cwd, and
240 /// - the input format is .rc
241 /// then this function will append `.rc` to the input filename
242 ///
243 /// Note: This behavior is different from the Win32 compiler.
244 /// It always appends .RC if the filename does not have
245 /// a `.` in it and it does not even try the verbatim name
246 /// in that scenario.
247 ///
248 /// The approach taken here is meant to give us a 'best of both
249 /// worlds' situation where we'll be compatible with most use-cases
250 /// of the .rc extension being omitted from the CLI args, but still
251 /// work fine if the file itself does not have an extension.
252 pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void {
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) {
259 error.FileNotFound => {
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);
262 @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
263 options.allocator.free(options.input_source.filename);
264 options.input_source = .{ .filename = filename_bytes };
265 },
266 else => {},
267 };
268 }
269 }
270
271 pub fn deinit(self: *Options) void {
272 for (self.extra_include_paths.items) |extra_include_path| {
273 self.allocator.free(extra_include_path);
274 }
275 self.extra_include_paths.deinit(self.allocator);
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 }
284 var symbol_it = self.symbols.iterator();
285 while (symbol_it.next()) |entry| {
286 self.allocator.free(entry.key_ptr.*);
287 entry.value_ptr.deinit(self.allocator);
288 }
289 self.symbols.deinit(self.allocator);
290 if (self.depfile_path) |depfile_path| {
291 self.allocator.free(depfile_path);
292 }
293 if (self.coff_options.define_external_symbol) |symbol_name| {
294 self.allocator.free(symbol_name);
295 }
296 }
297
298 pub fn dumpVerbose(self: *const Options, writer: *std.Io.Writer) !void {
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
313 if (self.extra_include_paths.items.len > 0) {
314 try writer.writeAll(" Extra include paths:\n");
315 for (self.extra_include_paths.items) |extra_include_path| {
316 try writer.print(" \"{s}\"\n", .{extra_include_path});
317 }
318 }
319 if (self.ignore_include_env_var) {
320 try writer.writeAll(" The INCLUDE environment variable will be ignored\n");
321 }
322 if (self.preprocess == .no) {
323 try writer.writeAll(" The preprocessor will not be invoked\n");
324 } else if (self.preprocess == .only) {
325 try writer.writeAll(" Only the preprocessor will be invoked\n");
326 }
327 if (self.symbols.count() > 0) {
328 try writer.writeAll(" Symbols:\n");
329 var it = self.symbols.iterator();
330 while (it.next()) |symbol| {
331 try writer.print(" {s} {s}", .{ switch (symbol.value_ptr.*) {
332 .define => "#define",
333 .undefine => "#undef",
334 }, symbol.key_ptr.* });
335 if (symbol.value_ptr.* == .define) {
336 try writer.print(" {s}", .{symbol.value_ptr.define});
337 }
338 try writer.writeAll("\n");
339 }
340 }
341 if (self.null_terminate_string_table_strings) {
342 try writer.writeAll(" Strings in string tables will be null-terminated\n");
343 }
344 if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) {
345 try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints});
346 }
347 if (self.silent_duplicate_control_ids) {
348 try writer.writeAll(" Duplicate control IDs will not emit warnings\n");
349 }
350 if (self.silent_duplicate_control_ids) {
351 try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n");
352 }
353
354 const language_id = self.default_language_id orelse res.Language.default;
355 const language_name = language_name: {
356 if (std.enums.fromInt(lang.LanguageId, language_id)) |lang_enum_val| {
357 break :language_name @tagName(lang_enum_val);
358 }
359 if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) {
360 break :language_name "LOCALE_CUSTOM_UNSPECIFIED";
361 }
362 break :language_name "<UNKNOWN>";
363 };
364 try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id });
365
366 const code_page = self.default_code_page orelse .windows1252;
367 try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) });
368 }
369};
370
371pub const Arg = struct {
372 prefix: enum { long, short, slash },
373 name_offset: usize,
374 full: []const u8,
375
376 pub fn fromString(str: []const u8) ?@This() {
377 if (std.mem.startsWith(u8, str, "--")) {
378 return .{ .prefix = .long, .name_offset = 2, .full = str };
379 } else if (std.mem.startsWith(u8, str, "-")) {
380 return .{ .prefix = .short, .name_offset = 1, .full = str };
381 } else if (std.mem.startsWith(u8, str, "/")) {
382 return .{ .prefix = .slash, .name_offset = 1, .full = str };
383 }
384 return null;
385 }
386
387 pub fn prefixSlice(self: Arg) []const u8 {
388 return self.full[0..(if (self.prefix == .long) 2 else 1)];
389 }
390
391 pub fn name(self: Arg) []const u8 {
392 return self.full[self.name_offset..];
393 }
394
395 pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 {
396 if (option_len == 0) return self.name();
397 return self.name()[0..option_len];
398 }
399
400 pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
401 return .{
402 .point_at_next_arg = true,
403 .value_offset = 0,
404 .name_offset = self.name_offset,
405 .prefix_len = self.prefixSlice().len,
406 };
407 }
408
409 pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan {
410 return self.optionSpan(0);
411 }
412
413 pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan {
414 return .{
415 .name_offset = self.name_offset,
416 .prefix_len = self.prefixSlice().len,
417 .name_len = option_len,
418 };
419 }
420
421 pub fn looksLikeFilepath(self: Arg) bool {
422 const meets_min_requirements = self.prefix == .slash and isSupportedInputExtension(std.fs.path.extension(self.full));
423 if (!meets_min_requirements) return false;
424
425 const could_be_fo_option = could_be_fo_option: {
426 var window_it = std.mem.window(u8, self.full[1..], 2, 1);
427 while (window_it.next()) |window| {
428 if (std.ascii.eqlIgnoreCase(window, "fo")) break :could_be_fo_option true;
429 // If we see '/' before "fo", then it's not possible for this to be a valid
430 // `/fo` option.
431 if (window[0] == '/') break;
432 }
433 break :could_be_fo_option false;
434 };
435 if (!could_be_fo_option) return true;
436
437 // It's still possible for a file path to look like a /fo option but not actually
438 // be one, e.g. `/foo/bar.rc`. As a last ditch effort to reduce false negatives,
439 // check if the file path exists and, if so, then we ignore the 'could be /fo option'-ness
440 std.fs.accessAbsolute(self.full, .{}) catch return false;
441 return true;
442 }
443
444 pub const Value = struct {
445 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>
448 index_increment: u2 = 1,
449
450 pub fn argSpan(self: Value, arg: Arg) Diagnostics.ErrorDetails.ArgSpan {
451 const prefix_len = arg.prefixSlice().len;
452 switch (self.index_increment) {
453 1 => return .{
454 .value_offset = @intFromPtr(self.slice.ptr) - @intFromPtr(arg.full.ptr),
455 .prefix_len = prefix_len,
456 .name_offset = arg.name_offset,
457 },
458 2 => return .{
459 .point_at_next_arg = true,
460 .prefix_len = prefix_len,
461 .name_offset = arg.name_offset,
462 },
463 else => unreachable,
464 }
465 }
466
467 pub fn index(self: Value, arg_index: usize) usize {
468 if (self.index_increment == 2) return arg_index + 1;
469 return arg_index;
470 }
471 };
472
473 pub fn value(self: Arg, option_len: usize, index: usize, args: []const []const u8) error{MissingValue}!Value {
474 const rest = self.full[self.name_offset + option_len ..];
475 if (rest.len > 0) return .{ .slice = rest };
476 if (index + 1 >= args.len) return error.MissingValue;
477 return .{ .slice = args[index + 1], .index_increment = 2 };
478 }
479
480 pub const Context = struct {
481 index: usize,
482 option_len: usize,
483 arg: Arg,
484 value: Value,
485 };
486};
487
488pub const ParseError = error{ParseError} || Allocator.Error;
489
490/// Note: Does not run `Options.maybeAppendRC` automatically. If that behavior is desired,
491/// it must be called separately.
492pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagnostics) ParseError!Options {
493 var options = Options{ .allocator = allocator };
494 errdefer options.deinit();
495
496 var output_filename: ?[]const u8 = null;
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;
509
510 var arg_i: usize = 0;
511 next_arg: while (arg_i < args.len) {
512 var arg = Arg.fromString(args[arg_i]) orelse break;
513 if (arg.name().len == 0) {
514 switch (arg.prefix) {
515 // -- on its own ends arg parsing
516 .long => {
517 arg_i += 1;
518 break;
519 },
520 // - or / on its own is an error
521 else => {
522 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
523 try err_details.msg.print(allocator, "invalid option: {s}", .{arg.prefixSlice()});
524 try diagnostics.append(err_details);
525 arg_i += 1;
526 continue :next_arg;
527 },
528 }
529 }
530
531 const args_remaining = args.len - arg_i;
532 if (args_remaining <= 2 and arg.looksLikeFilepath()) {
533 var err_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i };
534 try err_details.msg.appendSlice(allocator, "this argument was inferred to be a filepath, so argument parsing was terminated");
535 try diagnostics.append(err_details);
536
537 break;
538 }
539
540 while (arg.name().len > 0) {
541 const arg_name = arg.name();
542 // Note: These cases should be in order from longest to shortest, since
543 // shorter options that are a substring of a longer one could make
544 // the longer option's branch unreachable.
545 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
546 options.preprocess = .no;
547 arg.name_offset += ":no-preprocess".len;
548 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) {
549 const value = arg.value(":output-format".len, arg_i, args) catch {
550 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
551 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) });
552 try diagnostics.append(err_details);
553 arg_i += 1;
554 break :next_arg;
555 };
556 output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: {
557 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
558 try err_details.msg.print(allocator, "invalid output format setting: {s} ", .{value.slice});
559 try diagnostics.append(err_details);
560 break :blk output_format;
561 };
562 output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value };
563 arg_i += value.index_increment;
564 continue :next_arg;
565 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
566 const value = arg.value(":auto-includes".len, arg_i, args) catch {
567 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
568 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":auto-includes".len) });
569 try diagnostics.append(err_details);
570 arg_i += 1;
571 break :next_arg;
572 };
573 options.auto_includes = std.meta.stringToEnum(Options.AutoIncludes, value.slice) orelse blk: {
574 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
575 try err_details.msg.print(allocator, "invalid auto includes setting: {s} ", .{value.slice});
576 try diagnostics.append(err_details);
577 break :blk options.auto_includes;
578 };
579 arg_i += value.index_increment;
580 continue :next_arg;
581 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) {
582 const value = arg.value(":input-format".len, arg_i, args) catch {
583 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
584 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) });
585 try diagnostics.append(err_details);
586 arg_i += 1;
587 break :next_arg;
588 };
589 input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: {
590 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
591 try err_details.msg.print(allocator, "invalid input format setting: {s} ", .{value.slice});
592 try diagnostics.append(err_details);
593 break :blk input_format;
594 };
595 input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value };
596 arg_i += value.index_increment;
597 continue :next_arg;
598 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
599 const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
600 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
601 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) });
602 try diagnostics.append(err_details);
603 arg_i += 1;
604 break :next_arg;
605 };
606 options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: {
607 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
608 try err_details.msg.print(allocator, "invalid depfile format setting: {s} ", .{value.slice});
609 try diagnostics.append(err_details);
610 break :blk options.depfile_fmt;
611 };
612 arg_i += value.index_increment;
613 continue :next_arg;
614 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) {
615 const value = arg.value(":depfile".len, arg_i, args) catch {
616 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
617 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) });
618 try diagnostics.append(err_details);
619 arg_i += 1;
620 break :next_arg;
621 };
622 if (options.depfile_path) |overwritten_path| {
623 allocator.free(overwritten_path);
624 options.depfile_path = null;
625 }
626 const path = try allocator.dupe(u8, value.slice);
627 errdefer allocator.free(path);
628 options.depfile_path = path;
629 depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value };
630 arg_i += value.index_increment;
631 continue :next_arg;
632 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) {
633 const value = arg.value(":target".len, arg_i, args) catch {
634 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
635 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) });
636 try diagnostics.append(err_details);
637 arg_i += 1;
638 break :next_arg;
639 };
640 // Take the substring up to the first dash so that a full target triple
641 // can be used, e.g. x86_64-windows-gnu becomes x86_64
642 var target_it = std.mem.splitScalar(u8, value.slice, '-');
643 const arch_str = target_it.first();
644 const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse {
645 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
646 try err_details.msg.print(allocator, "invalid or unsupported target architecture: {s}", .{arch_str});
647 try diagnostics.append(err_details);
648 arg_i += value.index_increment;
649 continue :next_arg;
650 };
651 options.coff_options.target = arch.toCoffMachineType();
652 arg_i += value.index_increment;
653 continue :next_arg;
654 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
655 // No-op, we don't display any 'logo' to suppress
656 arg.name_offset += "nologo".len;
657 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) {
658 options.debug = true;
659 arg.name_offset += ":debug".len;
660 }
661 // Unsupported LCX/LCE options that need a value (within the same arg only)
662 else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) {
663 const rest = arg.full[arg.name_offset + 3 ..];
664 if (rest.len == 0) {
665 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{
666 .name_offset = arg.name_offset,
667 .prefix_len = arg.prefixSlice().len,
668 .value_offset = arg.name_offset + 3,
669 } };
670 try err_details.msg.print(allocator, "missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
671 try diagnostics.append(err_details);
672 }
673 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
674 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) });
675 try diagnostics.append(err_details);
676 arg_i += 1;
677 continue :next_arg;
678 }
679 // Unsupported LCX/LCE options that need a value
680 else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) {
681 const value = arg.value(2, arg_i, args) catch no_value: {
682 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
683 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
684 try diagnostics.append(err_details);
685 // dummy zero-length slice starting where the value would have been
686 const value_start = arg.name_offset + 2;
687 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
688 };
689 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
690 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
691 try diagnostics.append(err_details);
692 arg_i += value.index_increment;
693 continue :next_arg;
694 }
695 // Unsupported MUI options that need a value
696 else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or
697 std.ascii.startsWithIgnoreCase(arg_name, "gn") or
698 std.ascii.startsWithIgnoreCase(arg_name, "g2"))
699 {
700 const value = arg.value(2, arg_i, args) catch no_value: {
701 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
702 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
703 try diagnostics.append(err_details);
704 // dummy zero-length slice starting where the value would have been
705 const value_start = arg.name_offset + 2;
706 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
707 };
708 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
709 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
710 try diagnostics.append(err_details);
711 arg_i += value.index_increment;
712 continue :next_arg;
713 }
714 // Unsupported MUI options that do not need a value
715 else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) {
716 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
717 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
718 try diagnostics.append(err_details);
719 arg.name_offset += 2;
720 }
721 // Unsupported LCX/LCE options that do not need a value
722 else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or
723 std.ascii.startsWithIgnoreCase(arg_name, "tc") or
724 std.ascii.startsWithIgnoreCase(arg_name, "tw") or
725 std.ascii.startsWithIgnoreCase(arg_name, "te") or
726 std.ascii.startsWithIgnoreCase(arg_name, "ti") or
727 std.ascii.startsWithIgnoreCase(arg_name, "ta"))
728 {
729 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) };
730 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
731 try diagnostics.append(err_details);
732 arg.name_offset += 2;
733 } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) {
734 const value = arg.value(2, arg_i, args) catch {
735 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
736 try err_details.msg.print(allocator, "missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
737 try diagnostics.append(err_details);
738 arg_i += 1;
739 break :next_arg;
740 };
741 output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } };
742 output_filename = value.slice;
743 arg_i += value.index_increment;
744 continue :next_arg;
745 } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) {
746 const value = arg.value(2, arg_i, args) catch {
747 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
748 try err_details.msg.print(allocator, "missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
749 try diagnostics.append(err_details);
750 arg_i += 1;
751 break :next_arg;
752 };
753 const percent_str = value.slice;
754 const percent: u32 = parsePercent(percent_str) catch {
755 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
756 try err_details.msg.print(allocator, "invalid percent format '{s}'", .{percent_str});
757 try diagnostics.append(err_details);
758 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
759 try note_details.msg.appendSlice(allocator, "string length percent must be an integer between 1 and 100 (inclusive)");
760 try diagnostics.append(note_details);
761 arg_i += value.index_increment;
762 continue :next_arg;
763 };
764 if (percent == 0 or percent > 100) {
765 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
766 try err_details.msg.print(allocator, "percent out of range: {} (parsed from '{s}')", .{ percent, percent_str });
767 try diagnostics.append(err_details);
768 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i };
769 try note_details.msg.appendSlice(allocator, "string length percent must be an integer between 1 and 100 (inclusive)");
770 try diagnostics.append(note_details);
771 arg_i += value.index_increment;
772 continue :next_arg;
773 }
774 const percent_float = @as(f32, @floatFromInt(percent)) / 100;
775 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent);
776 arg_i += value.index_increment;
777 continue :next_arg;
778 } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) {
779 const value = arg.value(2, arg_i, args) catch {
780 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
781 try err_details.msg.print(allocator, "missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) });
782 try diagnostics.append(err_details);
783 arg_i += 1;
784 break :next_arg;
785 };
786 const tag = value.slice;
787 options.default_language_id = lang.tagToInt(tag) catch {
788 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
789 try err_details.msg.print(allocator, "invalid language tag: {s}", .{tag});
790 try diagnostics.append(err_details);
791 arg_i += value.index_increment;
792 continue :next_arg;
793 };
794 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) {
795 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
796 try err_details.msg.print(allocator, "language tag '{s}' does not have an assigned ID so it will be resolved to LOCALE_CUSTOM_UNSPECIFIED (id=0x{x})", .{ tag, lang.LOCALE_CUSTOM_UNSPECIFIED });
797 try diagnostics.append(err_details);
798 }
799 arg_i += value.index_increment;
800 continue :next_arg;
801 } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) {
802 const value = arg.value(1, arg_i, args) catch {
803 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
804 try err_details.msg.print(allocator, "missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
805 try diagnostics.append(err_details);
806 arg_i += 1;
807 break :next_arg;
808 };
809 const num_str = value.slice;
810 options.default_language_id = lang.parseInt(num_str) catch {
811 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
812 try err_details.msg.print(allocator, "invalid language ID: {s}", .{num_str});
813 try diagnostics.append(err_details);
814 arg_i += value.index_increment;
815 continue :next_arg;
816 };
817 arg_i += value.index_increment;
818 continue :next_arg;
819 } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) {
820 options.print_help_and_exit = true;
821 // If there's been an error to this point, then we still want to fail
822 if (diagnostics.hasError()) return error.ParseError;
823 return options;
824 }
825 // 1 char unsupported MUI options that need a value
826 else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or
827 std.ascii.startsWithIgnoreCase(arg_name, "g"))
828 {
829 const value = arg.value(1, arg_i, args) catch no_value: {
830 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
831 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
832 try diagnostics.append(err_details);
833 // dummy zero-length slice starting where the value would have been
834 const value_start = arg.name_offset + 1;
835 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
836 };
837 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
838 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
839 try diagnostics.append(err_details);
840 arg_i += value.index_increment;
841 continue :next_arg;
842 }
843 // Undocumented (and unsupported) options that need a value
844 // /z has to do something with font substitution
845 // /s has something to do with HWB resources being inserted into the .res
846 else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or
847 std.ascii.startsWithIgnoreCase(arg_name, "s"))
848 {
849 const value = arg.value(1, arg_i, args) catch no_value: {
850 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
851 try err_details.msg.print(allocator, "missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
852 try diagnostics.append(err_details);
853 // dummy zero-length slice starting where the value would have been
854 const value_start = arg.name_offset + 1;
855 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] };
856 };
857 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
858 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
859 try diagnostics.append(err_details);
860 arg_i += value.index_increment;
861 continue :next_arg;
862 }
863 // 1 char unsupported LCX/LCE options that do not need a value
864 else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) {
865 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
866 try err_details.msg.print(allocator, "the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
867 try diagnostics.append(err_details);
868 arg.name_offset += 1;
869 } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) {
870 const value = arg.value(1, arg_i, args) catch {
871 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
872 try err_details.msg.print(allocator, "missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
873 try diagnostics.append(err_details);
874 arg_i += 1;
875 break :next_arg;
876 };
877 const num_str = value.slice;
878 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch {
879 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
880 try err_details.msg.print(allocator, "invalid code page ID: {s}", .{num_str});
881 try diagnostics.append(err_details);
882 arg_i += value.index_increment;
883 continue :next_arg;
884 };
885 options.default_code_page = code_pages.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) {
886 error.InvalidCodePage => {
887 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
888 try err_details.msg.print(allocator, "invalid or unknown code page ID: {}", .{code_page_id});
889 try diagnostics.append(err_details);
890 arg_i += value.index_increment;
891 continue :next_arg;
892 },
893 error.UnsupportedCodePage => {
894 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
895 try err_details.msg.print(allocator, "unsupported code page: {s} (id={})", .{
896 @tagName(code_pages.getByIdentifier(code_page_id) catch unreachable),
897 code_page_id,
898 });
899 try diagnostics.append(err_details);
900 arg_i += value.index_increment;
901 continue :next_arg;
902 },
903 };
904 arg_i += value.index_increment;
905 continue :next_arg;
906 } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) {
907 options.verbose = true;
908 arg.name_offset += 1;
909 } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) {
910 options.ignore_include_env_var = true;
911 arg.name_offset += 1;
912 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) {
913 options.preprocess = .only;
914 preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined };
915 arg.name_offset += 1;
916 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) {
917 const value = arg.value(1, arg_i, args) catch {
918 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
919 try err_details.msg.print(allocator, "missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
920 try diagnostics.append(err_details);
921 arg_i += 1;
922 break :next_arg;
923 };
924 const path = value.slice;
925 const duped = try allocator.dupe(u8, path);
926 errdefer allocator.free(duped);
927 try options.extra_include_paths.append(options.allocator, duped);
928 arg_i += value.index_increment;
929 continue :next_arg;
930 } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) {
931 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line-
932 // "Ignored. Provided for compatibility with existing makefiles."
933 arg.name_offset += 1;
934 } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) {
935 options.null_terminate_string_table_strings = true;
936 arg.name_offset += 1;
937 } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) {
938 options.silent_duplicate_control_ids = true;
939 arg.name_offset += 1;
940 } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) {
941 options.warn_instead_of_error_on_invalid_code_page = true;
942 arg.name_offset += 1;
943 } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) {
944 // Undocumented option with unknown function
945 // TODO: More investigation to figure out what it does (if anything)
946 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) };
947 try err_details.msg.print(allocator, "option {s}{s} has no effect (it is undocumented and its function is unknown in the Win32 RC compiler)", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
948 try diagnostics.append(err_details);
949 arg.name_offset += 1;
950 } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) {
951 const value = arg.value(1, arg_i, args) catch {
952 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
953 try err_details.msg.print(allocator, "missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
954 try diagnostics.append(err_details);
955 arg_i += 1;
956 break :next_arg;
957 };
958 var tokenizer = std.mem.tokenizeScalar(u8, value.slice, '=');
959 // guaranteed to exist since an empty value.slice would invoke
960 // the 'missing symbol to define' branch above
961 const symbol = tokenizer.next().?;
962 const symbol_value = tokenizer.next() orelse "1";
963
964 if (isValidIdentifier(symbol)) {
965 try options.define(symbol, symbol_value);
966 } else {
967 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
968 try err_details.msg.print(allocator, "symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol});
969 try diagnostics.append(err_details);
970 }
971 arg_i += value.index_increment;
972 continue :next_arg;
973 } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) {
974 const value = arg.value(1, arg_i, args) catch {
975 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
976 try err_details.msg.print(allocator, "missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) });
977 try diagnostics.append(err_details);
978 arg_i += 1;
979 break :next_arg;
980 };
981 const symbol = value.slice;
982 if (isValidIdentifier(symbol)) {
983 try options.undefine(symbol);
984 } else {
985 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) };
986 try err_details.msg.print(allocator, "symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol});
987 try diagnostics.append(err_details);
988 }
989 arg_i += value.index_increment;
990 continue :next_arg;
991 } else {
992 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() };
993 try err_details.msg.print(allocator, "invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() });
994 try diagnostics.append(err_details);
995 arg_i += 1;
996 continue :next_arg;
997 }
998 } else {
999 // The while loop exited via its conditional, meaning we are done with
1000 // the current arg and can move on the the next
1001 arg_i += 1;
1002 continue;
1003 }
1004 }
1005
1006 const positionals = args[arg_i..];
1007
1008 if (positionals.len == 0) {
1009 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i };
1010 try err_details.msg.appendSlice(allocator, "missing input filename");
1011 try diagnostics.append(err_details);
1012
1013 if (args.len > 0) {
1014 const last_arg = args[args.len - 1];
1015 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) {
1016 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
1017 try note_details.msg.appendSlice(allocator, "if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing");
1018 try diagnostics.append(note_details);
1019 }
1020 }
1021
1022 // This is a fatal enough problem to justify an early return, since
1023 // things after this rely on the value of the input filename.
1024 return error.ParseError;
1025 }
1026 options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) };
1027 input_filename_arg_i = arg_i;
1028
1029 const InputFormatSource = enum {
1030 inferred_from_input_filename,
1031 input_format_arg,
1032 };
1033
1034 var input_format_source: InputFormatSource = undefined;
1035 if (input_format == null) {
1036 const ext = std.fs.path.extension(options.input_source.filename);
1037 if (std.ascii.eqlIgnoreCase(ext, ".res")) {
1038 input_format = .res;
1039 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
1040 input_format = .rcpp;
1041 } else {
1042 input_format = .rc;
1043 }
1044 input_format_source = .inferred_from_input_filename;
1045 } else {
1046 input_format_source = .input_format_arg;
1047 }
1048
1049 if (positionals.len > 1) {
1050 if (output_filename != null) {
1051 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 };
1052 try err_details.msg.appendSlice(allocator, "output filename already specified");
1053 try diagnostics.append(err_details);
1054 var note_details = Diagnostics.ErrorDetails{
1055 .type = .note,
1056 .arg_index = output_filename_context.arg.index,
1057 .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg),
1058 };
1059 try note_details.msg.appendSlice(allocator, "output filename previously specified here");
1060 try diagnostics.append(note_details);
1061 } else {
1062 output_filename = positionals[1];
1063 output_filename_context = .{ .positional = arg_i + 1 };
1064 }
1065 }
1066
1067 const OutputFormatSource = enum {
1068 inferred_from_input_filename,
1069 inferred_from_output_filename,
1070 output_format_arg,
1071 unable_to_infer_from_input_filename,
1072 unable_to_infer_from_output_filename,
1073 inferred_from_preprocess_only,
1074 };
1075
1076 var output_format_source: OutputFormatSource = undefined;
1077 if (output_filename == null) {
1078 if (output_format == null) {
1079 output_format_source = .inferred_from_input_filename;
1080 const input_ext = std.fs.path.extension(options.input_source.filename);
1081 if (std.ascii.eqlIgnoreCase(input_ext, ".res")) {
1082 output_format = .coff;
1083 } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) {
1084 output_format = .rcpp;
1085 output_format_source = .inferred_from_preprocess_only;
1086 } else {
1087 if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) {
1088 output_format_source = .unable_to_infer_from_input_filename;
1089 }
1090 output_format = .res;
1091 }
1092 } else {
1093 output_format_source = .output_format_arg;
1094 }
1095 options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) };
1096 } else {
1097 options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) };
1098 if (output_format == null) {
1099 output_format_source = .inferred_from_output_filename;
1100 const ext = std.fs.path.extension(options.output_source.filename);
1101 if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) {
1102 output_format = .coff;
1103 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) {
1104 output_format = .rcpp;
1105 } else {
1106 if (!std.ascii.eqlIgnoreCase(ext, ".res")) {
1107 output_format_source = .unable_to_infer_from_output_filename;
1108 }
1109 output_format = .res;
1110 }
1111 } else {
1112 output_format_source = .output_format_arg;
1113 }
1114 }
1115
1116 options.input_format = input_format.?;
1117 options.output_format = output_format.?;
1118
1119 // Check for incompatible options
1120 var print_input_format_source_note: bool = false;
1121 var print_output_format_source_note: bool = false;
1122 if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) {
1123 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) };
1124 if (options.input_format == .res) {
1125 try err_details.msg.print(allocator, "the {s}{s} option was ignored because the input format is '{s}'", .{
1126 depfile_context.arg.prefixSlice(),
1127 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
1128 @tagName(options.input_format),
1129 });
1130 print_input_format_source_note = true;
1131 } else if (options.output_format == .rcpp) {
1132 try err_details.msg.print(allocator, "the {s}{s} option was ignored because the output format is '{s}'", .{
1133 depfile_context.arg.prefixSlice(),
1134 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len),
1135 @tagName(options.output_format),
1136 });
1137 print_output_format_source_note = true;
1138 }
1139 try diagnostics.append(err_details);
1140 }
1141 if (!isSupportedTransformation(options.input_format, options.output_format)) {
1142 var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false };
1143 try err_details.msg.print(allocator, "input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) });
1144 try diagnostics.append(err_details);
1145 print_input_format_source_note = true;
1146 print_output_format_source_note = true;
1147 }
1148 if (options.preprocess == .only and options.output_format != .rcpp) {
1149 var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index };
1150 try err_details.msg.print(allocator, "the {s}{s} option cannot be used with output format '{s}'", .{
1151 preprocess_only_context.arg.prefixSlice(),
1152 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
1153 @tagName(options.output_format),
1154 });
1155 try diagnostics.append(err_details);
1156 print_output_format_source_note = true;
1157 }
1158 if (print_input_format_source_note) {
1159 switch (input_format_source) {
1160 .inferred_from_input_filename => {
1161 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
1162 try err_details.msg.appendSlice(allocator, "the input format was inferred from the input filename");
1163 try diagnostics.append(err_details);
1164 },
1165 .input_format_arg => {
1166 var err_details = Diagnostics.ErrorDetails{
1167 .type = .note,
1168 .arg_index = input_format_context.index,
1169 .arg_span = input_format_context.value.argSpan(input_format_context.arg),
1170 };
1171 try err_details.msg.appendSlice(allocator, "the input format was specified here");
1172 try diagnostics.append(err_details);
1173 },
1174 }
1175 }
1176 if (print_output_format_source_note) {
1177 switch (output_format_source) {
1178 .inferred_from_input_filename, .unable_to_infer_from_input_filename => {
1179 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i };
1180 if (output_format_source == .inferred_from_input_filename) {
1181 try err_details.msg.appendSlice(allocator, "the output format was inferred from the input filename");
1182 } else {
1183 try err_details.msg.appendSlice(allocator, "the output format was unable to be inferred from the input filename, so the default was used");
1184 }
1185 try diagnostics.append(err_details);
1186 },
1187 .inferred_from_output_filename, .unable_to_infer_from_output_filename => {
1188 var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) {
1189 .positional => |i| .{ .type = .note, .arg_index = i },
1190 .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) },
1191 .unspecified => unreachable,
1192 };
1193 if (output_format_source == .inferred_from_output_filename) {
1194 try err_details.msg.appendSlice(allocator, "the output format was inferred from the output filename");
1195 } else {
1196 try err_details.msg.appendSlice(allocator, "the output format was unable to be inferred from the output filename, so the default was used");
1197 }
1198 try diagnostics.append(err_details);
1199 },
1200 .output_format_arg => {
1201 var err_details = Diagnostics.ErrorDetails{
1202 .type = .note,
1203 .arg_index = output_format_context.index,
1204 .arg_span = output_format_context.value.argSpan(output_format_context.arg),
1205 };
1206 try err_details.msg.appendSlice(allocator, "the output format was specified here");
1207 try diagnostics.append(err_details);
1208 },
1209 .inferred_from_preprocess_only => {
1210 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index };
1211 try err_details.msg.print(allocator, "the output format was inferred from the usage of the {s}{s} option", .{
1212 preprocess_only_context.arg.prefixSlice(),
1213 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len),
1214 });
1215 try diagnostics.append(err_details);
1216 },
1217 }
1218 }
1219
1220 if (diagnostics.hasError()) {
1221 return error.ParseError;
1222 }
1223
1224 // Implied settings from input/output formats
1225 if (options.output_format == .rcpp) options.preprocess = .only;
1226 if (options.input_format == .res) options.output_format = .coff;
1227 if (options.input_format == .rcpp) options.preprocess = .no;
1228
1229 return options;
1230}
1231
1232pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 {
1233 var buf: std.ArrayList(u8) = .empty;
1234 errdefer buf.deinit(allocator);
1235 if (std.fs.path.dirname(path)) |dirname| {
1236 var end_pos = dirname.len;
1237 // We want to ensure that we write a path separator at the end, so if the dirname
1238 // doesn't end with a path sep then include the char after the dirname
1239 // which must be a path sep.
1240 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1;
1241 try buf.appendSlice(allocator, path[0..end_pos]);
1242 }
1243 try buf.appendSlice(allocator, std.fs.path.stem(path));
1244 try buf.appendSlice(allocator, ext);
1245 return try buf.toOwnedSlice(allocator);
1246}
1247
1248pub fn isSupportedInputExtension(ext: []const u8) bool {
1249 if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true;
1250 if (std.ascii.eqlIgnoreCase(ext, ".res")) return true;
1251 if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true;
1252 return false;
1253}
1254
1255pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool {
1256 return switch (input) {
1257 .rc => switch (output) {
1258 .res => true,
1259 .coff => true,
1260 .rcpp => true,
1261 },
1262 .res => switch (output) {
1263 .res => false,
1264 .coff => true,
1265 .rcpp => false,
1266 },
1267 .rcpp => switch (output) {
1268 .res => true,
1269 .coff => true,
1270 .rcpp => false,
1271 },
1272 };
1273}
1274
1275/// Returns true if the str is a valid C identifier for use in a #define/#undef macro
1276pub fn isValidIdentifier(str: []const u8) bool {
1277 for (str, 0..) |c, i| switch (c) {
1278 '0'...'9' => if (i == 0) return false,
1279 'a'...'z', 'A'...'Z', '_' => {},
1280 else => return false,
1281 };
1282 return true;
1283}
1284
1285/// This function is specific to how the Win32 RC command line interprets
1286/// max string literal length percent.
1287/// - Wraps on overflow of u32
1288/// - Stops parsing on any invalid hexadecimal digits
1289/// - Errors if a digit is not the first char
1290/// - `-` (negative) prefix is allowed
1291pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 {
1292 var result: u32 = 0;
1293 const radix: u8 = 10;
1294 var buf = str;
1295
1296 const Prefix = enum { none, minus };
1297 var prefix: Prefix = .none;
1298 switch (buf[0]) {
1299 '-' => {
1300 prefix = .minus;
1301 buf = buf[1..];
1302 },
1303 else => {},
1304 }
1305
1306 for (buf, 0..) |c, i| {
1307 const digit = switch (c) {
1308 // On invalid digit for the radix, just stop parsing but don't fail
1309 '0'...'9' => std.fmt.charToDigit(c, radix) catch break,
1310 else => {
1311 // First digit must be valid
1312 if (i == 0) {
1313 return error.InvalidFormat;
1314 }
1315 break;
1316 },
1317 };
1318
1319 if (result != 0) {
1320 result *%= radix;
1321 }
1322 result +%= digit;
1323 }
1324
1325 switch (prefix) {
1326 .none => {},
1327 .minus => result = 0 -% result,
1328 }
1329
1330 return result;
1331}
1332
1333test parsePercent {
1334 try std.testing.expectEqual(@as(u32, 16), try parsePercent("16"));
1335 try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A"));
1336 try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz"));
1337 try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1"));
1338 try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16"));
1339 try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297"));
1340 try std.testing.expectError(error.InvalidFormat, parsePercent("--1"));
1341 try std.testing.expectError(error.InvalidFormat, parsePercent("ha"));
1342 try std.testing.expectError(error.InvalidFormat, parsePercent("¹"));
1343 try std.testing.expectError(error.InvalidFormat, parsePercent("~1"));
1344}
1345
1346pub fn renderErrorMessage(writer: *std.Io.Writer, config: std.Io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void {
1347 try config.setColor(writer, .dim);
1348 try writer.writeAll("<cli>");
1349 try config.setColor(writer, .reset);
1350 try config.setColor(writer, .bold);
1351 try writer.writeAll(": ");
1352 switch (err_details.type) {
1353 .err => {
1354 try config.setColor(writer, .red);
1355 try writer.writeAll("error: ");
1356 },
1357 .warning => {
1358 try config.setColor(writer, .yellow);
1359 try writer.writeAll("warning: ");
1360 },
1361 .note => {
1362 try config.setColor(writer, .cyan);
1363 try writer.writeAll("note: ");
1364 },
1365 }
1366 try config.setColor(writer, .reset);
1367 try config.setColor(writer, .bold);
1368 try writer.writeAll(err_details.msg.items);
1369 try writer.writeByte('\n');
1370 try config.setColor(writer, .reset);
1371
1372 if (!err_details.print_args) {
1373 try writer.writeByte('\n');
1374 return;
1375 }
1376
1377 try config.setColor(writer, .dim);
1378 const prefix = " ... ";
1379 try writer.writeAll(prefix);
1380 try config.setColor(writer, .reset);
1381
1382 const arg_with_name = args[err_details.arg_index];
1383 const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len];
1384 const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset];
1385 var name_slice = arg_with_name[err_details.arg_span.name_offset..];
1386 if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len;
1387 const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..];
1388
1389 try writer.writeAll(prefix_slice);
1390 if (before_name_slice.len > 0) {
1391 try config.setColor(writer, .dim);
1392 try writer.writeAll(before_name_slice);
1393 try config.setColor(writer, .reset);
1394 }
1395 try writer.writeAll(name_slice);
1396 if (after_name_slice.len > 0) {
1397 try config.setColor(writer, .dim);
1398 try writer.writeAll(after_name_slice);
1399 try config.setColor(writer, .reset);
1400 }
1401
1402 var next_arg_len: usize = 0;
1403 if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) {
1404 const next_arg = args[err_details.arg_index + 1];
1405 try writer.writeByte(' ');
1406 try writer.writeAll(next_arg);
1407 next_arg_len = next_arg.len;
1408 }
1409
1410 const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index;
1411 if (last_shown_arg_index + 1 < args.len) {
1412 // special case for when pointing to a missing value within the same arg
1413 // as the name
1414 if (err_details.arg_span.value_offset >= arg_with_name.len) {
1415 try writer.writeByte(' ');
1416 }
1417 try config.setColor(writer, .dim);
1418 try writer.writeAll(" ...");
1419 try config.setColor(writer, .reset);
1420 }
1421 try writer.writeByte('\n');
1422
1423 try config.setColor(writer, .green);
1424 try writer.splatByteAll(' ', prefix.len);
1425 // Special case for when the option is *only* a prefix (e.g. invalid option: -)
1426 if (err_details.arg_span.prefix_len == arg_with_name.len) {
1427 try writer.splatByteAll('^', err_details.arg_span.prefix_len);
1428 } else {
1429 try writer.splatByteAll('~', err_details.arg_span.prefix_len);
1430 try writer.splatByteAll(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len);
1431 if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) {
1432 try writer.writeByte('^');
1433 try writer.splatByteAll('~', name_slice.len - 1);
1434 } else if (err_details.arg_span.value_offset > 0) {
1435 try writer.splatByteAll('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset);
1436 try writer.writeByte('^');
1437 if (err_details.arg_span.value_offset < arg_with_name.len) {
1438 try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.value_offset - 1);
1439 }
1440 } else if (err_details.arg_span.point_at_next_arg) {
1441 try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.name_offset + 1);
1442 try writer.writeByte('^');
1443 if (next_arg_len > 0) {
1444 try writer.splatByteAll('~', next_arg_len - 1);
1445 }
1446 }
1447 }
1448 try writer.writeByte('\n');
1449 try config.setColor(writer, .reset);
1450}
1451
1452fn testParse(args: []const []const u8) !Options {
1453 return (try testParseOutput(args, "")).?;
1454}
1455
1456fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options {
1457 return (try testParseOutput(args, expected_output)).?;
1458}
1459
1460fn testParseError(args: []const []const u8, expected_output: []const u8) !void {
1461 var maybe_options = try testParseOutput(args, expected_output);
1462 if (maybe_options != null) {
1463 std.debug.print("expected error, got options: {}\n", .{maybe_options.?});
1464 maybe_options.?.deinit();
1465 return error.TestExpectedError;
1466 }
1467}
1468
1469fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options {
1470 var diagnostics = Diagnostics.init(std.testing.allocator);
1471 defer diagnostics.deinit();
1472
1473 var output: std.Io.Writer.Allocating = .init(std.testing.allocator);
1474 defer output.deinit();
1475
1476 var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) {
1477 error.ParseError => {
1478 try diagnostics.renderToWriter(args, &output.writer, .no_color);
1479 try std.testing.expectEqualStrings(expected_output, output.written());
1480 return null;
1481 },
1482 else => |e| return e,
1483 };
1484 errdefer options.deinit();
1485
1486 try diagnostics.renderToWriter(args, &output.writer, .no_color);
1487 try std.testing.expectEqualStrings(expected_output, output.written());
1488 return options;
1489}
1490
1491test "parse errors: basic" {
1492 try testParseError(&.{"/"},
1493 \\<cli>: error: invalid option: /
1494 \\ ... /
1495 \\ ^
1496 \\<cli>: error: missing input filename
1497 \\
1498 \\
1499 );
1500 try testParseError(&.{"/ln"},
1501 \\<cli>: error: missing language tag after /ln option
1502 \\ ... /ln
1503 \\ ~~~~^
1504 \\<cli>: error: missing input filename
1505 \\
1506 \\
1507 );
1508 try testParseError(&.{"-vln"},
1509 \\<cli>: error: missing language tag after -ln option
1510 \\ ... -vln
1511 \\ ~ ~~~^
1512 \\<cli>: error: missing input filename
1513 \\
1514 \\
1515 );
1516 try testParseError(&.{"/_not-an-option"},
1517 \\<cli>: error: invalid option: /_not-an-option
1518 \\ ... /_not-an-option
1519 \\ ~^~~~~~~~~~~~~~
1520 \\<cli>: error: missing input filename
1521 \\
1522 \\
1523 );
1524 try testParseError(&.{"-_not-an-option"},
1525 \\<cli>: error: invalid option: -_not-an-option
1526 \\ ... -_not-an-option
1527 \\ ~^~~~~~~~~~~~~~
1528 \\<cli>: error: missing input filename
1529 \\
1530 \\
1531 );
1532 try testParseError(&.{"--_not-an-option"},
1533 \\<cli>: error: invalid option: --_not-an-option
1534 \\ ... --_not-an-option
1535 \\ ~~^~~~~~~~~~~~~~
1536 \\<cli>: error: missing input filename
1537 \\
1538 \\
1539 );
1540 try testParseError(&.{"/v_not-an-option"},
1541 \\<cli>: error: invalid option: /_not-an-option
1542 \\ ... /v_not-an-option
1543 \\ ~ ^~~~~~~~~~~~~~
1544 \\<cli>: error: missing input filename
1545 \\
1546 \\
1547 );
1548 try testParseError(&.{"-v_not-an-option"},
1549 \\<cli>: error: invalid option: -_not-an-option
1550 \\ ... -v_not-an-option
1551 \\ ~ ^~~~~~~~~~~~~~
1552 \\<cli>: error: missing input filename
1553 \\
1554 \\
1555 );
1556 try testParseError(&.{"--v_not-an-option"},
1557 \\<cli>: error: invalid option: --_not-an-option
1558 \\ ... --v_not-an-option
1559 \\ ~~ ^~~~~~~~~~~~~~
1560 \\<cli>: error: missing input filename
1561 \\
1562 \\
1563 );
1564}
1565
1566test "inferred absolute filepaths" {
1567 {
1568 var options = try testParseWarning(&.{ "/fo", "foo.res", "/home/absolute/path.rc" },
1569 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
1570 \\ ... /home/absolute/path.rc
1571 \\ ^~~~~~~~~~~~~~~~~~~~~~
1572 \\
1573 );
1574 defer options.deinit();
1575 }
1576 {
1577 var options = try testParseWarning(&.{ "/home/absolute/path.rc", "foo.res" },
1578 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated
1579 \\ ... /home/absolute/path.rc ...
1580 \\ ^~~~~~~~~~~~~~~~~~~~~~
1581 \\
1582 );
1583 defer options.deinit();
1584 }
1585 {
1586 // Only the last two arguments are checked, so the /h is parsed as an option
1587 var options = try testParse(&.{ "/home/absolute/path.rc", "foo.rc", "foo.res" });
1588 defer options.deinit();
1589
1590 try std.testing.expect(options.print_help_and_exit);
1591 }
1592 {
1593 var options = try testParse(&.{ "/xvFO/some/absolute/path.res", "foo.rc" });
1594 defer options.deinit();
1595
1596 try std.testing.expectEqual(true, options.verbose);
1597 try std.testing.expectEqual(true, options.ignore_include_env_var);
1598 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1599 try std.testing.expectEqualStrings("/some/absolute/path.res", options.output_source.filename);
1600 }
1601}
1602
1603test "parse errors: /ln" {
1604 try testParseError(&.{ "/ln", "invalid", "foo.rc" },
1605 \\<cli>: error: invalid language tag: invalid
1606 \\ ... /ln invalid ...
1607 \\ ~~~~^~~~~~~
1608 \\
1609 );
1610 try testParseError(&.{ "/lninvalid", "foo.rc" },
1611 \\<cli>: error: invalid language tag: invalid
1612 \\ ... /lninvalid ...
1613 \\ ~~~^~~~~~~
1614 \\
1615 );
1616}
1617
1618test "parse: options" {
1619 {
1620 var options = try testParse(&.{ "/v", "foo.rc" });
1621 defer options.deinit();
1622
1623 try std.testing.expectEqual(true, options.verbose);
1624 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1625 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
1626 }
1627 {
1628 var options = try testParse(&.{ "/vx", "foo.rc" });
1629 defer options.deinit();
1630
1631 try std.testing.expectEqual(true, options.verbose);
1632 try std.testing.expectEqual(true, options.ignore_include_env_var);
1633 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1634 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
1635 }
1636 {
1637 var options = try testParse(&.{ "/xv", "foo.rc" });
1638 defer options.deinit();
1639
1640 try std.testing.expectEqual(true, options.verbose);
1641 try std.testing.expectEqual(true, options.ignore_include_env_var);
1642 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1643 try std.testing.expectEqualStrings("foo.res", options.output_source.filename);
1644 }
1645 {
1646 var options = try testParse(&.{ "/xvFObar.res", "foo.rc" });
1647 defer options.deinit();
1648
1649 try std.testing.expectEqual(true, options.verbose);
1650 try std.testing.expectEqual(true, options.ignore_include_env_var);
1651 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
1652 try std.testing.expectEqualStrings("bar.res", options.output_source.filename);
1653 }
1654}
1655
1656test "parse: define and undefine" {
1657 {
1658 var options = try testParse(&.{ "/dfoo", "foo.rc" });
1659 defer options.deinit();
1660
1661 const action = options.symbols.get("foo").?;
1662 try std.testing.expectEqualStrings("1", action.define);
1663 }
1664 {
1665 var options = try testParse(&.{ "/dfoo=bar", "/dfoo=baz", "foo.rc" });
1666 defer options.deinit();
1667
1668 const action = options.symbols.get("foo").?;
1669 try std.testing.expectEqualStrings("baz", action.define);
1670 }
1671 {
1672 var options = try testParse(&.{ "/ufoo", "foo.rc" });
1673 defer options.deinit();
1674
1675 const action = options.symbols.get("foo").?;
1676 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1677 }
1678 {
1679 // Once undefined, future defines are ignored
1680 var options = try testParse(&.{ "/ufoo", "/dfoo", "foo.rc" });
1681 defer options.deinit();
1682
1683 const action = options.symbols.get("foo").?;
1684 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1685 }
1686 {
1687 // Undefined always takes precedence
1688 var options = try testParse(&.{ "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
1689 defer options.deinit();
1690
1691 const action = options.symbols.get("foo").?;
1692 try std.testing.expectEqual(Options.SymbolAction.undefine, action);
1693 }
1694 {
1695 // Warn + ignore invalid identifiers
1696 var options = try testParseWarning(
1697 &.{ "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
1698 \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
1699 \\ ... /dfoo bar ...
1700 \\ ~~^~~~~~~
1701 \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined
1702 \\ ... /u 0leadingdigit ...
1703 \\ ~~~^~~~~~~~~~~~~
1704 \\
1705 ,
1706 );
1707 defer options.deinit();
1708
1709 try std.testing.expectEqual(@as(usize, 0), options.symbols.count());
1710 }
1711}
1712
1713test "parse: /sl" {
1714 try testParseError(&.{ "/sl", "0", "foo.rc" },
1715 \\<cli>: error: percent out of range: 0 (parsed from '0')
1716 \\ ... /sl 0 ...
1717 \\ ~~~~^
1718 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1719 \\
1720 \\
1721 );
1722 try testParseError(&.{ "/sl", "abcd", "foo.rc" },
1723 \\<cli>: error: invalid percent format 'abcd'
1724 \\ ... /sl abcd ...
1725 \\ ~~~~^~~~
1726 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive)
1727 \\
1728 \\
1729 );
1730 {
1731 var options = try testParse(&.{"foo.rc"});
1732 defer options.deinit();
1733
1734 try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
1735 }
1736 {
1737 var options = try testParse(&.{ "/sl100", "foo.rc" });
1738 defer options.deinit();
1739
1740 try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
1741 }
1742 {
1743 var options = try testParse(&.{ "-SL33", "foo.rc" });
1744 defer options.deinit();
1745
1746 try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
1747 }
1748 {
1749 var options = try testParse(&.{ "/sl15", "foo.rc" });
1750 defer options.deinit();
1751
1752 try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
1753 }
1754}
1755
1756test "parse: unsupported MUI-related options" {
1757 try testParseError(&.{ "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
1758 \\<cli>: error: the /q option is unsupported
1759 \\ ... /q ...
1760 \\ ~^
1761 \\<cli>: error: the /g1 option is unsupported
1762 \\ ... /g1 ...
1763 \\ ~^~
1764 \\<cli>: error: the -G2 option is unsupported
1765 \\ ... -G2 ...
1766 \\ ~^~
1767 \\<cli>: error: the /fm option is unsupported
1768 \\ ... /fm ...
1769 \\ ~^~
1770 \\<cli>: error: the /g option is unsupported
1771 \\ ... /g ...
1772 \\ ~^
1773 \\
1774 );
1775}
1776
1777test "parse: unsupported LCX/LCE-related options" {
1778 try testParseError(&.{ "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
1779 \\<cli>: error: the /t option is unsupported
1780 \\ ... /t ...
1781 \\ ~^
1782 \\<cli>: error: missing value for /tp: option
1783 \\ ... /tp: ...
1784 \\ ~~~~^
1785 \\<cli>: error: the /tp: option is unsupported
1786 \\ ... /tp: ...
1787 \\ ~^~~
1788 \\<cli>: error: the /tp: option is unsupported
1789 \\ ... /tp:blah ...
1790 \\ ~^~~~~~~
1791 \\<cli>: error: the /tm option is unsupported
1792 \\ ... /tm ...
1793 \\ ~^~
1794 \\<cli>: error: the /tc option is unsupported
1795 \\ ... /tc ...
1796 \\ ~^~
1797 \\<cli>: error: the /tw option is unsupported
1798 \\ ... /tw ...
1799 \\ ~^~
1800 \\<cli>: error: the -TE option is unsupported
1801 \\ ... -TEti ...
1802 \\ ~^~
1803 \\<cli>: error: the -ti option is unsupported
1804 \\ ... -TEti ...
1805 \\ ~ ^~
1806 \\<cli>: error: the /ta option is unsupported
1807 \\ ... /ta ...
1808 \\ ~^~
1809 \\<cli>: error: the /tn option is unsupported
1810 \\ ... /tn ...
1811 \\ ~^~
1812 \\
1813 );
1814}
1815
1816test "parse: output filename specified twice" {
1817 try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" },
1818 \\<cli>: error: output filename already specified
1819 \\ ... foo.res
1820 \\ ^~~~~~~
1821 \\<cli>: note: output filename previously specified here
1822 \\ ... /fo foo.res ...
1823 \\ ~~~~^~~~~~~
1824 \\
1825 );
1826}
1827
1828test "parse: input and output formats" {
1829 {
1830 try testParseError(&.{ "/:output-format", "rcpp", "foo.res" },
1831 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
1832 \\
1833 \\<cli>: note: the input format was inferred from the input filename
1834 \\ ... foo.res
1835 \\ ^~~~~~~
1836 \\<cli>: note: the output format was specified here
1837 \\ ... /:output-format rcpp ...
1838 \\ ~~~~~~~~~~~~~~~~^~~~
1839 \\
1840 );
1841 }
1842 {
1843 try testParseError(&.{ "foo.res", "foo.rcpp" },
1844 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp'
1845 \\
1846 \\<cli>: note: the input format was inferred from the input filename
1847 \\ ... foo.res ...
1848 \\ ^~~~~~~
1849 \\<cli>: note: the output format was inferred from the output filename
1850 \\ ... foo.rcpp
1851 \\ ^~~~~~~~
1852 \\
1853 );
1854 }
1855 {
1856 try testParseError(&.{ "/:input-format", "res", "foo" },
1857 \\<cli>: error: input format 'res' cannot be converted to output format 'res'
1858 \\
1859 \\<cli>: note: the input format was specified here
1860 \\ ... /:input-format res ...
1861 \\ ~~~~~~~~~~~~~~~^~~
1862 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
1863 \\ ... foo
1864 \\ ^~~
1865 \\
1866 );
1867 }
1868 {
1869 try testParseError(&.{ "/p", "/:input-format", "res", "foo" },
1870 \\<cli>: error: input format 'res' cannot be converted to output format 'res'
1871 \\
1872 \\<cli>: error: the /p option cannot be used with output format 'res'
1873 \\ ... /p ...
1874 \\ ^~
1875 \\<cli>: note: the input format was specified here
1876 \\ ... /:input-format res ...
1877 \\ ~~~~~~~~~~~~~~~^~~
1878 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used
1879 \\ ... foo
1880 \\ ^~~
1881 \\
1882 );
1883 }
1884 {
1885 try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" },
1886 \\<cli>: error: the /p option cannot be used with output format 'coff'
1887 \\ ... /p ...
1888 \\ ^~
1889 \\<cli>: note: the output format was specified here
1890 \\ ... /:output-format coff ...
1891 \\ ~~~~~~~~~~~~~~~~^~~~
1892 \\
1893 );
1894 }
1895 {
1896 try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" },
1897 \\<cli>: error: the /p option cannot be used with output format 'res'
1898 \\ ... /p ...
1899 \\ ^~
1900 \\<cli>: note: the output format was inferred from the output filename
1901 \\ ... /fo foo.res ...
1902 \\ ~~~~^~~~~~~
1903 \\
1904 );
1905 }
1906 {
1907 try testParseError(&.{ "/p", "foo.rc", "foo.o" },
1908 \\<cli>: error: the /p option cannot be used with output format 'coff'
1909 \\ ... /p ...
1910 \\ ^~
1911 \\<cli>: note: the output format was inferred from the output filename
1912 \\ ... foo.o
1913 \\ ^~~~~
1914 \\
1915 );
1916 }
1917 {
1918 var options = try testParse(&.{"foo.rc"});
1919 defer options.deinit();
1920
1921 try std.testing.expectEqual(.rc, options.input_format);
1922 try std.testing.expectEqual(.res, options.output_format);
1923 }
1924 {
1925 var options = try testParse(&.{"foo.rcpp"});
1926 defer options.deinit();
1927
1928 try std.testing.expectEqual(.no, options.preprocess);
1929 try std.testing.expectEqual(.rcpp, options.input_format);
1930 try std.testing.expectEqual(.res, options.output_format);
1931 }
1932 {
1933 var options = try testParse(&.{ "foo.rc", "foo.rcpp" });
1934 defer options.deinit();
1935
1936 try std.testing.expectEqual(.only, options.preprocess);
1937 try std.testing.expectEqual(.rc, options.input_format);
1938 try std.testing.expectEqual(.rcpp, options.output_format);
1939 }
1940 {
1941 var options = try testParse(&.{ "foo.rc", "foo.obj" });
1942 defer options.deinit();
1943
1944 try std.testing.expectEqual(.rc, options.input_format);
1945 try std.testing.expectEqual(.coff, options.output_format);
1946 }
1947 {
1948 var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" });
1949 defer options.deinit();
1950
1951 try std.testing.expectEqual(.rc, options.input_format);
1952 try std.testing.expectEqual(.coff, options.output_format);
1953 }
1954 {
1955 var options = try testParse(&.{"foo.res"});
1956 defer options.deinit();
1957
1958 try std.testing.expectEqual(.res, options.input_format);
1959 try std.testing.expectEqual(.coff, options.output_format);
1960 }
1961 {
1962 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" },
1963 \\<cli>: warning: the /:depfile option was ignored because the output format is 'rcpp'
1964 \\ ... /:depfile foo.json ...
1965 \\ ~~~~~~~~~~^~~~~~~~
1966 \\<cli>: note: the output format was inferred from the output filename
1967 \\ ... foo.rcpp
1968 \\ ^~~~~~~~
1969 \\
1970 );
1971 defer options.deinit();
1972
1973 try std.testing.expectEqual(.rc, options.input_format);
1974 try std.testing.expectEqual(.rcpp, options.output_format);
1975 }
1976 {
1977 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" },
1978 \\<cli>: warning: the /:depfile option was ignored because the input format is 'res'
1979 \\ ... /:depfile foo.json ...
1980 \\ ~~~~~~~~~~^~~~~~~~
1981 \\<cli>: note: the input format was inferred from the input filename
1982 \\ ... foo.res ...
1983 \\ ^~~~~~~
1984 \\
1985 );
1986 defer options.deinit();
1987
1988 try std.testing.expectEqual(.res, options.input_format);
1989 try std.testing.expectEqual(.coff, options.output_format);
1990 }
1991}
1992
1993test "maybeAppendRC" {
1994 var tmp = std.testing.tmpDir(.{});
1995 defer tmp.cleanup();
1996
1997 var options = try testParse(&.{"foo"});
1998 defer options.deinit();
1999 try std.testing.expectEqualStrings("foo", options.input_source.filename);
2000
2001 // Create the file so that it's found. In this scenario, .rc should not get
2002 // appended.
2003 var file = try tmp.dir.createFile("foo", .{});
2004 file.close();
2005 try options.maybeAppendRC(tmp.dir);
2006 try std.testing.expectEqualStrings("foo", options.input_source.filename);
2007
2008 // Now delete the file and try again. But this time change the input format
2009 // to non-rc.
2010 try tmp.dir.deleteFile("foo");
2011 options.input_format = .res;
2012 try options.maybeAppendRC(tmp.dir);
2013 try std.testing.expectEqualStrings("foo", options.input_source.filename);
2014
2015 // Finally, reset the input format to rc. Since the verbatim name is no longer found
2016 // and the input filename does not have an extension, .rc should get appended.
2017 options.input_format = .rc;
2018 try options.maybeAppendRC(tmp.dir);
2019 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename);
2020}