const std = @import("std"); const code_pages = @import("code_pages.zig"); const SupportedCodePage = code_pages.SupportedCodePage; const lang = @import("lang.zig"); const res = @import("res.zig"); const Allocator = std.mem.Allocator; const lex = @import("lex.zig"); const cvtres = @import("cvtres.zig"); /// This is what /SL 100 will set the maximum string literal length to pub const max_string_literal_length_100_percent = 8192; pub const usage_string_after_command_name = \\ [options] [--] [] \\ \\The sequence -- can be used to signify when to stop parsing options. \\This is necessary when the input path begins with a forward slash. \\ \\Supported option prefixes are /, -, and --, so e.g. /h, -h, and --h all work. \\Drop-in compatible with the Microsoft Resource Compiler. \\ \\Supported Win32 RC Options: \\ /?, /h Print this help and exit. \\ /v Verbose (print progress messages). \\ /d [=] Define a symbol (during preprocessing). \\ /u Undefine a symbol (during preprocessing). \\ /fo Specify output file path. \\ /l Set default language using hexadecimal id (ex: 409). \\ /ln Set default language using language name (ex: en-us). \\ /i Add an include path. \\ /x Ignore INCLUDE environment variable. \\ /c Set default code page (ex: 65001). \\ /w Warn on invalid code page in .rc (instead of error). \\ /y Suppress warnings for duplicate control IDs. \\ /n Null-terminate all strings in string tables. \\ /sl Specify string literal length limit in percentage (1-100) \\ where 100 corresponds to a limit of 8192. If the /sl \\ option is not specified, the default limit is 4097. \\ /p Only run the preprocessor and output a .rcpp file. \\ \\No-op Win32 RC Options: \\ /nologo, /a, /r Options that are recognized but do nothing. \\ \\Unsupported Win32 RC Options: \\ /fm, /q, /g, /gn, /g1, /g2 Unsupported MUI-related options. \\ /?c, /hc, /t, /tp:, Unsupported LCX/LCE-related options. \\ /tn, /tm, /tc, /tw, /te, \\ /ti, /ta \\ /z Unsupported font-substitution-related option. \\ /s Unsupported HWB-related option. \\ \\Custom Options (resinator-specific): \\ /:no-preprocess Do not run the preprocessor. \\ /:debug Output the preprocessed .rc file and the parsed AST. \\ /:auto-includes Set the automatic include path detection behavior. \\ any (default) Use MSVC if available, fall back to MinGW \\ msvc Use MSVC include paths (must be present on the system) \\ gnu Use MinGW include paths \\ none Do not use any autodetected include paths \\ /:depfile Output a file containing a list of all the files that \\ the .rc includes or otherwise depends on. \\ /:depfile-fmt Output format of the depfile, if /:depfile is set. \\ json (default) A top-level JSON array of paths \\ /:input-format If not specified, the input format is inferred. \\ rc (default if input format cannot be inferred) \\ res Compiled .rc file, implies /:output-format coff \\ rcpp Preprocessed .rc file, implies /:no-preprocess \\ /:output-format If not specified, the output format is inferred. \\ res (default if output format cannot be inferred) \\ coff COFF object file (extension: .obj or .o) \\ rcpp Preprocessed .rc file, implies /p \\ /:target Set the target machine for COFF object files. \\ Can be specified either as PE/COFF machine constant \\ name (X64, ARM64, etc) or Zig/LLVM CPU name (x86_64, \\ aarch64, etc). The default is X64 (aka x86_64). \\ Also accepts a full Zig/LLVM triple, but everything \\ except the architecture is ignored. \\ \\Note: For compatibility reasons, all custom options start with : \\ ; pub fn writeUsage(writer: anytype, command_name: []const u8) !void { try writer.writeAll("Usage: "); try writer.writeAll(command_name); try writer.writeAll(usage_string_after_command_name); } pub const Diagnostics = struct { errors: std.ArrayListUnmanaged(ErrorDetails) = .empty, allocator: Allocator, pub const ErrorDetails = struct { arg_index: usize, arg_span: ArgSpan = .{}, msg: std.ArrayListUnmanaged(u8) = .empty, type: Type = .err, print_args: bool = true, pub const Type = enum { err, warning, note }; pub const ArgSpan = struct { point_at_next_arg: bool = false, name_offset: usize = 0, prefix_len: usize = 0, value_offset: usize = 0, name_len: usize = 0, }; }; pub fn init(allocator: Allocator) Diagnostics { return .{ .allocator = allocator, }; } pub fn deinit(self: *Diagnostics) void { for (self.errors.items) |*details| { details.msg.deinit(self.allocator); } self.errors.deinit(self.allocator); } pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void { try self.errors.append(self.allocator, error_details); } pub fn renderToStdErr(self: *Diagnostics, args: []const []const u8, config: std.io.tty.Config) void { const stderr = std.debug.lockStderrWriter(&.{}); defer std.debug.unlockStderrWriter(); self.renderToWriter(args, stderr, config) catch return; } pub fn renderToWriter(self: *Diagnostics, args: []const []const u8, writer: *std.io.Writer, config: std.io.tty.Config) !void { for (self.errors.items) |err_details| { try renderErrorMessage(writer, config, err_details, args); } } pub fn hasError(self: *const Diagnostics) bool { for (self.errors.items) |err| { if (err.type == .err) return true; } return false; } }; pub const Options = struct { allocator: Allocator, input_source: IoSource = .{ .filename = &[_]u8{} }, output_source: IoSource = .{ .filename = &[_]u8{} }, extra_include_paths: std.ArrayListUnmanaged([]const u8) = .empty, ignore_include_env_var: bool = false, preprocess: Preprocess = .yes, default_language_id: ?u16 = null, default_code_page: ?SupportedCodePage = null, verbose: bool = false, symbols: std.StringArrayHashMapUnmanaged(SymbolValue) = .empty, null_terminate_string_table_strings: bool = false, max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints, silent_duplicate_control_ids: bool = false, warn_instead_of_error_on_invalid_code_page: bool = false, debug: bool = false, print_help_and_exit: bool = false, auto_includes: AutoIncludes = .any, depfile_path: ?[]const u8 = null, depfile_fmt: DepfileFormat = .json, input_format: InputFormat = .rc, output_format: OutputFormat = .res, coff_options: cvtres.CoffOptions = .{}, pub const IoSource = union(enum) { stdio: std.fs.File, filename: []const u8, }; pub const AutoIncludes = enum { any, msvc, gnu, none }; pub const DepfileFormat = enum { json }; pub const InputFormat = enum { rc, res, rcpp }; pub const OutputFormat = enum { res, coff, rcpp, pub fn extension(format: OutputFormat) []const u8 { return switch (format) { .rcpp => ".rcpp", .coff => ".obj", .res => ".res", }; } }; pub const Preprocess = enum { no, yes, only }; pub const SymbolAction = enum { define, undefine }; pub const SymbolValue = union(SymbolAction) { define: []const u8, undefine: void, pub fn deinit(self: SymbolValue, allocator: Allocator) void { switch (self) { .define => |value| allocator.free(value), .undefine => {}, } } }; /// Does not check that identifier contains only valid characters pub fn define(self: *Options, identifier: []const u8, value: []const u8) !void { if (self.symbols.getPtr(identifier)) |val_ptr| { // If the symbol is undefined, then that always takes precedence so // we shouldn't change anything. if (val_ptr.* == .undefine) return; // Otherwise, the new value takes precedence. const duped_value = try self.allocator.dupe(u8, value); errdefer self.allocator.free(duped_value); val_ptr.deinit(self.allocator); val_ptr.* = .{ .define = duped_value }; return; } const duped_key = try self.allocator.dupe(u8, identifier); errdefer self.allocator.free(duped_key); const duped_value = try self.allocator.dupe(u8, value); errdefer self.allocator.free(duped_value); try self.symbols.put(self.allocator, duped_key, .{ .define = duped_value }); } /// Does not check that identifier contains only valid characters pub fn undefine(self: *Options, identifier: []const u8) !void { if (self.symbols.getPtr(identifier)) |action| { action.deinit(self.allocator); action.* = .{ .undefine = {} }; return; } const duped_key = try self.allocator.dupe(u8, identifier); errdefer self.allocator.free(duped_key); try self.symbols.put(self.allocator, duped_key, .{ .undefine = {} }); } /// If the current input filename: /// - does not have an extension, and /// - does not exist in the cwd, and /// - the input format is .rc /// then this function will append `.rc` to the input filename /// /// Note: This behavior is different from the Win32 compiler. /// It always appends .RC if the filename does not have /// a `.` in it and it does not even try the verbatim name /// in that scenario. /// /// The approach taken here is meant to give us a 'best of both /// worlds' situation where we'll be compatible with most use-cases /// of the .rc extension being omitted from the CLI args, but still /// work fine if the file itself does not have an extension. pub fn maybeAppendRC(options: *Options, cwd: std.fs.Dir) !void { switch (options.input_source) { .stdio => return, .filename => {}, } if (options.input_format == .rc and std.fs.path.extension(options.input_source.filename).len == 0) { cwd.access(options.input_source.filename, .{}) catch |err| switch (err) { error.FileNotFound => { var filename_bytes = try options.allocator.alloc(u8, options.input_source.filename.len + 3); @memcpy(filename_bytes[0..options.input_source.filename.len], options.input_source.filename); @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc"); options.allocator.free(options.input_source.filename); options.input_source = .{ .filename = filename_bytes }; }, else => {}, }; } } pub fn deinit(self: *Options) void { for (self.extra_include_paths.items) |extra_include_path| { self.allocator.free(extra_include_path); } self.extra_include_paths.deinit(self.allocator); switch (self.input_source) { .stdio => {}, .filename => |filename| self.allocator.free(filename), } switch (self.output_source) { .stdio => {}, .filename => |filename| self.allocator.free(filename), } var symbol_it = self.symbols.iterator(); while (symbol_it.next()) |entry| { self.allocator.free(entry.key_ptr.*); entry.value_ptr.deinit(self.allocator); } self.symbols.deinit(self.allocator); if (self.depfile_path) |depfile_path| { self.allocator.free(depfile_path); } if (self.coff_options.define_external_symbol) |symbol_name| { self.allocator.free(symbol_name); } } pub fn dumpVerbose(self: *const Options, writer: anytype) !void { const input_source_name = switch (self.input_source) { .stdio => "", .filename => |filename| filename, }; const output_source_name = switch (self.output_source) { .stdio => "", .filename => |filename| filename, }; try writer.print("Input filename: {s} (format={s})\n", .{ input_source_name, @tagName(self.input_format) }); try writer.print("Output filename: {s} (format={s})\n", .{ output_source_name, @tagName(self.output_format) }); if (self.output_format == .coff) { try writer.print(" Target machine type for COFF: {s}\n", .{@tagName(self.coff_options.target)}); } if (self.extra_include_paths.items.len > 0) { try writer.writeAll(" Extra include paths:\n"); for (self.extra_include_paths.items) |extra_include_path| { try writer.print(" \"{s}\"\n", .{extra_include_path}); } } if (self.ignore_include_env_var) { try writer.writeAll(" The INCLUDE environment variable will be ignored\n"); } if (self.preprocess == .no) { try writer.writeAll(" The preprocessor will not be invoked\n"); } else if (self.preprocess == .only) { try writer.writeAll(" Only the preprocessor will be invoked\n"); } if (self.symbols.count() > 0) { try writer.writeAll(" Symbols:\n"); var it = self.symbols.iterator(); while (it.next()) |symbol| { try writer.print(" {s} {s}", .{ switch (symbol.value_ptr.*) { .define => "#define", .undefine => "#undef", }, symbol.key_ptr.* }); if (symbol.value_ptr.* == .define) { try writer.print(" {s}", .{symbol.value_ptr.define}); } try writer.writeAll("\n"); } } if (self.null_terminate_string_table_strings) { try writer.writeAll(" Strings in string tables will be null-terminated\n"); } if (self.max_string_literal_codepoints != lex.default_max_string_literal_codepoints) { try writer.print(" Max string literal length: {}\n", .{self.max_string_literal_codepoints}); } if (self.silent_duplicate_control_ids) { try writer.writeAll(" Duplicate control IDs will not emit warnings\n"); } if (self.silent_duplicate_control_ids) { try writer.writeAll(" Invalid code page in .rc will produce a warning (instead of an error)\n"); } const language_id = self.default_language_id orelse res.Language.default; const language_name = language_name: { if (std.enums.fromInt(lang.LanguageId, language_id)) |lang_enum_val| { break :language_name @tagName(lang_enum_val); } if (language_id == lang.LOCALE_CUSTOM_UNSPECIFIED) { break :language_name "LOCALE_CUSTOM_UNSPECIFIED"; } break :language_name ""; }; try writer.print("Default language: {s} (id=0x{x})\n", .{ language_name, language_id }); const code_page = self.default_code_page orelse .windows1252; try writer.print("Default codepage: {s} (id={})\n", .{ @tagName(code_page), @intFromEnum(code_page) }); } }; pub const Arg = struct { prefix: enum { long, short, slash }, name_offset: usize, full: []const u8, pub fn fromString(str: []const u8) ?@This() { if (std.mem.startsWith(u8, str, "--")) { return .{ .prefix = .long, .name_offset = 2, .full = str }; } else if (std.mem.startsWith(u8, str, "-")) { return .{ .prefix = .short, .name_offset = 1, .full = str }; } else if (std.mem.startsWith(u8, str, "/")) { return .{ .prefix = .slash, .name_offset = 1, .full = str }; } return null; } pub fn prefixSlice(self: Arg) []const u8 { return self.full[0..(if (self.prefix == .long) 2 else 1)]; } pub fn name(self: Arg) []const u8 { return self.full[self.name_offset..]; } pub fn optionWithoutPrefix(self: Arg, option_len: usize) []const u8 { if (option_len == 0) return self.name(); return self.name()[0..option_len]; } pub fn missingSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan { return .{ .point_at_next_arg = true, .value_offset = 0, .name_offset = self.name_offset, .prefix_len = self.prefixSlice().len, }; } pub fn optionAndAfterSpan(self: Arg) Diagnostics.ErrorDetails.ArgSpan { return self.optionSpan(0); } pub fn optionSpan(self: Arg, option_len: usize) Diagnostics.ErrorDetails.ArgSpan { return .{ .name_offset = self.name_offset, .prefix_len = self.prefixSlice().len, .name_len = option_len, }; } pub fn looksLikeFilepath(self: Arg) bool { const meets_min_requirements = self.prefix == .slash and isSupportedInputExtension(std.fs.path.extension(self.full)); if (!meets_min_requirements) return false; const could_be_fo_option = could_be_fo_option: { var window_it = std.mem.window(u8, self.full[1..], 2, 1); while (window_it.next()) |window| { if (std.ascii.eqlIgnoreCase(window, "fo")) break :could_be_fo_option true; // If we see '/' before "fo", then it's not possible for this to be a valid // `/fo` option. if (window[0] == '/') break; } break :could_be_fo_option false; }; if (!could_be_fo_option) return true; // It's still possible for a file path to look like a /fo option but not actually // be one, e.g. `/foo/bar.rc`. As a last ditch effort to reduce false negatives, // check if the file path exists and, if so, then we ignore the 'could be /fo option'-ness std.fs.accessAbsolute(self.full, .{}) catch return false; return true; } pub const Value = struct { slice: []const u8, /// Amount to increment the arg index to skip over both the option and the value arg(s) /// e.g. 1 if /