cli.zig (97797B) - Raw
1 const std = @import("std"); 2 const code_pages = @import("code_pages.zig"); 3 const SupportedCodePage = code_pages.SupportedCodePage; 4 const lang = @import("lang.zig"); 5 const res = @import("res.zig"); 6 const Allocator = std.mem.Allocator; 7 const lex = @import("lex.zig"); 8 const cvtres = @import("cvtres.zig"); 9 10 /// This is what /SL 100 will set the maximum string literal length to 11 pub const max_string_literal_length_100_percent = 8192; 12 13 pub 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 83 pub fn writeUsage(writer: anytype, 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 89 pub const Diagnostics = struct { 90 errors: std.ArrayListUnmanaged(ErrorDetails) = .empty, 91 allocator: Allocator, 92 93 pub const ErrorDetails = struct { 94 arg_index: usize, 95 arg_span: ArgSpan = .{}, 96 msg: std.ArrayListUnmanaged(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, config: std.io.tty.Config) void { 128 const stderr = std.debug.lockStderrWriter(&.{}); 129 defer std.debug.unlockStderrWriter(); 130 self.renderToWriter(args, stderr, config) 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 147 pub const Options = struct { 148 allocator: Allocator, 149 input_source: IoSource = .{ .filename = &[_]u8{} }, 150 output_source: IoSource = .{ .filename = &[_]u8{} }, 151 extra_include_paths: std.ArrayListUnmanaged([]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: anytype) !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 371 pub 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 488 pub 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. 492 pub 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 var msg_writer = err_details.msg.writer(allocator); 524 try msg_writer.print("invalid option: {s}", .{arg.prefixSlice()}); 525 try diagnostics.append(err_details); 526 arg_i += 1; 527 continue :next_arg; 528 }, 529 } 530 } 531 532 const args_remaining = args.len - arg_i; 533 if (args_remaining <= 2 and arg.looksLikeFilepath()) { 534 var err_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i }; 535 var msg_writer = err_details.msg.writer(allocator); 536 try msg_writer.writeAll("this argument was inferred to be a filepath, so argument parsing was terminated"); 537 try diagnostics.append(err_details); 538 539 break; 540 } 541 542 while (arg.name().len > 0) { 543 const arg_name = arg.name(); 544 // Note: These cases should be in order from longest to shortest, since 545 // shorter options that are a substring of a longer one could make 546 // the longer option's branch unreachable. 547 if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) { 548 options.preprocess = .no; 549 arg.name_offset += ":no-preprocess".len; 550 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":output-format")) { 551 const value = arg.value(":output-format".len, arg_i, args) catch { 552 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 553 var msg_writer = err_details.msg.writer(allocator); 554 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":output-format".len) }); 555 try diagnostics.append(err_details); 556 arg_i += 1; 557 break :next_arg; 558 }; 559 output_format = std.meta.stringToEnum(Options.OutputFormat, value.slice) orelse blk: { 560 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 561 var msg_writer = err_details.msg.writer(allocator); 562 try msg_writer.print("invalid output format setting: {s} ", .{value.slice}); 563 try diagnostics.append(err_details); 564 break :blk output_format; 565 }; 566 output_format_context = .{ .index = arg_i, .option_len = ":output-format".len, .arg = arg, .value = value }; 567 arg_i += value.index_increment; 568 continue :next_arg; 569 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) { 570 const value = arg.value(":auto-includes".len, arg_i, args) catch { 571 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 572 var msg_writer = err_details.msg.writer(allocator); 573 try msg_writer.print("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 var msg_writer = err_details.msg.writer(allocator); 581 try msg_writer.print("invalid auto includes setting: {s} ", .{value.slice}); 582 try diagnostics.append(err_details); 583 break :blk options.auto_includes; 584 }; 585 arg_i += value.index_increment; 586 continue :next_arg; 587 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":input-format")) { 588 const value = arg.value(":input-format".len, arg_i, args) catch { 589 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 590 var msg_writer = err_details.msg.writer(allocator); 591 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":input-format".len) }); 592 try diagnostics.append(err_details); 593 arg_i += 1; 594 break :next_arg; 595 }; 596 input_format = std.meta.stringToEnum(Options.InputFormat, value.slice) orelse blk: { 597 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 598 var msg_writer = err_details.msg.writer(allocator); 599 try msg_writer.print("invalid input format setting: {s} ", .{value.slice}); 600 try diagnostics.append(err_details); 601 break :blk input_format; 602 }; 603 input_format_context = .{ .index = arg_i, .option_len = ":input-format".len, .arg = arg, .value = value }; 604 arg_i += value.index_increment; 605 continue :next_arg; 606 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) { 607 const value = arg.value(":depfile-fmt".len, arg_i, args) catch { 608 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 609 var msg_writer = err_details.msg.writer(allocator); 610 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) }); 611 try diagnostics.append(err_details); 612 arg_i += 1; 613 break :next_arg; 614 }; 615 options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: { 616 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 617 var msg_writer = err_details.msg.writer(allocator); 618 try msg_writer.print("invalid depfile format setting: {s} ", .{value.slice}); 619 try diagnostics.append(err_details); 620 break :blk options.depfile_fmt; 621 }; 622 arg_i += value.index_increment; 623 continue :next_arg; 624 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) { 625 const value = arg.value(":depfile".len, arg_i, args) catch { 626 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 627 var msg_writer = err_details.msg.writer(allocator); 628 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) }); 629 try diagnostics.append(err_details); 630 arg_i += 1; 631 break :next_arg; 632 }; 633 if (options.depfile_path) |overwritten_path| { 634 allocator.free(overwritten_path); 635 options.depfile_path = null; 636 } 637 const path = try allocator.dupe(u8, value.slice); 638 errdefer allocator.free(path); 639 options.depfile_path = path; 640 depfile_context = .{ .index = arg_i, .option_len = ":depfile".len, .arg = arg, .value = value }; 641 arg_i += value.index_increment; 642 continue :next_arg; 643 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":target")) { 644 const value = arg.value(":target".len, arg_i, args) catch { 645 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 646 var msg_writer = err_details.msg.writer(allocator); 647 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":target".len) }); 648 try diagnostics.append(err_details); 649 arg_i += 1; 650 break :next_arg; 651 }; 652 // Take the substring up to the first dash so that a full target triple 653 // can be used, e.g. x86_64-windows-gnu becomes x86_64 654 var target_it = std.mem.splitScalar(u8, value.slice, '-'); 655 const arch_str = target_it.first(); 656 const arch = cvtres.supported_targets.Arch.fromStringIgnoreCase(arch_str) orelse { 657 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 658 var msg_writer = err_details.msg.writer(allocator); 659 try msg_writer.print("invalid or unsupported target architecture: {s}", .{arch_str}); 660 try diagnostics.append(err_details); 661 arg_i += value.index_increment; 662 continue :next_arg; 663 }; 664 options.coff_options.target = arch.toCoffMachineType(); 665 arg_i += value.index_increment; 666 continue :next_arg; 667 } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) { 668 // No-op, we don't display any 'logo' to suppress 669 arg.name_offset += "nologo".len; 670 } else if (std.ascii.startsWithIgnoreCase(arg_name, ":debug")) { 671 options.debug = true; 672 arg.name_offset += ":debug".len; 673 } 674 // Unsupported LCX/LCE options that need a value (within the same arg only) 675 else if (std.ascii.startsWithIgnoreCase(arg_name, "tp:")) { 676 const rest = arg.full[arg.name_offset + 3 ..]; 677 if (rest.len == 0) { 678 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = .{ 679 .name_offset = arg.name_offset, 680 .prefix_len = arg.prefixSlice().len, 681 .value_offset = arg.name_offset + 3, 682 } }; 683 var msg_writer = err_details.msg.writer(allocator); 684 try msg_writer.print("missing value for {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) }); 685 try diagnostics.append(err_details); 686 } 687 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 688 var msg_writer = err_details.msg.writer(allocator); 689 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(3) }); 690 try diagnostics.append(err_details); 691 arg_i += 1; 692 continue :next_arg; 693 } 694 // Unsupported LCX/LCE options that need a value 695 else if (std.ascii.startsWithIgnoreCase(arg_name, "tn")) { 696 const value = arg.value(2, arg_i, args) catch no_value: { 697 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 698 var msg_writer = err_details.msg.writer(allocator); 699 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 700 try diagnostics.append(err_details); 701 // dummy zero-length slice starting where the value would have been 702 const value_start = arg.name_offset + 2; 703 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] }; 704 }; 705 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 706 var msg_writer = err_details.msg.writer(allocator); 707 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 708 try diagnostics.append(err_details); 709 arg_i += value.index_increment; 710 continue :next_arg; 711 } 712 // Unsupported MUI options that need a value 713 else if (std.ascii.startsWithIgnoreCase(arg_name, "fm") or 714 std.ascii.startsWithIgnoreCase(arg_name, "gn") or 715 std.ascii.startsWithIgnoreCase(arg_name, "g2")) 716 { 717 const value = arg.value(2, arg_i, args) catch no_value: { 718 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 719 var msg_writer = err_details.msg.writer(allocator); 720 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 721 try diagnostics.append(err_details); 722 // dummy zero-length slice starting where the value would have been 723 const value_start = arg.name_offset + 2; 724 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] }; 725 }; 726 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 727 var msg_writer = err_details.msg.writer(allocator); 728 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 729 try diagnostics.append(err_details); 730 arg_i += value.index_increment; 731 continue :next_arg; 732 } 733 // Unsupported MUI options that do not need a value 734 else if (std.ascii.startsWithIgnoreCase(arg_name, "g1")) { 735 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) }; 736 var msg_writer = err_details.msg.writer(allocator); 737 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 738 try diagnostics.append(err_details); 739 arg.name_offset += 2; 740 } 741 // Unsupported LCX/LCE options that do not need a value 742 else if (std.ascii.startsWithIgnoreCase(arg_name, "tm") or 743 std.ascii.startsWithIgnoreCase(arg_name, "tc") or 744 std.ascii.startsWithIgnoreCase(arg_name, "tw") or 745 std.ascii.startsWithIgnoreCase(arg_name, "te") or 746 std.ascii.startsWithIgnoreCase(arg_name, "ti") or 747 std.ascii.startsWithIgnoreCase(arg_name, "ta")) 748 { 749 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(2) }; 750 var msg_writer = err_details.msg.writer(allocator); 751 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 752 try diagnostics.append(err_details); 753 arg.name_offset += 2; 754 } else if (std.ascii.startsWithIgnoreCase(arg_name, "fo")) { 755 const value = arg.value(2, arg_i, args) catch { 756 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 757 var msg_writer = err_details.msg.writer(allocator); 758 try msg_writer.print("missing output path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 759 try diagnostics.append(err_details); 760 arg_i += 1; 761 break :next_arg; 762 }; 763 output_filename_context = .{ .arg = .{ .index = arg_i, .option_len = "fo".len, .arg = arg, .value = value } }; 764 output_filename = value.slice; 765 arg_i += value.index_increment; 766 continue :next_arg; 767 } else if (std.ascii.startsWithIgnoreCase(arg_name, "sl")) { 768 const value = arg.value(2, arg_i, args) catch { 769 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 770 var msg_writer = err_details.msg.writer(allocator); 771 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 772 try diagnostics.append(err_details); 773 arg_i += 1; 774 break :next_arg; 775 }; 776 const percent_str = value.slice; 777 const percent: u32 = parsePercent(percent_str) catch { 778 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 779 var msg_writer = err_details.msg.writer(allocator); 780 try msg_writer.print("invalid percent format '{s}'", .{percent_str}); 781 try diagnostics.append(err_details); 782 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i }; 783 var note_writer = note_details.msg.writer(allocator); 784 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)"); 785 try diagnostics.append(note_details); 786 arg_i += value.index_increment; 787 continue :next_arg; 788 }; 789 if (percent == 0 or percent > 100) { 790 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 791 var msg_writer = err_details.msg.writer(allocator); 792 try msg_writer.print("percent out of range: {} (parsed from '{s}')", .{ percent, percent_str }); 793 try diagnostics.append(err_details); 794 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = false, .arg_index = arg_i }; 795 var note_writer = note_details.msg.writer(allocator); 796 try note_writer.writeAll("string length percent must be an integer between 1 and 100 (inclusive)"); 797 try diagnostics.append(note_details); 798 arg_i += value.index_increment; 799 continue :next_arg; 800 } 801 const percent_float = @as(f32, @floatFromInt(percent)) / 100; 802 options.max_string_literal_codepoints = @intFromFloat(percent_float * max_string_literal_length_100_percent); 803 arg_i += value.index_increment; 804 continue :next_arg; 805 } else if (std.ascii.startsWithIgnoreCase(arg_name, "ln")) { 806 const value = arg.value(2, arg_i, args) catch { 807 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 808 var msg_writer = err_details.msg.writer(allocator); 809 try msg_writer.print("missing language tag after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(2) }); 810 try diagnostics.append(err_details); 811 arg_i += 1; 812 break :next_arg; 813 }; 814 const tag = value.slice; 815 options.default_language_id = lang.tagToInt(tag) catch { 816 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 817 var msg_writer = err_details.msg.writer(allocator); 818 try msg_writer.print("invalid language tag: {s}", .{tag}); 819 try diagnostics.append(err_details); 820 arg_i += value.index_increment; 821 continue :next_arg; 822 }; 823 if (options.default_language_id.? == lang.LOCALE_CUSTOM_UNSPECIFIED) { 824 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 825 var msg_writer = err_details.msg.writer(allocator); 826 try msg_writer.print("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 }); 827 try diagnostics.append(err_details); 828 } 829 arg_i += value.index_increment; 830 continue :next_arg; 831 } else if (std.ascii.startsWithIgnoreCase(arg_name, "l")) { 832 const value = arg.value(1, arg_i, args) catch { 833 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 834 var msg_writer = err_details.msg.writer(allocator); 835 try msg_writer.print("missing language ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 836 try diagnostics.append(err_details); 837 arg_i += 1; 838 break :next_arg; 839 }; 840 const num_str = value.slice; 841 options.default_language_id = lang.parseInt(num_str) catch { 842 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 843 var msg_writer = err_details.msg.writer(allocator); 844 try msg_writer.print("invalid language ID: {s}", .{num_str}); 845 try diagnostics.append(err_details); 846 arg_i += value.index_increment; 847 continue :next_arg; 848 }; 849 arg_i += value.index_increment; 850 continue :next_arg; 851 } else if (std.ascii.startsWithIgnoreCase(arg_name, "h") or std.mem.startsWith(u8, arg_name, "?")) { 852 options.print_help_and_exit = true; 853 // If there's been an error to this point, then we still want to fail 854 if (diagnostics.hasError()) return error.ParseError; 855 return options; 856 } 857 // 1 char unsupported MUI options that need a value 858 else if (std.ascii.startsWithIgnoreCase(arg_name, "q") or 859 std.ascii.startsWithIgnoreCase(arg_name, "g")) 860 { 861 const value = arg.value(1, arg_i, args) catch no_value: { 862 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 863 var msg_writer = err_details.msg.writer(allocator); 864 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 865 try diagnostics.append(err_details); 866 // dummy zero-length slice starting where the value would have been 867 const value_start = arg.name_offset + 1; 868 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] }; 869 }; 870 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 871 var msg_writer = err_details.msg.writer(allocator); 872 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 873 try diagnostics.append(err_details); 874 arg_i += value.index_increment; 875 continue :next_arg; 876 } 877 // Undocumented (and unsupported) options that need a value 878 // /z has to do something with font substitution 879 // /s has something to do with HWB resources being inserted into the .res 880 else if (std.ascii.startsWithIgnoreCase(arg_name, "z") or 881 std.ascii.startsWithIgnoreCase(arg_name, "s")) 882 { 883 const value = arg.value(1, arg_i, args) catch no_value: { 884 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 885 var msg_writer = err_details.msg.writer(allocator); 886 try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 887 try diagnostics.append(err_details); 888 // dummy zero-length slice starting where the value would have been 889 const value_start = arg.name_offset + 1; 890 break :no_value Arg.Value{ .slice = arg.full[value_start..value_start] }; 891 }; 892 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 893 var msg_writer = err_details.msg.writer(allocator); 894 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 895 try diagnostics.append(err_details); 896 arg_i += value.index_increment; 897 continue :next_arg; 898 } 899 // 1 char unsupported LCX/LCE options that do not need a value 900 else if (std.ascii.startsWithIgnoreCase(arg_name, "t")) { 901 var err_details = Diagnostics.ErrorDetails{ .type = .err, .arg_index = arg_i, .arg_span = arg.optionSpan(1) }; 902 var msg_writer = err_details.msg.writer(allocator); 903 try msg_writer.print("the {s}{s} option is unsupported", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 904 try diagnostics.append(err_details); 905 arg.name_offset += 1; 906 } else if (std.ascii.startsWithIgnoreCase(arg_name, "c")) { 907 const value = arg.value(1, arg_i, args) catch { 908 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 909 var msg_writer = err_details.msg.writer(allocator); 910 try msg_writer.print("missing code page ID after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 911 try diagnostics.append(err_details); 912 arg_i += 1; 913 break :next_arg; 914 }; 915 const num_str = value.slice; 916 const code_page_id = std.fmt.parseUnsigned(u16, num_str, 10) catch { 917 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 918 var msg_writer = err_details.msg.writer(allocator); 919 try msg_writer.print("invalid code page ID: {s}", .{num_str}); 920 try diagnostics.append(err_details); 921 arg_i += value.index_increment; 922 continue :next_arg; 923 }; 924 options.default_code_page = code_pages.getByIdentifierEnsureSupported(code_page_id) catch |err| switch (err) { 925 error.InvalidCodePage => { 926 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 927 var msg_writer = err_details.msg.writer(allocator); 928 try msg_writer.print("invalid or unknown code page ID: {}", .{code_page_id}); 929 try diagnostics.append(err_details); 930 arg_i += value.index_increment; 931 continue :next_arg; 932 }, 933 error.UnsupportedCodePage => { 934 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 935 var msg_writer = err_details.msg.writer(allocator); 936 try msg_writer.print("unsupported code page: {s} (id={})", .{ 937 @tagName(code_pages.getByIdentifier(code_page_id) catch unreachable), 938 code_page_id, 939 }); 940 try diagnostics.append(err_details); 941 arg_i += value.index_increment; 942 continue :next_arg; 943 }, 944 }; 945 arg_i += value.index_increment; 946 continue :next_arg; 947 } else if (std.ascii.startsWithIgnoreCase(arg_name, "v")) { 948 options.verbose = true; 949 arg.name_offset += 1; 950 } else if (std.ascii.startsWithIgnoreCase(arg_name, "x")) { 951 options.ignore_include_env_var = true; 952 arg.name_offset += 1; 953 } else if (std.ascii.startsWithIgnoreCase(arg_name, "p")) { 954 options.preprocess = .only; 955 preprocess_only_context = .{ .index = arg_i, .option_len = "p".len, .arg = arg, .value = undefined }; 956 arg.name_offset += 1; 957 } else if (std.ascii.startsWithIgnoreCase(arg_name, "i")) { 958 const value = arg.value(1, arg_i, args) catch { 959 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 960 var msg_writer = err_details.msg.writer(allocator); 961 try msg_writer.print("missing include path after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 962 try diagnostics.append(err_details); 963 arg_i += 1; 964 break :next_arg; 965 }; 966 const path = value.slice; 967 const duped = try allocator.dupe(u8, path); 968 errdefer allocator.free(duped); 969 try options.extra_include_paths.append(options.allocator, duped); 970 arg_i += value.index_increment; 971 continue :next_arg; 972 } else if (std.ascii.startsWithIgnoreCase(arg_name, "r")) { 973 // From https://learn.microsoft.com/en-us/windows/win32/menurc/using-rc-the-rc-command-line- 974 // "Ignored. Provided for compatibility with existing makefiles." 975 arg.name_offset += 1; 976 } else if (std.ascii.startsWithIgnoreCase(arg_name, "n")) { 977 options.null_terminate_string_table_strings = true; 978 arg.name_offset += 1; 979 } else if (std.ascii.startsWithIgnoreCase(arg_name, "y")) { 980 options.silent_duplicate_control_ids = true; 981 arg.name_offset += 1; 982 } else if (std.ascii.startsWithIgnoreCase(arg_name, "w")) { 983 options.warn_instead_of_error_on_invalid_code_page = true; 984 arg.name_offset += 1; 985 } else if (std.ascii.startsWithIgnoreCase(arg_name, "a")) { 986 // Undocumented option with unknown function 987 // TODO: More investigation to figure out what it does (if anything) 988 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = arg.optionSpan(1) }; 989 var msg_writer = err_details.msg.writer(allocator); 990 try msg_writer.print("option {s}{s} has no effect (it is undocumented and its function is unknown in the Win32 RC compiler)", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 991 try diagnostics.append(err_details); 992 arg.name_offset += 1; 993 } else if (std.ascii.startsWithIgnoreCase(arg_name, "d")) { 994 const value = arg.value(1, arg_i, args) catch { 995 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 996 var msg_writer = err_details.msg.writer(allocator); 997 try msg_writer.print("missing symbol to define after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 998 try diagnostics.append(err_details); 999 arg_i += 1; 1000 break :next_arg; 1001 }; 1002 var tokenizer = std.mem.tokenizeScalar(u8, value.slice, '='); 1003 // guaranteed to exist since an empty value.slice would invoke 1004 // the 'missing symbol to define' branch above 1005 const symbol = tokenizer.next().?; 1006 const symbol_value = tokenizer.next() orelse "1"; 1007 1008 if (isValidIdentifier(symbol)) { 1009 try options.define(symbol, symbol_value); 1010 } else { 1011 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 1012 var msg_writer = err_details.msg.writer(allocator); 1013 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be defined", .{symbol}); 1014 try diagnostics.append(err_details); 1015 } 1016 arg_i += value.index_increment; 1017 continue :next_arg; 1018 } else if (std.ascii.startsWithIgnoreCase(arg_name, "u")) { 1019 const value = arg.value(1, arg_i, args) catch { 1020 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() }; 1021 var msg_writer = err_details.msg.writer(allocator); 1022 try msg_writer.print("missing symbol to undefine after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(1) }); 1023 try diagnostics.append(err_details); 1024 arg_i += 1; 1025 break :next_arg; 1026 }; 1027 const symbol = value.slice; 1028 if (isValidIdentifier(symbol)) { 1029 try options.undefine(symbol); 1030 } else { 1031 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = arg_i, .arg_span = value.argSpan(arg) }; 1032 var msg_writer = err_details.msg.writer(allocator); 1033 try msg_writer.print("symbol \"{s}\" is not a valid identifier and therefore cannot be undefined", .{symbol}); 1034 try diagnostics.append(err_details); 1035 } 1036 arg_i += value.index_increment; 1037 continue :next_arg; 1038 } else { 1039 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.optionAndAfterSpan() }; 1040 var msg_writer = err_details.msg.writer(allocator); 1041 try msg_writer.print("invalid option: {s}{s}", .{ arg.prefixSlice(), arg.name() }); 1042 try diagnostics.append(err_details); 1043 arg_i += 1; 1044 continue :next_arg; 1045 } 1046 } else { 1047 // The while loop exited via its conditional, meaning we are done with 1048 // the current arg and can move on the the next 1049 arg_i += 1; 1050 continue; 1051 } 1052 } 1053 1054 const positionals = args[arg_i..]; 1055 1056 if (positionals.len == 0) { 1057 var err_details = Diagnostics.ErrorDetails{ .print_args = false, .arg_index = arg_i }; 1058 var msg_writer = err_details.msg.writer(allocator); 1059 try msg_writer.writeAll("missing input filename"); 1060 try diagnostics.append(err_details); 1061 1062 if (args.len > 0) { 1063 const last_arg = args[args.len - 1]; 1064 if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and isSupportedInputExtension(std.fs.path.extension(last_arg))) { 1065 var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 }; 1066 var note_writer = note_details.msg.writer(allocator); 1067 try note_writer.writeAll("if this argument was intended to be the input filename, adding -- in front of it will exclude it from option parsing"); 1068 try diagnostics.append(note_details); 1069 } 1070 } 1071 1072 // This is a fatal enough problem to justify an early return, since 1073 // things after this rely on the value of the input filename. 1074 return error.ParseError; 1075 } 1076 options.input_source = .{ .filename = try allocator.dupe(u8, positionals[0]) }; 1077 input_filename_arg_i = arg_i; 1078 1079 const InputFormatSource = enum { 1080 inferred_from_input_filename, 1081 input_format_arg, 1082 }; 1083 1084 var input_format_source: InputFormatSource = undefined; 1085 if (input_format == null) { 1086 const ext = std.fs.path.extension(options.input_source.filename); 1087 if (std.ascii.eqlIgnoreCase(ext, ".res")) { 1088 input_format = .res; 1089 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { 1090 input_format = .rcpp; 1091 } else { 1092 input_format = .rc; 1093 } 1094 input_format_source = .inferred_from_input_filename; 1095 } else { 1096 input_format_source = .input_format_arg; 1097 } 1098 1099 if (positionals.len > 1) { 1100 if (output_filename != null) { 1101 var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i + 1 }; 1102 var msg_writer = err_details.msg.writer(allocator); 1103 try msg_writer.writeAll("output filename already specified"); 1104 try diagnostics.append(err_details); 1105 var note_details = Diagnostics.ErrorDetails{ 1106 .type = .note, 1107 .arg_index = output_filename_context.arg.index, 1108 .arg_span = output_filename_context.arg.value.argSpan(output_filename_context.arg.arg), 1109 }; 1110 var note_writer = note_details.msg.writer(allocator); 1111 try note_writer.writeAll("output filename previously specified here"); 1112 try diagnostics.append(note_details); 1113 } else { 1114 output_filename = positionals[1]; 1115 output_filename_context = .{ .positional = arg_i + 1 }; 1116 } 1117 } 1118 1119 const OutputFormatSource = enum { 1120 inferred_from_input_filename, 1121 inferred_from_output_filename, 1122 output_format_arg, 1123 unable_to_infer_from_input_filename, 1124 unable_to_infer_from_output_filename, 1125 inferred_from_preprocess_only, 1126 }; 1127 1128 var output_format_source: OutputFormatSource = undefined; 1129 if (output_filename == null) { 1130 if (output_format == null) { 1131 output_format_source = .inferred_from_input_filename; 1132 const input_ext = std.fs.path.extension(options.input_source.filename); 1133 if (std.ascii.eqlIgnoreCase(input_ext, ".res")) { 1134 output_format = .coff; 1135 } else if (options.preprocess == .only and (input_format.? == .rc or std.ascii.eqlIgnoreCase(input_ext, ".rc"))) { 1136 output_format = .rcpp; 1137 output_format_source = .inferred_from_preprocess_only; 1138 } else { 1139 if (!std.ascii.eqlIgnoreCase(input_ext, ".res")) { 1140 output_format_source = .unable_to_infer_from_input_filename; 1141 } 1142 output_format = .res; 1143 } 1144 } else { 1145 output_format_source = .output_format_arg; 1146 } 1147 options.output_source = .{ .filename = try filepathWithExtension(allocator, options.input_source.filename, output_format.?.extension()) }; 1148 } else { 1149 options.output_source = .{ .filename = try allocator.dupe(u8, output_filename.?) }; 1150 if (output_format == null) { 1151 output_format_source = .inferred_from_output_filename; 1152 const ext = std.fs.path.extension(options.output_source.filename); 1153 if (std.ascii.eqlIgnoreCase(ext, ".obj") or std.ascii.eqlIgnoreCase(ext, ".o")) { 1154 output_format = .coff; 1155 } else if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) { 1156 output_format = .rcpp; 1157 } else { 1158 if (!std.ascii.eqlIgnoreCase(ext, ".res")) { 1159 output_format_source = .unable_to_infer_from_output_filename; 1160 } 1161 output_format = .res; 1162 } 1163 } else { 1164 output_format_source = .output_format_arg; 1165 } 1166 } 1167 1168 options.input_format = input_format.?; 1169 options.output_format = output_format.?; 1170 1171 // Check for incompatible options 1172 var print_input_format_source_note: bool = false; 1173 var print_output_format_source_note: bool = false; 1174 if (options.depfile_path != null and (options.input_format == .res or options.output_format == .rcpp)) { 1175 var err_details = Diagnostics.ErrorDetails{ .type = .warning, .arg_index = depfile_context.index, .arg_span = depfile_context.value.argSpan(depfile_context.arg) }; 1176 var msg_writer = err_details.msg.writer(allocator); 1177 if (options.input_format == .res) { 1178 try msg_writer.print("the {s}{s} option was ignored because the input format is '{s}'", .{ 1179 depfile_context.arg.prefixSlice(), 1180 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), 1181 @tagName(options.input_format), 1182 }); 1183 print_input_format_source_note = true; 1184 } else if (options.output_format == .rcpp) { 1185 try msg_writer.print("the {s}{s} option was ignored because the output format is '{s}'", .{ 1186 depfile_context.arg.prefixSlice(), 1187 depfile_context.arg.optionWithoutPrefix(depfile_context.option_len), 1188 @tagName(options.output_format), 1189 }); 1190 print_output_format_source_note = true; 1191 } 1192 try diagnostics.append(err_details); 1193 } 1194 if (!isSupportedTransformation(options.input_format, options.output_format)) { 1195 var err_details = Diagnostics.ErrorDetails{ .arg_index = input_filename_arg_i, .print_args = false }; 1196 var msg_writer = err_details.msg.writer(allocator); 1197 try msg_writer.print("input format '{s}' cannot be converted to output format '{s}'", .{ @tagName(options.input_format), @tagName(options.output_format) }); 1198 try diagnostics.append(err_details); 1199 print_input_format_source_note = true; 1200 print_output_format_source_note = true; 1201 } 1202 if (options.preprocess == .only and options.output_format != .rcpp) { 1203 var err_details = Diagnostics.ErrorDetails{ .arg_index = preprocess_only_context.index }; 1204 var msg_writer = err_details.msg.writer(allocator); 1205 try msg_writer.print("the {s}{s} option cannot be used with output format '{s}'", .{ 1206 preprocess_only_context.arg.prefixSlice(), 1207 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), 1208 @tagName(options.output_format), 1209 }); 1210 try diagnostics.append(err_details); 1211 print_output_format_source_note = true; 1212 } 1213 if (print_input_format_source_note) { 1214 switch (input_format_source) { 1215 .inferred_from_input_filename => { 1216 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; 1217 var msg_writer = err_details.msg.writer(allocator); 1218 try msg_writer.writeAll("the input format was inferred from the input filename"); 1219 try diagnostics.append(err_details); 1220 }, 1221 .input_format_arg => { 1222 var err_details = Diagnostics.ErrorDetails{ 1223 .type = .note, 1224 .arg_index = input_format_context.index, 1225 .arg_span = input_format_context.value.argSpan(input_format_context.arg), 1226 }; 1227 var msg_writer = err_details.msg.writer(allocator); 1228 try msg_writer.writeAll("the input format was specified here"); 1229 try diagnostics.append(err_details); 1230 }, 1231 } 1232 } 1233 if (print_output_format_source_note) { 1234 switch (output_format_source) { 1235 .inferred_from_input_filename, .unable_to_infer_from_input_filename => { 1236 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = input_filename_arg_i }; 1237 var msg_writer = err_details.msg.writer(allocator); 1238 if (output_format_source == .inferred_from_input_filename) { 1239 try msg_writer.writeAll("the output format was inferred from the input filename"); 1240 } else { 1241 try msg_writer.writeAll("the output format was unable to be inferred from the input filename, so the default was used"); 1242 } 1243 try diagnostics.append(err_details); 1244 }, 1245 .inferred_from_output_filename, .unable_to_infer_from_output_filename => { 1246 var err_details: Diagnostics.ErrorDetails = switch (output_filename_context) { 1247 .positional => |i| .{ .type = .note, .arg_index = i }, 1248 .arg => |ctx| .{ .type = .note, .arg_index = ctx.index, .arg_span = ctx.value.argSpan(ctx.arg) }, 1249 .unspecified => unreachable, 1250 }; 1251 var msg_writer = err_details.msg.writer(allocator); 1252 if (output_format_source == .inferred_from_output_filename) { 1253 try msg_writer.writeAll("the output format was inferred from the output filename"); 1254 } else { 1255 try msg_writer.writeAll("the output format was unable to be inferred from the output filename, so the default was used"); 1256 } 1257 try diagnostics.append(err_details); 1258 }, 1259 .output_format_arg => { 1260 var err_details = Diagnostics.ErrorDetails{ 1261 .type = .note, 1262 .arg_index = output_format_context.index, 1263 .arg_span = output_format_context.value.argSpan(output_format_context.arg), 1264 }; 1265 var msg_writer = err_details.msg.writer(allocator); 1266 try msg_writer.writeAll("the output format was specified here"); 1267 try diagnostics.append(err_details); 1268 }, 1269 .inferred_from_preprocess_only => { 1270 var err_details = Diagnostics.ErrorDetails{ .type = .note, .arg_index = preprocess_only_context.index }; 1271 var msg_writer = err_details.msg.writer(allocator); 1272 try msg_writer.print("the output format was inferred from the usage of the {s}{s} option", .{ 1273 preprocess_only_context.arg.prefixSlice(), 1274 preprocess_only_context.arg.optionWithoutPrefix(preprocess_only_context.option_len), 1275 }); 1276 try diagnostics.append(err_details); 1277 }, 1278 } 1279 } 1280 1281 if (diagnostics.hasError()) { 1282 return error.ParseError; 1283 } 1284 1285 // Implied settings from input/output formats 1286 if (options.output_format == .rcpp) options.preprocess = .only; 1287 if (options.input_format == .res) options.output_format = .coff; 1288 if (options.input_format == .rcpp) options.preprocess = .no; 1289 1290 return options; 1291 } 1292 1293 pub fn filepathWithExtension(allocator: Allocator, path: []const u8, ext: []const u8) ![]const u8 { 1294 var buf = std.array_list.Managed(u8).init(allocator); 1295 errdefer buf.deinit(); 1296 if (std.fs.path.dirname(path)) |dirname| { 1297 var end_pos = dirname.len; 1298 // We want to ensure that we write a path separator at the end, so if the dirname 1299 // doesn't end with a path sep then include the char after the dirname 1300 // which must be a path sep. 1301 if (!std.fs.path.isSep(dirname[dirname.len - 1])) end_pos += 1; 1302 try buf.appendSlice(path[0..end_pos]); 1303 } 1304 try buf.appendSlice(std.fs.path.stem(path)); 1305 try buf.appendSlice(ext); 1306 return try buf.toOwnedSlice(); 1307 } 1308 1309 pub fn isSupportedInputExtension(ext: []const u8) bool { 1310 if (std.ascii.eqlIgnoreCase(ext, ".rc")) return true; 1311 if (std.ascii.eqlIgnoreCase(ext, ".res")) return true; 1312 if (std.ascii.eqlIgnoreCase(ext, ".rcpp")) return true; 1313 return false; 1314 } 1315 1316 pub fn isSupportedTransformation(input: Options.InputFormat, output: Options.OutputFormat) bool { 1317 return switch (input) { 1318 .rc => switch (output) { 1319 .res => true, 1320 .coff => true, 1321 .rcpp => true, 1322 }, 1323 .res => switch (output) { 1324 .res => false, 1325 .coff => true, 1326 .rcpp => false, 1327 }, 1328 .rcpp => switch (output) { 1329 .res => true, 1330 .coff => true, 1331 .rcpp => false, 1332 }, 1333 }; 1334 } 1335 1336 /// Returns true if the str is a valid C identifier for use in a #define/#undef macro 1337 pub fn isValidIdentifier(str: []const u8) bool { 1338 for (str, 0..) |c, i| switch (c) { 1339 '0'...'9' => if (i == 0) return false, 1340 'a'...'z', 'A'...'Z', '_' => {}, 1341 else => return false, 1342 }; 1343 return true; 1344 } 1345 1346 /// This function is specific to how the Win32 RC command line interprets 1347 /// max string literal length percent. 1348 /// - Wraps on overflow of u32 1349 /// - Stops parsing on any invalid hexadecimal digits 1350 /// - Errors if a digit is not the first char 1351 /// - `-` (negative) prefix is allowed 1352 pub fn parsePercent(str: []const u8) error{InvalidFormat}!u32 { 1353 var result: u32 = 0; 1354 const radix: u8 = 10; 1355 var buf = str; 1356 1357 const Prefix = enum { none, minus }; 1358 var prefix: Prefix = .none; 1359 switch (buf[0]) { 1360 '-' => { 1361 prefix = .minus; 1362 buf = buf[1..]; 1363 }, 1364 else => {}, 1365 } 1366 1367 for (buf, 0..) |c, i| { 1368 const digit = switch (c) { 1369 // On invalid digit for the radix, just stop parsing but don't fail 1370 '0'...'9' => std.fmt.charToDigit(c, radix) catch break, 1371 else => { 1372 // First digit must be valid 1373 if (i == 0) { 1374 return error.InvalidFormat; 1375 } 1376 break; 1377 }, 1378 }; 1379 1380 if (result != 0) { 1381 result *%= radix; 1382 } 1383 result +%= digit; 1384 } 1385 1386 switch (prefix) { 1387 .none => {}, 1388 .minus => result = 0 -% result, 1389 } 1390 1391 return result; 1392 } 1393 1394 test parsePercent { 1395 try std.testing.expectEqual(@as(u32, 16), try parsePercent("16")); 1396 try std.testing.expectEqual(@as(u32, 0), try parsePercent("0x1A")); 1397 try std.testing.expectEqual(@as(u32, 0x1), try parsePercent("1zzzz")); 1398 try std.testing.expectEqual(@as(u32, 0xffffffff), try parsePercent("-1")); 1399 try std.testing.expectEqual(@as(u32, 0xfffffff0), try parsePercent("-16")); 1400 try std.testing.expectEqual(@as(u32, 1), try parsePercent("4294967297")); 1401 try std.testing.expectError(error.InvalidFormat, parsePercent("--1")); 1402 try std.testing.expectError(error.InvalidFormat, parsePercent("ha")); 1403 try std.testing.expectError(error.InvalidFormat, parsePercent("ยน")); 1404 try std.testing.expectError(error.InvalidFormat, parsePercent("~1")); 1405 } 1406 1407 pub fn renderErrorMessage(writer: *std.io.Writer, config: std.io.tty.Config, err_details: Diagnostics.ErrorDetails, args: []const []const u8) !void { 1408 try config.setColor(writer, .dim); 1409 try writer.writeAll("<cli>"); 1410 try config.setColor(writer, .reset); 1411 try config.setColor(writer, .bold); 1412 try writer.writeAll(": "); 1413 switch (err_details.type) { 1414 .err => { 1415 try config.setColor(writer, .red); 1416 try writer.writeAll("error: "); 1417 }, 1418 .warning => { 1419 try config.setColor(writer, .yellow); 1420 try writer.writeAll("warning: "); 1421 }, 1422 .note => { 1423 try config.setColor(writer, .cyan); 1424 try writer.writeAll("note: "); 1425 }, 1426 } 1427 try config.setColor(writer, .reset); 1428 try config.setColor(writer, .bold); 1429 try writer.writeAll(err_details.msg.items); 1430 try writer.writeByte('\n'); 1431 try config.setColor(writer, .reset); 1432 1433 if (!err_details.print_args) { 1434 try writer.writeByte('\n'); 1435 return; 1436 } 1437 1438 try config.setColor(writer, .dim); 1439 const prefix = " ... "; 1440 try writer.writeAll(prefix); 1441 try config.setColor(writer, .reset); 1442 1443 const arg_with_name = args[err_details.arg_index]; 1444 const prefix_slice = arg_with_name[0..err_details.arg_span.prefix_len]; 1445 const before_name_slice = arg_with_name[err_details.arg_span.prefix_len..err_details.arg_span.name_offset]; 1446 var name_slice = arg_with_name[err_details.arg_span.name_offset..]; 1447 if (err_details.arg_span.name_len > 0) name_slice.len = err_details.arg_span.name_len; 1448 const after_name_slice = arg_with_name[err_details.arg_span.name_offset + name_slice.len ..]; 1449 1450 try writer.writeAll(prefix_slice); 1451 if (before_name_slice.len > 0) { 1452 try config.setColor(writer, .dim); 1453 try writer.writeAll(before_name_slice); 1454 try config.setColor(writer, .reset); 1455 } 1456 try writer.writeAll(name_slice); 1457 if (after_name_slice.len > 0) { 1458 try config.setColor(writer, .dim); 1459 try writer.writeAll(after_name_slice); 1460 try config.setColor(writer, .reset); 1461 } 1462 1463 var next_arg_len: usize = 0; 1464 if (err_details.arg_span.point_at_next_arg and err_details.arg_index + 1 < args.len) { 1465 const next_arg = args[err_details.arg_index + 1]; 1466 try writer.writeByte(' '); 1467 try writer.writeAll(next_arg); 1468 next_arg_len = next_arg.len; 1469 } 1470 1471 const last_shown_arg_index = if (err_details.arg_span.point_at_next_arg) err_details.arg_index + 1 else err_details.arg_index; 1472 if (last_shown_arg_index + 1 < args.len) { 1473 // special case for when pointing to a missing value within the same arg 1474 // as the name 1475 if (err_details.arg_span.value_offset >= arg_with_name.len) { 1476 try writer.writeByte(' '); 1477 } 1478 try config.setColor(writer, .dim); 1479 try writer.writeAll(" ..."); 1480 try config.setColor(writer, .reset); 1481 } 1482 try writer.writeByte('\n'); 1483 1484 try config.setColor(writer, .green); 1485 try writer.splatByteAll(' ', prefix.len); 1486 // Special case for when the option is *only* a prefix (e.g. invalid option: -) 1487 if (err_details.arg_span.prefix_len == arg_with_name.len) { 1488 try writer.splatByteAll('^', err_details.arg_span.prefix_len); 1489 } else { 1490 try writer.splatByteAll('~', err_details.arg_span.prefix_len); 1491 try writer.splatByteAll(' ', err_details.arg_span.name_offset - err_details.arg_span.prefix_len); 1492 if (!err_details.arg_span.point_at_next_arg and err_details.arg_span.value_offset == 0) { 1493 try writer.writeByte('^'); 1494 try writer.splatByteAll('~', name_slice.len - 1); 1495 } else if (err_details.arg_span.value_offset > 0) { 1496 try writer.splatByteAll('~', err_details.arg_span.value_offset - err_details.arg_span.name_offset); 1497 try writer.writeByte('^'); 1498 if (err_details.arg_span.value_offset < arg_with_name.len) { 1499 try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.value_offset - 1); 1500 } 1501 } else if (err_details.arg_span.point_at_next_arg) { 1502 try writer.splatByteAll('~', arg_with_name.len - err_details.arg_span.name_offset + 1); 1503 try writer.writeByte('^'); 1504 if (next_arg_len > 0) { 1505 try writer.splatByteAll('~', next_arg_len - 1); 1506 } 1507 } 1508 } 1509 try writer.writeByte('\n'); 1510 try config.setColor(writer, .reset); 1511 } 1512 1513 fn testParse(args: []const []const u8) !Options { 1514 return (try testParseOutput(args, "")).?; 1515 } 1516 1517 fn testParseWarning(args: []const []const u8, expected_output: []const u8) !Options { 1518 return (try testParseOutput(args, expected_output)).?; 1519 } 1520 1521 fn testParseError(args: []const []const u8, expected_output: []const u8) !void { 1522 var maybe_options = try testParseOutput(args, expected_output); 1523 if (maybe_options != null) { 1524 std.debug.print("expected error, got options: {}\n", .{maybe_options.?}); 1525 maybe_options.?.deinit(); 1526 return error.TestExpectedError; 1527 } 1528 } 1529 1530 fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Options { 1531 var diagnostics = Diagnostics.init(std.testing.allocator); 1532 defer diagnostics.deinit(); 1533 1534 var output: std.io.Writer.Allocating = .init(std.testing.allocator); 1535 defer output.deinit(); 1536 1537 var options = parse(std.testing.allocator, args, &diagnostics) catch |err| switch (err) { 1538 error.ParseError => { 1539 try diagnostics.renderToWriter(args, &output.writer, .no_color); 1540 try std.testing.expectEqualStrings(expected_output, output.getWritten()); 1541 return null; 1542 }, 1543 else => |e| return e, 1544 }; 1545 errdefer options.deinit(); 1546 1547 try diagnostics.renderToWriter(args, &output.writer, .no_color); 1548 try std.testing.expectEqualStrings(expected_output, output.getWritten()); 1549 return options; 1550 } 1551 1552 test "parse errors: basic" { 1553 try testParseError(&.{"/"}, 1554 \\<cli>: error: invalid option: / 1555 \\ ... / 1556 \\ ^ 1557 \\<cli>: error: missing input filename 1558 \\ 1559 \\ 1560 ); 1561 try testParseError(&.{"/ln"}, 1562 \\<cli>: error: missing language tag after /ln option 1563 \\ ... /ln 1564 \\ ~~~~^ 1565 \\<cli>: error: missing input filename 1566 \\ 1567 \\ 1568 ); 1569 try testParseError(&.{"-vln"}, 1570 \\<cli>: error: missing language tag after -ln option 1571 \\ ... -vln 1572 \\ ~ ~~~^ 1573 \\<cli>: error: missing input filename 1574 \\ 1575 \\ 1576 ); 1577 try testParseError(&.{"/_not-an-option"}, 1578 \\<cli>: error: invalid option: /_not-an-option 1579 \\ ... /_not-an-option 1580 \\ ~^~~~~~~~~~~~~~ 1581 \\<cli>: error: missing input filename 1582 \\ 1583 \\ 1584 ); 1585 try testParseError(&.{"-_not-an-option"}, 1586 \\<cli>: error: invalid option: -_not-an-option 1587 \\ ... -_not-an-option 1588 \\ ~^~~~~~~~~~~~~~ 1589 \\<cli>: error: missing input filename 1590 \\ 1591 \\ 1592 ); 1593 try testParseError(&.{"--_not-an-option"}, 1594 \\<cli>: error: invalid option: --_not-an-option 1595 \\ ... --_not-an-option 1596 \\ ~~^~~~~~~~~~~~~~ 1597 \\<cli>: error: missing input filename 1598 \\ 1599 \\ 1600 ); 1601 try testParseError(&.{"/v_not-an-option"}, 1602 \\<cli>: error: invalid option: /_not-an-option 1603 \\ ... /v_not-an-option 1604 \\ ~ ^~~~~~~~~~~~~~ 1605 \\<cli>: error: missing input filename 1606 \\ 1607 \\ 1608 ); 1609 try testParseError(&.{"-v_not-an-option"}, 1610 \\<cli>: error: invalid option: -_not-an-option 1611 \\ ... -v_not-an-option 1612 \\ ~ ^~~~~~~~~~~~~~ 1613 \\<cli>: error: missing input filename 1614 \\ 1615 \\ 1616 ); 1617 try testParseError(&.{"--v_not-an-option"}, 1618 \\<cli>: error: invalid option: --_not-an-option 1619 \\ ... --v_not-an-option 1620 \\ ~~ ^~~~~~~~~~~~~~ 1621 \\<cli>: error: missing input filename 1622 \\ 1623 \\ 1624 ); 1625 } 1626 1627 test "inferred absolute filepaths" { 1628 { 1629 var options = try testParseWarning(&.{ "/fo", "foo.res", "/home/absolute/path.rc" }, 1630 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated 1631 \\ ... /home/absolute/path.rc 1632 \\ ^~~~~~~~~~~~~~~~~~~~~~ 1633 \\ 1634 ); 1635 defer options.deinit(); 1636 } 1637 { 1638 var options = try testParseWarning(&.{ "/home/absolute/path.rc", "foo.res" }, 1639 \\<cli>: note: this argument was inferred to be a filepath, so argument parsing was terminated 1640 \\ ... /home/absolute/path.rc ... 1641 \\ ^~~~~~~~~~~~~~~~~~~~~~ 1642 \\ 1643 ); 1644 defer options.deinit(); 1645 } 1646 { 1647 // Only the last two arguments are checked, so the /h is parsed as an option 1648 var options = try testParse(&.{ "/home/absolute/path.rc", "foo.rc", "foo.res" }); 1649 defer options.deinit(); 1650 1651 try std.testing.expect(options.print_help_and_exit); 1652 } 1653 { 1654 var options = try testParse(&.{ "/xvFO/some/absolute/path.res", "foo.rc" }); 1655 defer options.deinit(); 1656 1657 try std.testing.expectEqual(true, options.verbose); 1658 try std.testing.expectEqual(true, options.ignore_include_env_var); 1659 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 1660 try std.testing.expectEqualStrings("/some/absolute/path.res", options.output_source.filename); 1661 } 1662 } 1663 1664 test "parse errors: /ln" { 1665 try testParseError(&.{ "/ln", "invalid", "foo.rc" }, 1666 \\<cli>: error: invalid language tag: invalid 1667 \\ ... /ln invalid ... 1668 \\ ~~~~^~~~~~~ 1669 \\ 1670 ); 1671 try testParseError(&.{ "/lninvalid", "foo.rc" }, 1672 \\<cli>: error: invalid language tag: invalid 1673 \\ ... /lninvalid ... 1674 \\ ~~~^~~~~~~ 1675 \\ 1676 ); 1677 } 1678 1679 test "parse: options" { 1680 { 1681 var options = try testParse(&.{ "/v", "foo.rc" }); 1682 defer options.deinit(); 1683 1684 try std.testing.expectEqual(true, options.verbose); 1685 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 1686 try std.testing.expectEqualStrings("foo.res", options.output_source.filename); 1687 } 1688 { 1689 var options = try testParse(&.{ "/vx", "foo.rc" }); 1690 defer options.deinit(); 1691 1692 try std.testing.expectEqual(true, options.verbose); 1693 try std.testing.expectEqual(true, options.ignore_include_env_var); 1694 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 1695 try std.testing.expectEqualStrings("foo.res", options.output_source.filename); 1696 } 1697 { 1698 var options = try testParse(&.{ "/xv", "foo.rc" }); 1699 defer options.deinit(); 1700 1701 try std.testing.expectEqual(true, options.verbose); 1702 try std.testing.expectEqual(true, options.ignore_include_env_var); 1703 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 1704 try std.testing.expectEqualStrings("foo.res", options.output_source.filename); 1705 } 1706 { 1707 var options = try testParse(&.{ "/xvFObar.res", "foo.rc" }); 1708 defer options.deinit(); 1709 1710 try std.testing.expectEqual(true, options.verbose); 1711 try std.testing.expectEqual(true, options.ignore_include_env_var); 1712 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 1713 try std.testing.expectEqualStrings("bar.res", options.output_source.filename); 1714 } 1715 } 1716 1717 test "parse: define and undefine" { 1718 { 1719 var options = try testParse(&.{ "/dfoo", "foo.rc" }); 1720 defer options.deinit(); 1721 1722 const action = options.symbols.get("foo").?; 1723 try std.testing.expectEqualStrings("1", action.define); 1724 } 1725 { 1726 var options = try testParse(&.{ "/dfoo=bar", "/dfoo=baz", "foo.rc" }); 1727 defer options.deinit(); 1728 1729 const action = options.symbols.get("foo").?; 1730 try std.testing.expectEqualStrings("baz", action.define); 1731 } 1732 { 1733 var options = try testParse(&.{ "/ufoo", "foo.rc" }); 1734 defer options.deinit(); 1735 1736 const action = options.symbols.get("foo").?; 1737 try std.testing.expectEqual(Options.SymbolAction.undefine, action); 1738 } 1739 { 1740 // Once undefined, future defines are ignored 1741 var options = try testParse(&.{ "/ufoo", "/dfoo", "foo.rc" }); 1742 defer options.deinit(); 1743 1744 const action = options.symbols.get("foo").?; 1745 try std.testing.expectEqual(Options.SymbolAction.undefine, action); 1746 } 1747 { 1748 // Undefined always takes precedence 1749 var options = try testParse(&.{ "/dfoo", "/ufoo", "/dfoo", "foo.rc" }); 1750 defer options.deinit(); 1751 1752 const action = options.symbols.get("foo").?; 1753 try std.testing.expectEqual(Options.SymbolAction.undefine, action); 1754 } 1755 { 1756 // Warn + ignore invalid identifiers 1757 var options = try testParseWarning( 1758 &.{ "/dfoo bar", "/u", "0leadingdigit", "foo.rc" }, 1759 \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined 1760 \\ ... /dfoo bar ... 1761 \\ ~~^~~~~~~ 1762 \\<cli>: warning: symbol "0leadingdigit" is not a valid identifier and therefore cannot be undefined 1763 \\ ... /u 0leadingdigit ... 1764 \\ ~~~^~~~~~~~~~~~~ 1765 \\ 1766 , 1767 ); 1768 defer options.deinit(); 1769 1770 try std.testing.expectEqual(@as(usize, 0), options.symbols.count()); 1771 } 1772 } 1773 1774 test "parse: /sl" { 1775 try testParseError(&.{ "/sl", "0", "foo.rc" }, 1776 \\<cli>: error: percent out of range: 0 (parsed from '0') 1777 \\ ... /sl 0 ... 1778 \\ ~~~~^ 1779 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive) 1780 \\ 1781 \\ 1782 ); 1783 try testParseError(&.{ "/sl", "abcd", "foo.rc" }, 1784 \\<cli>: error: invalid percent format 'abcd' 1785 \\ ... /sl abcd ... 1786 \\ ~~~~^~~~ 1787 \\<cli>: note: string length percent must be an integer between 1 and 100 (inclusive) 1788 \\ 1789 \\ 1790 ); 1791 { 1792 var options = try testParse(&.{"foo.rc"}); 1793 defer options.deinit(); 1794 1795 try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints); 1796 } 1797 { 1798 var options = try testParse(&.{ "/sl100", "foo.rc" }); 1799 defer options.deinit(); 1800 1801 try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints); 1802 } 1803 { 1804 var options = try testParse(&.{ "-SL33", "foo.rc" }); 1805 defer options.deinit(); 1806 1807 try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints); 1808 } 1809 { 1810 var options = try testParse(&.{ "/sl15", "foo.rc" }); 1811 defer options.deinit(); 1812 1813 try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints); 1814 } 1815 } 1816 1817 test "parse: unsupported MUI-related options" { 1818 try testParseError(&.{ "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" }, 1819 \\<cli>: error: the /q option is unsupported 1820 \\ ... /q ... 1821 \\ ~^ 1822 \\<cli>: error: the /g1 option is unsupported 1823 \\ ... /g1 ... 1824 \\ ~^~ 1825 \\<cli>: error: the -G2 option is unsupported 1826 \\ ... -G2 ... 1827 \\ ~^~ 1828 \\<cli>: error: the /fm option is unsupported 1829 \\ ... /fm ... 1830 \\ ~^~ 1831 \\<cli>: error: the /g option is unsupported 1832 \\ ... /g ... 1833 \\ ~^ 1834 \\ 1835 ); 1836 } 1837 1838 test "parse: unsupported LCX/LCE-related options" { 1839 try testParseError(&.{ "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" }, 1840 \\<cli>: error: the /t option is unsupported 1841 \\ ... /t ... 1842 \\ ~^ 1843 \\<cli>: error: missing value for /tp: option 1844 \\ ... /tp: ... 1845 \\ ~~~~^ 1846 \\<cli>: error: the /tp: option is unsupported 1847 \\ ... /tp: ... 1848 \\ ~^~~ 1849 \\<cli>: error: the /tp: option is unsupported 1850 \\ ... /tp:blah ... 1851 \\ ~^~~~~~~ 1852 \\<cli>: error: the /tm option is unsupported 1853 \\ ... /tm ... 1854 \\ ~^~ 1855 \\<cli>: error: the /tc option is unsupported 1856 \\ ... /tc ... 1857 \\ ~^~ 1858 \\<cli>: error: the /tw option is unsupported 1859 \\ ... /tw ... 1860 \\ ~^~ 1861 \\<cli>: error: the -TE option is unsupported 1862 \\ ... -TEti ... 1863 \\ ~^~ 1864 \\<cli>: error: the -ti option is unsupported 1865 \\ ... -TEti ... 1866 \\ ~ ^~ 1867 \\<cli>: error: the /ta option is unsupported 1868 \\ ... /ta ... 1869 \\ ~^~ 1870 \\<cli>: error: the /tn option is unsupported 1871 \\ ... /tn ... 1872 \\ ~^~ 1873 \\ 1874 ); 1875 } 1876 1877 test "parse: output filename specified twice" { 1878 try testParseError(&.{ "/fo", "foo.res", "foo.rc", "foo.res" }, 1879 \\<cli>: error: output filename already specified 1880 \\ ... foo.res 1881 \\ ^~~~~~~ 1882 \\<cli>: note: output filename previously specified here 1883 \\ ... /fo foo.res ... 1884 \\ ~~~~^~~~~~~ 1885 \\ 1886 ); 1887 } 1888 1889 test "parse: input and output formats" { 1890 { 1891 try testParseError(&.{ "/:output-format", "rcpp", "foo.res" }, 1892 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp' 1893 \\ 1894 \\<cli>: note: the input format was inferred from the input filename 1895 \\ ... foo.res 1896 \\ ^~~~~~~ 1897 \\<cli>: note: the output format was specified here 1898 \\ ... /:output-format rcpp ... 1899 \\ ~~~~~~~~~~~~~~~~^~~~ 1900 \\ 1901 ); 1902 } 1903 { 1904 try testParseError(&.{ "foo.res", "foo.rcpp" }, 1905 \\<cli>: error: input format 'res' cannot be converted to output format 'rcpp' 1906 \\ 1907 \\<cli>: note: the input format was inferred from the input filename 1908 \\ ... foo.res ... 1909 \\ ^~~~~~~ 1910 \\<cli>: note: the output format was inferred from the output filename 1911 \\ ... foo.rcpp 1912 \\ ^~~~~~~~ 1913 \\ 1914 ); 1915 } 1916 { 1917 try testParseError(&.{ "/:input-format", "res", "foo" }, 1918 \\<cli>: error: input format 'res' cannot be converted to output format 'res' 1919 \\ 1920 \\<cli>: note: the input format was specified here 1921 \\ ... /:input-format res ... 1922 \\ ~~~~~~~~~~~~~~~^~~ 1923 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used 1924 \\ ... foo 1925 \\ ^~~ 1926 \\ 1927 ); 1928 } 1929 { 1930 try testParseError(&.{ "/p", "/:input-format", "res", "foo" }, 1931 \\<cli>: error: input format 'res' cannot be converted to output format 'res' 1932 \\ 1933 \\<cli>: error: the /p option cannot be used with output format 'res' 1934 \\ ... /p ... 1935 \\ ^~ 1936 \\<cli>: note: the input format was specified here 1937 \\ ... /:input-format res ... 1938 \\ ~~~~~~~~~~~~~~~^~~ 1939 \\<cli>: note: the output format was unable to be inferred from the input filename, so the default was used 1940 \\ ... foo 1941 \\ ^~~ 1942 \\ 1943 ); 1944 } 1945 { 1946 try testParseError(&.{ "/:output-format", "coff", "/p", "foo.rc" }, 1947 \\<cli>: error: the /p option cannot be used with output format 'coff' 1948 \\ ... /p ... 1949 \\ ^~ 1950 \\<cli>: note: the output format was specified here 1951 \\ ... /:output-format coff ... 1952 \\ ~~~~~~~~~~~~~~~~^~~~ 1953 \\ 1954 ); 1955 } 1956 { 1957 try testParseError(&.{ "/fo", "foo.res", "/p", "foo.rc" }, 1958 \\<cli>: error: the /p option cannot be used with output format 'res' 1959 \\ ... /p ... 1960 \\ ^~ 1961 \\<cli>: note: the output format was inferred from the output filename 1962 \\ ... /fo foo.res ... 1963 \\ ~~~~^~~~~~~ 1964 \\ 1965 ); 1966 } 1967 { 1968 try testParseError(&.{ "/p", "foo.rc", "foo.o" }, 1969 \\<cli>: error: the /p option cannot be used with output format 'coff' 1970 \\ ... /p ... 1971 \\ ^~ 1972 \\<cli>: note: the output format was inferred from the output filename 1973 \\ ... foo.o 1974 \\ ^~~~~ 1975 \\ 1976 ); 1977 } 1978 { 1979 var options = try testParse(&.{"foo.rc"}); 1980 defer options.deinit(); 1981 1982 try std.testing.expectEqual(.rc, options.input_format); 1983 try std.testing.expectEqual(.res, options.output_format); 1984 } 1985 { 1986 var options = try testParse(&.{"foo.rcpp"}); 1987 defer options.deinit(); 1988 1989 try std.testing.expectEqual(.no, options.preprocess); 1990 try std.testing.expectEqual(.rcpp, options.input_format); 1991 try std.testing.expectEqual(.res, options.output_format); 1992 } 1993 { 1994 var options = try testParse(&.{ "foo.rc", "foo.rcpp" }); 1995 defer options.deinit(); 1996 1997 try std.testing.expectEqual(.only, options.preprocess); 1998 try std.testing.expectEqual(.rc, options.input_format); 1999 try std.testing.expectEqual(.rcpp, options.output_format); 2000 } 2001 { 2002 var options = try testParse(&.{ "foo.rc", "foo.obj" }); 2003 defer options.deinit(); 2004 2005 try std.testing.expectEqual(.rc, options.input_format); 2006 try std.testing.expectEqual(.coff, options.output_format); 2007 } 2008 { 2009 var options = try testParse(&.{ "/fo", "foo.o", "foo.rc" }); 2010 defer options.deinit(); 2011 2012 try std.testing.expectEqual(.rc, options.input_format); 2013 try std.testing.expectEqual(.coff, options.output_format); 2014 } 2015 { 2016 var options = try testParse(&.{"foo.res"}); 2017 defer options.deinit(); 2018 2019 try std.testing.expectEqual(.res, options.input_format); 2020 try std.testing.expectEqual(.coff, options.output_format); 2021 } 2022 { 2023 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.rc", "foo.rcpp" }, 2024 \\<cli>: warning: the /:depfile option was ignored because the output format is 'rcpp' 2025 \\ ... /:depfile foo.json ... 2026 \\ ~~~~~~~~~~^~~~~~~~ 2027 \\<cli>: note: the output format was inferred from the output filename 2028 \\ ... foo.rcpp 2029 \\ ^~~~~~~~ 2030 \\ 2031 ); 2032 defer options.deinit(); 2033 2034 try std.testing.expectEqual(.rc, options.input_format); 2035 try std.testing.expectEqual(.rcpp, options.output_format); 2036 } 2037 { 2038 var options = try testParseWarning(&.{ "/:depfile", "foo.json", "foo.res", "foo.o" }, 2039 \\<cli>: warning: the /:depfile option was ignored because the input format is 'res' 2040 \\ ... /:depfile foo.json ... 2041 \\ ~~~~~~~~~~^~~~~~~~ 2042 \\<cli>: note: the input format was inferred from the input filename 2043 \\ ... foo.res ... 2044 \\ ^~~~~~~ 2045 \\ 2046 ); 2047 defer options.deinit(); 2048 2049 try std.testing.expectEqual(.res, options.input_format); 2050 try std.testing.expectEqual(.coff, options.output_format); 2051 } 2052 } 2053 2054 test "maybeAppendRC" { 2055 var tmp = std.testing.tmpDir(.{}); 2056 defer tmp.cleanup(); 2057 2058 var options = try testParse(&.{"foo"}); 2059 defer options.deinit(); 2060 try std.testing.expectEqualStrings("foo", options.input_source.filename); 2061 2062 // Create the file so that it's found. In this scenario, .rc should not get 2063 // appended. 2064 var file = try tmp.dir.createFile("foo", .{}); 2065 file.close(); 2066 try options.maybeAppendRC(tmp.dir); 2067 try std.testing.expectEqualStrings("foo", options.input_source.filename); 2068 2069 // Now delete the file and try again. But this time change the input format 2070 // to non-rc. 2071 try tmp.dir.deleteFile("foo"); 2072 options.input_format = .res; 2073 try options.maybeAppendRC(tmp.dir); 2074 try std.testing.expectEqualStrings("foo", options.input_source.filename); 2075 2076 // Finally, reset the input format to rc. Since the verbatim name is no longer found 2077 // and the input filename does not have an extension, .rc should get appended. 2078 options.input_format = .rc; 2079 try options.maybeAppendRC(tmp.dir); 2080 try std.testing.expectEqualStrings("foo.rc", options.input_source.filename); 2081 }