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